Skip to main content

grit_lib/
delta_islands.rs

1//! Delta islands — restrict cross-island deltas in `pack-objects` (`--delta-islands`).
2//!
3//! Port of Git's `delta-islands.c`. Islands group objects by the refs whose history
4//! reaches them. Each object gets a bitmap of the islands it belongs to. A delta from
5//! `trg` onto base `src` is only allowed when `trg`'s island set is a subset of `src`'s
6//! (`in_same_island`), so a packed delta never forces a client that only fetched one
7//! island to also download objects from another. `island_delta_cmp` additionally biases
8//! the "which object is the preferred base" ordering so that objects living in a superset
9//! of islands are preferred as bases.
10//!
11//! Islands are derived from the `pack.island` regexes (matched against full ref names,
12//! left-anchored) and the optional `pack.islandcore` name (whose objects are written
13//! first in the pack via layering).
14
15use crate::config::ConfigSet;
16use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind};
17use crate::repo::Repository;
18use std::collections::HashMap;
19
20/// One island membership bitmap (one bit per deduplicated island).
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct IslandBitmap {
23    bits: Vec<u32>,
24}
25
26impl IslandBitmap {
27    fn new(size: usize) -> Self {
28        IslandBitmap {
29            bits: vec![0u32; size],
30        }
31    }
32
33    fn set(&mut self, i: u32) {
34        let block = (i / 32) as usize;
35        let mask = 1u32 << (i % 32);
36        self.bits[block] |= mask;
37    }
38
39    fn get(&self, i: u32) -> bool {
40        let block = (i / 32) as usize;
41        let mask = 1u32 << (i % 32);
42        (self.bits[block] & mask) != 0
43    }
44
45    fn or(&mut self, other: &IslandBitmap) {
46        for (a, b) in self.bits.iter_mut().zip(other.bits.iter()) {
47            *a |= *b;
48        }
49    }
50
51    /// `self` ⊆ `super_`: every island bit set in `self` is also set in `super_`.
52    fn is_subset(&self, super_: &IslandBitmap) -> bool {
53        for (s, p) in self.bits.iter().zip(super_.bits.iter()) {
54            if (s & p) != *s {
55                return false;
56            }
57        }
58        true
59    }
60
61    fn is_empty(&self) -> bool {
62        self.bits.iter().all(|&b| b == 0)
63    }
64}
65
66/// Computed island marks for a `pack-objects` run.
67#[derive(Debug, Default)]
68pub struct DeltaIslands {
69    /// Per-object island membership.
70    marks: HashMap<ObjectId, IslandBitmap>,
71    /// Island bit assigned to the core island (`pack.islandcore`), if any.
72    core_island_bit: Option<u32>,
73    /// Whether any island was actually configured/loaded.
74    active: bool,
75}
76
77/// A raw island collected from refs, before deduplication.
78struct RemoteIsland {
79    hash: u64,
80    oids: Vec<ObjectId>,
81}
82
83impl DeltaIslands {
84    /// True when islands are in effect (at least one `pack.island` regex matched a ref).
85    #[must_use]
86    pub fn is_active(&self) -> bool {
87        self.active
88    }
89
90    /// Whether `trg` may be delta-compressed against base `src` under island rules.
91    ///
92    /// Mirrors `in_same_island`: objects with no bitmap can delta against anything (target)
93    /// but must not be used as a base (source). Otherwise the target's island set must be a
94    /// subset of the source's.
95    #[must_use]
96    pub fn in_same_island(&self, trg: &ObjectId, src: &ObjectId) -> bool {
97        if !self.active {
98            return true;
99        }
100        let Some(trg_marks) = self.marks.get(trg) else {
101            // Target isn't important — allow delta against anything.
102            return true;
103        };
104        let Some(src_marks) = self.marks.get(src) else {
105            // Base has no island — never base a delta on it.
106            return false;
107        };
108        trg_marks.is_subset(src_marks)
109    }
110
111    /// Ordering bias for choosing a delta base (`island_delta_cmp`).
112    ///
113    /// Returns `-1` when `a` should sort before `b` (preferred as a base because its island
114    /// set is a superset), `1` when `b` should, `0` when neither dominates.
115    #[must_use]
116    pub fn delta_cmp(&self, a: &ObjectId, b: &ObjectId) -> i32 {
117        if !self.active {
118            return 0;
119        }
120        let a_marks = self.marks.get(a);
121        let b_marks = self.marks.get(b);
122
123        if let Some(am) = a_marks {
124            if b_marks.is_none_or(|bm| !am.is_subset(bm)) {
125                return -1;
126            }
127        }
128        if let Some(bm) = b_marks {
129            if a_marks.is_none_or(|am| !bm.is_subset(am)) {
130                return 1;
131            }
132        }
133        0
134    }
135
136    /// Whether `oid` belongs to the core island (used for `pack.islandcore` layering).
137    #[must_use]
138    pub fn is_core_object(&self, oid: &ObjectId) -> bool {
139        let Some(bit) = self.core_island_bit else {
140            return false;
141        };
142        self.marks.get(oid).is_some_and(|m| m.get(bit))
143    }
144
145    /// Whether a core island is configured (`pack.islandcore`).
146    #[must_use]
147    pub fn has_core(&self) -> bool {
148        self.core_island_bit.is_some()
149    }
150}
151
152/// Compile the left-anchored island regexes from `pack.island` config (in load order).
153fn load_island_regexes(cfg: &ConfigSet) -> Vec<regex::Regex> {
154    cfg.get_all("pack.island")
155        .into_iter()
156        .filter_map(|v| {
157            let pat = if v.starts_with('^') {
158                v
159            } else {
160                format!("^{v}")
161            };
162            regex::Regex::new(&pat).ok()
163        })
164        .collect()
165}
166
167/// Build the island name for a ref: pick the last regex that matches (last-one-wins) and
168/// join its non-empty capture groups with `-`. Returns `None` if no regex matches.
169fn island_name_for_ref(regexes: &[regex::Regex], ref_name: &str) -> Option<String> {
170    // Walk backwards for last-one-wins ordering.
171    for rx in regexes.iter().rev() {
172        if let Some(caps) = rx.captures(ref_name) {
173            let mut name = String::new();
174            for m in caps.iter().skip(1).flatten() {
175                if m.as_str().is_empty() {
176                    continue;
177                }
178                if !name.is_empty() {
179                    name.push('-');
180                }
181                name.push_str(m.as_str());
182            }
183            return Some(name);
184        }
185    }
186    None
187}
188
189/// Load and compute delta-island marks for the objects in `packed`.
190///
191/// `packed` is the set of object ids being written, used to bound propagation to objects in
192/// this pack (matching Git, which only marks objects in `to_pack`). Returns an inactive
193/// [`DeltaIslands`] when no `pack.island` regex matches any ref.
194pub fn load_delta_islands(
195    repo: &Repository,
196    cfg: &ConfigSet,
197    packed: &std::collections::HashSet<ObjectId>,
198) -> DeltaIslands {
199    let regexes = load_island_regexes(cfg);
200    if regexes.is_empty() {
201        return DeltaIslands::default();
202    }
203    let core_name = cfg.get("pack.islandcore");
204
205    // Collect refs and assign each to an island by name.
206    let mut by_name: HashMap<String, RemoteIsland> = HashMap::new();
207    let refs = crate::refs::list_refs(&repo.git_dir, "refs/").unwrap_or_default();
208    for (ref_name, oid) in &refs {
209        let Some(name) = island_name_for_ref(&regexes, ref_name) else {
210            continue;
211        };
212        let entry = by_name.entry(name).or_insert_with(|| RemoteIsland {
213            hash: 0,
214            oids: Vec::new(),
215        });
216        entry.oids.push(*oid);
217        // Hash = running sum of the first 8 bytes of each oid (little-endian, as Git memcpy's
218        // the leading hash bytes into a uint64_t).
219        let b = oid.as_bytes();
220        let core = u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]);
221        entry.hash = entry.hash.wrapping_add(core);
222    }
223
224    if by_name.is_empty() {
225        return DeltaIslands::default();
226    }
227
228    // Resolve which island name is the core (looked up by name before dedup).
229    let core_hash = core_name
230        .as_deref()
231        .and_then(|n| by_name.get(n))
232        .map(|ri| ri.hash);
233
234    // Deduplicate islands sharing the same hash (Git keeps the first occurrence). The
235    // surviving order is the iteration order over the island list.
236    let mut islands: Vec<RemoteIsland> = by_name.into_values().collect();
237    // Stable order for determinism (Git's order is hash-map iteration; result is the same
238    // because dedup only drops exact-hash duplicates and marking is order-independent).
239    islands.sort_by(|a, b| a.hash.cmp(&b.hash));
240    let mut deduped: Vec<RemoteIsland> = Vec::new();
241    for isl in islands {
242        if deduped.iter().any(|d| d.hash == isl.hash) {
243            continue;
244        }
245        deduped.push(isl);
246    }
247
248    let island_count = deduped.len();
249    let bitmap_size = island_count / 32 + 1;
250
251    let mut islands_obj = DeltaIslands {
252        marks: HashMap::new(),
253        core_island_bit: None,
254        active: false,
255    };
256
257    // Mark each surviving island's ref tips with its bit.
258    for (bit, isl) in deduped.iter().enumerate() {
259        let bit = bit as u32;
260        if core_hash == Some(isl.hash) {
261            islands_obj.core_island_bit = Some(bit);
262        }
263        for oid in &isl.oids {
264            let marks = islands_obj
265                .marks
266                .entry(*oid)
267                .or_insert_with(|| IslandBitmap::new(bitmap_size));
268            marks.set(bit);
269        }
270    }
271
272    islands_obj.active = true;
273
274    // Propagate marks across the object graph: commits -> tree + parents, then trees -> entries.
275    propagate_marks(repo, &mut islands_obj, packed, bitmap_size);
276
277    islands_obj
278}
279
280/// Helper to OR `marks` into the object's bitmap (`set_island_marks`).
281fn add_marks(
282    table: &mut HashMap<ObjectId, IslandBitmap>,
283    oid: &ObjectId,
284    marks: &IslandBitmap,
285    bitmap_size: usize,
286) {
287    let entry = table
288        .entry(*oid)
289        .or_insert_with(|| IslandBitmap::new(bitmap_size));
290    entry.or(marks);
291}
292
293/// Propagate island marks from ref tips down through commits, trees, and blobs.
294fn propagate_marks(
295    repo: &Repository,
296    islands: &mut DeltaIslands,
297    packed: &std::collections::HashSet<ObjectId>,
298    bitmap_size: usize,
299) {
300    // First, follow tags down to their target object (mirrors mark_remote_island_1's tag loop).
301    let tag_oids: Vec<ObjectId> = islands
302        .marks
303        .keys()
304        .copied()
305        .filter(|oid| matches!(repo.odb.read(oid).map(|o| o.kind), Ok(ObjectKind::Tag)))
306        .collect();
307    for tag_oid in tag_oids {
308        let marks = islands.marks.get(&tag_oid).cloned();
309        let Some(marks) = marks else { continue };
310        let mut cur = tag_oid;
311        while let Ok(obj) = repo.odb.read(&cur) {
312            if obj.kind != ObjectKind::Tag {
313                break;
314            }
315            let Ok(tag) = parse_tag(&obj.data) else { break };
316            add_marks(&mut islands.marks, &tag.object, &marks, bitmap_size);
317            cur = tag.object;
318        }
319    }
320
321    // Commits: process all commits that carry island marks, propagating to their tree and
322    // parents (commit-graph order is irrelevant — marks are unioned, so order-independent).
323    // We iterate to a fixpoint over the commit ancestry so deeper history (shared roots)
324    // accumulates the union of every descendant island.
325    let mut commit_marks: Vec<(ObjectId, IslandBitmap)> = islands
326        .marks
327        .iter()
328        .filter(|(oid, _)| matches!(repo.odb.read(oid).map(|o| o.kind), Ok(ObjectKind::Commit)))
329        .map(|(oid, m)| (*oid, m.clone()))
330        .collect();
331
332    let mut tree_roots: HashMap<ObjectId, IslandBitmap> = HashMap::new();
333    let mut visited: std::collections::HashSet<ObjectId> = std::collections::HashSet::new();
334    while let Some((cid, _)) = commit_marks.pop() {
335        // Re-read the (possibly grown) marks for this commit.
336        let Some(marks) = islands.marks.get(&cid).cloned() else {
337            continue;
338        };
339        let Ok(obj) = repo.odb.read(&cid) else {
340            continue;
341        };
342        if obj.kind != ObjectKind::Commit {
343            continue;
344        }
345        let Ok(commit) = parse_commit(&obj.data) else {
346            continue;
347        };
348        // Record root tree marks (for tree propagation).
349        add_marks(&mut tree_roots, &commit.tree, &marks, bitmap_size);
350        add_marks(&mut islands.marks, &commit.tree, &marks, bitmap_size);
351        // Propagate to parents; revisit a parent if its marks grew.
352        for parent in &commit.parents {
353            let before = islands.marks.get(parent).cloned();
354            add_marks(&mut islands.marks, parent, &marks, bitmap_size);
355            let after = islands.marks.get(parent).cloned();
356            if before != after || !visited.contains(parent) {
357                visited.insert(*parent);
358                let pm = islands
359                    .marks
360                    .get(parent)
361                    .cloned()
362                    .unwrap_or_else(|| IslandBitmap::new(bitmap_size));
363                commit_marks.push((*parent, pm));
364            }
365        }
366    }
367
368    // Trees: propagate root-tree marks down to all reachable sub-trees and blobs. Process
369    // shallowest trees first (Git sorts by tree depth) so marks flow down correctly even when
370    // a sub-tree appears under multiple parents.
371    let mut tree_queue: Vec<(ObjectId, IslandBitmap)> = tree_roots.into_iter().collect();
372    let mut tree_visited: std::collections::HashSet<ObjectId> = std::collections::HashSet::new();
373    while let Some((tid, _)) = tree_queue.pop() {
374        let Some(marks) = islands.marks.get(&tid).cloned() else {
375            continue;
376        };
377        // Record that we've expanded this tree. Re-expanding is harmless (add_marks only ORs
378        // bits) but we only re-queue a sub-tree below when its marks actually grew.
379        tree_visited.insert(tid);
380        let Ok(obj) = repo.odb.read(&tid) else {
381            continue;
382        };
383        if obj.kind != ObjectKind::Tree {
384            continue;
385        }
386        let Ok(entries) = parse_tree(&obj.data) else {
387            continue;
388        };
389        for ent in entries {
390            // Skip gitlinks (submodule commits).
391            if ent.mode == crate::index::MODE_GITLINK {
392                continue;
393            }
394            let before = islands.marks.get(&ent.oid).cloned();
395            add_marks(&mut islands.marks, &ent.oid, &marks, bitmap_size);
396            let after = islands.marks.get(&ent.oid).cloned();
397            // Recurse into sub-trees whose marks changed.
398            if ent.mode == 0o040000 && (before != after || !tree_visited.contains(&ent.oid)) {
399                let em = islands
400                    .marks
401                    .get(&ent.oid)
402                    .cloned()
403                    .unwrap_or_else(|| IslandBitmap::new(bitmap_size));
404                tree_queue.push((ent.oid, em));
405            }
406        }
407    }
408
409    // Drop marks for objects not in this pack and any all-zero bitmaps (matching Git, where
410    // only objects whose bitmap has bits set are considered "important").
411    islands
412        .marks
413        .retain(|oid, m| !m.is_empty() && packed.contains(oid));
414}