Skip to content

Commit 964bec5

Browse files
committed
add: support pre-add hook
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>
1 parent b2826b5 commit 964bec5

File tree

4 files changed

+157
-1
lines changed

4 files changed

+157
-1
lines changed

Documentation/git-add.adoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ SYNOPSIS
1010
[synopsis]
1111
git add [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]
1212
[--edit | -e] [--[no-]all | -A | --[no-]ignore-removal | [--update | -u]] [--sparse]
13-
[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]
13+
[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize] [--no-verify]
1414
[--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]
1515
[--] [<pathspec>...]
1616

@@ -42,6 +42,9 @@ use the `--force` option to add ignored files. If you specify the exact
4242
filename of an ignored file, `git add` will fail with a list of ignored
4343
files. Otherwise it will silently ignore the file.
4444

45+
A pre-add hook can be run to inspect or reject the add operation before
46+
it stages files. See linkgit:githooks[5] for details.
47+
4548
Please see linkgit:git-commit[1] for alternative ways to add content to a
4649
commit.
4750

@@ -163,6 +166,10 @@ for `git add --no-all <pathspec>...`, i.e. ignored removed files.
163166
Don't add the file(s), but only refresh their stat()
164167
information in the index.
165168
169+
`--no-verify`::
170+
Bypass the pre-add hook if it exists. See linkgit:githooks[5] for
171+
more information about hooks.
172+
166173
`--ignore-errors`::
167174
If some files could not be added because of errors indexing
168175
them, do not abort the operation, but continue adding the
@@ -451,6 +458,7 @@ linkgit:git-reset[1]
451458
linkgit:git-mv[1]
452459
linkgit:git-commit[1]
453460
linkgit:git-update-index[1]
461+
linkgit:githooks[5]
454462

455463
GIT
456464
---

Documentation/githooks.adoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,23 @@ and is invoked after the patch is applied and a commit is made.
9494
This hook is meant primarily for notification, and cannot affect
9595
the outcome of `git am`.
9696
97+
pre-add
98+
~~~~~~~
99+
100+
This hook is invoked by linkgit:git-add[1], and can be bypassed with the
101+
`--no-verify` option. This hook is not invoked for `--interactive`, `--patch`,
102+
`--edit`, or `--dry-run`. It takes no parameters, and is invoked after pathspec
103+
validation and before any files are staged. Exiting with a non-zero status
104+
from this script causes the `git add` command to abort without modifying the
105+
index.
106+
107+
This hook is invoked with the environment variable `GIT_INDEX_FILE`
108+
which points to the index file. This allows the hook to inspect what
109+
files would be staged before the operation proceeds.
110+
111+
This hook is not invoked by `git commit -a` or `git commit --include` which
112+
still can run the pre-commit hook, providing a control point at commit time.
113+
97114
pre-commit
98115
~~~~~~~~~~
99116

builtin/add.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "strvec.h"
2626
#include "submodule.h"
2727
#include "add-interactive.h"
28+
#include "hook.h"
2829

