#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct StatusEntry {
pub path: String,
pub old_path: Option<String>,
pub xy: [char; 2],
pub is_rename: bool,
pub is_unmerged: bool,
}
pub fn parse_status_porcelain_z(data: &[u8]) -> Vec<StatusEntry> {
let parts: Vec<&[u8]> = data.split(|b| *b == 0).filter(|p| !p.is_empty()).collect();
let mut out = Vec::new();
let mut i = 0;
while i < parts.len() {
let rec = parts[i];
if rec.len() < 3 {
i += 1;
continue;
}
let x = rec[0] as char;
let y = rec[1] as char;
let path_bytes = if rec.len() > 3 && rec[2] == b' ' {
&rec[3..]
} else {
&rec[2..]
};
let path = std::str::from_utf8(path_bytes)
.map(str::to_string)
.unwrap_or_default();
let is_rename = x == 'R' || y == 'R';
let (final_path, old_path) = if is_rename {
if let Some(next) = parts.get(i + 1) {
let new_path = std::str::from_utf8(next)
.map(str::to_string)
.unwrap_or_default();
(new_path, Some(path))
} else {
(path, None)
}
} else {
(path, None)
};
let merged = StatusEntry {
path: final_path,
old_path,
xy: [x, y],
is_rename,
is_unmerged: detect_unmerged(x, y),
};
if is_rename && i + 1 < parts.len() {
out.push(merged);
i += 2;
} else {
out.push(merged);
i += 1;
}
}
out
}
fn detect_unmerged(x: char, y: char) -> bool {
matches!(
(x, y),
('U', _) | (_, 'U') | ('A', 'A') | ('A', 'D') | ('D', 'A') | ('D', 'D')
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn porcelain_z_parse_handles_rename_and_unmerged() {
let mut bytes: Vec<u8> = Vec::new();
bytes.extend_from_slice(b"M file.rs\0");
bytes.extend_from_slice(b"R old.rs\0new.rs\0");
bytes.extend_from_slice(b"UU conflicted.txt\0");
let entries = parse_status_porcelain_z(&bytes);
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].path, "file.rs");
assert_eq!(entries[0].old_path, None);
assert_eq!(entries[0].xy, ['M', ' ']);
assert!(!entries[0].is_rename);
assert!(!entries[0].is_unmerged);
assert_eq!(entries[1].path, "new.rs");
assert_eq!(entries[1].old_path.as_deref(), Some("old.rs"));
assert_eq!(entries[1].xy, ['R', ' ']);
assert!(entries[1].is_rename);
assert!(!entries[1].is_unmerged);
assert_eq!(entries[2].path, "conflicted.txt");
assert_eq!(entries[2].old_path, None);
assert_eq!(entries[2].xy, ['U', 'U']);
assert!(!entries[2].is_rename);
assert!(entries[2].is_unmerged);
}
#[test]
fn porcelain_z_parse_flags_au_and_ut_as_unmerged() {
let mut bytes: Vec<u8> = Vec::new();
bytes.extend_from_slice(b"AU halfmerged.txt\0");
bytes.extend_from_slice(b"UT typechanged.bin\0");
let entries = parse_status_porcelain_z(&bytes);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].xy, ['A', 'U']);
assert!(!entries[0].is_rename);
assert!(entries[0].is_unmerged, "AU must be flagged unmerged");
assert_eq!(entries[1].xy, ['U', 'T']);
assert!(!entries[1].is_rename);
assert!(entries[1].is_unmerged, "UT must be flagged unmerged");
}
}