1use crate::error::{Error, Result};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct DiffEntry {
13 pub kind: DiffKind,
15 pub path: String,
17 pub original_path: Option<String>,
19 pub similarity: Option<u32>,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub enum DiffKind {
27 Added,
29 Deleted,
31 Modified,
33 Renamed,
35 Copied,
37 TypeChanged,
39 Unmerged,
41 Other(char),
43}
44
45impl From<char> for DiffKind {
46 fn from(c: char) -> Self {
47 match c {
48 'A' => Self::Added,
49 'D' => Self::Deleted,
50 'M' => Self::Modified,
51 'R' => Self::Renamed,
52 'C' => Self::Copied,
53 'T' => Self::TypeChanged,
54 'U' => Self::Unmerged,
55 c => Self::Other(c),
56 }
57 }
58}
59
60pub fn parse_diff_name_status(input: &str) -> Result<Vec<DiffEntry>> {
78 let mut out = Vec::new();
79 let mut iter = input.split('\0');
80 while let Some(status) = iter.next() {
81 if status.is_empty() {
82 continue;
83 }
84 let mut chars = status.chars();
85 let first = chars
86 .next()
87 .ok_or_else(|| Error::parse_error("diff entry missing status character"))?;
88 let kind = DiffKind::from(first);
89 let similarity: Option<u32> = {
90 let rest: String = chars.collect();
91 if rest.is_empty() {
92 None
93 } else {
94 rest.parse().ok()
95 }
96 };
97 let is_rename_or_copy = matches!(kind, DiffKind::Renamed | DiffKind::Copied);
98 let (original, path) = if is_rename_or_copy {
99 let orig = iter
100 .next()
101 .ok_or_else(|| Error::parse_error("rename/copy missing original path"))?;
102 let new = iter
103 .next()
104 .ok_or_else(|| Error::parse_error("rename/copy missing new path"))?;
105 (Some(orig.to_string()), new.to_string())
106 } else {
107 let path = iter
108 .next()
109 .ok_or_else(|| Error::parse_error("diff entry missing path"))?;
110 (None, path.to_string())
111 };
112 out.push(DiffEntry {
113 kind,
114 path,
115 original_path: original,
116 similarity,
117 });
118 }
119 Ok(out)
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn simple_changes() {
128 let input = "M\0foo.txt\0A\0bar.txt\0D\0baz.txt\0";
129 let entries = parse_diff_name_status(input).unwrap();
130 assert_eq!(entries.len(), 3);
131 assert_eq!(entries[0].path, "foo.txt");
132 assert_eq!(entries[2].kind, DiffKind::Deleted);
133 }
134
135 #[test]
136 fn rename_with_similarity() {
137 let input = "R090\0a.rs\0b.rs\0";
138 let entries = parse_diff_name_status(input).unwrap();
139 assert_eq!(entries.len(), 1);
140 assert_eq!(entries[0].similarity, Some(90));
141 assert_eq!(entries[0].original_path.as_deref(), Some("a.rs"));
142 assert_eq!(entries[0].path, "b.rs");
143 }
144
145 #[test]
146 fn empty_ok() {
147 assert!(parse_diff_name_status("").unwrap().is_empty());
148 }
149}