Skip to main content

uv_git_types/
oid.rs

1use std::fmt::{Debug, Display, Formatter};
2use std::str::{self, FromStr};
3
4use thiserror::Error;
5
6/// Unique identity of any Git object (commit, tree, blob, tag).
7///
8/// This type's `FromStr` implementation validates that it's exactly 40 hex characters, i.e. a
9/// full-length git commit.
10///
11/// If Git's SHA-256 support becomes more widespread in the future (in particular if GitHub ever
12/// adds support), we might need to make this an enum.
13#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct GitOid {
15    bytes: [u8; 40],
16}
17
18impl GitOid {
19    /// Return the string representation of an object ID.
20    pub fn as_str(&self) -> &str {
21        str::from_utf8(&self.bytes).unwrap()
22    }
23
24    /// Return a truncated representation, i.e., the first 16 characters of the SHA.
25    pub fn as_short_str(&self) -> &str {
26        &self.as_str()[..16]
27    }
28
29    /// Return a (very) truncated representation, i.e., the first 8 characters of the SHA.
30    pub fn as_tiny_str(&self) -> &str {
31        &self.as_str()[..8]
32    }
33}
34
35impl Debug for GitOid {
36    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
37        write!(f, "GitOid(\"{}\")", self.as_str())
38    }
39}
40
41#[derive(Debug, Error, PartialEq)]
42pub enum OidParseError {
43    #[error("Object ID cannot be parsed from empty string")]
44    Empty,
45    #[error("Object ID must be exactly 40 hex characters")]
46    WrongLength,
47    #[error("Object ID must be valid hex characters")]
48    NotHex,
49}
50
51impl FromStr for GitOid {
52    type Err = OidParseError;
53
54    fn from_str(s: &str) -> Result<Self, Self::Err> {
55        if s.is_empty() {
56            return Err(OidParseError::Empty);
57        }
58
59        if s.len() != 40 {
60            return Err(OidParseError::WrongLength);
61        }
62
63        if !s.chars().all(|ch| ch.is_ascii_hexdigit()) {
64            return Err(OidParseError::NotHex);
65        }
66
67        let mut bytes = [0; 40];
68        bytes.copy_from_slice(s.as_bytes());
69        Ok(Self { bytes })
70    }
71}
72
73impl Display for GitOid {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "{}", self.as_str())
76    }
77}
78
79impl serde::Serialize for GitOid {
80    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81    where
82        S: serde::Serializer,
83    {
84        self.as_str().serialize(serializer)
85    }
86}
87
88impl<'de> serde::Deserialize<'de> for GitOid {
89    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90    where
91        D: serde::Deserializer<'de>,
92    {
93        struct Visitor;
94
95        impl serde::de::Visitor<'_> for Visitor {
96            type Value = GitOid;
97
98            fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
99                f.write_str("a string")
100            }
101
102            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
103                GitOid::from_str(v).map_err(serde::de::Error::custom)
104            }
105        }
106
107        deserializer.deserialize_str(Visitor)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use std::str::FromStr;
114
115    use super::{GitOid, OidParseError};
116
117    #[test]
118    fn git_oid() {
119        GitOid::from_str("4a23745badf5bf5ef7928f1e346e9986bd696d82").unwrap();
120        GitOid::from_str("4A23745BADF5BF5EF7928F1E346E9986BD696D82").unwrap();
121
122        assert_eq!(GitOid::from_str(""), Err(OidParseError::Empty));
123        assert_eq!(
124            GitOid::from_str(&str::repeat("a", 41)),
125            Err(OidParseError::WrongLength)
126        );
127        assert_eq!(
128            GitOid::from_str(&str::repeat("a", 39)),
129            Err(OidParseError::WrongLength)
130        );
131        assert_eq!(
132            GitOid::from_str(&str::repeat("x", 40)),
133            Err(OidParseError::NotHex)
134        );
135    }
136}