use std::fs;
use std::path::Path;
use ignore::WalkBuilder;
use super::error::{WorkspaceError, WorkspaceResult};
use super::registry::{WorkspaceRepoId, WorkspaceRepository};
use crate::config::buffers::max_repositories;
use crate::project::path_utils::is_ignored_dir;
const MANIFEST_FILE_NAME: &str = "manifest.json";
const GRAPH_DIR_SEGMENT: &str = "graph";
const SQRY_DIR_SEGMENT: &str = ".sqry";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscoveryMode {
IndexFiles,
GitRoots,
}
pub fn discover_repositories(
root: &Path,
mode: DiscoveryMode,
) -> WorkspaceResult<Vec<WorkspaceRepository>> {
let mut repositories = Vec::new();
let walker = WalkBuilder::new(root)
.hidden(false)
.ignore(false)
.git_ignore(true)
.git_exclude(true)
.parents(true)
.filter_entry(|entry| {
!is_ignored_dir(entry.file_name())
})
.build();
for result in walker {
let entry = match result {
Ok(ok) => ok,
Err(err) => {
let message = err.to_string();
let io_err = err
.into_io_error()
.unwrap_or_else(|| std::io::Error::other(message));
return Err(WorkspaceError::Discovery {
root: root.to_path_buf(),
source: io_err,
});
}
};
if entry.file_type().is_some_and(|ft| ft.is_dir()) {
continue;
}
if entry.file_name() != MANIFEST_FILE_NAME {
continue;
}
let manifest_path = entry.into_path();
let Some(graph_dir) = manifest_path.parent() else {
continue;
};
if graph_dir.file_name().and_then(|s| s.to_str()) != Some(GRAPH_DIR_SEGMENT) {
continue;
}
let Some(sqry_dir) = graph_dir.parent() else {
continue;
};
if sqry_dir.file_name().and_then(|s| s.to_str()) != Some(SQRY_DIR_SEGMENT) {
continue;
}
let Some(repo_root) = sqry_dir.parent().map(Path::to_path_buf) else {
continue;
};
if matches!(mode, DiscoveryMode::GitRoots) && !repo_root.join(".git").is_dir() {
continue;
}
let relative_path = repo_root.strip_prefix(root).unwrap_or(repo_root.as_path());
let repo_id = WorkspaceRepoId::new(relative_path);
let name = repo_root.file_name().map_or_else(
|| repo_id.as_str().to_string(),
|os| os.to_string_lossy().into_owned(),
);
let metadata = fs::metadata(&manifest_path);
let last_indexed_at = metadata.ok().and_then(|meta| meta.modified().ok());
let max_repos = max_repositories();
if repositories.len() >= max_repos {
return Err(WorkspaceError::TooManyRepositories {
found: repositories.len(),
limit: max_repos,
});
}
repositories.push(WorkspaceRepository::new(
repo_id,
name,
repo_root,
manifest_path,
last_indexed_at,
));
}
repositories.sort_by(|a, b| a.id.cmp(&b.id));
repositories.dedup_by(|a, b| a.id == b.id);
Ok(repositories)
}