Repository: j178/prefligit (language: python)

Issue:
pre-commit install/uninstall



Discussion / hints:

Fix patch:
diff --git a/src/cli/install.rs b/src/cli/install.rs
index 04fd222..4b5f4b8 100644
--- a/src/cli/install.rs
+++ b/src/cli/install.rs
@@ -33,28 +33,12 @@ pub(crate) async fn install(
         return Ok(ExitStatus::Failure);
     }
 
-    let project = Project::from_config_file(config);
-
-    let mut hook_types = if hook_types.is_empty() {
-        if let Ok(ref project) = project {
-            project
-                .config()
-                .default_install_hook_types
-                .clone()
-                .unwrap_or_default()
-        } else {
-            vec![]
-        }
-    } else {
-        hook_types
-    };
-    if hook_types.is_empty() {
-        hook_types = vec![HookType::PreCommit];
-    }
+    let hook_types = get_hook_types(config.clone(), hook_types);
 
     let hooks_path = git::get_git_common_dir().await?.join("hooks");
     fs_err::create_dir_all(&hooks_path)?;
 
+    let project = Project::from_config_file(config);
     let config_file = project.as_ref().ok().map(Project::config_file);
     for hook_type in hook_types {
         install_hook_script(
@@ -79,6 +63,29 @@ pub(crate) async fn install(
     Ok(ExitStatus::Success)
 }
 
+fn get_hook_types(config_file: Option<PathBuf>, hook_types: Vec<HookType>) -> Vec<HookType> {
+    let project = Project::from_config_file(config_file);
+
+    let mut hook_types = if hook_types.is_empty() {
+        if let Ok(ref project) = project {
+            project
+                .config()
+                .default_install_hook_types
+                .clone()
+                .unwrap_or_default()
+        } else {
+            vec![]
+        }
+    } else {
+        hook_types
+    };
+    if hook_types.is_empty() {
+        hook_types = vec![HookType::PreCommit];
+    }
+
+    hook_types
+}
+
 fn install_hook_script(
     config_file: Option<&Path>,
     hook_type: HookType,
@@ -184,3 +191,47 @@ fn is_our_script(hook_path: &Path) -> Result<bool> {
         .chain(PRIOR_HASHES.iter().copied())
         .any(|hash| content.contains(hash)))
 }
+
+pub(crate) async fn uninstall(
+    config: Option<PathBuf>,
+    hook_types: Vec<HookType>,
+    printer: Printer,
+) -> Result<ExitStatus> {
+    for hook_type in get_hook_types(config, hook_types) {
+        let hooks_path = git::get_git_common_dir().await?.join("hooks");
+        let hook_path = hooks_path.join(hook_type.as_str());
+        let legacy_path = hooks_path.join(format!("{}.legacy", hook_type.as_str()));
+
+        if !hook_path.try_exists()? {
+            writeln!(
+                printer.stderr(),
+                "{} does not exist, skipping.",
+                hook_path.user_display().cyan()
+            )?;
+        } else if !is_our_script(&hook_path)? {
+            writeln!(
+                printer.stderr(),
+                "{} is not managed by pre-commit, skipping.",
+                hook_path.user_display().cyan()
+            )?;
+        } else {
+            fs_err::remove_file(&hook_path)?;
+            writeln!(
+                printer.stdout(),
+                "uninstalled {}",
+                hook_type.as_str().cyan()
+            )?;
+
+            if legacy_path.try_exists()? {
+                fs_err::rename(&legacy_path, &hook_path)?;
+                writeln!(
+                    printer.stdout(),
+                    "Restored previous hook to {}",
+                    hook_path.user_display().cyan()
+                )?;
+            }
+        }
+    }
+
+    Ok(ExitStatus::Success)
+}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 2b0a22e..8ee5319 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -12,7 +12,7 @@ mod run;
 mod validate;
 
 pub(crate) use clean::clean;
-pub(crate) use install::install;
+pub(crate) use install::{install, uninstall};
 pub(crate) use run::run;
 pub(crate) use validate::{validate_configs, validate_manifest};
 
@@ -150,7 +150,7 @@ pub(crate) enum Command {
     /// Run hooks.
     Run(Box<RunArgs>),
     /// Uninstall the pre-commit script.
-    Uninstall,
+    Uninstall(UninstallArgs),
     /// Validate `.pre-commit-config.yaml` files.
     ValidateConfig(ValidateConfigArgs),
     /// Validate `.pre-commit-hooks.yaml` files.
@@ -197,6 +197,12 @@ pub(crate) struct InstallArgs {
     pub(crate) allow_missing_config: bool,
 }
 
+#[derive(Debug, Args)]
+pub(crate) struct UninstallArgs {
+    #[arg(short = 't', long, value_enum, value_delimiter = ' ')]
+    pub(crate) hook_types: Vec<HookType>,
+}
+
 #[derive(Debug, Clone, Args)]
 pub(crate) struct RunExtraArgs {
     #[arg(long, hide = true)]
diff --git a/src/main.rs b/src/main.rs
index 4dcc457..33acd23 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -170,6 +170,11 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
             )
             .await
         }
+        Command::Uninstall(args) => {
+            show_settings!(args);
+
+            cli::uninstall(cli.globals.config, args.hook_types, printer).await
+        }
         Command::Run(args) => {
             show_settings!(args);
 


Test patch:
diff --git a/tests/install.rs b/tests/install.rs
index 3a34139..64eeed4 100644
--- a/tests/install.rs
+++ b/tests/install.rs
@@ -1,6 +1,10 @@
-use crate::common::{cmd_snapshot, TestContext};
+use assert_cmd::assert::OutputAssertExt;
+use assert_fs::assert::PathAssert;
 use assert_fs::fixture::{FileWriteStr, PathChild};
 use insta::assert_snapshot;
+use predicates::prelude::predicate;
+
+use crate::common::{cmd_snapshot, TestContext};
 
 mod common;
 
@@ -155,3 +159,63 @@ fn install() -> anyhow::Result<()> {
 
     Ok(())
 }
+
+#[test]
+fn uninstall() -> anyhow::Result<()> {
+    let context = TestContext::new();
+
+    context.init_project();
+
+    // Hook does not exist.
+    cmd_snapshot!(context.filters(), context.uninstall(), @r#"
+    success: true
+    exit_code: 0
+    ----- stdout -----
+
+    ----- stderr -----
+    .git/hooks/pre-commit does not exist, skipping.
+    "#);
+
+    // Uninstall `pre-commit` hook.
+    context.install().assert().success();
+    cmd_snapshot!(context.filters(), context.uninstall(), @r#"
+    success: true
+    exit_code: 0
+    ----- stdout -----
+    uninstalled pre-commit
+
+    ----- stderr -----
+    "#);
+    context
+        .workdir()
+        .child(".git/hooks/pre-commit")
+        .assert(predicate::path::missing());
+
+    // Hook is not managed by `pre-commit`.
+    context
+        .workdir()
+        .child(".git/hooks/pre-commit")
+        .write_str("#!/bin/sh\necho 'pre-commit'\n")?;
+    cmd_snapshot!(context.filters(), context.uninstall(), @r#"
+    success: true
+    exit_code: 0
+    ----- stdout -----
+
+    ----- stderr -----
+    .git/hooks/pre-commit is not managed by pre-commit, skipping.
+    "#);
+
+    // Restore previous hook.
+    context.install().assert().success();
+    cmd_snapshot!(context.filters(), context.uninstall(), @r#"
+    success: true
+    exit_code: 0
+    ----- stdout -----
+    uninstalled pre-commit
+    Restored previous hook to .git/hooks/pre-commit
+
+    ----- stderr -----
+    "#);
+
+    Ok(())
+}