1use 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#[derive(Debug, Clone, Copy, Default)]
17pub struct PrunePackedOptions {
18 pub dry_run: bool,
20 pub quiet: bool,
22}
23
24pub fn prune_packed_objects(objects_dir: &Path, opts: PrunePackedOptions) -> Result<Vec<PathBuf>> {
39 let odb = Odb::new(objects_dir);
40 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 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}