# Using jujutsu (jj) with Soft Serve + custard [jujutsu](https://github.com/martinvonz/jj) (`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. ```sh # clone via jj jj git clone soft: # push changes jj git push --bookmark main # or --all ``` `jj git clone` names the remote `origin` (pointing at `soft:`), the same as stock git. Keep it named `origin`: custard's `release` verb pushes with `git push origin`, so the remote must be `origin` for releases to work. (`soft:` here is just the SSH host alias for the clone URL — not the remote's name.) ## 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's *native* commands 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. **Most jj repos here don't hit this gap.** If your repo is **colocated** (a `.git` alongside `.jj` — the default `jj git init --colocate` layout), plain `git` still works in it, so the `custard release vX.Y.Z` CLI — which runs `git tag` + `git push` — releases exactly like it does in a git repo. **Stay in the default `source: tag`.** This is the recommended path; reach for bookmark mode only if you have a reason not to run `git tag` at all. Bookmark mode is the fix for the case where you *won't* touch git tags — a non-colocated jj repo, or a workflow that releases purely through jj. Instead of tagging, 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. > **Don't cross the streams.** `custard release` *always tags*, regardless of `source`. In a > `source: bookmark` repo it will tag and push successfully and print `✓ released` — but the server > ignores tag pushes in bookmark mode, so nothing publishes. Pick one: **tag mode + `custard release`**, > or **bookmark mode + `jj bookmark set release/… && jj git push`**. Never bookmark mode + the CLI. 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: ```yaml brew: enabled: true package: "." # or ./cmd/ source: bookmark # opt in to bookmark-mode releases # bookmark_prefix: release/ # default; change only if you need a different prefix # must end in "/" so the version is its own path segment ``` Register the release webhook (same command as tag mode; see [MANUAL.md](MANUAL.md) for webhook secret generation and registration details): ```sh ssh soft repo webhook create https://your-host/hooks/release \ -e branch_tag_create -c json -s "$WEBHOOK_SECRET" ``` ### Per-release workflow ```sh # 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 ```sh brew tap you/tap https://your-host/git/homebrew-tap.git brew trust you/tap brew install brew upgrade ``` ## 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 ` + 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](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.