In short: with no paths and no -i, jj commit is jj describe
followed by jj new, in one operation. It rewrites @ (same change ID) with the
chosen description and the selected tree. Then it creates a new, undescribed commit on top
whose tree is the full, original working-copy tree, and moves every workspace that was on the old
@ there. So the files on disk never change: unselected changes simply show up as the new
@'s diff. The operation is commit <old @ id>.
The command is cmd_commit. It shares its description
handling with jj describe, its final
step with jj new and
jj edit (MutableRepo::edit),
and its change selection with jj split. Its own help text lists how it differs from
split (source): it isn't interactive by default, has
no -r, doesn't move bookmarks forward, and can't send the selection elsewhere. The
default alias ci comes from misc.toml.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
FILESETS (positional) | filesets, repeatable | all files | Keep only changes to these paths in the committed commit. The rest move to the new @. With none, the matcher is FilesetExpression::all() (source, parse_file_patterns). |
-i, --interactive | bool | false | Choose the changes to keep in a diff editor (source). |
--tool NAME | string | none | Use this diff editor. Implies --interactive (source). |
-m, --message MESSAGE | string, repeatable | none | Use this description and don't open the editor. Several values are joined as paragraphs, exactly as in describe (source). |
--editor | bool | false | Open the editor even with -m (source). |
Unlike describe, there is no --stdin and no revision argument: the target is
always this workspace's @ (source). The editor opens
when there's no -m, or when --editor is given
(line 139).
2. Configuration
| Key | Default | Effect |
|---|---|---|
ui.editor | nano / Notepad | The description editor. Precedence with $VISUAL, $EDITOR and $JJ_EDITOR is as on the describe page. |
templates.draft_commit_description | builtin_draft_commit_description | The text the description editor starts with, rendered against a hidden temporary commit that already has the selected tree, so its JJ: file list shows only what is being committed (source). |
templates.commit_trailers | empty | Trailers appended to the description, with the same empty-description rule as describe (source). |
ui.diff-editor |
unset; falls back to :builtin |
The diff editor for -i when --tool isn't given
(diff_editor, diff_selector). When unset,
jj uses the built-in editor and prints a hint saying so
(source). External tools are configured under
[merge-tools.<name>].
|
ui.diff-instructions | true (source) | Whether the diff editor is given the instruction text "You are splitting the working-copy commit: …" (source). |
experimental-advance-branches.enabled-branches / .disabled-branches |
both empty (source) |
Name patterns of bookmarks that advance automatically. With no enabled patterns the feature is off
(load_advance_bookmarks_matcher). When it is on,
matching bookmarks on @'s parents move to the committed commit
(get_advanceable_bookmarks).
|
user.name, user.email | unset | Committer of the rewritten commit, and author and committer of the new @. |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
Old @ | read, then rewritten | Rewritten into the committed commit: same change ID, parents and author, the selected tree, the new description, and you as committer now. | commits |
Parent tree of @ | read | The base the selection is built on (commit.parent_tree, source). For a merge, this is the merged parents' tree. | trees |
New @ | created | A new change on top of the committed commit, with the old @'s full tree, an empty description, and fresh author and committer signatures (for_new_commit). | commits |
| Temporary commit | written, hidden | Only when the editor opens: it carries the draft so the description template can render its diff summary. | describe |
View.wc_commit_ids | write | Every workspace whose @ was the old commit now points at the same new @ (workspaces_for_wc_commit_id). | view |
View.head_ids | write | The new @ becomes a head, and the old commit stops being visible. | view |
View.local_bookmarks | write | Bookmarks on the old @ follow the rewrite to the committed commit, not the new @. With advance-bookmarks enabled, matching bookmarks on @'s parents move to the committed commit too. | view |
| Operation | write | One operation, commit <old @ id>. | operations |
Working copy (TreeState) | read, then rewritten | Snapshotted at the start. The state is then reset to the new @, which has the same tree, so file contents don't change. | working-copy |
5. Transaction and operation
- Transaction: one, always. Even
jj commit -m ""on an empty@records an operation, because a new commit is created (source). - Operation description:
commit <id>, the full hex ID of the working-copy commit before the command (source). -
Commits created or rewritten: the old
@is rewritten, and one new commit is created. If the old@had descendants, which is possible but unusual,finishrebases them onto the committed commit. With a partial selection, that commit lacks the unselected changes, so the rebase may conflict. - Working copy: snapshotted first, so everything you've edited is included. After the operation, the new
@is checked out. Its tree equals the snapshot, so nothing on disk changes. jj undorestores the previous view: the old@comes back as@with its old description and all its changes, and the two new commits are hidden.
6. Algorithm and invariants
Choosing what to commit
DiffSelector::select works on two trees:
before is @'s parent tree, and after is @'s tree. It
first runs restore_tree, which starts from before and copies in every path the
matcher accepts from after. With no paths, the result is simply @'s tree.
Non-interactive selection stops there. Interactive selection then opens the diff editor on
before → that result, with the instruction text. The tree you leave on the right side is what
gets committed. If there's nothing to choose from, it prints Empty diff - won't run diff
editor. and skips the editor.
Building the description
- Start from the
-mparagraphs, or from@'s current description (source). -
Add trailers through
add_trailers, unless the description is empty and no editor will open. That would give the commit a description made only of trailers (source). -
If the editor is used, write a hidden temporary commit with the selected tree and the draft text,
render
templates.draft_commit_descriptionfor it, and edit it withedit_description. If the result is empty, jj hints that you canjj undo, or describe the parent withjj desc @-(source). The commit still goes ahead.
Rewriting and moving on
rewrite_commit(@).detach() takes the commit's fields and sets the selected tree and the
description. It is then written with write, which records the old @ as
rewritten (source, write).
Then, for the workspaces whose @ was the old commit, jj:
- creates one new commit whose parent is the committed commit and whose tree is the old
@'s full tree (commit.tree(), not the selection); - advances eligible bookmarks to the committed commit (a no-op unless the experimental setting is on);
- calls
MutableRepo::editfor each of those workspaces, pointing them all at that new commit (source).
Because the new @'s tree is the snapshotted tree, the diff between the committed commit
and the new @ is exactly the unselected changes.
Invariants: the working-copy files are never modified; the committed commit keeps
@'s change ID, so it is the "same change" you were working on; the new @ is
always a new change; and every workspace that shared the old @ ends up on the same new
@.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
This command requires a working copy | This workspace has no working-copy commit, for example after jj workspace forget. | commit.rs |
Warning: The given paths do not match any file: … | Paths were given but the selected tree equals the parent tree. The command still runs, committing an empty change. | commit.rs |
Empty diff - won't run diff editor. | Interactive mode with nothing to select. | cli_util.rs |
Hint: The commit message was left empty. … | The editor returned an empty description. The commit still goes ahead. | commit.rs |
Hint: Using default editor ':builtin'; run `jj config set --user ui.diff-editor :builtin` to disable this message. | Interactive mode with no ui.diff-editor configured. | merge_tools/mod.rs |
Failed to edit description, Trailers should be valid utf-8 | As on the describe page, minus the bulk-file errors (commit only ever edits one description). | description_util.rs |
cmd_commit makes no check_rewritable call of its own, unlike
describe. A @ that is a merge stays a merge: the committed
commit keeps all its parents, and the new @ has the committed commit as its only parent.
8. Worked example
Upstream's test_commit_paths: a
fresh repository whose @ (change qpvuntsm) has two new files,
file1 and file2. Running jj commit -m=first file1:
Upstream checks the result: jj diff -r @- shows only Added regular file file1,
and jj diff shows only file2. The change IDs are the deterministic ones
upstream's test environment assigns. In the companion
test_commit_paths_warning, the path
file3 matches nothing. That produces the warning, an empty first commit as
@-, and both files left in the new @:
Warning: The given paths do not match any file: file3
Working copy (@) now at: rlvkpnrz 4c6f0146 (no description set)
Parent commit (@-) : qpvuntsm 68a50538 (empty) first