gitbutlerapp/gitbutler · error
if it was found before it will be found as id
Error message
if it was found before it will be found as id
What it means
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.
Source
Thrown at crates/but-workspace/src/branch/apply.rs:822
/// Mark conflicting stacks as outside of the workspace commit.
///
/// This is used when the caller materializes a best-effort merge result despite conflicts. The
/// stack entries and branch metadata remain available, but their workspace relation is changed so
/// the conflicted branches are no longer represented by the checked-out workspace tree.
///
/// Each stack is expected to come from [correlate_conflicting_stacks], so a missing stack indicates
/// a programming error in the caller.
fn remove_conflicting_stacks_from_workspace(
ws_md: &mut Workspace,
conflicting_stacks: &[ConflictingStack],
) {
for conflicting_stack in conflicting_stacks {
let stack = ws_md
.stacks
.iter_mut()
.find(|s| s.id == conflicting_stack.id)
.expect("if it was found before it will be found as id");
// TODO: this might as well be 'Unmerged' to keep them in the workspace, but not let them be merged.
stack.workspacecommit_relation = Outside;
}
}
fn branch_has_applied_workspace_metadata(
branch: &FullNameRef,
ws: &but_graph::Workspace,
meta: &impl RefMetadata,
) -> anyhow::Result<bool> {
let Some(ws_ref_name) = ws.ref_name() else {
return Ok(true);
};
let Some(ws_md) = meta.workspace_opt(ws_ref_name)? else {
return Ok(true);
};
Ok(ws_md.find_branch(branch, StackKind::Applied).is_some()
|| (ws.is_entrypoint() && ws_ref_name == branch))View on GitHub (pinned to caf1f223d3)
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
Example fix
// before let stacks = correlate_conflicting_stacks(&ws_loaded_earlier, ...); apply_branch(&mut ws_md_fresh, ..., &stacks)?; // ids may not line up // after: correlate against the exact metadata being applied let stacks = correlate_conflicting_stacks(&ws_md, ...); assert!(stacks.iter().all(|s| ws_md.stacks.iter().any(|t| t.id == s.id))); apply_branch(&mut ws_md, ..., &stacks)?;
Defensive patterns
Strategy: validation
Validate before calling
// Correlate and apply against the same metadata snapshot:
let ws_md: Workspace = load_workspace_metadata(repo)?; // single read
let conflicting = correlate_conflicting_stacks(&ws_md, ...);
debug_assert!(
conflicting.iter().all(|c| ws_md.stacks.iter().any(|s| s.id == c.id)),
"conflicting stacks must come from this metadata"
);
apply_branch(repo, &mut ws_md, ..., &conflicting)?; Try / catch
let r = std::panic::catch_unwind(AssertUnwindSafe(|| apply_branch(...)));
if r.is_err() {
// metadata may have raced: reload and retry once with a fresh correlation
reload_and_retry(repo)?;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- AtSegment anchor always has oldest commit resolved
- a committed transaction always materializes a workspace
- anchor is always present in the order at this point
- checked all selectors are present
- validated non-empty squash commit list
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/bdf038b0d38c120f.
Report an issue: GitHub.