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