1use crate::checks::{self, RepoStatus};
11use crate::git::Git;
12use std::path::{Path, PathBuf};
13
14pub struct Entry {
16 pub path: PathBuf,
17 pub status: RepoStatus,
18}
19
20fn direct_submodules(git: &dyn Git, dir: &Path) -> Vec<PathBuf> {
23 git.run(dir, &["submodule", "status"])
24 .stdout
25 .lines()
26 .filter_map(|line| {
27 let status = line.chars().next()?;
28 if status == '-' {
29 return None; }
31 let path = line[1..].split_whitespace().nth(1)?;
33 Some(dir.join(path))
34 })
35 .collect()
36}
37
38pub fn repo_paths(git: &dyn Git, root: &Path) -> Vec<PathBuf> {
43 collect_repos(git, root)
44}
45
46fn is_work_tree(git: &dyn Git, dir: &Path) -> bool {
49 let r = git.run(dir, &["rev-parse", "--is-inside-work-tree"]);
50 r.success && r.trimmed() == "true"
51}
52
53fn collect_repos(git: &dyn Git, root: &Path) -> Vec<PathBuf> {
54 fn visit(git: &dyn Git, dir: &Path, order: &mut Vec<PathBuf>) {
55 for sub in direct_submodules(git, dir) {
56 visit(git, &sub, order);
57 }
58 order.push(dir.to_path_buf());
59 }
60 let mut order = Vec::new();
61 visit(git, root, &mut order);
62 order
63}
64
65pub fn evaluate_tree<G: Git + Sync>(
77 git: &G,
78 root: &Path,
79 base_override: Option<&str>,
80 fetch: bool,
81) -> Vec<Entry> {
82 if !is_work_tree(git, root) {
86 let reason = if root.exists() {
87 "not a git repository"
88 } else {
89 "no such directory"
90 };
91 return vec![Entry {
92 path: root.to_path_buf(),
93 status: RepoStatus::unusable(reason),
94 }];
95 }
96 let repos = collect_repos(git, root);
97 let last = repos.len().saturating_sub(1);
98 let mut slots: Vec<Option<RepoStatus>> = (0..repos.len()).map(|_| None).collect();
99
100 std::thread::scope(|scope| {
101 let mut handles = Vec::with_capacity(repos.len());
102 for (i, path) in repos.iter().enumerate() {
103 let is_root = i == last;
104 let ovr = if is_root { base_override } else { None };
105 let do_fetch = fetch; let path = path.clone();
107 let handle = scope.spawn(move || {
108 if do_fetch {
109 let _ = git.run(&path, &["fetch", "--quiet"]);
110 let _ = git.run(&path, &["remote", "prune", "origin"]);
111 }
112 let base = crate::config::resolve_base(git, &path, ovr);
113 let solo = crate::config::resolve_solo(git, &path);
114 let allow_diverged = crate::config::resolve_allow_diverged(git, &path);
115 checks::evaluate(git, &path, &base, solo, allow_diverged)
116 });
117 handles.push((i, handle));
118 }
119 for (i, handle) in handles {
120 slots[i] = Some(handle.join().expect("gkit: a check thread panicked"));
121 }
122 });
123
124 repos
125 .into_iter()
126 .zip(slots)
127 .map(|(path, status)| Entry {
128 path,
129 status: status.expect("every slot filled"),
130 })
131 .collect()
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::git::test_support::FakeGit;
138
139 #[test]
140 fn collect_repos_is_post_order_dfs() {
141 let git = FakeGit::new()
143 .ok_in("/r", "submodule status", " sha a (x)\n sha b (x)")
144 .ok_in("/r/a", "submodule status", "")
145 .ok_in("/r/b", "submodule status", " sha c (x)")
146 .ok_in("/r/b/c", "submodule status", "");
147 let order = collect_repos(&git, Path::new("/r"));
148 let got: Vec<String> = order
150 .iter()
151 .map(|p| p.display().to_string().replace('\\', "/"))
152 .collect();
153 assert_eq!(got, vec!["/r/a", "/r/b/c", "/r/b", "/r"]);
154 }
155
156 #[test]
157 fn non_repo_root_is_flagged_not_passed() {
158 let git = FakeGit::new().fail("rev-parse --is-inside-work-tree");
161 let entries = evaluate_tree(&git, Path::new("/not/a/repo"), None, false);
162 assert_eq!(entries.len(), 1);
163 assert!(!entries[0].status.ok());
164 assert!(entries[0].status.problem.is_some());
165 }
166
167 #[test]
168 fn skips_uninitialized_submodules() {
169 let git = FakeGit::new().ok_in("/r", "submodule status", "-sha a (x)\n sha b (x)\n");
170 let subs = direct_submodules(&git, Path::new("/r"));
171 let got: Vec<String> = subs
172 .iter()
173 .map(|p| p.display().to_string().replace('\\', "/"))
174 .collect();
175 assert_eq!(got, vec!["/r/b"]); }
177}