Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Documentation/config/hook.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ endif::git-hook[]
ifndef::git-hook[]
:see-git-hook: See linkgit:git-hook[1].
endif::git-hook[]
hook.allowNoVerify::
Specifies whether the `--no-verify` (or `-n`) command-line option
is permitted in commands that run client-side hooks, such as `git commit`,
`git push`, `git merge`, `git rebase`, and `git am`.
+
Allowed values are:
+
--
* `true`: `--no-verify` is permitted normally. This is the default.
* `warn`: `--no-verify` is permitted, but Git prints a warning on stderr.
* `false`: `--no-verify` is disallowed and Git aborts
with a fatal error accompanied by advice explaining how to override it.
--
+
In an emergency (for example, when a local hook crashes or during a critical
production hotfix), this guardrail can be overridden without modifying
configuration files by setting the `GIT_ALLOW_NO_VERIFY=1` environment variable
or by passing `-c hook.allowNoVerify=true` on the command line.
In automated or scripted environments (such as CI/CD runners), `hook.allowNoVerify = warn`
provides visibility on stderr while allowing unattended workflows to complete without failure.
+
NOTE: Client-side hooks execute in the developer's environment and belong to
the user. This configuration serves strictly as an ergonomic workflow guardrail
against accidental bypasses (such as muscle-memory `-n` or automated scripts),
and must not be relied upon as a security boundary. Authoritative enforcement
must always be implemented server-side (for example, via `pre-receive` hooks).

hook.<friendly-name>.command::
The command to execute for `hook.<friendly-name>`. `<friendly-name>`
Expand Down
5 changes: 5 additions & 0 deletions Documentation/git.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,11 @@ on some performance improvements or features). This variable currently
only affects clones and fetches; it is not yet used for pushes (but may
be in the future).

`GIT_ALLOW_NO_VERIFY`::
If this Boolean environment variable is set to true (e.g. `1`), permits the use
of `--no-verify` (or `-n`) even when `hooks.allowNoVerify` is set to `false`.
This serves as an emergency override mechanism for workflows when hooks fail unexpectedly.

