jj / How jj edit works

How jj edit works

jj edit makes an existing commit the working-copy commit, so the files on disk become that commit's contents and later changes are snapshotted straight into it. It creates no commit and rewrites no history: it only moves @.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. 4. Core architecture touchpoints
  5. 5. Transaction and operation
  6. 6. Algorithm and invariants
  7. 7. Errors and edge cases
  8. 8. Worked example

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

FlagTypeDefaultMeaning
REVSET (positional)revsetrequiredThe commit to edit. It must resolve to exactly one commit.
-r REVSETrevsetnoneHidden alias of the positional argument. The two are in one required clap group, so exactly one must be given.

EditArgs

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

jj edit: call flow cmd_edit loads the workspace through the shared CLI lifecycle, which snapshots the current working copy. It resolves the revision with resolve_single_rev, which fails unless exactly one commit matches, and checks the commit is rewritable. If the commit is already the working-copy commit it prints a message and stops. Otherwise it starts a transaction and calls edit, which runs MutableRepo::edit: possibly abandon the old working-copy commit, add the target as a head, and set it as this workspace's working-copy commit. finish records the operation "edit commit" and the shared lifecycle checks out the new commit's tree. Shared lifecycle: load workspace and repo, snapshot the working copy (CLI page) resolve_single_rev exactly one commit, or an error check_rewritable([id]) refuses immutable commits already @? "Already editing that commit." — no operation start_transaction / tx.edit MutableRepo::edit 1. maybe abandon the old @ 2. add_head(target) 3. set_wc_commit(workspace, target) tx.finish(ui, "edit commit <id>"): write operation, publish op head, check out the target's tree into the working copy (CLI page)

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Target commitreadResolved from the revset. It isn't modified, but from now on snapshots will rewrite it.commits
View.wc_commit_idswriteThis workspace's entry now names the target.view
View.head_idswriteThe 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 commitmaybe abandonedOnly if it is discardable (empty with no description), a head, and not referenced by a bookmark, tag or other workspace.transactions
OperationwriteOne operation with description edit commit <full commit id>.operations
Working copy (TreeState)read, then writeSnapshotted 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 undo moves @ 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

MessageWhenSource
Revset `…` didn't resolve to any revisionsThe revset is empty.revset_util.rs
Revset `…` resolved to more than one revisionSeveral commits match. A hint lists up to five, then ....revset_util.rs
Commit … is immutable / The root commit … is immutableThe 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 providedNeither 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:

jj edit B: before and after Before: commits A, B and C in a line, with an empty working-copy commit W on top of C. After jj edit B, the working copy is B and W has been abandoned because it was empty, undescribed, a head and unreferenced. No commit was rewritten. before after A B C W @ A B @ C W abandoned 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.