1use blake3::Hash as Blake3Hash;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::path::PathBuf;
11use thiserror::Error;
12
13#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
23pub struct Hash(pub [u8; 32]);
24
25impl Hash {
26 pub fn from_data(data: &[u8]) -> Self {
28 Self(*blake3::hash(data).as_bytes())
29 }
30
31 pub fn from_hex(hex: &str) -> Result<Self, CommonError> {
33 if hex.len() != 64 {
34 return Err(CommonError::InvalidHashLength(hex.len()));
35 }
36 let mut bytes = [0u8; 32];
37 hex.as_bytes()
38 .chunks_exact(2)
39 .zip(bytes.iter_mut())
40 .try_for_each(|(chunk, byte)| {
41 *byte = u8::from_str_radix(
42 std::str::from_utf8(chunk).map_err(|_| CommonError::InvalidHex)?,
43 16,
44 )
45 .map_err(|_| CommonError::InvalidHex)?;
46 Ok::<_, CommonError>(())
47 })?;
48 Ok(Self(bytes))
49 }
50
51 pub fn to_hex(&self) -> String {
53 self.0.iter().map(|b| format!("{b:02x}")).collect()
54 }
55
56 pub const ZERO: Self = Self([0u8; 32]);
58
59 pub fn as_blake3(&self) -> &Blake3Hash {
61 unsafe { &*(&self.0 as *const [u8; 32] as *const Blake3Hash) }
65 }
66}
67
68impl fmt::Debug for Hash {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "Hash({})", self.to_hex())
71 }
72}
73
74impl fmt::Display for Hash {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 let hex = self.to_hex();
78 write!(f, "{}…", &hex[..12])
79 }
80}
81
82impl From<Blake3Hash> for Hash {
83 fn from(h: Blake3Hash) -> Self {
84 Self(*h.as_bytes())
85 }
86}
87
88impl From<[u8; 32]> for Hash {
89 fn from(bytes: [u8; 32]) -> Self {
90 Self(bytes)
91 }
92}
93
94pub type PatchId = Hash;
96
97#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
99pub struct BranchName(pub String);
100
101impl BranchName {
102 pub fn new(name: impl Into<String>) -> Result<Self, CommonError> {
103 let s = name.into();
104 if s.is_empty() {
105 return Err(CommonError::EmptyBranchName);
106 }
107 if s.contains('\0') {
109 return Err(CommonError::InvalidBranchName(s));
110 }
111 Ok(Self(s))
112 }
113
114 pub fn as_str(&self) -> &str {
115 &self.0
116 }
117}
118
119impl fmt::Debug for BranchName {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 write!(f, "Branch({})", self.0)
122 }
123}
124
125impl fmt::Display for BranchName {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 write!(f, "{}", self.0)
128 }
129}
130
131impl AsRef<str> for BranchName {
132 fn as_ref(&self) -> &str {
133 &self.0
134 }
135}
136
137#[derive(Error, Debug)]
143pub enum CommonError {
144 #[error("invalid hash length: expected 64 hex chars, got {0}")]
145 InvalidHashLength(usize),
146
147 #[error("invalid hexadecimal string")]
148 InvalidHex,
149
150 #[error("branch name must not be empty")]
151 EmptyBranchName,
152
153 #[error("invalid branch name: {0}")]
154 InvalidBranchName(String),
155
156 #[error("I/O error: {0}")]
157 Io(#[from] std::io::Error),
158
159 #[error("{0}")]
160 Custom(String),
161}
162
163#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
169pub struct RepoPath(pub String);
170
171impl RepoPath {
172 pub fn new(path: impl Into<String>) -> Result<Self, CommonError> {
173 let s = path.into();
174 if s.is_empty() {
175 return Err(CommonError::Custom("repo path must not be empty".into()));
176 }
177 Ok(Self(s))
178 }
179
180 pub fn as_str(&self) -> &str {
181 &self.0
182 }
183
184 pub fn to_path_buf(&self) -> PathBuf {
185 PathBuf::from(&self.0)
186 }
187}
188
189impl fmt::Debug for RepoPath {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 write!(f, "RepoPath({})", self.0)
192 }
193}
194
195impl fmt::Display for RepoPath {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 write!(f, "{}", self.0)
198 }
199}
200
201#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
207pub enum FileStatus {
208 Added,
210 Modified,
212 Deleted,
214 Clean,
216 Untracked,
218}
219
220#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_hash_from_data_deterministic() {
230 let data = b"hello, suture";
231 let h1 = Hash::from_data(data);
232 let h2 = Hash::from_data(data);
233 assert_eq!(h1, h2, "Hash must be deterministic");
234 }
235
236 #[test]
237 fn test_hash_different_data() {
238 let h1 = Hash::from_data(b"hello");
239 let h2 = Hash::from_data(b"world");
240 assert_ne!(h1, h2, "Different data must produce different hashes");
241 }
242
243 #[test]
244 fn test_hash_hex_roundtrip() {
245 let data = b"test data for hex roundtrip";
246 let hash = Hash::from_data(data);
247 let hex = hash.to_hex();
248 assert_eq!(hex.len(), 64, "Hex string must be 64 characters");
249
250 let parsed = Hash::from_hex(&hex).expect("Valid hex must parse");
251 assert_eq!(hash, parsed, "Hex roundtrip must preserve hash");
252 }
253
254 #[test]
255 fn test_hash_from_hex_invalid() {
256 assert!(Hash::from_hex("too short").is_err());
257 assert!(Hash::from_hex("not hex!!characters!!64!!").is_err());
258 }
259
260 #[test]
261 fn test_hash_zero() {
262 let zero = Hash::ZERO;
263 assert_eq!(zero.to_hex(), "0".repeat(64));
264 }
265
266 #[test]
267 fn test_branch_name_valid() {
268 assert!(BranchName::new("main").is_ok());
269 assert!(BranchName::new("feature/my-feature").is_ok());
270 assert!(BranchName::new("fix-123").is_ok());
271 }
272
273 #[test]
274 fn test_branch_name_invalid() {
275 assert!(BranchName::new("").is_err());
276 assert!(BranchName::new("has\0null").is_err());
277 }
278
279 #[test]
280 fn test_repo_path() {
281 let path = RepoPath::new("src/main.rs").unwrap();
282 assert_eq!(path.as_str(), "src/main.rs");
283 }
284}