Skip to main content

grit_lib/
prune_packed.rs

1//! Library implementation of `prune-packed`.
2//!
3//! Removes loose objects that are already stored in a pack file, freeing
4//! disk space without losing any object data.
5
6use crate::error::Result;
7use crate::odb::store::ObjectStore;
8use crate::odb::Odb;
9use std::collections::HashSet;
10use std::fs;
11use std::io;
12use std::ops::ControlFlow;
13use std::path::{Path, PathBuf};
14
15/// Options controlling the behaviour of [`prune_packed_objects`].
16#[derive(Debug, Clone, Copy, Default)]
17pub struct PrunePackedOptions {
18    /// When `true`, print what would be deleted without actually deleting.
19    pub dry_run: bool,
20    /// When `true`, suppress informational output.
21    pub quiet: bool,
22}
23
24/// Remove loose objects that are already stored in a pack file.
25///
26/// For each loose object under `objects_dir` whose [`crate::objects::ObjectId`] appears in
27/// at least one local pack index, or in a non-promisor pack in an alternate object
28/// directory, the file is deleted (or, with [`PrunePackedOptions::dry_run`], paths are
29/// listed in the return value only). A duplicate loose object is kept when the alternate
30/// holds the same id only as loose, matching Git.
31/// Empty two-char prefix directories are removed afterwards.
32///
33/// Returns the list of paths that were (or would be) removed.
34///
35/// # Errors
36///
37/// - [`crate::error::Error::Io`] for directory or file access failures.
38pub fn prune_packed_objects(objects_dir: &Path, opts: PrunePackedOptions) -> Result<Vec<PathBuf>> {
39    let odb = Odb::new(objects_dir);
40    // External `git repack` does not notify grit; drop stale pack listings before enumerating packs.
41    odb.invalidate_packs();
42    let primary = odb.primary()?;
43    let loose = primary.loose_store();
44    let packed = primary.packed_objects();
45
46    let mut packed_ids = HashSet::new();
47    packed.for_each_object(&mut |oid| {
48        packed_ids.insert(*oid);
49        ControlFlow::Continue(())
50    })?;
51    let mut candidates = Vec::new();
52    let mut prefix_dirs = HashSet::new();
53    loose.for_each_object(&mut |oid| {
54        let in_local_pack = packed_ids.contains(oid);
55        let in_alternate_pack = if !in_local_pack {
56            odb.contained_in_alternate_pack(oid).unwrap_or(false)
57        } else {
58            false
59        };
60        if !in_local_pack && !in_alternate_pack {
61            return ControlFlow::Continue(());
62        }
63        let obj_path = loose.object_path(oid);
64        if let Some(parent) = obj_path.parent() {
65            prefix_dirs.insert(parent.to_path_buf());
66        }
67        candidates.push(obj_path);
68        ControlFlow::Continue(())
69    })?;
70
71    let mut removed = Vec::new();
72    for obj_path in candidates {
73        if !opts.dry_run {
74            match fs::remove_file(&obj_path) {
75                Ok(()) => {}
76                Err(err) if err.kind() == io::ErrorKind::NotFound => {}
77                Err(err) => return Err(crate::error::Error::Io(err)),
78            }
79        }
80        removed.push(obj_path);
81    }
82
83    if !opts.dry_run {
84        for dir in prefix_dirs {
85            let _ = fs::remove_dir(dir);
86        }
87    }
88
89    Ok(removed)
90}
91
92#[cfg(test)]
93mod tests {
94    #![allow(clippy::unwrap_used, clippy::expect_used)]
95
96    use super::*;
97    use crate::objects::ObjectKind;
98    use crate::odb::Odb;
99    use tempfile::TempDir;
100
101    #[test]
102    fn no_packs_leaves_loose_objects_intact() {
103        let dir = TempDir::new().unwrap();
104        let odb = Odb::new(dir.path());
105        let oid = odb.write(ObjectKind::Blob, b"hello").unwrap();
106
107        let opts = PrunePackedOptions {
108            dry_run: false,
109            quiet: true,
110        };
111        let removed = prune_packed_objects(dir.path(), opts).unwrap();
112        assert!(removed.is_empty());
113        assert!(odb.exists(&oid));
114    }
115
116    #[test]
117    fn dry_run_does_not_delete_files() {
118        let dir = TempDir::new().unwrap();
119        let odb = Odb::new(dir.path());
120        let oid = odb.write(ObjectKind::Blob, b"dry run test").unwrap();
121
122        // No pack indexes — nothing would be pruned.
123        let opts = PrunePackedOptions {
124            dry_run: true,
125            quiet: false,
126        };
127        let removed = prune_packed_objects(dir.path(), opts).unwrap();
128        assert!(removed.is_empty());
129        assert!(odb.exists(&oid));
130    }
131}