In short: the command turns its flags into a MoveCommitsLocation: the target commits (an
explicit set for -r, or roots plus all their descendants for -s/-b),
the new parents, and any new children. compute_move_commits computes a new parent list for
every commit that has to move. That includes descendants outside the set, which close the "hole" a
-r rebase leaves. apply_move_commits then rebases each one in topological
order with a 3-way tree merge, recording conflicts instead of stopping. Duplicate divergent commits
are abandoned along the way. One operation, such as rebase commit <id> and descendants,
records everything.
The command is cmd_rebase plus three planning
functions, one per mode. The engine is in jj-lib's
rewrite.rs, which jj duplicate,
jj parallelize and the -A/-B modes of other commands also use.
The help text draws a before/after graph for every mode (source).
This page explains the machinery behind those graphs. How a single commit's tree is rebased, and how
rewritten commits propagate, is on the transactions
page.
1. Command-line surface
What to rebase (at most one; default -b @)
| Flag | Type | Meaning |
|---|---|---|
-b, --branch REVSETS | revsets, repeatable | The whole "branch" relative to the destination: roots(destination..X) and all their descendants (source). |
-s, --source REVSETS | revsets, repeatable | These commits and all their descendants. Each one becomes a direct child of the destination, even if some descend from others (source). |
-r, --revision, --revisions REVSETS | revsets, repeatable | Exactly these commits, keeping the edges between them. Their descendants outside the set stay behind, reparented onto the moved commits' old parents. This is the only mode that may move a commit onto its own descendant (source). |
Where to (exactly one of -o or -A/-B)
| Flag | Type | Meaning |
|---|---|---|
-o, --onto, -d, --destination REVSETS | revsets, repeatable | New parents. Repeat it to make the moved roots merges. Existing children of the destination are untouched (source). |
-A, --insert-after, --after REVSETS | revsets, repeatable | New parents, and the destination's existing children are moved onto the rebased commits (source). |
-B, --insert-before, --before REVSETS | revsets, repeatable | New children: the commits go onto these commits' parents, and these commits are moved onto them. Can be combined with -A (source). |
RebaseDestinationArgs, a required clap group
Options
| Flag | Default | Meaning |
|---|---|---|
--skip-emptied | false | Abandon a target that becomes empty because of the rebase. Commits that were already empty, and merges, are kept. This becomes EmptyBehavior::AbandonNewlyEmpty (source). |
--keep-divergent | false | Don't abandon divergent commits whose identical twin is already in the destination (source). |
--simplify-parents | false | Drop any new parent of a target that is an ancestor of another of its new parents (source). |
2. Configuration
jj rebase reads no settings of its own. revset-aliases."immutable_heads()"
decides what check_rewritable refuses: the moved commits, and with
-A/-B the new children. user.name/user.email
become the rewritten commits' committer. The working-copy settings on the
working-copy page govern the snapshot and
checkout. The bookmark rules are fixed in code: delete_abandoned_bookmarks is
false, so a bookmark on an abandoned commit moves to its parent
(source).
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Target commits | rewritten | New parents and a merged tree. Each keeps its change ID, description and author. The committer becomes you, now. | commits |
| Descendants and new children | rewritten | Rebased with EmptyBehavior::Keep and no parent simplification, regardless of the flags (source). | transactions |
| Trees | read, merged | Each rebased tree is new base + (old tree − old base), where a base is the merge of a commit's parents' trees. Conflicts are stored in the result. | trees |
| Index | read | Ancestry checks (is_ancestor, heads) for destination validation, --simplify-parents and new-children handling. Revset evaluation for connected(), descendants() and ranges. | index (page queued) |
| Change-ID index | read | resolve_change_id finds other visible commits of each target's change, to detect duplicates. | commits |
View.head_ids | write | Old versions are replaced by rebased ones. | view |
View.local_bookmarks | write | Bookmarks follow their rebased commits. Bookmarks on abandoned commits move to the abandoned commit's parents. | view |
View.wc_commit_ids | write | Follows a rebased @. If @ was abandoned, a new empty @ is created on its parents. | view |
| Operation | write | One operation, described below. | operations |
Working copy (TreeState) | read, then maybe write | Snapshotted at the start. Checked out again if @'s tree changed. | working-copy |
5. Transaction and operation
- Transaction: one, started after planning, and only if there is at least one target (source).
-
Operation description (
tx_description): for-r,rebase commit <id>orrebase commit <id> and <n> more. For-s/-b,rebase commit <id> and descendantswith one root, orrebase <n> commits and their descendantswith several. IDs are full hex. - Commits created or rewritten: every target whose parents change, every descendant of a target or new child, and the new children themselves. Commits whose computed parents equal their current ones are skipped and counted as "already in place". No commit is created, except a replacement
@if@is abandoned. - Working copy: snapshotted first. If
@was rebased, its new tree is checked out, and jj reports the added, modified and removed files. jj undorestores the whole previous graph, including commits abandoned by--skip-emptiedor the divergence check.
6. Algorithm and invariants
Planning the move
-
-revaluates the union of revsets to a set, children first, and checks it is rewritable as a whole. It then refuses only a destination that is one of the targets (source). -
-sresolves each revset in order. With-o, it refuses a destination that is the source itself or one of its descendants (check_rebase_destinations). -
-bdefaults to@. It computesroots(new_parents..branch): the oldest commits that are ancestors of the branch commits but not of the destination. Those become the roots, checked as for-s(source). A branch already on top of the destination therefore yields its first commit above the destination, which is already in place and gets skipped. -
The destination comes from the shared
compute_commit_location.-ogives parents only.-A Xgives parentsXand childrenchildren(X).-B Ygives the parents ofYas parents andYas children. New children must be mutable, and must not create a cycle.
Computing every new parent list
compute_move_commits works in five steps:
-
Target set and roots (source). For
Roots, the target set is all descendants of the roots. ForCommits, it's the given set, and the roots are the targets with no parent insideconnected(set). Commits between two targets count as connecting them, so-r 'A|C'withBbetween them keepsCaboveA. - External parents (source). For each target, oldest first, find its nearest ancestors outside the set. A commit left behind that had a target as a parent gets these instead, which is how the hole closes.
-
Self-referencing destinations (source). A new
parent inside the set is replaced by its external parents, so
jj rebase -r A -B Aworks. A new child inside the set is replaced by its nearest descendants outside the set. - New children's parents (source). Each new child keeps its other parents, except that any parent which is one of the new parents is replaced by the heads of the target set. The target heads are then added if they aren't present, which inserts the targets between the two.
-
Everything to visit (source). These are the
descendants of the roots and the new children (
find_descendants_for_rebase). A root gets the new parents. Another target keeps its parents inside the set and drops outside parents that descend from a new child. A commit outside the set has any target parents replaced by external parents. Anything else keeps its parents.
Applying it
apply_move_commits hands the precomputed map to
transform_commits, which visits parents before
children. Each visited commit is handled in one of three ways. A commit marked by the divergence check
is abandoned. A commit whose parents didn't change is skipped. Otherwise it goes to
rebase_commit_with_options, using the command's
options for targets and keep-everything options for the rest. That function:
- with
--simplify-parents, keeps only the new parents that are heads among themselves (source); -
merges trees (
rebase_with_empty_behavior). If the parents' trees are unchanged, the old tree is kept with no merge. Otherwise the new tree ismerge(new base, old base, old tree), labelled(rebase destination),(parents of rebased revision)and(rebased revision)for conflict markers; -
applies the empty rule, single-parent results only.
--skip-emptiedabandons the commit if the new tree equals the parent's and the commit wasn't empty before (EmptyBehavior).
Duplicate divergent commits
find_duplicate_divergent_commits looks, for each
target, for other visible commits with the same change ID that would become new ancestors,
meaning those in roots..new_parents. For each such twin, it rebases the target onto the
twin's parents. If that gives exactly the twin's tree, the target is a duplicate. It is abandoned
during the rebase, and its descendants are rebased onto the parents it would have had. This cleans up the common case of
rebasing a local commit onto an upstream that already contains the same change.
Invariants: every commit's own diff is preserved, with conflicts recorded rather than failing. Every parent list is computed before anything is written, so each commit is rewritten at most once. Change IDs never change. Empty and merge commits are never abandoned unless asked, and merges never are. Descendants outside the target set never lose content; they're only reparented around a hole.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
No revisions to rebase. | Status message (not an error) when the target set is empty, for example -r 'none()'. No operation is recorded. | rebase.rs |
Cannot rebase <id> onto itself | -o names a commit that is being moved (all modes). | rebase.rs, rebase.rs |
Cannot rebase <id> onto descendant <id> | -s or -b with -o pointing into the moved subtree. Use -r instead. | rebase.rs |
Commit … is immutable / The root commit … is immutable | A target (or -b root), or an -A/-B new child, is immutable. | cli_util.rs |
Refusing to create a loop: commit … would be both an ancestor and a descendant of the rebased commits | -A/-B where a new child is an ancestor of a new parent. | cli_util.rs |
No revisions found to use as parent | The destination resolves to no parents. | cli_util.rs |
Abandoned <n> divergent commits that were already present in the destination: followed by their summaries | Status message: duplicates were dropped. | rebase.rs |
Skipped rebase of …, Rebased … commits to destination., Rebased … descendant commits., Abandoned … newly emptied commits. | Status statistics after the move. | rebase.rs |
clap: a required argument was not provided; -r/-s/-b conflict; -o with -A/-B | No destination given, two "what" modes given, or -o mixed with -A/-B. | rebase.rs |
8. Worked example
From upstream's test_rebase_single_revision.
b and c both sit on a, d merges them, and
e (@) is on d. Each commit adds a file named after itself. Run
jj rebase -r c -o b:
Upstream's snapshot of the output:
Rebased 1 commits to destination.
Rebased 2 descendant commits.
Working copy (@) now at: znkkpsqq 2baedee4 e | e
Parent commit (@-) : vruxwmqv 45142a83 d | d
Added 0 files, modified 0 files, removed 1 files
c was the only target, so it is the root and gets the new parent b.
d lies outside the set, but had c as a parent, so it gets c's
external parent a instead and becomes a merge of b and a. That
parent list isn't simplified, because descendants never are. e follows d.
c's file is no longer below @, hence "removed 1 files". The operation is
rebase commit <c's id>. In the same test, jj rebase -r d -o a moves the
merge itself, and e becomes a merge of b and c.