diff --git a/Cargo.lock b/Cargo.lock index e13dd9a..6252880 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1544,6 +1544,7 @@ dependencies = [ "rayon", "regex", "rusqlite", + "same-file", "serde", "serde_json", "serde_yaml", diff --git a/Cargo.toml b/Cargo.toml index 10d198e..3d74311 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ owo-colors = "4.1.0" rand = "0.8.5" rayon = "1.10.0" rusqlite = { version = "0.32.1", features = ["bundled"] } +same-file = "1.0.6" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.132" serde_yaml = "0.9.34" diff --git a/src/cli/install.rs b/src/cli/install.rs index 940cfa4..acaa8eb 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -5,11 +5,13 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use indoc::indoc; use owo_colors::OwoColorize; +use same_file::is_same_file; use crate::cli::run; use crate::cli::{ExitStatus, HookType}; use crate::fs::Simplified; use crate::git; +use crate::git::git_cmd; use crate::hook::Project; use crate::printer::Printer; use crate::store::Store; @@ -21,8 +23,9 @@ pub(crate) async fn install( overwrite: bool, allow_missing_config: bool, printer: Printer, + git_dir: Option<&Path>, ) -> Result { - if git::has_hooks_path_set().await? { + if git_dir.is_none() && git::has_hooks_path_set().await? { writeln!( printer.stderr(), indoc::indoc! {" @@ -35,7 +38,12 @@ pub(crate) async fn install( let hook_types = get_hook_types(config.clone(), hook_types); - let hooks_path = git::get_git_common_dir().await?.join("hooks"); + let hooks_path = if let Some(dir) = git_dir { + dir.join("hooks") + } else { + git::get_git_common_dir().await?.join("hooks") + }; + fs_err::create_dir_all(&hooks_path)?; let project = Project::from_config_file(config); @@ -233,3 +241,43 @@ pub(crate) async fn uninstall( Ok(ExitStatus::Success) } + +pub(crate) async fn init_template_dir( + directory: PathBuf, + config: Option, + hook_types: Vec, + requires_config: bool, + printer: Printer, +) -> Result { + install( + config, + hook_types, + false, + true, + !requires_config, + printer, + Some(&directory), + ) + .await?; + + let output = git_cmd("git config")? + .arg("config") + .arg("init.templateDir") + .check(false) + .output() + .await?; + let template_dir = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + if template_dir.is_empty() || !is_same_file(&directory, Path::new(&template_dir))? { + writeln!( + printer.stderr(), + "{}", + indoc::formatdoc! {" + `init.templateDir` not set to the target directory + try `git config --global init.templateDir '{directory}'`? + ", directory = directory.user_display().cyan() } + )?; + } + + Ok(ExitStatus::Success) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 1efae69..ce36749 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -16,7 +16,7 @@ mod validate; pub(crate) use clean::clean; pub(crate) use hook_impl::hook_impl; -pub(crate) use install::{install, uninstall}; +pub(crate) use install::{init_template_dir, install, uninstall}; pub(crate) use run::run; pub(crate) use sample_config::sample_config; pub(crate) use self_update::self_update; @@ -172,7 +172,7 @@ pub(crate) enum Command { Clean, /// Install hook script in a directory intended for use with `git config init.templateDir`. #[command(name = "init-templatedir")] - InitTemplateDir, + InitTemplateDir(InitTemplateDirArgs), /// Try the pre-commit hooks in the current repo. TryRepo(Box), @@ -339,3 +339,17 @@ pub(crate) struct GenerateShellCompletionArgs { #[arg(value_enum)] pub shell: clap_complete::Shell, } + +#[derive(Debug, Args)] +pub(crate) struct InitTemplateDirArgs { + /// The directory in which to write the hook script. + pub(crate) directory: PathBuf, + + /// Assume cloned repos should have a `pre-commit` config. + #[arg(long)] + pub(crate) no_allow_missing_config: bool, + + /// Which hook type to install. + #[arg(short = 't', long = "hook-type", value_name = "HOOK_TYPE", value_enum)] + pub(crate) hook_types: Vec, +} diff --git a/src/main.rs b/src/main.rs index 4991642..7580b77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -172,6 +172,7 @@ async fn run(mut cli: Cli) -> Result { args.overwrite, args.allow_missing_config, printer, + None, ) .await } @@ -241,6 +242,18 @@ async fn run(mut cli: Cli) -> Result { clap_complete::generate(args.shell, &mut command, bin_name, &mut std::io::stdout()); Ok(ExitStatus::Success) } + Command::InitTemplateDir(args) => { + show_settings!(args); + + cli::init_template_dir( + args.directory, + cli.globals.config, + args.hook_types, + args.no_allow_missing_config, + printer, + ) + .await + } _ => { writeln!(printer.stderr(), "Command not implemented yet")?; Ok(ExitStatus::Failure) diff --git a/tests/install.rs b/tests/install.rs index a5b8bd4..57c9433 100644 --- a/tests/install.rs +++ b/tests/install.rs @@ -218,3 +218,20 @@ fn uninstall() -> anyhow::Result<()> { Ok(()) } + +#[test] +fn init_template_dir() { + let context = TestContext::new(); + context.init_project(); + + cmd_snapshot!(context.filters(), context.command().arg("init-templatedir").arg(".git"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + pre-commit installed at .git/hooks/pre-commit + + ----- stderr ----- + `init.templateDir` not set to the target directory + try `git config --global init.templateDir '.git'`? + "#); +}