1use std::{
2 collections::HashMap,
3 fs,
4 path::{Path, PathBuf},
5};
6
7use gitcortex_core::{
8 error::{GitCortexError, Result},
9 graph::{Edge, GraphDiff, Node, NodeId, NodeMetadata, Span},
10 schema::{EdgeKind, NodeKind, Visibility},
11};
12use ignore::gitignore::{Gitignore, GitignoreBuilder};
13use rayon::prelude::*;
14
15use crate::{
16 differ::{Differ, FileChange},
17 parser::{parser_for_path, ParseResult},
18};
19
20type FileIndexResult = Result<(
21 GraphDiff,
22 Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, )>;
30
31pub struct IncrementalIndexer {
36 repo_root: PathBuf,
37 ignorer: Gitignore,
38}
39
40impl IncrementalIndexer {
41 pub fn new(repo_root: &Path) -> Result<Self> {
45 let ignorer = build_ignorer(repo_root);
46 Ok(Self {
47 repo_root: repo_root.to_owned(),
48 ignorer,
49 })
50 }
51
52 pub fn run(&self, from_sha: Option<&str>) -> Result<(GraphDiff, String)> {
60 let differ = Differ::open(&self.repo_root)?;
61 let head_sha = differ.head_sha()?;
62
63 if from_sha.map(|s| s == head_sha).unwrap_or(false) {
64 return Ok((GraphDiff::default(), head_sha));
65 }
66
67 let supported = self.supported_extensions();
68 let changes = differ.changed_files(from_sha, &supported)?;
69
70 if changes.is_empty() {
71 return Ok((GraphDiff::default(), head_sha));
72 }
73
74 let (to_parse, to_delete): (Vec<_>, Vec<_>) = changes
75 .into_iter()
76 .partition(|c| !matches!(c, FileChange::Deleted(_)));
77
78 let per_file: Vec<FileIndexResult> = to_parse
80 .par_iter()
81 .map(|change| self.index_file(change.path()))
82 .collect();
83
84 let mut merged = GraphDiff::default();
86 let mut all_calls: Vec<(NodeId, String)> = Vec::new();
87 let mut all_uses: Vec<(NodeId, String)> = Vec::new();
88 let mut all_implements: Vec<(NodeId, String)> = Vec::new();
89 let mut all_imports: Vec<(NodeId, String)> = Vec::new();
90 let mut all_inherits: Vec<(NodeId, String)> = Vec::new();
91 let mut all_throws: Vec<(NodeId, String)> = Vec::new();
92 let mut all_annotated: Vec<(NodeId, String)> = Vec::new();
93 for result in per_file {
94 let (diff, calls, uses, implements, imports, inherits, throws, annotated) = result?;
95 merged.merge(diff);
96 all_calls.extend(calls);
97 all_uses.extend(uses);
98 all_implements.extend(implements);
99 all_imports.extend(imports);
100 all_inherits.extend(inherits);
101 all_throws.extend(throws);
102 all_annotated.extend(annotated);
103 }
104
105 let name_to_ids: HashMap<&str, Vec<&NodeId>> = {
108 let mut map: HashMap<&str, Vec<&NodeId>> = HashMap::new();
109 for node in &merged.added_nodes {
110 map.entry(node.name.as_str()).or_default().push(&node.id);
111 }
112 map
113 };
114
115 merged.deferred_calls = resolve_deferred(
116 &name_to_ids,
117 &all_calls,
118 EdgeKind::Calls,
119 &mut merged.added_edges,
120 );
121 merged.deferred_uses = resolve_deferred(
122 &name_to_ids,
123 &all_uses,
124 EdgeKind::Uses,
125 &mut merged.added_edges,
126 );
127 merged.deferred_implements = resolve_deferred(
128 &name_to_ids,
129 &all_implements,
130 EdgeKind::Implements,
131 &mut merged.added_edges,
132 );
133 let _ = resolve_deferred(
136 &name_to_ids,
137 &all_imports,
138 EdgeKind::Imports,
139 &mut merged.added_edges,
140 );
141 merged.deferred_inherits = resolve_deferred(
142 &name_to_ids,
143 &all_inherits,
144 EdgeKind::Inherits,
145 &mut merged.added_edges,
146 );
147 merged.deferred_throws = resolve_deferred(
148 &name_to_ids,
149 &all_throws,
150 EdgeKind::Throws,
151 &mut merged.added_edges,
152 );
153 merged.deferred_annotated = resolve_deferred(
154 &name_to_ids,
155 &all_annotated,
156 EdgeKind::Annotated,
157 &mut merged.added_edges,
158 );
159
160 for deleted in to_delete {
161 merged.removed_files.push(deleted.path().to_owned());
162 }
163
164 let (struct_nodes, struct_edges) = build_structural_nodes(&merged);
167 merged.added_nodes.extend(struct_nodes);
168 merged.added_edges.extend(struct_edges);
169
170 Ok((merged, head_sha))
171 }
172
173 fn supported_extensions(&self) -> Vec<&'static str> {
176 vec!["rs", "py", "ts", "tsx", "js", "jsx", "mjs", "cjs", "go"]
177 }
178
179 fn index_file(&self, repo_relative_path: &Path) -> FileIndexResult {
180 let abs_path = self.repo_root.join(repo_relative_path);
181 let empty = || {
182 (
183 GraphDiff::default(),
184 Vec::<(NodeId, String)>::new(),
185 Vec::<(NodeId, String)>::new(),
186 Vec::<(NodeId, String)>::new(),
187 Vec::<(NodeId, String)>::new(),
188 Vec::<(NodeId, String)>::new(),
189 Vec::<(NodeId, String)>::new(),
190 Vec::<(NodeId, String)>::new(),
191 )
192 };
193
194 if self.should_ignore(repo_relative_path) {
195 return Ok(empty());
196 }
197
198 let source = fs::read_to_string(&abs_path).map_err(|e| GitCortexError::Parse {
199 file: abs_path.clone(),
200 message: e.to_string(),
201 })?;
202
203 if source.len() > 512 * 1024 {
204 return Ok(empty());
205 }
206
207 let parser = match parser_for_path(repo_relative_path) {
208 Some(p) => p,
209 None => return Ok(empty()),
210 };
211
212 let ParseResult {
213 nodes,
214 edges,
215 deferred_calls,
216 deferred_uses,
217 deferred_implements,
218 deferred_imports,
219 deferred_inherits,
220 deferred_throws,
221 deferred_annotated,
222 } = parser.parse(repo_relative_path, &source)?;
223
224 let mut diff = GraphDiff::default();
225 diff.removed_files.push(repo_relative_path.to_owned());
228 for ancestor in repo_relative_path.ancestors().skip(1) {
229 if ancestor == Path::new("") || ancestor == Path::new(".") {
230 break;
231 }
232 diff.removed_files.push(ancestor.to_path_buf());
233 }
234 diff.added_nodes = nodes;
235 diff.added_edges = edges;
236
237 Ok((
238 diff,
239 deferred_calls,
240 deferred_uses,
241 deferred_implements,
242 deferred_imports,
243 deferred_inherits,
244 deferred_throws,
245 deferred_annotated,
246 ))
247 }
248
249 fn should_ignore(&self, path: &Path) -> bool {
250 self.ignorer
251 .matched_path_or_any_parents(path, false)
252 .is_ignore()
253 }
254}
255
256fn resolve_deferred(
264 name_to_ids: &HashMap<&str, Vec<&NodeId>>,
265 deferred: &[(NodeId, String)],
266 kind: EdgeKind,
267 edges: &mut Vec<Edge>,
268) -> Vec<(NodeId, String)> {
269 let mut unresolved = Vec::new();
270 for (src_id, target_name) in deferred {
271 if let Some(dst_ids) = name_to_ids.get(target_name.as_str()) {
272 for dst_id in dst_ids {
273 let edge = Edge {
274 src: src_id.clone(),
275 dst: (*dst_id).clone(),
276 kind: kind.clone(),
277 };
278 if !edges.contains(&edge) {
279 edges.push(edge);
280 }
281 }
282 } else {
283 unresolved.push((src_id.clone(), target_name.clone()));
284 }
285 }
286 unresolved
287}
288
289fn build_structural_nodes(diff: &GraphDiff) -> (Vec<Node>, Vec<Edge>) {
298 use std::collections::HashSet;
299
300 let contained: HashSet<&NodeId> = diff
302 .added_edges
303 .iter()
304 .filter(|e| e.kind == EdgeKind::Contains)
305 .map(|e| &e.dst)
306 .collect();
307
308 let mut by_file: HashMap<&Path, Vec<&Node>> = HashMap::new();
310 for node in &diff.added_nodes {
311 by_file.entry(node.file.as_path()).or_default().push(node);
312 }
313
314 let mut new_nodes: Vec<Node> = Vec::new();
315 let mut new_edges: Vec<Edge> = Vec::new();
316 let mut folder_ids: HashMap<PathBuf, NodeId> = HashMap::new();
318
319 let mut file_ids: HashMap<&Path, NodeId> = HashMap::new();
321 for (file_path, code_nodes) in &by_file {
322 let file_id = NodeId::new();
323 let name = file_path
324 .file_name()
325 .and_then(|n| n.to_str())
326 .unwrap_or("")
327 .to_owned();
328 new_nodes.push(Node {
329 id: file_id.clone(),
330 name,
331 kind: NodeKind::File,
332 qualified_name: file_path.to_string_lossy().into_owned(),
333 file: file_path.to_path_buf(),
334 span: Span {
335 start_line: 1,
336 end_line: 1,
337 },
338 metadata: NodeMetadata {
339 visibility: Visibility::Pub,
340 ..Default::default()
341 },
342 });
343 file_ids.insert(file_path, file_id.clone());
344
345 for node in code_nodes {
347 if !contained.contains(&node.id) {
348 new_edges.push(Edge {
349 src: file_id.clone(),
350 dst: node.id.clone(),
351 kind: EdgeKind::Contains,
352 });
353 }
354 }
355 }
356
357 let unique_dirs: HashSet<PathBuf> = by_file
359 .keys()
360 .flat_map(|p| p.ancestors().skip(1))
361 .filter(|p| *p != Path::new("") && *p != Path::new("."))
362 .map(|p| p.to_path_buf())
363 .collect();
364
365 for dir in &unique_dirs {
366 let dir_id = folder_ids.entry(dir.clone()).or_default().clone();
367 let name = dir
368 .file_name()
369 .and_then(|n| n.to_str())
370 .unwrap_or(dir.to_str().unwrap_or(""))
371 .to_owned();
372 new_nodes.push(Node {
373 id: dir_id,
374 name,
375 kind: NodeKind::Folder,
376 qualified_name: dir.to_string_lossy().into_owned(),
377 file: dir.clone(),
378 span: Span {
379 start_line: 1,
380 end_line: 1,
381 },
382 metadata: NodeMetadata {
383 visibility: Visibility::Pub,
384 ..Default::default()
385 },
386 });
387 }
388
389 for (file_path, file_id) in &file_ids {
391 if let Some(parent) = file_path.parent().filter(|p| *p != Path::new("")) {
392 if let Some(dir_id) = folder_ids.get(parent) {
393 new_edges.push(Edge {
394 src: dir_id.clone(),
395 dst: file_id.clone(),
396 kind: EdgeKind::Contains,
397 });
398 }
399 }
400 }
401 for dir in &unique_dirs {
402 let child_id = folder_ids[dir].clone();
403 if let Some(parent) = dir
404 .parent()
405 .filter(|p| *p != Path::new("") && *p != Path::new("."))
406 {
407 if let Some(parent_id) = folder_ids.get(parent) {
408 new_edges.push(Edge {
409 src: parent_id.clone(),
410 dst: child_id,
411 kind: EdgeKind::Contains,
412 });
413 }
414 }
415 }
416
417 (new_nodes, new_edges)
418}
419
420fn build_ignorer(repo_root: &Path) -> Gitignore {
421 let ignore_path = repo_root.join(".gitcortex/ignore");
422 let mut builder = GitignoreBuilder::new(repo_root);
423 if ignore_path.exists() {
424 let _ = builder.add(ignore_path);
425 }
426 builder.build().unwrap_or_else(|_| Gitignore::empty())
427}