1use std::fs;
15use std::path::Path;
16
17use sley_core::{GitError, ObjectFormat, ObjectId, Result};
18use sley_protocol::ProtocolV2FetchShallowInfo;
19
20pub fn read_shallow(git_dir: &Path, format: ObjectFormat) -> Result<Vec<ObjectId>> {
24 let path = git_dir.join("shallow");
25 let contents = match fs::read_to_string(&path) {
26 Ok(contents) => contents,
27 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
28 Err(err) => return Err(err.into()),
29 };
30 let mut oids = Vec::new();
31 for line in contents.lines() {
32 let line = line.trim();
33 if line.is_empty() {
34 continue;
35 }
36 oids.push(ObjectId::from_hex(format, line).map_err(|err| {
37 GitError::InvalidFormat(format!("invalid oid in {}: {err}", path.display()))
38 })?);
39 }
40 Ok(oids)
41}
42
43pub fn write_shallow(git_dir: &Path, oids: &[ObjectId]) -> Result<()> {
47 let path = git_dir.join("shallow");
48 let mut hexes = oids.iter().map(ObjectId::to_hex).collect::<Vec<_>>();
49 hexes.sort();
50 hexes.dedup();
51 if hexes.is_empty() {
52 match fs::remove_file(&path) {
53 Ok(()) => {}
54 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
55 Err(err) => return Err(err.into()),
56 }
57 return Ok(());
58 }
59 let mut contents = String::with_capacity(hexes.iter().map(|hex| hex.len() + 1).sum());
60 for hex in &hexes {
61 contents.push_str(hex);
62 contents.push('\n');
63 }
64 fs::write(&path, contents)?;
65 Ok(())
66}
67
68pub fn apply_shallow_info(
74 git_dir: &Path,
75 format: ObjectFormat,
76 entries: &[ProtocolV2FetchShallowInfo],
77) -> Result<()> {
78 if entries.is_empty() {
79 return Ok(());
80 }
81 let mut oids = read_shallow(git_dir, format)?;
82 for entry in entries {
83 match entry {
84 ProtocolV2FetchShallowInfo::Shallow(oid) => {
85 if !oids.contains(oid) {
86 oids.push(*oid);
87 }
88 }
89 ProtocolV2FetchShallowInfo::Unshallow(oid) => oids.retain(|existing| existing != oid),
90 }
91 }
92 write_shallow(git_dir, &oids)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use std::sync::atomic::{AtomicU64, Ordering};
99
100 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
101
102 fn temp_dir() -> std::path::PathBuf {
103 let dir = std::env::temp_dir().join(format!(
104 "sley-remote-shallow-{}-{}",
105 std::process::id(),
106 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
107 ));
108 let _ = fs::remove_dir_all(&dir);
109 fs::create_dir_all(&dir).expect("create temp dir");
110 dir
111 }
112
113 fn oid(hex_byte: &str) -> ObjectId {
114 ObjectId::from_hex(ObjectFormat::Sha1, &hex_byte.repeat(40)).expect("valid oid")
115 }
116
117 #[test]
118 fn read_missing_shallow_is_empty() {
119 let dir = temp_dir();
120 assert!(
121 read_shallow(&dir, ObjectFormat::Sha1)
122 .expect("test operation should succeed")
123 .is_empty()
124 );
125 let _ = fs::remove_dir_all(&dir);
126 }
127
128 #[test]
129 fn write_sorts_dedups_and_round_trips() {
130 let dir = temp_dir();
131 let a = oid("1");
132 let b = oid("2");
133 write_shallow(&dir, &[b.clone(), a.clone(), b.clone()])
134 .expect("test operation should succeed");
135 let contents =
136 fs::read_to_string(dir.join("shallow")).expect("test operation should succeed");
137 assert_eq!(contents, format!("{}\n{}\n", a.to_hex(), b.to_hex()));
138 assert_eq!(
139 read_shallow(&dir, ObjectFormat::Sha1).expect("test operation should succeed"),
140 vec![a, b]
141 );
142 let _ = fs::remove_dir_all(&dir);
143 }
144
145 #[test]
146 fn write_empty_removes_file() {
147 let dir = temp_dir();
148 write_shallow(&dir, &[oid("3")]).expect("test operation should succeed");
149 assert!(dir.join("shallow").exists());
150 write_shallow(&dir, &[]).expect("test operation should succeed");
151 assert!(!dir.join("shallow").exists());
152 write_shallow(&dir, &[]).expect("test operation should succeed");
154 let _ = fs::remove_dir_all(&dir);
155 }
156
157 #[test]
158 fn apply_adds_shallow_and_drops_unshallow() {
159 let dir = temp_dir();
160 let keep = oid("a");
161 let added = oid("b");
162 let removed = oid("c");
163 write_shallow(&dir, &[keep.clone(), removed.clone()])
164 .expect("test operation should succeed");
165 apply_shallow_info(
166 &dir,
167 ObjectFormat::Sha1,
168 &[
169 ProtocolV2FetchShallowInfo::Shallow(added.clone()),
170 ProtocolV2FetchShallowInfo::Unshallow(removed),
171 ],
172 )
173 .expect("test operation should succeed");
174 assert_eq!(
177 read_shallow(&dir, ObjectFormat::Sha1).expect("test operation should succeed"),
178 vec![keep, added]
179 );
180 let _ = fs::remove_dir_all(&dir);
181 }
182
183 #[test]
184 fn apply_empty_entries_is_noop() {
185 let dir = temp_dir();
186 let existing = oid("d");
187 write_shallow(&dir, std::slice::from_ref(&existing))
188 .expect("test operation should succeed");
189 apply_shallow_info(&dir, ObjectFormat::Sha1, &[]).expect("test operation should succeed");
190 assert_eq!(
191 read_shallow(&dir, ObjectFormat::Sha1).expect("test operation should succeed"),
192 vec![existing]
193 );
194 let _ = fs::remove_dir_all(&dir);
195 }
196
197 #[test]
198 fn apply_unshallowing_last_boundary_removes_file() {
199 let dir = temp_dir();
200 let boundary = oid("e");
201 write_shallow(&dir, std::slice::from_ref(&boundary))
202 .expect("test operation should succeed");
203 apply_shallow_info(
204 &dir,
205 ObjectFormat::Sha1,
206 &[ProtocolV2FetchShallowInfo::Unshallow(boundary)],
207 )
208 .expect("test operation should succeed");
209 assert!(!dir.join("shallow").exists());
210 let _ = fs::remove_dir_all(&dir);
211 }
212}