1use crate::core::{find_repo_root, Object, ObjectHash};
2use crate::response::GcResponse;
3use crate::storage::ObjectStore;
4use std::fs;
5
6pub use crate::storage::pack::{
9 load_pack_index, read_pack_object, write_pack, write_pack_index, PackIndexEntry, INDEX_MAGIC,
10 INDEX_VERSION, PACK_MAGIC, PACK_VERSION,
11};
12
13pub fn execute() -> Result<GcResponse, crate::errors::LitError> {
16 let repo_root = find_repo_root()?;
17 let store = ObjectStore::new(&repo_root);
18
19 let all_hashes = store
20 .list()
21 .map_err(|e| format!("Failed to list objects: {}", e))?;
22
23 if all_hashes.is_empty() {
24 return Ok(GcResponse {
25 objects_packed: 0,
26 packs_created: 0,
27 loose_removed: 0,
28 bytes_saved: 0,
29 message: "No objects to pack".to_string(),
30 });
31 }
32
33 let mut objects: Vec<(ObjectHash, Object)> = Vec::new();
35 let mut total_loose_bytes: u64 = 0;
36 for hash in &all_hashes {
37 let obj = store.read(hash)?;
38 let loose_path = repo_root
40 .join(".lit")
41 .join("objects")
42 .join(&hash.as_str()[..4])
43 .join(&hash.as_str()[4..]);
44 if let Ok(meta) = fs::metadata(&loose_path) {
45 total_loose_bytes += meta.len();
46 }
47 objects.push((hash.clone(), obj));
48 }
49
50 let packs_dir = repo_root.join(".lit").join("packs");
52 fs::create_dir_all(&packs_dir)
53 .map_err(|e| format!("Failed to create packs directory: {}", e))?;
54
55 let pack_name = format!("pack-{}", chrono::Utc::now().format("%Y%m%d%H%M%S"));
57 let pack_path = packs_dir.join(format!("{}.pack", pack_name));
58 let index_path = packs_dir.join(format!("{}.idx", pack_name));
59
60 let encryption = store.encryption();
64 let index_entries = {
65 let encryption = encryption
66 .lock()
67 .map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
68 write_pack(&objects, &pack_path, &encryption)?
69 };
70 write_pack_index(&index_entries, &index_path)?;
71
72 let pack_bytes = fs::metadata(&pack_path).map(|m| m.len()).unwrap_or(0);
73 let index_bytes = fs::metadata(&index_path).map(|m| m.len()).unwrap_or(0);
74
75 let mut loose_removed = 0u64;
77 for hash in &all_hashes {
78 let dir = hash.as_str()[..4].to_string();
79 let file = hash.as_str()[4..].to_string();
80 let loose_path = repo_root
81 .join(".lit")
82 .join("objects")
83 .join(&dir)
84 .join(&file);
85 if loose_path.exists() {
86 if fs::remove_file(&loose_path).is_ok() {
87 loose_removed += 1;
88 }
89 let shard_dir = repo_root.join(".lit").join("objects").join(&dir);
91 if let Ok(mut entries) = fs::read_dir(&shard_dir) {
92 if entries.next().is_none() {
93 let _ = fs::remove_dir(&shard_dir);
94 }
95 }
96 }
97 }
98
99 let bytes_saved = if total_loose_bytes > (pack_bytes + index_bytes) {
100 total_loose_bytes - pack_bytes - index_bytes
101 } else {
102 0
103 };
104
105 Ok(GcResponse {
106 objects_packed: objects.len() as u64,
107 packs_created: 1,
108 loose_removed,
109 bytes_saved,
110 message: format!(
111 "Packed {} objects into {} ({} bytes saved)",
112 objects.len(),
113 pack_path.display(),
114 bytes_saved
115 ),
116 })
117}