{"record":{"id":"889781ed95fcc8ba","repo":"gitbutlerapp/gitbutler","slug":"change-id-is-ensured","errorCode":null,"errorMessage":"change ID is ensured","messagePattern":"change ID is ensured","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"info","filePath":"crates/but/src/id/mod.rs","lineNumber":1807,"sourceCode":"    ///\n    /// We dont show change ids for remote commits since they'll very likely conflict with local\n    /// commits which is confusing.\n    pub change_id: Option<but_core::ChangeId>,\n}\n\nimpl CommitId {\n    pub fn try_from_commit_id(\n        commit_id: gix::ObjectId,\n        repo: &gix::Repository,\n    ) -> anyhow::Result<Self> {\n        let commit = repo.find_commit(commit_id)?;\n        let commit = commit.decode()?;\n        let change_id =\n            but_core::commit::Headers::try_from_commit_headers(|| commit.extra_headers())\n                .unwrap_or_default()\n                .ensure_change_id(commit_id)\n                .change_id\n                .expect(\"change ID is ensured\");\n        Ok(Self {\n            commit_id,\n            change_id: Some(change_id),\n        })\n    }\n\n    pub fn as_ref(&self) -> CommitIdRef<'_> {\n        CommitIdRef {\n            commit_id: self.commit_id,\n            change_id: self.change_id.as_ref(),\n        }\n    }\n}\n\nimpl PartialEq for CommitId {\n    fn eq(&self, other: &Self) -> bool {\n        self.as_ref() == other.as_ref()\n    }","sourceCodeStart":1789,"sourceCodeEnd":1825,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but/src/id/mod.rs#L1789-L1825","documentation":"CommitId::try_from_commit_id maps a raw git object id to but's CommitId, which always carries a change ID. When the commit has no change-id GitButler header, Headers::ensure_change_id (crates/but-core/src/commit/mod.rs:68) unconditionally fills in a deterministic synthetic change ID derived from the commit id. The .expect asserts that post-condition on the Option field; since ensure_change_id always sets Some, the panic is an unreachable invariant in the current code.","triggerScenarios":"Any code path resolving a git commit to a CommitId (TUI commit selection, commands taking commit IDs) — normally fine even for headerless commits because ensure_change_id synthesizes the value. The expect fires only if ensure_change_id's semantics are changed to leave change_id as None (e.g. made fallible or synthetic IDs disabled).","commonSituations":"None for users of shipped builds. Developers refactoring but-core change-id/headers handling, or building a fork that removes synthetic change IDs, will see this panic the first time a plain (headerless) git commit is resolved.","solutions":["If hit on a modified build, verify Headers::ensure_change_id still sets change_id when it is None (crates/but-core/src/commit/mod.rs:68-76)","If the invariant is becoming conditional, convert to an explicit error: change_id.ok_or_else(|| anyhow!(...)) with context instead of expect","Keep the but-core tests for ensure_change_id (crates/but-core/tests/core/commit.rs:83) green when touching this area","Report upstream with a backtrace on unmodified builds"],"exampleFix":"// before\n.change_id.expect(\"change ID is ensured\")\n\n// after — explicit error path if the post-condition ever weakens\n.change_id.ok_or_else(|| anyhow!(\"change ID missing for commit {commit_id} after ensure_change_id\"))?","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["Unreachable as shipped — ensure_change_id always fills a None change ID; any hit means local modifications to but-core","Keep crates/but-core/tests/core/commit.rs ensure_change_id tests green when touching header logic","If making synthetic change IDs conditional, convert this expect to an explicit Result with context first"],"tags":["rust","invariant","change-id","git-commits","unreachable"],"backgroundTag":"internal-invariant-panic","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}