branchless/git/
oid.rs

1use std::ffi::{OsStr, OsString};
2use std::fmt::Display;
3use std::str::FromStr;
4
5use eyre::Context;
6use tracing::instrument;
7
8use crate::git::repo::wrap_git_error;
9
10/// Represents the ID of a Git object.
11#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct NonZeroOid {
13    pub(super) inner: git2::Oid,
14}
15
16impl NonZeroOid {
17    /// Convert this OID into its raw 20-byte slice.
18    pub fn as_bytes(&self) -> &[u8] {
19        self.inner.as_bytes()
20    }
21}
22
23impl std::fmt::Debug for NonZeroOid {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(f, "NonZeroOid({:?})", self.inner)
26    }
27}
28
29impl Display for NonZeroOid {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "{:?}", self.inner)
32    }
33}
34
35impl TryFrom<MaybeZeroOid> for NonZeroOid {
36    type Error = eyre::Error;
37
38    #[instrument]
39    fn try_from(value: MaybeZeroOid) -> Result<Self, Self::Error> {
40        match value {
41            MaybeZeroOid::NonZero(non_zero_oid) => Ok(non_zero_oid),
42            MaybeZeroOid::Zero => eyre::bail!("Expected a non-zero OID"),
43        }
44    }
45}
46
47impl TryFrom<OsString> for NonZeroOid {
48    type Error = eyre::Error;
49
50    #[instrument]
51    fn try_from(value: OsString) -> Result<Self, Self::Error> {
52        let value: &OsStr = &value;
53        value.try_into()
54    }
55}
56
57impl TryFrom<&OsStr> for NonZeroOid {
58    type Error = eyre::Error;
59
60    #[instrument]
61    fn try_from(value: &OsStr) -> Result<Self, Self::Error> {
62        let oid: MaybeZeroOid = value.try_into()?;
63        match oid {
64            MaybeZeroOid::Zero => eyre::bail!("OID was zero, but expected to be non-zero"),
65            MaybeZeroOid::NonZero(oid) => Ok(oid),
66        }
67    }
68}
69
70impl TryFrom<&[u8]> for NonZeroOid {
71    type Error = eyre::Error;
72
73    #[instrument]
74    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
75        let oid = MaybeZeroOid::try_from(value)?;
76        NonZeroOid::try_from(oid)
77    }
78}
79
80impl FromStr for NonZeroOid {
81    type Err = eyre::Error;
82
83    #[instrument]
84    fn from_str(value: &str) -> Result<Self, Self::Err> {
85        let oid: MaybeZeroOid = value.parse()?;
86        match oid {
87            MaybeZeroOid::NonZero(non_zero_oid) => Ok(non_zero_oid),
88            MaybeZeroOid::Zero => eyre::bail!("Expected a non-zero OID, but got: {:?}", value),
89        }
90    }
91}
92
93impl From<NonZeroOid> for git2::Oid {
94    fn from(oid: NonZeroOid) -> Self {
95        oid.inner
96    }
97}
98
99pub(super) fn make_non_zero_oid(oid: git2::Oid) -> NonZeroOid {
100    assert_ne!(oid, git2::Oid::zero());
101    NonZeroOid { inner: oid }
102}
103
104/// OID which may be zero or non-zero.
105///
106/// This exists because Git often represents the absence of an object using the
107/// zero OID. We want to statically check for those cases by using a more
108/// descriptive type.
109///
110/// This type is isomorphic to `Option<NonZeroOid>`. It should be used primarily
111/// when converting to and from string representations of OID values.
112#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
113pub enum MaybeZeroOid {
114    /// The zero OID (i.e. 40 `0`s).
115    Zero,
116
117    /// A non-zero OID.
118    NonZero(NonZeroOid),
119}
120
121impl Default for MaybeZeroOid {
122    fn default() -> Self {
123        Self::Zero
124    }
125}
126
127impl MaybeZeroOid {
128    /// Construct an OID from a raw 20-byte slice.
129    pub fn from_bytes(bytes: &[u8]) -> eyre::Result<Self> {
130        let oid = git2::Oid::from_bytes(bytes)?;
131        Ok(oid.into())
132    }
133}
134
135impl std::fmt::Debug for MaybeZeroOid {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(f, "{self}")
138    }
139}
140
141impl Display for MaybeZeroOid {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        let zero = git2::Oid::zero();
144        write!(
145            f,
146            "{:?}",
147            match self {
148                MaybeZeroOid::NonZero(NonZeroOid { inner }) => inner,
149                MaybeZeroOid::Zero => &zero,
150            }
151        )
152    }
153}
154
155impl FromStr for MaybeZeroOid {
156    type Err = eyre::Error;
157
158    fn from_str(s: &str) -> Result<Self, Self::Err> {
159        match s.parse() {
160            Ok(oid) if oid == git2::Oid::zero() => Ok(MaybeZeroOid::Zero),
161            Ok(oid) => Ok(MaybeZeroOid::NonZero(NonZeroOid { inner: oid })),
162            Err(err) => Err(wrap_git_error(err)).wrap_err("Could not parse OID from string"),
163        }
164    }
165}
166
167impl From<git2::Oid> for MaybeZeroOid {
168    fn from(oid: git2::Oid) -> Self {
169        if oid.is_zero() {
170            Self::Zero
171        } else {
172            Self::NonZero(make_non_zero_oid(oid))
173        }
174    }
175}
176
177impl TryFrom<&OsStr> for MaybeZeroOid {
178    type Error = eyre::Error;
179
180    fn try_from(value: &OsStr) -> Result<Self, Self::Error> {
181        match value.to_str() {
182            None => eyre::bail!("OID value was not a simple ASCII value: {:?}", value),
183            Some(value) => value.parse(),
184        }
185    }
186}
187
188impl TryFrom<OsString> for MaybeZeroOid {
189    type Error = eyre::Error;
190
191    fn try_from(value: OsString) -> Result<Self, Self::Error> {
192        MaybeZeroOid::try_from(value.as_os_str())
193    }
194}
195
196impl TryFrom<&[u8]> for MaybeZeroOid {
197    type Error = git2::Error;
198
199    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
200        let oid = git2::Oid::from_bytes(value)?;
201        Ok(MaybeZeroOid::from(oid))
202    }
203}
204
205impl From<NonZeroOid> for MaybeZeroOid {
206    fn from(oid: NonZeroOid) -> Self {
207        Self::NonZero(oid)
208    }
209}
210
211impl From<Option<NonZeroOid>> for MaybeZeroOid {
212    fn from(oid: Option<NonZeroOid>) -> Self {
213        match oid {
214            Some(oid) => MaybeZeroOid::NonZero(oid),
215            None => MaybeZeroOid::Zero,
216        }
217    }
218}
219
220impl From<MaybeZeroOid> for Option<NonZeroOid> {
221    fn from(oid: MaybeZeroOid) -> Self {
222        match oid {
223            MaybeZeroOid::Zero => None,
224            MaybeZeroOid::NonZero(oid) => Some(oid),
225        }
226    }
227}
228
229impl From<MaybeZeroOid> for git2::Oid {
230    fn from(oid: MaybeZeroOid) -> Self {
231        match oid {
232            MaybeZeroOid::Zero => git2::Oid::zero(),
233            MaybeZeroOid::NonZero(oid) => oid.into(),
234        }
235    }
236}