All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Chandra Kethi-Reddy via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Chandra Kethi-Reddy <chandrakr@pm.me>,
	Chandra Kethi-Reddy <chandrakr@pm.me>
Subject: [PATCH] add: support pre-add hook
Date: Tue, 10 Feb 2026 15:32:53 +0000	[thread overview]
Message-ID: <pull.2045.git.1770737573475.gitgitgadget@gmail.com> (raw)

From: Chandra Kethi-Reddy <chandrakr@pm.me>

git has no hook that fires during 'git add'.  Users who want to
validate files before staging must wrap 'git add' in a shell alias
or wait for pre-commit, which fires after staging is already done.

Add a pre-add hook that runs after pathspec validation and before
any files are staged.  If the hook exits non-zero, 'git add' aborts
without modifying the index.  The hook receives GIT_INDEX_FILE in
its environment, following the same convention as pre-commit.

The hook is bypassed with '--no-verify' (long flag only, since '-n'
is already '--dry-run' in 'git add').  It is not invoked for
--interactive, --patch, --edit, or --dry-run modes, nor by
'git commit -a' which stages files through its own code path in
builtin/commit.c.

The implementation calls run_hooks_opt() directly rather than the
run_commit_hook() wrapper, which sets GIT_EDITOR=: and is not
relevant for 'git add'.  When no hook is installed, there is no
performance impact.

Disclosure: developed with guidance from Claude Code (Anthropic)
and Codex CLI (OpenAI) for development, review and standards
compliance. The contributor handtyped and reviewed all tests, code,
and documentation.

Signed-off-by: Chandra Kethi-Reddy <chandrakr@pm.me>
---
    add: support pre-add hook

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2045%2Fshatachandra%2Fpre-add-hooks-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2045/shatachandra/pre-add-hooks-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2045

 Documentation/git-add.adoc  |  10 ++-
 Documentation/githooks.adoc |  17 ++++++
 builtin/add.c               |  14 +++++
 t/t3706-pre-add-hook.sh     | 117 ++++++++++++++++++++++++++++++++++++
 4 files changed, 157 insertions(+), 1 deletion(-)
 create mode 100644 t/t3706-pre-add-hook.sh

diff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc
index 6192daeb03..c60e0c65a5 100644
--- a/Documentation/git-add.adoc
+++ b/Documentation/git-add.adoc
@@ -10,7 +10,7 @@ SYNOPSIS
 [synopsis]
 git add [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]
 	[--edit | -e] [--[no-]all | -A | --[no-]ignore-removal | [--update | -u]] [--sparse]
-	[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]
+	[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] [--no-verify]
 	[--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]
 	[--] [<pathspec>...]
 
@@ -42,6 +42,9 @@ use the `--force` option to add ignored files. If you specify the exact
 filename of an ignored file, `git add` will fail with a list of ignored
 files. Otherwise it will silently ignore the file.
 
+A pre-add hook can be run to inspect or reject the add operation before
+it stages files. See linkgit:githooks[5] for details.
+
 Please see linkgit:git-commit[1] for alternative ways to add content to a
 commit.
 
@@ -163,6 +166,10 @@ for `git add --no-all <pathspec>...`, i.e. ignored removed files.
 	Don't add the file(s), but only refresh their stat()
 	information in the index.
 
+`--no-verify`::
+	Bypass the pre-add hook if it exists. See linkgit:githooks[5] for
+	more information about hooks.
+
 `--ignore-errors`::
 	If some files could not be added because of errors indexing
 	them, do not abort the operation, but continue adding the
@@ -451,6 +458,7 @@ linkgit:git-reset[1]
 linkgit:git-mv[1]
 linkgit:git-commit[1]
 linkgit:git-update-index[1]
+linkgit:githooks[5]
 
 GIT
 ---
diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index 056553788d..51156822bc 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -94,6 +94,23 @@ and is invoked after the patch is applied and a commit is made.
 This hook is meant primarily for notification, and cannot affect
 the outcome of `git am`.
 
