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

  1. 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)
  2. If the invariant is becoming conditional, convert to an explicit error: change_id.ok_or_else(|| anyhow!(...)) with context instead of expect
  3. Keep the but-core tests for ensure_change_id (crates/but-core/tests/core/commit.rs:83) green when touching this area
  4. 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

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


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/889781ed95fcc8ba. Report an issue: GitHub.