diff mbox series

git: add --no-hooks global option

Message ID pull.1899.git.1743719888430.gitgitgadget@gmail.com (mailing list archive)
State New
Headers show
Series git: add --no-hooks global option | expand

Commit Message

Derrick Stolee April 3, 2025, 10:38 p.m. UTC
From: Derrick Stolee <stolee@gmail.com>

Git has several hooks which are executed during certain events as long
as those hooks are enabled in the hooks directory (possibly moved from
.git/hooks via the core.hooksPath config option). These are configured
by the user, or perhaps by tooling the user has agreed to, and are not
required to operate a Git repository.

In some situations, these hooks have poor performance and expert users
may want to skip the hooks as they don't seem to affect the current
situation. One example is a pre-commit hook that checks for certain
structures in the local changes, but expert users are likely to have
done the right thing in advance.

I have come across users who have disabled hooks themselves either by
deleting hooks (supported, safe) or setting 'core.hooksPath' to some
bogus path (seems unsafe). The supported process is painful to swap
between the hook-enabled scenario and the hook-disabled scenario.

To that end, add a new --no-hooks global option to allow users to
disable hooks quickly. This option is modeled similarly to the
--no-advice option in b79deeb554 (advice: add --no-advice global option,
2024-05-03). This uses a GIT_HOOKS environment variable to communicate
to subprocesses as well as making this a backwards-compatible way for
tools to signal that they want to disable hooks.

The critical piece is that all hooks pass through run_hooks_opt() where
a static int will evaluate the environment variable and store that the
variable is initialized for faster repeated runs.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
---
    git: add --no-hooks global option
    
    This is hopefully a helpful feature to more than just the experts I've
    been hearing from.
    
    Thanks,
    
     * Stolee

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1899%2Fderrickstolee%2Fno-hooks-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1899/derrickstolee/no-hooks-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1899

 Documentation/git.adoc       | 13 ++++++++++++-
 environment.h                |  6 ++++++
 git.c                        |  6 +++++-
 hook.c                       |  7 +++++++
 t/t1350-config-hooks-path.sh | 34 ++++++++++++++++++++++++++++++++++
 5 files changed, 64 insertions(+), 2 deletions(-)


base-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff

Comments

brian m. carlson April 3, 2025, 10:55 p.m. UTC | #1
On 2025-04-03 at 22:38:08, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
> 
> Git has several hooks which are executed during certain events as long
> as those hooks are enabled in the hooks directory (possibly moved from
> .git/hooks via the core.hooksPath config option). These are configured
> by the user, or perhaps by tooling the user has agreed to, and are not
> required to operate a Git repository.
> 
> In some situations, these hooks have poor performance and expert users
> may want to skip the hooks as they don't seem to affect the current
> situation. One example is a pre-commit hook that checks for certain
> structures in the local changes, but expert users are likely to have
> done the right thing in advance.
> 
> I have come across users who have disabled hooks themselves either by
> deleting hooks (supported, safe) or setting 'core.hooksPath' to some
> bogus path (seems unsafe). The supported process is painful to swap
> between the hook-enabled scenario and the hook-disabled scenario.
> 
> To that end, add a new --no-hooks global option to allow users to
> disable hooks quickly. This option is modeled similarly to the
> --no-advice option in b79deeb554 (advice: add --no-advice global option,
> 2024-05-03). This uses a GIT_HOOKS environment variable to communicate
> to subprocesses as well as making this a backwards-compatible way for
> tools to signal that they want to disable hooks.
> 
> The critical piece is that all hooks pass through run_hooks_opt() where
> a static int will evaluate the environment variable and store that the
> variable is initialized for faster repeated runs.
> 
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
>     git: add --no-hooks global option
>     
>     This is hopefully a helpful feature to more than just the experts I've
>     been hearing from.

I think this is functionality that Jenkins wants because they've
configured `core.hooksPath` to `/dev/null`, allegedly for security
reasons.  Of course, enabling this feature will also break Git LFS, but
in a less noticeable and detectable way (currently, Git LFS will fail on
attempting to install hooks with that setting of `core.hooksPath`, which
is at least noticeable).

I do think that in general certain types of hooks, such as pre-commit
hooks, should absolutely be optional.  There are lots of reasons to
commit WIP data that doesn't meet whatever standard and we shouldn't
impede expert users from expert workflows, even if there are many fewer
reasons to do things like bypass pushing Git LFS objects (which are
important for integrity).

So I can see the utility of this feature but I can also see how it can
break lots of things when handled poorly.  Of course, we also have `git
reset --hard` and there's lots of hand-wringing on Stack Overflow about
having deleted important data, so we have some precedent for expert
features that could break things badly.

