Skip to content

Commit 1024415

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 after staging is already done and objects may already be in the object database. Introduce a "pre-add" hook that runs after "git add" computes the new index state but before committing it to disk. The hook receives two arguments: $1 -- path to a temporary copy of the index before this "git add" $2 -- path to the lockfile containing the proposed index $1 on first add can be a non-existent path representing an empty index. Hook authors can inspect the computed result with ordinary tools: GIT_INDEX_FILE="$2" git diff --cached --name-only HEAD without needing to interpret pathspec or mode flags like "-u" or "--renormalize" -- the proposed index already reflects their effect. The implementation creates a temporary copy of the index via the tempfile API when find_hook("pre-add") reports a hook is present, then lets all staging proceed normally. At the finish label, write_locked_index() writes the proposed index to the lockfile without COMMIT_LOCK. If the hook approves, commit_lock_file() atomically replaces the index. If the hook rejects, rollback_lock_file() discards the lockfile and the original index is left unchanged. When no hook is installed, the existing write_locked_index(COMMIT_LOCK | SKIP_IF_UNCHANGED) path is still taken. 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. Register t3706-pre-add-hook.sh in t/meson.build to synchronize Meson and Makefile lists. Signed-off-by: Chandra Kethi-Reddy <[email protected]>
1 parent b2826b5 commit 1024415

File tree

5 files changed

+329
-5
lines changed

5 files changed

+329
-5
lines changed

Documentation/git-add.adoc

Lines changed: 10 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,10 @@ 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 proposed index update
46+
after `git add` computes staging and writes it to the index lockfile,
47+
but before writing it to the final index. See linkgit:githooks[5].
48+
4549
Please see linkgit:git-commit[1] for alternative ways to add content to a
4650
commit.
4751

@@ -163,6 +167,10 @@ for `git add --no-all <pathspec>...`, i.e. ignored removed files.
163167
Don't add the file(s), but only refresh their stat()
164168
information in the index.
165169
170+
`--no-verify`::
171+
Bypass the pre-add hook if it exists. See linkgit:githooks[5] for
172+
more information about hooks.
173+
166174
`--ignore-errors`::
167175
If some files could not be added because of errors indexing
168176
them, do not abort the operation, but continue adding the
@@ -451,6 +459,7 @@ linkgit:git-reset[1]
451459
linkgit:git-mv[1]
452460
linkgit:git-commit[1]
453461
linkgit:git-update-index[1]
462+
linkgit:githooks[5]
454463

455464
GIT
456465
---

Documentation/githooks.adoc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,33 @@ 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 parameters: the path to a copy of the index before this
105+
invocation of `git add`, and the path to the lockfile containing the
106+
proposed index after staging. It does not read from standard input.
107+
If no index exists yet, the first parameter names a path that does not
108+
exist and should be treated as an empty index. No special environment
109+
variables are set. The hook is invoked after the index has been updated
110+
in memory and written to the lockfile, but before it is committed to the
111+
final location.
112+
113+
Exiting with a non-zero status causes `git add` to abort and leaves the
114+
index unchanged. Exiting with zero status causes the staged changes to
115+
take effect.
116+
117+
This hook can be used to prevent staging of files based on names, content,
118+
or sizes (e.g., to block `.env` files, secret keys, or large files).
119+
120+
This hook is not invoked by `git commit -a` or `git commit --include`
121+
which still can run the pre-commit hook, providing a control point at
122+
commit time.
123+
97124
pre-commit
98125
~~~~~~~~~~
99126

builtin/add.c

Lines changed: 64 additions & 4 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 "copy.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,9 @@ 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+
struct tempfile *orig_index = NULL;
400+
char *orig_index_path = NULL;
394401

395402
repo_config(repo, add_config, NULL);
396403

@@ -576,6 +583,34 @@ int cmd_add(int argc,
576583
string_list_clear(&only_match_skip_worktree, 0);
577584
}
578585

586+
if (!show_only && !no_verify && find_hook(repo, "pre-add")) {
587+
int fd_in, status;
588+
const char *index_file = repo_get_index_file(repo);
589+
char *template;
590+
591+
run_pre_add = 1;
592+
template = xstrfmt("%s.pre-add.XXXXXX", index_file);
593+
orig_index = xmks_tempfile(template);
594+
free(template);
595+
596+
fd_in = open(index_file, O_RDONLY);
597+
if (fd_in >= 0) {
598+
status = copy_fd(fd_in, get_tempfile_fd(orig_index));
599+
if (close(fd_in))
600+
die_errno(_("unable to close index for pre-add hook"));
601+
if (close_tempfile_gently(orig_index))
602+
die_errno(_("unable to close temporary index copy"));
603+
if (status < 0)
604+
die(_("failed to copy index for pre-add hook"));
605+
} else if (errno == ENOENT) {
606+
orig_index_path = xstrdup(get_tempfile_path(orig_index));
607+
if (delete_tempfile(&orig_index))
608+
die_errno(_("unable to remove temporary index copy"));
609+
} else {
610+
die_errno(_("unable to open index for pre-add hook"));
611+
}
612+
}
613+
579614
transaction = odb_transaction_begin(repo->objects);
580615

581616
ps_matched = xcalloc(pathspec.nr, 1);
@@ -587,8 +622,12 @@ int cmd_add(int argc,
587622
include_sparse, flags);
588623

589624
if (take_worktree_changes && !add_renormalize && !ignore_add_errors &&
590-
report_path_error(ps_matched, &pathspec))
625+
report_path_error(ps_matched, &pathspec)) {
626+
if (orig_index)
627+
delete_tempfile(&orig_index);
628+
free(orig_index_path);
591629
exit(128);
630+
}
592631

593632
if (add_new_files)
594633
exit_status |= add_files(repo, &dir, flags);
@@ -598,9 +637,30 @@ int cmd_add(int argc,
598637
odb_transaction_commit(transaction);
599638

600639
finish:
601-
if (write_locked_index(repo->index, &lock_file,
602-
COMMIT_LOCK | SKIP_IF_UNCHANGED))
603-
die(_("unable to write new index file"));
640+
if (run_pre_add && !exit_status && repo->index->cache_changed) {
641+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
642+
643+
if (write_locked_index(repo->index, &lock_file, 0))
644+
die(_("unable to write new index file"));
645+
646+
strvec_push(&opt.args, orig_index ? get_tempfile_path(orig_index) :
647+
orig_index_path);
648+
strvec_push(&opt.args, get_lock_file_path(&lock_file));
649+
if (run_hooks_opt(repo, "pre-add", &opt)) {
650+
rollback_lock_file(&lock_file); /* hook rejected */
651+
exit_status = 1;
652+
} else {
653+
if (commit_lock_file(&lock_file)) /* hook approved */
654+
die(_("unable to write new index file"));
655+
}
656+
} else {
657+
if (write_locked_index(repo->index, &lock_file,
658+
COMMIT_LOCK | SKIP_IF_UNCHANGED))
659+
die(_("unable to write new index file"));
660+
}
661+
662+
delete_tempfile(&orig_index);
663+
free(orig_index_path);
604664

605665
free(ps_matched);
606666
dir_clear(&dir);

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ integration_tests = [
412412
't3703-add-magic-pathspec.sh',
413413
't3704-add-pathspec-file.sh',
414414
't3705-add-sparse-checkout.sh',
415+
't3706-pre-add-hook.sh',
415416
't3800-mktag.sh',
416417
't3900-i18n-commit.sh',
417418
't3901-i18n-patch.sh',

0 commit comments

Comments
 (0)