1use crate::clone::run_hooks;
19use crate::conf::{expand_path, CloneConf, Repo};
20use crate::config;
21use crate::git::Git;
22use std::path::{Path, PathBuf};
23
24#[derive(Debug, PartialEq, Eq)]
25pub enum Outcome {
26 Stamped,
28 Skipped,
30 Failed(String),
32}
33
34#[derive(Debug)]
35pub struct StampReport {
36 pub name: String,
37 pub dir: PathBuf,
38 pub outcome: Outcome,
39}
40
41pub fn effective_post_clone(conf: &CloneConf, repo: &Repo) -> Vec<String> {
44 conf.post_clone
45 .0
46 .iter()
47 .chain(repo.post_clone.0.iter())
48 .cloned()
49 .collect()
50}
51
52fn is_git_repo(git: &dyn Git, dir: &Path) -> bool {
54 let r = git.run(dir, &["rev-parse", "--is-inside-work-tree"]);
55 r.success && r.trimmed() == "true"
56}
57
58pub fn stamp_all<G: Git>(git: &G, conf: &CloneConf) -> Vec<StampReport> {
65 conf.repo
66 .iter()
67 .map(|r| {
68 let name = r.name();
69 let dir_s = expand_path(&r.dir, |k| std::env::var(k).ok());
70 let dir = PathBuf::from(&dir_s);
71 let mk = |outcome| StampReport {
72 name: name.clone(),
73 dir: dir.clone(),
74 outcome,
75 };
76
77 if !dir.exists() {
80 let e = "no such directory".to_string();
81 println!("FAILED {name:<28} {dir_s} ({e})");
82 return mk(Outcome::Failed(e));
83 }
84 if !is_git_repo(git, &dir) {
85 let e = "not a git repository".to_string();
86 println!("FAILED {name:<28} {dir_s} ({e})");
87 return mk(Outcome::Failed(e));
88 }
89
90 let post = effective_post_clone(conf, r);
91 if post.is_empty() {
92 println!("skipped {name:<28} {dir_s} (no post-clone hooks)");
93 return mk(Outcome::Skipped);
94 }
95
96 let ns = conf.namespace_for(r).unwrap_or_default().to_string();
100 let url = format!("{}:{}/{}.git", conf.host, ns, name);
101 let env = [
102 ("GKIT_REPO", name.as_str()),
103 ("GKIT_DIR", dir_s.as_str()),
104 ("GKIT_URL", url.as_str()),
105 ("GKIT_HOST", conf.host.as_str()),
106 ("GKIT_NAMESPACE", ns.as_str()),
107 ("GKIT_USER_NAME", ""),
108 ("GKIT_USER_EMAIL", ""),
109 ];
110
111 if let Err(e) = run_hooks(&post, &dir, &env) {
112 println!("FAILED {name:<28} {e}");
113 return mk(Outcome::Failed(e));
114 }
115 println!("stamped {name:<28} {dir_s}");
116 mk(Outcome::Stamped)
117 })
118 .collect()
119}
120
121pub fn match_repo<'a>(conf: &'a CloneConf, repo_dir: &Path) -> Option<&'a Repo> {
125 conf.repo.iter().find(|r| {
126 let d = expand_path(&r.dir, |k| std::env::var(k).ok());
127 std::fs::canonicalize(&d)
128 .map(|c| c == *repo_dir)
129 .unwrap_or(false)
130 })
131}
132
133pub struct RepoPlan {
137 pub conf_path: String,
138 pub hooks: Vec<String>,
139 pub matched: bool,
140 pub env_repo: String,
141 pub env_url: String,
142 pub env_host: String,
143 pub env_namespace: String,
144}
145
146pub fn plan_repo<G: Git>(git: &G, repo_dir: &Path) -> Result<RepoPlan, String> {
151 let conf_path = config::resolve_conf(git, repo_dir).ok_or_else(|| {
152 format!(
153 "gkit.conf not set in {}; run `gkit stamp --conf <conf>` once to back-fill, or pass the conf",
154 repo_dir.display()
155 )
156 })?;
157 let text = std::fs::read_to_string(&conf_path)
158 .map_err(|e| format!("cannot read gkit.conf `{conf_path}`: {e}"))?;
159 let cfg = crate::conf::parse(&text).map_err(|e| format!("{conf_path}: {e}"))?;
160
161 let basename = repo_dir
162 .file_name()
163 .map(|s| s.to_string_lossy().into_owned())
164 .unwrap_or_else(|| repo_dir.display().to_string());
165 match match_repo(&cfg, repo_dir) {
166 Some(r) => {
167 let host = cfg.host.clone();
168 let ns = cfg.namespace_for(r).unwrap_or_default().to_string();
169 let repo = r.name();
170 let url = format!("{host}:{ns}/{repo}.git");
171 Ok(RepoPlan {
172 conf_path,
173 hooks: effective_post_clone(&cfg, r),
174 matched: true,
175 env_repo: repo,
176 env_url: url,
177 env_host: host,
178 env_namespace: ns,
179 })
180 }
181 None => Ok(RepoPlan {
182 conf_path,
183 hooks: cfg.post_clone.0.clone(),
184 matched: false,
185 env_repo: basename,
186 env_url: String::new(),
187 env_host: String::new(),
188 env_namespace: String::new(),
189 }),
190 }
191}
192
193pub fn stamp_repo<G: Git>(git: &G, repo_dir: &Path) -> StampReport {
198 let dir_s = repo_dir.display().to_string();
199 let basename = repo_dir
200 .file_name()
201 .map(|s| s.to_string_lossy().into_owned())
202 .unwrap_or_else(|| dir_s.clone());
203 let mk = |name: &str, outcome| StampReport {
204 name: name.to_string(),
205 dir: repo_dir.to_path_buf(),
206 outcome,
207 };
208
209 if !is_git_repo(git, repo_dir) {
210 let e = "not a git repository".to_string();
211 println!("FAILED {basename:<28} {dir_s} ({e})");
212 return mk(&basename, Outcome::Failed(e));
213 }
214 let plan = match plan_repo(git, repo_dir) {
215 Ok(p) => p,
216 Err(e) => {
217 println!("FAILED {basename:<28} {e}");
218 return mk(&basename, Outcome::Failed(e));
219 }
220 };
221 if !plan.matched {
222 println!(
223 "note: {dir_s} not listed in {} — running global post-clone only",
224 plan.conf_path
225 );
226 }
227 if plan.hooks.is_empty() {
228 println!(
229 "skipped {:<28} {dir_s} (no post-clone hooks)",
230 plan.env_repo
231 );
232 return mk(&plan.env_repo, Outcome::Skipped);
233 }
234 let env = [
235 ("GKIT_REPO", plan.env_repo.as_str()),
236 ("GKIT_DIR", dir_s.as_str()),
237 ("GKIT_URL", plan.env_url.as_str()),
238 ("GKIT_HOST", plan.env_host.as_str()),
239 ("GKIT_NAMESPACE", plan.env_namespace.as_str()),
240 ("GKIT_USER_NAME", ""),
241 ("GKIT_USER_EMAIL", ""),
242 ];
243 if let Err(e) = run_hooks(&plan.hooks, repo_dir, &env) {
244 println!("FAILED {:<28} {e}", plan.env_repo);
245 return mk(&plan.env_repo, Outcome::Failed(e));
246 }
247 println!("stamped {:<28} {dir_s}", plan.env_repo);
248 mk(&plan.env_repo, Outcome::Stamped)
249}
250
251pub fn backfill_conf<G: Git>(git: &G, conf: &CloneConf, abs_conf_path: &str) {
256 for r in &conf.repo {
257 let dir_s = expand_path(&r.dir, |k| std::env::var(k).ok());
258 let dir = PathBuf::from(&dir_s);
259 if !dir.exists() || !is_git_repo(git, &dir) {
260 continue;
261 }
262 if config::resolve_conf(git, &dir).is_none() {
263 println!("+ git config gkit.conf {abs_conf_path} ({dir_s})");
264 let out = git.run(&dir, &["config", "gkit.conf", abs_conf_path]);
265 if !out.success {
266 println!(
267 "warning: could not set gkit.conf in {dir_s}: {}",
268 out.stderr.trim()
269 );
270 }
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::conf::{Hooks, Repo};
279 use crate::git::test_support::FakeGit;
280
281 fn repo(dir: &str, post: &[&str]) -> Repo {
282 Repo {
283 dir: dir.to_string(),
284 namespace: None,
285 name: None,
286 depth: None,
287 branch: None,
288 single_branch: false,
289 clone_flags: vec![],
290 pre_clone: Hooks(vec![]),
291 post_clone: Hooks(post.iter().map(|s| s.to_string()).collect()),
292 }
293 }
294
295 fn conf(global_post: &[&str], repos: Vec<Repo>) -> CloneConf {
296 CloneConf {
297 host: "h".into(),
298 namespace: Some("ns".into()),
299 git_flags: vec![],
300 clone_flags: vec![],
301 pre_clone: Hooks(vec![]),
302 post_clone: Hooks(global_post.iter().map(|s| s.to_string()).collect()),
303 repo: repos,
304 }
305 }
306
307 #[test]
308 fn effective_post_clone_chains_global_then_repo() {
309 let c = conf(
310 &["git config gkit.solo true"],
311 vec![repo("/x", &["git config gkit.baseBranch dev"])],
312 );
313 assert_eq!(
314 effective_post_clone(&c, &c.repo[0]),
315 [
316 "git config gkit.solo true",
317 "git config gkit.baseBranch dev"
318 ]
319 );
320 }
321
322 #[test]
323 fn match_repo_by_canonical_dir() {
324 let base = std::env::temp_dir().join(format!("gkit-match-{}", std::process::id()));
327 let _ = std::fs::remove_dir_all(&base);
328 let a = base.join("a");
329 let b = base.join("b");
330 std::fs::create_dir_all(&a).unwrap();
331 std::fs::create_dir_all(&b).unwrap();
332 let c = conf(&[], vec![repo(a.to_str().unwrap(), &[])]);
333 let a_canon = std::fs::canonicalize(&a).unwrap();
334 let b_canon = std::fs::canonicalize(&b).unwrap();
335 assert!(match_repo(&c, &a_canon).is_some(), "listed dir matches");
336 assert!(match_repo(&c, &b_canon).is_none(), "unlisted dir → None");
337 let _ = std::fs::remove_dir_all(&base);
338 }
339
340 #[test]
341 fn missing_dir_is_failed() {
342 let c = conf(
345 &["git config gkit.solo true"],
346 vec![repo("/no/such/gkit-stamp-xyz", &[])],
347 );
348 let reports = stamp_all(&FakeGit::new(), &c);
349 assert_eq!(reports.len(), 1);
350 assert!(matches!(reports[0].outcome, Outcome::Failed(ref e) if e.contains("no such")));
351 }
352}