use std::borrow::Cow;
use std::fmt::Write;
use std::path::Path;
use prek_consts::env_vars::{EnvVars, EnvVarsRead};
use crate::git;
use crate::hook::Hook;
use crate::hooks::HookOutput;
use crate::hooks::pre_commit_hooks::{FilenamesArgs, hook_filenames, parse_hook_args};
pub(crate) async fn run(hook: &Hook, filenames: &[&Path]) -> Result<HookOutput, anyhow::Error> {
let args: FilenamesArgs = parse_hook_args(hook)?;
let diff_arg = if let (Ok(from_ref), Ok(to_ref)) = (
EnvVars.var("PRE_COMMIT_FROM_REF"),
EnvVars.var("PRE_COMMIT_TO_REF"),
) {
Cow::Owned(format!("{from_ref}...{to_ref}"))
} else {
Cow::Borrowed("--cached")
};
let stdout = git::git_cmd()?
.current_dir(hook.work_dir())
.arg("diff")
.arg("--relative")
.arg("--diff-filter=A")
.hidden_args(["--no-ext-diff"])
.arg("--raw")
.arg("-z")
.arg(diff_arg.as_ref())
.arg("--")
.file_args(hook_filenames(&args.filenames, filenames))
.check(true)
.output()
.await?
.stdout;
let new_submodules = collect_new_submodules(&stdout);
if new_submodules.is_empty() {
Ok(HookOutput::unchanged(0, Vec::new()))
} else {
let message = render_message(&new_submodules);
Ok(HookOutput::unchanged(1, message.into_bytes()))
}
}
fn render_message(new_submodules: &[&str]) -> String {
let mut message = String::new();
for filename in new_submodules {
writeln!(message, "{filename}: new submodule introduced")
.expect("writing to String should never fail");
}
message.push('\n');
message.push_str(indoc::indoc! {"
This commit introduces new git submodules.
Did you unintentionally `git add .`?
To fix this, run `git rm <submodule>`.
Also check `.gitmodules` for any unintended changes.
"});
message
}
fn collect_new_submodules(stdout: &[u8]) -> Vec<&str> {
let mut entries = stdout
.split(|&b| b == b'\0')
.filter(|entry| !entry.is_empty());
let mut new_submodules = Vec::new();
while let Some(metadata) = entries.next() {
let Some(filename) = entries.next() else {
break;
};
let Ok(metadata) = std::str::from_utf8(metadata) else {
continue;
};
let Ok(filename) = std::str::from_utf8(filename) else {
continue;
};
if metadata.split_whitespace().nth(1) == Some("160000") {
new_submodules.push(filename);
}
}
new_submodules
}
#[cfg(test)]
mod tests {
#[test]
fn collect_new_submodules_ignores_non_submodules() {
let stdout = b":000000 100644 0000000 abcdef1 A\0.gitmodules\0";
let new_submodules = super::collect_new_submodules(stdout);
assert_eq!(new_submodules, Vec::<&str>::new());
}
#[test]
fn collect_new_submodules_finds_new_submodules() {
let stdout = b":000000 100644 0000000 abcdef1 A\0.gitmodules\0\
:000000 160000 0000000 abcdef2 A\0project2/sub module\0\
:000000 160000 0000000 abcdef3 A\0vendor/dep\0";
let new_submodules = super::collect_new_submodules(stdout);
assert_eq!(new_submodules, vec!["project2/sub module", "vendor/dep"]);
}
#[test]
fn render_message() {
let message = super::render_message(&["project2/sub module", "vendor/dep"]);
assert_eq!(
message,
indoc::indoc! {"
project2/sub module: new submodule introduced
vendor/dep: new submodule introduced
This commit introduces new git submodules.
Did you unintentionally `git add .`?
To fix this, run `git rm <submodule>`.
Also check `.gitmodules` for any unintended changes.
"}
);
}
}