In short: jj evaluates the revisions (default @), refuses the root commit, and renders each
copy's description with templates.duplicate_description. Without a destination, each copy
gets the original's parents, with any parent that is also being copied swapped for its copy. Trees are
reused unchanged. With a destination, the roots of the set are rebased onto it, the rest follow, and
-A/-B rebase the destination's children onto the copies' heads. The originals
are never touched, so even immutable commits can be duplicated. The operation is
duplicate <n> commit(s).
The command is cmd_duplicate. The work happens in
jj-lib's duplicate_commits_onto_parents and
duplicate_commits. The second shares its
"connected set, internal parents, heads" logic with
jj rebase -r.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
REVSETS (positional) | revsets, repeatable | @ | The commits to copy. Positional and -r values are unioned. Any revision is allowed, including immutable ones (source). |
-r REVSETS | revsets, repeatable | none | Hidden alias for the positional argument (source). |
-o, --onto, -d, --destination REVSETS | revsets, repeatable | none | Put the copies' roots on these parents. Repeat it for a merge (source). |
-A, --insert-after, --after REVSETS | revsets, repeatable | none | Insert the copies after these, and move their children onto the copies (source). |
-B, --insert-before, --before REVSETS | revsets, repeatable | none | Insert the copies before these: on their parents, with them rebased on top (source). |
2. Configuration
| Key | Default | Effect |
|---|---|---|
templates.duplicate_description | description (templates.toml) | A commit template rendered against each original to give the copy's description. A trailing newline is added if missing (source). Upstream tests a custom template (test_duplicate_description_template). |
revset-aliases."immutable_heads()" | see the CLI page | Only matters for -A/-B, whose rebased children must be mutable. |
user.name, user.email | unset | Become the copies' committer. The author is kept. |
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Original commits | read only | Never rewritten. Copies are built from them with rewrite_commit, but clear_rewrite_source() keeps jj from recording them as rewritten, so no descendant is rebased and no bookmark moves. | commits |
| New commits | created | One per original. Each has a fresh change ID (generate_new_change_id), the original's author, and you as committer. | commits |
| Trees | reused or merged | Reused as they are without a destination. Rebased with a 3-way merge with one, which can conflict. | trees |
New children (-A/-B) | rewritten | Rebased onto the copies' heads, along with their descendants. | rebase |
View.head_ids | write | The copies become new heads, unless they got children. | view |
Bookmarks, wc_commit_ids | unchanged | They stay on the originals. Copies get no bookmarks, and @ doesn't move. | view |
| Operation | write | One operation, duplicate <n> commit(s). | operations |
5. Transaction and operation
- Transaction: one, unless nothing matched or the root was named (source).
- Operation description:
duplicate <n> commit(s), with the literal(s). Upstream's undo snapshot showsduplicate 1 commit(s)(source). - Commits created or rewritten: one copy per original. With
-A/-B, the destination's children and their descendants are rewritten too. - Working copy: snapshotted at the start.
@is never moved to a copy, so the files on disk change only if@was a rebased new child. jj undohides the copies again and restores any rebased children.
6. Algorithm and invariants
Without a destination
duplicate_commits_onto_parents walks the targets
oldest first. Each copy is rewrite_commit(original) with no rewrite source and a new change
ID. Its parents are the original's, except that a parent which has already been copied is replaced by
its copy. The tree isn't touched, so copying a stack reproduces the stack beside the original, with
identical content at every step.
With a destination
duplicate_commits treats the targets as a connected
set, like rebase -r:
- Compute each target's internal parents, its nearest ancestors within the set across any gaps. Targets with none are the roots (source).
- For
-A/-B, compute the set's heads (source). -
Oldest first, copy each target: roots onto the destination parents, others onto their internal
parents' copies. Use
CommitRewriter::rebase, so each copy's tree is its own diff re-applied on the new base. Then clear the rewrite source and draw a new change ID (source). - Rebase each new child. Any old parent that was a destination parent is replaced by the copies' heads, and the heads are added if missing (source).
Before any of this, jj warns, without stopping, if a copy would land among the descendants or ancestors of its own original (source).
Invariants: originals and everything referring to them are untouched. Every copy is a new change, so duplication never causes divergence. Copies keep their originals' internal graph shape. Without a destination, content is byte-identical.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
No revisions to duplicate. | Status: the revsets match nothing. | duplicate.rs |
Cannot duplicate the root commit | The root is among the targets, as with jj duplicate 'all()'. | duplicate.rs |
Warning: Duplicating commit <id> as a descendant of itself. | A destination parent descends from a target. | duplicate.rs |
Warning: Duplicating commit <id> as an ancestor of itself. | A new child is an ancestor of a target. | duplicate.rs |
Refusing to create a loop: … of the duplicated commits, No revisions found to use as parent, Commit … is immutable | Location errors for -A/-B, where an immutable commit would have to be rebased as a new child. | cli_util.rs |
Duplicated <id> as <summary>, Rebased <n> commits onto duplicated commits. | Status output. | duplicate.rs |
8. Worked example
From upstream's test_duplicate. a and b
sit on the root, and c (@) merges them. Plain jj duplicate
copies @:
Upstream's snapshot of the output:
Duplicated 387b928721d9 as lylxulpl 71c64df5 c
The copy keeps c's parents, tree and description (c), but it is a new change
(lylxulpl), with no bookmark, and @ stays on the original. Earlier in the
same test, jj duplicate a printed Duplicated 7d980be7a1d4 as kpqxywon 13eb8bd0 a,
and jj undo named the operation duplicate 1 commit(s). Upstream's
test_duplicate_destination covers -o, and the tests
after it cover -A and -B.