Skip to content

feat: add jj (Jujutsu) VCS support#1002

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

feat: add jj (Jujutsu) VCS support#1002
max-sixty wants to merge 49 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 17 commits February 14, 2026 12:38
Clarify that Worktrunk supports git and jj, with guidance on keeping the Workspace trait signatures simple and tied to actual implementation requirements rather than hypothetical backends.
The jj push output includes git commit hashes (hex, e.g., "26009b197b6c")
which vary between runs. The existing snapshot filters only matched
pure-alpha jj change IDs ([a-z]{12}), missing hex hashes with digits.

Add a [0-9a-f]{12} filter after the alpha-only filters so change IDs
are still caught first, and hex commit hashes are redacted as
[COMMIT_HASH].

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the global BASE_PATH mechanism with std::env::set_current_dir().
This simplifies path handling by leveraging the OS-level current working
directory instead of maintaining a separate base path variable. The -C flag
now changes the actual working directory, which affects all code (git, jj,
etc.) that uses relative paths or current_dir(). Remove set_base_path()
export and related static initialization.
Merge VCS-specific squash implementations into a single `do_squash()` function
in `step_commands.rs`. This eliminates code duplication and enables both git
and jj to share message generation, LLM prompting, and outcome handling.

Changes:
- Extract `do_squash()` core: count commits, generate message, execute squash
- Add `Workspace` trait methods for VCS-agnostic diff/prompt data:
  `feature_head()`, `diff_for_prompt()`, `recent_subjects()`
- Change `squash_commits()` return type from `String` to `SquashOutcome` enum
  (handles both success and "no net changes" cases)
- Remove jj-specific `handle_squash_jj()` and `collect_squash_message()`
- Update `handle_merge_jj()` to call shared `do_squash()` instead
- Enable `--show-prompt` for jj (now trait-based, not git-only)
- Update `remove_jj_workspace_and_cd()` signature to include hook control flags
Consolidate workspace detection and VCS routing into `open_workspace()`, eliminating the pattern of calling both `detect_vcs()` and `Repository::current()` separately. Commands now open the workspace once and downcast to `Repository` when git-specific features are needed.

This reduces boilerplate across command handlers while improving error consistency. `CommandEnv` methods now accept pre-opened workspaces, and `require_git_workspace()` replaces the old two-step `require_git()` + `Repository::current()` pattern.
…tests

jj config get doesn't accept --repo (unlike config set). This caused
switch_previous() to silently fail on jj 0.38+, making `wt switch -`
always error with "No previous workspace to switch to" in jj repos.

Also adds 5 integration tests for uncovered jj code paths:
- switch-previous, merge no-net-changes, merge no-squash,
  merge zero-commits-ahead, switch-to-current-workspace

Co-authored-by: Claude <noreply@anthropic.com>
- Merge at-trunk with removal (NoCommitsAhead + workspace removal)
- Merge no-net-changes with removal (NoNetChanges + workspace removal)
- Switch records previous workspace for `wt switch -`

Co-authored-by: Claude <noreply@anthropic.com>
Extend workspace/git.rs unit test to exercise commit(), switch_previous(),
set_switch_previous(), and advance_and_push() through the Workspace trait
interface. Add jj integration tests for merge with implicit target
(trunk_bookmark resolution) and step commit with empty description (fallback
message generation). Extend jj workspace trait test with feature_tip,
commit, commit_subjects, resolve_integration_target, wt_logs_dir,
set_switch_previous(None), and is_rebased_onto.

Co-Authored-By: Claude <noreply@anthropic.com>
Add 4 new jj integration tests:
- switch --create with hooks (post-create, post-switch, post-start)
- switch existing with hooks (post-switch)
- switch --create with --execute
- switch existing with --execute

Fix CI test failure: add repo-local git user.name/email config
for test_workspace_trait_on_real_repo (CI runners lack global config).

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Each command handler now calls open_workspace() once at the top, then
uses downcast_ref::<Repository>() to route git vs jj paths. This
eliminates scattered detect_vcs() calls and redundant Repository::current()
calls across 9+ command entry points.

Key changes:
- Add CommandEnv::with_workspace() constructors that accept pre-opened workspace
- Replace require_git() with require_git_workspace() (downcast-based)
- Move project_id resolution from main.rs into list/select handlers
- Simplify main.rs by passing raw CLI flags to handlers
- Fix jj step push panic: add .context() for multiline errors

Co-Authored-By: Claude <noreply@anthropic.com>
Part 1: Replace parallel build_commit_prompt/build_jj_commit_prompt and
generate_commit_message paths with single VCS-agnostic functions. Add
CommitInput struct and committable_diff_for_prompt trait method. Unify
--show-prompt to work for both git and jj.

Part 2: Add hook approval and execution (pre-merge, post-merge) to jj
merge handler, bringing it to feature parity with git merge.

Part 3: Add PostSwitch hook execution when removing the current jj
workspace, fixing gap where it was approved but never executed.

Co-authored-by: Claude <noreply@anthropic.com>
Add coverage for 15+ previously untested Workspace trait methods in
the git implementation: root_path, current_workspace_path, current_name,
project_identifier, feature_head, recent_subjects, load_project_config,
wt_logs_dir, resolve_integration_target, is_rebased_onto, diff_for_prompt,
committable_diff_for_prompt, and both rebase_onto code paths (fast-forward
and diverged). Adds git_at helper for linked worktree git commands.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace git-only require_git_workspace in config create --project with
VCS-agnostic workspace.current_workspace_path(). Implement
JjWorkspace::recent_subjects() via jj log so LLM commit messages
include recent commit style context, matching git behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
Consolidate jj removal logic into remove_command.rs, eliminating
handle_remove_jj as a public entry point. Extract approve_remove_hooks
as a shared helper used by both VCS backends. Update test snapshots to
reflect streamed git output during worktree creation.
Add two more test scenarios to test_workspace_trait_on_real_repo:
- advance_and_push with dirty target worktree: exercises the
  stash_target_if_dirty → push → restore_stash path
- rebase_onto with conflicting changes: exercises the RebaseConflict
  error path when both branches modify the same file

Also derive Debug on RebaseOutcome (required by unwrap_err in test).

Co-Authored-By: Claude <noreply@anthropic.com>
… coverage

- IntegrationReason::symbol() and description()
- LineDiff::is_empty() and From conversions
- path_dir_name()
- ProjectConfig::ci_platform(), load_from_root() (valid, invalid TOML, unreadable)
- ProjectListConfig::is_configured()
- CommandContext Debug format
- build_worktree_map() via git workspace test
- Simplify test assertion format strings to reduce uncovered lines

Co-Authored-By: Claude <noreply@anthropic.com>
max-sixty and others added 2 commits February 14, 2026 18:42
…ace_path fallback

- Fix CI: add command_executor.rs to no-direct-cmd-output exclude list
  (test fixtures use std::process::Command for git init, matching existing
  exclusions for config/test.rs and workspace/git.rs)
- Add 7 unit tests for CommandContext and build_hook_context covering:
  repo() downcast, branch_or_head(), project_id(), commit_generation(),
  hook context variable expansion, detached HEAD, and git-specific fields
- Add workspace_path directory-name fallback test (exercises lines 63-65)
- Add feature_head trait dispatch test

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

1 participant