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
934pub(crate) const UNRANKED_GENERATION: u64 = (1u64 << 63) - 1;
937
938#[derive(Debug, Clone)]
940pub(crate) struct GraphWalkEntry {
941 pub(crate) tree: ObjectId,
943 pub(crate) parents: Vec<ObjectId>,
945 pub(crate) time: i64,
947 pub(crate) generation: u64,
949}
950
951#[derive(Debug, Clone)]
953pub struct CommitGraphChain {
954 layers: Vec<CommitGraphLayer>,
955}
956
957#[doc(hidden)]
959pub fn commit_graph_chain_from_layers_for_test(layers: Vec<CommitGraphLayer>) -> CommitGraphChain {
960 CommitGraphChain { layers }
961}
962
963impl CommitGraphChain {
964 #[must_use]
966 pub fn top_layer_bloom_settings(&self) -> Option<BloomFilterSettings> {
967 self.layers.first()?.bloom_settings
968 }
969
970 #[must_use]
972 pub fn total_commits(&self) -> u32 {
973 self.layers.iter().map(|l| l.num_commits).sum()
974 }
975
976 #[must_use]
978 pub fn layer_paths_oldest_first(&self) -> Vec<PathBuf> {
979 self.layers.iter().rev().map(|l| l.path.clone()).collect()
980 }
981
982 #[must_use]
984 pub fn num_layers(&self) -> usize {
985 self.layers.len()
986 }
987
988 #[must_use]
990 pub fn layer_commit_counts_tip_first(&self) -> Vec<u32> {
991 self.layers.iter().map(|l| l.num_commits).collect()
992 }
993
994 #[must_use]
996 pub fn layer_has_generation_data_tip_first(&self) -> Vec<bool> {
997 self.layers
998 .iter()
999 .map(|l| l.chunk_generation_data.is_some())
1000 .collect()
1001 }
1002
1003 #[must_use]
1005 pub fn layer_hashes_tip_first(&self) -> Vec<String> {
1006 self.layers.iter().map(|l| l.layer_display_id()).collect()
1007 }
1008
1009 #[must_use]
1013 pub fn layer_object_dirs_tip_first(&self) -> Vec<PathBuf> {
1014 self.layers
1015 .iter()
1016 .map(|l| {
1017 let p = l.path.as_path();
1020 let info = if p
1021 .parent()
1022 .and_then(|d| d.file_name())
1023 .map(|n| n == "commit-graphs")
1024 .unwrap_or(false)
1025 {
1026 p.parent().and_then(|d| d.parent())
1027 } else {
1028 p.parent()
1029 };
1030 info.and_then(|d| d.parent())
1031 .map(Path::to_path_buf)
1032 .unwrap_or_else(|| PathBuf::from("."))
1033 })
1034 .collect()
1035 }
1036
1037 #[must_use]
1041 pub fn sub_chain_tip_first(&self, start: usize, end: usize) -> Option<Self> {
1042 let end = end.min(self.layers.len());
1043 if start >= end {
1044 return None;
1045 }
1046 Some(Self {
1047 layers: self.layers[start..end].to_vec(),
1048 })
1049 }
1050
1051 #[must_use]
1053 pub fn layer_oids(&self, tip_first_idx: usize) -> Vec<ObjectId> {
1054 let Some(layer) = self.layers.get(tip_first_idx) else {
1055 return Vec::new();
1056 };
1057 let mut out = Vec::with_capacity(layer.num_commits as usize);
1058 for i in 0..layer.num_commits {
1059 if let Some(oid) = layer.oid_at_lex(i) {
1060 out.push(oid);
1061 }
1062 }
1063 out
1064 }
1065
1066 pub fn try_load(objects_dir: &Path) -> Result<Option<Self>, Error> {
1071 Self::try_load_with_caches(objects_dir, None)
1072 }
1073
1074 pub fn try_load_with_caches(
1076 objects_dir: &Path,
1077 caches: Option<&crate::repo_caches::RepoCaches>,
1078 ) -> Result<Option<Self>, Error> {
1079 let info = objects_dir.join("info");
1080 let chain_path = info.join("commit-graphs").join("commit-graph-chain");
1081 if chain_path.is_file() {
1082 let algo =
1083 hash_algo_for_objects_dir(&crate::environment::Environment::empty(), objects_dir);
1084 let content = std::fs::read_to_string(&chain_path).map_err(Error::from)?;
1085 let mut layers = Vec::new();
1086 for line in content.lines() {
1087 let h = line.trim();
1088 if !commit_graph_chain_hash_is_valid(h, algo) {
1089 continue;
1090 }
1091 let graph_path = info.join("commit-graphs").join(format!("graph-{h}.graph"));
1092 let layer = CommitGraphLayer::try_open(graph_path)?;
1093 let n = layers.len();
1100 if n > 0 && layer.base_chunk_size / layer.hash_len < n {
1101 warn_once_for_base_chunk_too_small(caches, &layer.layer_display_id());
1102 break;
1103 }
1104 layers.push(layer);
1105 }
1106 if layers.is_empty() {
1107 return Ok(None);
1108 }
1109 layers.reverse();
1113 let mut chain = Self { layers };
1114 chain.validate_bloom_compatibility(caches);
1115 return Ok(Some(chain));
1116 }
1117 let single = info.join("commit-graph");
1118 if single.is_file() {
1119 let layer = CommitGraphLayer::try_open(single.clone())?;
1120 let mut chain = Self {
1121 layers: vec![layer],
1122 };
1123 chain.validate_bloom_compatibility(caches);
1124 return Ok(Some(chain));
1125 }
1126 Ok(None)
1127 }
1128
1129 pub fn load(objects_dir: &Path) -> Option<Self> {
1131 Self::try_load(objects_dir).ok().flatten()
1132 }
1133
1134 pub fn verify_on_disk(objects_dir: &Path) -> Result<(), Error> {
1142 let Some(chain) = Self::try_load(objects_dir)? else {
1143 return Ok(());
1144 };
1145 for path in chain.layer_paths_oldest_first() {
1146 let raw = std::fs::read(&path).map_err(Error::from)?;
1147 CommitGraphLayer::try_parse(path, raw)?;
1148 }
1149 Ok(())
1150 }
1151
1152 pub fn try_load_across(
1176 objects_dir: &Path,
1177 alt_dirs: &[PathBuf],
1178 ) -> Result<Option<Self>, Error> {
1179 Self::try_load_across_with_caches(objects_dir, alt_dirs, None)
1180 }
1181
1182 pub fn try_load_across_with_caches(
1188 objects_dir: &Path,
1189 alt_dirs: &[PathBuf],
1190 caches: Option<&crate::repo_caches::RepoCaches>,
1191 ) -> Result<Option<Self>, Error> {
1192 let layer_dir = |dir: &Path| dir.join("info").join("commit-graphs");
1193 let resolve_layer = |hash: &str| -> Option<PathBuf> {
1194 let name = format!("graph-{hash}.graph");
1195 let local = layer_dir(objects_dir).join(&name);
1196 if local.is_file() {
1197 return Some(local);
1198 }
1199 alt_dirs
1200 .iter()
1201 .map(|d| layer_dir(d).join(&name))
1202 .find(|p| p.is_file())
1203 };
1204
1205 let local_chain = layer_dir(objects_dir).join("commit-graph-chain");
1213 let local_single = objects_dir.join("info").join("commit-graph");
1214 let chain_path = if local_chain.is_file() {
1215 local_chain
1216 } else if local_single.is_file() {
1217 return Self::try_load_with_caches(objects_dir, caches);
1218 } else {
1219 match alt_dirs
1220 .iter()
1221 .map(PathBuf::as_path)
1222 .find(|d| layer_dir(d).join("commit-graph-chain").is_file())
1223 {
1224 Some(owner) => layer_dir(owner).join("commit-graph-chain"),
1225 None => return Ok(None),
1226 }
1227 };
1228
1229 let algo =
1230 hash_algo_for_objects_dir(&crate::environment::Environment::empty(), objects_dir);
1231 let content = std::fs::read_to_string(&chain_path).map_err(Error::from)?;
1232 let mut layers = Vec::new();
1233 for line in content.lines() {
1234 let h = line.trim();
1235 if !commit_graph_chain_hash_is_valid(h, algo) {
1236 continue;
1237 }
1238 let graph_path = resolve_layer(h).ok_or_else(|| {
1239 Error::from(std::io::Error::new(
1240 std::io::ErrorKind::NotFound,
1241 format!("commit-graph layer graph-{h}.graph not found"),
1242 ))
1243 })?;
1244 let layer = CommitGraphLayer::try_open(graph_path)?;
1245 let n = layers.len();
1246 if n > 0 && layer.base_chunk_size / layer.hash_len < n {
1247 warn_once_for_base_chunk_too_small(caches, &layer.layer_display_id());
1248 break;
1249 }
1250 layers.push(layer);
1251 }
1252 if layers.is_empty() {
1253 return Ok(None);
1254 }
1255 layers.reverse();
1256 let mut chain = Self { layers };
1257 chain.validate_bloom_compatibility(caches);
1258 Ok(Some(chain))
1259 }
1260
1261 fn validate_bloom_compatibility(&mut self, caches: Option<&crate::repo_caches::RepoCaches>) {
1262 let mut ref_settings: Option<BloomFilterSettings> = None;
1268 for layer in &mut self.layers {
1269 let Some(bs) = layer.bloom_settings else {
1270 continue;
1271 };
1272 match ref_settings {
1273 None => ref_settings = Some(bs),
1274 Some(r) => {
1275 if r.hash_version != bs.hash_version
1276 || r.num_hashes != bs.num_hashes
1277 || r.bits_per_entry != bs.bits_per_entry
1278 {
1279 let id = layer.layer_display_id();
1280 warn_once_for_disabled_bloom_layer(caches, &id);
1286 layer.disable_bloom();
1287 }
1288 }
1289 }
1290 }
1291 }
1292
1293 pub fn existing_filter_bytes(
1299 &self,
1300 oid: &ObjectId,
1301 want: &BloomFilterSettings,
1302 ) -> Option<Vec<u8>> {
1303 let (layer_idx, lex) = self.find_commit(oid)?;
1304 let layer = &self.layers[layer_idx];
1305 let settings = layer.bloom_settings.as_ref()?;
1306 if settings.hash_version != want.hash_version
1307 || settings.num_hashes != want.num_hashes
1308 || settings.bits_per_entry != want.bits_per_entry
1309 {
1310 return None;
1311 }
1312 match layer.bloom_filter_slice(lex) {
1319 Some(s) if !s.is_empty() => Some(s.to_vec()),
1320 _ => None,
1321 }
1322 }
1323
1324 pub fn upgradable_filter_bytes(
1330 &self,
1331 oid: &ObjectId,
1332 want: &BloomFilterSettings,
1333 ) -> Option<Vec<u8>> {
1334 let (layer_idx, lex) = self.find_commit(oid)?;
1335 let layer = &self.layers[layer_idx];
1336 let settings = layer.bloom_settings.as_ref()?;
1337 if settings.hash_version == want.hash_version {
1338 return None;
1339 }
1340 if settings.num_hashes != want.num_hashes || settings.bits_per_entry != want.bits_per_entry
1341 {
1342 return None;
1343 }
1344 match layer.bloom_filter_slice(lex) {
1345 Some(s) if !s.is_empty() => Some(s.to_vec()),
1346 _ => None,
1347 }
1348 }
1349
1350 pub(crate) fn bloom_settings_for_commit(&self, oid: &ObjectId) -> Option<BloomFilterSettings> {
1352 let (layer_idx, _) = self.find_commit(oid)?;
1353 self.layers[layer_idx].bloom_settings
1354 }
1355
1356 pub fn find_commit(&self, oid: &ObjectId) -> Option<(usize, u32)> {
1358 for (i, layer) in self.layers.iter().enumerate() {
1359 if let Some(lex) = layer.bsearch_oid(oid) {
1360 return Some((i, lex));
1361 }
1362 }
1363 None
1364 }
1365
1366 pub fn global_position(&self, oid: &ObjectId) -> Option<u32> {
1368 let (layer_idx, lex) = self.find_commit(oid)?;
1369 let below: u32 = self.layers[layer_idx + 1..]
1370 .iter()
1371 .map(|l| l.num_commits)
1372 .sum();
1373 Some(below + lex)
1374 }
1375
1376 fn oid_at_global_position(&self, pos: u32) -> Option<ObjectId> {
1379 let mut remaining = pos;
1381 for layer in self.layers.iter().rev() {
1382 if remaining < layer.num_commits {
1383 return layer.oid_at_lex(remaining);
1384 }
1385 remaining -= layer.num_commits;
1386 }
1387 None
1388 }
1389
1390 pub fn graph_commit(&self, oid: &ObjectId) -> Option<(Vec<ObjectId>, i64)> {
1393 let (layer_idx, lex) = self.find_commit(oid)?;
1394 self.graph_commit_at(layer_idx, lex)
1395 }
1396
1397 fn graph_commit_at(&self, layer_idx: usize, lex: u32) -> Option<(Vec<ObjectId>, i64)> {
1398 let layer = &self.layers[layer_idx];
1399 let (_, _, commit_time) = layer.cdat_record(lex)?;
1400 let oids = layer
1401 .parent_global_positions(lex)?
1402 .into_iter()
1403 .filter_map(|pos| self.oid_at_global_position(pos))
1404 .collect();
1405 Some((oids, commit_time))
1406 }
1407
1408 pub(crate) fn graph_commit_tree(&self, oid: &ObjectId) -> Option<ObjectId> {
1410 let (layer_idx, lex) = self.find_commit(oid)?;
1411 self.layers[layer_idx].cdat_root_tree(lex)
1412 }
1413
1414 pub(crate) fn graph_commit_meta(&self, oid: &ObjectId) -> Option<(ObjectId, Vec<ObjectId>)> {
1416 let (layer_idx, lex) = self.find_commit(oid)?;
1417 let tree = self.layers[layer_idx].cdat_root_tree(lex)?;
1418 let (parents, _) = self.graph_commit_at(layer_idx, lex)?;
1419 Some((tree, parents))
1420 }
1421
1422 pub(crate) fn graph_walk_entry(&self, oid: &ObjectId) -> Option<GraphWalkEntry> {
1425 let (layer_idx, lex) = self.find_commit(oid)?;
1426 let layer = &self.layers[layer_idx];
1427 let tree = layer.cdat_root_tree(lex)?;
1428 let (parents, time) = self.graph_commit_at(layer_idx, lex)?;
1429 Some(GraphWalkEntry {
1430 tree,
1431 parents,
1432 time,
1433 generation: layer
1434 .corrected_commit_generation(lex)
1435 .unwrap_or(UNRANKED_GENERATION),
1436 })
1437 }
1438
1439 #[must_use]
1441 pub fn commit_generation_for_walk(&self, oid: &ObjectId) -> u64 {
1442 let Some((layer_idx, lex)) = self.find_commit(oid) else {
1443 return UNRANKED_GENERATION;
1444 };
1445 self.layers[layer_idx]
1446 .corrected_commit_generation(lex)
1447 .unwrap_or(UNRANKED_GENERATION)
1448 }
1449
1450 #[must_use]
1453 pub fn parent_global_positions(&self, oid: &ObjectId) -> Option<Vec<u32>> {
1454 let (layer_idx, lex) = self.find_commit(oid)?;
1455 self.layers[layer_idx].parent_global_positions(lex)
1456 }
1457
1458 #[must_use]
1462 pub fn count_reachable_commits(
1463 &self,
1464 tips: &[ObjectId],
1465 hide: &[ObjectId],
1466 first_parent_only: bool,
1467 shallow: &std::collections::HashSet<ObjectId>,
1468 ) -> Option<usize> {
1469 if self.total_commits() == 0 {
1470 return None;
1471 }
1472 let mut seen = self.reachable_positions(tips, first_parent_only, shallow)?;
1473 if hide.is_empty() {
1474 return Some(seen.len());
1475 }
1476 let hidden = self.reachable_positions(hide, first_parent_only, shallow)?;
1477 seen.retain(|pos| !hidden.contains(pos));
1478 Some(seen.len())
1479 }
1480
1481 fn reachable_positions(
1482 &self,
1483 tips: &[ObjectId],
1484 first_parent_only: bool,
1485 shallow: &std::collections::HashSet<ObjectId>,
1486 ) -> Option<std::collections::HashSet<u32>> {
1487 let total = self.total_commits();
1488 let mut stack: Vec<u32> = Vec::new();
1489 for tip in tips {
1490 stack.push(self.global_position(tip)?);
1491 }
1492 let mut seen = std::collections::HashSet::new();
1493 while let Some(pos) = stack.pop() {
1494 if pos >= total || !seen.insert(pos) {
1495 continue;
1496 }
1497 #[cfg(test)]
1498 crate::merge_base::intersect_walk_test_hooks::record_commit_parent_lookup();
1499 let oid = self.oid_at_global_position(pos)?;
1500 let parents = if shallow.contains(&oid) {
1501 Vec::new()
1502 } else {
1503 self.parent_global_positions(&oid)?
1504 };
1505 let parents = if first_parent_only {
1506 parents.into_iter().take(1).collect::<Vec<_>>()
1507 } else {
1508 parents
1509 };
1510 for p in parents {
1511 if p >= total {
1512 return None;
1513 }
1514 stack.push(p);
1515 }
1516 }
1517 Some(seen)
1518 }
1519
1520 pub fn all_oids_in_order(&self) -> Vec<ObjectId> {
1522 let mut out = Vec::new();
1523 for layer in self.layers.iter().rev() {
1524 for i in 0..layer.num_commits {
1525 if let Some(oid) = layer.oid_at_lex(i) {
1526 out.push(oid);
1527 }
1528 }
1529 }
1530 out
1531 }
1532
1533 pub fn bloom_precheck_for_paths(
1535 &self,
1536 _odb: &Odb,
1537 oid: ObjectId,
1538 pathspecs: &[String],
1539 bloom_cwd: Option<&str>,
1540 requested_hash_version: i32,
1541 read_changed_paths: bool,
1542 ) -> std::result::Result<BloomPrecheck, crate::error::Error> {
1543 if !read_changed_paths {
1544 return Ok(BloomPrecheck::Inapplicable);
1545 }
1546 let Some((layer_idx, lex)) = self.find_commit(&oid) else {
1547 return Ok(BloomPrecheck::NotInGraph);
1548 };
1549 let layer = &self.layers[layer_idx];
1550 let Some(settings) = layer.bloom_settings.as_ref() else {
1551 return Ok(BloomPrecheck::FilterNotPresent);
1552 };
1553 let effective_version = if requested_hash_version < 0 {
1554 settings.hash_version as i32
1555 } else {
1556 requested_hash_version
1557 };
1558 if effective_version != settings.hash_version as i32 {
1559 return Ok(BloomPrecheck::FilterNotPresent);
1560 }
1561
1562 let filter = match layer.bloom_filter_slice(lex) {
1568 Some(s) => s,
1569 None => return Ok(BloomPrecheck::FilterNotPresent),
1570 };
1571 if filter.is_empty() {
1572 return Ok(BloomPrecheck::FilterNotPresent);
1573 }
1574
1575 let mut any_pathspec_maybe = false;
1578 let mut checked_any_keys = false;
1579 for spec in pathspecs {
1580 if spec.is_empty() || crate::pathspec::pathspec_is_exclude(spec) {
1581 continue;
1582 }
1583 let Some(norm) = crate::pathspec::bloom_lookup_prefix_with_cwd(spec, bloom_cwd) else {
1584 continue;
1585 };
1586 let keys = bloom_keyvec_for_path(norm.as_str(), settings);
1587 if keys.is_empty() {
1588 continue;
1589 }
1590 checked_any_keys = true;
1591 let mut all_keys_maybe = true;
1592 for key in &keys {
1593 match bloom_filter_contains(key, filter, settings) {
1594 Ok(true) => {}
1595 Ok(false) => {
1596 all_keys_maybe = false;
1597 break;
1598 }
1599 Err(crate::bloom::BloomFilterInvalid) => {
1600 all_keys_maybe = true;
1601 break;
1602 }
1603 }
1604 }
1605 if all_keys_maybe {
1606 any_pathspec_maybe = true;
1607 break;
1608 }
1609 }
1610 if checked_any_keys && !any_pathspec_maybe {
1611 return Ok(BloomPrecheck::DefinitelyNot);
1612 }
1613 Ok(BloomPrecheck::Maybe)
1614 }
1615
1616 pub(crate) fn bloom_definitely_not_for_keys(
1618 &self,
1619 oid: ObjectId,
1620 path_keys: &[Vec<u32>],
1621 ) -> bool {
1622 if path_keys.is_empty() {
1623 return false;
1624 }
1625 let Some((layer_idx, lex)) = self.find_commit(&oid) else {
1626 return false;
1627 };
1628 let layer = &self.layers[layer_idx];
1629 let Some(settings) = layer.bloom_settings.as_ref() else {
1630 return false;
1631 };
1632 let Some(filter) = layer.bloom_filter_slice(lex) else {
1633 return false;
1634 };
1635 if filter.is_empty() {
1636 return false;
1637 }
1638 bloom_definitely_not_from_filter(filter, settings, path_keys)
1639 }
1640}
1641
1642pub(crate) fn bloom_definitely_not_from_filter(
1644 filter: &[u8],
1645 settings: &crate::bloom::BloomFilterSettings,
1646 path_keys: &[Vec<u32>],
1647) -> bool {
1648 use crate::bloom::bloom_filter_contains;
1649
1650 for key in path_keys {
1651 match bloom_filter_contains(key, filter, settings) {
1652 Ok(false) => return true,
1653 Ok(true) => {}
1654 Err(crate::bloom::BloomFilterInvalid) => return false,
1655 }
1656 }
1657 false
1658}
1659
1660pub fn diff_changed_paths_for_bloom(
1662 odb: &Odb,
1663 parent_tree: Option<ObjectId>,
1664 commit_tree: ObjectId,
1665) -> crate::error::Result<(Vec<String>, usize)> {
1666 use crate::diff::diff_trees;
1667 let entries = diff_trees(odb, parent_tree.as_ref(), Some(&commit_tree), "")?;
1668 let raw_len = entries.len();
1669 let mut paths = Vec::new();
1670 for e in entries {
1671 let p = e.path().to_string();
1672 if !p.is_empty() {
1673 paths.push(p);
1674 }
1675 }
1676 Ok((paths, raw_len))
1677}
1678
1679pub use crate::bloom::collect_changed_paths_for_bloom;
1681
1682pub fn bloom_filter_for_commit_write(
1684 odb: &Odb,
1685 parents: &[ObjectId],
1686 tree_oid: ObjectId,
1687 settings: &BloomFilterSettings,
1688) -> crate::error::Result<(Vec<u8>, BloomBuildOutcome)> {
1689 let (changed_paths_vec, raw_count) = if let Some(first_parent) = parents.first() {
1693 let p = odb.commit_tree(first_parent)?;
1694 diff_changed_paths_for_bloom(odb, Some(p), tree_oid)?
1695 } else {
1696 diff_changed_paths_for_bloom(odb, None, tree_oid)?
1697 };
1698 let set = collect_changed_paths_for_bloom(&changed_paths_vec);
1699 Ok(crate::bloom::build_bloom_filter_data(
1700 &set, raw_count, settings,
1701 ))
1702}
1703
1704fn tree_has_high_bit_paths(odb: &Odb, tree_oid: ObjectId) -> bool {
1709 let Ok(obj) = odb.read(&tree_oid) else {
1710 return true;
1712 };
1713 let Ok(entries) = crate::objects::parse_tree(&obj.data) else {
1714 return true;
1715 };
1716 for e in &entries {
1717 if e.name.iter().any(|&b| b & 0x80 != 0) {
1718 return true;
1719 }
1720 if e.mode == 0o040000 && tree_has_high_bit_paths(odb, e.oid) {
1721 return true;
1722 }
1723 }
1724 false
1725}
1726
1727pub fn commit_tree_has_high_bit_paths(odb: &Odb, commit_oid: ObjectId) -> bool {
1730 match odb.commit_tree(&commit_oid) {
1731 Ok(tree) => tree_has_high_bit_paths(odb, tree),
1732 Err(_) => true,
1733 }
1734}
1735
1736pub fn parse_graph_file(path: &Path) -> Option<ParsedGraphDump> {
1738 let raw = std::fs::read(path).ok()?;
1739 if raw.len() < 28 {
1740 return None;
1741 }
1742 let hash_len = commit_graph_hash_len(raw[5])?;
1743 let body = &raw[..raw.len() - hash_len];
1744 if body.len() < 8 || &body[0..4] != SIGNATURE {
1745 return None;
1746 }
1747 let header_word = u32::from_be_bytes(body[0..4].try_into().ok()?);
1748 let num_chunks = body[6] as usize;
1749 let toc_start = 8;
1750 let mut present: std::collections::HashSet<u32> = std::collections::HashSet::new();
1751 for i in 0..num_chunks {
1752 let e = toc_start + i * 12;
1753 let id = u32::from_be_bytes(body[e..e + 4].try_into().ok()?);
1754 present.insert(id);
1755 }
1756 let mut chunk_names: Vec<String> = Vec::new();
1759 for (id, label) in [
1760 (CHUNK_OID_FANOUT, "oid_fanout"),
1761 (CHUNK_OID_LOOKUP, "oid_lookup"),
1762 (CHUNK_COMMIT_DATA, "commit_metadata"),
1763 (CHUNK_GENERATION_DATA, "generation_data"),
1764 (CHUNK_GENERATION_DATA_OVERFLOW, "generation_data_overflow"),
1765 (CHUNK_EXTRA_EDGES, "extra_edges"),
1766 (CHUNK_BLOOM_INDEXES, "bloom_indexes"),
1767 (CHUNK_BLOOM_DATA, "bloom_data"),
1768 ] {
1769 if present.contains(&id) {
1770 chunk_names.push(label.to_string());
1771 }
1772 }
1773 let layer = CommitGraphLayer::parse(path.to_path_buf(), raw.clone())?;
1774 let bloom_opt = layer.bloom_settings.map(|s| {
1775 format!(
1776 " bloom({},{},{})",
1777 s.hash_version, s.bits_per_entry, s.num_hashes
1778 )
1779 });
1780 let mut options = String::new();
1781 if let Some(b) = bloom_opt {
1782 options.push_str(&b);
1783 }
1784 if layer.read_generation_data {
1785 options.push_str(" read_generation_data");
1786 }
1787 Some(ParsedGraphDump {
1788 header_word,
1789 version: body[4],
1790 hash_ver: body[5],
1791 num_chunks: body[6],
1792 reserved: body[7],
1793 num_commits: layer.num_commits,
1794 chunks: chunk_names.join(" "),
1795 options,
1796 })
1797}
1798
1799pub struct ParsedGraphDump {
1801 pub header_word: u32,
1803 pub version: u8,
1805 pub hash_ver: u8,
1807 pub num_chunks: u8,
1809 pub reserved: u8,
1811 pub num_commits: u32,
1813 pub chunks: String,
1815 pub options: String,
1817}
1818
1819pub fn dump_bloom_filters(path: &Path) -> Option<Vec<String>> {
1821 let raw = std::fs::read(path).ok()?;
1822 let layer = CommitGraphLayer::parse(path.to_path_buf(), raw)?;
1823 let mut out = Vec::new();
1824 for i in 0..layer.num_commits {
1825 let slice = layer.bloom_filter_slice(i).unwrap_or(&[]);
1826 if slice.is_empty() {
1827 out.push(String::new());
1828 } else {
1829 let hex: String = slice.iter().map(|b| format!("{b:02x}")).collect();
1830 out.push(hex);
1831 }
1832 }
1833 Some(out)
1834}
1835
1836#[cfg(test)]
1837mod tests {
1838 use std::collections::HashMap;
1839
1840 use crate::bloom::BloomFilterSettings;
1841 use crate::commit_graph_write::{build_commit_graph_bytes, load_commit_graph_commit_info};
1842 use crate::index::{Index, IndexEntry, MODE_REGULAR};
1843 use crate::objects::{serialize_commit, CommitData, ObjectId, ObjectKind};
1844 use crate::repo::{init_repository, Repository};
1845 use crate::write_tree::write_tree_from_index;
1846
1847 use crate::commit_graph_write::CommitGraphCommitInfo;
1848 use crate::objects::OidHashMap;
1849
1850 fn infos_std(
1851 infos: &OidHashMap<CommitGraphCommitInfo>,
1852 ) -> HashMap<ObjectId, CommitGraphCommitInfo> {
1853 infos.iter().map(|(k, v)| (*k, v.clone())).collect()
1854 }
1855
1856 fn empty_byte_filters() -> HashMap<ObjectId, Vec<u8>> {
1857 HashMap::new()
1858 }
1859
1860 fn one_commit_repo() -> (tempfile::TempDir, Repository, ObjectId) {
1861 let dir = tempfile::tempdir().expect("tempdir");
1862 let repo = init_repository(
1863 dir.path(),
1864 false,
1865 "main",
1866 None,
1867 crate::RefStorageFormat::Files,
1868 )
1869 .expect("init");
1870 let mut index = Index::new();
1871 index.hash_algo = repo.odb().hash_algo();
1872 let blob = repo.odb().write(ObjectKind::Blob, b"x\n").expect("blob");
1873 index.add_or_replace(IndexEntry {
1874 ctime_sec: 0,
1875 ctime_nsec: 0,
1876 mtime_sec: 0,
1877 mtime_nsec: 0,
1878 dev: 0,
1879 ino: 0,
1880 mode: MODE_REGULAR,
1881 uid: 0,
1882 gid: 0,
1883 size: 2,
1884 oid: blob,
1885 flags: 4,
1886 flags_extended: None,
1887 path: b"f.txt".to_vec(),
1888 base_index_pos: 0,
1889 });
1890 let tree = write_tree_from_index(&repo.odb(), &index, "").expect("tree");
1891 let raw = serialize_commit(&CommitData {
1892 tree,
1893 parents: vec![],
1894 author: "T <t@example.com> 100 +0000".into(),
1895 committer: "T <t@example.com> 100 +0000".into(),
1896 author_raw: Vec::new(),
1897 committer_raw: Vec::new(),
1898 encoding: None,
1899 message: "m\n".into(),
1900 raw_message: None,
1901 extra_headers: Vec::new(),
1902 });
1903 let commit = repo.odb().write(ObjectKind::Commit, &raw).expect("commit");
1904 crate::refs::write_ref(&repo.git_dir(), "refs/heads/main", &commit).expect("ref");
1905 (dir, repo, commit)
1906 }
1907
1908 fn reseal_graph(bytes: &mut Vec<u8>, algo: crate::objects::HashAlgo) {
1909 let hash_len = algo.len();
1910 let body_len = bytes.len().saturating_sub(hash_len);
1911 let digest = algo.checksum_digest(&bytes[..body_len]);
1912 bytes.truncate(body_len);
1913 bytes.extend_from_slice(&digest);
1914 }
1915
1916 #[test]
1917 fn try_parse_rejects_bad_signature_and_truncated_toc() {
1918 let (_dir, repo, commit) = one_commit_repo();
1919 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
1920 let mut infos = crate::objects::oid_hash_map();
1921 infos.insert(commit, info);
1922 let bloom = BloomFilterSettings::default();
1923 let (mut bytes, _) = build_commit_graph_bytes(
1924 &[commit],
1925 &infos_std(&infos),
1926 &repo.odb(),
1927 false,
1928 &bloom,
1929 None,
1930 &[],
1931 None,
1932 &empty_byte_filters(),
1933 &empty_byte_filters(),
1934 true,
1935 )
1936 .expect("write");
1937 let algo = repo.odb().hash_algo();
1938 bytes[0] = 0;
1939 reseal_graph(&mut bytes, algo);
1940 let err = super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), bytes.clone())
1941 .unwrap_err();
1942 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
1943 bytes.truncate(40);
1944 let err =
1945 super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), bytes).unwrap_err();
1946 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
1947 let (mut good, _) = build_commit_graph_bytes(
1948 &[commit],
1949 &infos_std(&infos),
1950 &repo.odb(),
1951 false,
1952 &bloom,
1953 None,
1954 &[],
1955 None,
1956 &empty_byte_filters(),
1957 &empty_byte_filters(),
1958 true,
1959 )
1960 .expect("write");
1961 good[4] = 2;
1962 reseal_graph(&mut good, algo);
1963 let err =
1964 super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), good).unwrap_err();
1965 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
1966 let (mut checksum_bad, _) = build_commit_graph_bytes(
1967 &[commit],
1968 &infos_std(&infos),
1969 &repo.odb(),
1970 false,
1971 &bloom,
1972 None,
1973 &[],
1974 None,
1975 &empty_byte_filters(),
1976 &empty_byte_filters(),
1977 true,
1978 )
1979 .expect("write");
1980 let last = checksum_bad.len() - 1;
1981 checksum_bad[last] ^= 0xff;
1982 let err = super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), checksum_bad)
1983 .unwrap_err();
1984 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
1985 }
1986
1987 #[test]
1988 fn parse_graph_file_returns_none_for_invalid_inputs() {
1989 let dir = tempfile::tempdir().expect("tempdir");
1990 let missing = dir.path().join("nope.graph");
1991 assert!(super::parse_graph_file(&missing).is_none());
1992 std::fs::write(dir.path().join("tiny.graph"), b"CGPH").expect("write");
1993 assert!(super::parse_graph_file(&dir.path().join("tiny.graph")).is_none());
1994 assert!(super::dump_bloom_filters(&missing).is_none());
1995 }
1996
1997 #[test]
1998 fn try_load_missing_graph_is_none() {
1999 let dir = tempfile::tempdir().expect("tempdir");
2000 let objects = dir.path().join("objects");
2001 std::fs::create_dir_all(objects.join("info")).expect("info");
2002 assert!(super::CommitGraphChain::try_load(&objects)
2003 .expect("ok")
2004 .is_none());
2005 }
2006
2007 #[test]
2008 fn try_load_across_finds_single_graph() {
2009 let (_dir, repo, _commit) = one_commit_repo();
2010 let objects = repo.odb().objects_dir();
2011 let sorted = vec![_commit];
2012 let info = load_commit_graph_commit_info(&repo.odb(), _commit).expect("info");
2013 let mut infos = crate::objects::oid_hash_map();
2014 infos.insert(_commit, info);
2015 let bloom = BloomFilterSettings::default();
2016 let (bytes, _) = build_commit_graph_bytes(
2017 &sorted,
2018 &infos_std(&infos),
2019 &repo.odb(),
2020 false,
2021 &bloom,
2022 None,
2023 &[],
2024 None,
2025 &empty_byte_filters(),
2026 &empty_byte_filters(),
2027 true,
2028 )
2029 .expect("write");
2030 std::fs::write(objects.join("info/commit-graph"), bytes).expect("write");
2031 let chain = super::CommitGraphChain::try_load_across(objects, &[])
2032 .expect("across")
2033 .expect("some");
2034 assert_eq!(chain.total_commits(), 1);
2035 }
2036
2037 #[test]
2038 fn try_parse_rejects_oidf_toc_offset_out_of_range() {
2039 let (_dir, repo, commit) = one_commit_repo();
2040 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
2041 let mut infos = crate::objects::oid_hash_map();
2042 infos.insert(commit, info);
2043 let bloom = BloomFilterSettings::default();
2044 let (mut bytes, _) = build_commit_graph_bytes(
2045 &[commit],
2046 &infos_std(&infos),
2047 &repo.odb(),
2048 false,
2049 &bloom,
2050 None,
2051 &[],
2052 None,
2053 &empty_byte_filters(),
2054 &empty_byte_filters(),
2055 true,
2056 )
2057 .expect("write");
2058 let algo = repo.odb().hash_algo();
2059 let num_chunks = bytes[6] as usize;
2060 for i in 0..num_chunks {
2061 let e = 8 + i * 12;
2062 let id = u32::from_be_bytes(bytes[e..e + 4].try_into().unwrap());
2063 if id == 0x4f49_4446 {
2064 bytes[e + 4..e + 12].copy_from_slice(&u64::MAX.to_be_bytes());
2065 break;
2066 }
2067 }
2068 reseal_graph(&mut bytes, algo);
2069 let err =
2070 super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), bytes).unwrap_err();
2071 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
2072 }
2073
2074 #[test]
2075 fn generation_overflow_without_gdo2_fails_parse() {
2076 let (_dir, repo, commit) = one_commit_repo();
2077 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
2078 let mut infos = crate::objects::oid_hash_map();
2079 infos.insert(commit, info);
2080 let bloom = BloomFilterSettings::default();
2081 let (mut bytes, _) = build_commit_graph_bytes(
2082 &[commit],
2083 &infos_std(&infos),
2084 &repo.odb(),
2085 false,
2086 &bloom,
2087 None,
2088 &[],
2089 None,
2090 &empty_byte_filters(),
2091 &empty_byte_filters(),
2092 true,
2093 )
2094 .expect("write");
2095 if bytes.len() > 100 {
2096 for i in 80..120 {
2097 if i + 4 <= bytes.len() - 20 {
2098 bytes[i] = 0x80;
2099 bytes[i + 1] = 0x00;
2100 bytes[i + 2] = 0x00;
2101 bytes[i + 3] = 0x00;
2102 break;
2103 }
2104 }
2105 }
2106 let err =
2107 super::CommitGraphLayer::try_parse(std::path::PathBuf::from("g"), bytes).unwrap_err();
2108 assert!(matches!(err, crate::error::Error::CorruptObject(_)));
2109 }
2110
2111 #[test]
2112 fn bloom_precheck_not_in_graph_for_missing_oid() {
2113 let (_dir, repo, commit) = one_commit_repo();
2114 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
2115 let mut infos = crate::objects::oid_hash_map();
2116 infos.insert(commit, info);
2117 let bloom = BloomFilterSettings::default();
2118 let (bytes, _) = build_commit_graph_bytes(
2119 &[commit],
2120 &infos_std(&infos),
2121 &repo.odb(),
2122 true,
2123 &bloom,
2124 None,
2125 &[],
2126 None,
2127 &empty_byte_filters(),
2128 &empty_byte_filters(),
2129 true,
2130 )
2131 .expect("write");
2132 std::fs::write(repo.odb().objects_dir().join("info/commit-graph"), bytes).expect("write");
2133 let chain = super::CommitGraphChain::load(repo.odb().objects_dir()).expect("chain");
2134 let missing = ObjectId::from_hex("1234567890123456789012345678901234567890").expect("oid");
2135 let pre = chain
2136 .bloom_precheck_for_paths(&repo.odb(), missing, &[], None, -1, true)
2137 .expect("precheck");
2138 assert_eq!(pre, super::BloomPrecheck::NotInGraph);
2139 }
2140
2141 #[test]
2142 fn bloom_precheck_respects_read_changed_paths_flag() {
2143 let (_dir, repo, commit) = one_commit_repo();
2144 let mut sorted = vec![commit];
2145 let info = load_commit_graph_commit_info(&repo.odb(), commit).expect("info");
2146 let mut infos = crate::objects::oid_hash_map();
2147 infos.insert(commit, info);
2148 let bloom = BloomFilterSettings::default();
2149 let (bytes, _) = build_commit_graph_bytes(
2150 &sorted,
2151 &infos_std(&infos),
2152 &repo.odb(),
2153 true,
2154 &bloom,
2155 None,
2156 &[],
2157 None,
2158 &empty_byte_filters(),
2159 &empty_byte_filters(),
2160 true,
2161 )
2162 .expect("write");
2163 std::fs::write(repo.odb().objects_dir().join("info/commit-graph"), bytes).expect("write");
2164 let chain = super::CommitGraphChain::load(repo.odb().objects_dir()).expect("chain");
2165 let pre = chain
2166 .bloom_precheck_for_paths(&repo.odb(), commit, &["f.txt".to_owned()], None, -1, false)
2167 .expect("precheck");
2168 assert_eq!(pre, super::BloomPrecheck::Inapplicable);
2169 sorted.clear();
2170 }
2171
2172 #[test]
2173 fn bloom_skips_most_steps_when_tracked_path_unchanged_in_linear_history() {
2174 use crate::bloom::bloom_keyvec_for_path;
2175 use crate::commit_graph_write::{build_commit_graph_bytes, load_commit_graph_commit_info};
2176 use crate::index::{Index, IndexEntry, MODE_REGULAR};
2177 use crate::objects::{serialize_commit, CommitData, ObjectKind};
2178 use crate::pathspec::bloom_lookup_prefix_with_cwd;
2179 use crate::repo::init_repository;
2180 use crate::write_tree::write_tree_from_index;
2181 use crate::RefStorageFormat;
2182 use std::fs;
2183
2184 let dir = tempfile::tempdir().expect("tempdir");
2185 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)
2186 .expect("init");
2187 let path = "tracked.txt";
2188 let stable = b"stable payload\n";
2189 let blob = repo.odb().write(ObjectKind::Blob, stable).expect("blob");
2190 let mut parent: Option<ObjectId> = None;
2191 let mut oids = Vec::new();
2192 for i in 0..24usize {
2193 let mut index = Index::new();
2194 index.hash_algo = repo.odb().hash_algo();
2195 index.add_or_replace(IndexEntry {
2196 ctime_sec: 0,
2197 ctime_nsec: 0,
2198 mtime_sec: 0,
2199 mtime_nsec: 0,
2200 dev: 0,
2201 ino: 0,
2202 mode: MODE_REGULAR,
2203 uid: 0,
2204 gid: 0,
2205 size: stable.len() as u32,
2206 oid: blob,
2207 flags: 4,
2208 flags_extended: None,
2209 path: path.as_bytes().to_vec(),
2210 base_index_pos: 0,
2211 });
2212 if i % 5 == 3 {
2213 let noise = repo
2214 .odb()
2215 .write(ObjectKind::Blob, format!("noise {i}\n").as_bytes())
2216 .expect("noise blob");
2217 index.add_or_replace(IndexEntry {
2218 ctime_sec: 0,
2219 ctime_nsec: 0,
2220 mtime_sec: 0,
2221 mtime_nsec: 0,
2222 dev: 0,
2223 ino: 0,
2224 mode: MODE_REGULAR,
2225 uid: 0,
2226 gid: 0,
2227 size: 8,
2228 oid: noise,
2229 flags: 4,
2230 flags_extended: None,
2231 path: format!("other{i}.txt").into_bytes(),
2232 base_index_pos: 0,
2233 });
2234 }
2235 let tree = write_tree_from_index(&repo.odb(), &index, "").expect("tree");
2236 let mut parents = Vec::new();
2237 if let Some(p) = parent {
2238 parents.push(p);
2239 }
2240 let raw = serialize_commit(&CommitData {
2241 tree,
2242 parents,
2243 author: "T <t@example.com> 100 +0000".into(),
2244 committer: "T <t@example.com> 100 +0000".into(),
2245 author_raw: Vec::new(),
2246 committer_raw: Vec::new(),
2247 encoding: None,
2248 message: format!("c{i}\n"),
2249 raw_message: None,
2250 extra_headers: Vec::new(),
2251 });
2252 let commit = repo.odb().write(ObjectKind::Commit, &raw).expect("commit");
2253 oids.push(commit);
2254 parent = Some(commit);
2255 }
2256 let head = *oids.last().expect("head");
2257 let mut infos = crate::objects::oid_hash_map();
2258 for oid in &oids {
2259 infos.insert(
2260 *oid,
2261 load_commit_graph_commit_info(&repo.odb(), *oid).expect("info"),
2262 );
2263 }
2264 let bloom = BloomFilterSettings::default();
2265 let (bytes, _) = build_commit_graph_bytes(
2266 &oids,
2267 &infos_std(&infos),
2268 &repo.odb(),
2269 true,
2270 &bloom,
2271 None,
2272 &[],
2273 None,
2274 &empty_byte_filters(),
2275 &empty_byte_filters(),
2276 true,
2277 )
2278 .expect("graph bytes");
2279 fs::write(repo.odb().objects_dir().join("info/commit-graph"), bytes).expect("write graph");
2280 let chain = super::CommitGraphChain::load(repo.odb().objects_dir()).expect("chain");
2281 let settings = chain.top_layer_bloom_settings().expect("bloom");
2282 let norm = bloom_lookup_prefix_with_cwd(path, None).expect("prefix");
2283 let keys = bloom_keyvec_for_path(norm.as_str(), &settings);
2284 let mut current = head;
2285 let mut bloom_yes = 0usize;
2286 let mut bloom_no = 0usize;
2287 while bloom_yes + bloom_no < 10_000 {
2288 let Some((parents, _)) = chain.graph_commit(¤t) else {
2289 break;
2290 };
2291 let Some(parent) = parents.first().copied() else {
2292 break;
2293 };
2294 if chain.bloom_definitely_not_for_keys(current, keys.as_slice()) {
2295 bloom_yes += 1;
2296 } else {
2297 bloom_no += 1;
2298 }
2299 current = parent;
2300 }
2301 assert!(
2302 bloom_yes > bloom_no,
2303 "expected bloom to skip unchanged tracked path (yes={bloom_yes} no={bloom_no})"
2304 );
2305 }
2306
2307 #[test]
2308 #[ignore = "local git.git worktree; GRIT_BLAME_BENCH_REPO"]
2309 fn first_parent_makefile_bloom_skip_rate_on_git_dot_git() {
2310 use crate::bloom::bloom_keyvec_for_path;
2311 use crate::environment::RepositoryOptions;
2312 use crate::pathspec::bloom_lookup_prefix_with_cwd;
2313 use crate::repo::Repository;
2314
2315 let repo_path = std::env::var("GRIT_BLAME_BENCH_REPO")
2316 .unwrap_or_else(|_| "/tmp/grit-bench/git-dot-git-worktree".to_string());
2317 if !std::path::Path::new(&repo_path).join(".git").exists() {
2318 return;
2319 }
2320 let git_dir = std::path::Path::new(&repo_path).join(".git");
2321 let repo =
2322 Repository::open(git_dir.as_path(), &RepositoryOptions::default()).expect("open");
2323 let chain = repo
2324 .caches()
2325 .commit_graph_chain(repo.odb().objects_dir())
2326 .expect("graph");
2327 let settings = chain.top_layer_bloom_settings().expect("bloom");
2328 let norm = bloom_lookup_prefix_with_cwd("Makefile", None).expect("prefix");
2329 let keys = bloom_keyvec_for_path(norm.as_str(), &settings);
2330 let head = *crate::state::resolve_head(repo.git_dir())
2331 .expect("head")
2332 .oid()
2333 .expect("commit");
2334 let mut current = head;
2335 let mut bloom_yes = 0usize;
2336 let mut bloom_no = 0usize;
2337 for _ in 0..10_000 {
2338 let Some((parents, _)) = chain.graph_commit(¤t) else {
2339 break;
2340 };
2341 let Some(parent) = parents.first().copied() else {
2342 break;
2343 };
2344 if chain.bloom_definitely_not_for_keys(current, keys.as_slice()) {
2345 bloom_yes += 1;
2346 } else {
2347 bloom_no += 1;
2348 }
2349 current = parent;
2350 }
2351 assert!(
2352 bloom_yes > bloom_no,
2353 "expected mostly bloom skips, yes={bloom_yes} no={bloom_no}"
2354 );
2355 }
2356
2357 #[test]
2358 fn bloom_definitely_not_when_directory_key_present_but_not_full_path() {
2359 use crate::bloom::{
2360 bloom_filter_contains, bloom_keyvec_for_path, build_bloom_filter_data,
2361 collect_changed_paths_for_bloom,
2362 };
2363
2364 let bloom = BloomFilterSettings::default();
2365 let changed = collect_changed_paths_for_bloom(&["builtin/sibling.c".to_owned()]);
2366 let (filter, _) = build_bloom_filter_data(&changed, 1, &bloom);
2367 let path_keys = bloom_keyvec_for_path("builtin/commit.c", &bloom);
2368 assert!(path_keys.len() >= 2);
2369 assert_eq!(
2370 bloom_filter_contains(&path_keys[0], &filter, &bloom),
2371 Ok(false),
2372 "full path key must be absent when only a sibling changed"
2373 );
2374 assert_eq!(
2375 bloom_filter_contains(&path_keys[1], &filter, &bloom),
2376 Ok(true),
2377 "directory prefix key should be present"
2378 );
2379 assert!(super::bloom_definitely_not_from_filter(
2380 &filter, &bloom, &path_keys
2381 ));
2382 }
2383
2384 #[cfg_attr(
2385 not(feature = "full-tests"),
2386 ignore = "starts system git or is heavy; run with --features full-tests"
2387 )]
2388 #[test]
2389 fn large_git_split_chain_layers_use_mapped_backing() {
2390 use std::process::Command;
2391
2392 let git_ok = Command::new("git")
2393 .arg("--version")
2394 .output()
2395 .map(|o| o.status.success())
2396 .unwrap_or(false);
2397 if !git_ok {
2398 eprintln!("SKIP: git unavailable");
2399 return;
2400 }
2401 let dir = tempfile::tempdir().expect("tempdir");
2402 let run = |args: &[&str]| {
2403 let out = Command::new("git")
2404 .current_dir(dir.path())
2405 .args(args)
2406 .env("GIT_CONFIG_GLOBAL", "/dev/null")
2407 .env("GIT_CONFIG_SYSTEM", "/dev/null")
2408 .env("GIT_AUTHOR_NAME", "T")
2409 .env("GIT_AUTHOR_EMAIL", "t@example.com")
2410 .env("GIT_COMMITTER_NAME", "T")
2411 .env("GIT_COMMITTER_EMAIL", "t@example.com")
2412 .output()
2413 .expect("git");
2414 assert!(out.status.success(), "git {args:?}");
2415 };
2416 run(&["init", "-q", "-b", "main"]);
2417 run(&["commit", "--allow-empty", "-q", "-m", "bootstrap"]);
2418 for i in 0..150 {
2419 run(&[
2420 "commit",
2421 "--allow-empty",
2422 "-q",
2423 "-m",
2424 &format!("base-fill-{i}"),
2425 ]);
2426 }
2427 run(&[
2428 "commit-graph",
2429 "write",
2430 "--reachable",
2431 "--split",
2432 "--max-commits",
2433 "40",
2434 ]);
2435 for i in 150..190 {
2436 run(&[
2437 "commit",
2438 "--allow-empty",
2439 "-q",
2440 "-m",
2441 &format!("tip-fill-{i}"),
2442 ]);
2443 }
2444 run(&[
2445 "commit-graph",
2446 "write",
2447 "--reachable",
2448 "--split",
2449 "--max-commits",
2450 "40",
2451 ]);
2452 let objects = dir.path().join(".git/objects");
2453 let chain = super::CommitGraphChain::try_load(&objects)
2454 .expect("load")
2455 .expect("chain");
2456 assert!(
2457 chain.num_layers() >= 2,
2458 "expected multi-layer chain, got {}",
2459 chain.num_layers()
2460 );
2461 let mut saw_mapped_large_layer = false;
2462 for path in chain.layer_paths_oldest_first() {
2463 let len = std::fs::metadata(&path).expect("meta").len();
2464 if len <= 8192 {
2465 continue;
2466 }
2467 let storage = crate::pack_map::PackData::open_with_min_len(
2468 &path,
2469 super::COMMIT_GRAPH_MIN_FILE_LEN,
2470 )
2471 .expect("open layer");
2472 assert!(
2473 storage.is_mapped_backing(),
2474 "expected mmap backing for {} ({} bytes)",
2475 path.display(),
2476 len
2477 );
2478 saw_mapped_large_layer = true;
2479 }
2480 assert!(
2481 saw_mapped_large_layer,
2482 "expected at least one chain layer larger than 8192 bytes to use mmap"
2483 );
2484 }
2485}