{"record":{"id":"debc9e97affd4255","repo":"GitoxideLabs/gitoxide","slug":"review-finish-cannot-rewrite-merge-descendants","errorCode":null,"errorMessage":"review finish cannot rewrite merge descendants","messagePattern":"review finish cannot rewrite merge descendants","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gix-tix/src/edit/rebase.rs","lineNumber":1427,"sourceCode":"            })\n            .unwrap_or_default()\n    } else {\n        HashSet::new()\n    };\n    if !checkout_path.is_empty() {\n        reject_pending_checkout_path(\n            &repo,\n            checkout.as_ref().expect(\"a non-empty checkout path has a checkout\").0,\n        )?;\n    }\n    for id in review_ids.iter().chain(&natural_ids) {\n        if graph\n            .parents_of(*id)\n            .context(\"a review descendant is incomplete\")?\n            .len()\n            > 1\n        {\n            anyhow::bail!(\"review finish cannot rewrite merge descendants\");\n        }\n    }\n\n    let mut rewritten = HashMap::<ObjectId, Option<ObjectId>>::new();\n    let mut note_rewrites = Vec::new();\n    let mut finished_review = None;\n    let mut conflict = None;\n    for old in &review_ids {\n        let old_parents = graph.parents_of(*old).context(\"a review descendant is incomplete\")?;\n        let mut commit = repo.find_commit(*old)?.decode()?.into_owned()?;\n        let new_parents = if *old == review {\n            vec![tip]\n        } else {\n            old_parents\n                .iter()\n                .filter_map(|parent| rewritten.get(parent).copied().unwrap_or(Some(*parent)))\n                .collect()\n        };","sourceCodeStart":1409,"sourceCodeEnd":1445,"githubUrl":"https://github.com/GitoxideLabs/gitoxide/blob/e73179060badf27222d790981fac3f84c1830a7e/gix-tix/src/edit/rebase.rs#L1409-L1445","documentation":"`review finish` rewrites the ancestry of the finished review's commits, which the rebase machinery can only do for linear history. When any descendant commit of the review has more than one parent (a merge commit), the rewrite cannot be expressed, so the library bails with this message.","triggerScenarios":"Finishing a review whose merged/branch-derived commits have descendants that are merge commits — detected via `graph.parents_of(id).len() > 1` for every review descendant.","commonSituations":"A developer branched off a review branch and merged it back before the review was finished; CI automation created merge commits on top of the review stack; a team policy of merge-instead-of-rebase downstream of review branches.","solutions":["Rebase the merge descendants onto a linearized version of the review branch first (flatten the merges), then run review finish","Finish the review before downstream merges are created","Ask downstream authors to rebase instead of merging onto the review branch"],"exampleFix":"// before: merge commit M(=parents[A_review, X]) on top of review\nrepo.review_finish(review_id)?; // bails\n// after: linearize descendants first\nrebase_descendants_onto_linearized(&repo, review_tip)?;\nrepo.review_finish(review_id)?;","handlingStrategy":"validation","validationCode":"fn has_merge_descendants(graph: &Graph, tips: &[ObjectId]) -> bool {\n    tips.iter().all(|id| graph.parents_of(id).unwrap().len() <= 1)\n}","typeGuard":null,"tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"merge descendants\") => {\n        eprintln!(\"linearize downstream merges before finishing the review\");\n    }\n    r => r?,\n}","preventionTips":["Finish reviews before merging them downstream","Prefer rebase over merge onto review branches","Scan descendants for multi-parent commits before invoking finish"],"tags":["rebase","merge-commit","review"],"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"}