gix_commitgraph/
verify.rs1use gix_error::Result;
3use std::{
4 cmp::{max, min},
5 collections::BTreeMap,
6};
7
8use gix_error::{ResultExt, bail};
9
10use crate::{
11 GENERATION_NUMBER_MAX, Graph, Position,
12 file::{self},
13};
14
15#[derive(Clone, Debug, Eq, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
18pub struct Outcome {
19 pub longest_path_length: Option<u32>,
25 pub num_commits: u32,
27 pub parent_counts: BTreeMap<u32, u32>,
29}
30
31impl Graph {
32 pub fn verify_integrity<E>(
36 &self,
37 mut processor: impl FnMut(&file::Commit<'_>) -> std::result::Result<(), E>,
38 ) -> Result<Outcome>
39 where
40 E: std::error::Error + Send + Sync + 'static,
41 {
42 if self.files.len() > 256 {
43 bail!(
45 "Commit-graph should be composed of at most 256 files but actually contains {} files".corrupted(),
46 self.files.len()
47 );
48 }
49
50 let mut stats = Outcome {
51 longest_path_length: None,
52 num_commits: 0,
53 parent_counts: BTreeMap::new(),
54 };
55 let mut max_generation = 0u32;
56
57 let mut file_start_pos = Position(0);
61 for (file_index, file) in self.files.iter().enumerate() {
62 if usize::from(file.base_graph_count()) != file_index {
63 bail!(
64 "\"{}\" should have {} base graphs, but claims {} base graphs".corrupted(),
65 file.path().display(),
66 file_index,
67 file.base_graph_count()
68 );
69 }
70
71 for (base_graph_index, (expected, actual)) in self
72 .files
73 .iter()
74 .take(file_index)
75 .map(crate::File::checksum)
76 .zip(file.iter_base_graph_ids())
77 .enumerate()
78 {
79 if actual != expected {
80 bail!(
81 "\"{}\" base graph at index {} should have ID {} but is {}".corrupted(),
82 file.path().display(),
83 base_graph_index,
84 expected,
85 actual
86 );
87 }
88 }
89
90 let next_file_start_pos = Position(file_start_pos.0 + file.num_commits());
91 let file_stats = file.traverse(|commit| {
92 let mut max_parent_generation = 0u32;
93 let mut has_uncomputed_parent_generation = false;
94 for parent_pos in commit.iter_parents() {
95 let parent_pos = parent_pos?;
96 if parent_pos >= next_file_start_pos {
97 bail!(
98 "Commit {} has parent position {parent_pos} that is out of range (should be in range 0-{})".corrupted(),
99 commit.id(),
100 Position(next_file_start_pos.0 - 1)
101 );
102 }
103 let parent = self.commit_at(parent_pos);
104 max_parent_generation = max(max_parent_generation, parent.generation());
105 has_uncomputed_parent_generation |= parent.generation() == 0;
106 }
107
108 if commit.generation() == 0 || has_uncomputed_parent_generation {
110 bail!(
111 "Cannot verify generation numbers for commit {} because it or a parent has an uncomputed generation".unsupported(),
112 commit.id()
113 );
114 }
115
116 let expected_generation = min(max_parent_generation + 1, GENERATION_NUMBER_MAX);
119 if commit.generation() != expected_generation {
120 bail!(
121 "Commit {}'s generation should be {expected_generation} but is {}".corrupted(),
122 commit.id(),
123 commit.generation()
124 );
125 }
126
127 processor(commit).or_error()?;
128
129 Ok(())
130 })?;
131
132 max_generation = max(max_generation, file_stats.max_generation);
133 stats.num_commits += file_stats.num_commits;
134 for (key, value) in file_stats.parent_counts.into_iter() {
135 *stats.parent_counts.entry(key).or_insert(0) += value;
136 }
137 file_start_pos = next_file_start_pos;
138 }
139
140 stats.longest_path_length = if max_generation < GENERATION_NUMBER_MAX {
141 Some(max_generation.saturating_sub(1))
142 } else {
143 None
144 };
145 Ok(stats)
146 }
147}