1use crate::error::{Error, Result};
7use crate::objects::ObjectId;
8use crate::pack::read_local_pack_indexes;
9use std::collections::HashSet;
10use std::fs;
11use std::io;
12use std::path::{Path, PathBuf};
13
14#[derive(Debug, Clone, Copy, Default)]
16pub struct PrunePackedOptions {
17 pub dry_run: bool,
19 pub quiet: bool,
21}
22
23pub fn prune_packed_objects(objects_dir: &Path, opts: PrunePackedOptions) -> Result<Vec<PathBuf>> {
36 let packed_ids = collect_packed_ids(objects_dir)?;
37 if packed_ids.is_empty() {
38 return Ok(Vec::new());
39 }
40
41 let mut removed = Vec::new();
42 let rd = match fs::read_dir(objects_dir) {
43 Ok(rd) => rd,
44 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
45 Err(err) => return Err(Error::Io(err)),
46 };
47
48 for entry in rd {
49 let entry = entry.map_err(Error::Io)?;
50 let dir_name = entry.file_name().to_string_lossy().to_string();
51
52 if dir_name.len() != 2
54 || !dir_name.chars().all(|c| c.is_ascii_hexdigit())
55 || !entry.path().is_dir()
56 {
57 continue;
58 }
59
60 let sub_rd = match fs::read_dir(entry.path()) {
61 Ok(rd) => rd,
62 Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
63 Err(err) => return Err(Error::Io(err)),
64 };
65
66 for file in sub_rd {
67 let file = file.map_err(Error::Io)?;
68 let file_name = file.file_name().to_string_lossy().to_string();
69 if file_name.len() != 38 || !file_name.chars().all(|c| c.is_ascii_hexdigit()) {
71 continue;
72 }
73
74 let hex = format!("{dir_name}{file_name}");
75 let oid: ObjectId = match hex.parse() {
76 Ok(id) => id,
77 Err(_) => continue,
78 };
79
80 if !packed_ids.contains(&oid) {
81 continue;
82 }
83
84 let obj_path = file.path();
85 if opts.dry_run {
86 println!("rm -f {}", obj_path.display());
87 } else {
88 match fs::remove_file(&obj_path) {
89 Ok(()) => {}
90 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
91 Err(err) => return Err(Error::Io(err)),
92 }
93 }
94 removed.push(obj_path);
95 }
96
97 if !opts.dry_run {
99 let _ = fs::remove_dir(entry.path());
100 }
101 }
102
103 Ok(removed)
104}
105
106fn collect_packed_ids(objects_dir: &Path) -> Result<HashSet<ObjectId>> {
108 let indexes = read_local_pack_indexes(objects_dir)?;
109 let mut ids = HashSet::new();
110 for idx in indexes {
111 for entry in idx.entries {
112 if entry.oid.len() == 20 {
113 if let Ok(oid) = crate::objects::ObjectId::from_bytes(&entry.oid) {
114 ids.insert(oid);
115 }
116 }
117 }
118 }
119 Ok(ids)
120}
121
122#[cfg(test)]
123mod tests {
124 #![allow(clippy::unwrap_used, clippy::expect_used)]
125
126 use super::*;
127 use crate::objects::ObjectKind;
128 use crate::odb::Odb;
129 use tempfile::TempDir;
130
131 #[test]
132 fn no_packs_leaves_loose_objects_intact() {
133 let dir = TempDir::new().unwrap();
134 let odb = Odb::new(dir.path());
135 let oid = odb.write(ObjectKind::Blob, b"hello").unwrap();
136
137 let opts = PrunePackedOptions {
138 dry_run: false,
139 quiet: true,
140 };
141 let removed = prune_packed_objects(dir.path(), opts).unwrap();
142 assert!(removed.is_empty());
143 assert!(odb.exists(&oid));
144 }
145
146 #[test]
147 fn dry_run_does_not_delete_files() {
148 let dir = TempDir::new().unwrap();
149 let odb = Odb::new(dir.path());
150 let oid = odb.write(ObjectKind::Blob, b"dry run test").unwrap();
151
152 let opts = PrunePackedOptions {
154 dry_run: true,
155 quiet: false,
156 };
157 let removed = prune_packed_objects(dir.path(), opts).unwrap();
158 assert!(removed.is_empty());
159 assert!(odb.exists(&oid));
160 }
161}