Skip to main content

claw_store/
head.rs

1use claw_core::id::ObjectId;
2
3use crate::layout::RepoLayout;
4use crate::refs;
5use crate::StoreError;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum HeadState {
9    Symbolic { ref_name: String },
10    Detached { target: ObjectId },
11}
12
13pub fn read_head(layout: &RepoLayout) -> Result<HeadState, StoreError> {
14    let path = layout.head_file();
15    if !path.exists() {
16        return Ok(HeadState::Symbolic {
17            ref_name: "heads/main".to_string(),
18        });
19    }
20    let content = std::fs::read_to_string(&path)?;
21    let trimmed = content.trim();
22    if let Some(ref_name) = trimmed.strip_prefix("ref: ") {
23        Ok(HeadState::Symbolic {
24            ref_name: ref_name.to_string(),
25        })
26    } else {
27        let id = ObjectId::from_hex(trimmed)?;
28        Ok(HeadState::Detached { target: id })
29    }
30}
31
32pub fn write_head(layout: &RepoLayout, state: &HeadState) -> Result<(), StoreError> {
33    let content = match state {
34        HeadState::Symbolic { ref_name } => format!("ref: {}\n", ref_name),
35        HeadState::Detached { target } => format!("{}\n", target.to_hex()),
36    };
37    std::fs::write(layout.head_file(), content)?;
38    Ok(())
39}
40
41pub fn resolve_head(layout: &RepoLayout) -> Result<Option<ObjectId>, StoreError> {
42    let state = read_head(layout)?;
43    match state {
44        HeadState::Symbolic { ref_name } => refs::read_ref(layout, &ref_name),
45        HeadState::Detached { target } => Ok(Some(target)),
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use claw_core::hash::content_hash;
53    use claw_core::object::TypeTag;
54
55    #[test]
56    fn head_symbolic_roundtrip() {
57        let tmp = tempfile::tempdir().unwrap();
58        let layout = RepoLayout::new(tmp.path());
59        layout.create_dirs().unwrap();
60
61        let state = HeadState::Symbolic {
62            ref_name: "heads/main".to_string(),
63        };
64        write_head(&layout, &state).unwrap();
65        let read_back = read_head(&layout).unwrap();
66        assert_eq!(read_back, state);
67    }
68
69    #[test]
70    fn head_detached_roundtrip() {
71        let tmp = tempfile::tempdir().unwrap();
72        let layout = RepoLayout::new(tmp.path());
73        layout.create_dirs().unwrap();
74
75        let id = content_hash(TypeTag::Blob, b"test");
76        let state = HeadState::Detached { target: id };
77        write_head(&layout, &state).unwrap();
78        let read_back = read_head(&layout).unwrap();
79        assert_eq!(read_back, state);
80    }
81
82    #[test]
83    fn resolve_head_symbolic() {
84        let tmp = tempfile::tempdir().unwrap();
85        let layout = RepoLayout::new(tmp.path());
86        layout.create_dirs().unwrap();
87
88        let id = content_hash(TypeTag::Blob, b"test");
89        refs::write_ref(&layout, "heads/main", &id).unwrap();
90        write_head(
91            &layout,
92            &HeadState::Symbolic {
93                ref_name: "heads/main".to_string(),
94            },
95        )
96        .unwrap();
97
98        let resolved = resolve_head(&layout).unwrap();
99        assert_eq!(resolved, Some(id));
100    }
101}