code_system_graph_core/
incremental.rs1use std::collections::{BTreeMap, BTreeSet};
2
3use code_system_graph_model::{
4 ArtifactChange, ArtifactChangeKind, ArtifactFingerprint, CheckoutId, NativePath, RepoId
5};
6
7type ArtifactKey = (RepoId, CheckoutId, NativePath, String);
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct IncrementalPlan {
12 pub changes: Vec<ArtifactChange>,
14}
15
16impl IncrementalPlan {
17 #[must_use]
19 pub fn has_changes(&self) -> bool {
20 self.changes
21 .iter()
22 .any(|change| change.kind != ArtifactChangeKind::Unchanged)
23 }
24
25 #[must_use]
27 pub fn changed_count(&self) -> usize {
28 self.changes
29 .iter()
30 .filter(|change| change.kind != ArtifactChangeKind::Unchanged)
31 .count()
32 }
33}
34
35#[must_use]
37pub fn plan_incremental_scan(
38 previous: &[ArtifactFingerprint],
39 current: &[ArtifactFingerprint],
40) -> IncrementalPlan {
41 let previous = fingerprint_map(previous);
42 let current = fingerprint_map(current);
43 let keys = previous
44 .keys()
45 .chain(current.keys())
46 .cloned()
47 .collect::<BTreeSet<_>>();
48 let changes = keys
49 .into_iter()
50 .filter_map(|key| {
51 let kind = match (previous.get(&key), current.get(&key)) {
52 (None, Some(_)) => ArtifactChangeKind::Added,
53 (Some(_), None) => ArtifactChangeKind::Deleted,
54 (Some(before), Some(after)) if before.content_hash != after.content_hash => {
55 ArtifactChangeKind::Modified
56 }
57 (Some(_), Some(_)) => ArtifactChangeKind::Unchanged,
58 (None, None) => return None,
59 };
60 Some(ArtifactChange {
61 repo_id: key.0,
62 checkout_id: key.1,
63 path: key.2,
64 extractor: key.3,
65 kind,
66 })
67 })
68 .collect();
69 IncrementalPlan { changes }
70}
71
72fn fingerprint_map(
73 fingerprints: &[ArtifactFingerprint],
74) -> BTreeMap<ArtifactKey, &ArtifactFingerprint> {
75 fingerprints
76 .iter()
77 .map(|fingerprint| {
78 (
79 (
80 fingerprint.repo_id.clone(),
81 fingerprint.checkout_id.clone(),
82 fingerprint.path.clone(),
83 fingerprint.extractor.clone(),
84 ),
85 fingerprint,
86 )
87 })
88 .collect()
89}
90
91#[cfg(test)]
92mod tests {
93 use code_system_graph_model::{
94 ArtifactChangeKind, ArtifactFingerprint, CheckoutId, NativePath, NativePathEncoding, RepoId
95 };
96
97 use super::plan_incremental_scan;
98
99 fn fingerprint(path: &str, hash: &str) -> ArtifactFingerprint {
100 ArtifactFingerprint {
101 repo_id: RepoId::new("repo:api"),
102 checkout_id: CheckoutId::new("checkout:api"),
103 path: NativePath {
104 encoding: NativePathEncoding::Utf8,
105 bytes: path.as_bytes().to_vec(),
106 display: path.to_owned(),
107 },
108 extractor: "openapi".to_owned(),
109 content_hash: hash.to_owned(),
110 size_bytes: 1,
111 }
112 }
113
114 #[test]
115 fn plan_should_classify_add_modify_delete_and_unchanged() {
116 let previous = vec![
117 fingerprint("deleted.yaml", "a"),
118 fingerprint("modified.yaml", "a"),
119 fingerprint("same.yaml", "a"),
120 ];
121 let current = vec![
122 fingerprint("added.yaml", "a"),
123 fingerprint("modified.yaml", "b"),
124 fingerprint("same.yaml", "a"),
125 ];
126
127 let plan = plan_incremental_scan(&previous, ¤t);
128 let kinds = plan
129 .changes
130 .iter()
131 .map(|change| change.kind)
132 .collect::<Vec<_>>();
133
134 assert_eq!(
135 kinds,
136 vec![
137 ArtifactChangeKind::Added,
138 ArtifactChangeKind::Deleted,
139 ArtifactChangeKind::Modified,
140 ArtifactChangeKind::Unchanged,
141 ]
142 );
143 }
144}