Skip to main content

grit_lib/
pack_geometry.rs

1//! Pack geometry for `git repack --geometric` (factor-based progression).
2//!
3//! Mirrors the split logic in Git's `repack-geometry.c`: packs are weighted by
4//! object count from their index, sorted ascending, then a split index separates
5//! packs that will be rolled into a new pack from those retained.
6
7use std::path::Path;
8
9use crate::error::{Error, Result};
10use crate::pack::read_local_pack_indexes;
11
12/// One local pack considered for geometric repacking.
13#[derive(Debug, Clone)]
14pub struct GeometricPack {
15    /// `pack-<hex>` stem (no `.pack` / `.idx` suffix), matching `pack-objects` stdin lines.
16    pub stem: String,
17    /// Number of objects in the pack index.
18    pub object_count: usize,
19    /// Modification time of the `.pack` file (seconds), used for include-pack ordering.
20    pub mtime_secs: u64,
21}
22
23/// Split packs into "roll up" vs "keep" using Git's geometric progression rules.
24#[must_use]
25pub fn compute_geometry_split(weights: &[usize], split_factor: i32) -> usize {
26    let sf = split_factor.max(1) as u64;
27    let pack_nr = weights.len();
28    if pack_nr == 0 {
29        return 0;
30    }
31
32    // Packs are sorted ascending by weight (caller's responsibility).
33    // Match `repack-geometry.c`: compare `pack[i]` vs `pack[i-1]` for i = pack_nr-1 .. 1.
34    let mut split = 0usize;
35    let mut found = false;
36    for idx in (1..pack_nr).rev() {
37        let ours = weights[idx] as u64;
38        let prev = weights[idx - 1] as u64;
39        if ours < sf.saturating_mul(prev) {
40            split = idx;
41            found = true;
42            break;
43        }
44    }
45    if found {
46        split += 1;
47    }
48
49    let mut total_size: u64 = 0;
50    for j in 0..split {
51        total_size = total_size.saturating_add(weights[j] as u64);
52    }
53
54    let mut j = split;
55    while j < pack_nr {
56        let ours = weights[j] as u64;
57        if ours < sf.saturating_mul(total_size) {
58            split += 1;
59            total_size = total_size.saturating_add(ours);
60            j += 1;
61        } else {
62            break;
63        }
64    }
65
66    split
67}
68
69/// Load eligible non-promisor packs from `objects_dir/pack` for geometry.
70///
71/// Skips packs with a `.keep` file unless `pack_kept_objects` is set, and skips
72/// basenames listed in `keep_pack_names` (full `pack-*.pack` filename or basename).
73pub fn collect_geometry_packs(
74    objects_dir: &Path,
75    pack_kept_objects: bool,
76    keep_pack_names: &[String],
77) -> Result<Vec<GeometricPack>> {
78    let pack_dir = objects_dir.join("pack");
79    let indexes = read_local_pack_indexes(objects_dir)?;
80    let mut out = Vec::new();
81
82    for idx in indexes {
83        let pack_name = idx
84            .pack_path
85            .file_name()
86            .and_then(|s| s.to_str())
87            .map(str::to_owned)
88            .ok_or_else(|| Error::CorruptObject("invalid pack path".to_owned()))?;
89
90        if !pack_name.starts_with("pack-") || !pack_name.ends_with(".pack") {
91            continue;
92        }
93
94        if keep_pack_names.iter().any(|k| {
95            k == &pack_name
96                || k.strip_prefix("pack/").unwrap_or(k.as_str()) == pack_name
97                || Path::new(k).file_name().and_then(|s| s.to_str()) == Some(pack_name.as_str())
98        }) {
99            continue;
100        }
101
102        let stem = pack_name
103            .strip_suffix(".pack")
104            .unwrap_or(pack_name.as_str())
105            .to_string();
106
107        let keep_path = pack_dir.join(format!("{stem}.keep"));
108        if keep_path.is_file() && !pack_kept_objects {
109            continue;
110        }
111
112        if pack_dir.join(format!("{stem}.promisor")).is_file() {
113            continue;
114        }
115
116        let mtime_secs = std::fs::metadata(&idx.pack_path)
117            .map(|m| {
118                m.modified()
119                    .ok()
120                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
121                    .map(|d| d.as_secs())
122                    .unwrap_or(0)
123            })
124            .unwrap_or(0);
125
126        out.push(GeometricPack {
127            stem,
128            object_count: idx.entries.len(),
129            mtime_secs,
130        });
131    }
132
133    out.sort_by(|a, b| a.object_count.cmp(&b.object_count));
134    Ok(out)
135}
136
137/// Promisor packs only (sibling `.promisor` marker), for a second geometry pass.
138pub fn collect_promisor_geometry_packs(
139    objects_dir: &Path,
140    pack_kept_objects: bool,
141    keep_pack_names: &[String],
142) -> Result<Vec<GeometricPack>> {
143    let pack_dir = objects_dir.join("pack");
144    let indexes = read_local_pack_indexes(objects_dir)?;
145    let mut out = Vec::new();
146
147    for idx in indexes {
148        let pack_name = idx
149            .pack_path
150            .file_name()
151            .and_then(|s| s.to_str())
152            .map(str::to_owned)
153            .ok_or_else(|| Error::CorruptObject("invalid pack path".to_owned()))?;
154
155        if !pack_name.starts_with("pack-") || !pack_name.ends_with(".pack") {
156            continue;
157        }
158
159        if keep_pack_names.iter().any(|k| {
160            k == &pack_name
161                || k.strip_prefix("pack/").unwrap_or(k.as_str()) == pack_name
162                || Path::new(k).file_name().and_then(|s| s.to_str()) == Some(pack_name.as_str())
163        }) {
164            continue;
165        }
166
167        let stem = pack_name
168            .strip_suffix(".pack")
169            .unwrap_or(pack_name.as_str())
170            .to_string();
171
172        let keep_path = pack_dir.join(format!("{stem}.keep"));
173        if keep_path.is_file() && !pack_kept_objects {
174            continue;
175        }
176
177        if !pack_dir.join(format!("{stem}.promisor")).is_file() {
178            continue;
179        }
180
181        let mtime_secs = std::fs::metadata(&idx.pack_path)
182            .map(|m| {
183                m.modified()
184                    .ok()
185                    .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
186                    .map(|d| d.as_secs())
187                    .unwrap_or(0)
188            })
189            .unwrap_or(0);
190
191        out.push(GeometricPack {
192            stem,
193            object_count: idx.entries.len(),
194            mtime_secs,
195        });
196    }
197
198    out.sort_by(|a, b| a.object_count.cmp(&b.object_count));
199    Ok(out)
200}
201
202/// Preferred pack stem (largest retained non-promisor pack), for MIDX `--preferred-pack`.
203#[must_use]
204pub fn preferred_pack_stem_after_split(packs: &[GeometricPack], split: usize) -> Option<String> {
205    if split >= packs.len() {
206        return None;
207    }
208    // Largest pack in the retained suffix: rightmost after ascending sort.
209    packs.last().map(|p| p.stem.clone())
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn progression_intact_split_is_zero() {
218        // 3, 6, 12 with factor 2 forms a progression.
219        let w = vec![3, 6, 12];
220        assert_eq!(compute_geometry_split(&w, 2), 0);
221    }
222
223    #[test]
224    fn duplicate_small_packs_roll_up() {
225        // 3, 3, 6 — progression broken between 3 and 3; rollup extends through 6.
226        let w = vec![3, 3, 6];
227        assert_eq!(compute_geometry_split(&w, 2), 3);
228    }
229}