+pre-add
+~~~~~~~
+
+This hook is invoked by linkgit:git-add[1], and can be bypassed with the
+`--no-verify` option. This hook is not invoked for `--interactive`, `--patch`,
+`--edit`, or `--dry-run`. It takes no parameters, and is invoked after pathspec
+validation and before any files are staged. Exiting with a non-zero status
+from this script causes the `git add` command to abort without modifying the
+index.
+
+This hook is invoked with the environment variable `GIT_INDEX_FILE`
+which points to the index file. This allows the hook to inspect what
+files would be staged before the operation proceeds.
+
+This hook is not invoked by `git commit -a` or `git commit --include` which
+still can run the pre-commit hook, providing a control point at commit time.
+
 pre-commit
 ~~~~~~~~~~
 
diff --git a/builtin/add.c b/builtin/add.c
index 32709794b3..7747b41d10 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -25,6 +25,7 @@
 #include "strvec.h"
 #include "submodule.h"
 #include "add-interactive.h"
+#include "hook.h"
 
 static const char * const builtin_add_usage[] = {
 	N_("git add [<options>] [--] <pathspec>..."),
@@ -36,6 +37,7 @@ static int take_worktree_changes;
 static int add_renormalize;
 static int pathspec_file_nul;
 static int include_sparse;
+static int no_verify;
 static const char *pathspec_from_file;
 
 static int chmod_pathspec(struct repository *repo,
@@ -271,6 +273,7 @@ static struct option builtin_add_options[] = {
 	OPT_BOOL( 0 , "refresh", &refresh_only, N_("don't add, only refresh the index")),
 	OPT_BOOL( 0 , "ignore-errors", &ignore_add_errors, N_("just skip files which cannot be added because of errors")),
 	OPT_BOOL( 0 , "ignore-missing", &ignore_missing, N_("check if - even missing - files are ignored in dry run")),
+	OPT_BOOL( 0 , "no-verify", &no_verify, N_("bypass pre-add hook")),
 	OPT_BOOL(0, "sparse", &include_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
 	OPT_STRING(0, "chmod", &chmod_arg, "(+|-)x",
 		   N_("override the executable bit of the listed files")),
@@ -576,6 +579,17 @@ int cmd_add(int argc,
 		string_list_clear(&only_match_skip_worktree, 0);
 	}
 
+	if (!show_only && !no_verify) {
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+		strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s",
+			     repo_get_index_file(repo));
+		if (run_hooks_opt(repo, "pre-add", &opt)) {
+			exit_status = 1;
+			goto finish;
+		}
+	}
+
 	transaction = odb_transaction_begin(repo->objects);
 
 	ps_matched = xcalloc(pathspec.nr, 1);
diff --git a/t/t3706-pre-add-hook.sh b/t/t3706-pre-add-hook.sh
new file mode 100644
index 0000000000..e64ee51b25
--- /dev/null
+++ b/t/t3706-pre-add-hook.sh
@@ -0,0 +1,117 @@
+#!/bin/sh
+
+test_description='pre-add hook tests
+
+These tests run git add with and without pre-add hooks to ensure functionality. Largely derived from t7503 (pre-commit and pre-merge-commit hooks) and t5571 (pre-push hooks).'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
+. ./test-lib.sh
+
+test_expect_success 'with no hook' '
+	test_when_finished "rm -f actual" &&
+	echo content >file &&
+	git add file &&
+	test_path_is_missing actual
+'
+
+test_expect_success POSIXPERM 'with non-executable hook' '
+	test_when_finished "rm -f actual" &&
+	test_hook pre-add <<-\EOF &&
+	echo should-not-run >>actual
+	exit 1
+	EOF
+	chmod -x .git/hooks/pre-add &&
+
+	echo content >file &&
+	git add file &&
+	test_path_is_missing actual
+'
+
+test_expect_success '--no-verify with no hook' '
+	echo content >file &&
+	git add --no-verify file &&
+	test_path_is_missing actual
+'
+
+test_expect_success 'with succeeding hook' '
+	test_when_finished "rm -f actual expected" &&
+	echo "pre-add" >expected &&
+	test_hook pre-add <<-\EOF &&
+	echo pre-add >>actual
+	EOF
+
+	echo content >file &&
+	git add file &&
+	test_cmp expected actual
+'
+
+test_expect_success 'with failing hook' '
+	test_when_finished "rm -f actual" &&
+	test_hook pre-add <<-\EOF &&
+	echo pre-add-rejected >>actual
+	exit 1
+	EOF
+
+	echo content >file &&
+	test_must_fail git add file
+'
+
+test_expect_success '--no-verify with failing hook' '
+	test_when_finished "rm -f actual" &&
+	test_hook pre-add <<-\EOF &&
+	echo should-not-run >>actual
+	exit 1
+	EOF
+
+	echo content >file &&
+	git add --no-verify file &&
+	test_path_is_missing actual
+'
+
+test_expect_success 'hook receives GIT_INDEX_FILE environment variable' '
+	test_when_finished "rm -f actual expected" &&
+	echo "hook-saw-env" >expected &&
+	test_hook pre-add <<-\EOF &&
+	if test -z "$GIT_INDEX_FILE"
+	then
+		echo hook-missing-env >>actual
+	else
+		echo hook-saw-env >>actual
+	fi
+	EOF
+
+	echo content >file &&
+	git add file &&
+	test_cmp expected actual
+'
+
+test_expect_success 'with --dry-run (show-only) the hook is not invoked' '
+	test_when_finished "rm -f actual" &&
+	test_hook pre-add <<-\EOF &&
+	echo should-not-run >>actual
+	exit 1
+	EOF
+
+	echo content >file &&
+	git add --dry-run file &&
+	test_path_is_missing actual
+'
+
+test_expect_success 'hook is invoked with git add -u' '
+	test_when_finished "rm -f actual expected file" &&
+	echo "initial" >file &&
+	git add file &&
+	git commit -m "initial" &&
+	echo "pre-add" >expected &&
+	test_hook pre-add <<-\EOF &&
+	echo pre-add >>actual
+	EOF
+
+	echo modified >file &&
+	git add -u &&
+	test_cmp expected actual
+'
+
+test_done

base-commit: b2826b52eb7caff9f4ed6e85ec45e338bf02ad09
-- 
gitgitgadget

             reply	other threads:[~2026-02-10 15:32 UTC|newest]

Thread overview: 26+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-02-10 15:32 Chandra Kethi-Reddy via GitGitGadget [this message]
2026-02-10 18:16 ` [PATCH] add: support pre-add hook Junio C Hamano
2026-02-10 19:00   ` Junio C Hamano
2026-02-11 15:16     ` [PATCH] " Chandra
2026-02-11 15:05 ` [PATCH v2] " Chandra Kethi-Reddy via GitGitGadget
2026-02-11 19:50   ` Junio C Hamano
2026-02-11 21:11     ` Chandra
2026-02-11 21:24       ` Junio C Hamano
2026-02-11 21:54         ` Chandra
2026-02-25  2:15           ` [PATCH v3] " Chandra
2026-02-27  5:54   ` Chandra Kethi-Reddy via GitGitGadget
2026-03-03 23:06     ` Junio C Hamano
2026-03-04  9:49       ` Ben Knoble
2026-03-05 10:47     ` Phillip Wood
2026-03-05 11:40       ` [PATCH v4] " Chandra
2026-03-05 14:48       ` [PATCH v3] " Junio C Hamano
2026-03-05 11:36     ` [PATCH v4] " Chandra Kethi-Reddy via GitGitGadget
2026-03-05 12:03       ` Adrian Ratiu
2026-03-05 12:37         ` Chandra
2026-03-05 12:37       ` [PATCH v5] " Chandra Kethi-Reddy via GitGitGadget
2026-03-05 13:41         ` Adrian Ratiu
2026-03-05 13:46           ` Chandra
2026-03-05 19:23           ` Junio C Hamano
2026-03-06  2:20             ` Chandra
2026-03-13 14:39               ` Phillip Wood
2026-03-05 14:37         ` Phillip Wood

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=pull.2045.git.1770737573475.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=chandrakr@pm.me \
    --cc=git@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.