Skip to main content

git_stub/
hash.rs

1// Copyright 2026 Oxide Computer Company
2
3//! Git commit hash types.
4
5use crate::CommitHashParseError;
6use std::{fmt, str::FromStr};
7
8/// A Git commit hash.
9///
10/// This type guarantees the contained value is either:
11///
12/// - 20 bytes (SHA-1, displayed as 40 lowercase hex characters)
13/// - 32 bytes (SHA-256, displayed as 64 lowercase hex characters)
14///
15/// # Parsing
16///
17/// Parse from a hex string using [`FromStr`]:
18///
19/// ```
20/// use git_stub::GitCommitHash;
21///
22/// let hash: GitCommitHash =
23///     "0123456789abcdef0123456789abcdef01234567".parse().unwrap();
24/// ```
25#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum GitCommitHash {
27    /// A SHA-1 hash: the one traditionally used in Git.
28    Sha1([u8; 20]),
29    /// A SHA-256 hash, supported by newer versions of Git.
30    Sha256([u8; 32]),
31}
32
33impl FromStr for GitCommitHash {
34    type Err = CommitHashParseError;
35
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        let len = s.len();
38        match len {
39            40 => {
40                let mut bytes = [0; 20];
41                hex::decode_to_slice(s, &mut bytes)
42                    .map_err(CommitHashParseError::InvalidHex)?;
43                Ok(GitCommitHash::Sha1(bytes))
44            }
45            64 => {
46                let mut bytes = [0; 32];
47                hex::decode_to_slice(s, &mut bytes)
48                    .map_err(CommitHashParseError::InvalidHex)?;
49                Ok(GitCommitHash::Sha256(bytes))
50            }
51            _ => Err(CommitHashParseError::InvalidLength(len)),
52        }
53    }
54}
55
56impl fmt::Display for GitCommitHash {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            GitCommitHash::Sha1(bytes) => hex::encode(bytes).fmt(f),
60            GitCommitHash::Sha256(bytes) => hex::encode(bytes).fmt(f),
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    const VALID_SHA1: &str = "0123456789abcdef0123456789abcdef01234567";
70    const VALID_SHA256: &str =
71        "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
72
73    #[test]
74    fn test_commit_hash_valid() {
75        let hash: GitCommitHash = VALID_SHA1.parse().unwrap();
76        assert_eq!(hash.to_string(), VALID_SHA1);
77
78        let hash: GitCommitHash = VALID_SHA256.parse().unwrap();
79        assert_eq!(hash.to_string(), VALID_SHA256);
80    }
81
82    #[test]
83    fn test_commit_hash_invalid() {
84        assert_eq!(
85            "abc123".parse::<GitCommitHash>(),
86            Err(CommitHashParseError::InvalidLength(6)),
87            "too short"
88        );
89
90        assert_eq!(
91            VALID_SHA1[..39].parse::<GitCommitHash>(),
92            Err(CommitHashParseError::InvalidLength(39)),
93            "39 chars (one short of SHA-1)"
94        );
95
96        let input = format!("{}0", VALID_SHA1);
97        assert_eq!(
98            input.parse::<GitCommitHash>(),
99            Err(CommitHashParseError::InvalidLength(41)),
100            "41 chars (one over SHA-1)"
101        );
102
103        assert!(
104            matches!(
105                "0123456789abcdefg123456789abcdef01234567"
106                    .parse::<GitCommitHash>(),
107                Err(CommitHashParseError::InvalidHex(_))
108            ),
109            "non-hex character 'g'"
110        );
111
112        let input = format!(" {}", VALID_SHA1);
113        assert_eq!(
114            input.parse::<GitCommitHash>(),
115            Err(CommitHashParseError::InvalidLength(41)),
116            "leading whitespace (the CommitHash parser doesn't do trimming)"
117        );
118    }
119
120    #[test]
121    fn test_commit_hash_empty_string() {
122        assert_eq!(
123            "".parse::<GitCommitHash>(),
124            Err(CommitHashParseError::InvalidLength(0)),
125            "empty string"
126        );
127    }
128}