weavatrix_git/commit_graph/
mod.rs1mod bloom;
2mod format;
3
4use std::{fs, path::Path};
5
6use crate::{HashKind, ObjectId, Result, error::invalid};
7use format::Layer;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum PathBloom {
11 DefinitelyNot,
12 Maybe,
13}
14
15pub(crate) struct CommitGraph {
16 hash: HashKind,
17 layers: Vec<Layer>,
18}
19
20pub(crate) struct GraphCommit {
21 pub(crate) id: ObjectId,
22 pub(crate) tree: ObjectId,
23 pub(crate) parents: Vec<ObjectId>,
24 pub(crate) time: i64,
25}
26
27impl CommitGraph {
28 pub(crate) fn open(common_dir: &Path, hash: HashKind) -> Result<Option<Self>> {
29 let info = common_dir.join("objects").join("info");
30 let monolithic = info.join("commit-graph");
31 if monolithic.is_file() {
32 let Some(layer) = Layer::open(&monolithic, hash, 0, &[], None)? else {
33 return Ok(None);
34 };
35 return Ok(Some(Self {
36 hash,
37 layers: vec![layer],
38 }));
39 }
40 let directory = info.join("commit-graphs");
41 let chain = match fs::read_to_string(directory.join("commit-graph-chain")) {
42 Ok(chain) => chain,
43 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
44 Err(error) => return Err(error.into()),
45 };
46 let ids = chain
47 .lines()
48 .filter(|line| !line.is_empty())
49 .map(|line| ObjectId::from_hex_for(line, hash))
50 .collect::<Result<Vec<_>>>()?;
51 if ids.is_empty() || ids.len() > 64 {
52 return Err(invalid("commit-graph chain length is invalid"));
53 }
54 let mut layers = Vec::with_capacity(ids.len());
55 let mut base_count = 0;
56 for (index, id) in ids.iter().enumerate() {
57 let path = directory.join(format!("graph-{}.graph", id.to_hex()));
58 let layer = Layer::open(&path, hash, base_count, &ids[..index], Some(*id))?
59 .ok_or_else(|| invalid("commit-graph chain hash kind mismatch"))?;
60 base_count = base_count
61 .checked_add(layer.count())
62 .ok_or_else(|| invalid("commit-graph chain count overflow"))?;
63 layers.push(layer);
64 }
65 Ok(Some(Self { hash, layers }))
66 }
67
68 pub(crate) fn find(&self, id: ObjectId) -> Result<Option<GraphCommit>> {
69 if id.kind() != self.hash {
70 return Ok(None);
71 }
72 for (layer_index, layer) in self.layers.iter().enumerate().rev() {
73 if let Some(position) = layer.find_position(id)? {
74 return self.entry(layer_index, position).map(Some);
75 }
76 }
77 Ok(None)
78 }
79
80 pub(crate) const fn layer_count(&self) -> usize {
81 self.layers.len()
82 }
83
84 pub(crate) fn changed_path(&self, id: ObjectId, path: &[u8]) -> Result<Option<PathBloom>> {
85 if path.is_empty() || path.contains(&0) {
86 return Err(invalid("Bloom query path is empty or contains NUL"));
87 }
88 for layer in self.layers.iter().rev() {
89 if let Some(position) = layer.find_position(id)? {
90 return layer.changed_path(position, path);
91 }
92 }
93 Ok(None)
94 }
95
96 fn entry(&self, layer_index: usize, position: usize) -> Result<GraphCommit> {
97 let layer = &self.layers[layer_index];
98 let raw = layer.raw_commit(position)?;
99 let parents = raw
100 .parents
101 .into_iter()
102 .map(|position| self.id_at_global(position))
103 .collect::<Result<Vec<_>>>()?;
104 Ok(GraphCommit {
105 id: layer.id(position)?,
106 tree: raw.tree,
107 parents,
108 time: raw.time,
109 })
110 }
111
112 fn id_at_global(&self, position: usize) -> Result<ObjectId> {
113 let layer = self
114 .layers
115 .iter()
116 .find(|layer| {
117 position >= layer.base_count()
118 && position < layer.base_count().saturating_add(layer.count())
119 })
120 .ok_or_else(|| invalid("commit-graph parent is out of chain bounds"))?;
121 layer.id(position - layer.base_count())
122 }
123}