GitoxideLabs/gitoxide · error
moving the stack would create a commit cycle
Error message
moving the stack would create a commit cycle
What it means
During rebase-plan construction in `gix-tix/src/edit/rebase.rs`, moving a stack of commits is rejected if none of the proposed steps advanced the plan (steps.len() == before after an iteration). That means re-parenting the stack would point a step at one of its own descendants, i.e. a commit cycle. The library refuses because a cycle could never be serialized into a linear rebase.
Solutions
- Verify the destination position is not inside the ancestry of any moved commit before invoking the edit
- Recompute the requested ordering so moved steps always come after their new parent step
- If using a UI, disable moves that would drop a commit onto its own subtree
Example fix
// before: destination index inside the moved stack's ancestry edit.move_stack(&[c3, c4], Position::after(c4)); // cycle // after: place the stack after a commit outside its own chain edit.move_stack(&[c3, c4], Position::after(c1));
Defensive patterns
Strategy: validation
Validate before calling
fn is_valid_move(steps: &[usize], dest_parent: usize) -> bool {
// destination parent must not be inside the moved stack's own ancestry
!steps.contains(&dest_parent)
} Try / catch
match result {
Err(e) if e.to_string().contains("commit cycle") => {
eprintln!("requested order loops onto itself; pick a destination outside the moved stack");
}
r => r?,
} Prevention
- Reject UI moves where the drop target is a descendant of any moved commit
- Validate plan parent chains terminate before submitting
- Keep moved stacks strictly after their new parent step
When it happens
Trigger: Calling the stack-move/reorder edit when the requested new position places a picked commit underneath (an ancestor of) itself, e.g. moving commit A to be a child of its own descendant B, or moving a stack to position 0 when the stack's target parent chain loops back to the stack.
Common situations: Interactive tooling that lets users drag commits in a history view and constructs an order that loops; scripted rebases computed from parent/child arrays with an off-by-one that makes the destination index fall inside the moved range's own ancestry.
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
- a rebase plan contains an invalid or duplicate pick
- merge commits cannot be picked by the rebase editor
- the checkout ancestry contains a cycle
- 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/8c9f1ff1a380e530.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/rebase.rs:747
continue;
}
let parent = new_parent[id];
if scope_set.contains(&parent) && !step_by_id.contains_key(&parent) {
continue;
}
let parent = step_by_id
.get(&parent)
.copied()
.map_or(PlanParent::Existing(parent), PlanParent::Step);
step_by_id.insert(*id, steps.len());
steps.push(PlanStep {
parent,
commit: PlanCommit::Pick(*id),
squash: Vec::new(),
});
}
if steps.len() == before {
anyhow::bail!("moving the stack would create a commit cycle");
}
}
let head_step = PlanParent::Step(step_by_id[&head]);
let mut ref_scope = scope.clone();
if !scope_set.contains(&target) {
ref_scope.push(target);
}
let ref_scope_set: HashSet<_> = ref_scope.iter().copied().collect();
let mut non_leaves = HashSet::new();
for id in &ref_scope {
non_leaves.extend(
graph
.parents_of(*id)
.context("an affected move commit is incomplete")?
.into_iter()
.filter(|parent| ref_scope_set.contains(parent)),
);View on GitHub (pinned to e73179060b)