use crate::clone::{sh_squote, SUBMODULE_SWITCH};
use crate::git::Git;
use std::path::{Path, PathBuf};
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
Fixed,
Skipped,
Failed(String),
}
#[derive(Debug)]
pub struct FixsubReport {
pub root: PathBuf,
pub outcome: Outcome,
}
pub fn inherit_identity_cmd(root_name: Option<&str>, root_email: Option<&str>) -> Option<String> {
let parts: Vec<String> = [("user.name", root_name), ("user.email", root_email)]
.into_iter()
.filter_map(|(k, v)| {
v.map(|v| {
format!(
"git config --local {k} >/dev/null 2>&1 || git config {k} {}",
sh_squote(v)
)
})
})
.collect();
(!parts.is_empty()).then(|| parts.join("; "))
}
fn is_git_repo(git: &dyn Git, dir: &Path) -> bool {
let r = git.run(dir, &["rev-parse", "--is-inside-work-tree"]);
r.success && r.trimmed() == "true"
}
fn local_config(git: &dyn Git, dir: &Path, key: &str) -> Option<String> {
let o = git.run(dir, &["config", "--local", "--get", key]);
let v = o.trimmed();
(o.success && !v.is_empty()).then(|| v.to_string())
}
pub fn fixsub<G: Git>(git: &G, root: &Path, dry_run: bool, direnv: bool) -> FixsubReport {
let mk = |outcome| FixsubReport {
root: root.to_path_buf(),
outcome,
};
if !is_git_repo(git, root) {
return mk(Outcome::Failed("not a git repository".into()));
}
let name = local_config(git, root, "user.name");
let email = local_config(git, root, "user.email");
let mut body = SUBMODULE_SWITCH.to_string();
if let Some(id) = inherit_identity_cmd(name.as_deref(), email.as_deref()) {
body.push_str("; ");
body.push_str(&id);
}
if direnv {
body.push_str("; [ -f .envrc ] && direnv allow . 2>/dev/null || true");
}
println!("+ git submodule foreach --recursive {body}");
if dry_run {
return mk(Outcome::Fixed);
}
let out = git.run(
root,
&["submodule", "foreach", "--recursive", body.as_str()],
);
if !out.success {
return mk(Outcome::Failed(format!(
"submodule foreach failed: {}",
out.stderr.trim()
)));
}
mk(Outcome::Fixed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::test_support::FakeGit;
use std::path::Path;
#[test]
fn inherit_identity_cmd_set_if_unset_and_quotes() {
assert_eq!(
inherit_identity_cmd(Some("Jane Dev"), Some("jane@acme.com")).as_deref(),
Some(
"git config --local user.name >/dev/null 2>&1 || git config user.name 'Jane Dev'; \
git config --local user.email >/dev/null 2>&1 || git config user.email 'jane@acme.com'"
)
);
assert_eq!(
inherit_identity_cmd(Some("Jane"), None).as_deref(),
Some("git config --local user.name >/dev/null 2>&1 || git config user.name 'Jane'")
);
assert_eq!(inherit_identity_cmd(None, None), None);
assert_eq!(
inherit_identity_cmd(Some("O'Brien"), None).as_deref(),
Some(
r"git config --local user.name >/dev/null 2>&1 || git config user.name 'O'\''Brien'"
)
);
}
#[test]
fn dry_run_runs_no_git_mutations() {
let git = FakeGit::new().ok("rev-parse --is-inside-work-tree", "true");
let r = fixsub(&git, Path::new("/r"), true, true);
assert_eq!(r.outcome, Outcome::Fixed);
}
#[test]
fn non_git_root_fails() {
let git = FakeGit::new().fail("rev-parse --is-inside-work-tree");
let r = fixsub(&git, Path::new("/nope"), false, true);
assert!(matches!(r.outcome, Outcome::Failed(_)));
}
}