GitoxideLabs/gitoxide · error
the checkout ancestry contains a cycle
Error message
the checkout ancestry contains a cycle
What it means
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.
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`
Example fix
// before steps[2].parent = PlanParent::Step(5); steps[5].parent = PlanParent::Step(2); // loop // after steps[5].parent = PlanParent::Existing(base); steps[2].parent = PlanParent::Step(5);
Defensive patterns
Strategy: validation
Validate before calling
fn checkout_chain_acyclic(plan: &Plan) -> bool {
let mut seen = HashSet::new();
let mut cur = Some(plan.checkout);
while let Some(PlanParent::Step(i)) = cur {
if !seen.insert(i) { return false; }
cur = plan.steps.get(i).map(|s| s.parent);
}
true
} Prevention
- Keep parent indices strictly decreasing when splicing steps
- Round-trip test plan serialization
- Assert checkout ancestry terminates at Existing before submit
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- moving the stack would create a commit cycle
- a rebase plan contains an invalid or duplicate pick
- merge commits cannot be picked by the rebase editor
- rebase todo requires at least one -x/--hide revision when…
- the hidden and visible revisions have no editable fork point
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/75c49cd1b95987da.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/rebase.rs:1683
} else {
None
};
if plan.checkout.is_none()
&& checkout_target.is_some()
&& !plan
.steps
.iter()
.any(|step| matches!(step.commit, PlanCommit::Resolved(_)))
&& let Some(head) = repo.head()?.id().map(gix::Id::detach)
&& plan.scope.contains(&head)
{
reject_pending_checkout_path(&repo, head)?;
}
let mut eager = HashSet::new();
let mut cursor = checkout_target;
while let Some(PlanParent::Step(index)) = cursor {
if !eager.insert(index) {
anyhow::bail!("the checkout ancestry contains a cycle");
}
cursor = match plan.steps.get(index).context("the checkout step is missing")?.parent {
parent @ PlanParent::Step(_) => Some(parent),
PlanParent::Existing(_) => None,
};
}
let mut rewritten = HashMap::<ObjectId, Option<ObjectId>>::new();
let mut note_rewrites = Vec::new();
let mut produced = Vec::with_capacity(plan.steps.len());
let mut delete_refs = Vec::new();
let mut conflict = None;
let mut marked = false;
for (index, step) in plan.steps.iter().enumerate() {
let mut resolved_head = None;
let parent = match step.parent {
PlanParent::Existing(id) => {
repo.find_commit(id).context("could not find a fork target")?;View on GitHub (pinned to e73179060b)