Using jujutsu (jj) with Soft Serve + custard
jujutsu (jj) is a Git-compatible VCS that uses git as its
storage backend. This means it works naturally with Soft Serve and custard — no special server
support required. Replace your-host with your forge domain throughout.
What works unchanged
jj stores its history in a standard git repo. jj git clone and jj git push speak plain git to
Soft Serve over SSH (port 23231). Once pushed, every object — commits, trees, blobs, branches —
is ordinary git data. custard reads that data the same way it reads any git repo: commits show in
the log, file trees browse normally, blobs highlight with syntax color, refs list cleanly.
custard never knows whether a repo was authored in jj, stock git, or any other tool. There is nothing to configure on the forge side.
# clone via jj
jj git clone soft:<repo>
# push changes
jj git push --branch main # or --all
Concept mapping
| jj concept | What lands in git / custard |
|---|---|
| commit | git commit — appears in log, tree, blob views |
| bookmark | git branch — appears on the refs page under Branches |
jj describe |
edits the commit message of the working commit |
jj git push --bookmark |
pushes a branch to Soft Serve |
change ID (abc123...) |
local-only; never pushed, never visible in custard |
| op log | local-only; the forge has no concept of it |
| annotated tag | not supported (see below) |
jj change IDs and the operation log are entirely local. They exist inside your .jj/ directory and
are never transmitted over git push — so custard never sees them and has no way to display them.
The tag gap
jj cannot create or move annotated git tags. This matters because custard's Homebrew release pipeline is triggered when a tag is pushed (via a Soft Serve webhook), and the version pill on a repo's page normally reads from the highest semver tag.
For a pure-jj repo the fix is bookmark mode: instead of tagging a commit to publish a release,
you create or move a release/vX.Y.Z bookmark (a plain git branch) to the commit you want to
ship, then push it. custard detects the bookmark push and treats it as a release.
Tag-based repos are unaffected — bookmark mode is opt-in.
Cutting a release in bookmark mode
One-time setup
Add brew.source: bookmark to .custard.yaml in the repo:
brew:
enabled: true
package: "." # or ./cmd/<name>
source: bookmark # opt in to bookmark-mode releases
# bookmark_prefix: release/ # default; change only if you need a different prefix
Register the release webhook (same as tag mode):
ssh soft repo webhook create <name> https://your-host/hooks/release \
-e branch_tag_create -c json -s "$WEBHOOK_SECRET"
Per-release workflow
# 1. Finish the commit that will ship
jj describe -m "Add the new feature"
# 2. Create or move the release bookmark to the current working commit
jj bookmark set release/v1.2.0
# 3. Push the bookmark
jj git push --bookmark release/v1.2.0
The webhook fires on the branch push. custard strips the prefix (release/) to extract the
version (v1.2.0), builds the tarball, writes the formula to the homebrew-tap repo, and publishes.
The version pill on the repo's page shows v1.2.0, labelled with the tip commit's subject line (jj
bookmarks carry no annotated message — the commit subject stands in).
Versions must be vMAJOR.MINOR.PATCH (no pre-release suffixes).
Installing
brew tap you/tap https://your-host/git/homebrew-tap.git
brew trust you/tap
brew install <name>
brew upgrade <name>
How each jj operation shows up in custard
| jj operation | Effect in custard |
|---|---|
jj commit / jj describe + push |
new commit(s) appear in log; tree/blob views update |
jj bookmark set <name> + push |
branch appears (or updates) on the refs page |
jj bookmark set release/vX.Y.Z + push |
release publishes; version pill and tap formula update |
history rewrite + jj git push --force |
custard reads the new state (read-only, safe) |
jj git push --all |
all bookmarks land on Soft; all show on the refs page |
| change ID / op log | local-only; never visible in custard |
What's intentionally not supported
No auto-detection. custard cannot infer from the objects in a repo that it was authored with
jj. Bookmark mode must be enabled explicitly with brew.source: bookmark in .custard.yaml.
One mode at a time. A repo is in either tag mode (default) or bookmark mode. Switching is a one-way config flip — see MIGRATION.md for the procedure. After the flip, the tag path stops triggering releases and the bookmark path takes over. The two modes never mix, and there is no cross-mode version comparison.
Scope limit — the index list. The version shown for each repo on the forge's main index (repo
list) is always derived from the highest semver git tag. That code path does not read .custard.yaml
and is not mode-aware. For a pure-jj repo this means the index list shows no version; the
mode-aware version pill lives on each repo's own page and refs page, which is the primary surface.
This is a deliberate tradeoff and is not expected to change unless the index read is refactored.