use crate::checks::{self, RepoStatus};
use crate::git::Git;
use std::path::{Path, PathBuf};
pub struct Entry {
pub path: PathBuf,
pub status: RepoStatus,
}
fn direct_submodules(git: &dyn Git, dir: &Path) -> Vec<PathBuf> {
git.run(dir, &["submodule", "status"])
.stdout
.lines()
.filter_map(|line| {
let status = line.chars().next()?;
if status == '-' {
return None; }
let path = line[1..].split_whitespace().nth(1)?;
Some(dir.join(path))
})
.collect()
}
pub fn repo_paths(git: &dyn Git, root: &Path) -> Vec<PathBuf> {
collect_repos(git, root)
}
fn is_work_tree(git: &dyn Git, dir: &Path) -> bool {
let r = git.run(dir, &["rev-parse", "--is-inside-work-tree"]);
r.success && r.trimmed() == "true"
}
fn collect_repos(git: &dyn Git, root: &Path) -> Vec<PathBuf> {
fn visit(git: &dyn Git, dir: &Path, order: &mut Vec<PathBuf>) {
for sub in direct_submodules(git, dir) {
visit(git, &sub, order);
}
order.push(dir.to_path_buf());
}
let mut order = Vec::new();
visit(git, root, &mut order);
order
}
pub fn evaluate_tree<G: Git + Sync>(
git: &G,
root: &Path,
base_override: Option<&str>,
fetch: bool,
) -> Vec<Entry> {
if !is_work_tree(git, root) {
let reason = if root.exists() {
"not a git repository"
} else {
"no such directory"
};
return vec![Entry {
path: root.to_path_buf(),
status: RepoStatus::unusable(reason),
}];
}
let repos = collect_repos(git, root);
let last = repos.len().saturating_sub(1);
let mut slots: Vec<Option<RepoStatus>> = (0..repos.len()).map(|_| None).collect();
std::thread::scope(|scope| {
let mut handles = Vec::with_capacity(repos.len());
for (i, path) in repos.iter().enumerate() {
let is_root = i == last;
let ovr = if is_root { base_override } else { None };
let do_fetch = fetch && !is_root; let path = path.clone();
let handle = scope.spawn(move || {
if do_fetch {
let _ = git.run(&path, &["fetch", "--quiet"]);
let _ = git.run(&path, &["remote", "prune", "origin"]);
}
let base = crate::config::resolve_base(git, &path, ovr);
let solo = crate::config::resolve_solo(git, &path);
checks::evaluate(git, &path, &base, solo)
});
handles.push((i, handle));
}
for (i, handle) in handles {
slots[i] = Some(handle.join().expect("gkit: a check thread panicked"));
}
});
repos
.into_iter()
.zip(slots)
.map(|(path, status)| Entry {
path,
status: status.expect("every slot filled"),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::test_support::FakeGit;
#[test]
fn collect_repos_is_post_order_dfs() {
let git = FakeGit::new()
.ok_in("/r", "submodule status", " sha a (x)\n sha b (x)")
.ok_in("/r/a", "submodule status", "")
.ok_in("/r/b", "submodule status", " sha c (x)")
.ok_in("/r/b/c", "submodule status", "");
let order = collect_repos(&git, Path::new("/r"));
let got: Vec<String> = order
.iter()
.map(|p| p.display().to_string().replace('\\', "/"))
.collect();
assert_eq!(got, vec!["/r/a", "/r/b/c", "/r/b", "/r"]);
}
#[test]
fn non_repo_root_is_flagged_not_passed() {
let git = FakeGit::new().fail("rev-parse --is-inside-work-tree");
let entries = evaluate_tree(&git, Path::new("/not/a/repo"), None, false);
assert_eq!(entries.len(), 1);
assert!(!entries[0].status.ok());
assert!(entries[0].status.problem.is_some());
}
#[test]
fn skips_uninitialized_submodules() {
let git = FakeGit::new().ok_in("/r", "submodule status", "-sha a (x)\n sha b (x)\n");
let subs = direct_submodules(&git, Path::new("/r"));
let got: Vec<String> = subs
.iter()
.map(|p| p.display().to_string().replace('\\', "/"))
.collect();
assert_eq!(got, vec!["/r/b"]); }
}