I don't otherwise have a strong opinion either way, although I'd lean
slightly in favour of this series.  I'd of course welcome other people's
thoughts here.
Derrick Stolee April 4, 2025, 12:40 a.m. UTC | #2
On 4/3/2025 6:55 PM, brian m. carlson wrote:
> On 2025-04-03 at 22:38:08, Derrick Stolee via GitGitGadget wrote:
>> From: Derrick Stolee <stolee@gmail.com>

>> To that end, add a new --no-hooks global option to allow users to
>> disable hooks quickly. This option is modeled similarly to the
>> --no-advice option in b79deeb554 (advice: add --no-advice global option,
>> 2024-05-03). This uses a GIT_HOOKS environment variable to communicate
>> to subprocesses as well as making this a backwards-compatible way for
>> tools to signal that they want to disable hooks.
>>
>> The critical piece is that all hooks pass through run_hooks_opt() where
>> a static int will evaluate the environment variable and store that the
>> variable is initialized for faster repeated runs.

>>     This is hopefully a helpful feature to more than just the experts I've
>>     been hearing from.
> 
> I think this is functionality that Jenkins wants because they've
> configured `core.hooksPath` to `/dev/null`, allegedly for security
> reasons.  Of course, enabling this feature will also break Git LFS, but
> in a less noticeable and detectable way (currently, Git LFS will fail on
> attempting to install hooks with that setting of `core.hooksPath`, which
> is at least noticeable).

I agree that some hooks are necessary for certain repositories to keep
their data consistent. Another example is the pre- and post-command hooks
that are in the microsoft/git fork that are necessary to avoid issues with
the virtualization layer of VFS for Git. If a user disables those hooks
during a command that changes the index, then they are going to have a bad
time. But maybe they want to disable hooks because all they need is a
'git rev-parse HEAD' or similarly read-only operation. 
> I do think that in general certain types of hooks, such as pre-commit
> hooks, should absolutely be optional.  There are lots of reasons to
> commit WIP data that doesn't meet whatever standard and we shouldn't
> impede expert users from expert workflows, even if there are many fewer
> reasons to do things like bypass pushing Git LFS objects (which are
> important for integrity).

My intention is definitely more on the side of these optional hooks. 
> So I can see the utility of this feature but I can also see how it can
> break lots of things when handled poorly.  Of course, we also have `git
> reset --hard` and there's lots of hand-wringing on Stack Overflow about
> having deleted important data, so we have some precedent for expert
> features that could break things badly.
> 
> I don't otherwise have a strong opinion either way, although I'd lean
> slightly in favour of this series.  I'd of course welcome other people's
> thoughts here.
Thank you for chiming in! I look forward to other thoughts and whetherthe warning in the docs should be strengthened.

Thanks,
-Stolee
Phillip Wood April 4, 2025, 2:15 p.m. UTC | #3
Hi Stolee

On 03/04/2025 23:38, Derrick Stolee via GitGitGadget wrote:
> From: Derrick Stolee <stolee@gmail.com>
> 
> In some situations, these hooks have poor performance and expert users
> may want to skip the hooks as they don't seem to affect the current
> situation. One example is a pre-commit hook that checks for certain
> structures in the local changes, but expert users are likely to have
> done the right thing in advance.

Next they'll be saying that they never make a mistake when writing a one 
line patch! More seriously I agree there are times when one may want to 
bypass the pre-commit hook but we already have "git commit --no-verify" 
to do that. In general hooks that are so slow that the user wants to 
bypass them are self-defeating and I'd argue that the solution is to fix 
the performance of the hook rather than make it easier to skip it. One 
solution for speeding up pre-commit hooks is to process files in 
parallel. Unfortunately git does not provide support for that but there 
are hook frameworks that do.

> I have come across users who have disabled hooks themselves either by
> deleting hooks (supported, safe) or setting 'core.hooksPath' to some
> bogus path (seems unsafe).

I thought "git -c core.hooksPath=/dev/null" was a fairly standard way of 
disabling hooks on a one-off basis - what makes it unsafe?

> The supported process is painful to swap
> between the hook-enabled scenario and the hook-disabled scenario.
> 
> To that end, add a new --no-hooks global option to allow users to
> disable hooks quickly. This option is modeled similarly to the
> --no-advice option in b79deeb554 (advice: add --no-advice global option,
> 2024-05-03). This uses a GIT_HOOKS environment variable to communicate
> to subprocesses as well as making this a backwards-compatible way for
> tools to signal that they want to disable hooks.
> 
> The critical piece is that all hooks pass through run_hooks_opt() where
> a static int will evaluate the environment variable and store that the
> variable is initialized for faster repeated runs.

That certainly makes the implementation much more viable. However I'm 
not really convinced this is a good idea.

Best Wishes

Phillip

> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
>      git: add --no-hooks global option
>      
>      This is hopefully a helpful feature to more than just the experts I've
>      been hearing from.
>      
>      Thanks,
>      
>       * Stolee
> 
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1899%2Fderrickstolee%2Fno-hooks-v1
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1899/derrickstolee/no-hooks-v1
> Pull-Request: https://github.com/gitgitgadget/git/pull/1899
> 
>   Documentation/git.adoc       | 13 ++++++++++++-
>   environment.h                |  6 ++++++
>   git.c                        |  6 +++++-
>   hook.c                       |  7 +++++++
>   t/t1350-config-hooks-path.sh | 34 ++++++++++++++++++++++++++++++++++
>   5 files changed, 64 insertions(+), 2 deletions(-)
> 
> diff --git a/Documentation/git.adoc b/Documentation/git.adoc
> index 743b7b00e4d..a34c8cfbe78 100644
> --- a/Documentation/git.adoc
> +++ b/Documentation/git.adoc
> @@ -14,7 +14,7 @@ SYNOPSIS
>       [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--no-lazy-fetch]
>       [--no-optional-locks] [--no-advice] [--bare] [--git-dir=<path>]
>       [--work-tree=<path>] [--namespace=<name>] [--config-env=<name>=<envvar>]
> -    <command> [<args>]
> +    [--no-hooks] <command> [<args>]
>   
>   DESCRIPTION
>   -----------
> @@ -230,6 +230,12 @@ If you just want to run git as if it was started in `<path>` then use
>   	linkgit:gitattributes[5]. This is equivalent to setting the
>   	`GIT_ATTR_SOURCE` environment variable.
>   
> +--no-hooks::
> +	Skip running local Git hooks, even if configured locally. Hooks
> +	are an opt-in feature, so be sure that you know the impact of
> +	ignoring hooks when running with this option. This is equivalent
> +	to setting `GIT_HOOKS=0` environment variable.
> +
>   GIT COMMANDS
>   ------------
>   
> @@ -771,6 +777,11 @@ for further details.
>   	not set, Git will choose buffered or record-oriented flushing
>   	based on whether stdout appears to be redirected to a file or not.
>   
> +`GIT_HOOKS`::
> +	If this Boolean environment variable is set to false, then commands
> +	will ignore any configured hooks as if the `--no-hooks` option was
> +	provided.
> +
>   `GIT_TRACE`::
>   	Enables general trace messages, e.g. alias expansion, built-in
>   	command execution and external command execution.
> diff --git a/environment.h b/environment.h
> index 45e690f203f..22ddf201144 100644
> --- a/environment.h
> +++ b/environment.h
> @@ -50,6 +50,12 @@
>    */
>   #define GIT_ADVICE_ENVIRONMENT "GIT_ADVICE"
>   
> +/*
> + * Environment variable used to propagate the --no-hooks global option to
> + * the hooks layer and to any child processes.
> + */
> +#define GIT_HOOKS "GIT_HOOKS"
> +
>   /*
>    * Environment variable used in handshaking the wire protocol.
>    * Contains a colon ':' separated list of keys with optional values
> diff --git a/git.c b/git.c
> index 77c43595223..d7ebcf60947 100644
> --- a/git.c
> +++ b/git.c
> @@ -41,7 +41,7 @@ const char git_usage_string[] =
>   	   "           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--no-lazy-fetch]\n"
>   	   "           [--no-optional-locks] [--no-advice] [--bare] [--git-dir=<path>]\n"
>   	   "           [--work-tree=<path>] [--namespace=<name>] [--config-env=<name>=<envvar>]\n"
> -	   "           <command> [<args>]");
> +	   "           [--no-hooks] <command> [<args>]");
>   
>   const char git_more_info_string[] =
>   	N_("'git help -a' and 'git help -g' list available subcommands and some\n"
> @@ -349,6 +349,10 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
>   			setenv(GIT_ADVICE_ENVIRONMENT, "0", 1);
>   			if (envchanged)
>   				*envchanged = 1;
> +		} else if (!strcmp(cmd, "--no-hooks")) {
> +			setenv(GIT_HOOKS, "0", 1);
> +			if (envchanged)
> +				*envchanged = 1;
>   		} else {
>   			fprintf(stderr, _("unknown option: %s\n"), cmd);
>   			usage(git_usage_string);
> diff --git a/hook.c b/hook.c
> index b3de1048bf4..b209553d7a8 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -144,6 +144,13 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
>   
>   		.data = &cb_data,
>   	};
> +	static int do_run_hooks = -1;
> +
> +	if (do_run_hooks < 0)
> +		do_run_hooks = git_env_bool(GIT_HOOKS, 1);
> +
> +	if (!do_run_hooks)
> +		goto cleanup;
>   
>   	if (!options)
>   		BUG("a struct run_hooks_opt must be provided to run_hooks");
> diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
> index 45a04929170..4c6a0eafe4e 100755
> --- a/t/t1350-config-hooks-path.sh
> +++ b/t/t1350-config-hooks-path.sh
> @@ -48,4 +48,38 @@ test_expect_success 'core.hooksPath=/dev/null' '
>   	{ test /dev/null = "$value" || test nul = "$value"; }
>   '
>   
> +test_expect_success '--no-hooks' '
> +	rm -f actual &&
> +	test_might_fail git config --unset core.hooksPath &&
> +
> +	write_script .git/hooks/pre-commit <<-\EOF &&
> +	echo HOOK >>actual
> +	EOF
> +
> +	echo HOOK >expect &&
> +
> +	git commit --allow-empty -m "A" &&
> +	test_cmp expect actual &&
> +
> +	git --no-hooks commit --allow-empty -m "B" &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'GIT_HOOKS' '
> +	rm -f actual &&
> +	test_might_fail git config --unset core.hooksPath &&
> +
> +	write_script .git/hooks/pre-commit <<-\EOF &&
> +	echo HOOK >>actual
> +	EOF
> +
> +	echo HOOK >expect &&
> +
> +	GIT_HOOKS=1 git commit --allow-empty -m "A" &&
> +	test_cmp expect actual &&
> +
> +	GIT_HOOKS=0 git commit --allow-empty -m "B" &&
> +	test_cmp expect actual
> +'
> +
>   test_done
> 
> base-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff
D. Ben Knoble April 11, 2025, 6:53 p.m. UTC | #4
On Thu, Apr 3, 2025 at 6:38 PM Derrick Stolee via GitGitGadget
<gitgitgadget@gmail.com> wrote:
>
> From: Derrick Stolee <stolee@gmail.com>
>
> Git has several hooks which are executed during certain events as long
> as those hooks are enabled in the hooks directory (possibly moved from
> .git/hooks via the core.hooksPath config option). These are configured
> by the user, or perhaps by tooling the user has agreed to, and are not
> required to operate a Git repository.
>
> In some situations, these hooks have poor performance and expert users
> may want to skip the hooks as they don't seem to affect the current
> situation. One example is a pre-commit hook that checks for certain
> structures in the local changes, but expert users are likely to have
> done the right thing in advance.

Or a pre-push hook that runs tests which, for some reason, are very
slow to execute but which isn't properly shutdown by a normal SIGINT,
messing up my shell.

> I have come across users who have disabled hooks themselves either by
> deleting hooks (supported, safe) or setting 'core.hooksPath' to some
> bogus path (seems unsafe). The supported process is painful to swap
> between the hook-enabled scenario and the hook-disabled scenario.

In my particular case, the tool being used (husky) sets core.hooksPath
to a directory in the repo, so I can go back to using my preferred
hooks simply by unsetting core.hooksPath, which seems safe. But
swapping is still a bit painful, as mentioned.

Worse, every "npm install" in this repo re-enables the hooks, so I
have to remember to reset core.hooksPath :/ When I don't, I spend a
few frustrating minutes getting back to normal. And then I
accidentally train myself to use --no-verify often with this project,
which is IMO a worse state of affairs.

Obviously, this repo's hooks need fixed, but in the interim something
like --no-hooks seems useful to me. Not sure if I count among the
experts or not /shrug.

> To that end, add a new --no-hooks global option to allow users to
> disable hooks quickly. This option is modeled similarly to the
> --no-advice option in b79deeb554 (advice: add --no-advice global option,
> 2024-05-03). This uses a GIT_HOOKS environment variable to communicate
> to subprocesses as well as making this a backwards-compatible way for
> tools to signal that they want to disable hooks.

On a related note: with client-side hooks always being considered
optional, it seems like there ought to be a safe, sane way to turn
them off (rather than remember --no-verify for the various commands,
which is probably the safe, sane way to bypass them on a one-off
basis).

Maybe with this I'll have more ammo to convince folks that their
sanity/security/whatever checks belong in server-side hooks and
optional developer scripts rather than pre-commit hooks.

>
> The critical piece is that all hooks pass through run_hooks_opt() where
> a static int will evaluate the environment variable and store that the
> variable is initialized for faster repeated runs.
>
> Signed-off-by: Derrick Stolee <stolee@gmail.com>
> ---
>     git: add --no-hooks global option
>
>     This is hopefully a helpful feature to more than just the experts I've
>     been hearing from.
>
>     Thanks,
>
>      * Stolee
>
> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1899%2Fderrickstolee%2Fno-hooks-v1
> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1899/derrickstolee/no-hooks-v1
> Pull-Request: https://github.com/gitgitgadget/git/pull/1899
>
>  Documentation/git.adoc       | 13 ++++++++++++-
>  environment.h                |  6 ++++++
>  git.c                        |  6 +++++-
>  hook.c                       |  7 +++++++
>  t/t1350-config-hooks-path.sh | 34 ++++++++++++++++++++++++++++++++++
>  5 files changed, 64 insertions(+), 2 deletions(-)
>
> diff --git a/Documentation/git.adoc b/Documentation/git.adoc
> index 743b7b00e4d..a34c8cfbe78 100644
> --- a/Documentation/git.adoc
> +++ b/Documentation/git.adoc
> @@ -14,7 +14,7 @@ SYNOPSIS
>      [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--no-lazy-fetch]
>      [--no-optional-locks] [--no-advice] [--bare] [--git-dir=<path>]
>      [--work-tree=<path>] [--namespace=<name>] [--config-env=<name>=<envvar>]
> -    <command> [<args>]
> +    [--no-hooks] <command> [<args>]
>
>  DESCRIPTION
>  -----------
> @@ -230,6 +230,12 @@ If you just want to run git as if it was started in `<path>` then use
>         linkgit:gitattributes[5]. This is equivalent to setting the
>         `GIT_ATTR_SOURCE` environment variable.
>
> +--no-hooks::
> +       Skip running local Git hooks, even if configured locally. Hooks
> +       are an opt-in feature, so be sure that you know the impact of
> +       ignoring hooks when running with this option. This is equivalent
> +       to setting `GIT_HOOKS=0` environment variable.
> +
>  GIT COMMANDS
>  ------------
>
> @@ -771,6 +777,11 @@ for further details.
>         not set, Git will choose buffered or record-oriented flushing
>         based on whether stdout appears to be redirected to a file or not.
>
> +`GIT_HOOKS`::
> +       If this Boolean environment variable is set to false, then commands
> +       will ignore any configured hooks as if the `--no-hooks` option was
> +       provided.
> +
>  `GIT_TRACE`::
>         Enables general trace messages, e.g. alias expansion, built-in
>         command execution and external command execution.
> diff --git a/environment.h b/environment.h
> index 45e690f203f..22ddf201144 100644
> --- a/environment.h
> +++ b/environment.h
> @@ -50,6 +50,12 @@
>   */
>  #define GIT_ADVICE_ENVIRONMENT "GIT_ADVICE"
>
> +/*
> + * Environment variable used to propagate the --no-hooks global option to
> + * the hooks layer and to any child processes.
> + */
> +#define GIT_HOOKS "GIT_HOOKS"
> +
>  /*
>   * Environment variable used in handshaking the wire protocol.
>   * Contains a colon ':' separated list of keys with optional values
> diff --git a/git.c b/git.c
> index 77c43595223..d7ebcf60947 100644
> --- a/git.c
> +++ b/git.c
> @@ -41,7 +41,7 @@ const char git_usage_string[] =
>            "           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--no-lazy-fetch]\n"
>            "           [--no-optional-locks] [--no-advice] [--bare] [--git-dir=<path>]\n"
>            "           [--work-tree=<path>] [--namespace=<name>] [--config-env=<name>=<envvar>]\n"
> -          "           <command> [<args>]");
> +          "           [--no-hooks] <command> [<args>]");
>
>  const char git_more_info_string[] =
>         N_("'git help -a' and 'git help -g' list available subcommands and some\n"
> @@ -349,6 +349,10 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
>                         setenv(GIT_ADVICE_ENVIRONMENT, "0", 1);
>                         if (envchanged)
>                                 *envchanged = 1;
> +               } else if (!strcmp(cmd, "--no-hooks")) {
> +                       setenv(GIT_HOOKS, "0", 1);
> +                       if (envchanged)
> +                               *envchanged = 1;
>                 } else {
>                         fprintf(stderr, _("unknown option: %s\n"), cmd);
>                         usage(git_usage_string);
> diff --git a/hook.c b/hook.c
> index b3de1048bf4..b209553d7a8 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -144,6 +144,13 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
>
>                 .data = &cb_data,
>         };
> +       static int do_run_hooks = -1;
> +
> +       if (do_run_hooks < 0)
> +               do_run_hooks = git_env_bool(GIT_HOOKS, 1);
> +
> +       if (!do_run_hooks)
> +               goto cleanup;
>
>         if (!options)
>                 BUG("a struct run_hooks_opt must be provided to run_hooks");
> diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
> index 45a04929170..4c6a0eafe4e 100755
> --- a/t/t1350-config-hooks-path.sh
> +++ b/t/t1350-config-hooks-path.sh
> @@ -48,4 +48,38 @@ test_expect_success 'core.hooksPath=/dev/null' '
>         { test /dev/null = "$value" || test nul = "$value"; }
>  '
>
> +test_expect_success '--no-hooks' '
> +       rm -f actual &&
> +       test_might_fail git config --unset core.hooksPath &&
> +
> +       write_script .git/hooks/pre-commit <<-\EOF &&
> +       echo HOOK >>actual
> +       EOF
> +
> +       echo HOOK >expect &&
> +
> +       git commit --allow-empty -m "A" &&
> +       test_cmp expect actual &&
> +
> +       git --no-hooks commit --allow-empty -m "B" &&
> +       test_cmp expect actual
> +'
> +
> +test_expect_success 'GIT_HOOKS' '
> +       rm -f actual &&
> +       test_might_fail git config --unset core.hooksPath &&
> +
> +       write_script .git/hooks/pre-commit <<-\EOF &&
> +       echo HOOK >>actual
> +       EOF
> +
> +       echo HOOK >expect &&
> +
> +       GIT_HOOKS=1 git commit --allow-empty -m "A" &&
> +       test_cmp expect actual &&
> +
> +       GIT_HOOKS=0 git commit --allow-empty -m "B" &&
> +       test_cmp expect actual
> +'
> +
>  test_done
>
> base-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff
> --
> gitgitgadget
>
Derrick Stolee April 14, 2025, 10:59 a.m. UTC | #5
On 4/4/2025 10:15 AM, Phillip Wood wrote:
> Hi Stolee
> 
> On 03/04/2025 23:38, Derrick Stolee via GitGitGadget wrote:
>> From: Derrick Stolee <stolee@gmail.com>
>>
>> In some situations, these hooks have poor performance and expert users
>> may want to skip the hooks as they don't seem to affect the current
>> situation. One example is a pre-commit hook that checks for certain
>> structures in the local changes, but expert users are likely to have
>> done the right thing in advance.
> 
> Next they'll be saying that they never make a mistake when writing a
> one line patch! More seriously I agree there are times when one may
> want to bypass the pre-commit hook but we already have "git commit
> --no-verify" to do that. In general hooks that are so slow that the
> user wants to bypass them are self-defeating and I'd argue that the
> solution is to fix the performance of the hook rather than make it
> easier to skip it.

Both can also be an option.

> One solution for speeding up pre-commit hooks is
> to process files in parallel. Unfortunately git does not provide
> support for that but there are hook frameworks that do.

>> I have come across users who have disabled hooks themselves either by
>> deleting hooks (supported, safe) or setting 'core.hooksPath' to some
>> bogus path (seems unsafe).
> 
> I thought "git -c core.hooksPath=/dev/null" was a fairly standard
> way of disabling hooks on a one-off basis - what makes it unsafe?

You're right. I was thinking about setting it to a "directory that
doesn't exist" (but actually could be a path that exists accidentally
like "/bogus") but I forgot that we could use /dev/null.

I'll remove this "(seems unsafe)" wording. 
>> The supported process is painful to swap
>> between the hook-enabled scenario and the hook-disabled scenario.
>>
>> To that end, add a new --no-hooks global option to allow users to
>> disable hooks quickly. This option is modeled similarly to the
>> --no-advice option in b79deeb554 (advice: add --no-advice global option,
>> 2024-05-03). This uses a GIT_HOOKS environment variable to communicate
>> to subprocesses as well as making this a backwards-compatible way for
>> tools to signal that they want to disable hooks.
>>
>> The critical piece is that all hooks pass through run_hooks_opt() where
>> a static int will evaluate the environment variable and store that the
>> variable is initialized for faster repeated runs.
> 
> That certainly makes the implementation much more viable. However I'm
> not really convinced this is a good idea.

I don't read a strong reason in your message that this is a _bad_
idea either. As in, there's nothing that hints that this will cause
significant harm to users other than providing a new footgun (and we
have plenty of those for folks willing to look, including the
_existence_ of hooks).

Thanks,
-Stolee
Lucas Seiki Oshiro April 14, 2025, 3:25 p.m. UTC | #6
Hi!

> I thought "git -c core.hooksPath=/dev/null" was a fairly standard way of disabling hooks

Given that, wouldn't it be a case to turn this into a documentation patch?

I just searched here I found that we even have a test for it (introduced
in c8f6478), but I couldn't find that as a recommendation in our docs.
brian m. carlson April 14, 2025, 9:05 p.m. UTC | #7
On 2025-04-14 at 15:25:42, Lucas Seiki Oshiro wrote:
> Hi!
> 
> > I thought "git -c core.hooksPath=/dev/null" was a fairly standard way of disabling hooks
> 
> Given that, wouldn't it be a case to turn this into a documentation patch?
> 
> I just searched here I found that we even have a test for it (introduced
> in c8f6478), but I couldn't find that as a recommendation in our docs.

I think if we decide to keep this series, then we should probably keep
the documentation in this series, since it will be clearer and more
straightforward as a way to disable hooks, and if we don't, then a new
documentation patch would be a good idea.
Phillip Wood April 16, 2025, 10:29 a.m. UTC | #8
Hi Stolee

On 14/04/2025 11:59, Derrick Stolee wrote:
> On 4/4/2025 10:15 AM, Phillip Wood wrote:
>> On 03/04/2025 23:38, Derrick Stolee via GitGitGadget wrote:
>>> From: Derrick Stolee <stolee@gmail.com>
>>>
>>> To that end, add a new --no-hooks global option to allow users to
>>> disable hooks quickly. This option is modeled similarly to the
>>> --no-advice option in b79deeb554 (advice: add --no-advice global option,
>>> 2024-05-03). This uses a GIT_HOOKS environment variable to communicate
>>> to subprocesses as well as making this a backwards-compatible way for
>>> tools to signal that they want to disable hooks.
>>>
>>> The critical piece is that all hooks pass through run_hooks_opt() where
>>> a static int will evaluate the environment variable and store that the
>>> variable is initialized for faster repeated runs.
>>
>> That certainly makes the implementation much more viable. However I'm
>> not really convinced this is a good idea.
> 
> I don't read a strong reason in your message that this is a _bad_
> idea either. As in, there's nothing that hints that this will cause
> significant harm to users other than providing a new footgun (and we
> have plenty of those for folks willing to look, including the
> _existence_ of hooks).

It is certainly not a terrible idea given that it is possible to disable 
hooks already but I'm not clear what the motivation is. I don't find the 
example of a skipping a pre-commit hook persuasive as we already provide 
a convenient way for users to skip that hook. Elsewhere in this thread 
you mention the "pre-command" and "post-command" hooks but they are not 
part of git - if a fork is running its own hooks and that is causing 
problems for users I'm not sure we want to change the upstream project 
to address that. If there was a clearer motivation it would be easier to 
understand the benefits of this change.

Best Wishes

Phillip
Junio C Hamano April 16, 2025, 2:28 p.m. UTC | #9
Phillip Wood <phillip.wood123@gmail.com> writes:

>> I don't read a strong reason in your message that this is a _bad_
>> idea either. As in, there's nothing that hints that this will cause
>> significant harm to users other than providing a new footgun (and we
>> have plenty of those for folks willing to look, including the
>> _existence_ of hooks).
>
> It is certainly not a terrible idea given that it is possible to
> disable hooks already but I'm not clear what the motivation is. I
> don't find the example of a skipping a pre-commit hook persuasive as
> we already provide a convenient way for users to skip that
> hook. Elsewhere in this thread you mention the "pre-command" and
> "post-command" hooks but they are not part of git - if a fork is
> running its own hooks and that is causing problems for users I'm not
> sure we want to change the upstream project to address that. If there
> was a clearer motivation it would be easier to understand the benefits
> of this change.

Thanks for pushing back.  The default for any new changes is not to
apply unless there is a compelling reason why it is a good idea,
saying that this is not a bad thing does not serve as an effective
justification.

If we want to give scripters a more stable foundation to build on,
the answer should not be to pile more and more "no hooks, no
configurations, just a vanilla mode of operation" options to
end-user facing porcelain commands, but to clean up the internal
implementation of such porcelain commands to refactor into stable
plumbing commands that scripters can rely on.
Derrick Stolee April 16, 2025, 3:30 p.m. UTC | #10
On 4/16/2025 10:28 AM, Junio C Hamano wrote:
> Phillip Wood <phillip.wood123@gmail.com> writes:
> 
>>> I don't read a strong reason in your message that this is a _bad_
>>> idea either. As in, there's nothing that hints that this will cause
>>> significant harm to users other than providing a new footgun (and we
>>> have plenty of those for folks willing to look, including the
>>> _existence_ of hooks).
>>
>> It is certainly not a terrible idea given that it is possible to
>> disable hooks already but I'm not clear what the motivation is. I
>> don't find the example of a skipping a pre-commit hook persuasive as
>> we already provide a convenient way for users to skip that
>> hook. Elsewhere in this thread you mention the "pre-command" and
>> "post-command" hooks but they are not part of git - if a fork is
>> running its own hooks and that is causing problems for users I'm not
>> sure we want to change the upstream project to address that. If there
>> was a clearer motivation it would be easier to understand the benefits
>> of this change.
> 
> Thanks for pushing back.  The default for any new changes is not to
> apply unless there is a compelling reason why it is a good idea,
> saying that this is not a bad thing does not serve as an effective
> justification.
> 
> If we want to give scripters a more stable foundation to build on,
> the answer should not be to pile more and more "no hooks, no
> configurations, just a vanilla mode of operation" options to
> end-user facing porcelain commands, but to clean up the internal
> implementation of such porcelain commands to refactor into stable
> plumbing commands that scripters can rely on.

Thank you for a decisive answer. I'll move forward with a v2 that
is a doc-only change, documenting the /dev/null value as a supported
mechanism for disabling hooks.

Thanks,
-Stolee
diff mbox series

Patch

diff --git a/Documentation/git.adoc b/Documentation/git.adoc
index 743b7b00e4d..a34c8cfbe78 100644
--- a/Documentation/git.adoc
+++ b/Documentation/git.adoc
@@ -14,7 +14,7 @@  SYNOPSIS
     [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--no-lazy-fetch]
     [--no-optional-locks] [--no-advice] [--bare] [--git-dir=<path>]
     [--work-tree=<path>] [--namespace=<name>] [--config-env=<name>=<envvar>]
-    <command> [<args>]
+    [--no-hooks] <command> [<args>]
 
 DESCRIPTION
 -----------
@@ -230,6 +230,12 @@  If you just want to run git as if it was started in `<path>` then use
 	linkgit:gitattributes[5]. This is equivalent to setting the
 	`GIT_ATTR_SOURCE` environment variable.
 
+--no-hooks::
+	Skip running local Git hooks, even if configured locally. Hooks
+	are an opt-in feature, so be sure that you know the impact of
+	ignoring hooks when running with this option. This is equivalent
+	to setting `GIT_HOOKS=0` environment variable.
+
 GIT COMMANDS
 ------------
 
@@ -771,6 +777,11 @@  for further details.
 	not set, Git will choose buffered or record-oriented flushing
 	based on whether stdout appears to be redirected to a file or not.
 
+`GIT_HOOKS`::
+	If this Boolean environment variable is set to false, then commands
+	will ignore any configured hooks as if the `--no-hooks` option was
+	provided.
+
 `GIT_TRACE`::
 	Enables general trace messages, e.g. alias expansion, built-in
 	command execution and external command execution.
diff --git a/environment.h b/environment.h
index 45e690f203f..22ddf201144 100644
--- a/environment.h
+++ b/environment.h
@@ -50,6 +50,12 @@ 
  */
 #define GIT_ADVICE_ENVIRONMENT "GIT_ADVICE"
 
