This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-maintenance / src / repack.rs
8.1 kB 262 lines
1use std::collections::HashSet; 2use std::path::{Path, PathBuf}; 3use std::sync::atomic::AtomicBool; 4 5use gix::progress::Discard; 6use knot_git::{Haves, Repo, Wants}; 7use knot_types::Oid; 8 9use crate::fsio::{self, PackStem}; 10use crate::{FileCount, GeometricFactor, MaintError, ObjectCount, RepackReport, RepackStatus}; 11 12type RepackOutcome = ( 13 RepackReport, 14 Option<HashSet<Oid>>, 15 Option<PackStem>, 16 Vec<PackStem>, 17); 18 19pub fn run( 20 repo: &Repo, 21 objects_dir: &Path, 22 kind: gix::hash::Kind, 23 roots: Vec<Oid>, 24 max_objects: ObjectCount, 25 factor: GeometricFactor, 26 loose: &[(Oid, PathBuf)], 27) -> Result<RepackOutcome, MaintError> { 28 let closure = match repo.select_pack_objects(Wants::new(&roots), Haves::new(&[])) { 29 Ok(closure) => closure, 30 Err(_) => { 31 return Ok(( 32 RepackReport::skipped(RepackStatus::ClosureFailed), 33 None, 34 None, 35 Vec::new(), 36 )); 37 } 38 }; 39 if closure.is_empty() { 40 return Ok(( 41 RepackReport::skipped(RepackStatus::NothingReachable), 42 Some(HashSet::new()), 43 None, 44 Vec::new(), 45 )); 46 } 47 if closure.len() > max_objects.get() { 48 return Ok(( 49 RepackReport::skipped(RepackStatus::SkippedTooLarge), 50 None, 51 None, 52 Vec::new(), 53 )); 54 } 55 let reachable: HashSet<Oid> = closure.iter().copied().collect(); 56 57 let kept_large = if factor == GeometricFactor::full_repack() { 58 Vec::new() 59 } else { 60 kept_large_packs(objects_dir, kind, factor) 61 }; 62 let kept_large_oids: HashSet<Oid> = kept_large 63 .iter() 64 .filter_map(|idx| fsio::pack_oids(idx, kind)) 65 .flatten() 66 .collect(); 67 let new_objects: Vec<Oid> = closure 68 .into_iter() 69 .filter(|oid| !kept_large_oids.contains(oid)) 70 .collect(); 71 72 let new_stem = if new_objects.is_empty() { 73 None 74 } else { 75 build_and_install_pack(objects_dir, new_objects, kind)? 76 }; 77 78 let removed_loose = loose 79 .iter() 80 .filter(|(oid, _)| reachable.contains(oid)) 81 .filter(|(_, path)| std::fs::remove_file(path).is_ok()) 82 .count(); 83 84 let kept_large_stems: Vec<PackStem> = kept_large 85 .iter() 86 .filter_map(|idx| PackStem::of(idx)) 87 .collect(); 88 89 Ok(( 90 RepackReport { 91 status: RepackStatus::Repacked, 92 packed_objects: ObjectCount::new(reachable.len()), 93 removed_loose: FileCount::new(removed_loose), 94 removed_packs: FileCount::new(0), 95 }, 96 Some(reachable), 97 new_stem, 98 kept_large_stems, 99 )) 100} 101 102fn kept_large_packs( 103 objects_dir: &Path, 104 kind: gix::hash::Kind, 105 factor: GeometricFactor, 106) -> Vec<PathBuf> { 107 let pack_dir = objects_dir.join("pack"); 108 let mut eligible: Vec<(PathBuf, usize)> = fsio::pack_idx_paths(objects_dir) 109 .into_iter() 110 .filter(|idx| !is_excluded(idx, &pack_dir)) 111 .filter_map(|idx| fsio::pack_oids(&idx, kind).map(|oids| (idx, oids.len()))) 112 .collect(); 113 eligible.sort_by_key(|(_, count)| *count); 114 let weights: Vec<usize> = eligible.iter().map(|(_, count)| *count).collect(); 115 let split = compute_split(&weights, factor); 116 eligible 117 .into_iter() 118 .skip(split) 119 .map(|(idx, _)| idx) 120 .collect() 121} 122 123fn is_excluded(idx: &Path, pack_dir: &Path) -> bool { 124 let Some(stem) = idx.file_stem().and_then(|stem| stem.to_str()) else { 125 return true; 126 }; 127 pack_dir.join(format!("{stem}.mtimes")).exists() 128} 129 130fn compute_split(weights: &[usize], factor: GeometricFactor) -> usize { 131 let n = weights.len(); 132 if n == 0 { 133 return 0; 134 } 135 let geometric = 136 |big: usize, small: usize| (small as u64).saturating_mul(factor.get()) <= big as u64; 137 let split1 = (1..n) 138 .rev() 139 .find(|&i| !geometric(weights[i], weights[i - 1])) 140 .map(|i| i + 1) 141 .unwrap_or(0); 142 let total: u64 = weights[..split1].iter().map(|weight| *weight as u64).sum(); 143 let extended = (split1..n).try_fold((split1, total), |(split, total), j| { 144 match total.checked_mul(factor.get()) { 145 Some(threshold) if (weights[j] as u64) < threshold => std::ops::ControlFlow::Continue( 146 (split + 1, total.saturating_add(weights[j] as u64)), 147 ), 148 _ => std::ops::ControlFlow::Break((split, total)), 149 } 150 }); 151 match extended { 152 std::ops::ControlFlow::Continue((split, _)) => split, 153 std::ops::ControlFlow::Break((split, _)) => split, 154 } 155} 156 157fn build_and_install_pack( 158 objects_dir: &Path, 159 closure: Vec<Oid>, 160 kind: gix::hash::Kind, 161) -> Result<Option<PackStem>, MaintError> { 162 let pack_dir = objects_dir.join("pack"); 163 std::fs::create_dir_all(&pack_dir).map_err(|error| fsio::io_error(&pack_dir, error))?; 164 knot_resource::clear_stale(&pack_dir, ".knot-repack."); 165 let staging = pack_dir.join(format!( 166 ".knot-repack.{}.pack", 167 knot_resource::staging_nonce() 168 )); 169 let outcome = write_streaming_pack(objects_dir, closure, kind, &staging) 170 .and_then(|()| install_streamed_pack(&pack_dir, &staging, kind)); 171 let _ = std::fs::remove_file(&staging); 172 outcome 173} 174 175fn write_streaming_pack( 176 objects_dir: &Path, 177 closure: Vec<Oid>, 178 kind: gix::hash::Kind, 179 staging: &Path, 180) -> Result<(), MaintError> { 181 let file = std::fs::File::create(staging).map_err(|error| fsio::io_error(staging, error))?; 182 let mut writer = std::io::BufWriter::new(file); 183 knot_pack::write_pack(objects_dir, closure, None, &mut writer, kind) 184 .map_err(|error| MaintError::Pack(error.to_string()))?; 185 writer 186 .into_inner() 187 .map(|_| ()) 188 .map_err(|error| fsio::io_error(staging, error.into_error())) 189} 190 191fn install_streamed_pack( 192 pack_dir: &Path, 193 staging: &Path, 194 kind: gix::hash::Kind, 195) -> Result<Option<PackStem>, MaintError> { 196 let file = std::fs::File::open(staging).map_err(|error| fsio::io_error(staging, error))?; 197 let mut reader = std::io::BufReader::new(file); 198 let outcome = gix_pack::Bundle::write_to_directory( 199 &mut reader, 200 Some(pack_dir), 201 &mut Discard, 202 &AtomicBool::new(false), 203 None::<gix::odb::Handle>, 204 gix_pack::bundle::write::Options { 205 thread_limit: Some(1), 206 iteration_mode: gix_pack::data::input::Mode::Verify, 207 index_version: gix_pack::index::Version::default(), 208 object_hash: kind, 209 }, 210 ) 211 .map_err(|error| MaintError::Pack(error.to_string()))?; 212 213 if let Some(keep) = &outcome.keep_path { 214 let _ = std::fs::remove_file(keep); 215 } 216 [&outcome.data_path, &outcome.index_path] 217 .into_iter() 218 .flatten() 219 .try_for_each(|path| knot_resource::fsync_path(path))?; 220 knot_resource::fsync_path(pack_dir)?; 221 222 Ok(outcome 223 .data_path 224 .as_ref() 225 .and_then(|path| PackStem::of(path))) 226} 227 228#[cfg(test)] 229mod tests { 230 use super::{GeometricFactor, compute_split}; 231 232 #[test] 233 fn uniform_small_packs_all_roll_up() { 234 assert_eq!(compute_split(&[1, 1, 1, 1], GeometricFactor::new(2)), 4); 235 } 236 237 #[test] 238 fn a_clean_geometric_progression_rolls_up_nothing() { 239 assert_eq!(compute_split(&[1, 2, 4, 8], GeometricFactor::new(2)), 0); 240 } 241 242 #[test] 243 fn one_large_pack_with_tiny_additions_keeps_the_large_one() { 244 let split = compute_split(&[1, 1, 1, 1000], GeometricFactor::new(2)); 245 assert_eq!(split, 3); 246 } 247 248 #[test] 249 fn a_single_pack_is_never_rolled_up() { 250 assert_eq!(compute_split(&[42], GeometricFactor::new(2)), 0); 251 } 252 253 #[test] 254 fn empty_input_rolls_up_nothing() { 255 assert_eq!(compute_split(&[], GeometricFactor::new(2)), 0); 256 } 257 258 #[test] 259 fn a_huge_factor_saturates_without_overflow() { 260 assert_eq!(compute_split(&[1, 1, 1], GeometricFactor::full_repack()), 3); 261 } 262}