Skip to main content

lonca_core/
objects.rs

1//! The plain data of the object store: ids, file modes and flattened trees.
2//! No Git library: a client of the daemon reads and writes these without
3//! the store (feature `store`).
4
5use std::collections::BTreeMap;
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{Error, Result};
11
12/// A Git object id in lowercase hex.
13#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct GitId(pub String);
16
17/// Hex digits in a SHA-1 object id, the only kind Lonca stores.
18const HEX_LEN: usize = 40;
19
20impl GitId {
21    /// A SHA-1 id: 40 hex digits, either case, kept in lowercase.
22    pub fn parse(s: &str) -> Result<Self> {
23        if s.len() != HEX_LEN || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
24            return Err(Error::Invalid(format!(
25                "git id {s:?}: not {HEX_LEN} hex digits"
26            )));
27        }
28        Ok(Self(s.to_ascii_lowercase()))
29    }
30
31    pub fn short(&self) -> &str {
32        &self.0[..self.0.len().min(10)]
33    }
34}
35
36impl fmt::Display for GitId {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        f.write_str(&self.0)
39    }
40}
41
42impl fmt::Debug for GitId {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "GitId({})", self.short())
45    }
46}
47
48#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum FileMode {
51    Regular,
52    Executable,
53    Symlink,
54    /// A submodule commit; carried through unchanged, never materialized.
55    Submodule,
56}
57
58#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
59pub struct FileEntry {
60    pub mode: FileMode,
61    pub blob: GitId,
62}
63
64/// A flattened tree: repository path -> entry.
65pub type FileMap = BTreeMap<String, FileEntry>;
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn an_id_is_forty_hex_digits_kept_in_lowercase() {
73        let lower = "0123456789abcdef0123456789abcdef01234567";
74        assert_eq!(GitId::parse(lower).unwrap().0, lower);
75        let upper = lower.to_ascii_uppercase();
76        assert_eq!(GitId::parse(&upper).unwrap().0, lower);
77        for bad in [
78            "",
79            &lower[..39],
80            &format!("{lower}8"),
81            &lower.replace('a', "g"),
82            &"0".repeat(64),
83        ] {
84            assert!(GitId::parse(bad).is_err(), "{bad}");
85        }
86        // Not a byte count: a multi-byte character of the right length.
87        let wide = format!("{}é", &lower[..38]);
88        assert_eq!(wide.len(), 40);
89        assert!(GitId::parse(&wide).is_err());
90        assert_eq!(GitId(lower.into()).short(), "0123456789");
91        assert_eq!(GitId("abc".into()).short(), "abc");
92        assert_eq!(format!("{:?}", GitId(lower.into())), "GitId(0123456789)");
93        assert_eq!(GitId(lower.into()).to_string(), lower);
94    }
95}