GitoxideLabs/gitoxide · error

internal nodes have no object ID key

Error message

internal nodes have no object ID key

What it means

`Node::key()` in `gix-note` returns the identifier for a note or subtree node; `Internal` fanout nodes carry no object-ID key, so the arm panics. Hitting it means the note-tree traversal stored/encountered an internal node where only leaf-bearing nodes are expected.

Solutions

  1. Report upstream with the notes tree shape (e.g. `git notes` output and fanout levels).
  2. Update `gix-note` to the latest version.
  3. If maintaining, make `key()` return `Option<&oid>` or an error for internal nodes.

Example fix

// before
Node::Internal(_) => unreachable!("internal nodes have no object ID key"),
// after
Node::Internal(_) => return Err(message("internal note node has no key"))
Defensive patterns

Strategy: try-catch

Validate before calling

// Sanity-check the notes ref exists and looks like a note tree before use
// repo.find_reference("refs/notes/commits")?.peel_to_kind(gix_object::Kind::Tree)?;

Try / catch

// Wrap note operations in catch_unwind or a subprocess if robustness matters.
let out = std::panic::catch_unwind(|| notes.get(note_id));

Prevention

When it happens

Trigger: Only if the note tree in-memory structure invariants break (e.g. a bug in `insert`/`load_subtree` placing an `Internal` node where `key()` is called). Not triggered by note content or repository data alone.

Common situations: Should never occur; would surface while inserting or looking up notes in a fanout-style notes tree if traversal logic regressed.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/6b994a4635d271e3. Report an issue: GitHub.

Appendix: source

Thrown at gix-note/src/lib.rs:257

    ///
    /// This performance-sensitive trie stores `prefix` and `prefix_len` separately so it can compare whole bytes
    /// directly. Notes fanout advances by two hexadecimal digits at a time, so it does not need the nibble-granular
    /// lengths supported by [`gix_hash::Prefix`].
    prefix: ObjectId,
    /// The number of leading bytes in `prefix` that are significant.
    prefix_len: usize,
    /// The slash-separated path from the notes root to this fanout subtree.
    path: BString,
    /// The object ID of the on-disk tree represented by this subtree.
    tree_id: ObjectId,
}

impl Node {
    fn key(&self) -> &oid {
        match self {
            Node::Note(note) => &note.annotated_object_id,
            Node::Subtree(subtree) => &subtree.prefix,
            Node::Internal(_) => unreachable!("internal nodes have no object ID key"),
        }
    }
}

impl Subtree {
    fn contains(&self, id: &oid) -> bool {
        self.prefix.as_bytes()[..self.prefix_len] == id.as_bytes()[..self.prefix_len]
    }
}

impl InternalNode {
    fn get(
        &mut self,
        annotated_object_id: &oid,
        nibble: usize,
        objects: &impl Find,
        non_notes: &mut Vec<TreeEntry>,
    ) -> Result<Option<ObjectId>, Error> {

View on GitHub (pinned to e73179060b)