1use crate::core::{find_repo_root, Object, ObjectHash};
2use crate::response::GcResponse;
3use crate::storage::ObjectStore;
4use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
5use crc32fast::Hasher as Crc32Hasher;
6use flate2::read::ZlibDecoder;
7use flate2::write::ZlibEncoder;
8use flate2::Compression;
9use std::collections::HashMap;
10use std::fs;
11use std::io::{Cursor, Read, Write};
12use std::path::{Path, PathBuf};
13
14const PACK_MAGIC: &[u8; 4] = b"LITP";
16const PACK_VERSION: u32 = 1;
18
19const INDEX_MAGIC: &[u8; 4] = b"LITI";
21const INDEX_VERSION: u32 = 1;
23
24#[derive(Debug, Clone)]
26pub struct PackEntry {
27 pub obj_type: u8,
29 pub size: u64,
31 pub data: Vec<u8>,
33 pub crc32: u32,
35}
36
37#[derive(Debug, Clone)]
39pub struct PackIndexEntry {
40 pub hash: ObjectHash,
41 pub offset: u64,
42 pub crc32: u32,
43}
44
45pub fn write_pack(
47 objects: &[(ObjectHash, Object)],
48 pack_path: &Path,
49) -> Result<Vec<PackIndexEntry>, crate::errors::LitError> {
50 let mut buf: Vec<u8> = Vec::new();
51 let mut index_entries = Vec::new();
52
53 buf.extend_from_slice(PACK_MAGIC);
55 buf.write_u32::<BigEndian>(PACK_VERSION)
56 .map_err(|e| format!("Write error: {}", e))?;
57 buf.write_u32::<BigEndian>(objects.len() as u32)
58 .map_err(|e| format!("Write error: {}", e))?;
59
60 for (hash, obj) in objects {
61 let offset = buf.len() as u64;
62
63 let type_byte = match obj {
64 Object::Blob(_) => 1u8,
65 Object::Tree(_) => 2u8,
66 Object::Commit(_) => 3u8,
67 Object::Tag(_) => 4u8,
68 };
69
70 let raw = obj.to_bytes();
71 let uncompressed_size = raw.len() as u64;
72
73 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
75 encoder
76 .write_all(&raw)
77 .map_err(|e| format!("Compress error: {}", e))?;
78 let compressed = encoder
79 .finish()
80 .map_err(|e| format!("Compress finish error: {}", e))?;
81
82 let mut crc32 = Crc32Hasher::new();
84 crc32.update(&compressed);
85 let crc_val = crc32.finalize();
86
87 buf.push(type_byte);
89 buf.write_u64::<BigEndian>(uncompressed_size)
90 .map_err(|e| format!("Write error: {}", e))?;
91 buf.write_u64::<BigEndian>(compressed.len() as u64)
92 .map_err(|e| format!("Write error: {}", e))?;
93 buf.extend_from_slice(&compressed);
94
95 index_entries.push(PackIndexEntry {
96 hash: hash.clone(),
97 offset,
98 crc32: crc_val,
99 });
100 }
101
102 fs::write(pack_path, &buf).map_err(|e| format!("Failed to write pack: {}", e))?;
103
104 Ok(index_entries)
105}
106
107pub fn write_pack_index(
109 entries: &[PackIndexEntry],
110 index_path: &Path,
111) -> Result<(), crate::errors::LitError> {
112 let mut buf: Vec<u8> = Vec::new();
113
114 buf.extend_from_slice(INDEX_MAGIC);
116 buf.write_u32::<BigEndian>(INDEX_VERSION)
117 .map_err(|e| format!("Write error: {}", e))?;
118 buf.write_u32::<BigEndian>(entries.len() as u32)
119 .map_err(|e| format!("Write error: {}", e))?;
120
121 let mut sorted = entries.to_vec();
123 sorted.sort_by(|a, b| a.hash.as_str().cmp(b.hash.as_str()));
124
125 for entry in &sorted {
126 let hash_bytes = entry.hash.as_str().as_bytes();
127 buf.write_u32::<BigEndian>(hash_bytes.len() as u32)
128 .map_err(|e| format!("Write error: {}", e))?;
129 buf.extend_from_slice(hash_bytes);
130 buf.write_u64::<BigEndian>(entry.offset)
131 .map_err(|e| format!("Write error: {}", e))?;
132 buf.write_u32::<BigEndian>(entry.crc32)
133 .map_err(|e| format!("Write error: {}", e))?;
134 }
135
136 fs::write(index_path, &buf).map_err(|e| format!("Failed to write index: {}", e))?;
137 Ok(())
138}
139
140pub fn read_pack_object(pack_path: &Path, offset: u64) -> Result<Object, crate::errors::LitError> {
142 let pack_data = fs::read(pack_path).map_err(|e| format!("Failed to read pack: {}", e))?;
143 let mut cursor = Cursor::new(&pack_data);
144 cursor.set_position(offset);
145
146 let _type_byte = cursor.read_u8().map_err(|e| format!("Read error: {}", e))?;
148
149 let _uncompressed_size = cursor
151 .read_u64::<BigEndian>()
152 .map_err(|e| format!("Read error: {}", e))?;
153 let compressed_size = cursor
154 .read_u64::<BigEndian>()
155 .map_err(|e| format!("Read error: {}", e))?;
156
157 let pos = cursor.position() as usize;
159 let end = pos + compressed_size as usize;
160 if end > pack_data.len() {
161 return Err("Pack data truncated".into());
162 }
163 let compressed = &pack_data[pos..end];
164
165 let mut decoder = ZlibDecoder::new(compressed);
167 let mut raw = Vec::new();
168 decoder
169 .read_to_end(&mut raw)
170 .map_err(|e| format!("Decompress error: {}", e))?;
171
172 Object::from_bytes(&raw).map_err(Into::into)
173}
174
175pub fn load_pack_index(index_path: &Path) -> Result<HashMap<String, (PathBuf, u64)>, String> {
177 let data = fs::read(index_path).map_err(|e| format!("Failed to read index: {}", e))?;
178 let mut cursor = Cursor::new(&data);
179
180 let mut magic = [0u8; 4];
182 cursor
183 .read_exact(&mut magic)
184 .map_err(|e| format!("Read error: {}", e))?;
185 if &magic != INDEX_MAGIC {
186 return Err("Invalid pack index magic".into());
187 }
188
189 let _version = cursor
190 .read_u32::<BigEndian>()
191 .map_err(|e| format!("Read error: {}", e))?;
192 let count = cursor
193 .read_u32::<BigEndian>()
194 .map_err(|e| format!("Read error: {}", e))?;
195
196 let pack_path = index_path.with_extension("pack");
197 let mut map = HashMap::new();
198
199 for _ in 0..count {
200 let hash_len = cursor
201 .read_u32::<BigEndian>()
202 .map_err(|e| format!("Read error: {}", e))? as usize;
203
204 let pos = cursor.position() as usize;
205 if pos + hash_len > data.len() {
206 return Err("Index data truncated".into());
207 }
208 let hash_str = String::from_utf8(data[pos..pos + hash_len].to_vec())
209 .map_err(|e| format!("Invalid hash UTF-8: {}", e))?;
210 cursor.set_position((pos + hash_len) as u64);
211
212 let offset = cursor
213 .read_u64::<BigEndian>()
214 .map_err(|e| format!("Read error: {}", e))?;
215 let _crc32 = cursor
216 .read_u32::<BigEndian>()
217 .map_err(|e| format!("Read error: {}", e))?;
218
219 map.insert(hash_str, (pack_path.clone(), offset));
220 }
221
222 Ok(map)
223}
224
225pub fn execute() -> Result<GcResponse, crate::errors::LitError> {
228 let repo_root = find_repo_root()?;
229 let store = ObjectStore::new(&repo_root);
230
231 let all_hashes = store
232 .list()
233 .map_err(|e| format!("Failed to list objects: {}", e))?;
234
235 if all_hashes.is_empty() {
236 return Ok(GcResponse {
237 objects_packed: 0,
238 packs_created: 0,
239 loose_removed: 0,
240 bytes_saved: 0,
241 message: "No objects to pack".to_string(),
242 });
243 }
244
245 let mut objects: Vec<(ObjectHash, Object)> = Vec::new();
247 let mut total_loose_bytes: u64 = 0;
248 for hash in &all_hashes {
249 let obj = store.read(hash)?;
250 let loose_path = repo_root
252 .join(".lit")
253 .join("objects")
254 .join(&hash.as_str()[..4])
255 .join(&hash.as_str()[4..]);
256 if let Ok(meta) = fs::metadata(&loose_path) {
257 total_loose_bytes += meta.len();
258 }
259 objects.push((hash.clone(), obj));
260 }
261
262 let packs_dir = repo_root.join(".lit").join("packs");
264 fs::create_dir_all(&packs_dir)
265 .map_err(|e| format!("Failed to create packs directory: {}", e))?;
266
267 let pack_name = format!("pack-{}", chrono::Utc::now().format("%Y%m%d%H%M%S"));
269 let pack_path = packs_dir.join(format!("{}.pack", pack_name));
270 let index_path = packs_dir.join(format!("{}.idx", pack_name));
271
272 let index_entries = write_pack(&objects, &pack_path)?;
274 write_pack_index(&index_entries, &index_path)?;
275
276 let pack_bytes = fs::metadata(&pack_path).map(|m| m.len()).unwrap_or(0);
277 let index_bytes = fs::metadata(&index_path).map(|m| m.len()).unwrap_or(0);
278
279 let mut loose_removed = 0u64;
281 for hash in &all_hashes {
282 let dir = hash.as_str()[..4].to_string();
283 let file = hash.as_str()[4..].to_string();
284 let loose_path = repo_root
285 .join(".lit")
286 .join("objects")
287 .join(&dir)
288 .join(&file);
289 if loose_path.exists() {
290 if fs::remove_file(&loose_path).is_ok() {
291 loose_removed += 1;
292 }
293 let shard_dir = repo_root.join(".lit").join("objects").join(&dir);
295 if let Ok(mut entries) = fs::read_dir(&shard_dir) {
296 if entries.next().is_none() {
297 let _ = fs::remove_dir(&shard_dir);
298 }
299 }
300 }
301 }
302
303 let bytes_saved = if total_loose_bytes > (pack_bytes + index_bytes) {
304 total_loose_bytes - pack_bytes - index_bytes
305 } else {
306 0
307 };
308
309 Ok(GcResponse {
310 objects_packed: objects.len() as u64,
311 packs_created: 1,
312 loose_removed,
313 bytes_saved,
314 message: format!(
315 "Packed {} objects into {} ({} bytes saved)",
316 objects.len(),
317 pack_path.display(),
318 bytes_saved
319 ),
320 })
321}