1use std::{
2 collections::{HashMap, HashSet},
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::{EdgeConfidence, 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, u32)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, Vec<(NodeId, String)>, )>;
31
32pub struct IncrementalIndexer {
37 repo_root: PathBuf,
38 ignorer: Gitignore,
39}
40
41impl IncrementalIndexer {
42 pub fn new(repo_root: &Path) -> Result<Self> {
46 let ignorer = build_ignorer(repo_root);
47 Ok(Self {
48 repo_root: repo_root.to_owned(),
49 ignorer,
50 })
51 }
52
53 pub fn run(&self, from_sha: Option<&str>) -> Result<(GraphDiff, String)> {
61 let differ = Differ::open(&self.repo_root)?;
62 let head_sha = differ.head_sha()?;
63
64 if from_sha.map(|s| s == head_sha).unwrap_or(false) {
65 return Ok((GraphDiff::default(), head_sha));
66 }
67
68 let timing = std::env::var_os("GCX_TIMING").is_some();
69 let t0 = std::time::Instant::now();
70 macro_rules! mark {
71 ($label:expr) => {
72 if timing {
73 eprintln!("[gcx-timing] {:>8.2?} {}", t0.elapsed(), $label);
74 }
75 };
76 }
77
78 let supported = self.supported_extensions();
79 let changes = differ.changed_files(from_sha, &supported)?;
80
81 if changes.is_empty() {
82 return Ok((GraphDiff::default(), head_sha));
83 }
84
85 let (to_parse, to_delete): (Vec<_>, Vec<_>) = changes
86 .into_iter()
87 .partition(|c| !matches!(c, FileChange::Deleted(_)));
88 mark!(format!("diff: {} files to parse", to_parse.len()));
89
90 let per_file: Vec<FileIndexResult> = to_parse
92 .par_iter()
93 .map(|change| self.index_file(change.path()))
94 .collect();
95 mark!("parse (parallel) done");
96
97 let mut merged = GraphDiff::default();
99 let mut all_calls: Vec<(NodeId, String, u32)> = Vec::new();
100 let mut all_uses: Vec<(NodeId, String)> = Vec::new();
101 let mut all_implements: Vec<(NodeId, String)> = Vec::new();
102 let mut all_imports: Vec<(NodeId, String)> = Vec::new();
103 let mut all_inherits: Vec<(NodeId, String)> = Vec::new();
104 let mut all_throws: Vec<(NodeId, String)> = Vec::new();
105 let mut all_annotated: Vec<(NodeId, String)> = Vec::new();
106 let mut all_doc_refs: Vec<(NodeId, String)> = Vec::new();
107 for result in per_file {
108 let (diff, calls, uses, implements, imports, inherits, throws, annotated, doc_refs) =
109 result?;
110 merged.merge(diff);
111 all_calls.extend(calls);
112 all_uses.extend(uses);
113 all_implements.extend(implements);
114 all_imports.extend(imports);
115 all_inherits.extend(inherits);
116 all_throws.extend(throws);
117 all_annotated.extend(annotated);
118 all_doc_refs.extend(doc_refs);
119 }
120 mark!(format!(
121 "merge done: {} nodes, {} direct edges, {} deferred calls",
122 merged.added_nodes.len(),
123 merged.added_edges.len(),
124 all_calls.len()
125 ));
126
127 let name_to_ids: HashMap<&str, Vec<&NodeId>> = {
130 let mut map: HashMap<&str, Vec<&NodeId>> = HashMap::new();
131 for node in &merged.added_nodes {
132 map.entry(node.name.as_str()).or_default().push(&node.id);
133 }
134 map
135 };
136
137 let id_to_file: HashMap<&NodeId, &Path> = merged
142 .added_nodes
143 .iter()
144 .map(|n| (&n.id, n.file.as_path()))
145 .collect();
146
147 let imported_by_file: HashMap<&Path, HashSet<&str>> = {
152 let mut map: HashMap<&Path, HashSet<&str>> = HashMap::new();
153 for (mod_id, leaf_name) in &all_imports {
154 if let Some(&file) = id_to_file.get(mod_id) {
155 map.entry(file).or_default().insert(leaf_name.as_str());
156 }
157 }
158 map
159 };
160
161 let mut seen_edges: HashSet<(String, String, String)> = merged
166 .added_edges
167 .iter()
168 .map(|e| (e.src.as_str(), e.dst.as_str(), e.kind.to_string()))
169 .collect();
170
171 merged.deferred_calls = resolve_calls(
172 &name_to_ids,
173 &id_to_file,
174 &imported_by_file,
175 &all_calls,
176 &mut merged.added_edges,
177 &mut seen_edges,
178 );
179 merged.deferred_uses = resolve_deferred(
180 &name_to_ids,
181 &id_to_file,
182 &imported_by_file,
183 &all_uses,
184 EdgeKind::Uses,
185 &mut merged.added_edges,
186 &mut seen_edges,
187 );
188 merged.deferred_implements = resolve_deferred(
189 &name_to_ids,
190 &id_to_file,
191 &imported_by_file,
192 &all_implements,
193 EdgeKind::Implements,
194 &mut merged.added_edges,
195 &mut seen_edges,
196 );
197 let _ = resolve_deferred(
200 &name_to_ids,
201 &id_to_file,
202 &imported_by_file,
203 &all_imports,
204 EdgeKind::Imports,
205 &mut merged.added_edges,
206 &mut seen_edges,
207 );
208 merged.deferred_inherits = resolve_deferred(
209 &name_to_ids,
210 &id_to_file,
211 &imported_by_file,
212 &all_inherits,
213 EdgeKind::Inherits,
214 &mut merged.added_edges,
215 &mut seen_edges,
216 );
217 merged.deferred_throws = resolve_deferred(
218 &name_to_ids,
219 &id_to_file,
220 &imported_by_file,
221 &all_throws,
222 EdgeKind::Throws,
223 &mut merged.added_edges,
224 &mut seen_edges,
225 );
226 merged.deferred_annotated = resolve_deferred(
227 &name_to_ids,
228 &id_to_file,
229 &imported_by_file,
230 &all_annotated,
231 EdgeKind::Annotated,
232 &mut merged.added_edges,
233 &mut seen_edges,
234 );
235 merged.deferred_doc_refs = resolve_deferred(
240 &name_to_ids,
241 &id_to_file,
242 &imported_by_file,
243 &all_doc_refs,
244 EdgeKind::References,
245 &mut merged.added_edges,
246 &mut seen_edges,
247 );
248 mark!(format!(
249 "resolve_deferred done: {} total edges",
250 merged.added_edges.len()
251 ));
252
253 {
259 let mut ann_by_id: HashMap<String, Vec<String>> = HashMap::new();
260 for (node_id, name) in &all_annotated {
261 ann_by_id
262 .entry(node_id.as_str())
263 .or_default()
264 .push(name.clone());
265 }
266 for node in &mut merged.added_nodes {
267 if let Some(names) = ann_by_id.get(&node.id.as_str()) {
268 let mut seen: HashSet<String> = HashSet::new();
269 node.metadata.annotations = names
270 .iter()
271 .filter(|n| seen.insert((*n).clone()))
272 .cloned()
273 .collect();
274 }
275 }
276 }
277
278 for deleted in to_delete {
279 merged.removed_files.push(deleted.path().to_owned());
280 }
281
282 let (struct_nodes, struct_edges) = build_structural_nodes(&merged);
285 merged.added_nodes.extend(struct_nodes);
286 merged.added_edges.extend(struct_edges);
287 mark!("structural nodes done");
288
289 Ok((merged, head_sha))
290 }
291
292 pub fn index_files_from_disk(&self, abs_paths: &[PathBuf]) -> Result<GraphDiff> {
296 if abs_paths.is_empty() {
297 return Ok(GraphDiff::default());
298 }
299
300 let rel_paths: Vec<PathBuf> = abs_paths
301 .iter()
302 .filter_map(|p| p.strip_prefix(&self.repo_root).ok().map(|r| r.to_owned()))
303 .collect();
304
305 let per_file: Vec<FileIndexResult> =
306 rel_paths.par_iter().map(|p| self.index_file(p)).collect();
307
308 let mut merged = GraphDiff::default();
309 let mut all_calls: Vec<(NodeId, String, u32)> = Vec::new();
310 let mut all_uses: Vec<(NodeId, String)> = Vec::new();
311 let mut all_implements: Vec<(NodeId, String)> = Vec::new();
312 let mut all_imports: Vec<(NodeId, String)> = Vec::new();
313 let mut all_inherits: Vec<(NodeId, String)> = Vec::new();
314 let mut all_throws: Vec<(NodeId, String)> = Vec::new();
315 let mut all_annotated: Vec<(NodeId, String)> = Vec::new();
316 let mut all_doc_refs: Vec<(NodeId, String)> = Vec::new();
317 for result in per_file {
318 let (diff, calls, uses, implements, imports, inherits, throws, annotated, doc_refs) =
319 result?;
320 merged.merge(diff);
321 all_calls.extend(calls);
322 all_uses.extend(uses);
323 all_implements.extend(implements);
324 all_imports.extend(imports);
325 all_inherits.extend(inherits);
326 all_throws.extend(throws);
327 all_annotated.extend(annotated);
328 all_doc_refs.extend(doc_refs);
329 }
330
331 let name_to_ids: HashMap<&str, Vec<&NodeId>> = {
332 let mut map: HashMap<&str, Vec<&NodeId>> = HashMap::new();
333 for node in &merged.added_nodes {
334 map.entry(node.name.as_str()).or_default().push(&node.id);
335 }
336 map
337 };
338 let id_to_file: HashMap<&NodeId, &Path> = merged
339 .added_nodes
340 .iter()
341 .map(|n| (&n.id, n.file.as_path()))
342 .collect();
343 let imported_by_file: HashMap<&Path, HashSet<&str>> = {
344 let mut map: HashMap<&Path, HashSet<&str>> = HashMap::new();
345 for (mod_id, leaf_name) in &all_imports {
346 if let Some(&file) = id_to_file.get(mod_id) {
347 map.entry(file).or_default().insert(leaf_name.as_str());
348 }
349 }
350 map
351 };
352 let mut seen_edges: HashSet<(String, String, String)> = merged
353 .added_edges
354 .iter()
355 .map(|e| (e.src.as_str(), e.dst.as_str(), e.kind.to_string()))
356 .collect();
357
358 merged.deferred_calls = resolve_calls(
359 &name_to_ids,
360 &id_to_file,
361 &imported_by_file,
362 &all_calls,
363 &mut merged.added_edges,
364 &mut seen_edges,
365 );
366 merged.deferred_uses = resolve_deferred(
367 &name_to_ids,
368 &id_to_file,
369 &imported_by_file,
370 &all_uses,
371 EdgeKind::Uses,
372 &mut merged.added_edges,
373 &mut seen_edges,
374 );
375 merged.deferred_implements = resolve_deferred(
376 &name_to_ids,
377 &id_to_file,
378 &imported_by_file,
379 &all_implements,
380 EdgeKind::Implements,
381 &mut merged.added_edges,
382 &mut seen_edges,
383 );
384 let _ = resolve_deferred(
385 &name_to_ids,
386 &id_to_file,
387 &imported_by_file,
388 &all_imports,
389 EdgeKind::Imports,
390 &mut merged.added_edges,
391 &mut seen_edges,
392 );
393 merged.deferred_inherits = resolve_deferred(
394 &name_to_ids,
395 &id_to_file,
396 &imported_by_file,
397 &all_inherits,
398 EdgeKind::Inherits,
399 &mut merged.added_edges,
400 &mut seen_edges,
401 );
402 merged.deferred_throws = resolve_deferred(
403 &name_to_ids,
404 &id_to_file,
405 &imported_by_file,
406 &all_throws,
407 EdgeKind::Throws,
408 &mut merged.added_edges,
409 &mut seen_edges,
410 );
411 merged.deferred_annotated = resolve_deferred(
412 &name_to_ids,
413 &id_to_file,
414 &imported_by_file,
415 &all_annotated,
416 EdgeKind::Annotated,
417 &mut merged.added_edges,
418 &mut seen_edges,
419 );
420 merged.deferred_doc_refs = resolve_deferred(
421 &name_to_ids,
422 &id_to_file,
423 &imported_by_file,
424 &all_doc_refs,
425 EdgeKind::References,
426 &mut merged.added_edges,
427 &mut seen_edges,
428 );
429
430 let mut ann_by_id: HashMap<String, Vec<String>> = HashMap::new();
431 for (node_id, name) in &all_annotated {
432 ann_by_id
433 .entry(node_id.as_str())
434 .or_default()
435 .push(name.clone());
436 }
437 for node in &mut merged.added_nodes {
438 if let Some(names) = ann_by_id.get(&node.id.as_str()) {
439 let mut seen: HashSet<String> = HashSet::new();
440 node.metadata.annotations = names
441 .iter()
442 .filter(|n| seen.insert((*n).clone()))
443 .cloned()
444 .collect();
445 }
446 }
447
448 for p in abs_paths {
450 if let Ok(rel) = p.strip_prefix(&self.repo_root) {
451 merged.removed_files.push(rel.to_owned());
452 }
453 }
454
455 let (struct_nodes, struct_edges) = build_structural_nodes(&merged);
456 merged.added_nodes.extend(struct_nodes);
457 merged.added_edges.extend(struct_edges);
458
459 Ok(merged)
460 }
461
462 fn supported_extensions(&self) -> Vec<&'static str> {
465 vec![
466 "rs", "py", "ts", "tsx", "js", "jsx", "mjs", "cjs", "go", "java", "md", "markdown",
467 ]
468 }
469
470 fn index_file(&self, repo_relative_path: &Path) -> FileIndexResult {
471 let abs_path = self.repo_root.join(repo_relative_path);
472 let empty = || {
473 (
474 GraphDiff::default(),
475 Vec::<(NodeId, String, u32)>::new(),
476 Vec::<(NodeId, String)>::new(),
477 Vec::<(NodeId, String)>::new(),
478 Vec::<(NodeId, String)>::new(),
479 Vec::<(NodeId, String)>::new(),
480 Vec::<(NodeId, String)>::new(),
481 Vec::<(NodeId, String)>::new(),
482 Vec::<(NodeId, String)>::new(),
483 )
484 };
485
486 if self.should_ignore(repo_relative_path) {
487 return Ok(empty());
488 }
489
490 let source = fs::read_to_string(&abs_path).map_err(|e| GitCortexError::Parse {
491 file: abs_path.clone(),
492 message: e.to_string(),
493 })?;
494
495 if source.len() > 512 * 1024 {
496 return Ok(empty());
497 }
498
499 let parser = match parser_for_path(repo_relative_path) {
500 Some(p) => p,
501 None => return Ok(empty()),
502 };
503
504 let ParseResult {
505 nodes,
506 edges,
507 deferred_calls,
508 deferred_uses,
509 deferred_implements,
510 deferred_imports,
511 deferred_inherits,
512 deferred_throws,
513 deferred_annotated,
514 deferred_doc_refs,
515 } = parser.parse(repo_relative_path, &source)?;
516
517 let mut diff = GraphDiff::default();
518 diff.removed_files.push(repo_relative_path.to_owned());
521 for ancestor in repo_relative_path.ancestors().skip(1) {
522 if ancestor == Path::new("") || ancestor == Path::new(".") {
523 break;
524 }
525 diff.removed_files.push(ancestor.to_path_buf());
526 }
527 diff.added_nodes = nodes;
528 diff.added_edges = edges;
529
530 Ok((
531 diff,
532 deferred_calls,
533 deferred_uses,
534 deferred_implements,
535 deferred_imports,
536 deferred_inherits,
537 deferred_throws,
538 deferred_annotated,
539 deferred_doc_refs,
540 ))
541 }
542
543 fn should_ignore(&self, path: &Path) -> bool {
544 self.ignorer
545 .matched_path_or_any_parents(path, false)
546 .is_ignore()
547 }
548}
549
550const MAX_RESOLVE_FANOUT: usize = 8;
559
560fn resolve_deferred(
573 name_to_ids: &HashMap<&str, Vec<&NodeId>>,
574 id_to_file: &HashMap<&NodeId, &Path>,
575 imported_by_file: &HashMap<&Path, HashSet<&str>>,
576 deferred: &[(NodeId, String)],
577 kind: EdgeKind,
578 edges: &mut Vec<Edge>,
579 seen: &mut HashSet<(String, String, String)>,
580) -> Vec<(NodeId, String)> {
581 let mut unresolved = Vec::new();
582 for (src_id, target_name) in deferred {
583 let src_file = id_to_file.get(src_id).copied();
584 let src_exts = src_file.and_then(language_extensions_for_path);
585
586 let Some(dst_ids) = name_to_ids.get(target_name.as_str()) else {
587 unresolved.push((src_id.clone(), target_name.clone()));
588 continue;
589 };
590
591 let candidates: Vec<&NodeId> = dst_ids
593 .iter()
594 .copied()
595 .filter(|dst_id| match (src_exts, id_to_file.get(*dst_id)) {
596 (Some(exts), Some(dst_path)) => language_extensions_for_path(dst_path)
597 .map(|dst_exts| dst_exts.first() == exts.first())
598 .unwrap_or(true),
599 _ => true,
600 })
601 .collect();
602
603 if candidates.len() > MAX_RESOLVE_FANOUT {
606 continue;
607 }
608
609 let confidence = src_file
611 .and_then(|f| imported_by_file.get(f))
612 .map(|imports| {
613 if imports.contains(target_name.as_str()) {
614 EdgeConfidence::Resolved
615 } else {
616 EdgeConfidence::Inferred
617 }
618 })
619 .unwrap_or(EdgeConfidence::Inferred);
620
621 let mut matched_any = false;
622 for dst_id in candidates {
623 matched_any = true;
624 let key = (src_id.as_str(), dst_id.as_str(), kind.to_string());
625 if seen.insert(key) {
626 edges.push(
627 Edge::new(src_id.clone(), dst_id.clone(), kind.clone())
628 .with_confidence(confidence),
629 );
630 }
631 }
632 if !matched_any {
633 unresolved.push((src_id.clone(), target_name.clone()));
634 }
635 }
636 unresolved
637}
638
639fn resolve_calls(
644 name_to_ids: &HashMap<&str, Vec<&NodeId>>,
645 id_to_file: &HashMap<&NodeId, &Path>,
646 imported_by_file: &HashMap<&Path, HashSet<&str>>,
647 deferred: &[(NodeId, String, u32)],
648 edges: &mut Vec<Edge>,
649 seen: &mut HashSet<(String, String, String)>,
650) -> Vec<(NodeId, String, u32)> {
651 let mut unresolved = Vec::new();
652 for (src_id, target_name, line) in deferred {
653 let src_file = id_to_file.get(src_id).copied();
654 let src_exts = src_file.and_then(language_extensions_for_path);
655
656 let Some(dst_ids) = name_to_ids.get(target_name.as_str()) else {
657 unresolved.push((src_id.clone(), target_name.clone(), *line));
658 continue;
659 };
660
661 let candidates: Vec<&NodeId> = dst_ids
662 .iter()
663 .copied()
664 .filter(|dst_id| match (src_exts, id_to_file.get(*dst_id)) {
665 (Some(exts), Some(dst_path)) => language_extensions_for_path(dst_path)
666 .map(|dst_exts| dst_exts.first() == exts.first())
667 .unwrap_or(true),
668 _ => true,
669 })
670 .collect();
671
672 if candidates.len() > MAX_RESOLVE_FANOUT {
673 continue;
674 }
675
676 let confidence = src_file
678 .and_then(|f| imported_by_file.get(f))
679 .map(|imports| {
680 if imports.contains(target_name.as_str()) {
681 EdgeConfidence::Resolved
682 } else {
683 EdgeConfidence::Inferred
684 }
685 })
686 .unwrap_or(EdgeConfidence::Inferred);
687
688 let mut matched_any = false;
689 for dst_id in candidates {
690 matched_any = true;
691 let key = (
692 src_id.as_str(),
693 dst_id.as_str(),
694 EdgeKind::Calls.to_string(),
695 );
696 if seen.insert(key) {
697 edges.push(
698 Edge::call(src_id.clone(), dst_id.clone(), *line).with_confidence(confidence),
699 );
700 }
701 }
702 if !matched_any {
703 unresolved.push((src_id.clone(), target_name.clone(), *line));
704 }
705 }
706 unresolved
707}
708
709fn language_extensions_for_path(path: &Path) -> Option<&'static [&'static str]> {
712 let ext = path.extension()?.to_str()?;
713 match ext {
714 "rs" => Some(&[".rs"]),
715 "py" => Some(&[".py"]),
716 "ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs" => {
717 Some(&[".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"])
718 }
719 "go" => Some(&[".go"]),
720 "java" => Some(&[".java"]),
721 _ => None,
722 }
723}
724
725fn build_structural_nodes(diff: &GraphDiff) -> (Vec<Node>, Vec<Edge>) {
734 use std::collections::HashSet;
735
736 let contained: HashSet<&NodeId> = diff
738 .added_edges
739 .iter()
740 .filter(|e| e.kind == EdgeKind::Contains)
741 .map(|e| &e.dst)
742 .collect();
743
744 let mut by_file: HashMap<&Path, Vec<&Node>> = HashMap::new();
746 for node in &diff.added_nodes {
747 by_file.entry(node.file.as_path()).or_default().push(node);
748 }
749
750 let mut new_nodes: Vec<Node> = Vec::new();
751 let mut new_edges: Vec<Edge> = Vec::new();
752 let mut folder_ids: HashMap<PathBuf, NodeId> = HashMap::new();
754
755 let mut file_ids: HashMap<&Path, NodeId> = HashMap::new();
757 for (file_path, code_nodes) in &by_file {
758 let file_id = NodeId::new();
759 let name = file_path
760 .file_name()
761 .and_then(|n| n.to_str())
762 .unwrap_or("")
763 .to_owned();
764 new_nodes.push(Node {
765 id: file_id.clone(),
766 name,
767 kind: NodeKind::File,
768 qualified_name: file_path.to_string_lossy().into_owned(),
769 file: file_path.to_path_buf(),
770 span: Span {
771 start_line: 1,
772 end_line: 1,
773 },
774 metadata: NodeMetadata {
775 visibility: Visibility::Pub,
776 ..Default::default()
777 },
778 });
779 file_ids.insert(file_path, file_id.clone());
780
781 for node in code_nodes {
783 if !contained.contains(&node.id) {
784 new_edges.push(Edge {
785 src: file_id.clone(),
786 dst: node.id.clone(),
787 kind: EdgeKind::Contains,
788 line: None,
789 confidence: EdgeConfidence::Extracted,
790 });
791 }
792 }
793 }
794
795 let unique_dirs: HashSet<PathBuf> = by_file
797 .keys()
798 .flat_map(|p| p.ancestors().skip(1))
799 .filter(|p| *p != Path::new("") && *p != Path::new("."))
800 .map(|p| p.to_path_buf())
801 .collect();
802
803 for dir in &unique_dirs {
804 let dir_id = folder_ids.entry(dir.clone()).or_default().clone();
805 let name = dir
806 .file_name()
807 .and_then(|n| n.to_str())
808 .unwrap_or(dir.to_str().unwrap_or(""))
809 .to_owned();
810 new_nodes.push(Node {
811 id: dir_id,
812 name,
813 kind: NodeKind::Folder,
814 qualified_name: dir.to_string_lossy().into_owned(),
815 file: dir.clone(),
816 span: Span {
817 start_line: 1,
818 end_line: 1,
819 },
820 metadata: NodeMetadata {
821 visibility: Visibility::Pub,
822 ..Default::default()
823 },
824 });
825 }
826
827 for (file_path, file_id) in &file_ids {
829 if let Some(parent) = file_path.parent().filter(|p| *p != Path::new("")) {
830 if let Some(dir_id) = folder_ids.get(parent) {
831 new_edges.push(Edge {
832 src: dir_id.clone(),
833 dst: file_id.clone(),
834 kind: EdgeKind::Contains,
835 line: None,
836 confidence: EdgeConfidence::Extracted,
837 });
838 }
839 }
840 }
841 for dir in &unique_dirs {
842 let child_id = folder_ids[dir].clone();
843 if let Some(parent) = dir
844 .parent()
845 .filter(|p| *p != Path::new("") && *p != Path::new("."))
846 {
847 if let Some(parent_id) = folder_ids.get(parent) {
848 new_edges.push(Edge {
849 src: parent_id.clone(),
850 dst: child_id,
851 kind: EdgeKind::Contains,
852 line: None,
853 confidence: EdgeConfidence::Extracted,
854 });
855 }
856 }
857 }
858
859 (new_nodes, new_edges)
860}
861
862fn build_ignorer(repo_root: &Path) -> Gitignore {
863 let ignore_path = repo_root.join(".gitcortex/ignore");
864 let mut builder = GitignoreBuilder::new(repo_root);
865 if ignore_path.exists() {
866 let _ = builder.add(ignore_path);
867 }
868 builder.build().unwrap_or_else(|_| Gitignore::empty())
869}