1use std::collections::BTreeMap;
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{Error, Result};
11
12#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
17#[serde(try_from = "String", into = "String")]
18pub struct GitId(pub String);
19
20impl TryFrom<String> for GitId {
21 type Error = Error;
22
23 fn try_from(s: String) -> Result<Self> {
24 let id = Self::parse(&s)?;
25 if id.0 != s {
26 return Err(Error::Invalid(format!("git id {s:?}: not in lowercase")));
27 }
28 Ok(id)
29 }
30}
31
32impl From<GitId> for String {
33 fn from(id: GitId) -> String {
34 id.0
35 }
36}
37
38const HEX_LEN: usize = 40;
40
41impl GitId {
42 pub fn parse(s: &str) -> Result<Self> {
44 if s.len() != HEX_LEN || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
45 return Err(Error::Invalid(format!(
46 "git id {s:?}: not {HEX_LEN} hex digits"
47 )));
48 }
49 Ok(Self(s.to_ascii_lowercase()))
50 }
51
52 pub fn short(&self) -> &str {
54 let end = self
55 .0
56 .char_indices()
57 .nth(10)
58 .map_or(self.0.len(), |(i, _)| i);
59 &self.0[..end]
60 }
61}
62
63impl fmt::Display for GitId {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str(&self.0)
66 }
67}
68
69impl fmt::Debug for GitId {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "GitId({})", self.short())
72 }
73}
74
75#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
76#[serde(rename_all = "lowercase")]
77pub enum FileMode {
78 Regular,
79 Executable,
80 Symlink,
81 Submodule,
83}
84
85#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
86pub struct FileEntry {
87 pub mode: FileMode,
88 pub blob: GitId,
89}
90
91pub type FileMap = BTreeMap<String, FileEntry>;
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn an_id_is_forty_hex_digits_kept_in_lowercase() {
100 let lower = "0123456789abcdef0123456789abcdef01234567";
101 assert_eq!(GitId::parse(lower).unwrap().0, lower);
102 let upper = lower.to_ascii_uppercase();
103 assert_eq!(GitId::parse(&upper).unwrap().0, lower);
104 for bad in [
105 "",
106 &lower[..39],
107 &format!("{lower}8"),
108 &lower.replace('a', "g"),
109 &"0".repeat(64),
110 ] {
111 assert!(GitId::parse(bad).is_err(), "{bad}");
112 }
113 let wide = format!("{}é", &lower[..38]);
115 assert_eq!(wide.len(), 40);
116 assert!(GitId::parse(&wide).is_err());
117 assert_eq!(GitId(lower.into()).short(), "0123456789");
118 assert_eq!(GitId("abc".into()).short(), "abc");
119 assert_eq!(format!("{:?}", GitId(lower.into())), "GitId(0123456789)");
120 assert_eq!(GitId(lower.into()).to_string(), lower);
121 assert_eq!(GitId("ééééééééééé".into()).short(), "éééééééééé");
123 assert_eq!(GitId("ab".repeat(8)).short(), "ababababab");
124 }
125
126 #[test]
128 fn a_git_id_is_read_from_json_only_as_written() {
129 let lower = "0123456789abcdef0123456789abcdef01234567";
130 let id: GitId = serde_json::from_value(serde_json::json!(lower)).unwrap();
131 assert_eq!(id.0, lower);
132 assert_eq!(serde_json::to_value(&id).unwrap(), serde_json::json!(lower));
133 let upper = lower.to_uppercase();
134 for bad in [
135 "",
136 "abc",
137 "ééééééééééééééééééé",
138 &format!("{}é", &lower[..38]),
139 &upper,
140 ] {
141 let r = serde_json::from_value::<GitId>(serde_json::json!(bad));
142 assert!(r.is_err(), "{bad}");
143 }
144 }
145}