use std::path::Path;
use crate::error::Result;
use crate::git::cli::GitCli;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StatusEntry {
pub(crate) marker: char,
pub(crate) path: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct StatusSummary {
pub(crate) dirty: bool,
pub(crate) has_untracked: bool,
pub(crate) entries: Vec<StatusEntry>,
}
pub(crate) fn parse_status_porcelain(z: &str) -> StatusSummary {
let mut summary = StatusSummary::default();
let mut fields = z.split('\0');
while let Some(field) = fields.next() {
if field.len() < 3 {
continue;
}
let xy = &field[..2];
let path = &field[3..];
if xy == "??" {
summary.has_untracked = true;
summary.entries.push(StatusEntry {
marker: '?',
path: path.to_string(),
});
} else if xy == "!!" {
} else {
summary.dirty = true;
summary.entries.push(StatusEntry {
marker: 'M',
path: path.to_string(),
});
if xy.contains('R') || xy.contains('C') {
fields.next();
}
}
}
summary
}
pub(crate) fn status_of(git: &dyn GitCli, worktree_dir: &Path) -> Result<StatusSummary> {
let output = git.run(worktree_dir, &["status", "--porcelain=v1", "-z"])?;
Ok(parse_status_porcelain(&output))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::cli::RealGit;
use crate::testutil::TestRepo;
#[test]
fn clean_worktree_is_not_dirty() {
let s = parse_status_porcelain("");
assert!(!s.dirty);
assert!(!s.has_untracked);
assert!(s.entries.is_empty());
}
#[test]
fn parses_modified_staged_and_untracked() {
let z = " M src/a\0M src/b\0?? scratch\0";
let s = parse_status_porcelain(z);
assert!(s.dirty);
assert!(s.has_untracked);
assert_eq!(s.entries.len(), 3);
assert_eq!(
s.entries[0],
StatusEntry {
marker: 'M',
path: "src/a".into()
}
);
assert_eq!(
s.entries[2],
StatusEntry {
marker: '?',
path: "scratch".into()
}
);
}
#[test]
fn untracked_only_is_not_dirty() {
let s = parse_status_porcelain("?? new.txt\0");
assert!(!s.dirty);
assert!(s.has_untracked);
}
#[test]
fn ignored_entries_are_skipped() {
let s = parse_status_porcelain("!! target/\0");
assert!(!s.dirty);
assert!(!s.has_untracked);
assert!(s.entries.is_empty());
}
#[test]
fn rename_consumes_source_field() {
let z = "R new\0old\0?? u\0";
let s = parse_status_porcelain(z);
assert!(s.dirty);
assert!(s.has_untracked);
assert_eq!(s.entries.len(), 2);
assert_eq!(s.entries[0].path, "new");
assert_eq!(s.entries[1].path, "u");
}
#[test]
fn status_of_real_repo() {
let repo = TestRepo::init();
let s = status_of(&RealGit, repo.root()).unwrap();
assert!(!s.dirty && !s.has_untracked);
repo.write("README.md", "changed\n");
let s = status_of(&RealGit, repo.root()).unwrap();
assert!(s.dirty);
assert!(!s.has_untracked);
repo.write("scratch.txt", "x\n");
let s = status_of(&RealGit, repo.root()).unwrap();
assert!(s.dirty);
assert!(s.has_untracked);
}
}