1use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex};
5
6use crate::bloom::{
7 bloom_filter_contains, bloom_keyvec_for_path, BloomBuildOutcome, BloomFilterSettings,
8};
9use crate::error::Error;
10use crate::hash::verify_trailer;
11use crate::objects::{HashAlgo, ObjectId};
12use crate::odb::{hash_algo_for_objects_dir, Odb};
13use crate::pack_map::PackData;
14
15fn warn_once_for_disabled_bloom_layer(
16 caches: Option<&crate::repo_caches::RepoCaches>,
17 id: &str,
18) -> bool {
19 let key = format!("bloom-disabled:{id}");
20 caches
21 .map(|c| c.should_warn_commit_graph_once(&key))
22 .unwrap_or(true)
23}
24
25fn warn_once_for_base_chunk_too_small(
26 caches: Option<&crate::repo_caches::RepoCaches>,
27 id: &str,
28) -> bool {
29 let key = format!("base-chunk:{id}");
30 caches
31 .map(|c| c.should_warn_commit_graph_once(&key))
32 .unwrap_or(true)
33}
34
35const SIGNATURE: &[u8; 4] = b"CGPH";
36const GRAPH_VERSION: u8 = 1;
37const COMMIT_GRAPH_MIN_FILE_LEN: u64 = 28;
38
39const CHUNK_OID_FANOUT: u32 = 0x4f49_4446; const CHUNK_OID_LOOKUP: u32 = 0x4f49_444c; const CHUNK_COMMIT_DATA: u32 = 0x4344_4154; const CHUNK_GENERATION_DATA: u32 = 0x4744_4132; const CHUNK_GENERATION_DATA_OVERFLOW: u32 = 0x4744_4f32; const CHUNK_EXTRA_EDGES: u32 = 0x4544_4745; const GRAPH_PARENT_NONE: u32 = 0x7000_0000;
48const GRAPH_EXTRA_EDGES_NEEDED: u32 = 0x8000_0000;
51const GRAPH_LAST_EDGE: u32 = 0x8000_0000;
52const CHUNK_BLOOM_INDEXES: u32 = 0x4249_4458; const CHUNK_BLOOM_DATA: u32 = 0x4244_4154; const CHUNK_BASE_GRAPHS: u32 = 0x4241_5345; const BLOOM_HEADER: usize = crate::bloom::BLOOMDATA_HEADER_LEN;
57
58const CORRECTED_COMMIT_DATE_OFFSET_OVERFLOW: u32 = 1u32 << 31;
60
61fn warn_path_for_graph_file(path: &Path) -> String {
62 let s = path.to_string_lossy();
63 if let Some(idx) = s.find(".git/") {
64 return s[idx..].replace('\\', "/");
65 }
66 s.replace('\\', "/")
67}
68
69#[derive(Debug, Clone)]
71pub struct CommitGraphLayer {
72 pub path: PathBuf,
74 file: Arc<PackData>,
75 body_len: usize,
76 num_commits: u32,
77 oid_lookup_off: usize,
78 chunk_commit_data_off: usize,
79 chunk_generation_data: Option<usize>,
80 chunk_generation_overflow: Option<usize>,
81 read_generation_data: bool,
82 chunk_bloom_indexes: Option<usize>,
83 chunk_bloom_data: Option<(usize, usize)>,
84 bloom_settings: Option<BloomFilterSettings>,
85 bloom_disabled: bool,
86 base_chunk_size: usize,
89 hash_len: usize,
91 extra_edges_off: Option<usize>,
92 extra_edges_end: Option<usize>,
93}
94
95fn commit_graph_hash_len(hash_version: u8) -> Option<usize> {
97 HashAlgo::try_from(hash_version).ok().map(|a| a.len())
98}
99
100fn commit_graph_chain_hash_is_valid(h: &str, algo: HashAlgo) -> bool {
102 h.len() == algo.hex_len() && h.chars().all(|c| c.is_ascii_hexdigit())
103}
104
105const OIDF_BUCKET_BYTES: usize = 256 * 4;
106
107fn commit_graph_chunk_oob() -> Error {
108 Error::CorruptObject("commit-graph chunk extends past end of file".to_owned())
109}
110
111fn commit_graph_offset_out_of_range() -> Error {
112 Error::CorruptObject("commit-graph chunk offset out of range".to_owned())
113}
114
115fn checked_add_end(start: usize, len: usize, bound: usize) -> Result<usize, Error> {
116 start
117 .checked_add(len)
118 .filter(|&end| end <= bound)
119 .ok_or_else(commit_graph_chunk_oob)
120}
121
122fn body_slice(body: &[u8], start: usize, len: usize) -> Result<&[u8], Error> {
123 let end = checked_add_end(start, len, body.len())?;
124 body.get(start..end).ok_or_else(commit_graph_chunk_oob)
125}
126
127fn parse_toc_offset(body_len: usize, raw: [u8; 8]) -> Result<usize, Error> {
128 let off = u64::from_be_bytes(raw);
129 if off > body_len as u64 {
130 return Err(commit_graph_offset_out_of_range());
131 }
132 usize::try_from(off).map_err(|_| commit_graph_offset_out_of_range())
133}
134
135fn fanout_num_commits(body: &[u8], fanout_off: usize) -> Result<u32, Error> {
136 let fanout = body_slice(body, fanout_off, OIDF_BUCKET_BYTES)?;
137 let word = fanout[255 * 4..256 * 4]
138 .try_into()
139 .map_err(|_| Error::CorruptObject("commit-graph fanout corrupt".to_owned()))?;
140 Ok(u32::from_be_bytes(word))
141}
142
143#[allow(clippy::too_many_arguments)]
144fn validate_single_layer_parent_indices(
145 body: &[u8],
146 num_commits: u32,
147 commit_data_off: usize,
148 graph_data_width: usize,
149 hash_len: usize,
150 extra_edges_off: Option<usize>,
151 toc_entries: &[(u32, usize)],
152 file_end: usize,
153) -> Result<(), Error> {
154 fn chunk_byte_range(
155 start: usize,
156 toc_entries: &[(u32, usize)],
157 file_end: usize,
158 ) -> Result<usize, Error> {
159 let mut ends: Vec<usize> = toc_entries
160 .iter()
161 .map(|&(_, o)| o)
162 .filter(|&o| o > start)
163 .collect();
164 ends.sort_unstable();
165 Ok(ends.first().copied().unwrap_or(file_end))
166 }
167
168 for lex in 0..num_commits {
169 let base = commit_data_off + lex as usize * graph_data_width;
170 let p1_off = base + hash_len;
171 let p1 =
172 u32::from_be_bytes(body[p1_off..p1_off + 4].try_into().map_err(|_| {
173 Error::CorruptObject("commit-graph commit data corrupt".to_owned())
174 })?);
175 let p2 =
176 u32::from_be_bytes(body[p1_off + 4..p1_off + 8].try_into().map_err(|_| {
177 Error::CorruptObject("commit-graph commit data corrupt".to_owned())
178 })?);
179 if p1 != GRAPH_PARENT_NONE
180 && p1 & GRAPH_EXTRA_EDGES_NEEDED == 0
181 && p1 as usize >= num_commits as usize
182 {
183 return Err(Error::CorruptObject(
184 "commit-graph invalid parent index".to_owned(),
185 ));
186 }
187 if p2 != GRAPH_PARENT_NONE {
188 if p2 & GRAPH_EXTRA_EDGES_NEEDED != 0 {
189 let start = (p2 & !GRAPH_EXTRA_EDGES_NEEDED) as usize;
190 if let Some(edge_off) = extra_edges_off {
191 let edge_end = chunk_byte_range(edge_off, toc_entries, file_end)?;
192 let edge_len = edge_end.saturating_sub(edge_off);
193 if start * 4 >= edge_len {
194 return Err(Error::CorruptObject(
195 "commit-graph extra edge list out of range".to_owned(),
196 ));
197 }
198 let mut idx = start;
199 loop {
200 if idx * 4 + 4 > edge_len {
201 return Err(Error::CorruptObject(
202 "commit-graph extra edge list corrupt".to_owned(),
203 ));
204 }
205 let ev = u32::from_be_bytes(
206 body[edge_off + idx * 4..edge_off + idx * 4 + 4]
207 .try_into()
208 .map_err(|_| {
209 Error::CorruptObject(
210 "commit-graph extra edge list corrupt".to_owned(),
211 )
212 })?,
213 );
214 let parent = ev & !GRAPH_LAST_EDGE;
215 if parent != GRAPH_PARENT_NONE
216 && parent & GRAPH_EXTRA_EDGES_NEEDED == 0
217 && parent as usize >= num_commits as usize
218 {
219 return Err(Error::CorruptObject(
220 "commit-graph invalid parent index".to_owned(),
221 ));
222 }
223 if ev & GRAPH_LAST_EDGE != 0 {
224 break;
225 }
226 idx += 1;
227 }
228 }
229 } else if p2 as usize >= num_commits as usize {
230 return Err(Error::CorruptObject(
231 "commit-graph invalid parent index".to_owned(),
232 ));
233 }
234 }
235 }
236 Ok(())
237}
238
239#[derive(Clone, Copy, PartialEq, Eq)]
240enum CommitGraphParseMode {
241 Open,
243 Verify,
245}
246
247impl CommitGraphLayer {
248 fn body(&self) -> &[u8] {
249 &self.file[..self.body_len]
250 }
251
252 fn try_open(path: PathBuf) -> Result<Self, Error> {
254 let storage = PackData::open_with_min_len(&path, COMMIT_GRAPH_MIN_FILE_LEN)?;
255 Self::try_parse_storage(path, storage, CommitGraphParseMode::Open)
256 }
257
258 pub fn try_parse(path: PathBuf, raw: Vec<u8>) -> Result<Self, Error> {
260 Self::try_parse_storage(
261 path,
262 PackData::from_owned(raw),
263 CommitGraphParseMode::Verify,
264 )
265 }
266
267 fn try_parse_storage(
268 path: PathBuf,
269 storage: Arc<PackData>,
270 mode: CommitGraphParseMode,
271 ) -> Result<Self, Error> {
272 let raw = storage.as_ref();
273 if raw.len() < 28 {
274 return Err(Error::CorruptObject(
275 "commit-graph file too small".to_owned(),
276 ));
277 }
278 let hash_len = match commit_graph_hash_len(raw[5]) {
281 Some(n) => n,
282 None => {
283 return Err(Error::CorruptObject(format!(
284 "commit-graph version/hash not supported (version {} hash {})",
285 raw[4], raw[5]
286 )))
287 }
288 };
289 let algo = HashAlgo::try_from(raw[5]).map_err(|_| {
290 Error::CorruptObject(format!(
291 "commit-graph version/hash not supported (version {} hash {})",
292 raw[4], raw[5]
293 ))
294 })?;
295 if mode == CommitGraphParseMode::Verify {
296 verify_trailer(algo, raw)
297 .map_err(|_| Error::CorruptObject("commit-graph incorrect checksum".to_owned()))?;
298 }
299 let body_len = raw.len().saturating_sub(hash_len);
300 let body = &raw[..body_len];
301 if body.len() < 8 || &body[0..4] != SIGNATURE {
302 return Err(Error::CorruptObject(
303 "commit-graph has bad signature".to_owned(),
304 ));
305 }
306 if body[4] != GRAPH_VERSION {
307 return Err(Error::CorruptObject(format!(
308 "commit-graph version/hash not supported (version {} hash {})",
309 body[4], body[5]
310 )));
311 }
312 let num_chunks = body[6] as usize;
313 let toc_start: usize = 8;
314 let toc_end = num_chunks
315 .checked_add(1)
316 .and_then(|n| n.checked_mul(12))
317 .and_then(|n| toc_start.checked_add(n))
318 .ok_or_else(|| {
319 Error::CorruptObject("commit-graph truncated at chunk table".to_owned())
320 })?;
321 if body.len() < toc_end {
322 return Err(Error::CorruptObject(
323 "commit-graph truncated at chunk table".to_owned(),
324 ));
325 }
326
327 let mut fanout_off = None;
328 let mut oid_lookup_off = None;
329 let mut commit_data_off = None;
330 let mut generation_off = None;
331 let mut generation_overflow_off = None;
332 let mut bloom_idx_off = None;
333 let mut bloom_data_range = None;
334 let mut base_graphs_off = None;
335 let mut extra_edges_off = None;
336 let mut extra_edges_end = None;
337 let mut chunk_offsets: Vec<usize> = Vec::new();
338 let mut toc_entries: Vec<(u32, usize)> = Vec::with_capacity(num_chunks);
339
340 for i in 0..num_chunks {
341 let e = toc_start + i * 12;
342 let id = u32::from_be_bytes(
343 body[e..e + 4]
344 .try_into()
345 .map_err(|_| Error::CorruptObject("commit-graph bad TOC".to_owned()))?,
346 );
347 let off = parse_toc_offset(
348 body.len(),
349 body[e + 4..e + 12]
350 .try_into()
351 .map_err(|_| Error::CorruptObject("commit-graph bad TOC".to_owned()))?,
352 )?;
353 toc_entries.push((id, off));
354 chunk_offsets.push(off);
355 match id {
356 CHUNK_OID_FANOUT => fanout_off = Some(off),
357 CHUNK_OID_LOOKUP => oid_lookup_off = Some(off),
358 CHUNK_COMMIT_DATA => commit_data_off = Some(off),
359 CHUNK_GENERATION_DATA => generation_off = Some(off),
360 CHUNK_GENERATION_DATA_OVERFLOW => generation_overflow_off = Some(off),
361 CHUNK_BLOOM_INDEXES => bloom_idx_off = Some(off),
362 CHUNK_EXTRA_EDGES => {
363 extra_edges_off = Some(off);
364 extra_edges_end = Some(if i + 1 < num_chunks {
365 let e2 = toc_start
366 .checked_add((i + 1).checked_mul(12).ok_or_else(|| {
367 Error::CorruptObject("commit-graph bad TOC".to_owned())
368 })?)
369 .ok_or_else(|| {
370 Error::CorruptObject("commit-graph bad TOC".to_owned())
371 })?;
372 parse_toc_offset(
373 body.len(),
374 body[e2 + 4..e2 + 12].try_into().map_err(|_| {
375 Error::CorruptObject("commit-graph bad TOC".to_owned())
376 })?,
377 )?
378 } else {
379 let term = toc_start
380 .checked_add(num_chunks.checked_mul(12).ok_or_else(|| {
381 Error::CorruptObject("commit-graph bad TOC".to_owned())
382 })?)
383 .ok_or_else(|| {
384 Error::CorruptObject("commit-graph bad TOC".to_owned())
385 })?;
386 parse_toc_offset(
387 body.len(),
388 body[term + 4..term + 12].try_into().map_err(|_| {
389 Error::CorruptObject("commit-graph bad TOC".to_owned())
390 })?,
391 )?
392 });
393 }
394 CHUNK_BASE_GRAPHS => base_graphs_off = Some(off),
395 CHUNK_BLOOM_DATA => {
396 let end = if i + 1 < num_chunks {
397 let e2 = toc_start
398 .checked_add((i + 1).checked_mul(12).ok_or_else(|| {
399 Error::CorruptObject("commit-graph bad TOC".to_owned())
400 })?)
401 .ok_or_else(|| {
402 Error::CorruptObject("commit-graph bad TOC".to_owned())
403 })?;
404 parse_toc_offset(
405 body.len(),
406 body[e2 + 4..e2 + 12].try_into().map_err(|_| {
407 Error::CorruptObject("commit-graph bad TOC".to_owned())
408 })?,
409 )?
410 } else {
411 let term = toc_start
412 .checked_add(num_chunks.checked_mul(12).ok_or_else(|| {
413 Error::CorruptObject("commit-graph bad TOC".to_owned())
414 })?)
415 .ok_or_else(|| {
416 Error::CorruptObject("commit-graph bad TOC".to_owned())
417 })?;
418 parse_toc_offset(
419 body.len(),
420 body[term + 4..term + 12].try_into().map_err(|_| {
421 Error::CorruptObject("commit-graph bad TOC".to_owned())
422 })?,
423 )?
424 };
425 let len = end.checked_sub(off).ok_or_else(|| {
426 Error::CorruptObject("commit-graph chunk layout invalid".to_owned())
427 })?;
428 bloom_data_range = Some((off, len));
429 }
430 _ => {}
431 }
432 }
433 let file_end_pos = toc_start
434 .checked_add(
435 num_chunks
436 .checked_mul(12)
437 .ok_or_else(|| Error::CorruptObject("commit-graph bad file end".to_owned()))?,
438 )
439 .and_then(|t| t.checked_add(4))
440 .ok_or_else(|| Error::CorruptObject("commit-graph bad file end".to_owned()))?;
441 let file_end = parse_toc_offset(
442 body.len(),
443 body[file_end_pos..file_end_pos + 8]
444 .try_into()
445 .map_err(|_| Error::CorruptObject("commit-graph bad file end".to_owned()))?,
446 )?;
447 if file_end > body.len() {
448 return Err(commit_graph_offset_out_of_range());
449 }
450 chunk_offsets.push(file_end);
451 chunk_offsets.sort_unstable();
452 chunk_offsets.dedup();
453
454 fn chunk_byte_range(
455 start: usize,
456 toc_entries: &[(u32, usize)],
457 file_end: usize,
458 ) -> Result<usize, Error> {
459 let mut ends: Vec<usize> = toc_entries
460 .iter()
461 .map(|&(_, o)| o)
462 .filter(|&o| o > start)
463 .collect();
464 ends.sort_unstable();
465 let end = ends.first().copied().unwrap_or(file_end);
466 if end < start {
467 return Err(Error::CorruptObject(
468 "commit-graph chunk layout invalid".to_owned(),
469 ));
470 }
471 Ok(end)
472 }
473
474 if mode == CommitGraphParseMode::Verify {
475 if let Some(gda) = generation_off {
476 let gda_end = chunk_byte_range(gda, &toc_entries, file_end)?;
477 let gda_len = gda_end.saturating_sub(gda);
478 let fo = fanout_off.ok_or_else(|| {
479 Error::CorruptObject("commit-graph missing fanout".to_owned())
480 })?;
481 let num_commits_probe = fanout_num_commits(body, fo)?;
482 let expected = num_commits_probe as usize * 4;
483 if gda_len < expected {
484 return Err(Error::CorruptObject(
485 "commit-graph generation data chunk is too small".to_owned(),
486 ));
487 }
488 let gda_slice = body.get(gda..gda + expected).ok_or_else(|| {
489 Error::CorruptObject("commit-graph generation data OOB".to_owned())
490 })?;
491 let mut max_overflow_idx: Option<u32> = None;
492 for w in 0..num_commits_probe as usize {
493 let v = u32::from_be_bytes(gda_slice[w * 4..w * 4 + 4].try_into().map_err(
494 |_| Error::CorruptObject("commit-graph GDA2 corrupt".to_owned()),
495 )?);
496 if v & CORRECTED_COMMIT_DATE_OFFSET_OVERFLOW != 0 {
497 let pos = v ^ CORRECTED_COMMIT_DATE_OFFSET_OVERFLOW;
498 max_overflow_idx = Some(match max_overflow_idx {
499 None => pos,
500 Some(m) => m.max(pos),
501 });
502 }
503 }
504 if let Some(pos) = max_overflow_idx {
505 let Some(gdo_start) = generation_overflow_off else {
506 return Err(Error::CorruptObject(
507 "commit-graph requires overflow generation data but has none"
508 .to_owned(),
509 ));
510 };
511 let gdo_end = chunk_byte_range(gdo_start, &toc_entries, file_end)?;
512 let overflow_bytes = gdo_end.saturating_sub(gdo_start);
513 let n_slots = overflow_bytes / 8;
514 if n_slots <= pos as usize {
515 return Err(Error::CorruptObject(
516 "commit-graph overflow generation data is too small".to_owned(),
517 ));
518 }
519 }
520 }
521 }
522 let bidx_len = bloom_idx_off.and_then(|b| {
523 chunk_offsets
524 .iter()
525 .find(|&&o| o > b)
526 .map(|&next| next.saturating_sub(b))
527 });
528
529 let fanout_off = fanout_off.ok_or_else(|| {
530 Error::CorruptObject("commit-graph missing OID fanout chunk".to_owned())
531 })?;
532 let oid_lookup_off = oid_lookup_off.ok_or_else(|| {
533 Error::CorruptObject("commit-graph missing OID lookup chunk".to_owned())
534 })?;
535 let commit_data_off = commit_data_off.ok_or_else(|| {
536 Error::CorruptObject("commit-graph missing commit data chunk".to_owned())
537 })?;
538 checked_add_end(oid_lookup_off, 4, body.len())?;
539 let num_commits = fanout_num_commits(body, fanout_off)?;
540 let oid_lookup_bytes = (num_commits as usize)
541 .checked_mul(hash_len)
542 .ok_or_else(commit_graph_chunk_oob)?;
543 checked_add_end(oid_lookup_off, oid_lookup_bytes, body.len())?;
544 let graph_data_width = hash_len
545 .checked_add(16)
546 .ok_or_else(commit_graph_chunk_oob)?;
547 let cdat_bytes = (num_commits as usize)
548 .checked_mul(graph_data_width)
549 .ok_or_else(commit_graph_chunk_oob)?;
550 checked_add_end(commit_data_off, cdat_bytes, body.len())?;
551
552 if mode == CommitGraphParseMode::Verify {
553 for bucket in 0usize..255 {
554 let a_off = fanout_off
555 .checked_add(bucket.checked_mul(4).ok_or_else(commit_graph_chunk_oob)?)
556 .ok_or_else(commit_graph_chunk_oob)?;
557 let a =
558 u32::from_be_bytes(body_slice(body, a_off, 4)?.try_into().map_err(|_| {
559 Error::CorruptObject("commit-graph fanout corrupt".to_owned())
560 })?);
561 let b_off = fanout_off
562 .checked_add(
563 (bucket + 1)
564 .checked_mul(4)
565 .ok_or_else(commit_graph_chunk_oob)?,
566 )
567 .ok_or_else(commit_graph_chunk_oob)?;
568 let b =
569 u32::from_be_bytes(body_slice(body, b_off, 4)?.try_into().map_err(|_| {
570 Error::CorruptObject("commit-graph fanout corrupt".to_owned())
571 })?);
572 if a > b {
573 return Err(Error::CorruptObject(
574 "commit-graph fanout value non-monotonic".to_owned(),
575 ));
576 }
577 }
578
579 for lex in 1..num_commits {
580 let off_a = oid_lookup_off
581 .checked_add(
582 (lex as usize - 1)
583 .checked_mul(hash_len)
584 .ok_or_else(commit_graph_chunk_oob)?,
585 )
586 .ok_or_else(commit_graph_chunk_oob)?;
587 let off_b = oid_lookup_off
588 .checked_add(
589 (lex as usize)
590 .checked_mul(hash_len)
591 .ok_or_else(commit_graph_chunk_oob)?,
592 )
593 .ok_or_else(commit_graph_chunk_oob)?;
594 let slice_a = body_slice(body, off_a, hash_len)?;
595 let slice_b = body_slice(body, off_b, hash_len)?;
596 if slice_a >= slice_b {
597 return Err(Error::CorruptObject(
598 "commit-graph incorrect OID order".to_owned(),
599 ));
600 }
601 }
602
603 if base_graphs_off.is_none() {
604 validate_single_layer_parent_indices(
605 body,
606 num_commits,
607 commit_data_off,
608 graph_data_width,
609 hash_len,
610 extra_edges_off,
611 &toc_entries,
612 file_end,
613 )?;
614 }
615 }
616
617 let read_generation_data = generation_off.is_some();
618 let mut bloom_settings = None;
619 let mut chunk_bloom_data = None;
620 if let (Some(_bidx), Some((bdat_off, bdat_len))) = (bloom_idx_off, bloom_data_range) {
621 if bdat_len < BLOOM_HEADER {
622 } else if bdat_off + bdat_len <= body.len() {
623 let hdr = &body[bdat_off..bdat_off + BLOOM_HEADER];
624 let hash_version: [u8; 4] = hdr[0..4]
625 .try_into()
626 .map_err(|_| Error::CorruptObject("Bloom header corrupt".to_owned()))?;
627 let num_hashes: [u8; 4] = hdr[4..8]
628 .try_into()
629 .map_err(|_| Error::CorruptObject("Bloom header corrupt".to_owned()))?;
630 let bits_per_entry: [u8; 4] = hdr[8..12]
631 .try_into()
632 .map_err(|_| Error::CorruptObject("Bloom header corrupt".to_owned()))?;
633 bloom_settings = Some(BloomFilterSettings {
634 hash_version: u32::from_be_bytes(hash_version),
635 num_hashes: u32::from_be_bytes(num_hashes),
636 bits_per_entry: u32::from_be_bytes(bits_per_entry),
637 max_changed_paths: 512,
638 });
639 chunk_bloom_data = Some((bdat_off, bdat_len));
640 }
641 }
642
643 let bloom_indexes_ok = if let (Some(bidx), Some(bsize)) = (bloom_idx_off, bidx_len) {
644 !(bsize / 4 != num_commits as usize || bidx + bsize > body.len())
645 } else {
646 false
647 };
648 let bloom_pair_ok = bloom_settings.is_some()
649 && chunk_bloom_data.is_some()
650 && bloom_indexes_ok
651 && chunk_bloom_data.is_some_and(|(_, len)| len >= BLOOM_HEADER);
652
653 let (chunk_bloom_indexes, bloom_settings) = if bloom_pair_ok {
654 (bloom_idx_off, bloom_settings)
655 } else {
656 (None, None)
657 };
658 let chunk_bloom_data = if bloom_pair_ok {
659 chunk_bloom_data
660 } else {
661 None
662 };
663
664 let base_chunk_size = match base_graphs_off {
665 Some(off) => {
666 let end = chunk_byte_range(off, &toc_entries, file_end)?;
667 end.saturating_sub(off)
668 }
669 None => 0,
670 };
671
672 Ok(Self {
673 path,
674 file: storage,
675 body_len,
676 num_commits,
677 oid_lookup_off,
678 chunk_commit_data_off: commit_data_off,
679 chunk_generation_data: generation_off,
680 chunk_generation_overflow: generation_overflow_off,
681 read_generation_data,
682 chunk_bloom_indexes,
683 chunk_bloom_data,
684 bloom_settings,
685 bloom_disabled: false,
686 base_chunk_size,
687 hash_len,
688 extra_edges_off,
689 extra_edges_end,
690 })
691 }
692
693 fn parse(path: PathBuf, raw: Vec<u8>) -> Option<Self> {
694 Self::try_parse(path, raw).ok()
695 }
696
697 fn oid_at_lex(&self, lex_index: u32) -> Option<ObjectId> {
698 if lex_index >= self.num_commits {
699 return None;
700 }
701 let off = self.oid_lookup_off + lex_index as usize * self.hash_len;
702 ObjectId::from_bytes(self.body().get(off..off + self.hash_len)?).ok()
703 }
704
705 fn parent_global_positions(&self, lex_index: u32) -> Option<Vec<u32>> {
712 let (p1, p2, _) = self.cdat_record(lex_index)?;
713 let mut out = Vec::new();
714 if p1 == GRAPH_PARENT_NONE {
715 return Some(out);
716 }
717 if p1 & GRAPH_EXTRA_EDGES_NEEDED != 0 {
718 return None;
719 }
720 out.push(p1);
721 if p2 == GRAPH_PARENT_NONE {
722 return Some(out);
723 }
724 if p2 & GRAPH_EXTRA_EDGES_NEEDED != 0 {
725 let start = (p2 & !GRAPH_EXTRA_EDGES_NEEDED) as usize;
726 let edge_off = self.extra_edges_off?;
727 let edge_end = self.extra_edges_end?;
728 let edge_len = edge_end.saturating_sub(edge_off);
729 if start.saturating_mul(4) >= edge_len {
730 return None;
731 }
732 let mut idx = start;
733 loop {
734 if idx.saturating_mul(4).saturating_add(4) > edge_len {
735 return None;
736 }
737 let ev = u32::from_be_bytes(
738 self.body()[edge_off + idx * 4..edge_off + idx * 4 + 4]
739 .try_into()
740 .ok()?,
741 );
742 let parent = ev & !GRAPH_LAST_EDGE;
743 if parent != GRAPH_PARENT_NONE && parent & GRAPH_EXTRA_EDGES_NEEDED == 0 {
744 out.push(parent);
745 }
746 if ev & GRAPH_LAST_EDGE != 0 {
747 break;
748 }
749 idx += 1;
750 }
751 return Some(out);
752 }
753 out.push(p2);
754 Some(out)
755 }
756
757 fn cdat_root_tree(&self, lex_index: u32) -> Option<ObjectId> {
759 if lex_index >= self.num_commits {
760 return None;
761 }
762 let base = self.chunk_commit_data_off + lex_index as usize * (self.hash_len + 16);
763 ObjectId::from_bytes(self.body().get(base..base + self.hash_len)?).ok()
764 }
765
766 fn cdat_record(&self, lex_index: u32) -> Option<(u32, u32, i64)> {
767 if lex_index >= self.num_commits {
768 return None;
769 }
770 let base = self.chunk_commit_data_off + lex_index as usize * (self.hash_len + 16);
771 let p1_off = base + self.hash_len;
772 let p1 = u32::from_be_bytes(self.body().get(p1_off..p1_off + 4)?.try_into().ok()?);
773 let p2 = u32::from_be_bytes(self.body().get(p1_off + 4..p1_off + 8)?.try_into().ok()?);
774 let gen_time =
775 u64::from_be_bytes(self.body().get(p1_off + 8..p1_off + 16)?.try_into().ok()?);
776 let commit_time = (gen_time & 0x3_FFFF_FFFF) as i64;
777 Some((p1, p2, commit_time))
778 }
779
780 fn corrected_commit_generation(&self, lex_index: u32) -> Option<u64> {
782 if !self.read_generation_data {
783 return None;
784 }
785 let (_, _, commit_time) = self.cdat_record(lex_index)?;
786 let gda_off = self.chunk_generation_data?;
787 if lex_index >= self.num_commits {
788 return None;
789 }
790 let start = gda_off + lex_index as usize * 4;
791 let word = u32::from_be_bytes(self.body().get(start..start + 4)?.try_into().ok()?);
792 let offset = if word & CORRECTED_COMMIT_DATE_OFFSET_OVERFLOW != 0 {
793 let slot = (word ^ CORRECTED_COMMIT_DATE_OFFSET_OVERFLOW) as usize;
794 let gdo_off = self.chunk_generation_overflow?;
795 let off = gdo_off + slot * 8;
796 let hi = u32::from_be_bytes(self.body().get(off..off + 4)?.try_into().ok()?);
797 let lo = u32::from_be_bytes(self.body().get(off + 4..off + 8)?.try_into().ok()?);
798 (u64::from(hi) << 32) | u64::from(lo)
799 } else {
800 u64::from(word)
801 };
802 Some((commit_time as u64).saturating_add(offset))
803 }
804
805 fn bsearch_oid(&self, oid: &ObjectId) -> Option<u32> {
806 let mut lo = 0u32;
807 let mut hi = self.num_commits;
808 let bytes = oid.as_bytes();
809 while lo < hi {
810 let mid = (lo + hi) / 2;
811 let off = self.oid_lookup_off + mid as usize * self.hash_len;
812 let slice = &self.body()[off..off + self.hash_len];
813 match slice.cmp(bytes) {
814 std::cmp::Ordering::Less => lo = mid + 1,
815 std::cmp::Ordering::Greater => hi = mid,
816 std::cmp::Ordering::Equal => return Some(mid),
817 }
818 }
819 None
820 }
821
822 fn disable_bloom(&mut self) {
823 self.chunk_bloom_indexes = None;
824 self.chunk_bloom_data = None;
825 self.bloom_settings = None;
826 self.bloom_disabled = true;
827 }
828
829 fn layer_display_id(&self) -> String {
830 self.path
831 .file_stem()
832 .and_then(|s| s.to_str())
833 .map(|s| s.strip_prefix("graph-").unwrap_or(s).to_string())
834 .unwrap_or_else(|| "commit-graph".to_string())
835 }
836
837 fn bloom_filter_slice(&self, lex_index: u32) -> Option<&[u8]> {
838 let _settings = self.bloom_settings.as_ref()?;
839 let bidx_base = self.chunk_bloom_indexes?;
840 let (bdat_off, bdat_total) = self.chunk_bloom_data?;
841 let _graph_warn = warn_path_for_graph_file(self.path.as_path());
842 if lex_index >= self.num_commits {
843 return None;
844 }
845 let payload_len = bdat_total.saturating_sub(BLOOM_HEADER);
846 let end_rel = u32::from_be_bytes(
847 self.body()[bidx_base + lex_index as usize * 4..bidx_base + lex_index as usize * 4 + 4]
848 .try_into()
849 .ok()?,
850 ) as usize;
851 let start_rel = if lex_index == 0 {
852 0usize
853 } else {
854 u32::from_be_bytes(
855 self.body()[bidx_base + (lex_index as usize - 1) * 4
856 ..bidx_base + (lex_index as usize - 1) * 4 + 4]
857 .try_into()
858 .ok()?,
859 ) as usize
860 };
861 let max_payload = payload_len;
867 if end_rel > max_payload {
868 return None;
869 }
870 if start_rel > max_payload {
871 return None;
872 }
873 if end_rel < start_rel {
874 return None;
875 }
876 let data_base = bdat_off + BLOOM_HEADER;
877 let abs_start = data_base + start_rel;
878 let abs_end = data_base + end_rel;
879 if abs_end > bdat_off + bdat_total || abs_start > abs_end {
880 return None;
881 }
882 Some(&self.body()[abs_start..abs_end])
883 }
884}
885
886#[derive(Debug, Clone, Copy, PartialEq, Eq)]
888pub enum BloomPrecheck {
889 Inapplicable,
891 NotInGraph,
893 FilterNotPresent,
895 DefinitelyNot,
897 Maybe,
899}
900
901#[derive(Debug, Default, Clone)]
903pub struct BloomWalkStats {
904 pub filter_not_present: u32,
906 pub maybe: u32,
908 pub definitely_not: u32,
910 pub false_positive: u32,
912}
913
914impl BloomWalkStats {
915 pub fn record_precheck(&mut self, pre: BloomPrecheck) {
917 match pre {
918 BloomPrecheck::Inapplicable | BloomPrecheck::NotInGraph => {}
919 BloomPrecheck::FilterNotPresent => self.filter_not_present += 1,
920 BloomPrecheck::DefinitelyNot => self.definitely_not += 1,
921 BloomPrecheck::Maybe => self.maybe += 1,
922 }
923 }
924
925 pub fn record_false_positive(&mut self) {
927 self.false_positive += 1;
928 }
929}
930
931pub type BloomWalkStatsHandle = Arc<Mutex<BloomWalkStats>>;
933
934#[derive(Debug, Clone)]
936pub struct CommitGraphChain {
937 layers: Vec<CommitGraphLayer>,
938}
939
940#[doc(hidden)]
942pub fn commit_graph_chain_from_layers_for_test(layers: Vec<CommitGraphLayer>) -> CommitGraphChain {
943 CommitGraphChain { layers }
944}
945
946impl CommitGraphChain {
947 #[must_use]
949 pub fn top_layer_bloom_settings(&self) -> Option<BloomFilterSettings> {
950 self.layers.first()?.bloom_settings
951 }
952
953 #[must_use]
955 pub fn total_commits(&self) -> u32 {
956 self.layers.iter().map(|l| l.num_commits).sum()
957 }
958
959 #[must_use]
961 pub fn layer_paths_oldest_first(&self) -> Vec<PathBuf> {
962 self.layers.iter().rev().map(|l| l.path.clone()).collect()
963 }
964
965 #[must_use]
967 pub fn num_layers(&self) -> usize {
968 self.layers.len()
969 }
970
971 #[must_use]
973 pub fn layer_commit_counts_tip_first(&self) -> Vec<u32> {
974 self.layers.iter().map(|l| l.num_commits).collect()
975 }
976
977 #[must_use]
979 pub fn layer_has_generation_data_tip_first(&self) -> Vec<bool> {
980 self.layers
981 .iter()
982 .map(|l| l.chunk_generation_data.is_some())
983 .collect()
984 }
985
986 #[must_use]
988 pub fn layer_hashes_tip_first(&self) -> Vec<String> {
989 self.layers.iter().map(|l| l.layer_display_id()).collect()
990 }
991
992 #[must_use]
996 pub fn layer_object_dirs_tip_first(&self) -> Vec<PathBuf> {
997 self.layers
998 .iter()
999 .map(|l| {
1000 let p = l.path.as_path();
1003 let info = if p
1004 .parent()
1005 .and_then(|d| d.file_name())
1006 .map(|n| n == "commit-graphs")
1007 .unwrap_or(false)
1008 {
1009 p.parent().and_then(|d| d.parent())
1010 } else {
1011 p.parent()
1012 };
1013 info.and_then(|d| d.parent())
1014 .map(Path::to_path_buf)
1015 .unwrap_or_else(|| PathBuf::from("."))
1016 })
1017 .collect()
1018 }
1019
1020 #[must_use]
1024 pub fn sub_chain_tip_first(&self, start: usize, end: usize) -> Option<Self> {
1025 let end = end.min(self.layers.len());
1026 if start >= end {
1027 return None;
1028 }
1029 Some(Self {
1030 layers: self.layers[start..end].to_vec(),
1031 })
1032 }
1033
1034 #[must_use]
1036 pub fn layer_oids(&self, tip_first_idx: usize) -> Vec<ObjectId> {
1037 let Some(layer) = self.layers.get(tip_first_idx) else {
1038 return Vec::new();
1039 };
1040 let mut out = Vec::with_capacity(layer.num_commits as usize);
1041 for i in 0..layer.num_commits {
1042 if let Some(oid) = layer.oid_at_lex(i) {
1043 out.push(oid);
1044 }
1045 }
1046 out
1047 }
1048
1049 pub fn try_load(objects_dir: &Path) -> Result<Option<Self>, Error> {
1054 Self::try_load_with_caches(objects_dir, None)
1055 }
1056
1057 pub fn try_load_with_caches(
1059 objects_dir: &Path,
1060 caches: Option<&crate::repo_caches::RepoCaches>,
1061 ) -> Result<Option<Self>, Error> {
1062 let info = objects_dir.join("info");
1063 let chain_path = info.join("commit-graphs").join("commit-graph-chain");
1064 if chain_path.is_file() {
1065 let algo =
1066 hash_algo_for_objects_dir(&crate::environment::Environment::empty(), objects_dir);
1067 let content = std::fs::read_to_string(&chain_path).map_err(Error::from)?;
1068 let mut layers = Vec::new();
1069 for line in content.lines() {
1070 let h = line.trim();
1071 if !commit_graph_chain_hash_is_valid(h, algo) {
1072 continue;
1073 }
1074 let graph_path = info.join("commit-graphs").join(format!("graph-{h}.graph"));
1075 let layer = CommitGraphLayer::try_open(graph_path)?;
1076 let n = layers.len();
1083 if n > 0 && layer.base_chunk_size / layer.hash_len < n {
1084 warn_once_for_base_chunk_too_small(caches, &layer.layer_display_id());
1085 break;
1086 }
1087 layers.push(layer);
1088 }
1089 if layers.is_empty() {
1090 return Ok(None);
1091 }
1092 layers.reverse();
1096 let mut chain = Self { layers };
1097 chain.validate_bloom_compatibility(caches);
1098 return Ok(Some(chain));
1099 }
1100 let single = info.join("commit-graph");
1101 if single.is_file() {
1102 let layer = CommitGraphLayer::try_open(single.clone())?;
1103 let mut chain = Self {
1104 layers: vec![layer],
1105 };
1106 chain.validate_bloom_compatibility(caches);
1107 return Ok(Some(chain));
1108 }
1109 Ok(None)
1110 }
1111
1112 pub fn load(objects_dir: &Path) -> Option<Self> {
1114 Self::try_load(objects_dir).ok().flatten()
1115 }
1116
1117 pub fn verify_on_disk(objects_dir: &Path) -> Result<(), Error> {
1125 let Some(chain) = Self::try_load(objects_dir)? else {
1126 return Ok(());
1127 };
1128 for path in chain.layer_paths_oldest_first() {
1129 let raw = std::fs::read(&path).map_err(Error::from)?;
1130 CommitGraphLayer::try_parse(path, raw)?;
1131 }
1132 Ok(())
1133 }
1134
1135 pub fn try_load_across(
1159 objects_dir: &Path,
1160 alt_dirs: &[PathBuf],
1161 ) -> Result<Option<Self>, Error> {
1162 Self::try_load_across_with_caches(objects_dir, alt_dirs, None)
1163 }
1164
1165 pub fn try_load_across_with_caches(
1171 objects_dir: &Path,
1172 alt_dirs: &[PathBuf],
1173 caches: Option<&crate::repo_caches::RepoCaches>,
1174 ) -> Result<Option<Self>, Error> {
1175 let layer_dir = |dir: &Path| dir.join("info").join("commit-graphs");
1176 let resolve_layer = |hash: &str| -> Option<PathBuf> {
1177 let name = format!("graph-{hash}.graph");
1178 let local = layer_dir(objects_dir).join(&name);
1179 if local.is_file() {
1180 return Some(local);
1181 }
1182 alt_dirs
1183 .iter()
1184 .map(|d| layer_dir(d).join(&name))
1185 .find(|p| p.is_file())
1186 };
1187
1188 let local_chain = layer_dir(objects_dir).join("commit-graph-chain");
1196 let local_single = objects_dir.join("info").join("commit-graph");
1197 let chain_path = if local_chain.is_file() {
1198 local_chain
1199 } else if local_single.is_file() {
1200 return Self::try_load_with_caches(objects_dir, caches);
1201 } else {
1202 match alt_dirs
1203 .iter()
1204 .map(PathBuf::as_path)
1205 .find(|d| layer_dir(d).join("commit-graph-chain").is_file())
1206 {
1207 Some(owner) => layer_dir(owner).join("commit-graph-chain"),
1208 None => return Ok(None),
1209 }
1210 };
1211
1212 let algo =
1213 hash_algo_for_objects_dir(&crate::environment::Environment::empty(), objects_dir);
1214 let content = std::fs::read_to_string(&chain_path).map_err(Error::from)?;
1215 let mut layers = Vec::new();
1216 for line in content.lines() {
1217 let h = line.trim();
1218 if !commit_graph_chain_hash_is_valid(h, algo) {
1219 continue;
1220 }
1221 let graph_path = resolve_layer(h).ok_or_else(|| {
1222 Error::from(std::io::Error::new(
1223 std::io::ErrorKind::NotFound,
1224 format!("commit-graph layer graph-{h}.graph not found"),
1225 ))
1226 })?;
1227 let layer = CommitGraphLayer::try_open(graph_path)?;
1228 let n = layers.len();
1229 if n > 0 && layer.base_chunk_size / layer.hash_len < n {
1230 warn_once_for_base_chunk_too_small(caches, &layer.layer_display_id());
1231 break;
1232 }
1233 layers.push(layer);
1234 }
1235 if layers.is_empty() {
1236 return Ok(None);
1237 }
1238 layers.reverse();
1239 let mut chain = Self { layers };
1240 chain.validate_bloom_compatibility(caches);
1241 Ok(Some(chain))
1242 }
1243
1244 fn validate_bloom_compatibility(&mut self, caches: Option<&crate::repo_caches::RepoCaches>) {
1245 let mut ref_settings: Option<BloomFilterSettings> = None;
1251 for layer in &mut self.layers {
1252 let Some(bs) = layer.bloom_settings else {
1253 continue;
1254 };
1255 match ref_settings {
1256 None => ref_settings = Some(bs),
1257 Some(r) => {
1258 if r.hash_version != bs.hash_version
1259 || r.num_hashes != bs.num_hashes
1260 || r.bits_per_entry != bs.bits_per_entry
1261 {
1262 let id = layer.layer_display_id();
1263 warn_once_for_disabled_bloom_layer(caches, &id);
1269 layer.disable_bloom();
1270 }
1271 }
1272 }
1273 }
1274 }
1275
1276 pub fn existing_filter_bytes(
1282 &self,
1283 oid: &ObjectId,
1284 want: &BloomFilterSettings,
1285 ) -> Option<Vec<u8>> {
1286 let (layer_idx, lex) = self.find_commit(oid)?;
1287 let layer = &self.layers[layer_idx];
1288 let settings = layer.bloom_settings.as_ref()?;
1289 if settings.hash_version != want.hash_version
1290 || settings.num_hashes != want.num_hashes
1291 || settings.bits_per_entry != want.bits_per_entry
1292 {
1293 return None;
1294 }
1295 match layer.bloom_filter_slice(lex) {
1302 Some(s) if !s.is_empty() => Some(s.to_vec()),
1303 _ => None,
1304 }
1305 }
1306
1307 pub fn upgradable_filter_bytes(
1313 &self,
1314 oid: &ObjectId,
1315 want: &BloomFilterSettings,
1316 ) -> Option<Vec<u8>> {
1317 let (layer_idx, lex) = self.find_commit(oid)?;
1318 let layer = &self.layers[layer_idx];
1319 let settings = layer.bloom_settings.as_ref()?;
1320 if settings.hash_version == want.hash_version {
1321 return None;
1322 }
1323 if settings.num_hashes != want.num_hashes || settings.bits_per_entry != want.bits_per_entry
1324 {
1325 return None;
1326 }
1327 match layer.bloom_filter_slice(lex) {
1328 Some(s) if !s.is_empty() => Some(s.to_vec()),
1329 _ => None,
1330 }
1331 }
1332
1333 pub(crate) fn bloom_settings_for_commit(&self, oid: &ObjectId) -> Option<BloomFilterSettings> {
1335 let (layer_idx, _) = self.find_commit(oid)?;
1336 self.layers[layer_idx].bloom_settings
1337 }
1338
1339 pub fn find_commit(&self, oid: &ObjectId) -> Option<(usize, u32)> {
1341 for (i, layer) in self.layers.iter().enumerate() {
1342 if let Some(lex) = layer.bsearch_oid(oid) {
1343 return Some((i, lex));
1344 }
1345 }
1346 None
1347 }
1348
1349 pub fn global_position(&self, oid: &ObjectId) -> Option<u32> {
1351 let (layer_idx, lex) = self.find_commit(oid)?;
1352 let below: u32 = self.layers[layer_idx + 1..]
1353 .iter()
1354 .map(|l| l.num_commits)
1355 .sum();
1356 Some(below + lex)
1357 }
1358
1359 fn oid_at_global_position(&self, pos: u32) -> Option<ObjectId> {
1362 let mut remaining = pos;
1364 for layer in self.layers.iter().rev() {
1365 if remaining < layer.num_commits {
1366 return layer.oid_at_lex(remaining);
1367 }
1368 remaining -= layer.num_commits;
1369 }
1370 None
1371 }
1372
1373 pub fn graph_commit(&self, oid: &ObjectId) -> Option<(Vec<ObjectId>, i64)> {
1376 let (layer_idx, lex) = self.find_commit(oid)?;
1377 self.graph_commit_at(layer_idx, lex)
1378 }
1379
1380 fn graph_commit_at(&self, layer_idx: usize, lex: u32) -> Option<(Vec<ObjectId>, i64)> {
1381 let layer = &self.layers[layer_idx];
1382 let (_, _, commit_time) = layer.cdat_record(lex)?;
1383 let oids = layer
1384 .parent_global_positions(lex)?
1385 .into_iter()
1386 .filter_map(|pos| self.oid_at_global_position(pos))
1387 .collect();
1388 Some((oids, commit_time))
1389 }
1390
1391 pub(crate) fn graph_commit_tree(&self, oid: &ObjectId) -> Option<ObjectId> {
1393 let (layer_idx, lex) = self.find_commit(oid)?;
1394 self.layers[layer_idx].cdat_root_tree(lex)
1395 }
1396
1397 pub(crate) fn graph_commit_meta(&self, oid: &ObjectId) -> Option<(ObjectId, Vec<ObjectId>)> {
1399 let (layer_idx, lex) = self.find_commit(oid)?;
1400 let tree = self.layers[layer_idx].cdat_root_tree(lex)?;
1401 let (parents, _) = self.graph_commit_at(layer_idx, lex)?;
1402 Some((tree, parents))
1403 }
1404
1405 #[must_use]
1407 pub fn commit_generation_for_walk(&self, oid: &ObjectId) -> u64 {
1408 const UNRANKED: u64 = (1u64 << 63) - 1;
1409 let Some((layer_idx, lex)) = self.find_commit(oid) else {
1410 return UNRANKED;
1411 };
1412 self.layers[layer_idx]
1413 .corrected_commit_generation(lex)
1414 .unwrap_or(UNRANKED)
1415 }
1416
1417 #[must_use]
1420 pub fn parent_global_positions(&self, oid: &ObjectId) -> Option<Vec<u32>> {
1421 let (layer_idx, lex) = self.find_commit(oid)?;
1422 self.layers[layer_idx].parent_global_positions(lex)
1423 }
1424
1425 #[must_use]
1429 pub fn count_reachable_commits(
1430 &self,
1431 tips: &[ObjectId],
1432 hide: &[ObjectId],
1433 first_parent_only: bool,
1434 shallow: &std::collections::HashSet<ObjectId>,
1435 ) -> Option<usize> {
1436 if self.total_commits() == 0 {
1437 return None;
1438 }
1439 let mut seen = self.reachable_positions(tips, first_parent_only, shallow)?;
1440 if hide.is_empty() {
1441 return Some(seen.len());
1442 }
1443 let hidden = self.reachable_positions(hide, first_parent_only, shallow)?;
1444 seen.retain(|pos| !hidden.contains(pos));
1445 Some(seen.len())
1446 }
1447
1448 fn reachable_positions(
1449 &self,
1450 tips: &[ObjectId],
1451 first_parent_only: bool,
1452 shallow: &std::collections::HashSet<ObjectId>,
1453 ) -> Option<std::collections::HashSet<u32>> {
1454 let total = self.total_commits();
1455 let mut stack: Vec<u32> = Vec::new();
1456 for tip in tips {
1457 stack.push(self.global_position(tip)?);
1458 }
1459 let mut seen = std::collections::HashSet::new();
1460 while let Some(pos) = stack.pop() {
1461 if pos >= total || !seen.insert(pos) {
1462 continue;
1463 }
1464 let oid = self.oid_at_global_position(pos)?;
1465 let parents = if shallow.contains(&oid) {
1466 Vec::new()
1467 } else {
1468 self.parent_global_positions(&oid)?
1469 };
1470 let parents = if first_parent_only {
1471 parents.into_iter().take(1).collect::<Vec<_>>()
1472 } else {
1473 parents
1474 };
1475 for p in parents {
1476 if p >= total {
1477 return None;
1478 }
1479 stack.push(p);
1480 }
1481 }
1482 Some(seen)
1483 }
1484
1485 pub fn all_oids_in_order(&self) -> Vec<ObjectId> {
1487 let mut out = Vec::new();
1488 for layer in self.layers.iter().rev() {
1489 for i in 0..layer.num_commits {
1490 if let Some(oid) = layer.oid_at_lex(i) {
1491 out.push(oid);
1492 }
1493 }
1494 }
1495 out
1496 }
1497
1498 pub fn bloom_precheck_for_paths(
1500 &self,
1501 _odb: &Odb,
1502 oid: ObjectId,
1503 pathspecs: &[String],
1504 bloom_cwd: Option<&str>,
1505 requested_hash_version: i32,
1506 read_changed_paths: bool,
1507 ) -> std::result::Result<BloomPrecheck, crate::error::Error> {
1508 if !read_changed_paths {
1509 return Ok(BloomPrecheck::Inapplicable);
1510 }
1511 let Some((layer_idx, lex)) = self.find_commit(&oid) else {
1512 return Ok(BloomPrecheck::NotInGraph);
1513 };
1514 let layer = &self.layers[layer_idx];
1515 let Some(settings) = layer.bloom_settings.as_ref() else {
1516 return Ok(BloomPrecheck::FilterNotPresent);
1517 };
1518 let effective_version = if requested_hash_version < 0 {
1519 settings.hash_version as i32
1520 } else {
1521 requested_hash_version
1522 };
1523 if effective_version != settings.hash_version as i32 {
1524 return Ok(BloomPrecheck::FilterNotPresent);
1525 }
1526
1527 let filter = match layer.bloom_filter_slice(lex) {
1533 Some(s) => s,
1534 None => return Ok(BloomPrecheck::FilterNotPresent),
1535 };
1536 if filter.is_empty() {
1537 return Ok(BloomPrecheck::FilterNotPresent);
1538 }
1539
1540 let mut any_pathspec_maybe = false;
1543 let mut checked_any_keys = false;
1544 for spec in pathspecs {
1545 if spec.is_empty() || crate::pathspec::pathspec_is_exclude(spec) {
1546 continue;
1547 }
1548 let Some(norm) = crate::pathspec::bloom_lookup_prefix_with_cwd(spec, bloom_cwd) else {
1549 continue;
1550 };
1551 let keys = bloom_keyvec_for_path(norm.as_str(), settings);
1552 if keys.is_empty() {
1553 continue;
1554 }
1555 checked_any_keys = true;
1556 let mut all_keys_maybe = true;
1557 for key in &keys {
1558 match bloom_filter_contains(key, filter, settings) {
1559 Ok(true) => {}
1560 Ok(false) => {
1561 all_keys_maybe = false;
1562 break;
1563 }
1564 Err(crate::bloom::BloomFilterInvalid) => {
1565 all_keys_maybe = true;
1566 break;
1567 }
1568 }
1569 }
1570 if all_keys_maybe {
1571 any_pathspec_maybe = true;
1572 break;
1573 }
1574 }
1575 if checked_any_keys && !any_pathspec_maybe {
1576 return Ok(BloomPrecheck::DefinitelyNot);
1577 }
1578 Ok(BloomPrecheck::Maybe)
1579 }
1580
1581 pub(crate) fn bloom_definitely_not_for_keys(
1583 &self,
1584 oid: ObjectId,
1585 path_keys: &[Vec<u32>],
1586 ) -> bool {
1587 if path_keys.is_empty() {
1588 return false;
1589 }
1590 let Some((layer_idx, lex)) = self.find_commit(&oid) else {
1591 return false;
1592 };
1593 let layer = &self.layers[layer_idx];
1594 let Some(settings) = layer.bloom_settings.as_ref() else {
1595 return false;
1596 };
1597 let Some(filter) = layer.bloom_filter_slice(lex) else {
1598 return false;
1599 };
1600 if filter.is_empty() {
1601 return false;
1602 }
1603 bloom_definitely_not_from_filter(filter, settings, path_keys)
1604 }
1605}
1606
1607pub(crate) fn bloom_definitely_not_from_filter(
1609 filter: &[u8],
1610 settings: &crate::bloom::BloomFilterSettings,
1611 path_keys: &[Vec<u32>],
1612) -> bool {
1613 use crate::bloom::bloom_filter_contains;
1614
1615 for key in path_keys {
1616 match bloom_filter_contains(key, filter, settings) {
1617 Ok(false) => return true,
1618 Ok(true) => {}
1619 Err(crate::bloom::BloomFilterInvalid) => return false,
1620 }
1621 }
1622 false
1623}
1624
1625pub fn diff_changed_paths_for_bloom(
1627 odb: &Odb,
1628 parent_tree: Option<ObjectId>,
1629 commit_tree: ObjectId,
1630) -> crate::error::Result<(Vec<String>, usize)> {
1631 use crate::diff::diff_trees;
1632 let entries = diff_trees(odb, parent_tree.as_ref(), Some(&commit_tree), "")?;
1633 let raw_len = entries.len();
1634 let mut paths = Vec::new();
1635 for e in entries {
1636 let p = e.path().to_string();
1637 if !p.is_empty() {
1638 paths.push(p);
1639 }
1640 }
1641 Ok((paths, raw_len))
1642}
1643
1644pub use crate::bloom::collect_changed_paths_for_bloom;
1646
1647pub fn bloom_filter_for_commit_write(
1649 odb: &Odb,
1650 parents: &[ObjectId],
1651 tree_oid: ObjectId,
1652 settings: &BloomFilterSettings,
1653) -> crate::error::Result<(Vec<u8>, BloomBuildOutcome)> {
1654 let (changed_paths_vec, raw_count) = if let Some(first_parent) = parents.first() {
1658 let p = odb.commit_tree(first_parent)?;
1659 diff_changed_paths_for_bloom(odb, Some(p), tree_oid)?
1660 } else {
1661 diff_changed_paths_for_bloom(odb, None, tree_oid)?
1662 };
1663 let set = collect_changed_paths_for_bloom(&changed_paths_vec);
1664 Ok(crate::bloom::build_bloom_filter_data(
1665 &set, raw_count, settings,
1666 ))
1667}
1668
1669fn tree_has_high_bit_paths(odb: &Odb, tree_oid: ObjectId) -> bool {
1674 let Ok(obj) = odb.read(&tree_oid) else {
1675 return true;
1677 };
1678 let Ok(entries) = crate::objects::parse_tree(&obj.data) else {
1679 return true;
1680 };
1681 for e in &entries {
1682 if e.name.iter().any(|&b| b & 0x80 != 0) {
1683 return true;
1684 }
1685 if e.mode == 0o040000 && tree_has_high_bit_paths(odb, e.oid) {
1686 return true;
1687 }
1688 }
1689 false
1690}
1691
1692pub fn commit_tree_has_high_bit_paths(odb: &Odb, commit_oid: ObjectId) -> bool {
1695 match odb.commit_tree(&commit_oid) {
1696 Ok(tree) => tree_has_high_bit_paths(odb, tree),
1697 Err(_) => true,
1698 }
1699}
1700
1701pub fn parse_graph_file(path: &Path) -> Option<ParsedGraphDump> {
1703 let raw = std::fs::read(path).ok()?;
1704 if raw.len() < 28 {
1705 return None;
1706 }
1707 let hash_len = commit_graph_hash_len(raw[5])?;
1708 let body = &raw[..raw.len() - hash_len];
1709 if body.len() < 8 || &body[0..4] != SIGNATURE {
1710 return None;
1711 }
1712 let header_word = u32::from_be_bytes(body[0..4].try_into().ok()?);
1713 let num_chunks = body[6] as usize;
1714 let toc_start = 8;
1715 let mut present: std::collections::HashSet<u32> = std::collections::HashSet::new();
1716 for i in 0..num_chunks {
1717 let e = toc_start + i * 12;
1718 let id = u32::from_be_bytes(body[e..e + 4].try_into().ok()?);
1719 present.insert(id);
1720 }
1721 let mut chunk_names: Vec<String> = Vec::new();
1724 for (id, label) in [
1725 (CHUNK_OID_FANOUT, "oid_fanout"),
1726 (CHUNK_OID_LOOKUP, "oid_lookup"),
1727 (CHUNK_COMMIT_DATA, "commit_metadata"),
1728 (CHUNK_GENERATION_DATA, "generation_data"),
1729 (CHUNK_GENERATION_DATA_OVERFLOW, "generation_data_overflow"),
1730 (CHUNK_EXTRA_EDGES, "extra_edges"),
1731 (CHUNK_BLOOM_INDEXES, "bloom_indexes"),
1732 (CHUNK_BLOOM_DATA, "bloom_data"),
1733 ] {
1734 if present.contains(&id) {
1735 chunk_names.push(label.to_string());
1736 }
1737 }
1738 let layer = CommitGraphLayer::parse(path.to_path_buf(), raw.clone())?;
1739 let bloom_opt = layer.bloom_settings.map(|s| {
1740 format!(
1741 " bloom({},{},{})",
1742 s.hash_version, s.bits_per_entry, s.num_hashes
1743 )
1744 });
1745 let mut options = String::new();
1746 if let Some(b) = bloom_opt {
1747 options.push_str(&b);
1748 }
1749 if layer.read_generation_data {
1750 options.push_str(" read_generation_data");
1751 }
1752 Some(ParsedGraphDump {
1753 header_word,
1754 version: body[4],
1755 hash_ver: body[5],
1756 num_chunks: body[6],
1757 reserved: body[7],
1758 num_commits: layer.num_commits,
1759 chunks: chunk_names.join(" "),
1760 options,
1761 })
1762}
1763
1764pub struct ParsedGraphDump {
1766 pub header_word: u32,
1768 pub version: u8,
1770 pub hash_ver: u8,
1772 pub num_chunks: u8,
1774 pub reserved: u8,
1776 pub num_commits: u32,
1778 pub chunks: String,
1780 pub options: String,
1782}
1783
1784pub fn dump_bloom_filters(path: &Path) -> Option<Vec<String>> {
1786 let raw = std::fs::read(path).ok()?;
1787 let layer = CommitGraphLayer::parse(path.to_path_buf(), raw)?;
1788 let mut out = Vec::new();
1789 for i in 0..layer.num_commits {
1790 let slice = layer.bloom_filter_slice(i).unwrap_or(&[]);
1791 if slice.is_empty() {
1792 out.push(String::new());
1793 } else {
1794 let hex: String = slice.iter().map(|b| format!("{b:02x}")).collect();
1795 out.push(hex);
1796 }
1797 }
1798 Some(out)
1799}
1800
1801#[cfg(test)]
1802mod tests {
1803 use std::collections::HashMap;
1804
1805 use crate::bloom::BloomFilterSettings;
1806 use crate::commit_graph_write::{build_commit_graph_bytes, load_commit_graph_commit_info};
1807 use crate::index::{Index, IndexEntry, MODE_REGULAR};
1808 use crate::objects::{serialize_commit, CommitData, ObjectId, ObjectKind};
1809 use crate::repo::{init_repository, Repository};
1810 use crate::write_tree::write_tree_from_index;
1811
1812 use crate::commit_graph_write::CommitGraphCommitInfo;
1813 use crate::objects::OidHashMap;
1814
1815 fn infos_std(
1816 infos: &OidHashMap<CommitGraphCommitInfo>,
1817 ) -> HashMap<ObjectId, CommitGraphCommitInfo> {
1818 infos.iter().map(|(k, v)| (*k, v.clone())).collect()
1819 }
1820
1821 fn empty_byte_filters() -> HashMap<ObjectId, Vec<u8>> {
1822 HashMap::new()
1823 }
1824
1825 fn one_commit_repo() -> (tempfile::TempDir, Repository, ObjectId) {
1826 let dir = tempfile::tempdir().expect("tempdir");
1827 let repo = init_repository(
1828 dir.path(),
1829 false,
1830 "main",
1831 None,
1832 crate::RefStorageFormat::Files,
1833 )
1834 .expect("init");
1835 let mut index = Index::new();
1836 index.hash_algo = repo.odb().hash_algo();
1837 let blob = repo.odb().write(ObjectKind::Blob, b"x\n").expect("blob");
1838 index.add_or_replace(IndexEntry {
1839 ctime_sec: 0,
1840 ctime_nsec: 0,
1841 mtime_sec: 0,
1842 mtime_nsec: 0,
1843 dev: 0,
1844 ino: 0,
1845 mode: MODE_REGULAR,
1846 uid: 0,
1847 gid: 0,
1848 size: 2,
1849 oid: blob,
1850 flags: 4,
1851 flags_extended: None,
1852 path: b"f.txt".to_vec(),
1853 base_index_pos: 0,
1854 });
1855 let tree = write_tree_from_index(&repo.odb(), &index, "").expect("tree");
1856 let raw = serialize_commit(&CommitData {
1857 tree,
1858 parents: vec![],
1859 author: "T <t@example.com> 100 +0000".into(),
1860 committer: "T <t@example.com> 100 +0000".into(),
1861 author_raw: Vec::new(),
1862 committer_raw: Vec::new(),
1863 encoding: None,
1864 message: "m\n".into(),
1865 raw_message: None,
1866 extra_headers: Vec::new(),
1867 });
1868 let commit = repo.odb().write(ObjectKind::Commit, &raw).expect("commit");
1869 crate::refs::write_ref(&repo.git_dir(), "refs/heads/main", &commit).expect("ref");
1870 (dir, repo, commit)
1871 }
1872
1873 fn reseal_graph(bytes: &mut Vec<u8>, algo: crate::objects::HashAlgo) {
1874 let hash_len = algo.len();
1875 let body_len = bytes.len().saturating_sub(hash_len);
1876 let digest = algo.checksum_digest(&bytes[..body_len]);
1877 bytes.truncate(body_len);
1878 bytes.extend_from_slice(&digest);
1879 }
1880
1881 #[test]
1882 fn try_parse_rejects_bad_signature_and_truncated_toc() {
1883 let (_dir, repo, commit) = one_commit_repo();
1884 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
1885 let mut infos = crate::objects::oid_hash_map();
1886 infos.insert(commit, info);
1887 let bloom = BloomFilterSettings::default();
1888 let (mut bytes, _) = build_commit_graph_bytes(
1889 &[commit],
1890 &infos_std(&infos),
1891 &repo.odb(),
1892 false,
1893 &bloom,
1894 None,
1895 &[],
1896 None,
1897 &empty_byte_filters(),
1898 &empty_byte_filters(),
1899 true,
1900 )
1901 .expect("write");
1902 let algo = repo.odb().hash_algo();
1903 bytes[0] = 0;
1904 reseal_graph(&mut bytes, algo);
1905 let err = super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), bytes.clone())
1906 .unwrap_err();
1907 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
1908 bytes.truncate(40);
1909 let err =
1910 super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), bytes).unwrap_err();
1911 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
1912 let (mut good, _) = build_commit_graph_bytes(
1913 &[commit],
1914 &infos_std(&infos),
1915 &repo.odb(),
1916 false,
1917 &bloom,
1918 None,
1919 &[],
1920 None,
1921 &empty_byte_filters(),
1922 &empty_byte_filters(),
1923 true,
1924 )
1925 .expect("write");
1926 good[4] = 2;
1927 reseal_graph(&mut good, algo);
1928 let err =
1929 super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), good).unwrap_err();
1930 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
1931 let (mut checksum_bad, _) = build_commit_graph_bytes(
1932 &[commit],
1933 &infos_std(&infos),
1934 &repo.odb(),
1935 false,
1936 &bloom,
1937 None,
1938 &[],
1939 None,
1940 &empty_byte_filters(),
1941 &empty_byte_filters(),
1942 true,
1943 )
1944 .expect("write");
1945 let last = checksum_bad.len() - 1;
1946 checksum_bad[last] ^= 0xff;
1947 let err = super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), checksum_bad)
1948 .unwrap_err();
1949 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
1950 }
1951
1952 #[test]
1953 fn parse_graph_file_returns_none_for_invalid_inputs() {
1954 let dir = tempfile::tempdir().expect("tempdir");
1955 let missing = dir.path().join("nope.graph");
1956 assert!(super::parse_graph_file(&missing).is_none());
1957 std::fs::write(dir.path().join("tiny.graph"), b"CGPH").expect("write");
1958 assert!(super::parse_graph_file(&dir.path().join("tiny.graph")).is_none());
1959 assert!(super::dump_bloom_filters(&missing).is_none());
1960 }
1961
1962 #[test]
1963 fn try_load_missing_graph_is_none() {
1964 let dir = tempfile::tempdir().expect("tempdir");
1965 let objects = dir.path().join("objects");
1966 std::fs::create_dir_all(objects.join("info")).expect("info");
1967 assert!(super::CommitGraphChain::try_load(&objects)
1968 .expect("ok")
1969 .is_none());
1970 }
1971
1972 #[test]
1973 fn try_load_across_finds_single_graph() {
1974 let (_dir, repo, _commit) = one_commit_repo();
1975 let objects = repo.odb().objects_dir();
1976 let sorted = vec![_commit];
1977 let info = load_commit_graph_commit_info(&repo.odb(), _commit).expect("info");
1978 let mut infos = crate::objects::oid_hash_map();
1979 infos.insert(_commit, info);
1980 let bloom = BloomFilterSettings::default();
1981 let (bytes, _) = build_commit_graph_bytes(
1982 &sorted,
1983 &infos_std(&infos),
1984 &repo.odb(),
1985 false,
1986 &bloom,
1987 None,
1988 &[],
1989 None,
1990 &empty_byte_filters(),
1991 &empty_byte_filters(),
1992 true,
1993 )
1994 .expect("write");
1995 std::fs::write(objects.join("info/commit-graph"), bytes).expect("write");
1996 let chain = super::CommitGraphChain::try_load_across(objects, &[])
1997 .expect("across")
1998 .expect("some");
1999 assert_eq!(chain.total_commits(), 1);
2000 }
2001
2002 #[test]
2003 fn try_parse_rejects_oidf_toc_offset_out_of_range() {
2004 let (_dir, repo, commit) = one_commit_repo();
2005 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
2006 let mut infos = crate::objects::oid_hash_map();
2007 infos.insert(commit, info);
2008 let bloom = BloomFilterSettings::default();
2009 let (mut bytes, _) = build_commit_graph_bytes(
2010 &[commit],
2011 &infos_std(&infos),
2012 &repo.odb(),
2013 false,
2014 &bloom,
2015 None,
2016 &[],
2017 None,
2018 &empty_byte_filters(),
2019 &empty_byte_filters(),
2020 true,
2021 )
2022 .expect("write");
2023 let algo = repo.odb().hash_algo();
2024 let num_chunks = bytes[6] as usize;
2025 for i in 0..num_chunks {
2026 let e = 8 + i * 12;
2027 let id = u32::from_be_bytes(bytes[e..e + 4].try_into().unwrap());
2028 if id == 0x4f49_4446 {
2029 bytes[e + 4..e + 12].copy_from_slice(&u64::MAX.to_be_bytes());
2030 break;
2031 }
2032 }
2033 reseal_graph(&mut bytes, algo);
2034 let err =
2035 super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), bytes).unwrap_err();
2036 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
2037 }
2038
2039 #[test]
2040 fn generation_overflow_without_gdo2_fails_parse() {
2041 let (_dir, repo, commit) = one_commit_repo();
2042 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
2043 let mut infos = crate::objects::oid_hash_map();
2044 infos.insert(commit, info);
2045 let bloom = BloomFilterSettings::default();
2046 let (mut bytes, _) = build_commit_graph_bytes(
2047 &[commit],
2048 &infos_std(&infos),
2049 &repo.odb(),
2050 false,
2051 &bloom,
2052 None,
2053 &[],
2054 None,
2055 &empty_byte_filters(),
2056 &empty_byte_filters(),
2057 true,
2058 )
2059 .expect("write");
2060 if bytes.len() > 100 {
2061 for i in 80..120 {
2062 if i + 4 <= bytes.len() - 20 {
2063 bytes[i] = 0x80;
2064 bytes[i + 1] = 0x00;
2065 bytes[i + 2] = 0x00;
2066 bytes[i + 3] = 0x00;
2067 break;
2068 }
2069 }
2070 }
2071 let err =
2072 super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), bytes).unwrap_err();
2073 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
2074 }
2075
2076 #[test]
2077 fn bloom_precheck_not_in_graph_for_missing_oid() {
2078 let (_dir, repo, commit) = one_commit_repo();
2079 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
2080 let mut infos = crate::objects::oid_hash_map();
2081 infos.insert(commit, info);
2082 let bloom = BloomFilterSettings::default();
2083 let (bytes, _) = build_commit_graph_bytes(
2084 &[commit],
2085 &infos_std(&infos),
2086 &repo.odb(),
2087 true,
2088 &bloom,
2089 None,
2090 &[],
2091 None,
2092 &empty_byte_filters(),
2093 &empty_byte_filters(),
2094 true,
2095 )
2096 .expect("write");
2097 std::fs::write(repo.odb().objects_dir().join("info/commit-graph"), bytes).expect("write");
2098 let chain = super::CommitGraphChain::load(repo.odb().objects_dir()).expect("chain");
2099 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").expect("oid");
2100 let pre = chain
2101 .bloom_precheck_for_paths(&repo.odb(), missing, &[], None, -1, true)
2102 .expect("precheck");
2103 assert_eq!(pre, super::BloomPrecheck::NotInGraph);
2104 }
2105
2106 #[test]
2107 fn bloom_precheck_respects_read_changed_paths_flag() {
2108 let (_dir, repo, commit) = one_commit_repo();
2109 let mut sorted = vec![commit];
2110 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
2111 let mut infos = crate::objects::oid_hash_map();
2112 infos.insert(commit, info);
2113 let bloom = BloomFilterSettings::default();
2114 let (bytes, _) = build_commit_graph_bytes(
2115 &sorted,
2116 &infos_std(&infos),
2117 &repo.odb(),
2118 true,
2119 &bloom,
2120 None,
2121 &[],
2122 None,
2123 &empty_byte_filters(),
2124 &empty_byte_filters(),
2125 true,
2126 )
2127 .expect("write");
2128 std::fs::write(repo.odb().objects_dir().join("info/commit-graph"), bytes).expect("write");
2129 let chain = super::CommitGraphChain::load(repo.odb().objects_dir()).expect("chain");
2130 let pre = chain
2131 .bloom_precheck_for_paths(&repo.odb(), commit, &["f.txt".to_owned()], None, -1, false)
2132 .expect("precheck");
2133 assert_eq!(pre, super::BloomPrecheck::Inapplicable);
2134 sorted.clear();
2135 }
2136
2137 #[test]
2138 fn bloom_skips_most_steps_when_tracked_path_unchanged_in_linear_history() {
2139 use crate::bloom::bloom_keyvec_for_path;
2140 use crate::commit_graph_write::{build_commit_graph_bytes, load_commit_graph_commit_info};
2141 use crate::index::{Index, IndexEntry, MODE_REGULAR};
2142 use crate::objects::{serialize_commit, CommitData, ObjectKind};
2143 use crate::pathspec::bloom_lookup_prefix_with_cwd;
2144 use crate::repo::init_repository;
2145 use crate::write_tree::write_tree_from_index;
2146 use crate::RefStorageFormat;
2147 use std::fs;
2148
2149 let dir = tempfile::tempdir().expect("tempdir");
2150 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)
2151 .expect("init");
2152 let path = "tracked.txt";
2153 let stable = b"stable payload\n";
2154 let blob = repo.odb().write(ObjectKind::Blob, stable).expect("blob");
2155 let mut parent: Option<ObjectId> = None;
2156 let mut oids = Vec::new();
2157 for i in 0..24usize {
2158 let mut index = Index::new();
2159 index.hash_algo = repo.odb().hash_algo();
2160 index.add_or_replace(IndexEntry {
2161 ctime_sec: 0,
2162 ctime_nsec: 0,
2163 mtime_sec: 0,
2164 mtime_nsec: 0,
2165 dev: 0,
2166 ino: 0,
2167 mode: MODE_REGULAR,
2168 uid: 0,
2169 gid: 0,
2170 size: stable.len() as u32,
2171 oid: blob,
2172 flags: 4,
2173 flags_extended: None,
2174 path: path.as_bytes().to_vec(),
2175 base_index_pos: 0,
2176 });
2177 if i % 5 == 3 {
2178 let noise = repo
2179 .odb()
2180 .write(ObjectKind::Blob, format!("noise {i}\n").as_bytes())
2181 .expect("noise blob");
2182 index.add_or_replace(IndexEntry {
2183 ctime_sec: 0,
2184 ctime_nsec: 0,
2185 mtime_sec: 0,
2186 mtime_nsec: 0,
2187 dev: 0,
2188 ino: 0,
2189 mode: MODE_REGULAR,
2190 uid: 0,
2191 gid: 0,
2192 size: 8,
2193 oid: noise,
2194 flags: 4,
2195 flags_extended: None,
2196 path: format!("other{i}.txt").into_bytes(),
2197 base_index_pos: 0,
2198 });
2199 }
2200 let tree = write_tree_from_index(&repo.odb(), &index, "").expect("tree");
2201 let mut parents = Vec::new();
2202 if let Some(p) = parent {
2203 parents.push(p);
2204 }
2205 let raw = serialize_commit(&CommitData {
2206 tree,
2207 parents,
2208 author: "T <t@example.com> 100 +0000".into(),
2209 committer: "T <t@example.com> 100 +0000".into(),
2210 author_raw: Vec::new(),
2211 committer_raw: Vec::new(),
2212 encoding: None,
2213 message: format!("c{i}\n"),
2214 raw_message: None,
2215 extra_headers: Vec::new(),
2216 });
2217 let commit = repo.odb().write(ObjectKind::Commit, &raw).expect("commit");
2218 oids.push(commit);
2219 parent = Some(commit);
2220 }
2221 let head = *oids.last().expect("head");
2222 let mut infos = crate::objects::oid_hash_map();
2223 for oid in &oids {
2224 infos.insert(
2225 *oid,
2226 load_commit_graph_commit_info(&repo.odb(), *oid).expect("info"),
2227 );
2228 }
2229 let bloom = BloomFilterSettings::default();
2230 let (bytes, _) = build_commit_graph_bytes(
2231 &oids,
2232 &infos_std(&infos),
2233 &repo.odb(),
2234 true,
2235 &bloom,
2236 None,
2237 &[],
2238 None,
2239 &empty_byte_filters(),
2240 &empty_byte_filters(),
2241 true,
2242 )
2243 .expect("graph bytes");
2244 fs::write(repo.odb().objects_dir().join("info/commit-graph"), bytes).expect("write graph");
2245 let chain = super::CommitGraphChain::load(repo.odb().objects_dir()).expect("chain");
2246 let settings = chain.top_layer_bloom_settings().expect("bloom");
2247 let norm = bloom_lookup_prefix_with_cwd(path, None).expect("prefix");
2248 let keys = bloom_keyvec_for_path(norm.as_str(), &settings);
2249 let mut current = head;
2250 let mut bloom_yes = 0usize;
2251 let mut bloom_no = 0usize;
2252 while bloom_yes + bloom_no < 10_000 {
2253 let Some((parents, _)) = chain.graph_commit(¤t) else {
2254 break;
2255 };
2256 let Some(parent) = parents.first().copied() else {
2257 break;
2258 };
2259 if chain.bloom_definitely_not_for_keys(current, keys.as_slice()) {
2260 bloom_yes += 1;
2261 } else {
2262 bloom_no += 1;
2263 }
2264 current = parent;
2265 }
2266 assert!(
2267 bloom_yes > bloom_no,
2268 "expected bloom to skip unchanged tracked path (yes={bloom_yes} no={bloom_no})"
2269 );
2270 }
2271
2272 #[test]
2273 #[ignore = "local git.git worktree; GRIT_BLAME_BENCH_REPO"]
2274 fn first_parent_makefile_bloom_skip_rate_on_git_dot_git() {
2275 use crate::bloom::bloom_keyvec_for_path;
2276 use crate::environment::RepositoryOptions;
2277 use crate::pathspec::bloom_lookup_prefix_with_cwd;
2278 use crate::repo::Repository;
2279
2280 let repo_path = std::env::var("GRIT_BLAME_BENCH_REPO")
2281 .unwrap_or_else(|_| "/tmp/grit-bench/git-dot-git-worktree".to_string());
2282 if !std::path::Path::new(&repo_path).join(".git").exists() {
2283 return;
2284 }
2285 let git_dir = std::path::Path::new(&repo_path).join(".git");
2286 let repo =
2287 Repository::open(git_dir.as_path(), &RepositoryOptions::default()).expect("open");
2288 let chain = repo
2289 .caches()
2290 .commit_graph_chain(repo.odb().objects_dir())
2291 .expect("graph");
2292 let settings = chain.top_layer_bloom_settings().expect("bloom");
2293 let norm = bloom_lookup_prefix_with_cwd("Makefile", None).expect("prefix");
2294 let keys = bloom_keyvec_for_path(norm.as_str(), &settings);
2295 let head = *crate::state::resolve_head(repo.git_dir())
2296 .expect("head")
2297 .oid()
2298 .expect("commit");
2299 let mut current = head;
2300 let mut bloom_yes = 0usize;
2301 let mut bloom_no = 0usize;
2302 for _ in 0..10_000 {
2303 let Some((parents, _)) = chain.graph_commit(¤t) else {
2304 break;
2305 };
2306 let Some(parent) = parents.first().copied() else {
2307 break;
2308 };
2309 if chain.bloom_definitely_not_for_keys(current, keys.as_slice()) {
2310 bloom_yes += 1;
2311 } else {
2312 bloom_no += 1;
2313 }
2314 current = parent;
2315 }
2316 assert!(
2317 bloom_yes > bloom_no,
2318 "expected mostly bloom skips, yes={bloom_yes} no={bloom_no}"
2319 );
2320 }
2321
2322 #[test]
2323 fn bloom_definitely_not_when_directory_key_present_but_not_full_path() {
2324 use crate::bloom::{
2325 bloom_filter_contains, bloom_keyvec_for_path, build_bloom_filter_data,
2326 collect_changed_paths_for_bloom,
2327 };
2328
2329 let bloom = BloomFilterSettings::default();
2330 let changed = collect_changed_paths_for_bloom(&["builtin/sibling.c".to_owned()]);
2331 let (filter, _) = build_bloom_filter_data(&changed, 1, &bloom);
2332 let path_keys = bloom_keyvec_for_path("builtin/commit.c", &bloom);
2333 assert!(path_keys.len() >= 2);
2334 assert_eq!(
2335 bloom_filter_contains(&path_keys[0], &filter, &bloom),
2336 Ok(false),
2337 "full path key must be absent when only a sibling changed"
2338 );
2339 assert_eq!(
2340 bloom_filter_contains(&path_keys[1], &filter, &bloom),
2341 Ok(true),
2342 "directory prefix key should be present"
2343 );
2344 assert!(super::bloom_definitely_not_from_filter(
2345 &filter, &bloom, &path_keys
2346 ));
2347 }
2348
2349 #[test]
2350 fn large_git_split_chain_layers_use_mapped_backing() {
2351 use std::process::Command;
2352
2353 let git_ok = Command::new("git")
2354 .arg("--version")
2355 .output()
2356 .map(|o| o.status.success())
2357 .unwrap_or(false);
2358 if !git_ok {
2359 eprintln!("SKIP: git unavailable");
2360 return;
2361 }
2362 let dir = tempfile::tempdir().expect("tempdir");
2363 let run = |args: &[&str]| {
2364 let out = Command::new("git")
2365 .current_dir(dir.path())
2366 .args(args)
2367 .env("GIT_CONFIG_GLOBAL", "/dev/null")
2368 .env("GIT_CONFIG_SYSTEM", "/dev/null")
2369 .env("GIT_AUTHOR_NAME", "T")
2370 .env("GIT_AUTHOR_EMAIL", "t@example.com")
2371 .env("GIT_COMMITTER_NAME", "T")
2372 .env("GIT_COMMITTER_EMAIL", "t@example.com")
2373 .output()
2374 .expect("git");
2375 assert!(out.status.success(), "git {args:?}");
2376 };
2377 run(&["init", "-q", "-b", "main"]);
2378 run(&["commit", "--allow-empty", "-q", "-m", "bootstrap"]);
2379 for i in 0..150 {
2380 run(&[
2381 "commit",
2382 "--allow-empty",
2383 "-q",
2384 "-m",
2385 &format!("base-fill-{i}"),
2386 ]);
2387 }
2388 run(&[
2389 "commit-graph",
2390 "write",
2391 "--reachable",
2392 "--split",
2393 "--max-commits",
2394 "40",
2395 ]);
2396 for i in 150..190 {
2397 run(&[
2398 "commit",
2399 "--allow-empty",
2400 "-q",
2401 "-m",
2402 &format!("tip-fill-{i}"),
2403 ]);
2404 }
2405 run(&[
2406 "commit-graph",
2407 "write",
2408 "--reachable",
2409 "--split",
2410 "--max-commits",
2411 "40",
2412 ]);
2413 let objects = dir.path().join(".git/objects");
2414 let chain = super::CommitGraphChain::try_load(&objects)
2415 .expect("load")
2416 .expect("chain");
2417 assert!(
2418 chain.num_layers() >= 2,
2419 "expected multi-layer chain, got {}",
2420 chain.num_layers()
2421 );
2422 let mut saw_mapped_large_layer = false;
2423 for path in chain.layer_paths_oldest_first() {
2424 let len = std::fs::metadata(&path).expect("meta").len();
2425 if len <= 8192 {
2426 continue;
2427 }
2428 let storage = crate::pack_map::PackData::open_with_min_len(
2429 &path,
2430 super::COMMIT_GRAPH_MIN_FILE_LEN,
2431 )
2432 .expect("open layer");
2433 assert!(
2434 storage.is_mapped_backing(),
2435 "expected mmap backing for {} ({} bytes)",
2436 path.display(),
2437 len
2438 );
2439 saw_mapped_large_layer = true;
2440 }
2441 assert!(
2442 saw_mapped_large_layer,
2443 "expected at least one chain layer larger than 8192 bytes to use mmap"
2444 );
2445 }
2446}