Skip to content

Native Jujutsu (jj) support - #12

Open
CoreyKaylor wants to merge 2 commits into
mainfrom
jj
Open

CoreyKaylor wants to merge 2 commits into
mainfrom
jj

Conversation

@CoreyKaylor

Copy link
Copy Markdown
Owner

Adds native Jujutsu support (#11). In a jj workspace, review, staging, commit, log, compare and conflict resolution all run through jj, and diffbandit can act as jj's merge editor and diff editor.

This is open for jj users to try before it merges. Feedback on how the staging model feels is especially welcome.

Commits

  1. External merge and diff tool mode. Adds diffbandit merge|edit|diff, a pluggable merge host, and an editable folder diff. It doesn't depend on jj and also works as a Git mergetool.
  2. VCS backend proxy. Routes repository calls to Git or jj by repo. No behaviour change on its own, and it fixes three small Git bugs along the way.
  3. jj backend. Queues, the squash-workflow staging model, commit and amend, native conflicts, and 50_jj specs run against real jj repos. CI installs jj.
  4. Docs. README and :help diffbandit-jj.

Trying it

Point your plugin manager at this branch, for example with lazy.nvim:

{ "CoreyKaylor/diffbandit.nvim", branch = "jj" }

Then run diffbandit (or :DiffBanditLaunch) in a jj repo. To use it as jj's editor too, add this to ~/.config/jj/config.toml, with bin/diffbandit on your PATH:

[merge-tools.diffbandit]
program = "diffbandit"
merge-args = ["merge", "$base", "$left", "$right", "$output", "--path", "$path"]
edit-args = ["edit", "$left", "$right"]
diff-args = ["diff", "$left", "$right"]
merge-tool-edits-conflict-markers = false

[ui]
merge-editor = "diffbandit"
diff-editor = "diffbandit"

How staging maps to jj

  • The unstaged view shows @-→@, the same as plain jj diff.
  • Staging a hunk or file moves it from @ into the staging change. The staging change is @-, as long as it has no description and can still be edited.
  • If @- is already a described commit, your first stage creates an empty staging change under @. An existing commit is never changed by accident.
  • Amend mode stages straight into a described @-.
  • :w in the panel describes the staging change and opens a fresh one under @.

Worth trying

  • jj split, jj squash -i and jj resolve with the config above.
  • Staging hunks, committing, and amending from the panel.
  • Opening the log and a bookmark compare.
  • A colocated repo, including the vcs.prefer = "git" override.

Known limits

  • Conflicts with more than two sides, or involving a deletion, are handed off to jj resolve.
  • A merge @ lists only its conflicts.
  • The log shows commit ids rather than change ids.

Testing

  • The unit suite, including the 50_jj specs, and tests/integration/run.sh pass on each commit.
  • I checked these by hand in a terminal: jj resolve (quitting keeps the conflict, saving resolves it), jj split, and staging from the panel.

Closes #11

@CoreyKaylor
CoreyKaylor force-pushed the jj branch 2 times, most recently from 696fc31 to fc5db33 Compare November 9, 2026 14:59
@klement

klement commented Nov 9, 2026 •

Copy link
Copy Markdown

>> doesn't work in jj-split. "Git hunk actions are only available in Git diff sessions".

@CoreyKaylor

Copy link
Copy Markdown
Owner Author

>> doesn't work in jj-split. "Git hunk actions are only available in Git diff sessions".

The latest commit should resolve the issue.

@klement

klement commented Nov 11, 2026 •

Copy link
Copy Markdown

windo set scrollbind breaks the inter-buffer kitty "signs". This might be a generic diffbandit issue rather than jj flavour. I think the scrollbind should be the default applied to the side-by-side buffers or at least an option to have it so.

In a jj workspace (colocated or not) repository calls route to a native
jj backend, so reviews, hunk and file staging, the commit panel, log,
compare, checkout, and conflict resolution all work there.

- VCS proxy: consumers require diffbandit.vcs instead of diffbandit.git.
  It exposes the git module's function names and routes each call by its
  root/path/queue argument; vcs.prefer overrides detection. Queues from
  both backends keep kind = "git" and carry vcs = "git" | "jj".
- jj backend: staging follows jj's squash workflow. An undescribed,
  mutable @- is the staging change; staging moves hunks from @ into it
  with `jj diffedit --restore-descendants` (driven by a generated
  non-interactive tool script), so @'s content never changes. A described
  @- is a commit: the first stage from the unstaged view starts a fresh
  staging change, and only amend mode stages into or re-describes it.
  Views map onto git-shaped options (unstaged @- -> @, staged @-- -> @-,
  all @-- -> @, rev for any revsets) so git/sources.lua builds sources
  unchanged; ref_labels and index_tracks_additions are the only jj hooks
  there. The backend also implements read_blobs and queue_stage_states
  for main's index-position staging and panel stage states.
- External tool mode: `diffbandit merge <base> <left> <right> <output>`,
  `diffbandit edit <left> <right>`, and `diffbandit diff <left> <right>`
  need no repository and serve jj's merge-tools.* config and git
  mergetool/difftool.
- Docs: README and :help diffbandit-jj cover the view/staging/commit/
  amend/conflict mapping, jj merge-tools configuration, and limits.

CI installs a pinned jj; tests/specs/50_jj.lua runs against real jj repos
(including `jj resolve` and `jj split` through bin/diffbandit) and is
skipped when jj is missing.
jj split and squash -i open `diffbandit edit`, a folder diff with no
repository behind it, and the apply keys refused there with "Git hunk
actions are only available in Git diff sessions". Applying the left hunk
now writes it into the editable right buffer as an ordinary edit: one undo
step per apply, and :w saves the picked result. Applying the right hunk
says the left side is read-only.
@CoreyKaylor

Copy link
Copy Markdown
Owner Author

windo set scrollbind breaks the inter-buffer kitty "signs". This might be a generic diffbandit issue rather than jj flavour. I think the scrollbind should be the default applied to the side-by-side buffers or at least an option to have it so.

I made it an option and also made it the default. I wasn't sure I would like the behavior mostly from earlier prototypes but tried it out. Feels very nice and fluid with the new curved connectors.

This branch has been rebased on those latest changes in main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native Jujutsu (jj) support

2 participants