1use 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#[derive(Debug)]
16pub struct ReachablePackPlanBuilder<'repo> {
17 repo: &'repo Repository,
18 roots: Vec<ObjectId>,
19 excluded: HashSet<ObjectId>,
20 options: PackWriteOptions,
21}
22
23#[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#[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#[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#[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 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 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 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 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 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}