{"record":{"id":"25f605181a1a28e3","repo":"gitbutlerapp/gitbutler","slug":"cannot-restore-a-snapshot-without-checkout-identit","errorCode":null,"errorMessage":"cannot restore a snapshot without checkout identity outside the workspace branch","messagePattern":"cannot restore a snapshot without checkout identity outside the workspace branch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/gitbutler-oplog/src/oplog.rs","lineNumber":1043,"sourceCode":"        {\n            bail!(\"snapshot checkout and workspace commits disagree\");\n        }\n    }\n    // Managed snapshots already identify their checkout through the workspace commit entry.\n    let restored_checkout = restored_checkout.or_else(|| {\n        restored_workspace_commit.map(|commit_id| SnapshotCheckout {\n            ref_name: workspace_ref.to_owned(),\n            commit_id,\n        })\n    });\n\n    let head = repo.head()?;\n    let head_ref = head\n        .referent_name()\n        .context(\"We will not change a worktree in detached HEAD state\")?;\n    // Snapshots with neither checkout identity nor a workspace commit retain the old guard.\n    if restored_checkout.is_none() && head_ref != workspace_ref {\n        bail!(\"cannot restore a snapshot without checkout identity outside the workspace branch\");\n    }\n\n    let gix_repo = ctx.clone_repo_for_merging()?;\n    let workdir_tree_id = get_workdir_tree(None, snapshot_commit_id, &gix_repo)?;\n\n    // Check out the snapshot's worktree while HEAD still points at the pre-restore commit:\n    // safe_checkout diffs from `before_restore_snapshot_workdir_tree_id`, so\n    // the workspace ref is repointed only afterwards (below).\n    but_core::worktree::safe_checkout_from_head(\n        workdir_tree_id,\n        &gix_repo,\n        but_core::worktree::checkout::Options {\n            // `workdir_tree_id` is the restored snapshot's full workdir tree, so it already\n            // contains the uncommitted changes captured at that point. `safe_checkout_from_head`\n            // otherwise re-applies the current uncommitted changes on top via a 3-way merge whose\n            // base is `HEAD^{tree}`, which makes those changes collide with the identical ones\n            // already in the destination. Using the pre-restore workdir tree as the merge base\n            // means the current uncommitted changes equal the base and cancel out, so the restored","sourceCodeStart":1025,"sourceCodeEnd":1061,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/gitbutler-oplog/src/oplog.rs#L1025-L1061","documentation":"Legacy (unmanaged) snapshots record no checkout identity, only a workspace commit. Restoring them is only safe while HEAD sits on the GitButler workspace branch — otherwise GitButler would overwrite the checkout of an unrelated branch. This bail is that guard: it fires when an identity-less snapshot is restored while HEAD is on some other branch (detached HEAD fails earlier with a different context error).","triggerScenarios":"Restoring an old snapshot with no `checkout/ref` record while repository HEAD points at any branch other than the workspace ref — e.g. the user checked out a real branch with plain git.","commonSituations":"Long undo histories spanning the snapshot format change; users checking out regular branches in the underlying repo and then using GitButler undo.","solutions":["Check out the GitButler workspace branch first, then retry the restore","Or restore a newer (managed) snapshot that carries checkout identity","Avoid checking out non-workspace branches directly in GitButler-managed repositories"],"exampleFix":"# before: HEAD is on 'main' while restoring an old snapshot -> bail\ngit checkout gitbutler/workspace\n# after: restore succeeds from the workspace branch\n# (retry the restore in GitButler)","handlingStrategy":"validation","validationCode":"let head_ref = repo\n    .head()?\n    .referent_name()\n    .context(\"detached HEAD\")?;\nif snapshot_checkout(&snapshot_tree, &repo)?.is_none() && head_ref != *workspace_ref {\n    return Err(anyhow::anyhow!(\"switch to the workspace branch before restoring this legacy snapshot\"));\n}","typeGuard":"fn can_restore_without_checkout_identity(snapshot_has_checkout: bool, head_ref: Option<&gix::refs::FullName>, workspace_ref: &gix::refs::FullName) -> bool {\n    snapshot_has_checkout || head_ref == Some(workspace_ref)\n}","tryCatchPattern":"match oplog.restore(snapshot_id) {\n    Err(err) if err.to_string().contains(\"without checkout identity\") => {\n        // check out the workspace branch, then retry\n    }\n    other => other,\n}","preventionTips":["Stay on the workspace branch in GitButler-managed repos","Prefer recent snapshots when undoing across format migrations"],"tags":["rust","gitbutler","oplog","restore","legacy-snapshot","head-detached-guard"],"backgroundTag":"legacy-snapshot-restore","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}