gitbutlerapp/gitbutler · info
change ID is ensured
Error message
change ID is ensured
What it means
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.
Source
Thrown at crates/but/src/id/mod.rs:1807
///
/// We dont show change ids for remote commits since they'll very likely conflict with local
/// commits which is confusing.
pub change_id: Option<but_core::ChangeId>,
}
impl CommitId {
pub fn try_from_commit_id(
commit_id: gix::ObjectId,
repo: &gix::Repository,
) -> anyhow::Result<Self> {
let commit = repo.find_commit(commit_id)?;
let commit = commit.decode()?;
let change_id =
but_core::commit::Headers::try_from_commit_headers(|| commit.extra_headers())
.unwrap_or_default()
.ensure_change_id(commit_id)
.change_id
.expect("change ID is ensured");
Ok(Self {
commit_id,
change_id: Some(change_id),
})
}
pub fn as_ref(&self) -> CommitIdRef<'_> {
CommitIdRef {
commit_id: self.commit_id,
change_id: self.change_id.as_ref(),
}
}
}
impl PartialEq for CommitId {
fn eq(&self, other: &Self) -> bool {
self.as_ref() == other.as_ref()
}View on GitHub (pinned to 2497b8007a)
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
Example fix
// before
.change_id.expect("change ID is ensured")
// after — explicit error path if the post-condition ever weakens
.change_id.ok_or_else(|| anyhow!("change ID missing for commit {commit_id} after ensure_change_id"))? Defensive patterns
Strategy: validation
Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- deduping a NonEmpty will never make it empty
- programs was just checked to be non-empty
- below u16::MAX
- target OID must exist when ahead calculation is enabled
- target OID must exist when merge check is enabled
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/889781ed95fcc8ba.
Report an issue: GitHub.