grit_lib/
pack_geometry.rs1use std::path::Path;
8
9use crate::error::{Error, Result};
10use crate::pack::read_local_pack_indexes;
11
12#[derive(Debug, Clone)]
14pub struct GeometricPack {
15 pub stem: String,
17 pub object_count: usize,
19 pub mtime_secs: u64,
21 pub is_local: bool,
23}
24
25#[must_use]
27pub fn compute_geometry_split(weights: &[usize], split_factor: i32) -> usize {
28 let sf = split_factor.max(1) as u64;
29 let pack_nr = weights.len();
30 if pack_nr == 0 {
31 return 0;
32 }
33
34 let mut split = 0usize;
37 let mut found = false;
38 for idx in (1..pack_nr).rev() {
39 let ours = weights[idx] as u64;
40 let prev = weights[idx - 1] as u64;
41 if ours < sf.saturating_mul(prev) {
42 split = idx;
43 found = true;
44 break;
45 }
46 }
47 if found {
48 split += 1;
49 }
50
51 let mut total_size: u64 = 0;
52 for j in 0..split {
53 total_size = total_size.saturating_add(weights[j] as u64);
54 }
55
56 let mut j = split;
57 while j < pack_nr {
58 let ours = weights[j] as u64;
59 if ours < sf.saturating_mul(total_size) {
60 split += 1;
61 total_size = total_size.saturating_add(ours);
62 j += 1;
63 } else {
64 break;
65 }
66 }
67
68 split
69}
70
71pub fn collect_geometry_packs(
76 objects_dir: &Path,
77 pack_kept_objects: bool,
78 keep_pack_names: &[String],
79) -> Result<Vec<GeometricPack>> {
80 let pack_dir = objects_dir.join("pack");
81 let indexes = read_local_pack_indexes(objects_dir)?;
82 let mut out = Vec::new();
83
84 for idx in indexes {
85 let pack_name = idx
86 .pack_path
87 .file_name()
88 .and_then(|s| s.to_str())
89 .map(str::to_owned)
90 .ok_or_else(|| Error::CorruptObject("invalid pack path".to_owned()))?;
91
92 if !pack_name.starts_with("pack-") || !pack_name.ends_with(".pack") {
93 continue;
94 }
95
96 if keep_pack_names.iter().any(|k| {
97 k == &pack_name
98 || k.strip_prefix("pack/").unwrap_or(k.as_str()) == pack_name
99 || Path::new(k).file_name().and_then(|s| s.to_str()) == Some(pack_name.as_str())
100 }) {
101 continue;
102 }
103
104 let stem = pack_name
105 .strip_suffix(".pack")
106 .unwrap_or(pack_name.as_str())
107 .to_string();
108
109 let keep_path = pack_dir.join(format!("{stem}.keep"));
110 if keep_path.is_file() && !pack_kept_objects {
111 continue;
112 }
113
114 if pack_dir.join(format!("{stem}.promisor")).is_file() {
115 continue;
116 }
117
118 let mtime_secs = std::fs::metadata(&idx.pack_path)
119 .map(|m| {
120 m.modified()
121 .ok()
122 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
123 .map(|d| d.as_secs())
124 .unwrap_or(0)
125 })
126 .unwrap_or(0);
127
128 out.push(GeometricPack {
129 stem,
130 object_count: idx.entries.len(),
131 mtime_secs,
132 is_local: true,
133 });
134 }
135
136 out.sort_by_key(|a| a.object_count);
137 Ok(out)
138}
139
140pub fn collect_promisor_geometry_packs(
142 objects_dir: &Path,
143 pack_kept_objects: bool,
144 keep_pack_names: &[String],
145) -> Result<Vec<GeometricPack>> {
146 let pack_dir = objects_dir.join("pack");
147 let indexes = read_local_pack_indexes(objects_dir)?;
148 let mut out = Vec::new();
149
150 for idx in indexes {
151 let pack_name = idx
152 .pack_path
153 .file_name()
154 .and_then(|s| s.to_str())
155 .map(str::to_owned)
156 .ok_or_else(|| Error::CorruptObject("invalid pack path".to_owned()))?;
157
158 if !pack_name.starts_with("pack-") || !pack_name.ends_with(".pack") {
159 continue;
160 }
161
162 if keep_pack_names.iter().any(|k| {
163 k == &pack_name
164 || k.strip_prefix("pack/").unwrap_or(k.as_str()) == pack_name
165 || Path::new(k).file_name().and_then(|s| s.to_str()) == Some(pack_name.as_str())
166 }) {
167 continue;
168 }
169
170 let stem = pack_name
171 .strip_suffix(".pack")
172 .unwrap_or(pack_name.as_str())
173 .to_string();
174
175 let keep_path = pack_dir.join(format!("{stem}.keep"));
176 if keep_path.is_file() && !pack_kept_objects {
177 continue;
178 }
179
180 if !pack_dir.join(format!("{stem}.promisor")).is_file() {
181 continue;
182 }
183
184 let mtime_secs = std::fs::metadata(&idx.pack_path)
185 .map(|m| {
186 m.modified()
187 .ok()
188 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
189 .map(|d| d.as_secs())
190 .unwrap_or(0)
191 })
192 .unwrap_or(0);
193
194 out.push(GeometricPack {
195 stem,
196 object_count: idx.entries.len(),
197 mtime_secs,
198 is_local: true,
199 });
200 }
201
202 out.sort_by_key(|a| a.object_count);
203 Ok(out)
204}
205
206#[must_use]
208pub fn preferred_pack_stem_after_split(packs: &[GeometricPack], split: usize) -> Option<String> {
209 if split >= packs.len() {
210 return None;
211 }
212 packs
214 .iter()
215 .skip(split)
216 .rev()
217 .find(|p| p.is_local)
218 .map(|p| p.stem.clone())
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn progression_intact_split_is_zero() {
227 let w = vec![3, 6, 12];
229 assert_eq!(compute_geometry_split(&w, 2), 0);
230 }
231
232 #[test]
233 fn duplicate_small_packs_roll_up() {
234 let w = vec![3, 3, 6];
236 assert_eq!(compute_geometry_split(&w, 2), 3);
237 }
238}