Skip to content

Commit fc58c4c

Browse files
committed
add: support pre-add hook
"git add" has no hook that lets users inspect what is about to be staged. Users who want to reject certain paths or content must wrap the command in a shell alias or wait for pre-commit, which fires too late to prevent staging. Introduce a "pre-add" hook so that users can inspect or reject proposed index updates at staging time. $1 -- index path used by this invocation (may not exist yet) $2 -- lockfile path containing proposed staged index state Hook authors can inspect the result with ordinary Git commands: GIT_INDEX_FILE="$2" git diff --cached --name-only HEAD Both files should be treated as read-only. Exiting with non-zero status rejects the update and leaves the index unchanged. The hook accepts or rejects the entire proposed update. Per-path filtering is not supported. The hook is bypassed with "--no-verify" and is not invoked for --interactive, --patch, --edit, or --dry-run, nor by "git commit -a" which stages through its own code path. Signed-off-by: Chandra Kethi-Reddy <chandrakr@pm.me>
1 parent 7c02d39 commit fc58c4c

7 files changed

Lines changed: 388 additions & 7 deletions

File tree

Documentation/git-add.adoc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ 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]
1313
[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]
14-
[--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]
14+
[--no-verify] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]
1515
[--] [<pathspec>...]
1616

1717
DESCRIPTION
@@ -42,10 +42,11 @@ 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 used to reject `git add` (see linkgit:githooks[5]).
46+
4547
Please see linkgit:git-commit[1] for alternative ways to add content to a
4648
commit.
4749

48-
4950
OPTIONS
5051
-------
5152
`<pathspec>...`::
@@ -163,6 +164,10 @@ for `git add --no-all <pathspec>...`, i.e. ignored removed files.
163164
Don't add the file(s), but only refresh their stat()
164165
information in the index.
165166
167+
`--no-verify`::
168+
Bypass the `pre-add` hook if it exists. See linkgit:githooks[5] for
169+
more information about hooks.
170+
166171
`--ignore-errors`::
167172
If some files could not be added because of errors indexing
168173
them, do not abort the operation, but continue adding the
@@ -451,6 +456,7 @@ linkgit:git-reset[1]
451456
linkgit:git-mv[1]
452457
linkgit:git-commit[1]
453458
linkgit:git-update-index[1]
459+
linkgit:githooks[5]
454460

455461
GIT
456462
---

Documentation/githooks.adoc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,36 @@ 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. It is not invoked for `--interactive`, `--patch`,
102+
`--edit`, or `--dry-run`.
103+
104+
It takes two arguments: the path to the index file for this invocation
105+
of `git add`, and the path to the lockfile containing the proposed
106+
index after staging. If no index exists yet, the first argument names
107+
a path that does not exist and should be treated as an empty index.
108+
109+
The hook is invoked after the index has been updated in memory and
110+
written to the lockfile, but before it is committed to the final index
111+
path. Exiting with a non-zero status causes `git add` to reject the
112+
proposed state, roll back the lockfile, and leave the index unchanged.
113+
Exiting with zero status allows the index update to be committed. The
114+
hook accepts or rejects the entire proposed update; per-path filtering
115+
is not supported. Both files should be treated as read-only by the hook.
116+
117+
Hook authors may set `GIT_INDEX_FILE="$1"` to inspect the current index
118+
state and `GIT_INDEX_FILE="$2"` to inspect the proposed index state.
119+
120+
This hook can be used to prevent staging of files based on names, content,
121+
or sizes (e.g., to block `.env` files, secret keys, or large files).
122+
123+
This hook is not invoked by `git commit -a` or `git commit --include`
124+
which still can run the `pre-commit` hook, providing a control point at
125+
commit time.
126+
97127
pre-commit
98128
~~~~~~~~~~
99129

builtin/add.c

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
#include "strvec.h"
2626
#include "submodule.h"
2727
#include "add-interactive.h"
28+
#include "hook.h"
29+
#include "abspath.h"
2830

