1use lago_core::ManifestEntry;
2use serde::{Deserialize, Serialize};
3
4use crate::manifest::Manifest;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub enum DiffEntry {
9 Added { path: String, entry: ManifestEntry },
11 Removed { path: String, entry: ManifestEntry },
13 Modified {
15 path: String,
16 old: ManifestEntry,
17 new: ManifestEntry,
18 },
19}
20
21pub fn diff(old: &Manifest, new: &Manifest) -> Vec<DiffEntry> {
27 let mut result = Vec::new();
28
29 let old_entries = old.entries();
30 let new_entries = new.entries();
31
32 let mut old_iter = old_entries.iter().peekable();
33 let mut new_iter = new_entries.iter().peekable();
34
35 loop {
36 match (old_iter.peek(), new_iter.peek()) {
37 (Some((old_path, _)), Some((new_path, _))) => {
38 match old_path.cmp(new_path) {
39 std::cmp::Ordering::Less => {
40 let (path, entry) = old_iter.next().unwrap();
42 result.push(DiffEntry::Removed {
43 path: path.clone(),
44 entry: entry.clone(),
45 });
46 }
47 std::cmp::Ordering::Greater => {
48 let (path, entry) = new_iter.next().unwrap();
50 result.push(DiffEntry::Added {
51 path: path.clone(),
52 entry: entry.clone(),
53 });
54 }
55 std::cmp::Ordering::Equal => {
56 let (path, old_entry) = old_iter.next().unwrap();
58 let (_, new_entry) = new_iter.next().unwrap();
59 if old_entry.blob_hash != new_entry.blob_hash {
60 result.push(DiffEntry::Modified {
61 path: path.clone(),
62 old: old_entry.clone(),
63 new: new_entry.clone(),
64 });
65 }
66 }
67 }
68 }
69 (Some(_), None) => {
70 let (path, entry) = old_iter.next().unwrap();
72 result.push(DiffEntry::Removed {
73 path: path.clone(),
74 entry: entry.clone(),
75 });
76 }
77 (None, Some(_)) => {
78 let (path, entry) = new_iter.next().unwrap();
80 result.push(DiffEntry::Added {
81 path: path.clone(),
82 entry: entry.clone(),
83 });
84 }
85 (None, None) => break,
86 }
87 }
88
89 result
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use lago_core::BlobHash;
96
97 #[test]
98 fn diff_empty_manifests() {
99 let old = Manifest::new();
100 let new = Manifest::new();
101 let d = diff(&old, &new);
102 assert!(d.is_empty());
103 }
104
105 #[test]
106 fn diff_added_files() {
107 let old = Manifest::new();
108 let mut new = Manifest::new();
109 new.apply_write("/a.txt".to_string(), BlobHash::from_hex("aaa"), 10, None, 1);
110
111 let d = diff(&old, &new);
112 assert_eq!(d.len(), 1);
113 assert!(matches!(&d[0], DiffEntry::Added { path, .. } if path == "/a.txt"));
114 }
115
116 #[test]
117 fn diff_removed_files() {
118 let mut old = Manifest::new();
119 old.apply_write("/a.txt".to_string(), BlobHash::from_hex("aaa"), 10, None, 1);
120 let new = Manifest::new();
121
122 let d = diff(&old, &new);
123 assert_eq!(d.len(), 1);
124 assert!(matches!(&d[0], DiffEntry::Removed { path, .. } if path == "/a.txt"));
125 }
126
127 #[test]
128 fn diff_modified_files() {
129 let mut old = Manifest::new();
130 old.apply_write("/a.txt".to_string(), BlobHash::from_hex("aaa"), 10, None, 1);
131 let mut new = Manifest::new();
132 new.apply_write("/a.txt".to_string(), BlobHash::from_hex("bbb"), 20, None, 2);
133
134 let d = diff(&old, &new);
135 assert_eq!(d.len(), 1);
136 assert!(matches!(&d[0], DiffEntry::Modified { path, .. } if path == "/a.txt"));
137 }
138
139 #[test]
140 fn diff_unchanged_files_not_reported() {
141 let mut old = Manifest::new();
142 old.apply_write("/a.txt".to_string(), BlobHash::from_hex("aaa"), 10, None, 1);
143 let mut new = Manifest::new();
144 new.apply_write("/a.txt".to_string(), BlobHash::from_hex("aaa"), 10, None, 2);
145
146 let d = diff(&old, &new);
147 assert!(d.is_empty());
148 }
149
150 #[test]
151 fn diff_mixed_changes() {
152 let mut old = Manifest::new();
153 old.apply_write(
154 "/keep.txt".to_string(),
155 BlobHash::from_hex("aaa"),
156 1,
157 None,
158 1,
159 );
160 old.apply_write(
161 "/modify.txt".to_string(),
162 BlobHash::from_hex("bbb"),
163 2,
164 None,
165 1,
166 );
167 old.apply_write(
168 "/remove.txt".to_string(),
169 BlobHash::from_hex("ccc"),
170 3,
171 None,
172 1,
173 );
174
175 let mut new = Manifest::new();
176 new.apply_write(
177 "/keep.txt".to_string(),
178 BlobHash::from_hex("aaa"),
179 1,
180 None,
181 2,
182 );
183 new.apply_write(
184 "/modify.txt".to_string(),
185 BlobHash::from_hex("ddd"),
186 4,
187 None,
188 2,
189 );
190 new.apply_write(
191 "/add.txt".to_string(),
192 BlobHash::from_hex("eee"),
193 5,
194 None,
195 2,
196 );
197
198 let d = diff(&old, &new);
199 assert_eq!(d.len(), 3);
201
202 let added = d
203 .iter()
204 .filter(|e| matches!(e, DiffEntry::Added { .. }))
205 .count();
206 let removed = d
207 .iter()
208 .filter(|e| matches!(e, DiffEntry::Removed { .. }))
209 .count();
210 let modified = d
211 .iter()
212 .filter(|e| matches!(e, DiffEntry::Modified { .. }))
213 .count();
214
215 assert_eq!(added, 1);
216 assert_eq!(removed, 1);
217 assert_eq!(modified, 1);
218 }
219}