Skip to content

feat: add jj (Jujutsu) VCS support#1002

Draft
max-sixty wants to merge 74 commits intomainfrom
926
Draft

feat: add jj (Jujutsu) VCS support#1002
max-sixty wants to merge 74 commits intomainfrom
926

Conversation

@max-sixty
Copy link
Owner

Summary

  • Introduces a VCS-agnostic Workspace trait so commands can dispatch to git or jj handlers
  • Adds jj support for all core commands: list, switch, remove, merge, and step (commit/squash/rebase/push)
  • 50 integration tests covering all jj command paths

Part of #926

Test plan

  • All 1190 tests pass (cargo test)
  • All lints pass (pre-commit run --all-files)
  • 96% line coverage on new handle_step_jj.rs
  • 3 rounds of adversarial testing completed
  • CI passes

This was written by Claude Code on behalf of @max-sixty

max-sixty and others added 21 commits February 11, 2026 17:47
Add a Workspace trait that captures operations commands need,
independent of the underlying VCS. GitWorkspace wraps Repository
and delegates to existing methods. Nothing consumes the trait yet —
this is the foundation for jj support (#926).

Co-Authored-By: Claude <noreply@anthropic.com>
Phase 1 of jj workspace support:
- VCS detection by filesystem markers (.jj/ vs .git/, co-located prefers jj)
- JjWorkspace implementing Workspace trait via jj CLI
- Sequential data collection for jj repos (collect_jj)
- handle_list dispatches to jj or git path based on detected VCS

Git path is completely unchanged — the existing handle_list body is
renamed to handle_list_git with identical behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
Phase 2 of jj workspace support:
- VCS detection at top of handle_switch routes to jj handler
- Switch to existing workspace by name
- Create new workspace with --create (--base maps to --revision)
- Path computation uses same sibling-directory convention as git
- No PR/MR resolution (git-only feature)
- No hooks (git-only for now)
- Git path completely unchanged

Co-Authored-By: Claude <noreply@anthropic.com>
Route remove command to jj handler when in a jj repo. Forgets the
workspace via `jj workspace forget`, removes the directory, and cd's to
default workspace if removing current.

Co-Authored-By: Claude <noreply@anthropic.com>
Squash (default): creates new commit on trunk with combined feature
changes via `jj squash --from`. No-squash: rebases branch onto trunk.
Both modes update the target bookmark and push (best-effort for
co-located repos).

Handles jj's empty working-copy pattern by detecting the actual feature
tip (@- when @ is empty) to avoid referencing abandoned commits.

Co-Authored-By: Claude <noreply@anthropic.com>
Extract current_workspace() and trunk_bookmark() to JjWorkspace, and
share removal logic between merge and remove handlers via
remove_jj_workspace_and_cd().

Co-Authored-By: Claude <noreply@anthropic.com>
28 tests covering list, switch, remove, and merge commands against
real jj repositories. Includes JjTestRepo fixture with ANSI-aware
change ID filters for deterministic snapshots.

Co-Authored-By: Claude <noreply@anthropic.com>
Add `wt step commit/squash/rebase/push` for jj repos with VCS detection
routing. Replace all `trunk()` revset usages in shared helpers with the
resolved target bookmark name, since `trunk()` only resolves with remote
tracking branches.

Co-Authored-By: Claude <noreply@anthropic.com>
jj is not installed on CI runners. Gate the jj test module behind
`jj-integration-tests` feature flag, matching the existing pattern
for `shell-integration-tests`.

Co-Authored-By: Claude <noreply@anthropic.com>
The jj integration tests are behind a feature flag, but their snapshot
files are always present in the repo. On Linux CI, `--unreferenced reject`
catches these as orphaned. Fix by:

- Installing jj-cli on Linux CI (where --unreferenced reject runs)
- Conditionally adding jj-integration-tests feature when jj is available

Co-Authored-By: Claude <noreply@anthropic.com>
RUSTDOCFLAGS='-Dwarnings' treats these as errors. Rust auto-resolves
intra-doc links when the link text matches the type name.

Co-Authored-By: Claude <noreply@anthropic.com>
Install jj-cli on the code-coverage job and enable the
jj-integration-tests feature so jj handler code is covered.

Co-Authored-By: Claude <noreply@anthropic.com>
Remove separate jj-integration-tests feature flag. jj tests now run
under shell-integration-tests alongside shell/PTY tests, gated with
cfg(all(unix, feature = "shell-integration-tests")).

Install jj on macOS CI via brew to match Linux CI.

Co-Authored-By: Claude <noreply@anthropic.com>
- Clean workspace listing (is_dirty clean path)
- Switch without --cd (early return path)
- Remove current workspace without name arg
- Switch --create with --base revision
- List workspace with commits ahead (branch_diff_stats)
- Switch --create when path already exists (error path)

Co-Authored-By: Claude <noreply@anthropic.com>
Exercises all Workspace trait methods on a real git repository,
covering the Workspace for GitWorkspace implementation and
Repository::create_worktree. These thin wrappers had no direct
callers yet (git paths still use Repository directly).

Co-Authored-By: Claude <noreply@anthropic.com>
Two targeted tests for coverage gaps:
- test_jj_list_json: exercises the JSON output path in handle_list_jj
- test_jj_remove_already_deleted_directory: exercises the warning path
  when workspace directory was deleted externally

Co-Authored-By: Claude <noreply@anthropic.com>
Add src/workspace/git.rs to no-direct-cmd-output exclude list (test
fixtures use Command::output() directly). Fix rustfmt formatting in
jj integration test.

Co-Authored-By: Claude <noreply@anthropic.com>
Covers the new jj commit prompt builder function in the llm module.

Co-Authored-By: Claude <noreply@anthropic.com>
Directly calls kind(), has_staging_area(), default_branch_name(),
is_dirty() (both clean and dirty paths), and branch_diff_stats() on
JjWorkspace to cover ~28 lines that aren't reached by normal wt
command flows but are required by the Workspace trait.

Co-Authored-By: Claude <noreply@anthropic.com>
Expand the Workspace trait to be the primary VCS-agnostic interface,
replacing scattered detect_vcs() calls and the GitWorkspace wrapper.

Phase 1: Move LineDiff, IntegrationReason, path_dir_name to workspace::types
Phase 2: Add identity, commit, push methods to Workspace trait
Phase 3: Remove GitWorkspace wrapper, implement Workspace for Repository directly
Phase 4: Make CommandEnv hold Box<dyn Workspace> instead of Repository
Phase 5: Consolidate VCS routing into command modules (merge, step, remove)
Phase 6: Update jj handlers to use trait methods

Additional improvements:
- require_repo() returns Result instead of panicking
- require_git() guard gives clear errors for jj users on git-only commands
- handle_merge_jj respects user config defaults for squash/remove
- JjWorkspace::project_identifier uses git remote URL when available
- current_workspace_path() trait method eliminates downcast in CommandEnv

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@max-sixty
Copy link
Owner Author

(working on improving this, needs lots of work)

max-sixty and others added 8 commits February 14, 2026 00:21
Add advance_and_push and squash_commits as Workspace trait methods,
making step push fully trait-based with zero VcsKind branching.

Git: fast-forward check via is_ancestor, stash/restore target worktree,
local push. Jj: is_rebased_onto guard, bookmark set, jj git push.

Squash: Git uses reset --soft + commit, Jj uses new + squash --from +
bookmark set. Both jj handlers (step squash, merge) now use trait
methods instead of standalone functions.

Deleted: handle_push_jj, squash_into_trunk, push_bookmark.
Extracted: collect_squash_message helper for jj commit message assembly.

Co-Authored-By: Claude <noreply@anthropic.com>
advance_and_push now returns PushResult (commit count + stats summary)
instead of bare usize. Git impl emits progress message, commit graph,
and diffstat to stderr during the push operation, preserving the exact
output ordering (stash → graph → restore → success). Command handler
formats the final success message with stats parenthetical.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace &Repository with &dyn Workspace throughout the hook pipeline,
enabling hooks in jj repositories:

- expand_template: take worktree_map instead of &Repository
- spawn_detached: take log_dir: &Path instead of &Repository
- CommandContext: workspace field replaces repo field, with repo()
  downcast for git-specific operations
- CommandEnv::context() returns CommandContext directly (infallible)
- Workspace trait: add load_project_config, wt_logs_dir,
  switch_previous, set_switch_previous
- handle_switch_jj: full rewrite with hooks, --execute, switch-previous,
  shell integration
- Extract shared expand_and_execute_command for --execute handling

Co-Authored-By: Claude <noreply@anthropic.com>
Remove require_git() guards from all hook commands (run_hook,
add_approvals, clear_approvals, handle_hook_show) and replace
Repository-specific calls with Workspace trait equivalents.

The hook infrastructure was already generalized in prior commits —
this removes the last barrier preventing hooks from running in jj
repos.

Also adds VCS-neutral messaging guidance to the user output skill:
don't mention specific backends unless the context is already
VCS-specific.

Co-Authored-By: Claude <noreply@anthropic.com>
Env var rename (WT_TEST_* → WORKTRUNK_TEST_*), shell integration
hint addition, and jj push behavior changes from main.

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	.github/workflows/ci.yaml
…ring>

Both implementations (git, jj) are infallible — the Result wrapper
added no value and forced callers to .ok().flatten() unnecessarily.

Co-Authored-By: Claude <noreply@anthropic.com>
max-sixty and others added 3 commits February 14, 2026 19:09
Add tests for `prepare_commands()` / `expand_commands()` covering single,
named, template-var, and extra-var cases. Add tests for all 5 match arms
of the `generate_commit_message` fallback path (0, 1, 2, 3, 4+ files).

Simplify `init_test_repo()` to avoid uncoverable assert format-arg lines.

Co-Authored-By: Claude <noreply@anthropic.com>
Extract list_ignored_entries into Workspace trait and implement for both git
and jj backends. Git implementation uses git ls-files directly; jj uses git
ls-files with explicit --git-dir pointing to the git backend. Refactor
step_copy_ignored to work with workspace abstraction instead of git Repository,
enabling support for jj while maintaining git compatibility. Add jj integration
tests covering basic copy, --from flag, and --dry-run cases.
For material changes, add a blank line then a body paragraph explaining the change

Commands now use the workspace trait instead of directly depending on git.
This enables support for multiple VCS backends (git and jj) while maintaining
backward compatibility. Git-specific state (markers, CI cache, hints) is now
conditionally available through downcasting. Log management and branch tracking
work across all supported workspace types.
Comment on lines +167 to +168
"Branch not found"
"Uncommitted changes"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of these really apply to jj, tho.

You are always on a branch, but not a named branch. And the working copy is always committed, there's never uncommitted files (that aren't gitignored).

