In short: jj loads the workspace without loading the repo, so it works even when the repo state is
broken, and refuses --at-op. It reads the current op heads and resolves the argument: a
range A..B, or a single operation, meaning just that one. It refuses to abandon a current
head. It copies every operation from B up to the current heads with new parents that skip
the range, counts what became unreachable, replaces each op head with its copy, and moves the working
copy's operation ID along. The views are untouched, so the repo looks exactly the same.
The command is cmd_op_abandon. Its help gives the
two typical uses: jj op abandon ..<id> to discard old history, and
jj op restore <id> then jj op abandon <id>..@- to discard recent
operations. Old commit versions that only the abandoned operations referred to become eligible for
jj util gc (source). How operations and op heads are
stored is on the operations page.
1. Command-line surface
| Argument | Type | Default | Meaning |
|---|---|---|---|
OPERATION (positional) | operation or A..B | required | A single operation, or a range. ..B starts at the root operation; A.. runs to the current heads (source, parsing). |
As with revsets, A..B means the
ancestors of B that aren't ancestors of A: A itself is kept.
2. Configuration
None. The global --ignore-working-copy skips moving the working copy's operation ID
(source); --at-op is rejected
(source).
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Operation store | written | Copies of each later operation with new parents; everything else, including the view ID and commit predecessors, is copied as is (source). | operations |
| Op heads store | updated | Each current head is replaced by its copy (source, update_op_heads). | operations |
| Working-copy state | updated | Its recorded operation ID moves to the copy, so the workspace isn't considered stale (source). | working-copy |
| Views, commits | untouched | Abandoned operations, and commits only they referenced, stay on disk until jj util gc. | operations |
5. Transaction and operation
- No transaction and no new operation. The command writes rewritten copies of existing operations directly to the store (source).
- Operation IDs change. Operation IDs are content hashes, and a copy has different parents, so every operation after the range gets a new ID, including the current one. Old IDs you noted down no longer resolve to the live history.
- The repo isn't loaded, by design, "so that this command can be used to recover from corrupted repo state" (source).
- Not undoable with
jj undo: there is no operation to undo. The abandoned operations remain in the store until garbage collection.
6. Algorithm and invariants
With root and heads from the argument,
reparent_range is called with the abandoned heads as
its roots, the current heads as its heads, and root as the destination
(source):
- Collect the operations in
heads..current_heads; those are the ones to copy (source). - Count
root..heads: the operations about to become unreachable (source). - Walk the copies oldest first. Each parent that was already copied is replaced by its copy; the first parent that wasn't, which is in the abandoned range, is replaced by
root(at most once per operation). Write the copy and remember its ID (source). - Map each current head to its copy (source).
If every head maps to itself, nothing was in the range: Nothing changed.. Otherwise the op
heads are swapped. If the working copy's operation is one of the old heads, it is moved to the copy;
if it isn't, for instance after divergent operations, jj warns and leaves it, and you'll need to
"re-abandon their ancestors" (source).
Invariants: the current heads' views are unchanged, so every command sees the same repo before and after. The current operation can't be abandoned, nor can the root operation or a merge operation on their own. Each current head gets exactly one copy (asserted).
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Cannot abandon the current operation <id>, hint Run `jj undo` to revert the current operation, then use `jj op abandon` | The range or operation includes a current op head. The hint appears only with a single head. | abandon.rs |
--at-op is not respected (exit status 2) | --at-op was given. | abandon.rs |
Cannot abandon the root operation / Cannot abandon a merge operation | A single operation with no parent, or several. | abandon.rs |
Nothing changed. | An empty range, e.g. @-..@-. | abandon.rs |
Warning: The working copy operation <id> is not updated because it differs from the repo <ids>. | The working copy was at an operation that isn't a current head. | abandon.rs |
8. Worked example
From upstream's test_op_abandon_ancestors. After
jj commit -m 'commit 1' and jj commit -m 'commit 2', the operation log has four
entries. jj op abandon ..@- drops everything older than the current operation:
Upstream's output (source):
Abandoned 2 operations and reparented 1 descendant operations.
jj debug local-working-copy then reports the new ID 16e49894… as the working
copy's operation. Later in the test, jj op abandon ..@ fails with
Cannot abandon the current operation and the jj undo hint, and
jj op abandon @-..@- prints Nothing changed.