Repository: j178/prefligit (language: python)

Issue:
Implement `pre-commit hook-impl`
- [x] basic support #63 
- [ ] legacy mode
- [ ] pre-push hook


Discussion / hints:

Fix patch:
diff --git a/src/cli/hook_impl.rs b/src/cli/hook_impl.rs
new file mode 100644
index 0000000..2dc7231
--- /dev/null
+++ b/src/cli/hook_impl.rs
@@ -0,0 +1,102 @@
+use anyhow::Result;
+use std::ffi::OsString;
+use std::path::PathBuf;
+
+use crate::cli::{self, ExitStatus, RunArgs};
+use crate::config::HookType;
+use crate::printer::Printer;
+use anstream::eprintln;
+
+pub(crate) async fn hook_impl(
+    config: Option<PathBuf>,
+    hook_type: HookType,
+    _hook_dir: PathBuf,
+    skip_on_missing_config: bool,
+    args: Vec<OsString>,
+    printer: Printer,
+) -> Result<ExitStatus> {
+    // TODO: run in legacy mode
+
+    if let Some(ref config_file) = config {
+        if !config_file.try_exists()? {
+            return if skip_on_missing_config
+                || std::env::var_os("PRE_COMMIT_ALLOW_NO_CONFIG").is_some()
+            {
+                Ok(ExitStatus::Success)
+            } else {
+                eprintln!("Config file not found: {}", config_file.display());
+                eprintln!(
+                    "- To temporarily silence this, run `PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`"
+                );
+                eprintln!("- To permanently silence this, install hooks with the `--allow-missing-config` flag");
+                eprintln!("- To uninstall hooks, run `pre-commit uninstall`");
+                Ok(ExitStatus::Failure)
+            };
+        }
+    }
+
+    if !hook_type.num_args().contains(&args.len()) {
+        eprintln!("Invalid number of arguments for hook: {}", hook_type);
+        return Ok(ExitStatus::Failure);
+    }
+
+    let run_args = to_run_args(hook_type, &args);
+
+    cli::run(
+        config,
+        run_args.hook_id,
+        Some(hook_type.into()),
+        run_args.from_ref,
+        run_args.to_ref,
+        run_args.all_files,
+        vec![],
+        false,
+        run_args.extra,
+        false,
+        printer,
+    )
+    .await
+}
+
+fn to_run_args(hook_type: HookType, args: &[OsString]) -> RunArgs {
+    let mut run_args = RunArgs::default();
+
+    match hook_type {
+        HookType::PrePush => {
+            run_args.extra.remote_name = Some(args[0].to_string_lossy().into_owned());
+            run_args.extra.remote_url = Some(args[1].to_string_lossy().into_owned());
+            // TODO: implement pre-push
+        }
+        HookType::CommitMsg => {
+            run_args.extra.commit_msg_filename = Some(PathBuf::from(&args[0]));
+        }
+        HookType::PrepareCommitMsg => {
+            run_args.extra.commit_msg_filename = Some(PathBuf::from(&args[0]));
+            if args.len() > 1 {
+                run_args.extra.prepare_commit_message_source =
+                    Some(args[1].to_string_lossy().into_owned());
+            }
+            if args.len() > 2 {
+                run_args.extra.commit_object_name = Some(args[2].to_string_lossy().into_owned());
+            }
+        }
+        HookType::PostCheckout => {
+            run_args.from_ref = Some(args[0].to_string_lossy().into_owned());
+            run_args.to_ref = Some(args[1].to_string_lossy().into_owned());
+            run_args.extra.checkout_type = Some(args[2].to_string_lossy().into_owned());
+        }
+        HookType::PostMerge => run_args.extra.is_squash_merge = args[0] == "1",
+        HookType::PostRewrite => {
+            run_args.extra.rewrite_command = Some(args[0].to_string_lossy().into_owned());
+        }
+        HookType::PreRebase => {
+            run_args.extra.pre_rebase_upstream = Some(args[0].to_string_lossy().into_owned());
+            if args.len() > 1 {
+                run_args.extra.pre_rebase_branch = Some(args[1].to_string_lossy().into_owned());
+            }
+        }
+        HookType::PostCommit | HookType::PreMergeCommit | HookType::PreCommit => {}
+    }
+
+    run_args
+}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index cfee2d2..d28ffd4 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -7,12 +7,14 @@ use clap::{ArgAction, Args, Parser, Subcommand};
 use crate::config::{HookType, Stage};
 
 mod clean;
