In short: jj resolves the revisions (default @), keeps only the visible ones, and refuses
if any is immutable. One transform_descendants_with_options walk records each target as
abandoned onto its (possibly rewritten) parents. It rebases every other descendant, or with
--restore-descendants, reparents it so its content stays the same. Bookmarks are then
updated, which by default means deleting the abandoned commits' bookmarks. If @ was
abandoned, a new empty @ is created on its parents. One operation,
abandon commit <id>, records it all. The commits still exist in the store, and
jj undo brings them back.
The command is cmd_abandon. Its help text compares
it with jj restore --changes-in, which removes the same changes but keeps the change
rather than giving you a new one (source). The same
"abandoned commit" machinery runs whenever another command empties a commit:
squash sources,
rebase --skip-emptied, and the empty
working-copy commit edit and
new leave behind.
1. Command-line surface
| Flag | Type | Default | Meaning |
|---|---|---|---|
REVSETS (positional) | revsets, repeatable | @ | The commits to abandon. Positional and -r values are unioned (source). |
-r REVSETS | revsets, repeatable | none | Hidden alias for the positional argument (source). |
--retain-bookmarks | bool | false | Move the abandoned commits' bookmarks to their parents instead of deleting them (source). |
--restore-descendants | bool | false | Reparent descendants without changing their content, so the abandoned changes effectively move into the children (source). |
2. Configuration
jj abandon reads no settings of its own. revset-aliases."immutable_heads()"
decides what check_rewritable_expr refuses, so the root commit and anything immutable
can't be abandoned. user.name/user.email become the committer of rebased
descendants. For the "push deleted bookmarks" hint, the default remote that jj ignores when looking
for tracked bookmarks is git on Git-backed repositories
(has_tracked_remote_bookmarks).
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Target commits | abandoned | Recorded in the transaction's parent_mapping as Rewrite::Abandoned(new parents) (source). They aren't deleted: they stay in the store, simply no longer reachable from the view's heads. | transactions |
| Descendant commits | rewritten | Rebased with a 3-way tree merge, which drops the abandoned changes and may conflict, or reparented with the same tree under --restore-descendants. | rebase |
View.head_ids | write | Abandoned heads are removed. Their parents, or the rebased descendants, become heads as appropriate. | view |
View.local_bookmarks | write | A bookmark on an abandoned commit is set absent (deleted), or with --retain-bookmarks moved to the commit's new parents. Bookmarks on rebased commits follow them (update_local_bookmarks). | view |
View.remote_bookmarks | read | Only to decide whether to show the "push --deleted" hint. Remote bookmarks aren't changed: the deletion reaches the remote only when you push. | view |
View.wc_commit_ids | write | A workspace whose @ was abandoned gets a new empty commit on the abandoned commit's new parents (update_wc_commits). | view |
| Operation | write | One operation, described below. | operations |
Working copy (TreeState) | read, then maybe write | Snapshotted at the start. If @ was abandoned or rebased, its new tree is checked out, so files from abandoned ancestors disappear from disk. | working-copy |
5. Transaction and operation
- Transaction: one, only if at least one visible commit is to be abandoned (source).
-
Operation description (source):
abandon commit <id>orabandon commit <id> and <n> more. The ID is formatted withDisplay, which writes the full hex (core/src/object_id.rs). It is the first target in evaluation order, which puts children first. - Commits created or rewritten: none are created, except a replacement
@when@is abandoned. Every descendant of a target is rewritten. - Working copy: snapshotted first, so abandoning
@abandons your latest edits too. The new@'s tree, the merge of its parents, is then checked out. jj undorestores the previous view, which brings back the abandoned commits, their bookmarks and the old descendants. Nothing was deleted from the store.
6. Algorithm and invariants
Choosing the targets
The revsets are unioned, then intersected with ::visible_heads()
(source). A revset can name hidden commits, for example by full
commit ID. Those are counted and reported as skipped rather than
abandoned again (source). The immutability check runs only on the
visible part.
One walk over the descendants
Everything happens in one call to
transform_descendants_with_options rooted at the
targets (source). It visits the targets and all their
descendants, parents first. Each visit gets a CommitRewriter whose new parents already
reflect earlier visits. So when a target's parent was itself abandoned, the new parents are that
parent's parents, and chains of abandoned commits collapse correctly.
-
Target:
rewriter.abandon()recordsAbandoned(new parents)without writing anything. Later commits that had it as a parent get those parents instead. -
Other descendant, default:
rebase()mergesnew base + (tree − old base). The abandoned changes therefore vanish from every descendant, and a descendant that built on them may now conflict. -
Other descendant,
--restore-descendants:reparent()keeps the tree as it was. Its diff against the new parent now includes the abandoned changes, as if they'd been squashed into it.
Bookmarks and the working copy
At the end of the walk, update_rewritten_references runs with
RewriteRefsOptions { delete_abandoned_bookmarks: !retain_bookmarks }
(source).
update_local_bookmarks sets a bookmark on an
abandoned commit to absent. With --retain-bookmarks, it builds a new target from the new
parents with the old commit interleaved as the "removed" side. When the abandoned commit was a merge,
that means the bookmark becomes conflicted across both parents, shown as e??
in the upstream test below. The command then diffs the old and new bookmark tables to list what was
deleted (source).
update_wc_commits gives a workspace whose
@ was abandoned a new empty commit on the new parents, with their merged tree.
Invariants: nothing is deleted from storage, so the operation is fully undoable.
Every descendant survives, rebased or reparented, and none is abandoned implicitly. The root commit
and immutable commits can't be abandoned. Abandoned changes don't vanish under
--restore-descendants; they move into the children.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
No revisions to abandon. | Status message: no visible commit matched. No operation is recorded. | abandon.rs |
Skipping <n> revisions that are already hidden. | Status message: some matches were hidden commits. | abandon.rs |
Commit … is immutable / The root commit … is immutable | A visible target is immutable, for example jj abandon 'root()'. | cli_util.rs |
Abandoned <n> commits: followed by summaries; Deleted bookmarks: … | Status output on success. | abandon.rs |
Rebased <n> descendant commits onto parents of abandoned commits. (or … (while preserving their content) …) | Some descendants were rewritten. | abandon.rs |
Hint: Deleted bookmarks can be pushed by name or all at once with `jj git push --deleted`. | Git backend, and a deleted bookmark has a tracked remote bookmark, ignoring the git pseudo-remote. | abandon.rs |
8. Worked example
From upstream's test_basics.
b is on a, d is on c, and e
(@) merges a and d. Each commit has a bookmark of its own name
and adds a file of that name. Run jj abandon 'descendants(d)':
Upstream's snapshot of the output:
Abandoned 2 commits:
znkkpsqq 03e0d4bf e | e
vruxwmqv 636920e4 d | d
Deleted bookmarks: d, e
Working copy (@) now at: xtnwkqum 1c70f4d2 (empty) (no description set)
Parent commit (@-) : rlvkpnrz 7d980be7 a | a
Parent commit (@-) : royxmykx c12952d9 c | c
Added 0 files, modified 0 files, removed 2 files
e is listed first because it comes first in evaluation order, so the operation is
abandon commit 03e0d4bf… and 1 more. d was abandoned onto c.
When the walk reached e, its new parents were already a and
c, so the replacement @ merges those two. The same test shows the bookmark
variants. jj abandon --retain-bookmarks d moves bookmark d onto
c. Abandoning the merge e with --retain-bookmarks leaves
bookmark e conflicted between a and d, shown as
a e?? and d e??.