1use std::path::Path;
4
5use crate::{ConvertError, Converted, Details, FileDiff};
6
7const HEADER: &str = "Diff in ";
8const AT_LINE: &str = " at line ";
9
10pub(crate) fn convert(bytes: &[u8], root: &Path) -> Result<Converted, ConvertError> {
20 let text = str::from_utf8(bytes)?;
21 let mut items: Vec<FileDiff> = Vec::new();
22 let mut current = None;
23 for line in text.split_inclusive('\n') {
24 if let Some(header) = line.strip_prefix(HEADER) {
25 let (path, rest) = split_header(header);
26 let file = relative(path, root);
27 let index = items
28 .iter()
29 .position(|item| item.file == file)
30 .unwrap_or(items.len());
31 if index == items.len() {
32 items.push(FileDiff {
33 file: file.clone(),
34 text: String::new(),
35 });
36 }
37 current = Some(index);
38 if let Some(item) = items.get_mut(index) {
39 item.text.extend([HEADER, &file, rest]);
40 }
41 } else if let Some(item) = current.and_then(|index| items.get_mut(index)) {
42 item.text.push_str(line);
43 }
44 }
45 Ok(Converted {
46 details: Details::Diff { items },
47 terminal: text.to_owned(),
48 })
49}
50
51fn split_header(header: &str) -> (&str, &str) {
53 let body = header.trim_end();
54 let path = body.find(AT_LINE).map_or_else(
55 || {
56 body.strip_suffix(':')
57 .and_then(|body| body.rsplit_once(':'))
58 .map_or(body, |(path, _)| path)
59 },
60 |index| body.get(..index).unwrap_or(body),
61 );
62 header.split_at(path.len())
63}
64
65fn relative(path: &str, root: &Path) -> String {
66 Path::new(path).strip_prefix(root).map_or_else(
67 |_| path.to_owned(),
68 |relative| relative.display().to_string(),
69 )
70}
71
72#[cfg(test)]
73mod tests {
74 use std::path::Path;
75
76 use super::convert;
77 use crate::{ConvertError, Details};
78
79 fn items(input: &str) -> Vec<(String, String)> {
80 let Details::Diff { items } = convert(input.as_bytes(), Path::new("/work"))
81 .unwrap()
82 .details
83 else {
84 panic!("expected a diff");
85 };
86 items
87 .into_iter()
88 .map(|item| (item.file, item.text))
89 .collect()
90 }
91
92 #[test]
93 fn empty_output_has_no_items() {
94 assert_eq!(items("").len(), 0);
95 }
96
97 #[test]
98 fn two_files_become_two_items() {
99 let input = "Diff in /work/src/a.rs at line 1:\n-let x=1;\n+let x = 1;\nDiff in \
100 /work/src/b.rs at line 7:\n-fn f(){}\n+fn f() {}\n";
101 let items = items(input);
102 assert_eq!(items.len(), 2);
103 assert_eq!(items[0].0, "src/a.rs");
104 assert_eq!(
105 items[0].1,
106 "Diff in src/a.rs at line 1:\n-let x=1;\n+let x = 1;\n"
107 );
108 assert_eq!(items[1].0, "src/b.rs");
109 }
110
111 #[test]
112 fn hunks_of_one_file_merge_and_foreign_paths_stay() {
113 let input = "Diff in /work/a.rs at line 1:\n-a\n+b\nDiff in /other/c.rs at line \
114 2:\n-c\n+d\nDiff in /work/a.rs at line 9:\n-e\n+f\n";
115 let items = items(input);
116 assert_eq!(items.len(), 2);
117 assert_eq!(items[0].0, "a.rs");
118 assert_eq!(items[0].1.matches("Diff in").count(), 2);
119 assert_eq!(items[1].0, "/other/c.rs");
120 }
121
122 #[test]
123 fn the_colon_header_form_is_understood() {
124 let items = items("Diff in /work/a.rs:12:\n-a\n+b\n");
125 assert_eq!(items[0].0, "a.rs");
126 assert_eq!(items[0].1, "Diff in a.rs:12:\n-a\n+b\n");
127 }
128
129 #[test]
130 fn invalid_utf8_is_an_error() {
131 assert!(matches!(
132 convert(b"\xff", Path::new("/")),
133 Err(ConvertError::Utf8(_))
134 ));
135 }
136}