1use std::{ffi::OsStr, io, path::Path, str::FromStr, time::Instant};
2
3use anyhow::anyhow;
4use gix::{
5 Count, NestedProgress, Progress, hash, hash::ObjectId, interrupt, objs::bstr::ByteVec, odb::pack,
6 parallel::InOrderIter, prelude::Finalize, progress, traverse,
7};
8
9use crate::OutputFormat;
10
11pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=2;
12
13#[derive(Default, Eq, PartialEq, Debug, Clone)]
14pub enum ObjectExpansion {
15 #[default]
16 None,
17 TreeTraversal,
18 TreeDiff,
19}
20
21impl ObjectExpansion {
22 pub fn variants() -> &'static [&'static str] {
23 &["none", "tree-traversal", "tree-diff"]
24 }
25}
26
27impl FromStr for ObjectExpansion {
28 type Err = String;
29
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 use ObjectExpansion::*;
32 let slc = s.to_ascii_lowercase();
33 Ok(match slc.as_str() {
34 "none" => None,
35 "tree-traversal" => TreeTraversal,
36 "tree-diff" => TreeDiff,
37 _ => return Err("invalid value".into()),
38 })
39 }
40}
41
42impl From<ObjectExpansion> for pack::data::output::count::objects::ObjectExpansion {
43 fn from(v: ObjectExpansion) -> Self {
44 use pack::data::output::count::objects::ObjectExpansion::*;
45 match v {
46 ObjectExpansion::None => AsIs,
47 ObjectExpansion::TreeTraversal => TreeContents,
48 ObjectExpansion::TreeDiff => TreeAdditionsComparedToAncestor,
49 }
50 }
51}
52
53pub struct Context<W> {
55 pub expansion: ObjectExpansion,
57 pub nondeterministic_thread_count: Option<usize>,
62 pub thin: bool,
65 pub thread_limit: Option<usize>,
69 pub statistics: Option<OutputFormat>,
71 pub pack_cache_size_in_bytes: usize,
77 pub object_cache_size_in_bytes: usize,
83 pub out: W,
85}
86
87pub fn create<W, P>(
88 repository_path: impl AsRef<Path>,
89 tips: impl IntoIterator<Item = impl AsRef<OsStr>>,
90 input: Option<impl io::BufRead + Send + 'static>,
91 output_directory: Option<impl AsRef<Path>>,
92 mut progress: P,
93 Context {
94 expansion,
95 nondeterministic_thread_count,
96 thin,
97 thread_limit,
98 statistics,
99 pack_cache_size_in_bytes,
100 object_cache_size_in_bytes,
101 mut out,
102 }: Context<W>,
103) -> anyhow::Result<()>
104where
105 W: std::io::Write,
106 P: NestedProgress,
107 P::SubProgress: 'static,
108{
109 type ObjectIdIter = dyn Iterator<Item = Result<ObjectId, Box<dyn std::error::Error + Send + Sync>>> + Send;
110
111 let repo = gix::discover(repository_path)?;
112 let pack_compression = repo.pack_compression()?;
113 let repo = repo.into_sync();
114 progress.init(Some(2), progress::steps());
115 let tips = tips.into_iter();
116 let make_cancellation_err = || anyhow!("Cancelled by user");
117 let (mut handle, mut input): (_, Box<ObjectIdIter>) = match input {
118 None => {
119 let mut progress = progress.add_child("traversing");
120 progress.init(None, progress::count("commits"));
121 let tips = tips
122 .map({
123 let easy = repo.to_thread_local();
124 move |tip| {
125 ObjectId::from_hex(&Vec::from_os_str_lossy(tip.as_ref())).or_else(|_| {
126 easy.find_reference(tip.as_ref())
127 .map_err(anyhow::Error::from)
128 .and_then(|r| r.into_fully_peeled_id().map(gix::Id::detach).map_err(Into::into))
129 })
130 }
131 })
132 .collect::<Result<Vec<_>, _>>()?;
133 let handle = repo.objects.into_shared_arc().to_cache_arc();
134 let iter = Box::new(
135 traverse::commit::Simple::new(tips, handle.clone())
136 .map(|res| res.map_err(|err| Box::new(err) as Box<_>).map(|c| c.id))
137 .inspect(move |_| progress.inc()),
138 );
139 (handle, iter)
140 }
141 Some(input) => {
142 let mut progress = progress.add_child("iterating");
143 progress.init(None, progress::count("objects"));
144 let handle = repo.objects.into_shared_arc().to_cache_arc();
145 (
146 handle,
147 Box::new(
148 input
149 .lines()
150 .map(|hex_id| {
151 hex_id
152 .map_err(|err| Box::new(err) as Box<_>)
153 .and_then(|hex_id| ObjectId::from_hex(hex_id.as_bytes()).map_err(Into::into))
154 })
155 .inspect(move |_| progress.inc()),
156 ),
157 )
158 }
159 };
160
161 let mut stats = Statistics::default();
162 let chunk_size = 1000; let counts = {
164 let mut progress = progress.add_child("counting");
165 progress.init(None, progress::count("objects"));
166 let may_use_multiple_threads =
167 nondeterministic_thread_count.is_some() || matches!(expansion, ObjectExpansion::None);
168 let thread_limit = if may_use_multiple_threads {
169 nondeterministic_thread_count.or(thread_limit)
170 } else {
171 Some(1)
172 };
173 if nondeterministic_thread_count.is_some() && !may_use_multiple_threads {
174 progress.fail("Cannot use multi-threaded counting in tree-diff object expansion mode as it may yield way too many objects.".into());
175 }
176 let (_, _, thread_count) = gix::parallel::optimize_chunk_size_and_thread_limit(50, None, thread_limit, None);
177 let progress = progress::ThroughputOnDrop::new(progress);
178
179 {
180 let per_thread_object_pack_size = pack_cache_size_in_bytes / thread_count;
181 if per_thread_object_pack_size >= 10_000 {
182 handle.set_pack_cache(move || {
183 Box::new(pack::cache::lru::MemoryCappedHashmap::new(per_thread_object_pack_size))
184 });
185 }
186 if matches!(expansion, ObjectExpansion::TreeDiff) {
187 handle.set_object_cache(move || {
188 let per_thread_object_cache_size = object_cache_size_in_bytes / thread_count;
189 Box::new(pack::cache::object::MemoryCappedHashmap::new(
190 per_thread_object_cache_size,
191 ))
192 });
193 }
194 }
195 let input_object_expansion = expansion.into();
196 handle.prevent_pack_unload();
197 handle.ignore_replacements = true;
198 let (mut counts, count_stats) = if may_use_multiple_threads {
199 pack::data::output::count::objects(
200 handle.clone(),
201 input,
202 &progress,
203 &interrupt::IS_INTERRUPTED,
204 pack::data::output::count::objects::Options {
205 thread_limit,
206 chunk_size,
207 input_object_expansion,
208 },
209 )?
210 } else {
211 pack::data::output::count::objects_unthreaded(
212 &handle,
213 &mut input,
214 &progress,
215 &interrupt::IS_INTERRUPTED,
216 input_object_expansion,
217 )?
218 };
219 stats.counts = count_stats;
220 counts.shrink_to_fit();
221 counts
222 };
223
224 progress.inc();
225 let num_objects = counts.len();
226 let mut in_order_entries = {
227 let progress = progress.add_child("creating entries");
228 InOrderIter::from(pack::data::output::entry::iter_from_counts(
229 counts,
230 handle,
231 Box::new(progress),
232 pack::data::output::entry::iter_from_counts::Options {
233 thread_limit,
234 mode: pack::data::output::entry::iter_from_counts::Mode::PackCopyAndBaseObjects,
235 allow_thin_pack: thin,
236 chunk_size,
237 version: Default::default(),
238 compression: pack_compression,
239 },
240 ))
241 };
242
243 let mut entries_progress = progress.add_child("consuming");
244 entries_progress.init(Some(num_objects), progress::count("entries"));
245 let mut write_progress = progress.add_child("writing");
246 write_progress.init(None, progress::bytes());
247 let start = Instant::now();
248
249 let mut named_tempfile_store: Option<tempfile::NamedTempFile> = None;
250 let mut sink_store: std::io::Sink;
251 let (mut pack_file, output_directory): (&mut dyn std::io::Write, Option<_>) = match output_directory {
252 Some(dir) => {
253 named_tempfile_store = Some(tempfile::NamedTempFile::new_in(dir.as_ref())?);
254 (named_tempfile_store.as_mut().expect("packfile just set"), Some(dir))
255 }
256 None => {
257 sink_store = std::io::sink();
258 (&mut sink_store, None)
259 }
260 };
261 let mut interruptible_output_iter = interrupt::Iter::new(
262 pack::data::output::bytes::FromEntriesIter::new(
263 in_order_entries.by_ref().inspect(|e| {
264 if let Ok(entries) = e {
265 entries_progress.inc_by(entries.len());
266 }
267 }),
268 &mut pack_file,
269 num_objects as u32,
270 pack::data::Version::default(),
271 hash::Kind::default(),
272 ),
273 make_cancellation_err,
274 );
275 for io_res in interruptible_output_iter.by_ref() {
276 let written = io_res??;
277 write_progress.inc_by(written as usize);
278 }
279
280 let hash = interruptible_output_iter
281 .into_inner()
282 .digest()
283 .expect("iteration is done");
284 let pack_name = format!("{hash}.pack");
285 if let (Some(pack_file), Some(dir)) = (named_tempfile_store.take(), output_directory) {
286 pack_file.persist(dir.as_ref().join(pack_name))?;
287 } else {
288 writeln!(out, "{pack_name}")?;
289 }
290 stats.entries = in_order_entries.inner.finalize()?;
291
292 write_progress.show_throughput(start);
293 entries_progress.show_throughput(start);
294
295 if let Some(format) = statistics {
296 print(stats, format, out)?;
297 }
298 progress.inc();
299 Ok(())
300}
301
302fn print(stats: Statistics, format: OutputFormat, out: impl std::io::Write) -> anyhow::Result<()> {
303 match format {
304 OutputFormat::Human => human_output(stats, out).map_err(Into::into),
305 #[cfg(feature = "serde")]
306 OutputFormat::Json => serde_json::to_writer_pretty(out, &stats).map_err(Into::into),
307 }
308}
309
310fn human_output(
311 Statistics {
312 counts:
313 pack::data::output::count::objects::Outcome {
314 input_objects,
315 expanded_objects,
316 decoded_objects,
317 total_objects,
318 },
319 entries:
320 pack::data::output::entry::iter_from_counts::Outcome {
321 decoded_and_recompressed_objects,
322 missing_objects,
323 objects_copied_from_pack,
324 ref_delta_objects,
325 },
326 }: Statistics,
327 mut out: impl std::io::Write,
328) -> std::io::Result<()> {
329 let width = 30;
330 writeln!(out, "counting phase")?;
331 #[rustfmt::skip]
332 writeln!(
333 out,
334 "\t{:<width$} {}\n\t{:<width$} {}\n\t{:<width$} {}\n\t{:<width$} {}",
335 "input objects", input_objects,
336 "expanded objects", expanded_objects,
337 "decoded objects", decoded_objects,
338 "total objects", total_objects,
339 width = width
340 )?;
341 writeln!(out, "generation phase")?;
342 #[rustfmt::skip]
343 writeln!(
344 out,
345 "\t{:<width$} {}\n\t{:<width$} {}\n\t{:<width$} {}\n\t{:<width$} {}",
346 "decoded and recompressed", decoded_and_recompressed_objects,
347 "pack-to-pack copies", objects_copied_from_pack,
348 "ref-delta-objects", ref_delta_objects,
349 "missing objects", missing_objects,
350 width = width
351 )?;
352 Ok(())
353}
354
355#[derive(Default)]
356#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
357struct Statistics {
358 counts: pack::data::output::count::objects::Outcome,
359 entries: pack::data::output::entry::iter_from_counts::Outcome,
360}
361
362pub mod input_iteration {
363 use gix::{hash, traverse};
364 #[derive(Debug, thiserror::Error)]
365 pub enum Error {
366 #[error("input objects couldn't be iterated completely")]
367 Iteration(#[from] traverse::commit::simple::Error),
368 #[error("An error occurred while reading hashes from standard input")]
369 InputLinesIo(#[from] std::io::Error),
370 #[error("Could not decode hex hash provided on standard input")]
371 HashDecode(#[from] hash::decode::Error),
372 }
373}