1use crate::conf::{expand_path, CloneConf};
14use crate::git::Git;
15use std::path::{Path, PathBuf};
16use std::process::Command;
17
18#[derive(Debug, PartialEq, Eq)]
19pub enum Outcome {
20 Cloned,
21 Skipped,
22 Failed(String),
23}
24
25#[derive(Debug)]
26pub struct CloneReport {
27 pub name: String,
28 pub dir: PathBuf,
29 pub outcome: Outcome,
30 pub command: String,
31}
32
33pub struct Opts {
34 pub submodule_branch: bool,
35 pub direnv: bool,
36}
37
38impl Default for Opts {
39 fn default() -> Self {
40 Self {
41 submodule_branch: true,
42 direnv: true,
43 }
44 }
45}
46
47const SUBMODULE_SWITCH: &str = "b=$(git config -f \"$toplevel/.gitmodules\" \"submodule.$name.branch\" 2>/dev/null || echo main); git switch \"$b\" 2>/dev/null || true";
48
49fn run_hooks(cmds: &[String], cwd: &Path, env: &[(&str, &str)]) -> Result<(), String> {
52 for cmd in cmds {
53 println!("+ {cmd}");
54 let mut c = Command::new("sh");
55 c.arg("-c").arg(cmd).current_dir(cwd);
56 for (k, v) in env {
57 c.env(k, v);
58 }
59 match c.status() {
60 Ok(s) if s.success() => {}
61 Ok(s) => return Err(format!("hook `{cmd}` exited {}", s.code().unwrap_or(-1))),
62 Err(e) => return Err(format!("hook `{cmd}` failed to start: {e}")),
63 }
64 }
65 Ok(())
66}
67
68pub fn clone_all<G: Git>(git: &G, conf: &CloneConf, opts: &Opts) -> Vec<CloneReport> {
71 conf.repo
72 .iter()
73 .map(|r| {
74 let name = r.name();
75 let dir_s = expand_path(&r.dir, |k| std::env::var(k).ok());
76 let dir = PathBuf::from(&dir_s);
77 let ns = match conf.namespace_for(r) {
80 Some(n) => n.to_string(),
81 None => {
82 let e = format!("no namespace for {}", r.dir);
83 println!("FAILED {name:<28} {e}");
84 return CloneReport {
85 name,
86 dir,
87 outcome: Outcome::Failed(e),
88 command: String::new(),
89 };
90 }
91 };
92 let url = format!("{}:{}/{}.git", conf.host, ns, name);
93
94 let mut args: Vec<String> = Vec::new();
96 args.extend(conf.git_flags.iter().cloned());
97 args.push("clone".into());
98 if let Some(d) = r.depth {
99 args.push("--depth".into());
100 args.push(d.to_string());
101 }
102 if let Some(b) = &r.branch {
103 args.push("--branch".into());
104 args.push(b.clone());
105 args.push("--single-branch".into());
106 }
107 args.push("--recurse-submodules".into());
108 args.extend(conf.clone_flags.iter().cloned());
109 args.extend(r.clone_flags.iter().cloned());
110 args.push(url.clone());
111 args.push(dir_s.clone());
112 let command = format!("git {}", args.join(" "));
113
114 let mk = |outcome| CloneReport {
115 name: name.clone(),
116 dir: dir.clone(),
117 outcome,
118 command: command.clone(),
119 };
120
121 if dir.join(".git").exists() {
122 println!("+ {command}");
123 println!("skipped {name:<28} {dir_s} (exists)");
124 return mk(Outcome::Skipped);
125 }
126
127 let env = [
128 ("GKIT_REPO", name.as_str()),
129 ("GKIT_DIR", dir_s.as_str()),
130 ("GKIT_URL", url.as_str()),
131 ("GKIT_HOST", conf.host.as_str()),
132 ("GKIT_NAMESPACE", ns.as_str()),
133 ];
134
135 let parent = dir.parent().unwrap_or(Path::new("."));
137 let _ = std::fs::create_dir_all(parent);
138 let pre: Vec<String> = conf
139 .pre_clone
140 .0
141 .iter()
142 .chain(r.pre_clone.0.iter())
143 .cloned()
144 .collect();
145 if let Err(e) = run_hooks(&pre, parent, &env) {
146 println!("FAILED {name:<28} {e}");
147 return mk(Outcome::Failed(e));
148 }
149
150 println!("+ {command}");
152 let refs: Vec<&str> = args.iter().map(String::as_str).collect();
153 let out = git.run(Path::new("."), &refs);
154 if !out.success {
155 let e = out.stderr.trim().to_string();
156 println!("FAILED {name:<28} {}", e.lines().next().unwrap_or(""));
157 return mk(Outcome::Failed(e));
158 }
159
160 if opts.submodule_branch {
162 let _ = git.run(
163 &dir,
164 &["submodule", "foreach", "--recursive", SUBMODULE_SWITCH],
165 );
166 }
167 if opts.direnv && dir.join(".envrc").exists() {
168 let _ = Command::new("direnv").arg("allow").arg(&dir).output(); }
170
171 let post: Vec<String> = conf
173 .post_clone
174 .0
175 .iter()
176 .chain(r.post_clone.0.iter())
177 .cloned()
178 .collect();
179 if let Err(e) = run_hooks(&post, &dir, &env) {
180 println!("FAILED {name:<28} {e}");
181 return mk(Outcome::Failed(e));
182 }
183
184 println!("cloned {name:<28} {dir_s}");
185 mk(Outcome::Cloned)
186 })
187 .collect()
188}
189
190#[cfg(test)]
191mod tests {
192 use crate::conf;
193
194 #[test]
195 fn builds_expected_url_shape() {
196 let c = conf::parse("host = \"tlbb\"\nnamespace = \"example-org\"\n[[repo]]\ndir = \"$HOME/x/cosp\"\ndepth = 1\n").unwrap();
197 assert_eq!(c.repo[0].name(), "cosp");
198 assert_eq!(c.repo[0].depth, Some(1));
199 let ns = c.namespace_for(&c.repo[0]).unwrap();
200 let url = format!("{}:{}/{}.git", c.host, ns, c.repo[0].name());
201 assert_eq!(url, "tlbb:example-org/cosp.git");
202 }
203
204 #[test]
205 fn per_repo_namespace_drives_url() {
206 let c = conf::parse("host=\"gh\"\n[[repo]]\ndir=\"$HOME/x/foo\"\nnamespace=\"alice\"\n")
207 .unwrap();
208 let ns = c.namespace_for(&c.repo[0]).unwrap();
209 let url = format!("{}:{}/{}.git", c.host, ns, c.repo[0].name());
210 assert_eq!(url, "gh:alice/foo.git");
211 }
212}