feat: add Jujutsu (jj) support via VCSOperations abstraction#496
Draft
jucor wants to merge 8 commits intoejoffe:masterfrom
Draft
feat: add Jujutsu (jj) support via VCSOperations abstraction#496jucor wants to merge 8 commits intoejoffe:masterfrom
jucor wants to merge 8 commits intoejoffe:masterfrom
Conversation
8194d10 to
c1d1e8c
Compare
When spr detects a jj-colocated repo (.jj/ directory present), it uses jj commands for history-rewriting operations instead of git rebase. This preserves jj change IDs across spr update/merge/amend/edit cycles. Key changes: - New vcs/ package with VCSOperations interface abstracting the 7 operations where git and jj differ (FetchAndRebase, GetLocalCommitStack, AmendInto, EditStart/Finish/Abort, PrepareForPush) - GitOps: pure extraction of existing git logic (no behavior change) - JjOps: jj-native implementation using jj describe, jj rebase, jj squash, jj edit (preserves change IDs) - Auto-detection: .jj/ directory triggers jj mode - Opt-out: --no-jj flag, SPR_NOJJ env var, or noJJ: true in ~/.spr.yml - 31 new tests, all existing tests pass unchanged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> commit-id:6bdd63bc
commit-id:44dc4bb5
GetInfo previously called git.GetLocalCommitStack internally, bypassing the VCSOperations abstraction. In jj mode this panicked because git rebase cannot add commit-id trailers to jj commits. Now all callers provide commits via vcsOps.GetLocalCommitStack, and GetInfo receives them as a parameter. Also adds CheckStackCompleteness to warn when @ has descendants in jj mode (commits above @ would be excluded from trunk()..@), prompting for confirmation before proceeding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> commit-id:0e2e9f53
JjCmd.Jj() uses strings.Fields to split args, which breaks template strings containing spaces (e.g. -T 'commit_id ++ ...'). Switch to JjArgs which passes pre-split arguments directly to exec.Command, bypassing shell quoting entirely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> commit-id:95f2de31
Removed JJ_CONFIG= from jj command execution. Setting this env var to empty caused jj to skip loading user config, resulting in empty committer name/email on every rebase and describe operation. commit-id:d56062ca
When GetLocalCommitStack encounters a commit without a commit-id trailer and jj describe fails (e.g. because the commit is immutable due to untracked remote bookmarks), spr now panics with an actionable message: which commit is affected, why it is likely immutable, and how to fix it (jj bookmark track). Also adds error response support to the jj mock and injects the commit-id generator for test determinism. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EditStart used MustJj() with shell-quoted template arguments like -T 'id.short(16)'. MustJj() splits with strings.Fields which passes the quotes as literal characters to jj, causing the template to be echoed as-is instead of evaluated. This resulted in op_id=id.short(16) in the edit state file, making EditAbort fail. Switch to JjArgs() which passes pre-split arguments directly to exec.Command, matching the fix already applied to GetLocalCommitStack. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EditCommit hardcoded "git spr" in its output messages. When invoked via "jj spr edit", the instructions told users to run "git spr edit --done" which is confusing. Add CommandName() to the VCSOperations interface, returning "git spr" or "jj spr" depending on the implementation. Use it in all edit session messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi @ejoffe
Full transparency: I have used Claude Code (Opus 4.6 extended) a lot for this PR, just as I do for my own work nowadays. I held it on a tight leash, and will be beta-testing intensively on the large stack of commits I have in my current main work repo. This PR solves a real need I have for work.
However, I am accutely aware of the hot debate about using AI in open-source, and the risk of unwelcome "drive-by PRs". I do not want to be a bother: if you do not want AI-generated code here, please do feel free to say so, and I can just keep using my fork in my corner! That's also why I tag this PR as "draft". It's functional, just wanted to discuss first.
Summary
Adds Jujutsu (jj) support to spr. When a
.jj/directory is detected (jj-colocated repo), spr uses jj-native commands for history-rewriting operations instead ofgit rebase. This preserves jj change IDs across all spr operations.Key changes:
vcs/package with aVCSOperationsinterface abstracting the 7 operations where git and jj differGitOps: pure extraction of existing logic (zero behavior change for git-only repos)JjOps: jj-native implementation usingjj describe,jj rebase,jj squash,jj edit.jj/directory, with opt-out via--no-jjflag,SPR_NOJJenv var, ornoJJ: truein~/.spr.ymljj-setupcommand to register ajj spraliasMotivation
jj is gaining traction as a git-compatible VCS with an updated mental model (immutable commits, change IDs that survive rewrites). spr is the best stacked-PR tool for squash-merge workflows on GitHub. But currently, every
spr updaterunsgit rebase, which destroys jj change IDs in a colocated repo — making the two tools painful to combine.This PR makes them work together cleanly: spr's commit-id trailers + PR management coexist with jj's change IDs and operation model.
There exists a work-in-progress jj-spr tool, but it is lacking several features (need 4 manual commands for each merge or the stack breaks), so I figured it was probably better to adapt the battle-tested spr to
jjrather than have a new project from scratch.How it works
git fetch+git rebase --autostashjj git fetch+jj rebase -b @ -d main@origingit rebase -iwithspr_reword_helperjj describe -r <change-id>git commit --fixup+git rebase -i --autosquashjj squash --into <change-id>git rebase -iwitheditstopjj edit <change-id>git stash/git stash popGit push, branch management, and all GitHub API calls remain unchanged.
Backward compatibility
NewStackedPRconstructor acceptsvcsOpsas a variadic parameter — existing callers work without modification.jj/exists; plain git repos behave identically to before-race, zero modifications neededcommit-idtrailer format is unchanged (commit-id:XXXXXXXX)Test plan
go test -race ./...)