use std::path::{Path, PathBuf};
use git2::{Index, Repository};
use crate::error::HoldError;
pub(crate) struct TrackedFiles {
pub(crate) repo_root: PathBuf,
pub(crate) files: Vec<PathBuf>,
pub(crate) symlink_count: usize,
pub(crate) inaccessible_files: Vec<PathBuf>,
}
impl TrackedFiles {
pub(crate) fn processable_count(&self) -> usize {
self.files.len() + self.inaccessible_files.len()
}
}
pub(crate) fn discover_tracked_files(repo_path: &Path) -> Result<TrackedFiles, HoldError> {
let repo = Repository::discover(repo_path)
.map_err(|_| HoldError::RepoNotFound(repo_path.to_path_buf()))?;
let repo_root = repo
.workdir()
.ok_or_else(|| HoldError::RepoNotFound(repo_path.to_path_buf()))?
.to_path_buf();
let index = repo.index().map_err(HoldError::IndexError)?;
let (tracked_files, symlink_count, inaccessible_files) =
collect_index_paths(&index, &repo_root)?;
Ok(TrackedFiles {
repo_root,
files: tracked_files,
symlink_count,
inaccessible_files,
})
}
fn collect_index_paths(
index: &Index,
repo_root: &Path,
) -> Result<(Vec<PathBuf>, usize, Vec<PathBuf>), HoldError> {
let mut paths = Vec::new();
let mut symlink_count = 0;
let mut inaccessible_paths = Vec::new();
for entry in index.iter() {
if entry.mode == 0o160000 {
continue;
}
let path = entry.path;
let path_str = std::str::from_utf8(&path).map_err(|e| HoldError::InvalidPath {
message: format!("Invalid UTF-8 in path: {e}"),
})?;
let path_buf = PathBuf::from(path_str);
let full_path = repo_root.join(&path_buf);
match std::fs::symlink_metadata(&full_path) {
Ok(metadata) => {
if metadata.is_symlink() {
symlink_count += 1;
continue; }
}
Err(_) => {
inaccessible_paths.push(path_buf);
continue; }
}
paths.push(path_buf);
}
Ok((paths, symlink_count, inaccessible_paths))
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::TempDir;
use super::*;
fn setup_test_repo() -> (TempDir, Repository) {
let temp_dir = TempDir::new().unwrap();
let repo = Repository::init(temp_dir.path()).unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "test content").unwrap();
let mut index = repo.index().unwrap();
index.add_path(Path::new("test.txt")).unwrap();
index.write().unwrap();
(temp_dir, repo)
}
#[test]
fn test_discover_tracked_files() {
let (temp_dir, _repo) = setup_test_repo();
let discovered = discover_tracked_files(temp_dir.path()).unwrap();
assert_eq!(
discovered.repo_root.canonicalize().unwrap(),
temp_dir.path().canonicalize().unwrap()
);
assert_eq!(discovered.files.len(), 1);
assert!(discovered.files[0].ends_with("test.txt"));
assert_eq!(discovered.symlink_count, 0);
assert!(discovered.inaccessible_files.is_empty());
assert_eq!(discovered.processable_count(), 1);
}
#[test]
fn test_discover_tracked_files_reports_missing_tracked_files() {
let (temp_dir, _repo) = setup_test_repo();
fs::remove_file(temp_dir.path().join("test.txt")).unwrap();
let discovered = discover_tracked_files(temp_dir.path()).unwrap();
assert!(discovered.files.is_empty());
assert_eq!(
discovered.inaccessible_files,
vec![PathBuf::from("test.txt")]
);
assert_eq!(discovered.processable_count(), 1);
}
#[test]
fn test_repo_not_found() {
let temp_dir = TempDir::new().unwrap();
let result = discover_tracked_files(temp_dir.path());
assert!(matches!(result, Err(HoldError::RepoNotFound { .. })));
}
}