Skip to main content

sley/
pack_plan.rs

1//! Reachable-pack planning facade for embedders.
2
3use std::collections::HashSet;
4use std::fs;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7
8use sley_object::ObjectType;
9use sley_odb::ObjectReader;
10use sley_pack::{PackFile, PackWriteOptions, PackWriteSummary};
11
12use crate::{GitError, ObjectFormat, ObjectId, Repository, Result};
13
14/// Builder for a stable reachable-pack plan.
15#[derive(Debug)]
16pub struct ReachablePackPlanBuilder<'repo> {
17    repo: &'repo Repository,
18    roots: Vec<ObjectId>,
19    excluded: HashSet<ObjectId>,
20    options: PackWriteOptions,
21}
22
23/// A stable selection of reachable objects plus pack write options.
24#[derive(Debug, Clone)]
25pub struct ReachablePackPlan<'repo> {
26    repo: &'repo Repository,
27    object_ids: Vec<ObjectId>,
28    format: ObjectFormat,
29    options: PackWriteOptions,
30}
31
32/// Summary of a streamed or prepared pack.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ReachablePackSummary {
35    pub checksum: ObjectId,
36    pub object_count: usize,
37    pub delta_count: u32,
38    pub pack_size: u64,
39}
40
41/// A pack prepared in memory.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct PreparedReachablePack {
44    pub pack: Vec<u8>,
45    pub index: Vec<u8>,
46    pub summary: ReachablePackSummary,
47}
48
49/// A pack prepared on disk.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct PreparedReachablePackFile {
52    pub pack_path: PathBuf,
53    pub index: Vec<u8>,
54    pub summary: ReachablePackSummary,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58struct ReachablePackObjectMeta {
59    oid: ObjectId,
60    object_type: ObjectType,
61    size: u64,
62}
63
64impl Repository {
65    /// Start planning a reachable pack from this repository.
66    pub fn reachable_pack_plan(&self) -> ReachablePackPlanBuilder<'_> {
67        ReachablePackPlanBuilder {
68            repo: self,
69            roots: Vec::new(),
70            excluded: HashSet::new(),
71            options: PackWriteOptions::new(),
72        }
73    }
74}
75
76impl<'repo> ReachablePackPlanBuilder<'repo> {
77    pub fn root(mut self, root: ObjectId) -> Self {
78        self.roots.push(root);
79        self
80    }
81
82    pub fn roots<I>(mut self, roots: I) -> Self
83    where
84        I: IntoIterator<Item = ObjectId>,
85    {
86        self.roots.extend(roots);
87        self
88    }
89
90    pub fn exclude(mut self, oid: ObjectId) -> Self {
91        self.excluded.insert(oid);
92        self
93    }
94
95    pub fn exclusions<I>(mut self, excluded: I) -> Self
96    where
97        I: IntoIterator<Item = ObjectId>,
98    {
99        self.excluded.extend(excluded);
100        self
101    }
102
103    pub fn pack_options(mut self, options: PackWriteOptions) -> Self {
104        self.options = options;
105        self
106    }
107
108    /// Resolve roots/exclusions once and freeze the object order.
109    pub fn build(self) -> Result<Option<ReachablePackPlan<'repo>>> {
110        let format = self.repo.object_format();
111        let objects = self.repo.objects();
112        let reachable = sley_odb::collect_reachable_object_ids_excluding(
113            objects.as_ref(),
114            format,
115            self.roots,
116            &self.excluded,
117        )?;
118        if reachable.is_empty() {
119            return Ok(None);
120        }
121        let mut metadata = Vec::with_capacity(reachable.len());
122        for oid in reachable {
123            let (object_type, size) = match self.repo.read_object_header(&oid)? {
124                Some(header) => header,
125                None => {
126                    let object = self.repo.read_object(&oid)?;
127                    (object.object_type, object.body.len() as u64)
128                }
129            };
130            metadata.push(ReachablePackObjectMeta {
131                oid,
132                object_type,
133                size,
134            });
135        }
136        sort_pack_metadata(&mut metadata);
137        Ok(Some(ReachablePackPlan {
138            repo: self.repo,
139            object_ids: metadata.into_iter().map(|meta| meta.oid).collect(),
140            format,
141            options: self.options,
142        }))
143    }
144}
145
146impl ReachablePackPlan<'_> {
147    pub fn object_ids(&self) -> &[ObjectId] {
148        &self.object_ids
149    }
150
151    pub fn object_count(&self) -> usize {
152        self.object_ids.len()
153    }
154
155    pub fn object_format(&self) -> ObjectFormat {
156        self.format
157    }
158
159    pub fn pack_options(&self) -> &PackWriteOptions {
160        &self.options
161    }
162
163    /// Stream this exact plan to `writer`.
164    pub fn stream_to<W>(&self, writer: &mut W) -> Result<ReachablePackSummary>
165    where
166        W: Write,
167    {
168        if self.object_ids.is_empty() {
169            return Err(GitError::Unsupported(
170                "empty reachable pack plan cannot be streamed".into(),
171            ));
172        }
173        let objects = self.repo.objects();
174        let summary = PackFile::write_packed_from_source_to_writer(
175            &self.object_ids,
176            self.format,
177            &self.options,
178            |oid| objects.read_object(oid),
179            writer,
180        )?;
181        Ok(reachable_pack_summary(&summary))
182    }
183
184    /// Prepare this exact plan in memory, computing size/checksum without a
185    /// second compression pass.
186    pub fn prepare_to_memory(&self) -> Result<PreparedReachablePack> {
187        let mut pack = Vec::new();
188        let objects = self.repo.objects();
189        let summary = PackFile::write_packed_from_source_to_writer(
190            &self.object_ids,
191            self.format,
192            &self.options,
193            |oid| objects.read_object(oid),
194            &mut pack,
195        )?;
196        Ok(PreparedReachablePack {
197            pack,
198            index: summary.index.clone(),
199            summary: reachable_pack_summary(&summary),
200        })
201    }
202
203    /// Prepare this exact plan on disk, computing size/checksum without a
204    /// second compression pass.
205    pub fn prepare_to_file(
206        &self,
207        pack_path: impl AsRef<Path>,
208    ) -> Result<PreparedReachablePackFile> {
209        let pack_path = pack_path.as_ref();
210        if let Some(parent) = pack_path.parent() {
211            fs::create_dir_all(parent)?;
212        }
213        let mut file = fs::OpenOptions::new()
214            .write(true)
215            .create(true)
216            .truncate(true)
217            .open(pack_path)?;
218        let objects = self.repo.objects();
219        let summary = PackFile::write_packed_from_source_to_writer(
220            &self.object_ids,
221            self.format,
222            &self.options,
223            |oid| objects.read_object(oid),
224            &mut file,
225        )?;
226        file.sync_all()?;
227        Ok(PreparedReachablePackFile {
228            pack_path: pack_path.to_path_buf(),
229            index: summary.index.clone(),
230            summary: reachable_pack_summary(&summary),
231        })
232    }
233}
234
235fn reachable_pack_summary(summary: &PackWriteSummary) -> ReachablePackSummary {
236    ReachablePackSummary {
237        checksum: summary.checksum,
238        object_count: summary.entries.len(),
239        delta_count: summary.delta_count,
240        pack_size: summary.pack_size,
241    }
242}
243
244fn sort_pack_metadata(metadata: &mut [ReachablePackObjectMeta]) {
245    metadata.sort_by(|left, right| {
246        reachable_pack_type_rank(left.object_type)
247            .cmp(&reachable_pack_type_rank(right.object_type))
248            .then_with(|| right.size.cmp(&left.size))
249            .then_with(|| left.oid.as_bytes().cmp(right.oid.as_bytes()))
250    });
251}
252
253fn reachable_pack_type_rank(object_type: ObjectType) -> u8 {
254    match object_type {
255        ObjectType::Commit => 0,
256        ObjectType::Tag => 1,
257        ObjectType::Tree => 2,
258        ObjectType::Blob => 3,
259    }
260}