Under the hood, what jj commit does is a combination of jj describe to update the working copy commit's description, and then jj new, to make a new child commit, and then make that the working copy. It's sugar to be familiar to git users, but it's not necessary to actually commit anything.

max-sixty and others added 2 commits February 15, 2026 00:15
# Conflicts:
#	src/main.rs
The merge from main removed trailing colons from hook status messages.
Update the jj_switch_create_with_hooks snapshot to match.

Co-Authored-By: Claude <noreply@anthropic.com>
@max-sixty max-sixty force-pushed the 926 branch 2 times, most recently from 2c5f433 to 46ffab6 Compare February 15, 2026 09:42
max-sixty and others added 7 commits February 15, 2026 02:53
# Conflicts:
#	src/commands/handle_merge_jj.rs
#	src/commands/merge.rs
#	src/commands/mod.rs
#	src/commands/repository_ext.rs
#	src/commands/select/mod.rs
#	src/commands/step_commands.rs
#	src/workspace/git.rs
#	src/workspace/jj.rs
#	src/workspace/mod.rs
#	src/workspace/types.rs
#	tests/integration_tests/jj.rs
#	tests/snapshots/integration__integration_tests__jj__jj_merge_implicit_target.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_no_remove.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_squash.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_squash_with_directive_file.snap
#	tests/snapshots/integration__integration_tests__jj__jj_step_push_no_remote.snap
#	tests/snapshots/integration__integration_tests__jj__jj_step_squash_already_integrated.snap
Scope branch diff stats to sparse checkout cone, fix skim cleanup bug, rename advance_and_push to local_push, and update CI dependencies.
Replace the git-specific collect_merge_commands + approve_command_batch
pattern with the standard approve_hooks helper that jj merge already
uses. Both VCS paths now follow the same "Approve at the Gate" pattern.

