use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use gix::bstr::ByteSlice;
use gix::Repository;
use endringer_core::types::{ChangeKind, StatusEntry, WorktreeStatus};
use crate::object::collect_blob_oids;
pub(crate) fn is_dirty(repo: &Repository) -> Result<bool> {
if repo.workdir().is_none() {
return Ok(false);
}
let status = worktree_status(repo)?;
Ok(!status.staged.is_empty() || !status.unstaged.is_empty())
}
pub(crate) fn worktree_status(repo: &Repository) -> Result<WorktreeStatus> {
if repo.workdir().is_none() {
return Ok(WorktreeStatus::default());
}
let index = repo.open_index().context("open git index")?;
let workdir = repo.workdir().expect("checked above");
let head_blobs: HashMap<Vec<u8>, gix::ObjectId> =
match repo.head().ok().and_then(|mut h| h.peel_to_commit().ok()) {
Some(commit) => {
let tree = commit.tree().context("HEAD tree")?;
collect_blob_oids(repo, tree.id)?
}
None => HashMap::new(),
};
let mut index_blobs: HashMap<Vec<u8>, gix::ObjectId> = HashMap::new();
for entry in index.entries() {
index_blobs.insert(entry.path(&index).to_vec(), entry.id);
}
let index_path_set: HashSet<Vec<u8>> = index_blobs.keys().cloned().collect();
let mut staged = Vec::new();
for (path, &index_oid) in &index_blobs {
match head_blobs.get(path.as_slice()) {
None => staged.push(make_entry(path, ChangeKind::Added)),
Some(&head_oid) if head_oid != index_oid => {
staged.push(make_entry(path, ChangeKind::Modified))
}
_ => {}
}
}
for path in head_blobs.keys() {
if !index_blobs.contains_key(path.as_slice()) {
staged.push(make_entry(path, ChangeKind::Deleted));
}
}
staged.sort_by(|a, b| a.path.cmp(&b.path));
let mut unstaged = Vec::new();
for entry in index.entries() {
let path_bytes = entry.path(&index);
let abs = workdir.join(Path::new(
path_bytes.to_os_str_lossy().as_ref() as &std::ffi::OsStr,
));
match std::fs::metadata(&abs) {
Err(_) => unstaged.push(make_entry(path_bytes, ChangeKind::Deleted)),
Ok(meta) => {
let cached_secs = entry.stat.mtime.secs;
let cached_size = entry.stat.size;
let disk_secs = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as u32);
let disk_size = meta.len() as u32;
let mtime_ok = disk_secs == Some(cached_secs);
let size_ok = disk_size == cached_size;
if !mtime_ok || !size_ok {
unstaged.push(make_entry(path_bytes, ChangeKind::Modified));
} else {
let disk_oid = blob_sha1_of_file(&abs);
if disk_oid.map_or(true, |oid| oid != entry.id) {
unstaged.push(make_entry(path_bytes, ChangeKind::Modified));
}
}
}
}
}
unstaged.sort_by(|a, b| a.path.cmp(&b.path));
let mut untracked = Vec::new();
let mut exclude_stack = repo
.excludes(
&*index,
None,
gix::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
)
.ok();
collect_untracked(
workdir,
workdir,
&index_path_set,
&mut exclude_stack,
&mut untracked,
)?;
untracked.sort();
Ok(WorktreeStatus { staged, unstaged, untracked })
}
fn make_entry(path_bytes: &[u8], kind: ChangeKind) -> StatusEntry {
let s = String::from_utf8_lossy(path_bytes);
StatusEntry {
path: PathBuf::from(s.replace('/', std::path::MAIN_SEPARATOR_STR).as_str()),
kind,
}
}
fn blob_sha1_of_file(path: &Path) -> Option<gix::ObjectId> {
let content = std::fs::read(path).ok()?;
let header = format!("blob {}\0", content.len());
let mut hasher = gix::hash::hasher(gix::hash::Kind::Sha1);
hasher.update(header.as_bytes());
hasher.update(&content);
hasher.try_finalize().ok()
}
fn collect_untracked(
workdir: &Path,
dir: &Path,
index_paths: &HashSet<Vec<u8>>,
exclude_stack: &mut Option<gix::AttributeStack<'_>>,
result: &mut Vec<PathBuf>,
) -> Result<()> {
for e in std::fs::read_dir(dir).with_context(|| format!("reading {}", dir.display()))? {
let e = e?;
let abs = e.path();
let rel = abs.strip_prefix(workdir).unwrap_or(&abs);
if rel.components().next().map_or(false, |c| c.as_os_str() == ".git") {
continue;
}
let ft = e.file_type()?;
if ft.is_dir() {
collect_untracked(workdir, &abs, index_paths, exclude_stack, result)?;
} else if ft.is_file() {
let rel_key = rel.to_str().unwrap_or("").replace('\\', "/").into_bytes();
if !index_paths.contains(&rel_key) {
let ignored = exclude_stack
.as_mut()
.and_then(|stack| stack.at_path(rel, None).ok())
.map_or(false, |platform| platform.is_excluded());
if !ignored {
result.push(rel.to_path_buf());
}
}
}
}
Ok(())
}
pub(crate) fn rich_worktree_status(
repo: &gix::Repository,
options: endringer_core::types::StatusOptions,
) -> anyhow::Result<endringer_core::types::RichWorktreeStatus> {
use endringer_core::types::{
ConflictStatus, FileStatusKind, RichStatusEntry, RichWorktreeStatus,
};
use std::collections::BTreeMap;
let simple = worktree_status(repo)?;
let mut entries: BTreeMap<std::path::PathBuf, RichStatusEntry> = BTreeMap::new();
for se in &simple.staged {
let kind = match se.kind {
endringer_core::types::ChangeKind::Added => FileStatusKind::Added,
endringer_core::types::ChangeKind::Modified => FileStatusKind::Modified,
endringer_core::types::ChangeKind::Deleted => FileStatusKind::Deleted,
};
entries.entry(se.path.clone()).or_insert_with(|| RichStatusEntry {
path: se.path.clone(), old_path: None,
index: None, worktree: None, conflict: None,
}).index = Some(kind);
}
for se in &simple.unstaged {
let kind = match se.kind {
endringer_core::types::ChangeKind::Added => FileStatusKind::Added,
endringer_core::types::ChangeKind::Modified => FileStatusKind::Modified,
endringer_core::types::ChangeKind::Deleted => FileStatusKind::Deleted,
};
entries.entry(se.path.clone()).or_insert_with(|| RichStatusEntry {
path: se.path.clone(), old_path: None,
index: None, worktree: None, conflict: None,
}).worktree = Some(kind);
}
if let Ok(Some(index)) = repo.try_index() {
let mut conflict_map: BTreeMap<std::path::PathBuf, Vec<u8>> = BTreeMap::new();
for entry in index.entries() {
let stage = entry.flags.stage();
if stage != gix::index::entry::Stage::Unconflicted {
let stage_num = match stage {
gix::index::entry::Stage::Base => 1u8,
gix::index::entry::Stage::Ours => 2u8,
gix::index::entry::Stage::Theirs => 3u8,
_ => continue,
};
if let Ok(p) = std::str::from_utf8(entry.path(&index)) {
conflict_map.entry(std::path::PathBuf::from(p))
.or_default().push(stage_num);
}
}
}
for (path, stages) in conflict_map {
entries.entry(path.clone()).or_insert_with(|| RichStatusEntry {
path, old_path: None, index: None, worktree: None, conflict: None,
}).conflict = Some(ConflictStatus { stages });
}
}
if options.include_untracked {
for path in &simple.untracked {
entries.entry(path.clone()).or_insert_with(|| RichStatusEntry {
path: path.clone(), old_path: None,
index: None, worktree: Some(FileStatusKind::Untracked), conflict: None,
});
}
}
let sorted_entries: Vec<RichStatusEntry> = entries.into_values().collect();
Ok(RichWorktreeStatus { entries: sorted_entries })
}