{"record":{"id":"6bc4d6d3c361cdc2","repo":"gitbutlerapp/gitbutler","slug":"commit-is-not-a-snapshot","errorCode":null,"errorMessage":"Commit is not a snapshot","messagePattern":"Commit is not a snapshot","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/gitbutler-oplog/src/oplog.rs","lineNumber":266,"sourceCode":"    ) -> Result<gix::ObjectId> {\n        let PreparedSnapshot {\n            tree_id,\n            target_base_oid,\n        } = prepare_snapshot_with_target(self, perm.read_permission())?;\n        let repo = self.repo.get()?;\n        commit_snapshot(self, &repo, tree_id, details, perm, target_base_oid)\n    }\n\n    #[instrument(skip(self), err(Debug))]\n    fn get_snapshot(&self, sha: gix::ObjectId) -> Result<Snapshot> {\n        let repo = self.repo.get()?;\n        let commit = repo.find_commit(sha)?;\n        let details = commit\n            .message_raw()?\n            .to_str()\n            .ok()\n            .and_then(|msg| SnapshotDetails::from_str(msg).ok())\n            .ok_or(anyhow!(\"Commit is not a snapshot\"))?;\n\n        let snapshot = Snapshot {\n            commit_id: sha,\n            created_at: commit.time()?,\n            details: Some(details),\n        };\n        Ok(snapshot)\n    }\n\n    #[instrument(skip(self), err(Debug))]\n    fn snapshots_iter(\n        &self,\n        oplog_commit_id: Option<gix::ObjectId>,\n        exclude_kind: Vec<OperationKind>,\n        include_kind: Option<Vec<OperationKind>>,\n    ) -> Result<impl Iterator<Item = Result<Snapshot>>> {\n        let repo = self.repo.get()?.clone();\n        let next_commit_id = match oplog_commit_id {","sourceCodeStart":248,"sourceCodeEnd":284,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/gitbutler-oplog/src/oplog.rs#L248-L284","documentation":"OplogExt::get_snapshot (crates/gitbutler-oplog/src/oplog.rs) treats a commit as an oplog snapshot only if its raw commit message parses into SnapshotDetails (via SnapshotDetails::from_str). If the message is not valid UTF-8 or does not parse, the commit is rejected with this error. Not every commit in the repository is an oplog entry — only those created by the oplog with serialized SnapshotDetails in the message.","triggerScenarios":"Calling get_snapshot(sha) with an arbitrary workspace/user commit sha instead of one produced by the oplog; passing a sha obtained from user branches, gitbutler/workspace, or stash-like commits; an oplog commit written by a much older version whose message format no longer parses; a commit whose message was rewritten externally.","commonSituations":"Frontend or SDK code feeding a branch head sha where a snapshot sha is expected; deserialization drift after format changes between app versions; sha read from a stale settings/URL parameter; restoring by sha copied from an old timeline entry after a format change.","solutions":["Only pass shas that come from the oplog itself: oplog_head() or snapshots_iter() entries","If integrating by sha from external input, validate it first by walking snapshots_iter and matching the id","If snapshots from an old version fail to parse, update the app — parsers keep compatibility for known formats","Inspect the commit directly (`git cat-file commit <sha>`) to confirm whether the message really lacks SnapshotDetails"],"exampleFix":"// before\nlet snap = ctx.get_snapshot(user_provided_sha)?; // user commit -> 'Commit is not a snapshot'\n\n// after: only use shas produced by the oplog\nlet Some(head) = ctx.oplog_head()? else {\n    return Ok(());\n};\nlet snap = ctx.get_snapshot(head)?;","handlingStrategy":"validation","validationCode":"// only accept shas the oplog itself produced\nlet valid: HashSet<gix::ObjectId> = ctx\n    .snapshots_iter(None, vec![], None)?\n    .filter_map(|s| s.ok().map(|s| s.commit_id))\n    .collect();\nif !valid.contains(&requested_sha) {\n    anyhow::bail!(\"{requested_sha} is not an oplog snapshot\");\n}\nlet snap = ctx.get_snapshot(requested_sha)?;","typeGuard":"fn is_oplog_snapshot(ctx: &Context, sha: gix::ObjectId) -> bool {\n    ctx.snapshots_iter(None, vec![], None)\n        .map(|mut it| it.any(|s| s.ok().is_some_and(|s| s.commit_id == sha)))\n        .unwrap_or(false)\n}","tryCatchPattern":"match ctx.get_snapshot(sha) {\n    Ok(snap) => Ok(Some(snap)),\n    Err(err) if err.to_string().contains(\"Commit is not a snapshot\") => Ok(None), // caller passed a non-oplog commit\n    Err(err) => Err(err),\n}","preventionTips":["Never feed user/branch commit shas into get_snapshot — source shas only from oplog_head()/snapshots_iter()","Treat external sha input (URLs, saved settings) as untrusted and verify against the oplog before use","Keep snapshot message parsing backward compatible when changing SnapshotDetails formats"],"tags":["gitbutler","oplog","snapshot","undo","serialization"],"backgroundTag":"oplog-snapshot-parse-failed","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}