{"record":{"id":"8ac40657f282e981","repo":"gitbutlerapp/gitbutler","slug":"cannot-unapply-branch-branch-from-an-ad-hoc-wo-8ac406","errorCode":null,"errorMessage":"Cannot unapply branch '{branch}' from an ad-hoc workspace because non-tip branches can only disappear if their now removed metadata disambiguated them","messagePattern":"Cannot unapply branch '(.+?)' from an ad-hoc workspace because non-tip branches can only disappear if their now removed metadata disambiguated them","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-workspace/src/branch/unapply.rs","lineNumber":270,"sourceCode":"            bail!(\"Cannot unapply a branch from an ad-hoc detached workspace\");\n        };\n        let mut ws_md = meta.workspace(workspace_ref_name.as_ref())?;\n        if ws.kind.has_managed_ref() || ws.has_metadata() {\n            ws.reconcile_metadata(&mut ws_md)?;\n        }\n        let branch_removed_from_ws_meta = ws_md.unapply_branch(branch);\n        if !branch_removed_from_ws_meta {\n            // The branch wasn't in workspace metadata, yet it was present, so also delete its branch metadata\n            // as it could be used to disambiguate the segment.\n            // TODO: this will actually be observable even if it doens't work, unless it's run in a transaction, which right now it's not!\n            //       Should be able to redo the traversal with an overlay that hides branch metadata, but I'd say it's not important enough.\n            meta.remove(branch)?;\n            let graph = ws\n                .graph\n                .redo_traversal_with_overlay(repo, meta, Overlay::default())?;\n            let workspace = graph.into_workspace()?;\n            if workspace.refname_is_segment(branch) {\n                bail!(\n                    \"Cannot unapply branch '{branch}' from an ad-hoc workspace because non-tip branches can only disappear if their now removed metadata disambiguated them\",\n                    branch = branch.shorten()\n                );\n            }\n            return Ok(Outcome::new(Cow::Owned(workspace)));\n        }\n\n        // Everything past this point is stricly in non-dry-run mode and we may totally end up in intermediate states\n        // if something fails.\n        // Redo the traversal with the changed workspace metadata so code below can rely on the reconciled version.\n        let ws = ws\n            .graph\n            .redo_traversal_with_overlay(\n                repo,\n                meta,\n                Overlay::default()\n                    .with_dropped_references([branch.to_owned()])\n                    .with_workspace_metadata_override(Some((","sourceCodeStart":252,"sourceCodeEnd":288,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-workspace/src/branch/unapply.rs#L252-L288","documentation":"After removing the branch's metadata, unapply re-runs the traversal with an empty overlay to see whether the branch really disappears from the workspace. If it still resolves as a segment, the branch is not the tip of its own history — other refs make it visible — and removing only its metadata cannot make it vanish. The operation aborts rather than leave a half-removed state.","triggerScenarios":"Unapplying a branch in an ad-hoc workspace where the branch is non-tip: it is still reachable/visible through other refs or segments after its disambiguating metadata is gone.","commonSituations":"Stacked branches sharing history where the lower branch ref still points into the stack; workspaces where the branch was only projected thanks to metadata that has now been removed.","solutions":["Unapply branches top-down (tip first) so each removal actually detaches the segment.","Delete or move the other refs that keep the branch visible (e.g. the branch's own git ref) before unapplying.","Use the workspace unapply flow, which handles ref and metadata disposal together."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// before unapplying, verify the branch would disappear once its metadata is dropped\nlet graph = ws.graph.redo_traversal_with_overlay(repo, meta, Overlay::default())?;\nlet check = graph.into_workspace()?;\nif !check.refname_is_segment(branch) || check.refname_is_segment(branch) {\n    // cheap pre-check: only attempt when the branch owns its tip\n}\n// simplest reliable guard: unapply stacks top-down (tip branches first)","typeGuard":null,"tryCatchPattern":"match unapply(repo, meta, &ws, branch, opts) {\n    Err(err) if err.to_string().contains(\"non-tip branches can only disappear\") => {\n        ui::info(\"Unapply the branch above this one first\");\n        Ok(default_outcome())\n    }\n    other => other,\n}","preventionTips":["Always unapply stacked branches top-down, tip first.","For lower stacked branches, delete their git ref as part of the operation.","Surface stack order in the UI so users unapply in a valid sequence."],"tags":["gitbutler","unapply","stacked-branches","metadata","rust"],"backgroundTag":"branch-still-referenced","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}