git-bug 0.2.4

A rust library for interfacing with git-bug repositories
Documentation
// git-bug-rs - A rust library for interfacing with git-bug repositories
//
// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of git-bug-rs/git-gub.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/agpl.txt>.

//! [`Ids`][`Id`] are needed to provide globally consistent references to
//! [`Entities`][`super::Entity`].
//!
//! The use will probably only interface with [`Prefixes`][`IdPrefix`].

use std::fmt::Display;

use combined_id::CombinedId;
use prefix::IdPrefix;
use serde::{Deserialize, Serialize};

pub mod combined_id;
pub mod entity_id;
pub mod prefix;

/// The Id is an identifier for an entity or part of an entity
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
pub struct Id {
    /// The 64 hex-chars composing the underlying sha256 hash.
    sha256_value: [u8; 32],
}

impl Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut buf = [0; 64];
        write!(f, "{}", self.hex_to_buf(&mut buf))
    }
}

impl std::fmt::Debug for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Sha256(")?;
        for b in self.as_bytes() {
            write!(f, "{b:02x}")?;
        }
        f.write_str(")")
    }
}

#[allow(missing_docs)]
pub mod decode {
    use std::str::FromStr;

    #[derive(Debug, thiserror::Error)]
    pub enum Error {
        #[error("Your hex input buffer was not 64 bytes long, but: {0}")]
        InvalidLen(usize),

        #[error("Your hex input contained an invalid char. The buffer is: '{0:?}'")]
        InvalidChar(Vec<u8>),
    }

    impl FromStr for super::Id {
        type Err = Error;

        fn from_str(s: &str) -> Result<Self, Self::Err> {
            Self::from_hex(s.as_bytes())
        }
    }

    impl TryFrom<&str> for super::Id {
        type Error = Error;

        fn try_from(value: &str) -> Result<Self, Self::Error> {
            <Self as FromStr>::from_str(value)
        }
    }
    impl TryFrom<String> for super::Id {
        type Error = Error;

        fn try_from(value: String) -> Result<Self, Self::Error> {
            <Self as FromStr>::from_str(&value)
        }
    }
}

impl From<Id> for String {
    fn from(value: Id) -> Self {
        value.to_string()
    }
}

impl Id {
    /// Parse the `buffer` as a sequence of hex bytes from which to decode use.
    ///
    /// # Note
    /// This means that the length of the buffer must be 64.
    ///
    /// # Errors
    /// This will return an error, if the `buffer` cannot be interpreted as
    /// sequence of hex bytes, or is too long/short.
    pub fn from_hex(buffer: &[u8]) -> Result<Self, decode::Error> {
        if buffer.len() != 64 {
            return Err(decode::Error::InvalidLen(buffer.len()));
        }

        let mut buf = [0; 32];
        faster_hex::hex_decode(buffer, &mut buf).map_err(|err| match err {
            faster_hex::Error::InvalidChar => decode::Error::InvalidChar(buffer.to_owned()),
            faster_hex::Error::InvalidLength(len) => {
                unreachable!("Bug: Id Length is invalid but was checked: {len}")
            }
            faster_hex::Error::Overflow => unreachable!("We allocated enough"),
        })?;

        Ok(Self { sha256_value: buf })
    }

    /// Instantiate this Id from already decoded hex bytes.
    ///
    /// These bytes can be the result of a sha256 hashing operation.
    ///
    /// # Panics
    /// If your bytes are not exactly 32 elements long.
    pub(crate) fn from_sha256_hash(input: &[u8]) -> Self {
        let mut buffer = [0; 32];
        buffer.copy_from_slice(input);
        Self {
            sha256_value: buffer,
        }
    }

    fn as_bytes(&self) -> &[u8] {
        &self.sha256_value
    }

    /// Return the raw byte slice representing this [`Id`].
    #[must_use]
    pub fn as_slice(&self) -> &[u8] {
        &self.sha256_value
    }

    /// Write ourselves to the `out` in hexadecimal notation, returning the
    /// hex-string ready for display.
    ///
    /// # Panics
    /// If the buffer isn't big enough to hold twice as many bytes as stored (32
    /// * 2).
    pub fn hex_to_buf<'a>(&self, buf: &'a mut [u8]) -> &'a mut str {
        let num_hex_bytes = self.sha256_value.len() * 2;
        faster_hex::hex_encode(self.as_bytes(), &mut buf[..num_hex_bytes]).expect("We can count")
    }

    /// Shorten an [`Id`] to the required length, so that it stays unique, but
    /// is still short enough for human consumption.
    #[must_use]
    pub fn shorten(&self) -> IdPrefix {
        (*self).into()
    }

    /// Combine this id with another [`Id`].
    ///
    /// This is useful, if you want to get a shorten prefix that can be used to direct to a comment
    /// (for example).
    ///
    /// # Note
    /// The underlying algorithm assumes that there are more
    /// [`Ids`][`Id`] for the type of `self` than for `other.`
    #[must_use]
    pub fn combine_with(self, other: Id) -> CombinedId {
        CombinedId {
            primary_id: self,
            secondary_id: other,
        }
    }
}