In short: jj resolves one mutable commit (default @) and selects changes from it, in a diff
editor unless you gave paths. It then builds two commits from the original. The "selected" commit
gets the chosen tree. The "remaining" commit gets the full original tree (on top of the selected one)
or the original minus the selection (in parallel mode). Which of the two keeps the original change ID
depends on the mode. Both descriptions are edited in one file. Descendants and working copies are moved
onto the remaining commit, and so are bookmarks, by default. One operation records it all:
split commit <id>.
The command is cmd_split. Its help text draws the
three layouts (source). Selection is the shared
DiffSelector (see jj commit,
whose help text compares itself with split). The bulk description editor is the one
jj describe uses for several
commits. The -o/-A/-B mode hands the selected commit to the
rebase engine.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
FILESETS (positional) | filesets | all files | Files matching these go in the selected changes. Giving any turns interactive mode off unless -i is also given (source). |
-i, --interactive | bool | on when no filesets | Choose changes in a diff editor (source, default). |
--tool NAME | string | none | Diff editor to use. Implies -i (source). |
-r, --revision REVSET | revset | @ | The commit to split, which must be exactly one (source). |
-o, --onto, -d, --destination REVSETS | revsets, repeatable | none | Extract the selection into a new commit on these parents. Conflicts with --parallel (source). |
-A, --insert-after, --after REVSETS | revsets, repeatable | none | Extract the selection into a new commit inserted after these (source). |
-B, --insert-before, --before REVSETS | revsets, repeatable | none | Extract the selection into a new commit inserted before these (source). |
-m, --message MESSAGE | string, repeatable | none | Description for the selected commit, without opening the editor. The remaining commit keeps the original description (source). |
--editor | bool | false | Open the description editor even with -m (source). |
-p, --parallel | bool | false | Make the two commits siblings instead of parent and child (source). |
2. Configuration
| Key | Default | Effect |
|---|---|---|
split.legacy-bookmark-behavior |
true (misc.toml) |
When true, bookmarks on the split commit move to the remaining commit. When false, an experimental setting according to its comment, they stay with the selected commit (source). Upstream tests all three cases (test_split_with_bookmarks). |
ui.diff-editor, ui.diff-instructions | :builtin with a hint; true | The diff editor and its instruction text, as on the commit page. |
ui.editor, templates.draft_commit_description, templates.commit_trailers | see describe | The description editor, its draft template, and trailers added to both descriptions. |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Target commit | read, then replaced by two | Both new commits are built with rewrite_commit(target), so both copy its author (and author date), description and parents. | commits |
| Change IDs | one kept, one new | By default the selected commit keeps the change ID and is recorded as the rewrite of the target; the remaining commit gets a fresh change ID and no rewrite source. With -o/-A/-B it's the other way round (source, source). | commits |
| Trees | read, merged | The selected tree comes from the diff selector. In parallel mode, the remaining tree is merge(tree, inverted selected diff) (source). | trees |
| Descendants | rebased | Onto the remaining commit, or onto both with --parallel. With -o/-A/-B, onto the remaining commit, and the selected commit's new children onto it. | rebase |
View.local_bookmarks | write | Follow the target's recorded rewrite. In the default and parallel layouts that is the remaining commit with the legacy setting, and otherwise the selected one. With -o/-A/-B it is always the remaining commit, which continues the change. | view |
View.wc_commit_ids | write | Every workspace whose @ was the target is moved to the remaining commit with MutableRepo::edit (source). | view |
| Temporary commits | written, hidden | When the editor is used, so both drafts can render their JJ: file lists. | describe |
| Operation | write | One operation, split commit <id>. | operations |
Working copy (TreeState) | read, then maybe write | Snapshotted first. In the default layout the new @ has the full original tree, so files don't change. With --parallel or a move, they may. | working-copy |
5. Transaction and operation
- Transaction: one, always. Even an empty split writes two commits (source).
- Operation description:
split commit <full hex id of the target>(source). - Commits created or rewritten: two replace the target, and every descendant is rebased. With
-A/-B, the new children of the selected commit are rebased too. - Working copy: snapshotted at the start. If
@was split, it becomes the remaining commit. jj undorestores the single original commit and the old descendants.
6. Algorithm and invariants
Selecting
select_diff runs the diff selector on
parent tree → commit tree, with instructions explaining that the right side becomes the new
first commit. Paths without -i select whole files. The result is a
CommitWithSelection. If the selection is everything, jj warns that the original revision
becomes empty. If it is nothing, jj warns that the new revision will be empty. Neither is an error.
Building the two commits
-
Selected (source): the selected tree, the
-mtext or the original description, with trailers added only if the description isn't empty. In move mode it drops the rewrite source and draws a new change ID, so the original commit doesn't become divergent. - Remaining (source): the full original tree. On top of the selected commit, its diff is exactly the unselected changes. In parallel mode, the tree is the original with the selected diff reverse-applied, so its diff against the shared parents is the unselected changes. Outside move mode it is the one with the new change ID.
-
Descriptions (source): unless
-mwas given without--editor, both drafts are written as hidden commits, with parents set as they will be. They are edited in one bulk file, under the intros "Enter a description for the selected changes." and "Enter a description for the remaining changes.". The result is validated likedescribe's bulk edit.
Placing them
rewrite_descendants (default and parallel)
first, with the legacy setting, re-records the target as rewritten to the remaining commit,
which is what carries bookmarks there. It then walks the target's descendants, replacing whichever
parent now stands for the target with the remaining commit, or with both commits in parallel mode,
and rebases each one. Finally it points every workspace that was on the target at the remaining
commit.
move_first_commit (-o/-A/-B)
first rebases the target's descendants onto the remaining commit, which kept the change ID. It maps
the requested parents and children through those rewrites, then runs
move_commits on the selected commit alone. It
counts rebased commits carefully so that a commit rewritten twice is reported once.
Invariants: the two commits' changes add up to the original's. Exactly one of them continues the original change; the other is a new change. Descendants keep their content relative to the combined pair. Author and author date carry over to both, as upstream's test_split_by_paths checks.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Warning: All changes have been selected, so the original revision will become empty. | Full selection, which is also the case for an empty commit. | split.rs |
Warning: No changes have been selected, so the new revision will be empty. | Empty selection. | split.rs |
Warning: No matching entries for paths: … | A path exists in neither the parent tree nor the commit's tree. Common when a revision is passed positionally, as in jj split @. | split.rs |
Commit … is immutable | The target is immutable. | split.rs |
Refusing to create a loop: … of the split-out commit, No revisions found to use as parent | -o/-A/-B location errors. | cli_util.rs |
| Bulk description errors (missing, duplicate or unexpected sections) | The edited description file is malformed. | description_util.rs |
clap: --parallel with -o/-A/-B; -o with -A/-B | Incompatible layouts. | split.rs |
The help text says "Splitting an empty commit is not supported because the same effect can be
achieved with jj new" (source). The code has no
such check, though: upstream's test_split_empty splits an empty
commit successfully, with the "Empty diff" and "All changes have been selected" warnings, and gets two
empty commits.
8. Worked example
From upstream's test_split_by_paths: a fresh @
(change qpvuntsm) adds file1, file2 and file3.
Running jj split file2 (paths given, so no diff editor):
Upstream's snapshot of the output:
Selected changes : qpvuntsm 6dbc7747 (no description set)
Remaining changes: zsuskuln 42cbbc02 (no description set)
Working copy (@) now at: zsuskuln 42cbbc02 (no description set)
Parent commit (@-) : qpvuntsm 6dbc7747 (no description set)
The bulk editor showed two sections, JJ: Enter a description for the selected changes. and
JJ: Enter a description for the remaining changes., each listing its own files. The test
also sets a commit-trailers template, so Trailer: value was pre-filled in both. Both new
commits keep the original author date, while their committer dates are newer. The operation is
split commit <qpvuntsm's old id>.