gitbutlerapp/gitbutler · warning

but_meta::Stack::derived_name: Stack is uninitialized

Error message

but_meta::Stack::derived_name: Stack is uninitialized

What it means

Returned by Stack::derived_name in crates/but-meta/src/virtual_branches_legacy_types.rs when the legacy Stack has an empty heads vector — derived_name takes the name of the top-most head (heads.last()), and with no heads there is no name to derive, so the stack is treated as uninitialized.

Source

Thrown at crates/but-meta/src/virtual_branches_legacy_types.rs:97

        )]
        #[serde(default)]
        pub updated_timestamp_ms: u128,
        #[deprecated(note = "Legacy field, do not use. Kept for backwards compatibility.")]
        #[serde(default)]
        pub name: String,
        #[deprecated(note = "Legacy field, do not use. Kept for backwards compatibility.")]
        #[serde(with = "but_serde::object_id")]
        #[serde(default = "default_null_object_id")]
        pub head: gix::ObjectId,
    }

    impl Stack {
        /// This is the name of the top-most branch, provided by the API for convenience.
        pub fn derived_name(&self) -> anyhow::Result<String> {
            self.heads
                .last()
                .map(|head| head.name.clone())
                .ok_or_else(|| anyhow!("but_meta::Stack::derived_name: Stack is uninitialized"))
        }
    }

    fn default_null_object_id() -> gix::ObjectId {
        gix::hash::Kind::Sha1.null()
    }

    fn default_true() -> bool {
        true
    }
    fn default_false() -> bool {
        false
    }

    fn serialize_u128<S>(x: &u128, s: S) -> anyhow::Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Guard callers: skip stacks whose heads are empty instead of calling derived_name on them.
  2. Filter during migration so only initialized stacks (heads non-empty) are converted.
  3. If every stack is empty, the source metadata is malformed — restore from backup or reinitialize.

Example fix

// before
let name = stack.derived_name()?;

// after
let name = stack
    .heads
    .last()
    .map(|head| head.name.clone())
    .unwrap_or_else(|| "(uninitialized)".to_string());
Defensive patterns

Strategy: type-guard

Validate before calling

// Only derive names for initialized stacks
for stack in stacks {
    if stack.heads.is_empty() {
        continue; // skip uninitialized legacy stack
    }
    let name = stack.derived_name()?;
}

Type guard

fn stack_is_initialized(stack: &Stack) -> bool {
    !stack.heads.is_empty()
}

Try / catch

let name = match stack.derived_name() {
    Ok(n) => n,
    Err(e) if e.to_string().contains("uninitialized") => {
        tracing::warn!("skipping uninitialized stack {:?}", stack); continue;
    },
    Err(e) => return Err(e),
};

Prevention

When it happens

Trigger: Deserializing a legacy stack entry that lists no heads (serde default empty vec) and then calling derived_name() on it; stacks created but never assigned branches in old data; migration code calling derived_name over all stacks without filtering.

Common situations: Processing imported or hand-written legacy metadata containing zero-head stacks; a partially written stack entry from an old crash; iterating stacks from an older format where heads were stored separately and the link was lost.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/fde06ee8cf3dc69a. Report an issue: GitHub.