{"record":{"id":"75c49cd1b95987da","repo":"GitoxideLabs/gitoxide","slug":"the-checkout-ancestry-contains-a-cycle","errorCode":null,"errorMessage":"the checkout ancestry contains a cycle","messagePattern":"the checkout ancestry contains a cycle","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gix-tix/src/edit/rebase.rs","lineNumber":1683,"sourceCode":"    } else {\n        None\n    };\n    if plan.checkout.is_none()\n        && checkout_target.is_some()\n        && !plan\n            .steps\n            .iter()\n            .any(|step| matches!(step.commit, PlanCommit::Resolved(_)))\n        && let Some(head) = repo.head()?.id().map(gix::Id::detach)\n        && plan.scope.contains(&head)\n    {\n        reject_pending_checkout_path(&repo, head)?;\n    }\n    let mut eager = HashSet::new();\n    let mut cursor = checkout_target;\n    while let Some(PlanParent::Step(index)) = cursor {\n        if !eager.insert(index) {\n            anyhow::bail!(\"the checkout ancestry contains a cycle\");\n        }\n        cursor = match plan.steps.get(index).context(\"the checkout step is missing\")?.parent {\n            parent @ PlanParent::Step(_) => Some(parent),\n            PlanParent::Existing(_) => None,\n        };\n    }\n\n    let mut rewritten = HashMap::<ObjectId, Option<ObjectId>>::new();\n    let mut note_rewrites = Vec::new();\n    let mut produced = Vec::with_capacity(plan.steps.len());\n    let mut delete_refs = Vec::new();\n    let mut conflict = None;\n    let mut marked = false;\n    for (index, step) in plan.steps.iter().enumerate() {\n        let mut resolved_head = None;\n        let parent = match step.parent {\n            PlanParent::Existing(id) => {\n                repo.find_commit(id).context(\"could not find a fork target\")?;","sourceCodeStart":1665,"sourceCodeEnd":1701,"githubUrl":"https://github.com/GitoxideLabs/gitoxide/blob/e73179060badf27222d790981fac3f84c1830a7e/gix-tix/src/edit/rebase.rs#L1665-L1701","documentation":"When validating a rebase plan, the library walks the parent chain of the plan's checkout target step-by-step and records visited step indices. If the same index is seen twice, the checkout ancestry forms a loop (a step whose ancestry includes itself), which cannot be executed.","triggerScenarios":"Constructing a plan whose `checkout` PlanParent::Step chain points back to an earlier step — e.g. step 2's parent is step 5 while step 5's ancestry leads back to step 2; programmatically splicing steps with incorrect parent indices.","commonSituations":"Custom tooling that builds rebase plans by index and sets a wrong parent index after reordering; buggy plan-serialization round-trips that corrupt parent links.","solutions":["Fix the parent indices so each step's ancestry is strictly decreasing (no step is its own ancestor)","Rebuild the plan from the intended linear order instead of patching indices","Add a pre-submit assertion that following `parent` from `plan.checkout` terminates at `Existing`"],"exampleFix":"// before\nsteps[2].parent = PlanParent::Step(5); steps[5].parent = PlanParent::Step(2); // loop\n// after\nsteps[5].parent = PlanParent::Existing(base); steps[2].parent = PlanParent::Step(5);","handlingStrategy":"validation","validationCode":"fn checkout_chain_acyclic(plan: &Plan) -> bool {\n    let mut seen = HashSet::new();\n    let mut cur = Some(plan.checkout);\n    while let Some(PlanParent::Step(i)) = cur {\n        if !seen.insert(i) { return false; }\n        cur = plan.steps.get(i).map(|s| s.parent);\n    }\n    true\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep parent indices strictly decreasing when splicing steps","Round-trip test plan serialization","Assert checkout ancestry terminates at Existing before submit"],"tags":["rebase","cycle","plan-validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"e73179060badf27222d790981fac3f84c1830a7e","analyzedAt":"2026-09-08T11:26:50.865Z","contentChangedAt":"2026-09-08T11:26:50.865Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}