1use crate::clone::{sh_squote, SUBMODULE_SWITCH};
22use crate::git::Git;
23use std::path::{Path, PathBuf};
24
25#[derive(Debug, PartialEq, Eq)]
26pub enum Outcome {
27 Fixed,
29 Skipped,
31 Failed(String),
33}
34
35#[derive(Debug)]
36pub struct FixsubReport {
37 pub root: PathBuf,
38 pub outcome: Outcome,
39}
40
41pub fn inherit_identity_cmd(root_name: Option<&str>, root_email: Option<&str>) -> Option<String> {
47 let parts: Vec<String> = [("user.name", root_name), ("user.email", root_email)]
48 .into_iter()
49 .filter_map(|(k, v)| {
50 v.map(|v| {
51 format!(
52 "git config --local {k} >/dev/null 2>&1 || git config {k} {}",
53 sh_squote(v)
54 )
55 })
56 })
57 .collect();
58 (!parts.is_empty()).then(|| parts.join("; "))
59}
60
61fn is_git_repo(git: &dyn Git, dir: &Path) -> bool {
63 let r = git.run(dir, &["rev-parse", "--is-inside-work-tree"]);
64 r.success && r.trimmed() == "true"
65}
66
67fn local_config(git: &dyn Git, dir: &Path, key: &str) -> Option<String> {
69 let o = git.run(dir, &["config", "--local", "--get", key]);
70 let v = o.trimmed();
71 (o.success && !v.is_empty()).then(|| v.to_string())
72}
73
74pub fn fixsub<G: Git>(git: &G, root: &Path, dry_run: bool, direnv: bool) -> FixsubReport {
78 let mk = |outcome| FixsubReport {
79 root: root.to_path_buf(),
80 outcome,
81 };
82 if !is_git_repo(git, root) {
83 return mk(Outcome::Failed("not a git repository".into()));
84 }
85
86 let name = local_config(git, root, "user.name");
90 let email = local_config(git, root, "user.email");
91 let mut body = SUBMODULE_SWITCH.to_string();
92 if let Some(id) = inherit_identity_cmd(name.as_deref(), email.as_deref()) {
93 body.push_str("; ");
94 body.push_str(&id);
95 }
96 if direnv {
97 body.push_str("; [ -f .envrc ] && direnv allow . 2>/dev/null || true");
98 }
99
100 println!("+ git submodule foreach --recursive {body}");
101 if dry_run {
102 return mk(Outcome::Fixed);
103 }
104 let out = git.run(
105 root,
106 &["submodule", "foreach", "--recursive", body.as_str()],
107 );
108 if !out.success {
109 return mk(Outcome::Failed(format!(
110 "submodule foreach failed: {}",
111 out.stderr.trim()
112 )));
113 }
114 mk(Outcome::Fixed)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::git::test_support::FakeGit;
121 use std::path::Path;
122
123 #[test]
124 fn inherit_identity_cmd_set_if_unset_and_quotes() {
125 assert_eq!(
127 inherit_identity_cmd(Some("Jane Dev"), Some("jane@acme.com")).as_deref(),
128 Some(
129 "git config --local user.name >/dev/null 2>&1 || git config user.name 'Jane Dev'; \
130 git config --local user.email >/dev/null 2>&1 || git config user.email 'jane@acme.com'"
131 )
132 );
133 assert_eq!(
135 inherit_identity_cmd(Some("Jane"), None).as_deref(),
136 Some("git config --local user.name >/dev/null 2>&1 || git config user.name 'Jane'")
137 );
138 assert_eq!(inherit_identity_cmd(None, None), None);
140 assert_eq!(
142 inherit_identity_cmd(Some("O'Brien"), None).as_deref(),
143 Some(
144 r"git config --local user.name >/dev/null 2>&1 || git config user.name 'O'\''Brien'"
145 )
146 );
147 }
148
149 #[test]
150 fn dry_run_runs_no_git_mutations() {
151 let git = FakeGit::new().ok("rev-parse --is-inside-work-tree", "true");
154 let r = fixsub(&git, Path::new("/r"), true, true);
155 assert_eq!(r.outcome, Outcome::Fixed);
156 }
157
158 #[test]
159 fn non_git_root_fails() {
160 let git = FakeGit::new().fail("rev-parse --is-inside-work-tree");
161 let r = fixsub(&git, Path::new("/nope"), false, true);
162 assert!(matches!(r.outcome, Outcome::Failed(_)));
163 }
164}