GitoxideLabs/gitoxide · error
the squash target must have exactly one parent
Error message
the squash target must have exactly one parent
What it means
The squash algorithm rewrites the target commit's single-parent base onto the new combined tree; it therefore requires the target to have exactly one parent. If the target is a root commit (no parents) or a merge commit (multiple parents), the rewrite cannot be expressed and the plan is rejected.
Solutions
- Choose a non-root, non-merge commit as the squash target.
- If the goal is to edit the root commit, use a root-aware edit/rebase workflow instead of squash.
- Linearize history first so the intended target becomes an ordinary single-parent commit.
Example fix
// before: squashing into the root commit plan_squash(repo, &graph, tip, root_commit)? // error: the squash target must have exactly one parent // after: squash into the root's first non-root descendant's parent chain, // or use a root-edit operation plan_squash(repo, &graph, tip, first_non_root_commit)?
Defensive patterns
Strategy: validation
Validate before calling
// Rust: ensure the target has exactly one parent
let parents = graph.parents_of(target)?;
if parents.len() != 1 { eprintln!("squash target must not be a root or merge commit"); } Type guard
fn is_single_parent(commit: ObjectId, graph: &HistoryGraph) -> bool {
graph.parents_of(commit).map(|p| p.len() == 1).unwrap_or(false)
} Prevention
- Do not squash into the repository's root commit; use root-edit workflows instead.
- Exclude merge commits from squash-target selection.
When it happens
Trigger: Calling squash planning with a `target` whose `graph.parents_of(target)` yields anything other than exactly one parent — a root commit (0 parents) or a merge commit (2+ parents).
Common situations: Squashing into the repository's very first commit (root, no parent); accidentally choosing a merge commit as the squash target.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- the squash source must have exactly one parent
- descendant merge commits cannot be squashed
- the squash target must be a strict ancestor of the source
- copying a commit requires it to have exactly one parent
- copying a commit cannot rewrite root or merge commits
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/0c58437473b4f802.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/rebase.rs:459
graph: &HistoryGraph,
source: ObjectId,
target: ObjectId,
) -> Result<Plan> {
if source == target || !graph.is_ancestor(target, source) {
anyhow::bail!("the squash target must be a strict ancestor of the source");
}
let source_parents = graph
.parents_of(source)
.context("the squash source is not in the loaded history")?;
let [source_parent] = source_parents.as_slice() else {
anyhow::bail!("the squash source must have exactly one parent");
};
let source_parent = *source_parent;
let target_parents = graph
.parents_of(target)
.context("the squash target is not in the loaded history")?;
let [base] = target_parents.as_slice() else {
anyhow::bail!("the squash target must have exactly one parent");
};
let base = *base;
let scope = graph
.descendants_in_parent_order(target)
.context("the squash target is not in the loaded history")?;
let scope_set: HashSet<_> = scope.iter().copied().collect();
let mut non_leaves = HashSet::new();
for id in &scope {
let parents = graph
.parents_of(*id)
.context("an affected squash commit is incomplete")?;
if parents.len() > 1 {
anyhow::bail!("descendant merge commits cannot be squashed");
}
non_leaves.extend(parents.into_iter().filter(|parent| scope_set.contains(parent)));
}
let tips: Vec<_> = scope.iter().copied().filter(|id| !non_leaves.contains(id)).collect();
let mut steps = Vec::with_capacity(scope.len().saturating_sub(1));View on GitHub (pinned to e73179060b)