jj / How jj simplify-parents works

How jj simplify-parents works

jj simplify-parents removes redundant parent edges. If one parent of a merge is already an ancestor of another parent, that edge adds nothing to the history, so jj drops it. Trees are not changed.

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 resolves the commits to simplify: -s sources with their descendants, plus -r revisions, or by default revsets.simplify-parents (reachable(@, mutable())). It checks they are all mutable. In one transform_descendants pass, each selected commit with more than one parent keeps only the parents that are heads of its parent set. Every commit whose parents changed is reparented with its tree unchanged. It reports Removed E edges from S out of N commits. and records simplify N commits.

The command is cmd_simplify_parents. The edge removal itself is CommitRewriter::simplify_ancestor_merge in jj-lib. jj rebase applies the same helper to the rebased commits when given --simplify-parents (source, use).

1. Command-line surface

FlagTypeDefaultMeaning
-s, --source REVSETSrevsets, repeatablenoneSimplify these commits and all their descendants (source).
-r, --revision, --revisions REVSETSrevsets, repeatablenoneSimplify exactly these commits (source).

The two flags are combined: descendants(sources) | revisions (source). With neither, the default revset applies.

SimplifyParentsArgs

2. Configuration

KeyDefaultEffect
revsets.simplify-parentsreachable(@, mutable())The commits to simplify when no flags are given: the mutable commits connected to @ (default, source).
revset-aliases."immutable_heads()"(built-in)Every selected commit must be mutable, even ones that end up unchanged (source).

3. End-to-end flow

jj simplify-parents: call flow cmd_simplify_parents resolves the commits: descendants of the sources unioned with the revisions, or revsets.simplify-parents if neither is given. It checks them all rewritable and starts a transaction. transform_descendants visits each commit parents first. A selected commit with more than one parent calls simplify_ancestor_merge, which keeps only the parents that are heads of the parent set. Any commit whose parents changed is reparented, keeping its tree, and counted as simplified (fewer parents) or rebased. It prints the counts, and tx.finish records "simplify N commits". load workspace (snapshot); revs = descendants(-s) ∪ -r, or revsets.simplify-parents check_rewritable_expr(revs); N = number of selected commits start_transaction selected and > 1 parent simplify_ancestor_merge(): keep parents that are heads(parents) i.e. drop any parent reachable via another index().heads(new_parents) the index answers which parents are not ancestors of the others; order of the kept parents is preserved parents changed → reparent().write() — same tree, new parent list fewer parents → simplified_commits += 1, edges += removed otherwise (a descendant whose parent was rewritten) → reparented_descendants += 1 "Removed E edges from S out of N commits." [+ "Rebased R descendant commits."]; tx.finish("simplify N commits")

4. Core architecture touchpoints

StructureRead / writeHowExplained on
Index (ancestry)readheads() over each merge's parent list decides which edges are redundant.index
Commit parentswriteRedundant parents are removed. Descendants of rewritten commits get the new IDs.commits
TreesreadKept as is, because reparent() copies the old tree (the help text: "This has no effect on any revision's contents").trees
Bookmarks, @, predecessorswriteFollow the rewritten commits when the transaction finishes.transactions
OperationwriteOne operation, or none if nothing changed.operations

5. Transaction and operation

  • Transaction: one, after the rewritability check (source).
  • Operation description: simplify <N> commits, where N is the number of selected commits, not the number changed (source).
  • Commits rewritten: merges with a redundant parent, plus every descendant of a rewritten commit.
  • No-op runs: no parents change, so the transaction finishes with Nothing changed. (test).

6. Algorithm and invariants

For a commit A with parents (B, C) where C is an ancestor of B, the edge A→C is redundant: every commit reachable through C is already reachable through B. simplify_ancestor_merge asks the index for the heads of A's parent list and keeps only those, in their original order. If all parents are independent, as in a normal diamond merge, nothing changes (test_simplify_parents_no_change_diamond).

Only commits in the selected set are simplified. Their descendants are visited too, because transform_descendants walks all descendants. Those only get their parent IDs remapped, even if they have redundant edges themselves (source). The parent count used for the statistics is taken from the rewriter's intended new parents, which are already remapped to rewritten IDs (source).

Invariants: the set of ancestors of every commit is unchanged. Trees are unchanged. Only selected commits lose edges. Nothing immutable is touched.

7. Errors and edge cases

MessageWhenSource
The root commit 000000000000 is immutable / Commit … is immutableThe selection includes an immutable commit.simplify_parents.rs, test
Nothing changed.The selection is empty, or no merge in it has a redundant parent.test
(no "Removed …" line)The statistics are only printed when at least one commit lost an edge (source).simplify_parents.rs

8. Worked example

From upstream's test_simplify_parents_redundant_parent. Commit c (@) has parents a and b, and b is a child of a. Then jj simplify-parents -s @- (or -r @ -r @-) runs:

jj simplify-parents removes a redundant edge Before: root, a, b on a, and c with parents a and b. After: c has only b as a parent. The edge c→a was redundant because a is an ancestor of b. c's tree is unchanged. before after simplify-parents -s @- root a b @ c parents: a, b root a b @ c parents: b

Upstream's snapshot:

$ jj simplify-parents -s @-
Removed 1 edges from 1 out of 3 commits.
Working copy  (@) now at: royxmykx 265f0407 c | c
Parent commit (@-)      : zsuskuln 123b4d91 b | b

@- is a | b (both parents of c), so -s @- selects a, b and c: "3 commits". Only c is a merge, and heads(a, b) = b, so one edge goes. c gets a new commit ID but the same tree, and the log is now the straight line a → b → c.