1use std::path::PathBuf;
2
3use crate::types::{FileChange, FileChangeKind};
4
5pub fn parse_git_diff_name_status(output: &str) -> Vec<FileChange> {
15 let mut changes = Vec::new();
16 for line in output.lines() {
17 let line = line.trim_end();
18 if line.is_empty() {
19 continue;
20 }
21
22 let mut parts = line.split('\t');
23 let Some(status) = parts.next() else { continue };
24
25 let kind = match status.chars().next() {
26 Some('M') => FileChangeKind::Modified,
27 Some('A') => FileChangeKind::Added,
28 Some('D') => FileChangeKind::Deleted,
29 Some('R') => FileChangeKind::Renamed,
30 Some('C') => FileChangeKind::Copied,
31 _ => continue,
32 };
33
34 match kind {
35 FileChangeKind::Renamed | FileChangeKind::Copied => {
36 let (Some(from), Some(to)) = (parts.next(), parts.next()) else {
37 continue;
38 };
39 changes.push(FileChange {
40 kind,
41 path: PathBuf::from(to),
42 from_path: Some(PathBuf::from(from)),
43 });
44 }
45 _ => {
46 let Some(path) = parts.next() else { continue };
47 changes.push(FileChange {
48 kind,
49 path: PathBuf::from(path),
50 from_path: None,
51 });
52 }
53 }
54 }
55 changes
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn empty() {
64 assert!(parse_git_diff_name_status("").is_empty());
65 }
66
67 #[test]
68 fn modified() {
69 let changes = parse_git_diff_name_status("M\tsrc/lib.rs");
70 assert_eq!(changes.len(), 1);
71 assert_eq!(changes[0].kind, FileChangeKind::Modified);
72 assert_eq!(changes[0].path, PathBuf::from("src/lib.rs"));
73 }
74
75 #[test]
76 fn added() {
77 let changes = parse_git_diff_name_status("A\tnew.rs");
78 assert_eq!(changes[0].kind, FileChangeKind::Added);
79 }
80
81 #[test]
82 fn deleted() {
83 let changes = parse_git_diff_name_status("D\told.rs");
84 assert_eq!(changes[0].kind, FileChangeKind::Deleted);
85 }
86
87 #[test]
88 fn renamed_with_similarity_score() {
89 let changes = parse_git_diff_name_status("R100\told.rs\tnew.rs");
90 assert_eq!(changes.len(), 1);
91 assert_eq!(changes[0].kind, FileChangeKind::Renamed);
92 assert_eq!(changes[0].path, PathBuf::from("new.rs"));
93 assert_eq!(changes[0].from_path, Some(PathBuf::from("old.rs")));
94 }
95
96 #[test]
97 fn copied_with_similarity_score() {
98 let changes = parse_git_diff_name_status("C75\tfrom.rs\tto.rs");
99 assert_eq!(changes[0].kind, FileChangeKind::Copied);
100 assert_eq!(changes[0].from_path, Some(PathBuf::from("from.rs")));
101 assert_eq!(changes[0].path, PathBuf::from("to.rs"));
102 }
103
104 #[test]
105 fn multiple_lines() {
106 let output = "M\ta.rs\nA\tb.rs\nD\tc.rs\nR100\told.rs\tnew.rs";
107 let changes = parse_git_diff_name_status(output);
108 assert_eq!(changes.len(), 4);
109 assert_eq!(changes[0].kind, FileChangeKind::Modified);
110 assert_eq!(changes[3].kind, FileChangeKind::Renamed);
111 }
112
113 #[test]
114 fn skips_blank_lines() {
115 let changes = parse_git_diff_name_status("\nM\ta.rs\n\nA\tb.rs\n");
116 assert_eq!(changes.len(), 2);
117 }
118
119 #[test]
120 fn skips_unknown_status() {
121 let changes = parse_git_diff_name_status("X\tfoo.rs\nM\tbar.rs");
122 assert_eq!(changes.len(), 1);
123 assert_eq!(changes[0].path, PathBuf::from("bar.rs"));
124 }
125
126 #[test]
127 fn rename_without_second_path_skipped() {
128 let changes = parse_git_diff_name_status("R100\tjust_one.rs");
129 assert!(changes.is_empty());
130 }
131
132 #[test]
133 fn path_with_spaces_preserved() {
134 let changes = parse_git_diff_name_status("M\tpath with spaces.rs");
135 assert_eq!(changes[0].path, PathBuf::from("path with spaces.rs"));
136 }
137}