`GIT_OPTIONAL_LOCKS`::
If this Boolean environment variable is set to false, Git will complete any requested operation without
performing any optional sub-operations that require taking a lock.
Expand Down
3 changes: 3 additions & 0 deletions builtin/am.c
Original file line number Diff line number Diff line change
Expand Up @@ -2457,6 +2457,9 @@ int cmd_am(int argc,

argc = parse_options(argc, argv, prefix, options, usage, 0);

if (state.no_verify)
validate_no_verify(the_repository, "--no-verify");

if (binary >= 0)
fprintf_ln(stderr, _("The -b/--binary option has been a no-op for long time, and\n"
"it will be removed. Please do not use it anymore."));
Expand Down
4 changes: 4 additions & 0 deletions builtin/commit.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "environment.h"
#include "diff.h"
#include "commit.h"
#include "hook.h"
#include "add-interactive.h"
#include "gettext.h"
#include "revision.h"
Expand Down Expand Up @@ -1316,6 +1317,9 @@ static int parse_and_validate_options(int argc, const char *argv[],
argc = parse_options(argc, argv, prefix, options, usage, 0);
finalize_deferred_config(s);

if (no_verify)
validate_no_verify(the_repository, "--no-verify");

if (force_author && !strchr(force_author, '>'))
force_author = find_author_by_nickname(force_author);

Expand Down
2 changes: 2 additions & 0 deletions builtin/merge.c
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,8 @@ int cmd_merge(int argc,
parse_branch_merge_options(branch_mergeoptions);
argc = parse_options(argc, argv, prefix, builtin_merge_options,
builtin_merge_usage, 0);
if (no_verify)
validate_no_verify(the_repository, "--no-verify");
if (shortlog_len < 0)
shortlog_len = (merge_log_config > 0) ? merge_log_config : 0;

Expand Down
3 changes: 3 additions & 0 deletions builtin/push.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "environment.h"
#include "gettext.h"
#include "hex.h"
#include "hook.h"
#include "refspec.h"
#include "run-command.h"
#include "remote.h"
Expand Down Expand Up @@ -746,6 +747,8 @@ int cmd_push(int argc,
packet_trace_identity("push");
repo_config(the_repository, git_push_config, &flags);
argc = parse_options(argc, argv, prefix, options, push_usage, 0);
if (flags & TRANSPORT_PUSH_NO_HOOK)
validate_no_verify(the_repository, "--no-verify");
push_options = (push_options_cmdline.nr
? &push_options_cmdline
: &push_options_config);
Expand Down
3 changes: 3 additions & 0 deletions builtin/rebase.c
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,9 @@ int cmd_rebase(int argc,
builtin_rebase_options,
builtin_rebase_usage, 0);

if (ok_to_skip_pre_rebase)
validate_no_verify(the_repository, "--no-verify");

if (options.trailer_args.nr) {
if (validate_trailer_args(&options.trailer_args))
die(NULL);
Expand Down
30 changes: 30 additions & 0 deletions hook.c
Original file line number Diff line number Diff line change
Expand Up @@ -858,3 +858,33 @@ int run_hooks_l(struct repository *r, const char *hook_name, ...)

return run_hooks_opt(r, hook_name, &opt);
}

void validate_no_verify(struct repository *r, const char *opt)
{
const char *val = NULL;
int maybe_bool;

if (git_env_bool("GIT_ALLOW_NO_VERIFY", 0))
return;

if (!r || (repo_config_get_value(r, "hook.allownoverify", &val) &&
repo_config_get_value(r, "hooks.allownoverify", &val)))
return;

maybe_bool = git_parse_maybe_bool(val);
if (maybe_bool == 1) {
return;
} else if (val && !strcasecmp(val, "warn")) {
warning(_("bypassing hooks with '%s' is discouraged by 'hook.allowNoVerify'"), opt);
return;
} else if (maybe_bool == 0) {
advise(_("this repository disallows '%s' as a workflow guardrail against accidental bypass.\n"
"In an emergency (e.g. broken hook or urgent hotfix), you can override it with:\n"
" git -c hook.allowNoVerify=true <command>\n"
"or:\n"
" GIT_ALLOW_NO_VERIFY=1 git <command>"), opt);
die(_("the use of '%s' is disabled by 'hook.allowNoVerify'"), opt);
} else {
warning(_("unknown value for 'hook.allowNoVerify': '%s'"), val);
}
}
12 changes: 12 additions & 0 deletions hook.h
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,16 @@ int run_hooks(struct repository *r, const char *hook_name);
*/
LAST_ARG_MUST_BE_NULL
int run_hooks_l(struct repository *r, const char *hook_name, ...);

/**
* Check if the use of '--no-verify' (or '-n') is permitted according to
* the 'hooks.allowNoVerify' configuration and 'GIT_ALLOW_NO_VERIFY' environment
* variable.
*
* If permitted, this function returns normally (or emits a warning if configured
* to 'warn'). If disallowed, it outputs advice on how to override the workflow
* guardrail in an emergency, then aborts with die().
*/
void validate_no_verify(struct repository *r, const char *opt);

#endif
1 change: 1 addition & 0 deletions t/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,7 @@ integration_tests = [
't7526-commit-pathspec-file.sh',
't7527-builtin-fsmonitor.sh',
't7528-signed-commit-ssh.sh',
't7599-hooks-allownoverify.sh',
't7600-merge.sh',
't7601-merge-pull-config.sh',
't7602-merge-octopus-many.sh',
Expand Down
224 changes: 224 additions & 0 deletions t/t7599-hooks-allownoverify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
#!/bin/sh

test_description='support hook.allowNoVerify configuration to disallow --no-verify'

GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME

. ./test-lib.sh

test_expect_success 'setup test repository and hooks' '
test_commit init &&
test_hook --setup pre-commit <<-\HOOK_EOF &&
echo "pre-commit executed" >>pre-commit.log
if test -f fail-pre-commit
then
exit 1
fi
exit 0
HOOK_EOF
test_hook --setup pre-push <<-\HOOK_EOF &&
echo "pre-push executed" >>pre-push.log
if test -f fail-pre-push
then
exit 1
fi
exit 0
HOOK_EOF
git init --bare remote.git &&
git remote add origin remote.git &&
git push -u origin main &&
rm -f pre-commit.log pre-push.log
'

test_expect_success 'default: --no-verify is permitted for git commit' '
test_when_finished "rm -f pre-commit.log" &&
echo "change1" >>init.t &&
git add init.t &&
git commit --no-verify -m "commit with no-verify (default)" &&
test_path_is_missing pre-commit.log
'

test_expect_success 'default: -n is permitted for git commit' '
test_when_finished "rm -f pre-commit.log" &&
echo "change2" >>init.t &&
git add init.t &&
git commit -n -m "commit with -n (default)" &&
test_path_is_missing pre-commit.log
'

test_expect_success 'default: --no-verify is permitted for git push' '
test_when_finished "rm -f pre-push.log" &&
rm -f pre-push.log &&
git push --no-verify origin main &&
test_path_is_missing pre-push.log
'

test_expect_success 'explicit hook.allowNoVerify=true allows --no-verify' '
test_when_finished "rm -f pre-commit.log" &&
test_config hook.allowNoVerify true &&
echo "change3" >>init.t &&
git add init.t &&
git commit --no-verify -m "commit with no-verify allowed" &&
test_path_is_missing pre-commit.log
'

test_expect_success 'hook.allowNoVerify=false disallows git commit --no-verify' '
test_config hook.allowNoVerify false &&
echo "change4" >>init.t &&
git add init.t &&
test_must_fail git commit --no-verify -m "should fail" 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'hook.allowNoVerify=false disallows git commit -n' '
test_config hook.allowNoVerify false &&
echo "change5" >>init.t &&
git add init.t &&
test_must_fail git commit -n -m "should fail" 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'hook.allowNoVerify=false disallows git push --no-verify' '
test_config hook.allowNoVerify false &&
test_must_fail git push --no-verify origin main 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'hook.allowNoVerify=false disallows git merge --no-verify' '
test_config hook.allowNoVerify false &&
git checkout -b branch-merge main &&
echo "merge change" >merge_file &&
git add merge_file &&
git commit -m "merge commit" &&
git checkout main &&
test_must_fail git merge --no-verify branch-merge -m "merge fail" 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'hook.allowNoVerify=false disallows git rebase --no-verify' '
test_config hook.allowNoVerify false &&
test_must_fail git rebase --no-verify main branch-merge 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'hook.allowNoVerify=false disallows git am --no-verify' '
test_when_finished "rm -f patch && git am --abort || true" &&
test_config hook.allowNoVerify false &&
git format-patch -1 --stdout branch-merge >patch &&
test_must_fail git am --no-verify patch 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'hook.allowNoVerify=false still runs hooks when --no-verify is not used' '
test_when_finished "rm -f pre-commit.log" &&
test_config hook.allowNoVerify false &&
echo "change6" >>init.t &&
git add init.t &&
git commit -m "normal commit" &&
test_path_is_file pre-commit.log
'

test_expect_success 'hook.allowNoVerify=false enforces hook execution (hook failure prevents commit)' '
test_when_finished "rm -f fail-pre-commit pre-commit.log" &&
test_config hook.allowNoVerify false &&
touch fail-pre-commit &&
echo "change7" >>init.t &&
git add init.t &&
test_must_fail git commit -m "failing hook" &&
test_must_fail git commit --no-verify -m "cannot bypass" 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'hook.allowNoVerify=false still runs pre-push hook on git push' '
test_when_finished "rm -f pre-push.log" &&
test_config hook.allowNoVerify false &&
git push origin main &&
test_path_is_file pre-push.log
'

test_expect_success 'CLI -c hook.allowNoVerify=false overrides local true' '
test_config hook.allowNoVerify true &&
echo "change8" >>init.t &&
git add init.t &&
test_must_fail git -c hook.allowNoVerify=false commit --no-verify -m "override" 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'local hook.allowNoVerify=false overrides global true' '
test_config_global hook.allowNoVerify true &&
test_config hook.allowNoVerify false &&
echo "change9" >>init.t &&
git add init.t &&
test_must_fail git commit --no-verify -m "local override" 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'CLI -c hook.allowNoVerify=true overrides local false' '
test_config hook.allowNoVerify false &&
echo "change10" >>init.t &&
git add init.t &&
git -c hook.allowNoVerify=true commit --no-verify -m "override false with CLI true"
'

test_expect_success 'hook.allowNoVerify=false provides emergency override advice' '
test_config hook.allowNoVerify false &&
echo "change11" >>init.t &&
git add init.t &&
test_must_fail git commit --no-verify -m "fail advice" 2>err &&
test_grep "GIT_ALLOW_NO_VERIFY=1" err &&
test_grep "git -c hook.allowNoVerify=true" err
'

test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git commit --no-verify even when configured to false' '
test_when_finished "rm -f pre-commit.log" &&
test_config hook.allowNoVerify false &&
echo "change12" >>init.t &&
git add init.t &&
GIT_ALLOW_NO_VERIFY=1 git commit --no-verify -m "emergency commit" &&
test_path_is_missing pre-commit.log
'

test_expect_success 'GIT_ALLOW_NO_VERIFY=1 permits git push --no-verify even when configured to false' '
test_when_finished "rm -f pre-push.log" &&
test_config hook.allowNoVerify false &&
GIT_ALLOW_NO_VERIFY=1 git push --no-verify origin main &&
test_path_is_missing pre-push.log
'

test_expect_success 'hook.allowNoVerify=warn permits --no-verify and warns on stderr' '
test_when_finished "rm -f pre-commit.log err" &&
test_config hook.allowNoVerify warn &&
echo "change13" >>init.t &&
git add init.t &&
git commit --no-verify -m "commit with warn" 2>err &&
test_path_is_missing pre-commit.log &&
test_grep "bypassing hooks with .--no-verify. is discouraged" err
'

test_expect_success 'hook.allowNoVerify=0 disallows --no-verify' '
test_config hook.allowNoVerify 0 &&
echo "change14" >>init.t &&
git add init.t &&
test_must_fail git commit --no-verify -m "fail 0" 2>err &&
test_grep "hook.allowNoVerify" err
'

test_expect_success 'hook.allowNoVerify=1 allows --no-verify' '
test_when_finished "rm -f pre-commit.log" &&
test_config hook.allowNoVerify 1 &&
echo "change15" >>init.t &&
git add init.t &&
git commit --no-verify -m "commit 1" &&
test_path_is_missing pre-commit.log
'

test_expect_success 'legacy hooks.allowNoVerify (plural) is accepted as fallback' '
test_config hooks.allowNoVerify false &&
echo "change16" >>init.t &&
git add init.t &&
test_must_fail git commit --no-verify -m "fail fallback" 2>err &&
test_grep "hook.allowNoVerify" err
'

test_done
Loading