This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-maintenance / src / commitgraph.rs
25 kB 795 lines
1use std::collections::{HashMap, HashSet}; 2 3use gix::ObjectId; 4use gix::objs::tree::EntryKind as TreeEntryKind; 5use gix::objs::{CommitRef, Kind, TagRefIter, TreeRef}; 6use gix::prelude::FindExt; 7use knot_git::Repo; 8use knot_types::UnixSeconds; 9 10use crate::MaintError; 11use crate::fsio; 12 13const GRAPH_PARENT_NONE: u32 = 0x7000_0000; 14const GRAPH_EXTRA_EDGES_NEEDED: u32 = 0x8000_0000; 15const GRAPH_LAST_EDGE: u32 = 0x8000_0000; 16const GRAPH_GENERATION_MAX: u32 = 0x3FFF_FFFF; 17const MAX_PEEL_DEPTH: usize = 32; 18 19const CORRECTED_OFFSET_OVERFLOW: u32 = 0x8000_0000; 20const CORRECTED_OFFSET_MAX: u64 = (1 << 31) - 1; 21 22const BLOOM_HASH_VERSION: u32 = 2; 23const BLOOM_NUM_HASHES: u32 = 7; 24const BLOOM_BITS_PER_ENTRY: u32 = 10; 25const BLOOM_MAX_CHANGED_PATHS: usize = 512; 26const BLOOM_SEED0: u32 = 0x293a_e76f; 27const BLOOM_SEED1: u32 = 0x7e64_6e2c; 28const MURMUR_C1: u32 = 0xcc9e_2d51; 29const MURMUR_C2: u32 = 0x1b87_3593; 30const MURMUR_N: u32 = 0xe654_6b64; 31 32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] 33struct Generation(u32); 34 35impl Generation { 36 const fn new(value: u32) -> Self { 37 Self(value) 38 } 39 40 const fn get(self) -> u32 { 41 self.0 42 } 43 44 const fn succ(self) -> Self { 45 Self(self.0.saturating_add(1)) 46 } 47} 48 49knot_types::scalar_newtype! { 50 struct GraphPosition(u32); 51} 52 53#[derive(Debug, Clone, Copy)] 54struct ParentField(u32); 55 56impl ParentField { 57 const NONE: Self = Self(GRAPH_PARENT_NONE); 58 59 fn from_position(position: Option<GraphPosition>) -> Self { 60 position.map_or(Self::NONE, |position| Self(position.get())) 61 } 62 63 fn extra_edges(index: usize) -> Self { 64 Self(GRAPH_EXTRA_EDGES_NEEDED | index as u32) 65 } 66 67 fn to_be_bytes(self) -> [u8; 4] { 68 self.0.to_be_bytes() 69 } 70} 71 72#[derive(Debug, Clone, Copy)] 73struct ParentFields { 74 first: ParentField, 75 second: ParentField, 76} 77 78#[derive(Debug, Clone, Copy)] 79struct EdgeField(u32); 80 81impl EdgeField { 82 fn new(parent: ParentField, last: bool) -> Self { 83 Self(parent.0 | if last { GRAPH_LAST_EDGE } else { 0 }) 84 } 85 86 fn to_be_bytes(self) -> [u8; 4] { 87 self.0.to_be_bytes() 88 } 89} 90 91knot_types::scalar_newtype! { 92 struct CorrectedDate(u64); 93} 94 95struct ChangedPathFilter(Vec<u8>); 96 97impl ChangedPathFilter { 98 fn bytes(&self) -> &[u8] { 99 &self.0 100 } 101 102 fn len_bytes(&self) -> usize { 103 self.0.len() 104 } 105} 106 107struct CommitMeta { 108 tree: ObjectId, 109 parents: Vec<ObjectId>, 110 seconds: UnixSeconds, 111} 112 113pub fn graph_path(repo: &Repo) -> std::path::PathBuf { 114 repo.objects_dir().join("info").join("commit-graph") 115} 116 117pub fn exists(repo: &Repo) -> bool { 118 graph_path(repo).exists() 119} 120 121pub fn write(repo: &Repo) -> Result<bool, MaintError> { 122 let kind = repo.object_format().kind(); 123 let Some(commits) = collect(repo, kind)? else { 124 return Ok(false); 125 }; 126 if commits.is_empty() { 127 return Ok(false); 128 } 129 let blooms = changed_path_filters(repo, &commits, kind)?; 130 let bytes = serialize(&commits, &blooms, kind); 131 let path = graph_path(repo); 132 let info_dir = repo.objects_dir().join("info"); 133 std::fs::create_dir_all(&info_dir).map_err(|error| fsio::io_error(&info_dir, error))?; 134 clear_chain(&info_dir)?; 135 knot_resource::atomic_write_bytes(&path, &bytes, knot_resource::FileMode::Inherited)?; 136 Ok(true) 137} 138 139pub fn remove(repo: &Repo) -> Result<(), MaintError> { 140 let path = graph_path(repo); 141 match std::fs::remove_file(&path) { 142 Ok(()) => {} 143 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} 144 Err(error) => return Err(fsio::io_error(&path, error)), 145 } 146 clear_chain(&repo.objects_dir().join("info")) 147} 148 149fn clear_chain(info_dir: &std::path::Path) -> Result<(), MaintError> { 150 let chain_dir = info_dir.join("commit-graphs"); 151 match std::fs::remove_dir_all(&chain_dir) { 152 Ok(()) => Ok(()), 153 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), 154 Err(error) => Err(fsio::io_error(&chain_dir, error)), 155 } 156} 157 158fn collect( 159 repo: &Repo, 160 kind: gix::hash::Kind, 161) -> Result<Option<HashMap<ObjectId, CommitMeta>>, MaintError> { 162 let odb = &repo.git().objects; 163 let mut stack: Vec<ObjectId> = repo 164 .references()? 165 .into_iter() 166 .filter_map(|record| peel_to_commit(odb, record.target.object_id(), kind, MAX_PEEL_DEPTH)) 167 .collect(); 168 let mut commits: HashMap<ObjectId, CommitMeta> = HashMap::new(); 169 while let Some(oid) = stack.pop() { 170 if commits.contains_key(&oid) { 171 continue; 172 } 173 let mut buf = Vec::new(); 174 let data = match odb.find(&oid, &mut buf) { 175 Ok(data) => data, 176 Err(_) => return Ok(None), 177 }; 178 if data.kind != Kind::Commit { 179 return Ok(None); 180 } 181 let commit = CommitRef::from_bytes(data.data, kind) 182 .map_err(|error| MaintError::CommitGraph(error.to_string()))?; 183 let tree = commit.tree(); 184 let parents: Vec<ObjectId> = commit.parents().collect(); 185 let seconds = UnixSeconds::new(commit.committer().map(|sig| sig.seconds()).unwrap_or(0)); 186 parents.iter().for_each(|parent| stack.push(*parent)); 187 commits.insert( 188 oid, 189 CommitMeta { 190 tree, 191 parents, 192 seconds, 193 }, 194 ); 195 } 196 Ok(Some(commits)) 197} 198 199fn peel_to_commit( 200 odb: &gix::odb::Handle, 201 oid: ObjectId, 202 kind: gix::hash::Kind, 203 depth: usize, 204) -> Option<ObjectId> { 205 if depth == 0 { 206 return None; 207 } 208 let mut buf = Vec::new(); 209 let data = odb.find(&oid, &mut buf).ok()?; 210 match data.kind { 211 Kind::Commit => Some(oid), 212 Kind::Tag => { 213 let target = TagRefIter::from_bytes(data.data, kind).target_id().ok()?; 214 peel_to_commit(odb, target, kind, depth - 1) 215 } 216 _ => None, 217 } 218} 219 220fn serialize( 221 commits: &HashMap<ObjectId, CommitMeta>, 222 blooms: &HashMap<ObjectId, ChangedPathFilter>, 223 kind: gix::hash::Kind, 224) -> Vec<u8> { 225 let hash_len = match kind { 226 gix::hash::Kind::Sha256 => 32, 227 _ => 20, 228 }; 229 let mut oids: Vec<ObjectId> = commits.keys().copied().collect(); 230 oids.sort(); 231 let position: HashMap<ObjectId, GraphPosition> = oids 232 .iter() 233 .enumerate() 234 .map(|(index, oid)| (*oid, GraphPosition::new(index as u32))) 235 .collect(); 236 let generations = generations(commits, &oids); 237 let corrected = corrected_dates(commits, &oids); 238 239 let mut edges: Vec<EdgeField> = Vec::new(); 240 let mut cdat: Vec<u8> = Vec::with_capacity(oids.len() * (hash_len + 16)); 241 oids.iter().for_each(|oid| { 242 let meta = &commits[oid]; 243 cdat.extend_from_slice(meta.tree.as_slice()); 244 let parents = parent_fields(&meta.parents, &position, &mut edges); 245 cdat.extend_from_slice(&parents.first.to_be_bytes()); 246 cdat.extend_from_slice(&parents.second.to_be_bytes()); 247 let generation = generations 248 .get(oid) 249 .copied() 250 .unwrap_or(Generation::new(0)) 251 .get() as u64; 252 let date = (meta.seconds.get().max(0) as u64) & 0x3_FFFF_FFFF; 253 let packed = (generation << 34) | date; 254 cdat.extend_from_slice(&packed.to_be_bytes()); 255 }); 256 257 let (gda2, overflow) = oids.iter().fold( 258 (Vec::with_capacity(oids.len() * 4), Vec::<u64>::new()), 259 |(mut bytes, mut ovf), oid| { 260 let date = commits[oid].seconds.get().max(0) as u64; 261 let offset = corrected 262 .get(oid) 263 .copied() 264 .unwrap_or(CorrectedDate::new(date)) 265 .get() 266 .saturating_sub(date); 267 let packed = if offset > CORRECTED_OFFSET_MAX { 268 let index = ovf.len() as u32; 269 ovf.push(offset); 270 CORRECTED_OFFSET_OVERFLOW | index 271 } else { 272 offset as u32 273 }; 274 bytes.extend_from_slice(&packed.to_be_bytes()); 275 (bytes, ovf) 276 }, 277 ); 278 let gdo2: Vec<u8> = overflow 279 .iter() 280 .flat_map(|value| value.to_be_bytes()) 281 .collect(); 282 283 let bidx: Vec<u8> = oids 284 .iter() 285 .scan(0u32, |acc, oid| { 286 *acc = acc.saturating_add(filter_for(blooms, oid).len_bytes() as u32); 287 Some(acc.to_be_bytes()) 288 }) 289 .flatten() 290 .collect(); 291 let bdat: Vec<u8> = [BLOOM_HASH_VERSION, BLOOM_NUM_HASHES, BLOOM_BITS_PER_ENTRY] 292 .iter() 293 .flat_map(|value| value.to_be_bytes()) 294 .chain( 295 oids.iter() 296 .flat_map(|oid| filter_for(blooms, oid).bytes().iter().copied()), 297 ) 298 .collect(); 299 300 let oidf = fanout(&oids); 301 let mut oidl: Vec<u8> = Vec::with_capacity(oids.len() * hash_len); 302 oids.iter() 303 .for_each(|oid| oidl.extend_from_slice(oid.as_slice())); 304 let mut edge_bytes: Vec<u8> = Vec::with_capacity(edges.len() * 4); 305 edges 306 .iter() 307 .for_each(|edge| edge_bytes.extend_from_slice(&edge.to_be_bytes())); 308 309 let mut chunks: Vec<(&[u8; 4], Vec<u8>)> = vec![ 310 (b"OIDF", oidf), 311 (b"OIDL", oidl), 312 (b"CDAT", cdat), 313 (b"GDA2", gda2), 314 ]; 315 if !gdo2.is_empty() { 316 chunks.push((b"GDO2", gdo2)); 317 } 318 if !edge_bytes.is_empty() { 319 chunks.push((b"EDGE", edge_bytes)); 320 } 321 chunks.push((b"BIDX", bidx)); 322 chunks.push((b"BDAT", bdat)); 323 324 assemble(chunks, kind) 325} 326 327fn filter_for<'a>( 328 blooms: &'a HashMap<ObjectId, ChangedPathFilter>, 329 oid: &ObjectId, 330) -> &'a ChangedPathFilter { 331 static EMPTY: ChangedPathFilter = ChangedPathFilter(Vec::new()); 332 blooms.get(oid).unwrap_or(&EMPTY) 333} 334 335fn parent_fields( 336 parents: &[ObjectId], 337 position: &HashMap<ObjectId, GraphPosition>, 338 edges: &mut Vec<EdgeField>, 339) -> ParentFields { 340 let pos = |oid: &ObjectId| ParentField::from_position(position.get(oid).copied()); 341 match parents { 342 [] => ParentFields { 343 first: ParentField::NONE, 344 second: ParentField::NONE, 345 }, 346 [first] => ParentFields { 347 first: pos(first), 348 second: ParentField::NONE, 349 }, 350 [first, second] => ParentFields { 351 first: pos(first), 352 second: pos(second), 353 }, 354 [first, rest @ ..] => { 355 let edge_index = edges.len(); 356 let last = rest.len() - 1; 357 rest.iter().enumerate().for_each(|(index, parent)| { 358 edges.push(EdgeField::new(pos(parent), index == last)); 359 }); 360 ParentFields { 361 first: pos(first), 362 second: ParentField::extra_edges(edge_index), 363 } 364 } 365 } 366} 367 368fn fanout(oids: &[ObjectId]) -> Vec<u8> { 369 let mut buckets = [0u32; 256]; 370 oids.iter() 371 .for_each(|oid| buckets[oid.as_slice()[0] as usize] += 1); 372 (1..256).for_each(|index| buckets[index] += buckets[index - 1]); 373 buckets 374 .iter() 375 .flat_map(|count| count.to_be_bytes()) 376 .collect() 377} 378 379fn resolve_topo<V: Copy>( 380 commits: &HashMap<ObjectId, CommitMeta>, 381 oids: &[ObjectId], 382 transform: impl Fn(&[V], &CommitMeta) -> V, 383) -> HashMap<ObjectId, V> { 384 let mut value: HashMap<ObjectId, V> = HashMap::new(); 385 oids.iter().for_each(|root| { 386 if value.contains_key(root) { 387 return; 388 } 389 let mut stack = vec![*root]; 390 while let Some(top) = stack.last().copied() { 391 if value.contains_key(&top) { 392 stack.pop(); 393 continue; 394 } 395 let parents = &commits[&top].parents; 396 let unresolved: Vec<ObjectId> = parents 397 .iter() 398 .filter(|parent| commits.contains_key(*parent) && !value.contains_key(*parent)) 399 .copied() 400 .collect(); 401 if unresolved.is_empty() { 402 let resolved: Vec<V> = parents 403 .iter() 404 .filter_map(|parent| value.get(parent)) 405 .copied() 406 .collect(); 407 let computed = transform(&resolved, &commits[&top]); 408 value.insert(top, computed); 409 stack.pop(); 410 } else { 411 unresolved.into_iter().for_each(|parent| stack.push(parent)); 412 } 413 } 414 }); 415 value 416} 417 418fn generations( 419 commits: &HashMap<ObjectId, CommitMeta>, 420 oids: &[ObjectId], 421) -> HashMap<ObjectId, Generation> { 422 resolve_topo(commits, oids, |parents: &[Generation], _meta| { 423 parents 424 .iter() 425 .copied() 426 .max() 427 .unwrap_or(Generation::new(0)) 428 .succ() 429 .min(Generation::new(GRAPH_GENERATION_MAX)) 430 }) 431} 432 433fn corrected_dates( 434 commits: &HashMap<ObjectId, CommitMeta>, 435 oids: &[ObjectId], 436) -> HashMap<ObjectId, CorrectedDate> { 437 resolve_topo(commits, oids, |parents: &[CorrectedDate], meta| { 438 let max_parent = parents.iter().map(|date| date.get()).max().unwrap_or(0); 439 let date = meta.seconds.get().max(0) as u64; 440 let base = if date > max_parent { 441 date - 1 442 } else { 443 max_parent 444 }; 445 CorrectedDate::new(base + 1) 446 }) 447} 448 449fn tuned(handle: &gix::odb::Handle) -> gix::odb::Handle { 450 let mut odb = handle.clone(); 451 odb.refresh_never(); 452 odb.prevent_pack_unload(); 453 odb 454} 455 456fn one_filter( 457 odb: &gix::odb::Handle, 458 commits: &HashMap<ObjectId, CommitMeta>, 459 meta: &CommitMeta, 460 kind: gix::hash::Kind, 461) -> Result<ChangedPathFilter, MaintError> { 462 let parent_tree = meta 463 .parents 464 .first() 465 .and_then(|parent| commits.get(parent)) 466 .map(|found| found.tree); 467 let changed = diff_trees(odb, parent_tree, Some(meta.tree), kind)?; 468 Ok(build_filter(&changed)) 469} 470 471fn changed_path_filters( 472 repo: &Repo, 473 commits: &HashMap<ObjectId, CommitMeta>, 474 kind: gix::hash::Kind, 475) -> Result<HashMap<ObjectId, ChangedPathFilter>, MaintError> { 476 let entries: Vec<(&ObjectId, &CommitMeta)> = commits.iter().collect(); 477 let path = repo.path().to_owned(); 478 let produced = knot_resource::map_chunks(&entries, |batch| { 479 let local = Repo::open(&path)?; 480 let odb = tuned(&local.git().objects); 481 batch 482 .iter() 483 .map(|(oid, meta)| Ok((**oid, one_filter(&odb, commits, meta, kind)?))) 484 .collect::<Result<Vec<_>, MaintError>>() 485 })?; 486 Ok(produced.into_iter().collect()) 487} 488 489fn tree_entries( 490 odb: &gix::odb::Handle, 491 oid: Option<ObjectId>, 492 kind: gix::hash::Kind, 493) -> Result<HashMap<Vec<u8>, (TreeEntryKind, ObjectId)>, MaintError> { 494 let Some(oid) = oid else { 495 return Ok(HashMap::new()); 496 }; 497 if oid == ObjectId::empty_tree(kind) { 498 return Ok(HashMap::new()); 499 } 500 let mut buf = Vec::new(); 501 let data = odb 502 .find(&oid, &mut buf) 503 .map_err(|error| MaintError::CommitGraph(error.to_string()))?; 504 if data.kind != Kind::Tree { 505 return Ok(HashMap::new()); 506 } 507 let tree = TreeRef::from_bytes(data.data, kind) 508 .map_err(|error| MaintError::CommitGraph(error.to_string()))?; 509 Ok(tree 510 .entries 511 .into_iter() 512 .map(|entry| { 513 ( 514 entry.filename.to_vec(), 515 (entry.mode.kind(), entry.oid.to_owned()), 516 ) 517 }) 518 .collect()) 519} 520 521fn diff_trees( 522 odb: &gix::odb::Handle, 523 parent: Option<ObjectId>, 524 commit: Option<ObjectId>, 525 kind: gix::hash::Kind, 526) -> Result<Vec<Vec<u8>>, MaintError> { 527 let is_tree = |kind: &TreeEntryKind| matches!(kind, TreeEntryKind::Tree); 528 let mut out: Vec<Vec<u8>> = Vec::new(); 529 let mut stack: Vec<(Option<ObjectId>, Option<ObjectId>, Vec<u8>)> = 530 vec![(parent, commit, Vec::new())]; 531 while let Some((parent, commit, prefix)) = stack.pop() { 532 let parent_entries = tree_entries(odb, parent, kind)?; 533 let commit_entries = tree_entries(odb, commit, kind)?; 534 let names: HashSet<&Vec<u8>> = parent_entries.keys().chain(commit_entries.keys()).collect(); 535 names.into_iter().for_each(|name| { 536 let full: Vec<u8> = prefix.iter().copied().chain(name.iter().copied()).collect(); 537 let subprefix = 538 || -> Vec<u8> { full.iter().copied().chain(std::iter::once(b'/')).collect() }; 539 match (parent_entries.get(name), commit_entries.get(name)) { 540 (None, Some((ck, co))) => { 541 if is_tree(ck) { 542 stack.push((None, Some(*co), subprefix())); 543 } else { 544 out.push(full); 545 } 546 } 547 (Some((pk, po)), None) => { 548 if is_tree(pk) { 549 stack.push((Some(*po), None, subprefix())); 550 } else { 551 out.push(full); 552 } 553 } 554 (Some((pk, po)), Some((ck, co))) => match (is_tree(pk), is_tree(ck)) { 555 (true, true) => { 556 if po != co { 557 stack.push((Some(*po), Some(*co), subprefix())); 558 } 559 } 560 (false, false) => { 561 if po != co || pk != ck { 562 out.push(full); 563 } 564 } 565 (true, false) => { 566 stack.push((Some(*po), None, subprefix())); 567 out.push(full); 568 } 569 (false, true) => { 570 stack.push((None, Some(*co), subprefix())); 571 out.push(full); 572 } 573 }, 574 (None, None) => {} 575 } 576 }); 577 } 578 Ok(out) 579} 580 581fn build_filter(changed: &[Vec<u8>]) -> ChangedPathFilter { 582 if changed.len() > BLOOM_MAX_CHANGED_PATHS { 583 // `0xFF` = git's "too many changed paths" marker. 584 return ChangedPathFilter(vec![0xFF]); 585 } 586 let paths: HashSet<Vec<u8>> = changed.iter().flat_map(|path| prefixes(path)).collect(); 587 if paths.len() > BLOOM_MAX_CHANGED_PATHS { 588 return ChangedPathFilter(vec![0xFF]); 589 } 590 let bits = paths.len() * BLOOM_BITS_PER_ENTRY as usize; 591 let len_bytes = bits.div_ceil(8).max(1); 592 let modulus = (len_bytes * 8) as u64; 593 let data = paths.iter().fold(vec![0u8; len_bytes], |mut data, path| { 594 let hash0 = murmur3(BLOOM_SEED0, path); 595 let hash1 = murmur3(BLOOM_SEED1, path); 596 (0..BLOOM_NUM_HASHES).for_each(|index| { 597 let combined = hash0.wrapping_add(index.wrapping_mul(hash1)); 598 let position = (combined as u64) % modulus; 599 data[(position / 8) as usize] |= 1 << (position % 8); 600 }); 601 data 602 }); 603 ChangedPathFilter(data) 604} 605 606fn prefixes(path: &[u8]) -> Vec<Vec<u8>> { 607 std::iter::once(path.to_vec()) 608 .chain( 609 path.iter() 610 .enumerate() 611 .filter(|(_, byte)| **byte == b'/') 612 .map(|(index, _)| path[..index].to_vec()), 613 ) 614 .collect() 615} 616 617fn murmur3(seed: u32, data: &[u8]) -> u32 { 618 let body = data.len() / 4; 619 let mixed = (0..body).fold(seed, |seed, index| { 620 let base = index * 4; 621 let block = 622 u32::from_le_bytes([data[base], data[base + 1], data[base + 2], data[base + 3]]); 623 let block = block 624 .wrapping_mul(MURMUR_C1) 625 .rotate_left(15) 626 .wrapping_mul(MURMUR_C2); 627 (seed ^ block) 628 .rotate_left(13) 629 .wrapping_mul(5) 630 .wrapping_add(MURMUR_N) 631 }); 632 let tail = &data[body * 4..]; 633 let tail_key = tail.iter().enumerate().fold(0u32, |key, (index, byte)| { 634 key | ((*byte as u32) << (8 * index)) 635 }); 636 let mixed = if tail.is_empty() { 637 mixed 638 } else { 639 mixed 640 ^ tail_key 641 .wrapping_mul(MURMUR_C1) 642 .rotate_left(15) 643 .wrapping_mul(MURMUR_C2) 644 }; 645 let mixed = mixed ^ (data.len() as u32); 646 let mixed = (mixed ^ (mixed >> 16)).wrapping_mul(0x85eb_ca6b); 647 let mixed = (mixed ^ (mixed >> 13)).wrapping_mul(0xc2b2_ae35); 648 mixed ^ (mixed >> 16) 649} 650 651fn assemble(chunks: Vec<(&[u8; 4], Vec<u8>)>, kind: gix::hash::Kind) -> Vec<u8> { 652 let num_chunks = chunks.len() as u8; 653 let table_len = (chunks.len() + 1) * 12; 654 let data_start = 8 + table_len; 655 let hash_version = match kind { 656 gix::hash::Kind::Sha256 => 2u8, 657 _ => 1u8, 658 }; 659 660 let mut out: Vec<u8> = Vec::new(); 661 out.extend_from_slice(b"CGPH"); 662 out.push(1); 663 out.push(hash_version); 664 out.push(num_chunks); 665 out.push(0); 666 667 let mut offset = data_start as u64; 668 chunks.iter().for_each(|(id, body)| { 669 out.extend_from_slice(*id); 670 out.extend_from_slice(&offset.to_be_bytes()); 671 offset += body.len() as u64; 672 }); 673 out.extend_from_slice(&[0, 0, 0, 0]); 674 out.extend_from_slice(&offset.to_be_bytes()); 675 676 chunks 677 .iter() 678 .for_each(|(_, body)| out.extend_from_slice(body)); 679 680 let mut hasher = gix_hash::hasher(kind); 681 hasher.update(&out); 682 let checksum = hasher 683 .try_finalize() 684 .expect("commit-graph checksum finalizes"); 685 out.extend_from_slice(checksum.as_slice()); 686 out 687} 688 689#[cfg(test)] 690mod tests { 691 use super::{GRAPH_GENERATION_MAX, Generation, build_filter, murmur3}; 692 693 #[test] 694 fn succ_advances_by_one_and_orders_above_its_source() { 695 let base = Generation::new(7); 696 assert_eq!(base.succ(), Generation::new(8)); 697 assert!(base.succ() > base); 698 } 699 700 #[test] 701 fn succ_saturates_at_the_numeric_ceiling() { 702 assert_eq!(Generation::new(u32::MAX).succ(), Generation::new(u32::MAX)); 703 } 704 705 #[test] 706 fn a_child_sits_one_above_its_highest_parent() { 707 let parents = [Generation::new(2), Generation::new(5), Generation::new(3)]; 708 let child = parents.into_iter().max().unwrap().succ(); 709 assert_eq!(child, Generation::new(6)); 710 } 711 712 #[test] 713 fn clamping_holds_the_value_at_the_format_maximum() { 714 let value = Generation::new(GRAPH_GENERATION_MAX) 715 .succ() 716 .min(Generation::new(GRAPH_GENERATION_MAX)); 717 assert_eq!(value, Generation::new(GRAPH_GENERATION_MAX)); 718 } 719 720 #[test] 721 fn empty_change_set_is_a_single_zero_word() { 722 let filter = build_filter(&[]); 723 assert_eq!(filter.bytes(), &[0u8]); 724 } 725 726 #[test] 727 fn overlarge_change_set_is_a_single_saturated_word() { 728 let many: Vec<Vec<u8>> = (0..600).map(|n| format!("p{n}").into_bytes()).collect(); 729 let filter = build_filter(&many); 730 assert_eq!(filter.bytes(), &[0xFFu8]); 731 } 732 733 #[test] 734 fn murmur3_matches_known_vectors() { 735 assert_eq!(murmur3(0, b""), 0); 736 assert_eq!(murmur3(0, b"hello"), 0x248bfa47); 737 } 738 739 #[test] 740 fn diff_trees_walks_a_deeply_nested_tree_without_overflowing_the_stack() { 741 use knot_git::{EntryKind, Repo, StagedAction, StagedChange}; 742 use knot_types::Oid; 743 744 let depth = 4000usize; 745 let dir = tempfile::tempdir().unwrap(); 746 let git_dir = dir.path().join("deep.git"); 747 748 let build_dir = git_dir.clone(); 749 let tree = std::thread::Builder::new() 750 .stack_size(64 * 1024 * 1024) 751 .spawn(move || { 752 let repo = Repo::create(&build_dir).unwrap(); 753 let kind = repo.object_format().kind(); 754 let base = Oid::from(gix::ObjectId::empty_tree(kind)); 755 let path = (0..depth) 756 .map(|_| "d") 757 .chain(std::iter::once("leaf.txt")) 758 .collect::<Vec<_>>() 759 .join("/"); 760 repo.write_staged_tree( 761 base, 762 &[StagedChange { 763 path: knot_types::RepoPath::new(path).unwrap(), 764 action: StagedAction::Put { 765 content: b"leaf\n".to_vec(), 766 kind: EntryKind::Blob, 767 }, 768 }], 769 ) 770 .unwrap() 771 .object_id() 772 }) 773 .unwrap() 774 .join() 775 .unwrap(); 776 777 let changed = std::thread::Builder::new() 778 .stack_size(256 * 1024) 779 .spawn(move || { 780 let repo = Repo::open(&git_dir).unwrap(); 781 let kind = repo.object_format().kind(); 782 super::diff_trees(&repo.git().objects, None, Some(tree), kind).unwrap() 783 }) 784 .unwrap() 785 .join() 786 .expect("diff_trees on a 256 KiB stack mustn't overflow on a 4000-deep tree"); 787 788 assert_eq!(changed.len(), 1, "the single leaf is the only changed path"); 789 assert_eq!( 790 changed[0].iter().filter(|byte| **byte == b'/').count(), 791 depth, 792 "the changed path retains every nesting level" 793 ); 794 } 795}