use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StatusEntry {
pub index: StatusKind,
pub worktree: StatusKind,
pub path: String,
pub original_path: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum StatusKind {
Unmodified,
Modified,
Added,
Deleted,
Renamed,
Copied,
Unmerged,
Untracked,
Ignored,
TypeChanged,
Other(char),
}
impl From<char> for StatusKind {
fn from(c: char) -> Self {
match c {
' ' => Self::Unmodified,
'M' => Self::Modified,
'A' => Self::Added,
'D' => Self::Deleted,
'R' => Self::Renamed,
'C' => Self::Copied,
'U' => Self::Unmerged,
'?' => Self::Untracked,
'!' => Self::Ignored,
'T' => Self::TypeChanged,
c => Self::Other(c),
}
}
}
pub fn parse_status(input: &str) -> Result<Vec<StatusEntry>> {
let mut out = Vec::new();
let mut iter = input.split('\0').peekable();
while let Some(record) = iter.next() {
if record.is_empty() {
continue;
}
let mut chars = record.chars();
let x = chars
.next()
.ok_or_else(|| Error::parse_error("status entry missing X field"))?;
let y = chars
.next()
.ok_or_else(|| Error::parse_error("status entry missing Y field"))?;
if chars.next() != Some(' ') {
return Err(Error::parse_error(
"status entry missing space after XY field",
));
}
let path: String = chars.collect();
let kind_x = StatusKind::from(x);
let kind_y = StatusKind::from(y);
let original = if matches!(kind_x, StatusKind::Renamed | StatusKind::Copied)
|| matches!(kind_y, StatusKind::Renamed | StatusKind::Copied)
{
iter.next().map(str::to_string)
} else {
None
};
out.push(StatusEntry {
index: kind_x,
worktree: kind_y,
path,
original_path: original,
});
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_simple_entries() {
let input = "MM a.txt\0A b.txt\0?? c.txt\0";
let entries = parse_status(input).unwrap();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].path, "a.txt");
assert_eq!(entries[1].index, StatusKind::Added);
assert_eq!(entries[1].worktree, StatusKind::Unmodified);
assert_eq!(entries[2].index, StatusKind::Untracked);
}
#[test]
fn parses_rename_with_original() {
let input = "R new.txt\0old.txt\0";
let entries = parse_status(input).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].index, StatusKind::Renamed);
assert_eq!(entries[0].path, "new.txt");
assert_eq!(entries[0].original_path.as_deref(), Some("old.txt"));
}
#[test]
fn empty_input_yields_no_entries() {
assert!(parse_status("").unwrap().is_empty());
}
#[test]
fn malformed_missing_space_errors() {
let input = "MMa.txt\0";
assert!(parse_status(input).is_err());
}
}