2931
static const char * const builtin_add_usage[] = {
3032
N_("git add [<options>] [--] <pathspec>..."),
@@ -36,6 +38,7 @@ static int take_worktree_changes;
3638
static int add_renormalize;
3739
static int pathspec_file_nul;
3840
static int include_sparse;
41+
static int no_verify;
3942
static const char *pathspec_from_file;
4043

4144
static int chmod_pathspec(struct repository *repo,
@@ -271,6 +274,7 @@ static struct option builtin_add_options[] = {
271274
OPT_BOOL( 0 , "refresh", &refresh_only, N_("don't add, only refresh the index")),
272275
OPT_BOOL( 0 , "ignore-errors", &ignore_add_errors, N_("just skip files which cannot be added because of errors")),
273276
OPT_BOOL( 0 , "ignore-missing", &ignore_missing, N_("check if - even missing - files are ignored in dry run")),
277+
OPT_BOOL( 0 , "no-verify", &no_verify, N_("bypass pre-add hook")),
274278
OPT_BOOL(0, "sparse", &include_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
275279
OPT_STRING(0, "chmod", &chmod_arg, "(+|-)x",
276280
N_("override the executable bit of the listed files")),
@@ -391,6 +395,8 @@ int cmd_add(int argc,
391395
char *ps_matched = NULL;
392396
struct lock_file lock_file = LOCK_INIT;
393397
struct odb_transaction *transaction;
398+
int run_pre_add = 0;
399+
char *orig_index_path = NULL;
394400

395401
repo_config(repo, add_config, NULL);
396402

@@ -576,6 +582,11 @@ int cmd_add(int argc,
576582
string_list_clear(&only_match_skip_worktree, 0);
577583
}
578584

585+
if (!show_only && !no_verify && hook_exists(repo, "pre-add")) {
586+
run_pre_add = 1;
587+
orig_index_path = absolute_pathdup(repo_get_index_file(repo));
588+
}
589+
579590
transaction = odb_transaction_begin(repo->objects);
580591

581592
ps_matched = xcalloc(pathspec.nr, 1);
@@ -598,9 +609,30 @@ int cmd_add(int argc,
598609
odb_transaction_commit(transaction);
599610

600611
finish:
601-
if (write_locked_index(repo->index, &lock_file,
602-
COMMIT_LOCK | SKIP_IF_UNCHANGED))
603-
die(_("unable to write new index file"));
612+
if (run_pre_add && repo->index->cache_changed) {
613+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
614+
615+
if (write_locked_index(repo->index, &lock_file,
616+
SKIP_INDEX_CHANGE_HOOK))
617+
die(_("unable to write proposed index"));
618+
619+
strvec_push(&opt.args, orig_index_path);
620+
strvec_push(&opt.args, get_lock_file_path(&lock_file));
621+
if (run_hooks_opt(repo, "pre-add", &opt)) {
622+
rollback_lock_file(&lock_file); /* hook rejected */
623+
exit_status = 1;
624+
} else if (commit_lock_file(&lock_file)) {
625+
die(_("unable to write new index file"));
626+
} else {
627+
emit_post_index_change(repo->index);
628+
}
629+
} else {
630+
if (write_locked_index(repo->index, &lock_file,
631+
COMMIT_LOCK | SKIP_IF_UNCHANGED))
632+
die(_("unable to write new index file"));
633+
}
634+
635+
free(orig_index_path);
604636

605637
free(ps_matched);
606638
dir_clear(&dir);

read-cache-ll.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ int is_index_unborn(struct index_state *);
284284
/* For use with `write_locked_index()`. */
285285
#define COMMIT_LOCK (1 << 0)
286286
#define SKIP_IF_UNCHANGED (1 << 1)
287+
#define SKIP_INDEX_CHANGE_HOOK (1 << 2)
288+
289+
void emit_post_index_change(struct index_state *istate);
287290

288291
/*
289292
* Write the index while holding an already-taken lock. Close the lock,

read-cache.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3161,13 +3161,18 @@ static int do_write_locked_index(struct index_state *istate,
31613161
else
31623162
ret = close_lock_file_gently(lock);
31633163

3164+
if (!(flags & SKIP_INDEX_CHANGE_HOOK))
3165+
emit_post_index_change(istate);
3166+
return ret;
3167+
}
3168+
3169+
void emit_post_index_change(struct index_state *istate)
3170+
{
31643171
run_hooks_l(the_repository, "post-index-change",
31653172
istate->updated_workdir ? "1" : "0",
31663173
istate->updated_skipworktree ? "1" : "0", NULL);
31673174
istate->updated_workdir = 0;
31683175
istate->updated_skipworktree = 0;
3169-
3170-
return ret;
31713176
}
31723177

31733178
static int write_split_index(struct index_state *istate,

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ integration_tests = [
415415
't3703-add-magic-pathspec.sh',
416416
't3704-add-pathspec-file.sh',
417417
't3705-add-sparse-checkout.sh',
418+
't3706-pre-add-hook.sh',
418419
't3800-mktag.sh',
419420
't3900-i18n-commit.sh',
420421
't3901-i18n-patch.sh',

0 commit comments

Comments
 (0)