GitoxideLabs/gitoxide · error

the squash source must have exactly one parent

Error message

the squash source must have exactly one parent

What it means

Squashing a commit into its target is only defined when the source has exactly one parent: the rewritten history replaces the single parent link. Merge commits (two or more parents) or graph queries that fail to find the source's parents cannot be squashed this way, so tix rejects the plan.

Solutions

  1. Pick a non-merge commit as the squash source; squash its child or a leaf commit instead.
  2. First linearize history (rebase to drop the merge) if merging is not required, then squash.
  3. Use a different operation for merge commits (e.g. reword or drop) rather than squash.

Example fix

// before: squashing a merge commit
plan_squash(repo, &graph, merge_commit, base)?

// after: squash the linear child of the target instead
plan_squash(repo, &graph, linear_commit_after_target, target)?
Defensive patterns

Strategy: validation

Validate before calling

// Rust: ensure the source is a non-merge commit
let parents = graph.parents_of(source)?;
if parents.len() != 1 { eprintln!("pick a non-merge commit as squash source"); }

Type guard

fn is_linear(commit: ObjectId, graph: &HistoryGraph) -> bool {
    graph.parents_of(commit).map(|p| p.len() == 1).unwrap_or(false)
}

Prevention

When it happens

Trigger: Calling squash planning with a `source` commit that is a merge commit, or whose parent lookup in the `HistoryGraph` returns more than one parent (`parents_of(source)` not matching `[one]`).

Common situations: Trying to squash a merge commit into its first parent; selecting a merge commit in the interactive history view as the squash source.

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


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/f5a137f7b13a0e99. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/edit/rebase.rs:452

        }
    }
    Ok(out)
}

pub(crate) fn squash_plan(
    repo: &gix::Repository,
    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")?;

View on GitHub (pinned to e73179060b)