use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use walkdir::WalkDir;
use super::BackgroundMsg;
use super::emit_git_info;
use super::emit_service_signal;
use super::tree;
use crate::channel::Sender;
use crate::ci::CiRun;
use crate::ci::OwnerRepo;
use crate::config::NonRustInclusion;
use crate::constants::CARGO_TOML;
use crate::constants::GIT_DIR;
use crate::constants::TARGET_DIR;
use crate::enrichment;
use crate::http::HttpClient;
use crate::project;
use crate::project::AbsolutePath;
use crate::project::GitRepoPresence;
use crate::project::ProjectFields;
use crate::project::ProjectPrData;
use crate::project::RootItem;
pub(crate) fn discover_project_item(root_dir: &Path) -> Option<RootItem> {
let mut items = Vec::new();
let mut iter = WalkDir::new(root_dir).into_iter();
while let Some(Ok(entry)) = iter.next() {
if entry.file_type().is_dir() {
let name = entry.file_name();
if name == TARGET_DIR || name == GIT_DIR {
iter.skip_current_dir();
continue;
}
}
if entry.file_type().is_file() && entry.file_name() == CARGO_TOML {
let parsed = project::from_cargo_toml(entry.path()).ok()?;
items.push(tree::cargo_project_to_item(parsed));
}
}
if items.is_empty() {
return None;
}
tree::build_tree(&items, &[])
.into_iter()
.find(|item| item.path() == root_dir)
}
pub(crate) struct FetchContext {
pub client: HttpClient,
}
pub(crate) struct ProjectDetailRequest<'a> {
pub sender: &'a Sender<BackgroundMsg>,
pub fetch_context: &'a FetchContext,
pub _project_path: &'a str,
pub abs_path: &'a Path,
pub project_name: Option<&'a str>,
pub repo_presence: GitRepoPresence,
}
pub(crate) fn fetch_project_details(req: &ProjectDetailRequest<'_>) {
let sender = req.sender;
let fetch_context = req.fetch_context;
let abs_path = req.abs_path;
let abs: AbsolutePath = abs_path.to_path_buf().into();
let project_name = req.project_name;
let repo_presence = req.repo_presence;
let client = &fetch_context.client;
if repo_presence.is_in_repo() {
emit_git_info(sender, &abs);
}
let submodules = if repo_presence.is_in_repo() {
project::get_submodules(abs_path)
} else {
Vec::new()
};
for sub in &submodules {
if let Some(name) = sub.crates_io_name() {
let _ = sender.send(BackgroundMsg::CratesIoFetchQueued {
name: name.to_string(),
});
}
}
if let Some(name) = project_name {
let _ = sender.send(BackgroundMsg::CratesIoFetchQueued {
name: name.to_string(),
});
let (info, signal) = client.fetch_crates_io_info(name);
emit_service_signal(sender, signal);
if let Some(info) = info {
let _ = sender.send(BackgroundMsg::CratesIoVersion {
path: abs.clone(),
version: info.version,
prerelease: info.prerelease,
downloads: info.downloads,
});
}
let _ = sender.send(BackgroundMsg::CratesIoFetchComplete {
name: name.to_string(),
});
}
if !submodules.is_empty() {
let _ = sender.send(BackgroundMsg::Submodules {
path: abs.clone(),
submodules: submodules.clone(),
});
for sub in &submodules {
enrichment::enrich(sub, sender, fetch_context);
}
}
let _ = sender.send(BackgroundMsg::ProjectDetailsDeclared { path: abs });
}
#[derive(Clone)]
pub(crate) struct RepoMetaInfo {
pub stars: u64,
pub description: Option<String>,
}
#[derive(Clone)]
pub(crate) struct CachedRepoData {
pub(crate) runs: Vec<CiRun>,
pub(crate) meta: Option<RepoMetaInfo>,
pub(crate) github_total: u32,
pub(crate) pr_data: ProjectPrData,
}
pub(crate) type RepoCache = Arc<Mutex<HashMap<OwnerRepo, CachedRepoData>>>;
pub(crate) fn new_repo_cache() -> RepoCache { Arc::new(Mutex::new(HashMap::new())) }
pub(crate) fn load_cached_repo_data(
repo_cache: &RepoCache,
owner_repo: &OwnerRepo,
) -> Option<CachedRepoData> {
repo_cache
.lock()
.ok()
.and_then(|cache| cache.get(owner_repo).cloned())
}
pub(crate) fn store_cached_repo_data(
repo_cache: &RepoCache,
owner_repo: &OwnerRepo,
data: CachedRepoData,
) {
if let Ok(mut cache) = repo_cache.lock() {
cache.insert(owner_repo.clone(), data);
}
}
pub(crate) fn invalidate_cached_repo_data(repo_cache: &RepoCache, owner_repo: &OwnerRepo) {
if let Ok(mut cache) = repo_cache.lock() {
cache.remove(owner_repo);
}
}
pub(crate) fn resolve_include_dirs(include_dirs: &[String]) -> Vec<AbsolutePath> {
include_dirs
.iter()
.map(|dir| {
let expanded = expand_home_path(dir);
let resolved = if expanded.is_absolute() {
expanded
} else {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(&expanded)
};
AbsolutePath::from(resolved.canonicalize().unwrap_or(resolved))
})
.collect()
}
fn expand_home_path(raw: &str) -> PathBuf {
if raw == "~" {
return dirs::home_dir().unwrap_or_else(|| PathBuf::from(raw));
}
if let Some(rest) = raw.strip_prefix("~/") {
return dirs::home_dir().map_or_else(|| PathBuf::from(raw), |home| home.join(rest));
}
PathBuf::from(raw)
}
pub(super) struct Phase1DiscoverStats {
pub(super) visited_dirs: usize,
pub(super) manifests: usize,
pub(super) projects: usize,
pub(super) non_rust_projects: usize,
}
pub(super) struct Phase1DiscoverResult {
pub(super) items: Vec<RootItem>,
pub(super) disk_entries: Vec<(String, AbsolutePath)>,
pub(super) stats: Phase1DiscoverStats,
}
fn discover_non_rust_project(
entry_path: &Path,
items: &mut Vec<RootItem>,
disk_entries: &mut Vec<(String, AbsolutePath)>,
stats: &mut Phase1DiscoverStats,
) {
let project = project::from_git_dir(entry_path);
let abs_path = project.path().clone();
stats.projects += 1;
stats.non_rust_projects += 1;
items.push(RootItem::NonRust(project));
let disk_path = abs_path.to_string_lossy().into_owned();
disk_entries.push((disk_path, abs_path));
}
pub(super) fn phase1_discover(
scan_dirs: &[AbsolutePath],
non_rust: NonRustInclusion,
) -> Phase1DiscoverResult {
let mut items = Vec::new();
let mut disk_entries = Vec::new();
let mut stats = Phase1DiscoverStats {
visited_dirs: 0,
manifests: 0,
projects: 0,
non_rust_projects: 0,
};
for dir in scan_dirs {
if !dir.is_dir() {
continue;
}
let mut iter = WalkDir::new(dir).into_iter();
while let Some(Ok(entry)) = iter.next() {
if entry.file_type().is_dir() {
stats.visited_dirs += 1;
let name = entry.file_name();
if name == TARGET_DIR || name == GIT_DIR {
iter.skip_current_dir();
continue;
}
if non_rust.includes_non_rust()
&& entry.path().join(GIT_DIR).is_dir()
&& !entry.path().join(CARGO_TOML).exists()
{
iter.skip_current_dir();
discover_non_rust_project(
entry.path(),
&mut items,
&mut disk_entries,
&mut stats,
);
continue;
}
}
if entry.file_type().is_file() && entry.file_name() == CARGO_TOML {
stats.manifests += 1;
let manifest_started = std::time::Instant::now();
let Ok(cargo_project) = project::from_cargo_toml(entry.path()) else {
continue;
};
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
elapsed_ms = tui_pane::perf_log_ms(manifest_started.elapsed().as_millis()),
manifest = %entry.path().display(),
"phase1_manifest_parse"
);
stats.projects += 1;
let item = tree::cargo_project_to_item(cargo_project);
let abs_path = item.path().clone();
let repo_presence_started = std::time::Instant::now();
let repo_presence = if project::git_repo_root(&abs_path).is_some() {
GitRepoPresence::InRepo
} else {
GitRepoPresence::OutsideRepo
};
tracing::trace!(
target: tui_pane::PERF_LOG_TARGET,
elapsed_ms = tui_pane::perf_log_ms(repo_presence_started.elapsed().as_millis()),
path = %abs_path,
in_repo = repo_presence.is_in_repo(),
"phase1_repo_presence"
);
items.push(item);
disk_entries.push((abs_path.to_string_lossy().into_owned(), abs_path));
}
}
}
Phase1DiscoverResult {
items,
disk_entries,
stats,
}
}