{"record":{"id":"bdf038b0d38c120f","repo":"gitbutlerapp/gitbutler","slug":"if-it-was-found-before-it-will-be-found-as-id","errorCode":null,"errorMessage":"if it was found before it will be found as id","messagePattern":"if it was found before it will be found as id","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-workspace/src/branch/apply.rs","lineNumber":822,"sourceCode":"\n/// Mark conflicting stacks as outside of the workspace commit.\n///\n/// This is used when the caller materializes a best-effort merge result despite conflicts. The\n/// stack entries and branch metadata remain available, but their workspace relation is changed so\n/// the conflicted branches are no longer represented by the checked-out workspace tree.\n///\n/// Each stack is expected to come from [correlate_conflicting_stacks], so a missing stack indicates\n/// a programming error in the caller.\nfn remove_conflicting_stacks_from_workspace(\n    ws_md: &mut Workspace,\n    conflicting_stacks: &[ConflictingStack],\n) {\n    for conflicting_stack in conflicting_stacks {\n        let stack = ws_md\n            .stacks\n            .iter_mut()\n            .find(|s| s.id == conflicting_stack.id)\n            .expect(\"if it was found before it will be found as id\");\n        // TODO: this might as well be 'Unmerged' to keep them in the workspace, but not let them be merged.\n        stack.workspacecommit_relation = Outside;\n    }\n}\n\nfn branch_has_applied_workspace_metadata(\n    branch: &FullNameRef,\n    ws: &but_graph::Workspace,\n    meta: &impl RefMetadata,\n) -> anyhow::Result<bool> {\n    let Some(ws_ref_name) = ws.ref_name() else {\n        return Ok(true);\n    };\n    let Some(ws_md) = meta.workspace_opt(ws_ref_name)? else {\n        return Ok(true);\n    };\n    Ok(ws_md.find_branch(branch, StackKind::Applied).is_some()\n        || (ws.is_entrypoint() && ws_ref_name == branch))","sourceCodeStart":804,"sourceCodeEnd":840,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-workspace/src/branch/apply.rs#L804-L840","documentation":"Panic from `expect(\"if it was found before it will be found as id\")` in `remove_conflicting_stacks_from_workspace` (but-workspace apply.rs:822). The function walks `conflicting_stacks` (documented to come from `correlate_conflicting_stacks`) and looks each one up in `ws_md.stacks` by id. The invariant is that the stacks were found in this very metadata moments earlier; a miss means the caller passed stacks correlated against a different or mutated Workspace.","triggerScenarios":"Calling the apply flow with `conflicting_stacks` computed from an older/newer `Workspace` metadata snapshot than the `ws_md` being edited (the stack ids differ after re-serialization or an id change); a TOCTOU where the workspace metadata was reloaded between `correlate_conflicting_stacks` and `remove_conflicting_stacks_from_workspace`; any internal refactor that regenerates stack ids between the two calls.","commonSituations":"Concurrent workspace edits (two app windows or a CLI + desktop app racing apply-branch); an external process rewriting GitButler metadata files mid-operation; version changes in but-workspace that altered stack id derivation.","solutions":["Ensure `conflicting_stacks` and `ws_md` come from the same metadata read — re-correlate right before apply instead of reusing a stale result","Serialize workspace-mutating operations (single writer / take the app's workspace lock) so metadata cannot change between correlate and apply","Reload workspace metadata and retry the whole apply-branch operation if it is older than the on-disk state","If single-threaded and still hitting it, capture both id sets and report as a but-workspace bug"],"exampleFix":"// before\nlet stacks = correlate_conflicting_stacks(&ws_loaded_earlier, ...);\napply_branch(&mut ws_md_fresh, ..., &stacks)?; // ids may not line up\n\n// after: correlate against the exact metadata being applied\nlet stacks = correlate_conflicting_stacks(&ws_md, ...);\nassert!(stacks.iter().all(|s| ws_md.stacks.iter().any(|t| t.id == s.id)));\napply_branch(&mut ws_md, ..., &stacks)?;","handlingStrategy":"validation","validationCode":"// Correlate and apply against the same metadata snapshot:\nlet ws_md: Workspace = load_workspace_metadata(repo)?; // single read\nlet conflicting = correlate_conflicting_stacks(&ws_md, ...);\ndebug_assert!(\n    conflicting.iter().all(|c| ws_md.stacks.iter().any(|s| s.id == c.id)),\n    \"conflicting stacks must come from this metadata\"\n);\napply_branch(repo, &mut ws_md, ..., &conflicting)?;","typeGuard":null,"tryCatchPattern":"let r = std::panic::catch_unwind(AssertUnwindSafe(|| apply_branch(...)));\nif r.is_err() {\n    // metadata may have raced: reload and retry once with a fresh correlation\n    reload_and_retry(repo)?;\n}","preventionTips":["Take the app's workspace lock so only one writer mutates metadata at a time","Never reuse correlation results across a metadata reload","Retry apply-branch from scratch (reload + correlate) rather than patching stale ids"],"tags":["rust","panic","expect","workspace-metadata","stack-ids","race-condition","apply-branch","gitbutler"],"backgroundTag":"stale-snapshot-id-mismatch-panic","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}