1use std::collections::{BTreeMap, BTreeSet};
12
13use crate::cache::{CacheError, ObjectCache};
14use crate::extract::Extractor;
15use crate::git::{GitError, Repo};
16use crate::store::StoreError;
17use crate::{Edge, EdgeKind, FactSet, NodeKind, Store};
18
19#[derive(Debug, thiserror::Error)]
21pub enum SyncError {
22 #[error(transparent)]
24 Store(#[from] StoreError),
25 #[error(transparent)]
27 Cache(#[from] CacheError),
28 #[error(transparent)]
30 Git(#[from] GitError),
31}
32
33#[derive(Debug, Clone, Default, serde::Serialize)]
35pub struct SyncReport {
36 pub tree: String,
38 pub no_op: bool,
40 pub blobs_total: usize,
42 pub blobs_extracted: usize,
44 pub blobs_cached: usize,
46 pub nodes: u64,
48 pub edges: u64,
50}
51
52pub fn sync(
59 store: &mut Store,
60 repo: &Repo,
61 cache: &ObjectCache,
62 extractor: &dyn Extractor,
63) -> Result<SyncReport, SyncError> {
64 let tree = repo.head_tree_id()?;
65
66 if store.sync_state()?.as_deref() == Some(tree.as_str()) {
67 return Ok(SyncReport {
68 no_op: true,
69 nodes: store.node_count()?,
70 edges: store.edge_count()?,
71 tree,
72 ..SyncReport::default()
73 });
74 }
75
76 let blobs = repo.walk_blobs()?;
77 let mut assembled = FactSet::new();
78 let mut extracted = 0usize;
79 let mut cached = 0usize;
80
81 for blob in &blobs {
82 let key = cache_key(&blob.path, &blob.oid);
89 let facts = if let Some(facts) = cache.get(&key)? {
90 cached += 1;
91 facts
92 } else {
93 let bytes = repo.read_blob(&blob.oid)?;
94 let facts = extractor.extract(&blob.path, &blob.oid, &bytes);
95 cache.put(&key, &facts)?;
96 extracted += 1;
97 facts
98 };
99 let FactSet { nodes, edges } = facts;
100 assembled.nodes.extend(nodes);
101 assembled.edges.extend(edges);
102 }
103
104 resolve_calls(&mut assembled);
105 store.rebuild(&assembled, &tree)?;
106
107 Ok(SyncReport {
108 no_op: false,
109 blobs_total: blobs.len(),
110 blobs_extracted: extracted,
111 blobs_cached: cached,
112 nodes: store.node_count()?,
113 edges: store.edge_count()?,
114 tree,
115 })
116}
117
118fn resolve_calls(facts: &mut FactSet) {
127 let mut by_name: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
129 for n in &facts.nodes {
130 if n.kind == NodeKind::Fn {
131 by_name
132 .entry(n.name.as_str())
133 .or_default()
134 .push(n.key.as_str());
135 }
136 }
137
138 let mut resolved: BTreeSet<(String, String)> = BTreeSet::new();
140 for n in &facts.nodes {
141 if n.kind != NodeKind::Fn {
142 continue;
143 }
144 let Some(calls) = n.meta.get("calls").and_then(|v| v.as_array()) else {
145 continue;
146 };
147 for callee in calls.iter().filter_map(|v| v.as_str()) {
148 if let Some(targets) = by_name.get(callee)
149 && targets.len() == 1
150 {
151 resolved.insert((n.key.clone(), targets[0].to_owned()));
152 }
153 }
154 }
155
156 for (src, dst) in resolved {
157 facts.edges.push(Edge::derived(src, dst, EdgeKind::Calls));
158 }
159}
160
161fn cache_key(path: &str, oid: &str) -> String {
166 format!("{oid}-{:016x}", fnv1a64(path.as_bytes()))
167}
168
169fn fnv1a64(bytes: &[u8]) -> u64 {
172 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
173 for &b in bytes {
174 hash ^= u64::from(b);
175 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
176 }
177 hash
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{cache_key, resolve_calls};
183 use crate::{EdgeKind, FactSet, Node, NodeKind};
184
185 fn fn_node(key: &str, name: &str, calls: &[&str]) -> Node {
186 let mut n = Node::new(key, NodeKind::Fn, name);
187 if !calls.is_empty() {
188 n.meta = serde_json::json!({ "calls": calls });
189 }
190 n
191 }
192
193 #[test]
194 fn resolve_calls_links_unique_names_only() {
195 let mut fs = FactSet::new()
196 .with_node(fn_node(
197 "sym:rust:a.rs#caller",
198 "caller",
199 &["target", "dup", "missing"],
200 ))
201 .with_node(fn_node("sym:rust:a.rs#target", "target", &[]))
202 .with_node(fn_node("sym:rust:a.rs#dup", "dup", &[]))
204 .with_node(fn_node("sym:rust:b.rs#dup", "dup", &[]));
205
206 resolve_calls(&mut fs);
207
208 let calls: Vec<_> = fs
209 .edges
210 .iter()
211 .filter(|e| e.kind == EdgeKind::Calls)
212 .collect();
213 assert_eq!(
214 calls.len(),
215 1,
216 "only the unambiguous, known callee is linked"
217 );
218 assert_eq!(calls[0].src, "sym:rust:a.rs#caller");
219 assert_eq!(calls[0].dst, "sym:rust:a.rs#target");
220 }
221
222 #[test]
223 fn cache_key_separates_paths_but_is_stable() {
224 let oid = "abc123";
225 assert_eq!(cache_key("src/a.rs", oid), cache_key("src/a.rs", oid));
227 assert_ne!(cache_key("src/a.rs", oid), cache_key("src/b.rs", oid));
229 assert_ne!(cache_key("src/a.rs", "aaa"), cache_key("src/a.rs", "bbb"));
231 assert!(cache_key("src/a.rs", oid).starts_with("abc123-"));
233 }
234}