jj / How jj operation abandon works

How jj operation abandon works

jj op abandon removes operations from the operation log's history. Unlike every other command, it doesn't add an operation: it rewrites the operations that come after the abandoned range so their parent skips over it, then repoints the op heads and the working copy at the rewritten ones.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. 4. Core architecture touchpoints
  5. 5. Transaction and operation
  6. 6. Algorithm and invariants
  7. 7. Errors and edge cases
  8. 8. Worked example

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

ArgumentTypeDefaultMeaning
OPERATION (positional)operation or A..BrequiredA single operation, or a range. ..B starts at the root operation; A.. runs to the current heads (source, parsing).

OperationAbandonArgs

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

jj op abandon: call flow cmd_op_abandon loads the workspace but not the repo, and rejects --at-op. It reads the current op heads and resolves the argument into a root operation and head operations: for A..B, A (or the root operation) and B (or the current heads); for a single operation X, X's single parent and X. It refuses if a head to abandon is a current op head. reparent_range copies the operations between the abandoned heads and the current heads onto the root, giving new IDs. If nothing changed it says so. Otherwise it prints the counts, swaps each op head for its copy, and updates the working copy's operation ID. load_workspace (repo not loaded); reject --at-op; current op heads A..B root = A (or the root operation) heads = [B] (or the current heads) single operation X root = X's only parent, heads = [X] root or merge operation → error refuse if any head to abandon is a current op head ("Cannot abandon the current operation …") reparent_range: copy heads..current_heads onto root, oldest first; new IDs unreachable_count = root..heads; rewritten_count = copies heads unchanged? "Nothing changed." and stop "Abandoned N operations and reparented M descendant operations." update_op_heads(old head → copy) for each changed head working copy: finish(copy of its operation), or a warning if its operation isn't one of the heads

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Operation storewrittenCopies of each later operation with new parents; everything else, including the view ID and commit predecessors, is copied as is (source).operations
Op heads storeupdatedEach current head is replaced by its copy (source, update_op_heads).operations
Working-copy stateupdatedIts recorded operation ID moves to the copy, so the workspace isn't considered stale (source).working-copy
Views, commitsuntouchedAbandoned 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):

  1. Collect the operations in heads..current_heads; those are the ones to copy (source).
  2. Count root..heads: the operations about to become unreachable (source).
  3. 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).
  4. 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

MessageWhenSource
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 operationA 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:

jj op abandon ..@- on the operation log Before: root, add workspace (f63ee16f), commit 1 (5d7c2b45), commit 2 (b004de1c, current). The range ..@- covers add workspace and commit 1. After: root, then commit 2 with a new ID, 16e49894, whose parent is now the root. Abandoned 2 operations and reparented 1. op log before after jj op abandon ..@- b004de1c commit 2 @ 5d7c2b45 commit 1 in ..@-: abandoned f63ee16f add workspace in ..@-: abandoned 000000000000 root() 16e49894 commit 2 @ same view, new parent → new ID 000000000000 root()

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.