+/*
+ * Environment variable used to propagate the --no-hooks global option to
+ * the hooks layer and to any child processes.
+ */
+#define GIT_HOOKS "GIT_HOOKS"
+
 /*
  * Environment variable used in handshaking the wire protocol.
  * Contains a colon ':' separated list of keys with optional values
diff --git a/git.c b/git.c
index 77c43595223..d7ebcf60947 100644
--- a/git.c
+++ b/git.c
@@ -41,7 +41,7 @@  const char git_usage_string[] =
 	   "           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--no-lazy-fetch]\n"
 	   "           [--no-optional-locks] [--no-advice] [--bare] [--git-dir=<path>]\n"
 	   "           [--work-tree=<path>] [--namespace=<name>] [--config-env=<name>=<envvar>]\n"
-	   "           <command> [<args>]");
+	   "           [--no-hooks] <command> [<args>]");
 
 const char git_more_info_string[] =
 	N_("'git help -a' and 'git help -g' list available subcommands and some\n"
@@ -349,6 +349,10 @@  static int handle_options(const char ***argv, int *argc, int *envchanged)
 			setenv(GIT_ADVICE_ENVIRONMENT, "0", 1);
 			if (envchanged)
 				*envchanged = 1;
+		} else if (!strcmp(cmd, "--no-hooks")) {
+			setenv(GIT_HOOKS, "0", 1);
+			if (envchanged)
+				*envchanged = 1;
 		} else {
 			fprintf(stderr, _("unknown option: %s\n"), cmd);
 			usage(git_usage_string);
diff --git a/hook.c b/hook.c
index b3de1048bf4..b209553d7a8 100644
--- a/hook.c
+++ b/hook.c
@@ -144,6 +144,13 @@  int run_hooks_opt(struct repository *r, const char *hook_name,
 
 		.data = &cb_data,
 	};
+	static int do_run_hooks = -1;
+
+	if (do_run_hooks < 0)
+		do_run_hooks = git_env_bool(GIT_HOOKS, 1);
+
+	if (!do_run_hooks)
+		goto cleanup;
 
 	if (!options)
 		BUG("a struct run_hooks_opt must be provided to run_hooks");
diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
index 45a04929170..4c6a0eafe4e 100755
--- a/t/t1350-config-hooks-path.sh
+++ b/t/t1350-config-hooks-path.sh
@@ -48,4 +48,38 @@  test_expect_success 'core.hooksPath=/dev/null' '
 	{ test /dev/null = "$value" || test nul = "$value"; }
 '
 
+test_expect_success '--no-hooks' '
+	rm -f actual &&
+	test_might_fail git config --unset core.hooksPath &&
+
+	write_script .git/hooks/pre-commit <<-\EOF &&
+	echo HOOK >>actual
+	EOF
+
+	echo HOOK >expect &&
+
+	git commit --allow-empty -m "A" &&
+	test_cmp expect actual &&
+
+	git --no-hooks commit --allow-empty -m "B" &&
+	test_cmp expect actual
+'
+
+test_expect_success 'GIT_HOOKS' '
+	rm -f actual &&
+	test_might_fail git config --unset core.hooksPath &&
+
+	write_script .git/hooks/pre-commit <<-\EOF &&
+	echo HOOK >>actual
+	EOF
+
+	echo HOOK >expect &&
+
+	GIT_HOOKS=1 git commit --allow-empty -m "A" &&
+	test_cmp expect actual &&
+
+	GIT_HOOKS=0 git commit --allow-empty -m "B" &&
+	test_cmp expect actual
+'
+
 test_done