use std::{collections::BTreeMap, fs, path::Path};
use crate::{EntryKind, GitError, IndexEntry, ObjectKind, Repository, Result, error::invalid};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StatusKind {
Unmodified,
Added,
Modified,
Deleted,
TypeChanged,
Unmerged,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StatusEntry {
pub path: Vec<u8>,
pub index: StatusKind,
pub worktree: StatusKind,
}
#[derive(Clone, Copy)]
struct HeadEntry {
mode: u32,
id: crate::ObjectId,
}
impl Repository {
pub fn status(&self) -> Result<Vec<StatusEntry>> {
let work_dir = self
.work_dir()
.ok_or_else(|| GitError::Unsupported("status in a bare repository".to_owned()))?;
let index = self.index_shared()?;
let mut head = self.head_entries()?;
let mut result = Vec::new();
for entry in index.entries() {
if entry.stage > 0 {
result.push(StatusEntry {
path: entry.path.clone(),
index: StatusKind::Unmerged,
worktree: StatusKind::Unmerged,
});
continue;
}
let staged = staged(entry, head.remove(&entry.path));
let working = working(self, work_dir, entry)?;
if staged != StatusKind::Unmodified || working != StatusKind::Unmodified {
result.push(StatusEntry {
path: entry.path.clone(),
index: staged,
worktree: working,
});
}
}
result.extend(head.into_keys().map(|path| StatusEntry {
path,
index: StatusKind::Deleted,
worktree: StatusKind::Unmodified,
}));
result.sort_unstable_by(|left, right| left.path.cmp(&right.path));
Ok(result)
}
fn head_entries(&self) -> Result<BTreeMap<Vec<u8>, HeadEntry>> {
let Some(head) = self.head()?.target else {
return Ok(BTreeMap::new());
};
let root = self.commit(head)?.tree;
let mut pending = vec![(Vec::new(), root, 0_usize)];
let mut result = BTreeMap::new();
while let Some((prefix, tree, depth)) = pending.pop() {
if depth > self.limits().max_tree_depth {
return Err(GitError::LimitExceeded {
resource: "status tree depth",
limit: self.limits().max_tree_depth,
});
}
for entry in self.tree(tree)?.entries {
let mut path = prefix.clone();
if !path.is_empty() {
path.push(b'/');
}
path.extend(&entry.name);
if entry.kind == EntryKind::Tree {
pending.push((path, entry.id, depth + 1));
} else {
result.insert(
path,
HeadEntry {
mode: entry.mode,
id: entry.id,
},
);
}
}
}
Ok(result)
}
}
fn staged(index: &IndexEntry, head: Option<HeadEntry>) -> StatusKind {
let Some(head) = head else {
return StatusKind::Added;
};
if mode_kind(index.mode) != mode_kind(head.mode) {
StatusKind::TypeChanged
} else if index.id != head.id || index.mode != head.mode {
StatusKind::Modified
} else {
StatusKind::Unmodified
}
}
fn working(repository: &Repository, root: &Path, entry: &IndexEntry) -> Result<StatusKind> {
if entry.skip_worktree || entry.intent_to_add {
return Ok(StatusKind::Unmodified);
}
let path_text = std::str::from_utf8(&entry.path)
.map_err(|_| GitError::Unsupported("non-UTF-8 status path".to_owned()))?;
let path = root.join(path_text);
let metadata = match fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(StatusKind::Deleted);
}
Err(error) => return Err(error.into()),
};
match mode_kind(entry.mode) {
0o100_000 if metadata.is_file() => compare_file(repository, &path, entry),
0o120_000 if metadata.file_type().is_symlink() => {
let target = fs::read_link(path)?;
let target = target
.to_str()
.ok_or_else(|| GitError::Unsupported("non-UTF-8 symlink target".to_owned()))?;
compare_bytes(repository, target.as_bytes(), entry)
}
0o160_000 if metadata.is_dir() => Err(GitError::Unsupported(
"submodule worktree status".to_owned(),
)),
_ => Ok(StatusKind::TypeChanged),
}
}
fn compare_file(repository: &Repository, path: &Path, entry: &IndexEntry) -> Result<StatusKind> {
let length = usize::try_from(fs::metadata(path)?.len())
.map_err(|_| invalid("working file length overflow"))?;
if length > repository.limits().max_object_bytes {
return Err(GitError::LimitExceeded {
resource: "status file bytes",
limit: repository.limits().max_object_bytes,
});
}
compare_bytes(repository, &fs::read(path)?, entry)
}
fn compare_bytes(repository: &Repository, actual: &[u8], entry: &IndexEntry) -> Result<StatusKind> {
let object = repository.object(entry.id)?;
if object.kind != ObjectKind::Blob {
return Err(invalid("index entry does not reference a blob"));
}
Ok(if actual == object.data {
StatusKind::Unmodified
} else {
StatusKind::Modified
})
}
const fn mode_kind(mode: u32) -> u32 {
mode & 0o170_000
}