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}