1use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum DiffKind {
12 Added,
14 Modified,
16 Deleted,
18 #[default]
20 Unchanged,
21}
22
23impl DiffKind {
24 pub fn is_change(&self) -> bool {
26 !matches!(self, DiffKind::Unchanged)
27 }
28}
29
30impl fmt::Display for DiffKind {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 DiffKind::Added => write!(f, "added"),
34 DiffKind::Modified => write!(f, "modified"),
35 DiffKind::Deleted => write!(f, "deleted"),
36 DiffKind::Unchanged => write!(f, "unchanged"),
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct FileChange {
44 pub path: String,
46 pub kind: DiffKind,
48}
49
50impl FileChange {
51 pub fn new(path: impl Into<String>, kind: DiffKind) -> Self {
53 Self {
54 path: path.into(),
55 kind,
56 }
57 }
58
59 pub fn added(path: impl Into<String>) -> Self {
61 Self::new(path, DiffKind::Added)
62 }
63
64 pub fn modified(path: impl Into<String>) -> Self {
66 Self::new(path, DiffKind::Modified)
67 }
68
69 pub fn deleted(path: impl Into<String>) -> Self {
71 Self::new(path, DiffKind::Deleted)
72 }
73
74 pub fn into_tuple(self) -> (String, DiffKind) {
76 (self.path, self.kind)
77 }
78
79 pub fn from_tuple((path, kind): (String, DiffKind)) -> Self {
81 Self { path, kind }
82 }
83}
84
85impl From<(String, DiffKind)> for FileChange {
86 fn from(tuple: (String, DiffKind)) -> Self {
87 Self::from_tuple(tuple)
88 }
89}
90
91impl From<FileChange> for (String, DiffKind) {
92 fn from(change: FileChange) -> Self {
93 change.into_tuple()
94 }
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct FileChangeSet {
100 changes: Vec<FileChange>,
101}
102
103impl FileChangeSet {
104 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn with_capacity(capacity: usize) -> Self {
111 Self {
112 changes: Vec::with_capacity(capacity),
113 }
114 }
115
116 pub fn push(&mut self, change: FileChange) {
118 self.changes.push(change);
119 }
120
121 pub fn push_added(&mut self, path: impl Into<String>) {
123 self.changes.push(FileChange::added(path));
124 }
125
126 pub fn push_modified(&mut self, path: impl Into<String>) {
128 self.changes.push(FileChange::modified(path));
129 }
130
131 pub fn push_deleted(&mut self, path: impl Into<String>) {
133 self.changes.push(FileChange::deleted(path));
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.changes.is_empty()
139 }
140
141 pub fn len(&self) -> usize {
143 self.changes.len()
144 }
145
146 pub fn iter(&self) -> impl Iterator<Item = &FileChange> {
148 self.changes.iter()
149 }
150
151 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut FileChange> {
153 self.changes.iter_mut()
154 }
155
156 pub fn into_vec(self) -> Vec<FileChange> {
158 self.changes
159 }
160
161 pub fn added(&self) -> impl Iterator<Item = &FileChange> {
163 self.changes.iter().filter(|c| c.kind == DiffKind::Added)
164 }
165
166 pub fn modified(&self) -> impl Iterator<Item = &FileChange> {
168 self.changes.iter().filter(|c| c.kind == DiffKind::Modified)
169 }
170
171 pub fn deleted(&self) -> impl Iterator<Item = &FileChange> {
173 self.changes.iter().filter(|c| c.kind == DiffKind::Deleted)
174 }
175
176 pub fn is_clean(&self) -> bool {
178 self.changes.is_empty()
179 }
180
181 pub fn added_count(&self) -> usize {
183 self.added().count()
184 }
185
186 pub fn modified_count(&self) -> usize {
188 self.modified().count()
189 }
190
191 pub fn deleted_count(&self) -> usize {
193 self.deleted().count()
194 }
195}
196
197impl Extend<FileChange> for FileChangeSet {
198 fn extend<T: IntoIterator<Item = FileChange>>(&mut self, iter: T) {
199 self.changes.extend(iter);
200 }
201}
202
203impl From<Vec<FileChange>> for FileChangeSet {
204 fn from(changes: Vec<FileChange>) -> Self {
205 Self { changes }
206 }
207}
208
209impl From<Vec<(String, DiffKind)>> for FileChangeSet {
210 fn from(changes: Vec<(String, DiffKind)>) -> Self {
211 Self {
212 changes: changes.into_iter().map(FileChange::from_tuple).collect(),
213 }
214 }
215}
216
217impl IntoIterator for FileChangeSet {
218 type Item = FileChange;
219 type IntoIter = std::vec::IntoIter<FileChange>;
220
221 fn into_iter(self) -> Self::IntoIter {
222 self.changes.into_iter()
223 }
224}
225
226impl<'a> IntoIterator for &'a FileChangeSet {
227 type Item = &'a FileChange;
228 type IntoIter = std::slice::Iter<'a, FileChange>;
229
230 fn into_iter(self) -> Self::IntoIter {
231 self.changes.iter()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_file_change_creation() {
241 let change = FileChange::added("src/main.rs");
242 assert_eq!(change.path, "src/main.rs");
243 assert_eq!(change.kind, DiffKind::Added);
244
245 let change = FileChange::modified("src/lib.rs");
246 assert_eq!(change.kind, DiffKind::Modified);
247
248 let change = FileChange::deleted("old.txt");
249 assert_eq!(change.kind, DiffKind::Deleted);
250 }
251
252 #[test]
253 fn test_file_change_tuple_conversion() {
254 let change = FileChange::added("foo.txt");
255 let tuple: (String, DiffKind) = change.into();
256 assert_eq!(tuple, (String::from("foo.txt"), DiffKind::Added));
257
258 let change: FileChange = (String::from("bar.txt"), DiffKind::Modified).into();
259 assert_eq!(change.path, "bar.txt");
260 assert_eq!(change.kind, DiffKind::Modified);
261 }
262
263 #[test]
264 fn test_file_change_set_basic() {
265 let mut set = FileChangeSet::new();
266 assert!(set.is_empty());
267 assert_eq!(set.len(), 0);
268
269 set.push_added("a.txt");
270 set.push_modified("b.txt");
271 set.push_deleted("c.txt");
272
273 assert!(!set.is_empty());
274 assert_eq!(set.len(), 3);
275 assert_eq!(set.added_count(), 1);
276 assert_eq!(set.modified_count(), 1);
277 assert_eq!(set.deleted_count(), 1);
278 }
279
280 #[test]
281 fn test_file_change_set_iterators() {
282 let mut set = FileChangeSet::new();
283 set.push_added("a.txt");
284 set.push_modified("b.txt");
285 set.push_deleted("c.txt");
286
287 let added: Vec<_> = set.added().map(|c| &c.path).collect();
288 assert_eq!(added, vec!["a.txt"]);
289
290 let modified: Vec<_> = set.modified().map(|c| &c.path).collect();
291 assert_eq!(modified, vec!["b.txt"]);
292
293 let deleted: Vec<_> = set.deleted().map(|c| &c.path).collect();
294 assert_eq!(deleted, vec!["c.txt"]);
295 }
296
297 #[test]
298 fn test_file_change_set_conversion() {
299 let tuples = vec![
300 (String::from("a.txt"), DiffKind::Added),
301 (String::from("b.txt"), DiffKind::Modified),
302 ];
303 let set = FileChangeSet::from(tuples);
304
305 assert_eq!(set.len(), 2);
306 assert_eq!(set.added_count(), 1);
307 assert_eq!(set.modified_count(), 1);
308 }
309
310 #[test]
311 fn test_diff_kind_display() {
312 assert_eq!(DiffKind::Added.to_string(), "added");
313 assert_eq!(DiffKind::Modified.to_string(), "modified");
314 assert_eq!(DiffKind::Deleted.to_string(), "deleted");
315 assert_eq!(DiffKind::Unchanged.to_string(), "unchanged");
316 }
317
318 #[test]
319 fn test_diff_kind_is_change() {
320 assert!(!DiffKind::Unchanged.is_change());
321 assert!(DiffKind::Added.is_change());
322 assert!(DiffKind::Modified.is_change());
323 assert!(DiffKind::Deleted.is_change());
324 }
325}