Co-Authored-By: Claude <noreply@anthropic.com>
When removing a jj workspace after merge, compute and display the
integration reason (e.g., "ancestor of main") in the success message,
matching git's removal output style. Pass integration info through the
removal flow so both `wt merge` and `wt remove` can show how the
workspace was integrated into its target.
Add `prepare_commit(path, mode)` to the Workspace trait, replacing
duplicated staging logic (warn about untracked files + git add) across
commit.rs and step_commands.rs with a single trait method call.

- Git: warns about untracked files when StageMode::All, runs git add
- Jj: no-op (jj auto-snapshots the working copy)
- Remove dead `warn_about_untracked` field from CommitOptions
- Remove `warn_if_auto_staging_untracked` from RepositoryCliExt

Co-Authored-By: Claude <noreply@anthropic.com>
Move marker, hint, and switch-previous operations from Repository
git-config methods to the Workspace trait. This enables jj support
while maintaining git compatibility.

Key changes:
- Add trait methods: branch_marker, set_branch_marker, clear_branch_marker,
  list_all_markers, clear_all_markers, has_shown_hint, mark_hint_shown,
  clear_hint, list_shown_hints, clear_all_hints, clear_switch_previous
- Implement trait methods for both Repository (git) and JjWorkspace
- Update state.rs handlers to use workspace trait methods instead of
  git-config downcasts
