In short: jj resolves the revision to exactly one commit, refuses if that commit is immutable, and
(unless it is already @) records one operation, edit commit <id>, that
points this workspace's wc_commit_ids entry at it. If the commit you leave is empty,
undescribed, a head and not referenced elsewhere, it is abandoned in the same operation. Then the
shared lifecycle checks out the new commit's tree. The help text itself recommends jj new
followed by jj squash instead, so you aren't editing a finished commit directly.
The command is 27 lines: cmd_edit. Everything
interesting happens in shared code: resolve_single_rev, check_rewritable,
MutableRepo::edit (also used by
jj new), and the
CLI lifecycle.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
REVSET (positional) | revset | required | The commit to edit. It must resolve to exactly one commit. |
-r REVSET | revset | none | Hidden alias of the positional argument. The two are in one required clap group, so exactly one must be given. |
2. Configuration
jj edit reads no command-specific settings. The settings that affect it come through the
shared machinery: revset-aliases."immutable_heads()" decides what counts as immutable,
templates.commit_summary formats the commits listed in errors, and the working-copy
settings on the working-copy page govern the
snapshot and checkout.
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Target commit | read | Resolved from the revset. It isn't modified, but from now on snapshots will rewrite it. | commits |
View.wc_commit_ids | write | This workspace's entry now names the target. | view |
View.head_ids | write | The target is made reachable from the heads (add_head), which makes a hidden commit visible again. An abandoned old @ stops being a head. | view |
| Old working-copy commit | maybe abandoned | Only if it is discardable (empty with no description), a head, and not referenced by a bookmark, tag or other workspace. | transactions |
| Operation | write | One operation with description edit commit <full commit id>. | operations |
Working copy (TreeState) | read, then write | Snapshotted at the start. After the operation commits, updated to the target's tree, rewriting files on disk. | working-copy |
5. Transaction and operation
- Transaction: one, only when the target isn't already
@(source). - Operation description:
edit commit <id>, with the full hex commit ID. - Commits created or rewritten: none. The only possible history change is abandoning the old working-copy commit, and since nothing is rewritten, no descendants are rebased.
- Working copy: snapshotted when the workspace is loaded, so pending edits are saved into the commit you're leaving before you leave it. After the operation commits, the target's tree is checked out.
jj undomoves@back and restores an abandoned old@. The next command then checks out the old commit's files again.
6. Algorithm and invariants
The decision is just two checks. The target must be mutable: editing a commit means
every later snapshot rewrites it, so check_rewritable rejects anything in
immutable_heads():: and the root commit before any transaction starts. Editing
@ is a no-op, and no operation is recorded.
Everything else is MutableRepo::edit, which
jj new shares. First
maybe_abandon_wc_commit decides whether the commit
being left can be thrown away. It must be discardable (empty with no description), not the working copy
of another workspace, not pointed at by a local bookmark or tag, and a head. Then
add_head makes sure the target is visible, and set_wc_commit records it. The
invariant this keeps is that an empty, unnamed working-copy commit never piles up when you jump around:
leaving it behind cleans it up, but only when nothing else could still want it.
Because add_head accepts hidden commits, jj edit with the ID of an abandoned or
rewritten-away commit brings it back into view and makes it the working copy.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Revset `…` didn't resolve to any revisions | The revset is empty. | revset_util.rs |
Revset `…` resolved to more than one revision | Several commits match. A hint lists up to five, then .... | revset_util.rs |
Commit … is immutable / The root commit … is immutable | The target is immutable, with hints about immutable_heads(). | cli_util.rs |
Already editing that commit. | Status message (not an error) when the target is already @. No operation is written. | edit.rs |
| clap: a required argument was not provided | Neither a positional revision nor -r was given. | edit.rs |
8. Worked example
Start with A ← B ← C, and a fresh, empty working-copy commit W on top of
C. Running jj edit B:
The files on disk now show B's tree. Any edit you make and the next command's snapshot
rewrite B into a new commit with the same change ID, and C is rebased onto it
automatically, possibly picking up conflicts. If W had a description, a bookmark, or
another workspace on it, it would have been kept.