2930
static const char * const builtin_add_usage[] = {
3031
N_("git add [<options>] [--] <pathspec>..."),
@@ -36,6 +37,7 @@ static int take_worktree_changes;
3637
static int add_renormalize;
3738
static int pathspec_file_nul;
3839
static int include_sparse;
40+
static int no_verify;
3941
static const char *pathspec_from_file;
4042

4143
static int chmod_pathspec(struct repository *repo,
@@ -271,6 +273,7 @@ static struct option builtin_add_options[] = {
271273
OPT_BOOL( 0 , "refresh", &refresh_only, N_("don't add, only refresh the index")),
272274
OPT_BOOL( 0 , "ignore-errors", &ignore_add_errors, N_("just skip files which cannot be added because of errors")),
273275
OPT_BOOL( 0 , "ignore-missing", &ignore_missing, N_("check if - even missing - files are ignored in dry run")),
276+
OPT_BOOL( 0 , "no-verify", &no_verify, N_("bypass pre-add hook")),
274277
OPT_BOOL(0, "sparse", &include_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
275278
OPT_STRING(0, "chmod", &chmod_arg, "(+|-)x",
276279
N_("override the executable bit of the listed files")),
@@ -576,6 +579,17 @@ int cmd_add(int argc,
576579
string_list_clear(&only_match_skip_worktree, 0);
577580
}
578581

582+
if (!show_only && !no_verify) {
583+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
584+
585+
strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s",
586+
repo_get_index_file(repo));
587+
if (run_hooks_opt(repo, "pre-add", &opt)) {
588+
exit_status = 1;
589+
goto finish;
590+
}
591+
}
592+
579593
transaction = odb_transaction_begin(repo->objects);
580594

581595
ps_matched = xcalloc(pathspec.nr, 1);

t/t3706-pre-add-hook.sh

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/bin/sh
2+
3+
test_description='pre-add hook tests
4+
5+
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).'
6+
7+
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
8+
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
9+
10+
. ./test-lib.sh
11+
12+
test_expect_success 'with no hook' '
13+
test_when_finished "rm -f actual" &&
14+
echo content >file &&
15+
git add file &&
16+
test_path_is_missing actual
17+
'
18+
19+
test_expect_success POSIXPERM 'with non-executable hook' '
20+
test_when_finished "rm -f actual" &&
21+
test_hook pre-add <<-\EOF &&
22+
echo should-not-run >>actual
23+
exit 1
24+
EOF
25+
chmod -x .git/hooks/pre-add &&
26+
27+
echo content >file &&
28+
git add file &&
29+
test_path_is_missing actual
30+
'
31+
32+
test_expect_success '--no-verify with no hook' '
33+
echo content >file &&
34+
git add --no-verify file &&
35+
test_path_is_missing actual
36+
'
37+
38+
test_expect_success 'with succeeding hook' '
39+
test_when_finished "rm -f actual expected" &&
40+
echo "pre-add" >expected &&
41+
test_hook pre-add <<-\EOF &&
42+
echo pre-add >>actual
43+
EOF
44+
45+
echo content >file &&
46+
git add file &&
47+
test_cmp expected actual
48+
'
49+
50+
test_expect_success 'with failing hook' '
51+
test_when_finished "rm -f actual" &&
52+
test_hook pre-add <<-\EOF &&
53+
echo pre-add-rejected >>actual
54+
exit 1
55+
EOF
56+
57+
echo content >file &&
58+
test_must_fail git add file
59+
'
60+
61+
test_expect_success '--no-verify with failing hook' '
62+
test_when_finished "rm -f actual" &&
63+
test_hook pre-add <<-\EOF &&
64+
echo should-not-run >>actual
65+
exit 1
66+
EOF
67+
68+
echo content >file &&
69+
git add --no-verify file &&
70+
test_path_is_missing actual
71+
'
72+
73+
test_expect_success 'hook receives GIT_INDEX_FILE environment variable' '
74+
test_when_finished "rm -f actual expected" &&
75+
echo "hook-saw-env" >expected &&
76+
test_hook pre-add <<-\EOF &&
77+
if test -z "$GIT_INDEX_FILE"
78+
then
79+
echo hook-missing-env >>actual
80+
else
81+
echo hook-saw-env >>actual
82+
fi
83+
EOF
84+
85+
echo content >file &&
86+
git add file &&
87+
test_cmp expected actual
88+
'
89+
90+
test_expect_success 'with --dry-run (show-only) the hook is not invoked' '
91+
test_when_finished "rm -f actual" &&
92+
test_hook pre-add <<-\EOF &&
93+
echo should-not-run >>actual
94+
exit 1
95+
EOF
96+
97+
echo content >file &&
98+
git add --dry-run file &&
99+
test_path_is_missing actual
100+
'
101+
102+
test_expect_success 'hook is invoked with git add -u' '
103+
test_when_finished "rm -f actual expected file" &&
104+
echo "initial" >file &&
105+
git add file &&
106+
git commit -m "initial" &&
107+
echo "pre-add" >expected &&
108+
test_hook pre-add <<-\EOF &&
109+
echo pre-add >>actual
110+
EOF
111+
112+
echo modified >file &&
113+
git add -u &&
114+
test_cmp expected actual
115+
'
116+
117+
test_done

0 commit comments

Comments
 (0)