{"record":{"id":"42ac49932a7c72af","repo":"GitoxideLabs/gitoxide","slug":"descendant-merge-commits-cannot-be-squashed","errorCode":null,"errorMessage":"descendant merge commits cannot be squashed","messagePattern":"descendant merge commits cannot be squashed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gix-tix/src/edit/rebase.rs","lineNumber":472,"sourceCode":"    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\")?;\n        if parents.len() > 1 {\n            anyhow::bail!(\"descendant merge commits cannot be squashed\");\n        }\n        non_leaves.extend(parents.into_iter().filter(|parent| scope_set.contains(parent)));\n    }\n    let tips: Vec<_> = scope.iter().copied().filter(|id| !non_leaves.contains(id)).collect();\n    let mut steps = Vec::with_capacity(scope.len().saturating_sub(1));\n    let mut step_by_id = HashMap::new();\n    for id in scope.iter().copied().filter(|id| *id != source) {\n        let original_parent = graph\n            .parents_of(id)\n            .and_then(|parents| parents.first().copied())\n            .context(\"an affected squash commit has no parent\")?;\n        let parent = if original_parent == source {\n            source_parent\n        } else {\n            original_parent\n        };\n        let parent = step_by_id\n            .get(&parent)","sourceCodeStart":454,"sourceCodeEnd":490,"githubUrl":"https://github.com/GitoxideLabs/gitoxide/blob/e73179060badf27222d790981fac3f84c1830a7e/gix-tix/src/edit/rebase.rs#L454-L490","documentation":"After determining the set of descendant commits affected by a squash, tix requires each of them to be a non-merge commit; a merge commit among the descendants would need its second parent's history rewritten or duplicated, which the squash rewrite does not support. The plan is rejected as soon as any scoped commit has more than one parent.","triggerScenarios":"Squashing a commit that has descendants (between target and source) containing at least one merge commit — `graph.parents_of(id)` returns len > 1 for some id in `descendants_in_parent_order(target)`.","commonSituations":"Squashing an early commit in a branch-based history where feature branches were merged back in after the target commit; histories with `--no-ff` merges between the squash target and source.","solutions":["Choose a squash target/source pair that encloses no merge commits (a purely linear range).","Rebase the merges away (linearize) before squashing, if losing merge topology is acceptable.","Use `git rebase --rebase-merges`-style tooling or a manual edit if preserving merges while rewriting is required."],"exampleFix":"// before: range target..source contains a merge\nplan_squash(repo, &graph, after_merge_commit, early_commit)?\n\n// after: limit the squash to the linear segment below the merge\nplan_squash(repo, &graph, last_linear_commit_before_merge, early_commit)?","handlingStrategy":"validation","validationCode":"// Rust: verify the affected range has no merge commits before squashing\nlet scope: Vec<_> = graph.descendants_in_parent_order(target)?.collect();\nfor id in &scope {\n    if graph.parents_of(*id)?.len() > 1 {\n        eprintln!(\"merge commit {id} in range; squash not supported\");\n    }\n}","typeGuard":"fn range_is_linear(scope: &[ObjectId], graph: &HistoryGraph) -> bool {\n    scope.iter().all(|id| graph.parents_of(*id).map(|p| p.len() <= 1).unwrap_or(false))\n}","tryCatchPattern":null,"preventionTips":["Keep the target..source range linear (no `--no-ff` merges inside it).","Linearize or split the squash into ranges that exclude merges.","Prefer `git rebase --rebase-merges` when merge topology must be preserved."],"tags":["rebase","squash","merge-commit","history-graph","git"],"backgroundTag":"unsupported-operation","analyzedSha":"e73179060badf27222d790981fac3f84c1830a7e","analyzedAt":"2026-09-08T11:26:50.865Z","contentChangedAt":"2026-09-08T11:26:50.865Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}