use std::fmt::Write as _;
use std::path::Path;
use owo_colors::OwoColorize;
use crate::git;
use crate::hook::Hook;
use crate::hooks::HookOutput;
use crate::hooks::pre_commit_hooks::shebangs::{
file_has_shebang, git_index_stage_output, matching_git_index_paths_by_executable_bit,
};
use crate::hooks::pre_commit_hooks::{
FilenamesArgs, hook_filenames, parse_hook_args, run_blocking_file_checks,
};
pub(crate) async fn run(hook: &Hook, filenames: &[&Path]) -> Result<HookOutput, anyhow::Error> {
let args: FilenamesArgs = parse_hook_args(hook)?;
let filenames = hook_filenames(&args.filenames, filenames).collect::<Vec<_>>();
if filenames.is_empty() {
return Ok(HookOutput::unchanged(0, Vec::new()));
}
let mut cmd = git::git_cmd()?;
let output = cmd
.arg("config")
.arg("core.fileMode")
.check(false)
.output()
.await?;
let tracks_executable_bit = if output.status.code() == Some(1) {
true
} else {
let output = cmd.check_output(output)?;
std::str::from_utf8(&output.stdout)?.trim() != "false"
};
let file_base = hook.project().relative_path();
if tracks_executable_bit {
check_shebangs(file_base, &filenames).await
} else {
git_check_shebangs(file_base, &filenames).await
}
}
async fn check_shebangs(file_base: &Path, paths: &[&Path]) -> Result<HookOutput, anyhow::Error> {
run_blocking_file_checks(file_base, &[], paths, |file_path, file| {
if file_has_shebang(file_path)? {
Ok(HookOutput::unchanged(0, Vec::new()))
} else {
let msg = build_missing_shebang_warning(file)?;
Ok(HookOutput::unchanged(1, msg.into_bytes()))
}
})
.await
}
fn build_missing_shebang_warning(path: &Path) -> Result<String, std::fmt::Error> {
let path_str = path.display();
let mut warning = String::new();
writeln!(
warning,
"{}",
format!(
"{} marked executable but has no (or invalid) shebang!",
path_str.yellow()
)
.bold()
)?;
writeln!(
warning,
"{}",
format!(" If it isn't supposed to be executable, try: 'chmod -x {path_str}'").dimmed()
)?;
writeln!(
warning,
"{}",
format!(" If on Windows, you may also need to: 'git add --chmod=-x {path_str}'").dimmed()
)?;
writeln!(
warning,
"{}",
" If it is supposed to be executable, double-check its shebang.".dimmed()
)?;
Ok(warning)
}
async fn git_check_shebangs(
file_base: &Path,
filenames: &[&Path],
) -> Result<HookOutput, anyhow::Error> {
let stdout = git_index_stage_output(file_base).await?;
let entries = matching_git_index_paths_by_executable_bit(&stdout, file_base, filenames, true)
.collect::<Vec<_>>();
check_shebangs(file_base, &entries).await
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_check_shebangs_with_shebang() -> Result<(), anyhow::Error> {
let file = NamedTempFile::new()?;
fs_err::tokio::write(file.path(), b"#!/bin/bash\necho ok\n").await?;
let files = vec![file.path()];
let result = check_shebangs(Path::new(""), &files).await?;
assert_eq!(result.exit_status, 0);
assert_eq!(result.output, b"");
Ok(())
}
#[tokio::test]
async fn test_check_shebangs_without_shebang() -> Result<(), anyhow::Error> {
let file = NamedTempFile::new()?;
fs_err::tokio::write(file.path(), b"echo ok\n").await?;
let files = vec![file.path()];
let result = check_shebangs(Path::new(""), &files).await?;
assert_eq!(result.exit_status, 1);
assert!(
String::from_utf8_lossy(&result.output)
.contains("marked executable but has no (or invalid) shebang!")
);
Ok(())
}
#[tokio::test]
async fn test_check_shebangs_empty_input() -> Result<(), anyhow::Error> {
let result = check_shebangs(Path::new(""), &[]).await?;
assert_eq!(result.exit_status, 0);
assert_eq!(result.output, b"");
Ok(())
}
}