use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc::Sender;
use tokio::task;
use crate::app::state::{GitDirState, GitFileStatus};
use crate::workers::WorkerMsg;
#[derive(Debug, Clone)]
pub struct CachedGit {
dir_state: Option<GitDirState>,
file_statuses: Vec<(String, GitFileStatus)>,
}
pub type GitCache = Arc<Mutex<HashMap<PathBuf, CachedGit>>>;
pub fn new_cache() -> GitCache {
Arc::new(Mutex::new(HashMap::new()))
}
pub fn invalidate(cache: &GitCache, path: &Path) {
if let Ok(mut c) = cache.lock() {
c.remove(path);
tracing::debug!(?path, "git cache invalidated");
}
}
pub fn spawn_git_status(path: PathBuf, tx: Sender<WorkerMsg>, cache: GitCache) {
if let Ok(c) = cache.lock() {
if let Some(cached) = c.get(&path) {
let msg = WorkerMsg::Git {
path: path.clone(),
state: cached.dir_state.clone(),
file_statuses: cached.file_statuses.clone(),
};
let tx2 = tx.clone();
tokio::spawn(async move {
let _ = tx2.send(msg).await;
});
return;
}
}
tokio::spawn(async move {
let path2 = path.clone();
let result = task::spawn_blocking(move || compute_git_status(&path2)).await;
let (dir_state, file_statuses) = match result {
Ok(r) => r,
Err(e) => {
tracing::debug!("git worker task panicked: {e}");
return;
}
};
if let Ok(mut c) = cache.lock() {
c.insert(
path.clone(),
CachedGit {
dir_state: dir_state.clone(),
file_statuses: file_statuses.clone(),
},
);
}
let _ = tx
.send(WorkerMsg::Git {
path,
state: dir_state,
file_statuses,
})
.await;
});
}
fn compute_git_status(path: &Path) -> (Option<GitDirState>, Vec<(String, GitFileStatus)>) {
let repo = match gix::discover(path) {
Ok(r) => r,
Err(_) => {
tracing::debug!(?path, "no git repository found");
return (None, vec![]);
}
};
let branch = match repo.head_name() {
Ok(Some(name)) => name.shorten().to_string(),
Ok(None) => "HEAD".to_owned(), Err(e) => {
tracing::debug!(?path, "failed to read HEAD: {e}");
"HEAD".to_owned()
}
};
let is_dirty = match repo.is_dirty() {
Ok(d) => d,
Err(e) => {
tracing::debug!(?path, "failed to check dirty state: {e}");
false
}
};
let dir_state = Some(GitDirState { branch, is_dirty });
let file_statuses = compute_file_statuses(&repo, path);
(dir_state, file_statuses)
}
fn compute_file_statuses(repo: &gix::Repository, dir: &Path) -> Vec<(String, GitFileStatus)> {
let workdir = match repo.workdir() {
Some(p) => p.to_owned(),
None => return vec![], };
let index = match repo.index_or_empty() {
Ok(i) => i,
Err(e) => {
tracing::debug!("failed to read git index: {e}");
return vec![];
}
};
let mut statuses: Vec<(String, GitFileStatus)> = Vec::new();
for entry in index.entries() {
let rel_path_bytes = entry.path(&index);
let rel_path_str = match std::str::from_utf8(rel_path_bytes) {
Ok(s) => s,
Err(_) => continue,
};
let abs_path = workdir.join(rel_path_str);
let parent = match abs_path.parent() {
Some(p) => p,
None => continue,
};
if parent != dir {
continue;
}
let file_name = match abs_path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_owned(),
None => continue,
};
let status = if abs_path.exists() {
GitFileStatus::Clean
} else {
GitFileStatus::Deleted
};
statuses.push((file_name, status));
}
statuses
}