Skip to main content

lit/commands/
gc.rs

1use crate::core::{find_repo_root, Object, ObjectHash};
2use crate::response::GcResponse;
3use crate::storage::ObjectStore;
4use std::fs;
5
6// The pack format lives in `storage::pack`, next to the ObjectStore that reads
7// it back. These re-exports keep the previously public paths resolving.
8pub 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
13/// Execute the `gc` (garbage collection) command.
14/// Packs all loose objects into a single pack file and removes the loose files.
15pub 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    // Read all loose objects
34    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        // Track size of loose file
39        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    // Create packs directory
51    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    // Generate pack name from timestamp
56    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    // Write pack and index
61    // Pack through the store's own encryption manager, so a packed object is
62    // protected exactly as the loose one it replaces was.
63    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    // Remove loose objects
76    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            // Clean up empty shard dirs
90            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}