{"record":{"id":"487a19aafadb882c","repo":"gitbutlerapp/gitbutler","slug":"stack-has-no-id","errorCode":null,"errorMessage":"Stack has no ID","messagePattern":"Stack has no ID","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-api/src/legacy/absorb.rs","lineNumber":154,"sourceCode":"                perm.read_permission(),\n            )?;\n            let all_assignments = worktree_changes.assignments;\n            let dependencies = worktree_changes.dependencies;\n\n            // Get the stack ID for this branch\n            let stacks = crate::legacy::workspace::stacks(ctx, None)?;\n\n            // Find the stack that contains this branch\n            let stack = stacks\n                .iter()\n                .find(|s| {\n                    s.heads\n                        .iter()\n                        .any(|h| h.name.to_str().map(|n| n == branch_name).unwrap_or(false))\n                })\n                .ok_or_else(|| anyhow::anyhow!(\"Branch not found: {branch_name}\"))?;\n\n            let stack_id = stack.id.ok_or_else(|| anyhow::anyhow!(\"Stack has no ID\"))?;\n\n            // Filter assignments to just this stack\n            let candidates: Vec<AbsorbCandidate> = all_assignments\n                .into_iter()\n                .filter(|a| a.stack_id == Some(stack_id))\n                .map(Into::into)\n                .collect();\n\n            if candidates.is_empty() {\n                anyhow::bail!(\"No uncommitted changes assigned to branch: {branch_name}\");\n            }\n\n            (candidates, dependencies)\n        }\n        AbsorptionTarget::TreeChanges {\n            changes,\n            assigned_stack_id,\n        } => {","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but-api/src/legacy/absorb.rs#L136-L172","documentation":"After finding the stack containing the target branch, the absorb plan reads stack.id to filter hunk assignments by stack. If the stack entry carries no ID, planning aborts with 'Stack has no ID'. This is an internal invariant violation: current stacks are expected to always have IDs, so a None here means legacy or damaged workspace metadata.","triggerScenarios":"absorption_plan_with_perm(Branch) on workspace data where the matching stack's id field is None — older metadata formats, partially migrated projects, or corrupted stack entries from interrupted operations.","commonSituations":"Projects carried across major GitButler version upgrades without running migrations; manually edited metadata; restored databases.","solutions":["Recreate the stack (new stack, then move/recreate the branch in it) so it gets a fresh ID, and retry absorb.","Update GitButler so pending workspace metadata migrations run for the project.","If reproducible on a current version, report it — stacks without IDs should not be reachable."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"fn stack_has_id(stack: &Stack) -> bool {\n    stack.id.is_some()\n}","typeGuard":"fn usable_for_absorb(stack: &Stack) -> bool {\n    stack.id.is_some() && !stack.heads.is_empty()\n}","tryCatchPattern":"match absorption_plan_with_perm(ctx, target, perm) {\n    Err(err) if err.to_string().contains(\"Stack has no ID\") => {\n        rebuild_stack_metadata(ctx)?; // recreate the stack with a fresh ID; do not retry unchanged\n    }\n    other => other?,\n}","preventionTips":["Run pending app migrations before agent/absorb flows.","Fail fast on stacks lacking IDs when listing workspaces.","Back up workspace metadata across upgrades."],"tags":["absorb","stacks","metadata","invariant","workspace"],"backgroundTag":"corrupt-workspace-metadata","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}