- Move hints handling from Repository to trait (git config → both VCS)
- Simplify command handlers by removing require_git_workspace calls for
  marker/hint operations
- Update for_each.rs and select.rs to work with generic Workspace
@max-sixty
Copy link
Owner Author

Two commits from this branch were accidentally pushed directly to main:

  • cd8420235 feat: add jj (Jujutsu) support alongside git
  • c4259247f Add VCS-agnostic workspace abstraction with jj support

These have been reverted on main in f0c5d81f9 and 7ed8cdb42. The changes remain on the feature branch (926) and will be reintroduced properly when the PR is merged.

This was written by Claude Code on behalf of max-sixty

max-sixty and others added 14 commits February 15, 2026 20:10
Merges main (which reverted jj support) using ours strategy to preserve
the jj code on this branch, then cherry-picks non-revert commits from main.

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	src/commands/commit.rs
#	src/commands/handle_merge_jj.rs
#	src/commands/handle_remove_jj.rs
#	src/commands/merge.rs
#	src/commands/remove_command.rs
#	src/commands/repository_ext.rs
#	src/commands/step_commands.rs
#	src/workspace/git.rs
#	src/workspace/jj.rs
#	src/workspace/mod.rs
#	tests/snapshots/integration__integration_tests__jj__jj_merge_implicit_target.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_multi_commit.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_no_net_changes.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_no_net_changes_with_remove.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_squash.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_squash_with_directive_file.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_with_no_squash.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_workspace_with_no_user_commits.snap
#	tests/snapshots/integration__integration_tests__jj__jj_merge_zero_commits_ahead_with_remove.snap
# Conflicts:
#	src/commands/config/hints.rs
#	src/commands/config/state.rs
#	src/commands/for_each.rs
#	src/commands/list/mod.rs
#	src/commands/select/mod.rs
#	src/commands/step_commands.rs
#	tests/integration_tests/jj.rs
Resolve conflicts between jj workspace generalization and typed
TemplateExpandError from main. Keep workspace abstraction (worktree_map),
adopt simplified error propagation (plain ? instead of .map_err).

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Documents the planned approach for eliminating ~16 downcast sites
via a Box<dyn VcsOps + '_> trait on Workspace. Records current
progress (prepare_commit unified) and next steps (introduce VcsOps
trait, add guarded_push for merge stash-push-restore pattern).

Co-Authored-By: Claude <noreply@anthropic.com>
Bring in 926's merge-main and cargo-fmt commits. Fix type mismatches
from TemplateExpandError (expand_template now returns structured error
instead of String): add .map_err in format_path and resolve callsites.

Co-Authored-By: Claude <noreply@anthropic.com>
Update snapshot tests and assertion for the new TemplateExpandError
type from #1041. Error messages now use "Failed to expand {name}:"
format instead of "Failed to expand command template".

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	src/commands/list/mod.rs
#	src/commands/select/mod.rs
#	src/main.rs
Co-Authored-By: Claude <noreply@anthropic.com>
Resolve conflicts between workspace abstraction (926) and Approvals
refactoring (main). Key decisions:
- Keep workspace-based API (ctx.workspace instead of ctx.repo)
- Apply Approvals separation (Approvals::load() instead of via config)
- Apply shell_escape::escape over shlex::try_quote
- Apply SwitchSuggestionCtx for --execute error hints
- Fix handle_list to not pass config (collect no longer needs it)
- Fix select/mod.rs auto-merge issues (duplicate config resolution,
  repo scoping for summary generation)

Co-Authored-By: Claude <noreply@anthropic.com>
# Conflicts:
#	src/commands/config/show.rs
The commit generation setup prompt appeared in PTY tests when claude was
installed on the host, causing snapshot mismatches. Add WORKTRUNK_NO_PROMPTS
to the test environment and check it in prompt_commit_generation().

Co-Authored-By: Claude <noreply@anthropic.com>
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.

2 participants