On Thu, Apr 3, 2025 at 6:38 PM Derrick Stolee via GitGitGadget <gitgitgadget@xxxxxxxxx> wrote: > > From: Derrick Stolee <stolee@xxxxxxxxx> > > 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@xxxxxxxxx> > --- > 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 > -- D. Ben Knoble