Introduction
Bad Juju is an LSP-powered, editor-agnostic frontend for the Jujutsu VCS. Instead of inventing a custom UI, Bad Juju lets you drive Jujutsu from any editor that speaks the Language Server Protocol — which today means most of them.
The core idea: everything is a text file. Want to see the status of
your working copy? Bad Juju writes it to .jj/badjuju/status.jujutsu and
your editor opens it. Want to edit a commit message? You open a buffer
and edit it. Want to squash a hunk between two commits? The squash window
is, again, just a buffer you edit and save.
This means you can:
- Use your editor's keybindings, motions, and search — because the views are real text, you can grep them, copy from them, jump around with the same shortcuts you already know.
- Plug Bad Juju into any LSP-capable editor. Neovim, VS Code, Helix, and Emacs all have first-party integrations in this repo; any other editor that supports LSP code actions can drive Bad Juju too.
- Avoid context-switching. No separate Magit-style window manager,
no
lazygitoverlay, no IDE tool window with its own keyboard conventions. Bad Juju lives inside the editor you're already in.
If you've used Magit, Fugitive, or Lazygit, the workflows here will feel familiar — single-key actions on a status buffer, log views you can navigate, hunk-level squashing — just translated to Jujutsu's mental model and rendered as plain text inside your editor.
What you'll find in this guide
- Getting Started walks through installing the server and pointing your editor at it.
- Usage covers the everyday workflows — opening the status window, creating new revisions, viewing diffs, and rewriting history.
- Clients collects the editor-specific notes for each supported frontend, including the per-buffer keymaps.
- Reference is the detailed tour of each
buffer Bad Juju produces, for when you want to know exactly what the
JJ:lines mean or which commands a buffer responds to.
Status
Bad Juju is under active development. Expect rough edges — but also expect that the foundations (status, log, diff, describe, squash, hunk editing) all work today, and that bugs and rough edges are tracked openly in the issue tracker.
Getting Started
Bad Juju has two pieces:
- The server — a Rust LSP binary called
badjuju. Every editor integration talks to this same server. - A client — the editor-side glue that launches the server and forwards commands. Each editor has its own setup.
You'll install both. The server first, then the client for whichever editor you use.
Prerequisites
-
Jujutsu (
jj) on yourPATH. Bad Juju drivesjjunder the hood; without it, nothing works. -
Rust (edition 2024 or later) if you're building from source.
-
pnpm 10+ if you plan to build the VS Code extension or run client tests.
-
A build runner. Bad Juju uses apenwarr's
redo— see the Getting Started guide for installation instructions (on macOS,brew install redo).If you'd rather not install
redo, the repo ships a self-contained./doshell script as a drop-in replacement. Anywhere this guide saysredo <target>you can substitute./do <target>instead.
1. Install the server
There's no published release yet, so install from a checkout:
git clone https://github.com/jennings/badjuju
cd badjuju
redo server/install
This builds the badjuju binary in release mode and installs it to
~/.cargo/bin/badjuju. Make sure ~/.cargo/bin is on your PATH.
Verify the install:
badjuju --version
2. Install a client
Pick the editor you use day-to-day:
- Neovim — plugin-manager
recipes for lazy.nvim, packer, vim-plug, pathogen, Vundle, and
built-in
pack/directories. - VS Code — install the extension from the marketplace or build a local VSIX.
- Emacs — recipes for
use-package,straight.el, and Doom Emacs. - Other editors — Helix is supported via a
languages.tomlsnippet, and any LSP-capable editor can drive Bad Juju through code actions.
3. Open the status window
Once the server is installed and your editor knows how to launch it, open a Jujutsu repository and ask for the status window. Each client has its own entry point — see Basic Usage for the exact commands per editor.
The first time you open the status window in a workspace, Bad Juju
creates a .jj/badjuju/ directory for the buffers it writes. There's
nothing to clean up later — it lives alongside .jj/ and gets ignored
along with the rest of the Jujutsu metadata.
You're ready. Head to Basic Usage for a tour of the everyday workflows.
Usage
This section walks through the workflows you'll use day-to-day.
- Basic Usage — open the status window, create a new revision, edit a description, view a diff.
- Manipulating Commits — abandon, reword, squash, and rewrite history from inside your editor.
For an exhaustive reference of what each buffer contains and the keys it responds to, see the Reference chapter.
Basic Usage
This page covers the everyday actions: looking at your working copy,
starting a new change, editing a commit message, and viewing a diff.
The examples use the default magit keymap that ships with each
client — if you've rebound them, substitute your own keys.
Opening the status window
The status window is your home base. It shows what files you've
touched, the stack of commits leading up to @ (the working copy),
and a one-screen command reference at the bottom.
To open it:
| Client | How |
|---|---|
| Neovim | :JJStatus |
| VS Code | Command Palette → jj: Status |
| Emacs | M-x badjuju-status |
| Helix | hx "$(badjuju status)" from the shell |
You'll see something like:
STATUS:
The working copy has no changes.
Working copy (@) : kpkzwvqm 909679d0 (empty) (no description set)
Parent commit (@-): xorwskru 66bfbfdf feat(neovim): buffer-local keymaps...
STACK: ancestors(reachable(@, mutable()), 2)
@ kpkzwvqm 909679d0 1min stephen@example.com
│ (empty) (no description set)
○ xorwskru 66bfbfdf 2min stephen@example.com
│ feat(neovim): buffer-local keymaps on status.jj and log.jj
◆ spxlzwpr 18d66a82 20min stephen@example.com main
│ fix(ci): set DESTDIR when installing redo
~
COMMAND REFERENCE:
n new change
L open log
d describe
...
Once the buffer is open, single-key bindings (or M-x / Command
Palette equivalents) drive every other action. Press ? at any time
to see the active key map for the current buffer.
Creating a new revision
You finished a commit and want to start working on the next thing?
That's jj new, but you don't need to leave the editor:
| Client | Key |
|---|---|
| Neovim, VS Code (magit) | n |
| Emacs | n |
| Helix | code action New child of <rev> |
n runs jj new against the working copy. If you'd rather branch off
a different commit, place your cursor on that commit's header line
first — in clients with hotkeys, the cursor position is what
determines the target.
Editing an existing revision
There are two flavors of "edit" in Jujutsu:
-
Move
@to a commit so you can keep editing its working tree. Presse(or run:JJEdit/M-x badjuju-edit) with the cursor on the commit you want to land on. Bad Juju runsjj edit <rev>and refreshes the status window. -
Change a commit's message without touching its tree. That's
describe. Pressdwith the cursor on the commit; Bad Juju opens adescribe.jujutsubuffer pre-populated with the current message. Edit it, save, and Bad Juju callsjj describe -mfor you.
In the describe buffer:
| Client | Finalize | Abort |
|---|---|---|
| Neovim | <C-c><C-c> | <C-c><C-k> |
| VS Code | Ctrl+Enter | Escape Escape |
| Emacs | C-c C-c | C-c C-k |
| Helix | :write then :quit | :quit! |
Lines starting with JJ: are comments — they're stripped before the
message is saved, so you can leave reminders in them.
Viewing a diff
There are two diff modes:
- Change diff — pinned to a change id (the stable identifier that follows a commit as you amend it). The diff buffer refreshes automatically when the change is amended. This is what you want most of the time.
- Commit diff — pinned to an immutable commit id. The view never changes; useful for "what did this exact snapshot look like?"
To open a change diff, place the cursor on a commit and press D. In
Emacs, that's D from the status or log buffer; in VS Code/Neovim
magit, also D. To open a commit diff in VS Code, use
Ctrl+Shift+D.
You can have multiple diff buffers open simultaneously — each one is a
separate file (diff-change-<id>.jujutsu or
diff-commit-<id>.jujutsu), so you can compare two revisions side by
side.
Viewing a single file's history
For the commits that touched one file, place the cursor on a file row
in the status buffer and press l f (Magit/Vim profile) or pick
Log <file> from the Helix code-action menu. Bad Juju opens a
per-file buffer showing jj log -r ..@ -p restricted to that path —
see the log file buffer reference.
Refreshing and closing buffers
Bad Juju auto-refreshes open status, log, and diff buffers when a jj
operation happens — whether you triggered it through Bad Juju or
through jj in a terminal. You should rarely need to refresh
manually, but if you want to:
- Press
R(Neovim, VS Code, Emacs) in the buffer. - Or run
:JJRefresh/M-x badjuju-refresh/ the command-palette refresh action.
To close a buffer: press q. In Helix, use :bd (buffer close).
What's next
You've got the basics. Up next:
- Manipulating Commits covers abandoning, rewording, and squashing — the operations that actually rewrite history.
- The Reference chapter has the full catalog of each buffer and the keys it responds to.
Manipulating Commits
Once you can navigate the status window, the next step is rewriting history. Jujutsu is built around the assumption that commits are mutable — you reword them, split them, squash hunks between them, and move them around. Bad Juju exposes those operations as cursor-driven actions on the status and log buffers.
This page walks through the most common ones.
Abandoning a change
If you decide a commit shouldn't exist at all — say you started a spike, hated it, and want it gone — abandon it. The commit's descendants get rebased onto its parent automatically.
- Open the status or log buffer.
- Place the cursor on the commit you want to delete.
- Press
a(magit profile in Neovim/VS Code/Emacs) or invoke the Abandon commit<rev>code action.
Bad Juju runs jj abandon <rev> and refreshes the buffer. With no
cursor on a commit row, a defaults to the working copy.
Heads up: Jujutsu's
op logmakes this reversible. If you abandoned the wrong thing, pressu(orUin Emacs) to invokejj undo.
Rewording a commit (describe)
If the commit is fine but the message isn't — typo, missing context, wrong issue number — you want to describe it.
- Place the cursor on the commit.
- Press
d.
Bad Juju opens describe.jujutsu populated with the existing message.
Lines beginning with JJ: are comments that get stripped on save.
Save and close (<C-c><C-c>, Ctrl+Enter, or C-c C-c depending on
the editor) to apply.
If you change your mind, abort instead of saving (<C-c><C-k>,
Escape Escape, or C-c C-k).
Squashing a single file into the parent
Suppose you realize a change you made belongs in the previous commit,
not in your current working copy. Squashing a file moves its
changes from @ (the working copy) into the parent commit.
- Open the status buffer.
- Place the cursor on the file in the WORKING COPY CHANGES list.
- Press
s.
The file disappears from @'s changes and lands in the parent. If
the working copy ends up empty, you can keep working on the same
commit or move on with n (new).
Need to pull a file back out of the parent? Press U (or Ctrl+K U
in VS Code if U is shadowed by another keymap) — that's unsquash.
Multiple parents? If the working copy is a merge commit, Bad Juju will pick the parent that already touches the file. If the file isn't in either parent (or both), the client prompts you to pick one.
Squashing changes between revisions
Suppose you decide a change should be moved to a different revision — not just the immediate parent, and maybe only some of the hunks. Bad Juju's commit-to-commit squash workflow handles this.
The flow has three steps: mark a source, mark a destination, then pick the hunks.
1. Mark the source
Place your cursor on the commit whose changes you want to move out
of. Press s (Emacs) or the Squash from this revision code action
(VS Code, Neovim, Helix).
The status/log buffer header updates to confirm the pending source.
2. Mark the destination
Now move the cursor to the commit you want the changes to land in.
Press s again (Emacs) or invoke Squash into this revision.
Bad Juju materializes a squash window at
.jj/badjuju/squash/<from>-<to>.jujutsu. It has two sections:
- REMAINING CHANGES — every hunk in the source that hasn't been selected yet.
- SELECTED CHANGES — the hunks you're moving into the destination.
Initially, everything is in REMAINING.
3. Pick the hunks
In the squash window, navigate to a hunk and toggle it between REMAINING and SELECTED:
| Client | Key |
|---|---|
| Neovim, VS Code | s (toggle hunk/file under cursor) |
| Emacs | s |
| Helix | code action Move hunk to SELECTED / Move hunk to REMAINING |
You can also:
- Select everything with
a(Emacs) or Move all hunks to SELECTED — equivalent to a plainjj squash. - Deselect everything with
A(Emacs) or Move all hunks to REMAINING. - Edit a hunk before squashing. Press
e(Emacs) on a hunk to openhunk-edit.jujutsu. You can tweak the additions and context lines, save, and Bad Juju applies the edited hunk viajj squash --interactive. See the Hunk edit buffer reference for details.
4. Finalize
Close the squash window when you're happy with the SELECTED set. Bad Juju applies the move and refreshes the status/log buffers. If you change your mind partway through, cancel the pending squash via the Cancel pending squash action (or just close the squash window with everything still in REMAINING — nothing gets moved).
Rebasing onto a different destination
To move a commit (and its descendants) onto a new parent, press r
with the cursor on the commit you want to rebase. The client prompts
for a destination revset; on submit, Bad Juju runs jj rebase -r <src> -d <dest>.
Managing bookmarks
Press b to open the bookmark manager. It's a single popup/picker
that handles create, move, delete, track, and forget operations — the
same five things jj bookmark does, just from inside your editor.
Undoing the last operation
Jujutsu records every mutating operation in its op log, which makes mistakes cheap. To undo whatever you just did:
| Client | Key |
|---|---|
| Neovim, VS Code (magit) | u |
| Emacs | U |
| Helix | code action via shell jj undo |
This invokes jj undo and refreshes the status buffer.
Pulling from / pushing to the Git remote
If your repo is colocated with Git, you can fetch and push without leaving the editor:
| Action | Key |
|---|---|
jj git fetch | f |
jj git push | p |
jj git push --force-with-lease | P |
These are best-effort wrappers — they run the corresponding jj
command and surface the output. For anything more nuanced (specific
remotes, branches, or refspecs), drop into the terminal.
Clients
Bad Juju ships first-party client integrations for five editors. They
all talk to the same badjuju LSP server, so the underlying
operations are identical — what differs is the install steps and the
exact key bindings.
- Neovim — Lua plugin with
:JJ*commands and buffer-local keymaps. Supports lazy.nvim, packer, vim-plug, pathogen, Vundle, and built-inpack/directories. - VS Code — extension with Command Palette entries
and configurable keymap profiles (
magit,vim,none). - Emacs —
eglot-powered package modeled on Magit, withtransientpopup menus andM-x badjuju-*commands. - Kakoune — kak-lsp plugin with
:JJ*commands and user-mode keymaps (magitorvimprofile). - Other Editors — Helix support via
languages.tomlplus notes for any LSP-capable editor that can fire code actions.
Neovim
The Neovim client lives in clients/neovim/ in the repo. It uses
Neovim 0.11's built-in LSP API (vim.lsp.enable) with automatic
workspace detection (root_markers = { '.jj' }).
Requirements
- Neovim 0.11+
badjujuserver binary on yourPATH(see Getting Started)
Installation
Pick the recipe that matches your plugin manager. Replace
/absolute/path/to/bad-juju with the path to your local checkout.
lazy.nvim (recommended)
{
dir = '/absolute/path/to/bad-juju/clients/neovim',
name = 'bad-juju',
ft = 'jujutsu',
opts = {},
}
packer.nvim
use {
'/absolute/path/to/bad-juju/clients/neovim',
config = function() require('badjuju').setup({}) end,
}
vim-plug
Plug 'jennings/bad-juju', { 'rtp': 'clients/neovim' }
require('badjuju').setup({})
pathogen
ln -s /absolute/path/to/bad-juju/clients/neovim \
~/.vim/bundle/bad-juju
Vundle
Plugin 'jennings/bad-juju'
set rtp+=~/.vim/bundle/bad-juju/clients/neovim
Neovim built-in packages
ln -s /absolute/path/to/bad-juju/clients/neovim \
~/.local/share/nvim/site/pack/badjuju/start/bad-juju
Manual / no plugin manager
vim.opt.rtp:prepend('/absolute/path/to/bad-juju/clients/neovim')
require('badjuju').setup({})
Configuration
setup() is optional. Only call it if you want to override defaults:
require('badjuju').setup({
-- Path to the jj binary; forwarded to the server.
binaryPath = nil,
-- Default revset for :JJLog when called with no argument.
defaultLogRevset = nil,
-- Hotkey profile: "magit" (default), "vim", or "none".
keymapProfile = nil,
})
Commands
| Command | Description |
|---|---|
:JJStatus | Open .jj/badjuju/status.jujutsu |
:JJLog [revset] | Open the log; defaults to @ |
:JJLogFile [revset] | Open the per-file log for the current buffer's file; defaults to ..@ |
:JJDescribe [revision] | Edit a commit message (defaults to @) |
:JJDiff [revision] | Open a diff (defaults to @) |
:JJNew | Create a new change |
:JJRefresh | Refresh the badjuju buffer at the cursor |
:JJSquash [file] [revision] | Squash a file into its parent |
:JJUnsquash [file] [revision] | Unsquash a file from parent into child |
:JJUndo | Run jj undo and refresh |
:JJAbandon [revision] | Abandon a revision (defaults to @) |
Commands auto-start the LSP for the current workspace if it isn't already running.
Keymaps
Two built-in profiles, plus "none" to disable defaults. Set
keymapProfile in setup() to switch.
magit profile (default)
status.jujutsu
| Key | Action |
|---|---|
R | Refresh |
n | New change |
L | Open log |
l f | Open the per-file log for the file at cursor |
f | Git fetch |
p | Git push |
P | Git push --force-with-lease |
e | Edit commit at cursor (move @) |
b c/m/d/t/f | Bookmark: create / move / delete / track / forget |
r s | Mark rebase source (jj rebase -s) |
r r | Mark rebase source (jj rebase -r) |
r b | Mark rebase source (jj rebase -b) |
r o | Rebase onto commit at cursor (-d) |
r A | Insert after commit at cursor (--insert-after) |
r B | Insert before commit at cursor (--insert-before) |
d | Diff change at cursor (updates on amend) |
D | Diff commit at cursor (pinned, immutable) |
c w | Commit transient → reword (describe) commit at cursor |
c n | Commit transient → new child commit |
s | Select squash source or destination (two-step) |
S | Squash file at cursor into parent |
u | Unsquash file at cursor |
a | Abandon commit at cursor |
U | Undo |
x | Cancel pending operation (squash or rebase) |
= | Diff (alias for d) |
q | Close window |
? | Show key binding help |
log.jujutsu
| Key | Action |
|---|---|
R | Refresh |
e | Edit commit at cursor (move @) |
b c/m/d/t/f | Bookmark: create / move / delete / track / forget |
r s | Mark rebase source (jj rebase -s) |
r r | Mark rebase source (jj rebase -r) |
r b | Mark rebase source (jj rebase -b) |
r o | Rebase onto commit at cursor (-d) |
r A | Insert after commit at cursor (--insert-after) |
r B | Insert before commit at cursor (--insert-before) |
d | Diff change at cursor (updates on amend) |
D | Diff commit at cursor (pinned, immutable) |
c w | Commit transient → reword (describe) commit at cursor |
c n | Commit transient → new child commit |
s | Select squash source or destination (two-step) |
a | Abandon commit at cursor |
U | Undo |
x | Cancel pending operation (squash or rebase) |
= | Diff (alias for d) |
<CR> | Apply revset shortcut on cursor line |
q | Close window |
? | Show key binding help |
diff.jujutsu
| Key | Action |
|---|---|
R | Refresh (re-runs jj diff) |
q | Close window |
? | Show key binding help |
describe.jujutsu
| Key (mode) | Action |
|---|---|
<C-c><C-c> (normal) | Finalize commit (save and close) |
<C-c><C-k> (normal) | Abort (close without saving) |
<C-c> (insert) | Finalize commit (save and close) |
? (normal) | Show key binding help |
vim profile
Two-letter verb chords inspired by Fugitive. Most actions use doubled
letters (nn, dd, etc.) to keep single keys free for text
navigation:
| Key | Action |
|---|---|
nn | New change |
ll | Open log |
ee | Edit commit at cursor (move @) |
dd | Describe commit at cursor |
d | Diff change at cursor (updates on amend) |
D | Diff commit at cursor (pinned, immutable) |
= | Diff (alias for d) |
bb c/m/d/t/f | Bookmark: create / move / delete / track / forget |
rr s | Mark rebase source (jj rebase -s) |
rr r | Mark rebase source (jj rebase -r) |
rr b | Mark rebase source (jj rebase -b) |
rr o | Rebase onto commit at cursor (-d) |
rr A | Insert after commit at cursor (--insert-after) |
rr B | Insert before commit at cursor (--insert-before) |
ss | Select squash source or destination (two-step) |
SS | Squash file at cursor into parent |
uu | Unsquash file at cursor |
UU | Undo |
aa | Abandon commit at cursor |
x | Cancel pending operation (squash or rebase) |
ff | Git fetch |
pp | Git push |
PP | Git push --force-with-lease |
Enable with:
require('badjuju').setup({ keymapProfile = 'vim' })
none profile
Disables all built-in keymaps. Define your own using the :JJ*
commands above.
Auto-refresh
Open status, log, and diff buffers auto-refresh whenever a jj
operation runs — even one you ran in a terminal. No manual reload
required.
Syntax highlighting
Highlights come from the LSP via semantic tokens. Your colorscheme's standard token groups (comments, keywords, strings, types, enum members, numbers, operators) are picked up automatically; no extra configuration needed.
VS Code
The VS Code extension lives in clients/vscode/ and is the most
feature-complete client today.
Requirements
- VS Code (any recent version)
badjujuserver binary on yourPATH(see Getting Started)
Installation
From the marketplace
(Coming soon.) For now, build and install a local VSIX.
Build a local VSIX
# From the repo root:
redo clients/vscode/install
This builds the VSIX for your platform and installs it via
code --install-extension --force. The code CLI must be on your
PATH — in VS Code, run Shell Command: Install 'code' command in
PATH from the Command Palette.
For other platforms or a packaged set of VSIXs:
# Single non-host VSIX
TARGET=x86_64-unknown-linux-gnu redo clients/vscode/all
# All platforms at once (requires zig + cargo-zigbuild)
redo clients/vscode/pack
Commands
Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) and type
jj to filter.
| Command ID | Palette name | Description |
|---|---|---|
badjuju.status.open | jj: Status | Open status / change stack |
badjuju.log.open | jj: Open log | Open the revision log |
badjuju.describe.open | jj: Describe working copy | Edit the current commit message |
badjuju.new.open | jj: New commit | Create a new empty change |
badjuju.next.open | jj: Move forward (jj next) | Move @ to next child |
badjuju.next.edit | jj: Edit next in place | Edit next change in place |
badjuju.prev.open | jj: Move back (jj prev) | Move @ to previous parent |
badjuju.prev.edit | jj: Edit previous in place | Edit previous change in place |
badjuju.refresh.open | jj: Refresh | Re-run the current buffer's command |
badjuju.undo.open | jj: Undo last operation | Undo last jj op |
badjuju.fetch.run | jj: Git fetch | Run jj git fetch |
badjuju.push.normal | jj: Git push | Run jj git push |
badjuju.push.forceWithLease | jj: Git push --force-with-lease | Force push with lease |
badjuju.edit.cursor | jj: Edit commit at cursor | Move @ to commit under cursor |
badjuju.abandon.cursor | jj: Abandon commit at cursor | Abandon commit under cursor |
badjuju.diff.cursor | jj: Show diff for commit at cursor | Diff for commit under cursor |
badjuju.describe.finalize | jj: Finalize commit description | Save and close describe buffer |
badjuju.squash.file | jj: Squash file at cursor | Move file under cursor into parent |
badjuju.unsquash.file | jj: Unsquash file at cursor | Pull file back from parent |
badjuju.rebase.source.cursor | jj: Mark rebase source | Mark source + mode for two-step rebase |
badjuju.rebase.commit.cursor | jj: Execute pending rebase | Execute rebase to commit at cursor |
badjuju.cancel.run | jj: Cancel pending operation | Cancel pending squash or rebase |
badjuju.bookmark.prompt | jj: Bookmark | Interactive bookmark manager |
badjuju.log.applyShortcut | jj: Apply revset shortcut | Follow revset link in log |
badjuju.help.open | jj: Show hotkey help | Cheat sheet for current buffer |
badjuju.version.open | jj: Show version | Server and jj versions |
badjuju.restartLanguageServer | jj: Restart Language Server | Restart the LSP |
Keymap profiles
Set badjuju.keymapProfile to choose:
"magit"(default) — single-key bindings"vim"— two-letter verb chords"none"— no built-in keymaps
magit profile — selected bindings
status.jujutsu / log.jujutsu
| Key | Action |
|---|---|
R | Refresh |
n | New commit |
L | Open log |
Ctrl+N / Ctrl+P | Move forward / back (jj next / jj prev) |
Ctrl+Shift+N / Ctrl+Shift+P | Edit next / previous in place |
f / p / P | Fetch / push / force push |
e | Edit commit at cursor |
b | Bookmark |
r s / r r / r b | Mark rebase source (--source / --revisions / --branch) |
r o / r A / r B | Execute rebase (onto / insert-after / insert-before) |
d | Diff (change) |
D | Diff (commit, pinned) |
c w | Commit transient → reword (describe) commit at cursor |
c n | Commit transient → new child commit |
s | Select squash source or destination (two-step) |
S | Squash file at cursor into parent |
x | Cancel pending operation (squash or rebase) |
u, Ctrl+K u | Unsquash file at cursor |
a | Abandon |
U | Undo |
= | Diff (alias for d) |
q | Close |
? | Help |
Squash window (.jj/badjuju/squash/*.jujutsu)
| Key | Action |
|---|---|
s | Toggle hunk / file between SELECTED and REMAINING |
e | Edit hunk before squashing |
a | Select all changes |
A | Deselect all changes |
u | Undo |
Tab | Toggle fold |
q | Close |
describe.jujutsu
| Key | Action |
|---|---|
Ctrl+Enter | Finalize commit (save and close) |
Escape Escape | Abort (close without saving) |
? | Help |
vim profile
Doubled letters: nn, ll, dd, ss, SS, uu, UU, aa, ee,
bb, ff, pp, PP. Rebase chords use a three-key sequence: r r s /
r r r / r r b for source, r r o / r r A / r r B for destination.
x cancels any pending operation. Single-key bindings: D, =, q, ?.
See the in-repo clients/vscode/README.md for the complete table.
Settings
| Setting | Purpose |
|---|---|
badjuju.binaryPath | Path to the jj binary; blank uses PATH |
badjuju.defaultLogRevset | Default revset for badjuju.log.open |
badjuju.keymapProfile | magit, vim, or none |
Auto-refresh
Open status, log, and diff buffers auto-refresh when jj operations
happen — including ones triggered from a terminal outside VS Code.
Emacs
The Emacs client lives in clients/emacs/ and is modeled on Magit.
It uses eglot (Emacs 29+ built-in LSP client) and transient (the
popup-menu library Magit itself uses).
Requirements
- Emacs 29+ (ships built-in
eglotandtransient) badjujuserver binary on yourPATHjjon yourPATH
Installation
Vanilla Emacs (manual)
(add-to-list 'load-path "/path/to/badjuju/clients/emacs")
(require 'badjuju)
use-package
(use-package badjuju
:load-path "/path/to/badjuju/clients/emacs"
:commands (badjuju-status badjuju-log badjuju-diff))
straight.el
(straight-use-package
'(badjuju :type git
:host github
:repo "jennings/badjuju"
:files ("clients/emacs/*.el")))
Or with use-package integration:
(use-package badjuju
:straight (badjuju :type git
:host github
:repo "jennings/badjuju"
:files ("clients/emacs/*.el"))
:commands (badjuju-status badjuju-log badjuju-diff))
Doom Emacs
In packages.el:
(package! badjuju
:recipe (:host github :repo "jennings/badjuju"
:files ("clients/emacs/*.el")))
In config.el:
(use-package! badjuju
:commands (badjuju-status badjuju-log badjuju-diff)
:config
(map! :leader "g j" #'badjuju-status))
Configuration
;; Path to the jj binary; nil uses PATH.
(setq badjuju-binary-path nil)
;; Hotkey profile: "magit" (default) or "none".
(setq badjuju-keymap-profile "magit")
Top-level commands
| Command | Description |
|---|---|
M-x badjuju-status | Open the working-copy status view |
M-x badjuju-log | Open the commit log |
M-x badjuju-diff | Diff for @ (change mode, updates on amend) |
M-x badjuju-describe | Edit the commit message for @ |
M-x badjuju-new | Create a new child change |
M-x badjuju-edit | Move @ to a different commit |
M-x badjuju-abandon | Abandon the working copy |
M-x badjuju-squash | Squash into the parent |
M-x badjuju-unsquash | Pull a file back from the parent |
M-x badjuju-undo | Undo the last operation |
M-x badjuju-fetch | jj git fetch |
M-x badjuju-push | jj git push |
M-x badjuju-refresh | Refresh the current buffer |
Most workflows live in the status buffer — open it with
M-x badjuju-status and use the hotkeys below. Press ? at any time
for a popup of active bindings in the current buffer.
Keybindings (magit profile)
Status buffer
| Key | Action |
|---|---|
n | New child change |
c | Commit transient (reword / new child) |
c w | Commit transient → reword (describe) commit at cursor |
c n | Commit transient → new child commit |
d | Diff change at cursor (updates on amend) |
D | Diff commit at cursor (pinned, immutable) |
= | Diff (alias for d) |
e | Edit commit at cursor (move @) |
a | Abandon commit at cursor |
s | Select squash source or destination (two-step) |
S | Squash file at cursor into parent |
u | Unsquash file at cursor |
U | Undo last operation |
r s | Mark rebase source (--source) |
r r | Mark rebase source (--revisions) |
r b | Mark rebase source (--branch) |
r o | Execute rebase onto cursor |
r A | Execute rebase insert-after cursor |
r B | Execute rebase insert-before cursor |
x | Cancel pending squash or rebase |
b | Bookmark manager |
f | jj git fetch |
p | jj git push |
P | jj git push --force-with-lease |
L | Open log |
R | Refresh |
TAB | Toggle fold at cursor |
RET | Go to definition |
gd | Go to definition |
? | Show help popup |
q | Bury buffer |
Code actions intentionally have no default binding — use the global
M-x eglot-code-actions (Emacs binds it to C-c C-a by default in
eglot-managed buffers).
Log buffer
| Key | Action |
|---|---|
c | Commit transient (reword / new child) |
c w | Commit transient → reword (describe) commit at cursor |
c n | Commit transient → new child commit |
d | Diff change at cursor (updates on amend) |
D | Diff commit at cursor (pinned, immutable) |
= | Diff (alias for d) |
e | Edit commit at cursor |
a | Abandon commit at cursor |
s | Select squash source or destination (two-step) |
S | Squash file at cursor into parent |
U | Undo |
r s | Mark rebase source (--source) |
r r | Mark rebase source (--revisions) |
r b | Mark rebase source (--branch) |
r o | Execute rebase onto cursor |
r A | Execute rebase insert-after cursor |
r B | Execute rebase insert-before cursor |
x | Cancel pending squash or rebase |
b | Bookmark manager |
R | Refresh |
RET | Apply revset shortcut on a JJ: line / go to definition |
gd | Go to definition |
? | Show help popup |
q | Bury buffer |
Diff buffer
| Key | Action |
|---|---|
R | Refresh |
RET / gd | Go to definition |
? | Show help popup |
q | Bury buffer |
Squash buffer
| Key | Action |
|---|---|
s | Toggle hunk/file between SELECTED and REMAINING |
e | Edit hunk before squashing |
a | Select all changes |
A | Deselect all changes |
u | Undo |
TAB | Toggle fold |
gd | Go to definition |
? | Show help popup |
q | Bury buffer |
Describe buffer
| Key | Action |
|---|---|
C-c C-c | Finalize and close (saves the commit message) |
C-c C-k | Abort without saving |
Folding
Status and squash buffers open fully folded. The WORKING COPY CHANGES and PARENT CHANGES sections are automatically expanded on
first open; TAB toggles individual sections.
Kakoune
The Kakoune client lives in clients/kakoune/ in the repo. It uses
kak-lsp to talk to the
badjuju LSP server and exposes all operations as :JJ* commands with
an optional keymap via Kakoune user-modes.
Requirements
- Kakoune 2023.08.05+
- kak-lsp 0.14+ on your
PATH jjon yourPATHbadjujubinary on yourPATH(see Getting Started)
Install
plug.kak
plug "jennings/bad-juju" subset [clients/kakoune/badjuju.kak] config %{
# Optional: switch to vim-profile chords (default: magit)
# set-option global badjuju_keymap_profile vim
}
Manual clone
Symlink the entry point into your autoload directory:
ln -s /absolute/path/to/bad-juju/clients/kakoune/badjuju.kak \
~/.config/kak/autoload/badjuju.kak
Or source it directly from your kakrc:
source '/absolute/path/to/bad-juju/clients/kakoune/badjuju.kak'
Setup
1. Configure kak-lsp
Merge the kak-lsp.toml snippet from this directory into
~/.config/kak-lsp/kak-lsp.toml:
cat /path/to/bad-juju/clients/kakoune/kak-lsp.toml \
>> ~/.config/kak-lsp/kak-lsp.toml
Then start kak-lsp in your kakrc (if not already):
eval %sh{ kak-lsp --kakoune -s $kak_session }
2. Choose a keymap profile
The default is magit (single-letter bindings). To switch to the
vim profile (double-letter chords), set the option before sourcing
the plugin:
set-option global badjuju_keymap_profile vim
source '/path/to/badjuju.kak'
Or in plug.kak config:
plug "jennings/bad-juju" ... config %{
set-option global badjuju_keymap_profile vim
}
Opening buffers
The canonical one-liner opens the working-copy status view in Kakoune:
kak "$(badjuju status)"
Similarly for other views:
kak "$(badjuju log)"
kak "$(badjuju diff)"
kak "$(badjuju diff --revision abc123)"
Commands
| Command | Description |
|---|---|
:JJStatus | Open .jj/badjuju/status.jujutsu |
:JJLog [revset] | Open the log buffer |
:JJLogFile | Open per-file log for the file at cursor |
:JJDescribe [revision] | Edit a commit message (default: @) |
:JJDiff [revision] | Open a change diff (updates on amend) |
:JJDiffCommit [revision] | Open a pinned commit diff |
:JJNew | Create a new change |
:JJNext | Move @ to the next child |
:JJPrev | Move @ to the previous parent |
:JJRefresh | Refresh the current badjuju buffer |
:JJSquash | Squash file at cursor into its parent |
:JJUnsquash | Unsquash file at cursor from parent |
:JJUndo | Undo the last jj operation |
:JJAbandon [revision] | Abandon a revision (default: @ at cursor) |
:JJEdit [revision] | Move @ to this revision |
:JJFetch | Run jj git fetch |
:JJPush [!] | Run jj git push (! for --force-with-lease) |
:JJCancel | Cancel pending squash or rebase |
Commands auto-start the LSP for the current workspace if it isn't already running.
Keymap reference
With a *.jujutsu buffer active, press <space> to enter the badjuju
user-mode. The bindings below use the magit profile (default).
magit profile — status.jujutsu
| Key | Action |
|---|---|
R | Refresh |
n | New change |
L | Open log |
f | Git fetch |
p | Git push |
P | Git push --force-with-lease |
U | Undo |
a | Abandon revision at cursor |
e | Edit commit at cursor (move @) |
d | Diff change at cursor (updates on amend) |
D | Diff commit at cursor (pinned) |
s | Select squash source/dest (two-step) |
S | Squash file at cursor into parent |
u | Unsquash file at cursor |
x | Cancel pending operation |
q | Close buffer |
b | Bookmark… (chord) |
r | Rebase… (chord) |
c | Commit… (chord) |
? | Show key binding help |
magit profile — log.jujutsu
| Key | Action |
|---|---|
R | Refresh |
n | New change |
U | Undo |
a | Abandon revision at cursor |
e | Edit commit at cursor (move @) |
d | Diff change at cursor (updates on amend) |
D | Diff commit at cursor (pinned) |
s | Select squash source/dest (two-step) |
x | Cancel pending operation |
q | Close buffer |
b | Bookmark… (chord) |
r | Rebase… (chord) |
c | Commit… (chord) |
? | Show key binding help |
magit profile — diff.jujutsu
| Key | Action |
|---|---|
R | Refresh |
q | Close buffer |
? | Show key binding help |
magit profile — squash window
| Key | Action |
|---|---|
s | Toggle hunk/file at cursor |
e | Edit hunk before squashing |
a | Select all changes |
A | Deselect all changes |
q | Close buffer |
? | Show key binding help |
magit profile — describe.jujutsu
| Key | Action |
|---|---|
<C-c><C-c> | Save and close (finalize commit message) |
<C-c><C-k> | Abort (close without saving) |
? | Show key binding help |
Chord workflows
Bookmark (b prefix in magit / bb prefix in vim)
| Key | Action |
|---|---|
c | Create bookmark (prompts for name) |
m | Move bookmark (prompts for name) |
d | Delete bookmark (prompts for name) |
t | Track remote bookmark (prompts for name@remote) |
f | Forget bookmark (prompts for name) |
Rebase (r / rr)
| Key | Action |
|---|---|
s | Mark source with --source |
r | Mark source with --revisions |
b | Mark source with --branch |
o | Complete: rebase onto this commit |
A | Complete: insert after this commit |
B | Complete: insert before this commit |
Commit transient (c / cc)
| Key | Action |
|---|---|
w | Reword — open describe.jujutsu |
n | New child commit |
Commit-to-commit squash
- Press
son the source commit in status or log → server marks it. - Press
son the destination commit → server opens the squash window. - In the squash window, use
s/e/a/Ato manage hunks. :wfinalizes the squash;:q!aborts.
vim profile
Double-letter chords inspired by Fugitive. Enable with:
set-option global badjuju_keymap_profile vim
| Key | Action |
|---|---|
nn | New change |
ll | Open log |
ee | Edit commit at cursor (move @) |
dd | Describe commit |
dd | Diff change at cursor (updates on amend) |
DD | Diff commit at cursor (pinned) |
ss | Squash source/dest (two-step) |
SS | Squash file at cursor |
uu | Unsquash file at cursor |
UU | Undo |
aa | Abandon revision |
ff | Git fetch |
pp | Git push |
PP | Git push --force-with-lease |
RR | Refresh |
qq | Close buffer |
x / xx | Cancel pending operation |
bb + c/m/d/t/f | Bookmark chord |
rr + s/r/b/o/A/B | Rebase chord |
cc + w/n | Commit transient chord |
Save flow
describe.jujutsu: edit the commit message, then:wrunsjj describe -m <message>.<C-c><C-c>saves and closes;<C-c><C-k>aborts (magit profile).hunk-edit-*.jujutsu: move hunks between SELECTED/REMAINING sections, then:wapplies the selection viajj squash.
Customization
Override the <space> leader
Re-bind <space> after sourcing the plugin:
hook global WinSetOption filetype=jujutsu %{
map window normal <tab> ': enter-user-mode badjuju-status<ret>'
}
Disable the default keymap
Set the profile to any unrecognized value; the source block in badjuju.kak
will then source keymap-magit.kak (fallback). To truly disable, edit the
sourcing logic or just don't map <space>:
hook global WinSetOption filetype=jujutsu %{
unmap window normal <space>
}
Troubleshooting
kak-lsp not attaching to jujutsu buffers
Check that the [language.jujutsu] section is present in
~/.config/kak-lsp/kak-lsp.toml and that kak-lsp is running:
:lsp-show-server
:w on describe.jujutsu has no effect
Make sure include_text_on_save = true is set in your kak-lsp.toml
snippet. Without it, the server receives the save event but no text.
Auto-reload not firing after mutations
workspace/applyEdit is the mechanism the server uses to push updated
content to open buffers. Verify kak-lsp is attached (lsp-show-server)
and that it supports apply-edit (kak-lsp 0.14+).
Other Editors
Bad Juju is "just" an LSP server, which means any editor with a sufficiently capable LSP client can drive it. The first-party Helix configuration is documented here as the canonical example; the same principles apply to other LSP-capable editors.
Helix
Helix has no plugin system, so the integration is a small
languages.toml snippet plus a few CLI one-liners.
Requirements
- Helix 25.01+
jjandbadjujuon yourPATH
Setup
- Install the server. See Getting Started.
- Merge the language config. Copy
clients/helix/languages.tomlfrom the repo into either:~/.config/helix/languages.toml(user-wide), or.helix/languages.tomlat the root of your project (per-project).
Opening buffers
Helix doesn't auto-open files returned by code actions, so the entry point is the shell:
hx "$(badjuju status)"
hx "$(badjuju log)"
hx "$(badjuju diff)" # change diff for @
hx "$(badjuju diff --revision abc123)" # diff a specific revision
You can open multiple diffs at once:
hx "$(badjuju diff --revision abc)" "$(badjuju diff --revision def)"
Navigation
Once a .jujutsu buffer is open, Helix uses Space a for code
actions. With the cursor on a commit row you'll get:
| Action | Description |
|---|---|
Edit commit <rev> | Move @ to this commit |
Abandon commit <rev> | Delete this commit |
Describe commit <rev> | Edit commit message |
Show diff for <rev> | Open the change diff |
New child of <rev> | jj new <rev> |
Bookmark <rev>… | Bookmark management menu |
| Squash from this revision | Mark this commit as squash source |
| Rebase --source from this revision | Mark source for jj rebase -s |
| Rebase --revisions from this revision | Mark source for jj rebase -r |
| Rebase --branch from this revision | Mark source for jj rebase -b |
After marking a squash source, Squash into this revision and Cancel pending operation appear on every commit row.
After marking a rebase source, three destination actions appear:
| Action | Description |
|---|---|
| Rebase onto this revision | jj rebase … -d <dest> |
| Insert after this revision | jj rebase … --insert-after <dest> |
| Insert before this revision | jj rebase … --insert-before <dest> |
| Cancel pending operation | Clear the pending rebase source |
See Manipulating Commits for the full walkthrough.
For file rows in the status buffer:
| Action | Description |
|---|---|
Squash <file> | Move file from @ into parent |
Unsquash <file> | Pull file from parent back into @ |
Log <file> | Open the log file buffer for this path |
The Log <file> action opens
.jj/badjuju/file/<path>.jujutsu on demand. Helix doesn't auto-open
files returned by code actions, so open it manually after invoking
the action.
For squash-window rows:
| Action | Description |
|---|---|
| Move hunk to SELECTED / REMAINING | Toggle the hunk under the cursor |
Move file <name> to SELECTED / REMAINING | Toggle a whole file |
| Move all hunks to SELECTED / REMAINING | Select / deselect everything |
Log shortcuts
In log.jujutsu, lines beginning with JJ: are revset shortcuts.
Place the cursor on one and choose Apply revset: <label> from
Space a to re-run the log with that revset.
Other clients bind
Enter/RETto a context-aware dispatch — on shortcut lines it applies the revset, elsewhere it falls through to go-to-definition. Helix has no keybinding layer in Bad Juju, so useSpace afor both.
Auto-reload
When a jj operation runs outside Helix, the server pushes the
refreshed buffer content via workspace/applyEdit. Helix's handler
marks the buffer modified after the edit even though the on-disk
file already matches; :write is then a no-op. You can ignore the
modified indicator until your next real edit.
Known limitations
Helix doesn't auto-open files returned by code actions. If
Show diff for <rev> or Squash into this revision report a
new file path, open it manually:
:open .jj/badjuju/diff-change-<id>.jujutsu
:open .jj/badjuju/squash/<from>-<to>.jujutsu
Any other LSP-capable editor
If your editor:
- Speaks the Language Server Protocol,
- Can launch a stdio LSP server with a custom command, and
- Can invoke code actions (
textDocument/codeAction),
…then it can drive Bad Juju. You'll need to wire up:
- A filetype for
*.jujutsufiles. - An LSP server config that launches
badjuju lspand detects the workspace via a.jj/marker. - (Optional) Keybindings or commands that send
workspace/executeCommandfor thebadjuju.*operations listed in the Status buffer reference and elsewhere.
Reading the VS Code extension
source,
the Helix languages.toml
snippet,
or the Kakoune client (which uses kak-lsp and exposes
full command dispatch and keymaps) will give you a working template. If
you build an integration for a new editor, please open an
issue — we'd love to
include it.
Reference
Bad Juju produces a handful of buffer types, each with its own layout and key handlers. This chapter is the detailed tour of each:
- Status buffer (
status.jujutsu) — working copy summary, stack view, command reference. - Log buffer (
log.jujutsu) — revision log with editableREVSET:header andJJ:shortcut lines. - Diff buffer (
diff-change-<id>.jujutsu/diff-commit-<id>.jujutsu) — change-mode and commit-mode diffs. - Hunk edit buffer (
hunk-edit.jujutsu) — interactive squash with line-level edits.
For the everyday-task version of this material, see Usage. For client-specific keybindings, see Clients.
Status buffer
The status buffer lives at .jj/badjuju/status.jujutsu and is your
home base. It combines jj status, the working-copy stack (jj log
over a focused revset), and a one-screen command reference.
Layout
STATUS:
<jj status output>
STACK: <revset expression>
<jj log output over STACK>
COMMAND REFERENCE:
<one line per available action>
STATUS: section
Shows whether the working copy has changes, and the working-copy
commit (@) and its parent (@-). This is whatever jj status
prints, verbatim.
STACK: section
The STACK: line is a revset expression — by default
ancestors(reachable(@, mutable()), 2), which shows the working
copy, every commit you can still rewrite, and two layers of immutable
ancestors for context.
Some clients support --stat rendering server-side; when enabled, each
commit's line is followed by a per-file change summary.
COMMAND REFERENCE: section
A short, buffer-specific cheat sheet describing the key bindings or
commands available in this buffer. The contents vary slightly by
client; clients can also override the reference via initialization
options. The aim is that you never have to leave the buffer to
remember what s does.
Generated commands
The server exposes these LSP commands, which clients map to keys or menu entries:
| Command | What it does |
|---|---|
badjuju.status | (Re)write status.jujutsu and return its URI |
badjuju.refresh | Re-run the command that produced the current buffer |
badjuju.new | jj new (with optional cursor-target revision) |
badjuju.next / badjuju.prev | Move @ forward / back |
badjuju.edit | Move @ to the commit at the cursor |
badjuju.abandon | Abandon the commit at the cursor (or @) |
badjuju.describe | Open describe buffer for cursor commit |
badjuju.diff | Open change diff for cursor commit |
badjuju.diff.commit | Open commit diff for cursor commit |
badjuju.squash | Squash file at cursor into parent |
badjuju.unsquash | Unsquash file at cursor from parent |
badjuju.squash.commit | Mark source / open squash window |
badjuju.rebase.source | Mark rebase source (--source, --revisions, or --branch) |
badjuju.rebase.commit | Execute pending rebase (--destination, --insert-after, or --insert-before) |
badjuju.cancel | Clear any pending operation (squash or rebase) |
badjuju.undo | jj undo |
badjuju.fetch | jj git fetch |
badjuju.push | jj git push (with optional forceWithLease) |
badjuju.bookmark | Interactive bookmark manager |
badjuju.keymap / badjuju.help | Show the active key map |
badjuju.version | Display badjuju and jj versions |
See Clients for the per-editor key bindings that invoke these commands.
Cursor targeting
Cursor-driven actions (edit, abandon, describe, diff,
squash, unsquash, rebase.source, rebase.commit, bookmark)
read the cursor line and identify a commit or file:
- On a commit-header row (e.g. the line with
@ kpkzwvqm 909679d0 …), the target is that commit's change ID. - On a file row in the
STATUS:section, the target is that file relative to@.
If the cursor is on something else (a blank line, a section header,
the command reference), most actions either operate on @ as a
sensible default or report that no target was found.
Auto-refresh
Open status buffers refresh automatically whenever a jj operation
runs — whether it came from Bad Juju, a different editor, or a
terminal jj invocation. The server watches the op log for new
heads and pushes updated content to every open client.
Folding
Some clients (notably Emacs) open the status buffer fully folded by
default, with the WORKING COPY CHANGES and PARENT CHANGES
sections expanded. Use the editor's fold key (TAB in Emacs) to
toggle individual sections.
Log buffer
The log buffer lives at .jj/badjuju/log.jujutsu. It runs jj log
against a configurable revset and lets you both edit that revset
inline and jump to predefined shortcuts.
Layout
REVSET: <current revset>
JJ: <shortcut label>: <shortcut revset>
JJ: <shortcut label>: <shortcut revset>
...
OUTPUT:
<jj log output for REVSET>
COMMAND REFERENCE:
<one line per available action>
REVSET: header
The first line is an editable revset expression. Save the buffer
after editing this line and Bad Juju re-runs jj log with the new
expression and rewrites the buffer in place.
The default value is whatever revset you opened the log with — @ if
you called :JJLog / badjuju-log / palette jj: Open log with
no argument, or a configured default (badjuju.defaultLogRevset in
VS Code, defaultLogRevset in Neovim setup()).
JJ: shortcut lines
Each JJ: line is a named shortcut: a label and a revset expression.
Place the cursor on a shortcut line and:
- In Neovim, VS Code, Emacs (magit profile): press
Enter. The editor invokesbadjuju.log.applyShortcut, which replacesREVSET:with the shortcut's revset and re-runs the log. - In Helix: press
Space aand pick Apply revset:<label>.
Shortcut lines are also useful as documentation — they're plain text
in the buffer, so you can copy them, edit them, or paste them into
the REVSET: line by hand.
OUTPUT: section
This is the raw output of jj log -r <REVSET>. With --stat enabled
(toggle via =), each commit row is followed by its per-file change
summary.
Cursor-driven actions
Most of the same actions as the status
buffer work in the log:
edit, abandon, describe, diff, rebase.source,
rebase.commit, cancel, bookmark, squash.commit. The cursor
must be on a commit row (one of the lines emitted by jj log).
Enter on a JJ: line is special-cased to apply the shortcut. Some
clients (Neovim/VS Code with the magit profile, Emacs) also
fall back to go-to-definition on Enter when the cursor isn't on a
shortcut line.
Auto-refresh
Like the status buffer, the log auto-refreshes whenever a jj
operation runs. The REVSET: value is preserved across refreshes,
so editing the header gives you a sticky filtered log.
Log file buffer
The log file buffer shows the history of a single file — every commit that touched the path, each followed by its inline diff. It's the equivalent of:
jj log -r ..@ -p path/to/file.txt
Modeled on Magit's magit-log-buffer-file: a separate buffer per
file, distinct from the main log buffer.
Opening the buffer
Place the cursor on a file row inside the status buffer and:
- Magit profile (Neovim, VS Code, Emacs): press
l f. - Vim profile: press
l f. - Helix: press
Space aand pick Log<file>.
You can also drive it directly:
- Neovim:
:JJLogFile path/to/file.txt - VS Code: Command Palette →
jj: Log file… - Emacs:
M-x badjuju-log-file
Re-invoking the command on a path that's already open reuses the same buffer with refreshed content.
Delivery
| Client capability | Delivery | URI |
|---|---|---|
| Virtual (VS Code, Neovim) | workspace/textDocumentContent | badjuju-filelog:///<repo-rel-path> |
| File-based (Helix, Emacs) | Physical file | .jj/badjuju/file/<repo-rel-path>.jujutsu |
The .jujutsu suffix on the physical path keeps the buffer in jujutsu
syntax in every editor — matching the status.jujutsu /
log.jujutsu / diff-*.jujutsu convention.
Layout
FILE: <repo-relative path>
REVSET: <current revset>
JJ: <shortcut label>: <shortcut revset>
JJ: <shortcut label>: <shortcut revset>
...
OUTPUT:
<jj log -p output for REVSET, restricted to FILE>
COMMAND REFERENCE:
<one line per available action>
FILE: header
Workspace-relative path of the file being viewed. Saving the buffer after editing this header (file-based clients only) regenerates the buffer for the new path.
REVSET: header
The default revset is ..@ — every commit reachable from the working
copy minus the root. Saving the buffer after editing this header
(file-based clients only) reruns the query with the new revset and
rewrites the buffer in place.
JJ: shortcut lines
Same as the regular log buffer. Apply via Enter (Magit profile) or
Space a (Helix) to substitute the shortcut's revset into the
REVSET: header.
OUTPUT: section
The raw output of jj log -r <REVSET> -p -- <FILE>. Each commit's
header is followed by the unified diff of that commit's changes to
the file.
Per-file buffer model
Each path has its own URI / on-disk file, keyed by the workspace- relative path. Opening the same file twice doesn't create a new buffer; the existing one is refreshed.
The view is per file — there is no per-(file, revset) variant in
this release. Changing the REVSET: header replaces the query for
that file.
Auto-refresh
Like the status and log buffers, the file-history buffer auto-refreshes
after every jj operation. The FILE: and REVSET: headers are
preserved across refreshes.
Out of scope
The following are intentionally not supported in this release:
--followfor renames.- Region-restricted log (Magit's
-L). - Per-
(file, revset)buffers so the same file's history with different revsets can coexist.
Diff buffer
Bad Juju produces diffs in two flavors, distinguished by URI scheme (or filename, on file-based clients):
- Change diff — pinned to a change ID. Auto-refreshes when the
change is amended. This is what
badjuju.diff/dopens. - Commit diff — pinned to an immutable commit ID. The view is
frozen to that exact snapshot. Opened via
badjuju.diff.commit(Din VS Code, Neovim, and Emacs).
Filenames and URIs
| Mode | Virtual URI (VS Code, Neovim) | File-based (Helix) |
|---|---|---|
| Change | badjuju-diff:///change/<id> | .jj/badjuju/diff-change-<12char>.jujutsu |
| Commit | badjuju-diff:///commit/<id> | .jj/badjuju/diff-commit-<12char>.jujutsu |
The server detects whether the client supports virtual URIs (via
initializationOptions.virtualDiffs: true) and picks the delivery
mode accordingly:
- Virtual-capable clients (VS Code, Neovim) receive diff content
through the LSP 3.18
workspace/textDocumentContentrequest — no file ever hits disk. - File-based clients (Helix) get a real file under
.jj/badjuju/.
After mutations to a change (describe, new, squash, etc.) the
server sends a workspace/textDocumentContent/refresh for every open
change-diff URI, or rewrites the on-disk file for file-based clients.
Commit diffs are never refreshed.
Layout
<jj diff -r <rev> output>
That's it — the buffer is just whatever jj diff printed. Unlike the
status and log buffers, there's no COMMAND REFERENCE: section
appended.
Opening multiple diffs
Because each diff is keyed by ID, you can have any number of diff buffers open simultaneously. Use this to compare two revisions:
# Helix example
hx "$(badjuju diff --revision abc)" "$(badjuju diff --revision def)"
In VS Code and Neovim you can open as many diffs as you like via the
command palette / :JJDiff — they all live as separate virtual URIs.
Key bindings
The diff buffer has a minimal key map:
| Key | Action |
|---|---|
R | Refresh (re-runs jj diff for the same revision) |
q | Close window |
? | Show key binding help |
Emacs additionally maps RET / gd to go-to-definition. Code
actions intentionally have no badjuju-specific binding in any client —
use your editor's native binding (M-x eglot-code-actions in Emacs,
vim.lsp.buf.code_action in Neovim, Cmd+. / Ctrl+. in VS Code).
Auto-refresh
Change diffs auto-refresh whenever the change they target is amended — by any operation, including ones triggered outside Bad Juju. Commit diffs never refresh; they're frozen to their commit ID.
Hunk edit buffer
The hunk edit buffer lives at .jj/badjuju/hunk-edit.jujutsu. It's
opened from inside a squash window when you want to tweak the
contents of a single hunk before squashing it into the destination
commit.
When you'd use it
You're in a commit-to-commit squash (see Manipulating Commits). You've picked a hunk to move from the source commit into the destination, but a couple of lines in that hunk don't actually belong in the destination. Rather than splitting the hunk by hand or moving the whole thing and re-editing afterward, you edit it once, at selection time.
How it works
-
From the squash window, place the cursor on a hunk header.
-
Press
e(Emacs) or invoke the Edit hunk code action. -
Bad Juju opens
hunk-edit.jujutsupopulated with the hunk's contents:JJ: Editing hunk in <file> JJ: Lines beginning with '-' are deletions and cannot be edited. JJ: Edit '+' (added) and ' ' (context) lines; save to apply. <file path> @@ -<old_start>,<old_len> +<new_start>,<new_len> @@ context line -deleted line +added line context line -
Edit the
+(additions) and(context) lines.-(deletion) lines are read-only — editing them has no effect. -
Save. Bad Juju:
- Recomputes the
@@header (the line lengths after your edits). - Runs
jj squash --interactive --tool badjujuto apply the edited hunk to the source commit. - Refreshes the squash window so you can continue picking hunks.
- Recomputes the
Status messages
The buffer's first action after save is to print a terminal status line:
| Status | Meaning |
|---|---|
EDIT APPLIED | The hunk was applied successfully. |
EDIT ABORTED | The body was cleared; no change was made. |
STALE SOURCE | The source commit was abandoned (or otherwise rewritten) while you were editing. Reopen the squash window and try again. |
Constraints
- Only one hunk-edit buffer at a time. The path is
.jj/badjuju/hunk-edit.jujutsu— a single shared location. If you open a hunk-edit for a second hunk, it replaces the first. - The buffer is only meaningful when the parent squash window is still alive. Closing the squash window invalidates the edit.
JJ:-prefixed lines and the@@header are advisory metadata. Don't edit them — Bad Juju regenerates them on save based on your edits to the body.
Key bindings
| Client | Finalize (save) | Close without saving |
|---|---|---|
| Neovim | :write | :bd! |
| VS Code | Ctrl+S | Close editor without save |
| Emacs | C-x C-s | C-c C-k (where bound) |
The buffer behaves like a normal editor file — save semantics drive the apply step, not a dedicated keybinding.