In short: jj refuses if no signing backend is configured. It resolves -r, or the
revsets.sign setting (default reachable(@, mutable())), and checks the commits are
mutable. In one transaction it rewrites them and their descendants: target commits are written with the
sign behavior forced on, optionally with --key; descendants are just reparented, which signs
them only as signing.behavior says. It prints Signed N commits:, warns about commits
authored by someone else, and records sign commit <id>.
The command is cmd_sign. The reverse is
jj unsign. Signatures are also made
automatically when commits are written, depending on signing.behavior, and at push time with
git.sign-on-push; see jj git push.
How commits are stored is on the commits page.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
-r, --revision (alias --revisions) | revsets, repeatable | revsets.sign | What to sign. Always re-signed: jj can't check whether a commit is already signed by you without creating a signature (source). |
--key KEY | string | signing.key | The key to sign with, passed to the backend (source). |
2. Configuration
| Key | Default | Effect |
|---|---|---|
signing.backend | "none" | Which backend signs; with none, jj sign refuses (default). |
signing.behavior | "keep" | What every commit write does about signatures, including the rebased descendants here (§6). |
signing.key | unset | Default key; --key overrides it. |
signing.backends.gpg, .gpgsm, .ssh | programs gpg, gpgsm, ssh-keygen | Backend-specific settings (defaults). |
revsets.sign | "reachable(@, mutable())" | What to sign without -r (default, test). |
3. End-to-end flow
- Backend check. The store's signer can sign only if a main backend is configured
(
Signer::can_sign, source). - Targets.
-rvalues are unioned; without any,revsets.signis parsed. Immutable commits are refused before anything is written (source). - Rewrite.
transform_descendantsvisits the targets and all their descendants, parents first, with new parents already filled in (source). A progress bar shows the change being signed (source). - Report and finish. See §5 and §7.
4. Core architecture touchpoints
- Commit writing: every commit write decides whether to sign with
write_to_store: sign only if the signer can sign andshould_signsays so, then pass a signing function to the backend. - Git backend: the backend serializes the commit, signs those bytes, and adds the signature as a header of the Git commit object, so the commit ID changes (source); see the commits page.
- Signer: wraps the main backend plus the others used for verifying, with a cache
(
Signer). - Rewrites: each signed commit is a rewrite of the old one, so bookmarks follow and the
working copy moves if
@was signed.
5. Transaction and operation
- Transaction: one, covering all targets and descendants.
- Operation description:
sign commit <id>, orsign commit <first id> and <N> more, using the new commit IDs (source). - No targets:
-r 'none()'is a valid no-op that printsNothing changed.(test). jj undorestores the unsigned commits.
6. Algorithm and invariants
Sign behaviors
SignBehavior and
SignSettings::should_sign decide per commit. "Yours"
means the commit's author email equals user.email. With the default keep, a signed
descendant you authored is signed again when jj sign rebases it; an unsigned one stays unsigned
(test with drop).
Invariants: every target is signed afterwards, whoever authored it. Commit contents, descriptions and authors are unchanged; the committer timestamp and the commit IDs change. No immutable commit is touched.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
No signing backend configured, with a hint linking the commit-signing docs | signing.backend is none. | sign.rs, test |
Warning: N of these commits are not authored by you. | Some targets have another author email; they are still signed. | sign.rs, test |
| Immutable-commit error | A target is immutable. | sign.rs |
Signing error from the backend | The signing program failed. | signing.rs |
Re-signing on every run is deliberate but costly with hardware keys: upstream issue #5786 tracks skipping commits already signed by you (source).
8. Worked example
From upstream's test_sign, with a test signing backend and
behavior = "keep". Three commits one, two, three and an empty
@ are unsigned. jj sign -r ..@ signs all four, so every commit ID changes:
Upstream's output (source):
Signed 4 commits:
qpvuntsm 7fb98da0 (empty) one
rlvkpnrz 062a3c5a (empty) two
kkmpptxz d2174a79 (empty) three
zsuskuln 8d7bc037 (empty) (no description set)
Working copy (@) now at: zsuskuln 8d7bc037 (empty) (no description set)
Parent commit (@-) : kkmpptxz d2174a79 (empty) three
Running the same command again signs all four again, with new IDs (source).
There is no Rebased line because every descendant was itself a target.