void_graph/
void_backend.rs1use std::collections::VecDeque;
7use std::sync::Arc;
8
9use camino::{Utf8Path, Utf8PathBuf};
10use thiserror::Error;
11use void_core::{
12 cid as void_cid,
13 crypto::KeyVault,
14 metadata::Commit,
15 refs::{self, HeadRef},
16 store::{FsStore, ObjectStoreExt},
17 support::ToVoidCid,
18};
19
20#[derive(Debug, Error)]
22pub enum VoidBackendError {
23 #[error("repository not found at {0}")]
24 NotFound(String),
25
26 #[error("invalid key: {0}")]
27 InvalidKey(String),
28
29 #[error("invalid CID: {0}")]
30 InvalidCid(String),
31
32 #[error("decryption failed: {0}")]
33 Decryption(String),
34
35 #[error("io error: {0}")]
36 Io(#[from] std::io::Error),
37
38 #[error("void error: {0}")]
39 Void(#[from] void_core::VoidError),
40}
41
42pub type Result<T> = std::result::Result<T, VoidBackendError>;
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub struct CommitCid(pub String);
48
49impl CommitCid {
50 pub fn short(&self) -> &str {
52 &self.0[..self.0.len().min(8)]
53 }
54}
55
56#[derive(Debug, Clone)]
58pub enum VoidHead {
59 Branch(String),
61 Detached(CommitCid),
63}
64
65#[derive(Debug, Clone)]
67pub struct VoidRef {
68 pub name: String,
70 pub kind: RefKind,
72 pub target: CommitCid,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum RefKind {
79 Branch,
81 Tag,
83}
84
85#[derive(Debug, Clone)]
87pub struct VoidCommit {
88 pub cid: CommitCid,
90 pub parents: Vec<CommitCid>,
92 pub message: String,
94 pub timestamp_ms: u64,
96 pub is_signed: bool,
98 pub signature_valid: Option<bool>,
100 pub author: Option<String>,
102}
103
104#[derive(Debug, Clone, Copy, Default)]
106pub enum SortOrder {
107 #[default]
109 Chronological,
110 Topological,
112}
113
114pub struct VoidRepository {
116 void_dir: Utf8PathBuf,
118 vault: Arc<KeyVault>,
120 store: FsStore,
122}
123
124impl VoidRepository {
125 pub fn open(path: &Utf8Path, vault: Arc<KeyVault>) -> Result<Self> {
131 let void_dir = find_void_dir(path)?;
132 let store = FsStore::new(void_dir.join("objects"))?;
133 Ok(Self {
134 void_dir,
135 vault,
136 store,
137 })
138 }
139
140 pub fn head(&self) -> Result<Option<VoidHead>> {
142 match refs::read_head(&self.void_dir)? {
143 None => Ok(None),
144 Some(HeadRef::Symbolic(branch)) => Ok(Some(VoidHead::Branch(branch))),
145 Some(HeadRef::Detached(commit_cid)) => {
146 let cid = void_cid::from_bytes(commit_cid.as_bytes())?;
147 Ok(Some(VoidHead::Detached(CommitCid(cid.to_string()))))
148 }
149 }
150 }
151
152 pub fn refs(&self) -> Result<Vec<VoidRef>> {
154 let mut result = Vec::new();
155
156 for name in refs::list_branches(&self.void_dir)? {
157 if let Some(commit_cid) = refs::read_branch(&self.void_dir, &name)? {
158 let cid = void_cid::from_bytes(commit_cid.as_bytes())?;
159 result.push(VoidRef {
160 name,
161 kind: RefKind::Branch,
162 target: CommitCid(cid.to_string()),
163 });
164 }
165 }
166
167 for name in refs::list_tags(&self.void_dir)? {
168 if let Some(commit_cid) = refs::read_tag(&self.void_dir, &name)? {
169 let cid = void_cid::from_bytes(commit_cid.as_bytes())?;
170 result.push(VoidRef {
171 name,
172 kind: RefKind::Tag,
173 target: CommitCid(cid.to_string()),
174 });
175 }
176 }
177
178 Ok(result)
179 }
180
181 pub fn resolve_head(&self) -> Result<Option<CommitCid>> {
183 match refs::resolve_head(&self.void_dir)? {
184 None => Ok(None),
185 Some(commit_cid) => {
186 let cid = void_cid::from_bytes(commit_cid.as_bytes())?;
187 Ok(Some(CommitCid(cid.to_string())))
188 }
189 }
190 }
191
192 pub fn load_commit(&self, cid: &CommitCid) -> Result<VoidCommit> {
194 let cid_obj = void_cid::parse(&cid.0)?;
195 let encrypted: void_core::crypto::EncryptedCommit = self.store.get_blob(&cid_obj)?;
196
197 let (decrypted, _reader) = void_core::crypto::CommitReader::open_with_vault(&self.vault, &encrypted)
199 .map_err(|e| VoidBackendError::Decryption(e.to_string()))?;
200
201 let commit: Commit = decrypted.parse()?;
203
204 let is_signed = commit.is_signed();
206 let signature_valid = if is_signed {
207 Some(commit.verify().unwrap_or(false))
208 } else {
209 None
210 };
211
212 let parents = commit
214 .parents
215 .iter()
216 .map(|p| {
217 let parent_cid = p.to_void_cid()?;
218 Ok(CommitCid(parent_cid.to_string()))
219 })
220 .collect::<Result<Vec<_>>>()?;
221
222 let author = commit.author.map(|a| a.to_hex());
224
225 Ok(VoidCommit {
226 cid: cid.clone(),
227 parents,
228 timestamp_ms: commit.timestamp,
229 message: commit.message,
230 is_signed,
231 signature_valid,
232 author,
233 })
234 }
235
236 pub fn walk_commits(
240 &self,
241 starts: &[CommitCid],
242 order: SortOrder,
243 limit: Option<usize>,
244 ) -> Result<Vec<VoidCommit>> {
245 let mut visited = rustc_hash::FxHashSet::default();
246 let mut commits = Vec::new();
247 let mut queue: VecDeque<CommitCid> = starts.iter().cloned().collect();
248
249 while let Some(cid) = queue.pop_front() {
250 if visited.contains(&cid) {
251 continue;
252 }
253 visited.insert(cid.clone());
254
255 let commit = self.load_commit(&cid)?;
256
257 for parent in &commit.parents {
259 if !visited.contains(parent) {
260 queue.push_back(parent.clone());
261 }
262 }
263
264 commits.push(commit);
265
266 if let Some(max) = limit {
268 if commits.len() >= max {
269 break;
270 }
271 }
272 }
273
274 match order {
276 SortOrder::Chronological => {
277 commits.sort_by(|a, b| b.timestamp_ms.cmp(&a.timestamp_ms));
278 }
279 SortOrder::Topological => {
280 }
282 }
283
284 Ok(commits)
285 }
286}
287
288fn find_void_dir(start: &Utf8Path) -> Result<Utf8PathBuf> {
290 let mut current = start.to_path_buf();
291 loop {
292 let void_dir = current.join(".void");
293 if void_dir.exists() {
294 return Ok(void_dir);
295 }
296 if !current.pop() {
297 return Err(VoidBackendError::NotFound(start.to_string()));
298 }
299 }
300}