{"record":{"id":"f5a137f7b13a0e99","repo":"GitoxideLabs/gitoxide","slug":"the-squash-source-must-have-exactly-one-parent","errorCode":null,"errorMessage":"the squash source must have exactly one parent","messagePattern":"the squash source must have exactly one parent","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gix-tix/src/edit/rebase.rs","lineNumber":452,"sourceCode":"        }\n    }\n    Ok(out)\n}\n\npub(crate) fn squash_plan(\n    repo: &gix::Repository,\n    graph: &HistoryGraph,\n    source: ObjectId,\n    target: ObjectId,\n) -> Result<Plan> {\n    if source == target || !graph.is_ancestor(target, source) {\n        anyhow::bail!(\"the squash target must be a strict ancestor of the source\");\n    }\n    let source_parents = graph\n        .parents_of(source)\n        .context(\"the squash source is not in the loaded history\")?;\n    let [source_parent] = source_parents.as_slice() else {\n        anyhow::bail!(\"the squash source must have exactly one parent\");\n    };\n    let source_parent = *source_parent;\n    let target_parents = graph\n        .parents_of(target)\n        .context(\"the squash target is not in the loaded history\")?;\n    let [base] = target_parents.as_slice() else {\n        anyhow::bail!(\"the squash target must have exactly one parent\");\n    };\n    let base = *base;\n    let scope = graph\n        .descendants_in_parent_order(target)\n        .context(\"the squash target is not in the loaded history\")?;\n    let scope_set: HashSet<_> = scope.iter().copied().collect();\n    let mut non_leaves = HashSet::new();\n    for id in &scope {\n        let parents = graph\n            .parents_of(*id)\n            .context(\"an affected squash commit is incomplete\")?;","sourceCodeStart":434,"sourceCodeEnd":470,"githubUrl":"https://github.com/GitoxideLabs/gitoxide/blob/e73179060badf27222d790981fac3f84c1830a7e/gix-tix/src/edit/rebase.rs#L434-L470","documentation":"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.","triggerScenarios":"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]`).","commonSituations":"Trying to squash a merge commit into its first parent; selecting a merge commit in the interactive history view as the squash source.","solutions":["Pick a non-merge commit as the squash source; squash its child or a leaf commit instead.","First linearize history (rebase to drop the merge) if merging is not required, then squash.","Use a different operation for merge commits (e.g. reword or drop) rather than squash."],"exampleFix":"// before: squashing a merge commit\nplan_squash(repo, &graph, merge_commit, base)?\n\n// after: squash the linear child of the target instead\nplan_squash(repo, &graph, linear_commit_after_target, target)?","handlingStrategy":"validation","validationCode":"// Rust: ensure the source is a non-merge commit\nlet parents = graph.parents_of(source)?;\nif parents.len() != 1 { eprintln!(\"pick a non-merge commit as squash source\"); }","typeGuard":"fn is_linear(commit: ObjectId, graph: &HistoryGraph) -> bool {\n    graph.parents_of(commit).map(|p| p.len() == 1).unwrap_or(false)\n}","tryCatchPattern":null,"preventionTips":["Exclude merge commits from squash-source selection in tooling/UIs.","Linearize history before squashing around merges."],"tags":["rebase","squash","merge-commit","git","precondition-failed"],"backgroundTag":"unsupported-operation","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"}