git_spawn/parse/
status.rs1use crate::error::{Error, Result};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct StatusEntry {
15 pub index: StatusKind,
17 pub worktree: StatusKind,
19 pub path: String,
21 pub original_path: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub enum StatusKind {
29 Unmodified,
31 Modified,
33 Added,
35 Deleted,
37 Renamed,
39 Copied,
41 Unmerged,
43 Untracked,
45 Ignored,
47 TypeChanged,
49 Other(char),
51}
52
53impl From<char> for StatusKind {
54 fn from(c: char) -> Self {
55 match c {
56 ' ' => Self::Unmodified,
57 'M' => Self::Modified,
58 'A' => Self::Added,
59 'D' => Self::Deleted,
60 'R' => Self::Renamed,
61 'C' => Self::Copied,
62 'U' => Self::Unmerged,
63 '?' => Self::Untracked,
64 '!' => Self::Ignored,
65 'T' => Self::TypeChanged,
66 c => Self::Other(c),
67 }
68 }
69}
70
71pub fn parse_status(input: &str) -> Result<Vec<StatusEntry>> {
88 let mut out = Vec::new();
89 let mut iter = input.split('\0').peekable();
90 while let Some(record) = iter.next() {
91 if record.is_empty() {
92 continue;
93 }
94 let mut chars = record.chars();
95 let x = chars
96 .next()
97 .ok_or_else(|| Error::parse_error("status entry missing X field"))?;
98 let y = chars
99 .next()
100 .ok_or_else(|| Error::parse_error("status entry missing Y field"))?;
101 if chars.next() != Some(' ') {
103 return Err(Error::parse_error(
104 "status entry missing space after XY field",
105 ));
106 }
107 let path: String = chars.collect();
108 let kind_x = StatusKind::from(x);
109 let kind_y = StatusKind::from(y);
110 let original = if matches!(kind_x, StatusKind::Renamed | StatusKind::Copied) {
118 iter.next().map(str::to_string)
119 } else {
120 None
121 };
122 out.push(StatusEntry {
123 index: kind_x,
124 worktree: kind_y,
125 path,
126 original_path: original,
127 });
128 }
129 Ok(out)
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn parses_simple_entries() {
138 let input = "MM a.txt\0A b.txt\0?? c.txt\0";
139 let entries = parse_status(input).unwrap();
140 assert_eq!(entries.len(), 3);
141 assert_eq!(entries[0].path, "a.txt");
142 assert_eq!(entries[1].index, StatusKind::Added);
143 assert_eq!(entries[1].worktree, StatusKind::Unmodified);
144 assert_eq!(entries[2].index, StatusKind::Untracked);
145 }
146
147 #[test]
148 fn parses_rename_with_original() {
149 let input = "R new.txt\0old.txt\0";
150 let entries = parse_status(input).unwrap();
151 assert_eq!(entries.len(), 1);
152 assert_eq!(entries[0].index, StatusKind::Renamed);
153 assert_eq!(entries[0].path, "new.txt");
154 assert_eq!(entries[0].original_path.as_deref(), Some("old.txt"));
155 }
156
157 #[test]
158 fn parses_rename_with_worktree_modification() {
159 let input = "RM new.txt\0old.txt\0MM other.txt\0";
163 let entries = parse_status(input).unwrap();
164 assert_eq!(entries.len(), 2);
165 assert_eq!(entries[0].index, StatusKind::Renamed);
166 assert_eq!(entries[0].worktree, StatusKind::Modified);
167 assert_eq!(entries[0].path, "new.txt");
168 assert_eq!(entries[0].original_path.as_deref(), Some("old.txt"));
169 assert_eq!(entries[1].path, "other.txt");
170 assert_eq!(entries[1].original_path, None);
171 }
172
173 #[test]
174 fn parses_copy_with_original() {
175 let input = "C copy.txt\0src.txt\0";
176 let entries = parse_status(input).unwrap();
177 assert_eq!(entries.len(), 1);
178 assert_eq!(entries[0].index, StatusKind::Copied);
179 assert_eq!(entries[0].path, "copy.txt");
180 assert_eq!(entries[0].original_path.as_deref(), Some("src.txt"));
181 }
182
183 #[test]
184 fn empty_input_yields_no_entries() {
185 assert!(parse_status("").unwrap().is_empty());
186 }
187
188 #[test]
189 fn malformed_missing_space_errors() {
190 let input = "MMa.txt\0";
191 assert!(parse_status(input).is_err());
192 }
193}