+mod hook_impl;
 mod install;
 mod run;
 mod sample_config;
 mod validate;
 
 pub(crate) use clean::clean;
+pub(crate) use hook_impl::hook_impl;
 pub(crate) use install::{install, uninstall};
 pub(crate) use run::run;
 pub(crate) use sample_config::sample_config;
@@ -205,7 +207,7 @@ pub(crate) struct UninstallArgs {
     pub(crate) hook_types: Vec<HookType>,
 }
 
-#[derive(Debug, Clone, Args)]
+#[derive(Debug, Clone, Default, Args)]
 pub(crate) struct RunExtraArgs {
     #[arg(long, hide = true)]
     pub(crate) remote_branch: Option<String>,
@@ -233,7 +235,7 @@ pub(crate) struct RunExtraArgs {
     pub(crate) rewrite_command: Option<String>,
 }
 
-#[derive(Debug, Clone, Args)]
+#[derive(Debug, Clone, Default, Args)]
 pub(crate) struct RunArgs {
     /// The hook ID to run.
     #[arg(value_name = "HOOK")]
@@ -292,9 +294,9 @@ pub(crate) struct AutoUpdateArgs {
 #[derive(Debug, Args)]
 pub(crate) struct HookImplArgs {
     #[arg(long)]
-    pub(crate) hook_type: Option<HookType>,
+    pub(crate) hook_type: HookType,
     #[arg(long)]
-    pub(crate) hook_dir: Option<PathBuf>,
+    pub(crate) hook_dir: PathBuf,
     #[arg(long)]
     pub(crate) skip_on_missing_config: bool,
     #[arg(last = true)]
diff --git a/src/cli/run.rs b/src/cli/run.rs
index 5abe841..e486ea5 100644
--- a/src/cli/run.rs
+++ b/src/cli/run.rs
@@ -163,9 +163,7 @@ pub(crate) async fn run(
         verbose,
         printer,
     )
-    .await?;
-
-    Ok(ExitStatus::Success)
+    .await
 }
 
 async fn config_not_staged(config: &Path) -> Result<bool> {
diff --git a/src/config.rs b/src/config.rs
index b2df162..97973ba 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,5 +1,6 @@
 use std::collections::HashMap;
 use std::fmt::Display;
+use std::ops::RangeInclusive;
 use std::path::Path;
 use std::str::FromStr;
 
@@ -101,6 +102,22 @@ impl HookType {
             Self::PrepareCommitMsg => "prepare-commit-msg",
         }
     }
+
+    /// Return the number of arguments this hook type expects.
+    pub fn num_args(self) -> RangeInclusive<usize> {
+        match self {
+            Self::CommitMsg => 1..=1,
+            Self::PostCheckout => 3..=3,
+            Self::PreCommit => 0..=0,
+            Self::PostCommit => 0..=0,
+            Self::PreMergeCommit => 0..=0,
+            Self::PostMerge => 1..=1,
+            Self::PostRewrite => 1..=1,
+            Self::PrePush => 2..=2,
+            Self::PreRebase => 1..=2,
+            Self::PrepareCommitMsg => 1..=3,
+        }
+    }
 }
 
 impl Display for HookType {
@@ -129,6 +146,23 @@ pub enum Stage {
     PrepareCommitMsg,
 }
 
+impl From<HookType> for Stage {
+    fn from(value: HookType) -> Self {
+        match value {
+            HookType::CommitMsg => Self::CommitMsg,
+            HookType::PostCheckout => Self::PostCheckout,
+            HookType::PostCommit => Self::PostCommit,
+            HookType::PostMerge => Self::PostMerge,
+            HookType::PostRewrite => Self::PostRewrite,
+            HookType::PreCommit => Self::PreCommit,
+            HookType::PreMergeCommit => Self::PreMergeCommit,
+            HookType::PrePush => Self::PrePush,
+            HookType::PreRebase => Self::PreRebase,
+            HookType::PrepareCommitMsg => Self::PrepareCommitMsg,
+        }
+    }
+}
+
 impl Stage {
     pub fn as_str(&self) -> &str {
         match self {
diff --git a/src/main.rs b/src/main.rs
index 1171fe8..4c0c86b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -193,6 +193,19 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
             )
             .await
         }
+        Command::HookImpl(args) => {
+            show_settings!(args);
+
+            cli::hook_impl(
+                cli.globals.config,
+                args.hook_type,
+                args.hook_dir,
+                args.skip_on_missing_config,
+                args.args,
+                printer,
+            )
+            .await
+        }
         Command::Clean => cli::clean(printer),
         Command::ValidateConfig(args) => {
             show_settings!(args);
diff --git a/src/run.rs b/src/run.rs
index 2627894..5fc05a3 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -7,6 +7,7 @@ use std::path::Path;
 use std::sync::Arc;
 
 use anstream::ColorChoice;
+use anyhow::Result;
 use fancy_regex::{self as regex, Regex};
 use owo_colors::{OwoColorize, Style};
 use rand::prelude::{SliceRandom, StdRng};
@@ -17,6 +18,7 @@ use tokio::task::JoinSet;
 use tracing::{error, trace};
 use unicode_width::UnicodeWidthStr;
 
+use crate::cli::ExitStatus;
 use crate::git::get_diff;
 use crate::hook::Hook;
 use crate::identify::tags_from_path;
@@ -126,7 +128,7 @@ pub async fn run_hooks(
     show_diff_on_failure: bool,
     verbose: bool,
     printer: Printer,
-) -> anyhow::Result<()> {
+) -> Result<ExitStatus> {
     let env_vars = Arc::new(env_vars);
 
     let columns = calculate_columns(hooks);
@@ -172,7 +174,11 @@ pub async fn run_hooks(
             .await?;
     };
 
-    Ok(())
+    if success {
+        Ok(ExitStatus::Success)
+    } else {
+        Ok(ExitStatus::Failure)
+    }
 }
 
 /// Shuffle the files so that they more evenly fill out the xargs


Test patch:
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index ce58fef..d4be8b4 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -204,6 +204,22 @@ impl TestContext {
             .assert()
             .success();
 
+        Command::new("git")
+            .arg("config")
+            .current_dir(&self.temp_dir)
+            .arg("user.name")
+            .arg("Test User")
+            .assert()
+            .success();
+
+        Command::new("git")
+            .arg("config")
+            .current_dir(&self.temp_dir)
+            .arg("user.email")
+            .arg("test@example.com")
+            .assert()
+            .success();
+
         Command::new("git")
             .arg("add")
             .arg(".")
diff --git a/tests/hook_impl.rs b/tests/hook_impl.rs
new file mode 100644
index 0000000..462441f
--- /dev/null
+++ b/tests/hook_impl.rs
@@ -0,0 +1,69 @@
+use anyhow::Result;
+use assert_cmd::assert::OutputAssertExt;
+use assert_fs::fixture::{FileWriteStr, PathChild};
+use common::TestContext;
+use indoc::indoc;
+use std::process::Command;
+
+use crate::common::cmd_snapshot;
+
+mod common;
+
+#[test]
+fn hook_impl() -> Result<()> {
+    let context = TestContext::new();
+
+    context.init_project();
+
+    context
+        .workdir()
+        .child(".pre-commit-config.yaml")
+        .write_str(indoc! { r"
+            repos:
+            - repo: local
+              hooks:
+               - id: fail
+                 name: fail
+                 language: fail
+                 entry: always fail
+                 always_run: true
+            "
+        })?;
+
+    Command::new("git")
+        .arg("add")
+        .current_dir(context.workdir())
+        .arg(".")
+        .assert()
+        .success();
+
+    let mut commit = Command::new("git");
+    commit
+        .arg("commit")
+        .current_dir(context.workdir())
+        .arg("-m")
+        .arg("Initial commit");
+
+    cmd_snapshot!(context.filters(), context.install(), @r#"
+    success: true
+    exit_code: 0
+    ----- stdout -----
+    pre-commit installed at .git/hooks/pre-commit
+
+    ----- stderr -----
+    "#);
+
+    cmd_snapshot!(context.filters(), commit, @r#"
+    success: false
+    exit_code: 1
+    ----- stdout -----
+
+    ----- stderr -----
+    fail.....................................................................Failed
+    - hook id: fail
+    - exit code: 1
+    always fail
+    "#);
+
+    Ok(())
+}
diff --git a/tests/language.rs b/tests/language.rs
index e4b596d..af633e5 100644
--- a/tests/language.rs
+++ b/tests/language.rs
@@ -35,8 +35,8 @@ fn fail() -> Result<()> {
         .success();
 
     cmd_snapshot!(context.filters(), context.run(), @r#"
-    success: true
-    exit_code: 0
+    success: false
+    exit_code: 1
     ----- stdout -----
     changelogs must be rst...................................................Failed
     - hook id: changelogs-rst
diff --git a/tests/run.rs b/tests/run.rs
index 585efe1..0790313 100644
--- a/tests/run.rs
+++ b/tests/run.rs
@@ -40,8 +40,8 @@ fn run_basic() -> Result<()> {
         .success();
 
     cmd_snapshot!(context.filters(), context.run(), @r#"
-    success: true
-    exit_code: 0
+    success: false
+    exit_code: 1
     ----- stdout -----
     Cloning https://github.com/pre-commit/pre-commit-hooks@v5.0.0
     Installing environment for https://github.com/pre-commit/pre-commit-hooks@v5.0.0
@@ -277,8 +277,8 @@ fn files_and_exclude() -> Result<()> {
         .success();
 
     cmd_snapshot!(context.filters(), context.run(), @r#"
-    success: true
-    exit_code: 0
+    success: false
+    exit_code: 1
     ----- stdout -----
     Cloning https://github.com/pre-commit/pre-commit-hooks@v5.0.0
     Installing environment for https://github.com/pre-commit/pre-commit-hooks@v5.0.0
@@ -319,8 +319,8 @@ fn files_and_exclude() -> Result<()> {
         .success();
 
     cmd_snapshot!(context.filters(), context.run(), @r#"
-    success: true
-    exit_code: 0
+    success: false
+    exit_code: 1
     ----- stdout -----
     trim trailing whitespace.................................................Failed
     - hook id: trailing-whitespace
@@ -377,8 +377,8 @@ fn file_types() -> Result<()> {
         .success();
 
     cmd_snapshot!(context.filters(), context.run(), @r#"
-    success: true
-    exit_code: 0
+    success: false
+    exit_code: 1
     ----- stdout -----
     Cloning https://github.com/pre-commit/pre-commit-hooks@v5.0.0
     Installing environment for https://github.com/pre-commit/pre-commit-hooks@v5.0.0
@@ -444,8 +444,8 @@ fn fail_fast() -> Result<()> {
         .success();
 
     cmd_snapshot!(context.filters(), context.run(), @r#"
-    success: true
-    exit_code: 0
+    success: false
+    exit_code: 1
     ----- stdout -----
     Cloning https://github.com/pre-commit/pre-commit-hooks@v5.0.0
     Installing environment for https://github.com/pre-commit/pre-commit-hooks@v5.0.0
@@ -500,8 +500,8 @@ fn subdirectory() -> Result<()> {
         .success();
 
     cmd_snapshot!(context.filters(), context.run().current_dir(&child).arg("--files").arg("file.txt"), @r#"
-    success: true
-    exit_code: 0
+    success: false
+    exit_code: 1
     ----- stdout -----
     Cloning https://github.com/pre-commit/pre-commit-hooks@v5.0.0
     Installing environment for https://github.com/pre-commit/pre-commit-hooks@v5.0.0
@@ -549,8 +549,8 @@ fn log_file() -> Result<()> {
         .success();
 
     cmd_snapshot!(context.filters(), context.run(), @r#"
-    success: true
-    exit_code: 0
+    success: false
+    exit_code: 1
     ----- stdout -----
     Cloning https://github.com/pre-commit/pre-commit-hooks@v5.0.0
     Installing environment for https://github.com/pre-commit/pre-commit-hooks@v5.0.0