gitbutlerapp/gitbutler · error
cannot restore a snapshot without checkout identity outside
Error message
cannot restore a snapshot without checkout identity outside the workspace branch
What it means
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).
Source
Thrown at crates/gitbutler-oplog/src/oplog.rs:1043
{
bail!("snapshot checkout and workspace commits disagree");
}
}
// Managed snapshots already identify their checkout through the workspace commit entry.
let restored_checkout = restored_checkout.or_else(|| {
restored_workspace_commit.map(|commit_id| SnapshotCheckout {
ref_name: workspace_ref.to_owned(),
commit_id,
})
});
let head = repo.head()?;
let head_ref = head
.referent_name()
.context("We will not change a worktree in detached HEAD state")?;
// Snapshots with neither checkout identity nor a workspace commit retain the old guard.
if restored_checkout.is_none() && head_ref != workspace_ref {
bail!("cannot restore a snapshot without checkout identity outside the workspace branch");
}
let gix_repo = ctx.clone_repo_for_merging()?;
let workdir_tree_id = get_workdir_tree(None, snapshot_commit_id, &gix_repo)?;
// Check out the snapshot's worktree while HEAD still points at the pre-restore commit:
// safe_checkout diffs from `before_restore_snapshot_workdir_tree_id`, so
// the workspace ref is repointed only afterwards (below).
but_core::worktree::safe_checkout_from_head(
workdir_tree_id,
&gix_repo,
but_core::worktree::checkout::Options {
// `workdir_tree_id` is the restored snapshot's full workdir tree, so it already
// contains the uncommitted changes captured at that point. `safe_checkout_from_head`
// otherwise re-applies the current uncommitted changes on top via a 3-way merge whose
// base is `HEAD^{tree}`, which makes those changes collide with the identical ones
// already in the destination. Using the pre-restore workdir tree as the merge base
// means the current uncommitted changes equal the base and cancel out, so the restoredView on GitHub (pinned to caf1f223d3)
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
Example fix
# before: HEAD is on 'main' while restoring an old snapshot -> bail git checkout gitbutler/workspace # after: restore succeeds from the workspace branch # (retry the restore in GitButler)
Defensive patterns
Strategy: validation
Validate before calling
let head_ref = repo
.head()?
.referent_name()
.context("detached HEAD")?;
if snapshot_checkout(&snapshot_tree, &repo)?.is_none() && head_ref != *workspace_ref {
return Err(anyhow::anyhow!("switch to the workspace branch before restoring this legacy snapshot"));
} Type guard
fn can_restore_without_checkout_identity(snapshot_has_checkout: bool, head_ref: Option<&gix::refs::FullName>, workspace_ref: &gix::refs::FullName) -> bool {
snapshot_has_checkout || head_ref == Some(workspace_ref)
} Try / catch
match oplog.restore(snapshot_id) {
Err(err) if err.to_string().contains("without checkout identity") => {
// check out the workspace branch, then retry
}
other => other,
} Prevention
- Stay on the workspace branch in GitButler-managed repos
- Prefer recent snapshots when undoing across format migrations
When it happens
Trigger: 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.
Common situations: Long undo histories spanning the snapshot format change; users checking out regular branches in the underlying repo and then using GitButler undo.
Related errors
- targetRef in project_meta.toml is not a remote-tracking bran
- targetCommitId in project_meta.toml is null
- Invalid conflict stage '{}'
- snapshot checkout ref is not a local branch
- snapshot checkout commit is null
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/25f605181a1a28e3.
Report an issue: GitHub.