threads / patch / 56717

v2, 13 partshook.[ch]: new library to run hooks + simple hook conversion

Subject: [PATCH v2 00/13] hook.[ch]: new library to run hooks + simple hook conversion

## tl;dr

115 messages between Oct 15, 2021 and Jan 7, 2022. Diffs are folded; open one to read it.

replies: 114people: 5as markdown or json

Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · lore

Part 2 of the greater configurable hook saga, starting by converting some existing simple hooks to the new hook.[ch] library and "git hook run" utility.

See the v1 [1] CL for more context. I've dropped the git-p4 folks from the CC list, your feedback on git-p4 would still be very appreciated, but I don't think I need to re-spam you for every re-roll of this.

This v2:
 * Should address all outstanding comments on v1, which were mainly of
   the form that some boilerplate here didn't reflect the actual state
   of the code.
   I've updated the docs, code etc. to 1=1 map to the existing
   behavior. I said I'd punt on e.g. the one-member "struct hook" in
   reply to René Scharfe, but when I tried it and rebased the rest on
   it it actually made the progression more understandable, so I've
   done that here.
   I didn't convert some things, which are noted in the updated commit
   message on v1, e.g. we still use the parallel run-command.c API
   with jobs=1, and have a function called run_hooks() that only ever
   runs one hook.
1. https://lore.kernel.org/git/cover-00.13-00000000000-20211012T131934Z-avarab@gmail.com/
Emily Shaffer (12):
  hook: add 'run' subcommand
  gc: use hook library for pre-auto-gc hook
  rebase: convert pre-rebase to use hook.h
  am: convert applypatch to use hook.h
  hooks: convert 'post-checkout' hook to hook library
  merge: convert post-merge to use hook.h
  send-email: use 'git hook run' for 'sendemail-validate'
  git-p4: use 'git hook' to run hooks
  commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
  read-cache: convert post-index-change to use hook.h
  receive-pack: convert push-to-checkout hook to hook.h
  run-command: remove old run_hook_{le,ve}() hook API
Ævar Arnfjörð Bjarmason (1):
  git hook run: add an --ignore-missing flag
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  45 +++++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/am.c               |   8 ++-
 builtin/checkout.c         |  14 ++--
 builtin/clone.c            |   7 +-
 builtin/gc.c               |   3 +-
 builtin/hook.c             |  90 +++++++++++++++++++++++++
 builtin/merge.c            |   4 +-
 builtin/rebase.c           |   8 ++-
 builtin/receive-pack.c     |   7 +-
 builtin/worktree.c         |  28 ++++----
 command-list.txt           |   1 +
 commit.c                   |  15 +++--
 git-p4.py                  |  72 ++------------------
 git-send-email.perl        |  22 +++---
 git.c                      |   1 +
 hook.c                     | 121 +++++++++++++++++++++++++++++++++
 hook.h                     |  56 ++++++++++++++++
 read-cache.c               |  11 ++-
 reset.c                    |  14 ++--
 run-command.c              |  32 ---------
 run-command.h              |  17 -----
 t/t1800-hook.sh            | 134 +++++++++++++++++++++++++++++++++++++
 t/t9001-send-email.sh      |   4 +-
 27 files changed, 551 insertions(+), 170 deletions(-)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Range-diff against v1:
 1:  a39c0748d3f !  1:  ba64faf0580 hook: add 'run' subcommand
    @@ Commit message
         pattern here mirrors that of
         builtin/{commit-graph,multi-pack-index}.c.
     
    +    Some of the implementation here, such as a function being named
    +    run_hooks() when it's tasked with running one hook, to using the
    +    run_processes_parallel_tr2() API to run with jobs=1 is somewhere
    +    between a bit odd and and an overkill for the current features of this
    +    "hook run" command and the hook.[ch] API.
    +
    +    This code will eventually be able to run multiple hooks declared in
    +    config in parallel, by starting out with these names and APIs we
    +    reduce the later churn of renaming functions, switching from the
    +    run_command() to run_processes_parallel_tr2() API etc.
    +
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
    @@ Documentation/git-hook.txt (new)
     +
     +NAME
     +----
    -+git-hook - run git hooks
    ++git-hook - Run git hooks
     +
     +SYNOPSIS
     +--------
    @@ Documentation/git-hook.txt (new)
     +DESCRIPTION
     +-----------
     +
    -+This command is an interface to git hooks (see linkgit:githooks[5]).
    -+Currently it only provides a convenience wrapper for running hooks for
    -+use by git itself. In the future it might gain other functionality.
    ++A command interface to running git hooks (see linkgit:githooks[5]),
    ++for use by other scripted git commands.
     +
     +SUBCOMMANDS
     +-----------
     +
     +run::
     +	Run the `<hook-name>` hook. See linkgit:githooks[5] for
    -+	the hook names we support.
    ++	supported hook names.
     ++
    -+Any positional arguments to the hook should be passed after an
    -+optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The
    -+arguments (if any) differ by hook name, see linkgit:githooks[5] for
    -+what those are.
    ++
    ++Any positional arguments to the hook should be passed after a
    ++mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
    ++linkgit:githooks[5] for arguments hooks might expect (if any).
     +
     +SEE ALSO
     +--------
    @@ builtin/hook.c (new)
     +	/* Need to take into account core.hooksPath */
     +	git_config(git_default_config, NULL);
     +
    -+	/*
    -+	 * We are not using a plain run_hooks() because we'd like to
    -+	 * detect missing hooks. Let's find it ourselves and call
    -+	 * run_hooks() instead.
    -+	 */
     +	hook_name = argv[0];
     +	hook_path = find_hook(hook_name);
     +	if (!hook_path) {
    @@ command-list.txt: git-grep                                mainporcelain
      git-gui                                 mainporcelain
      git-hash-object                         plumbingmanipulators
      git-help                                ancillaryinterrogators          complete
    -+git-hook                                mainporcelain
    ++git-hook                                purehelpers
      git-http-backend                        synchingrepositories
      git-http-fetch                          synchelpers
      git-http-push                           synchelpers
    @@ hook.c: int hook_exists(const char *name)
     +			  void **pp_task_cb)
     +{
     +	struct hook_cb_data *hook_cb = pp_cb;
    -+	struct hook *run_me = hook_cb->run_me;
    ++	const char *hook_path = hook_cb->hook_path;
     +
    -+	if (!run_me)
    ++	if (!hook_path)
     +		return 0;
     +
     +	cp->no_stdin = 1;
    @@ hook.c: int hook_exists(const char *name)
     +	cp->stdout_to_stderr = 1;
     +	cp->trace2_hook_name = hook_cb->hook_name;
     +
    -+	/* add command */
    -+	strvec_push(&cp->args, run_me->hook_path);
    -+
    -+	/*
    -+	 * add passed-in argv, without expanding - let the user get back
    -+	 * exactly what they put in
    -+	 */
    ++	strvec_push(&cp->args, hook_path);
     +	strvec_pushv(&cp->args, hook_cb->options->args.v);
     +
     +	/* Provide context for errors if necessary */
    -+	*pp_task_cb = run_me;
    ++	*pp_task_cb = (char *)hook_path;
     +
     +	/*
     +	 * This pick_next_hook() will be called again, we're only
     +	 * running one hook, so indicate that no more work will be
     +	 * done.
     +	 */
    -+	hook_cb->run_me = NULL;
    ++	hook_cb->hook_path = NULL;
     +
     +	return 1;
     +}
    @@ hook.c: int hook_exists(const char *name)
     +				void *pp_task_cp)
     +{
     +	struct hook_cb_data *hook_cb = pp_cb;
    -+	struct hook *attempted = pp_task_cp;
    ++	const char *hook_path = pp_task_cp;
     +
     +	hook_cb->rc |= 1;
     +
     +	strbuf_addf(out, _("Couldn't start hook '%s'\n"),
    -+		    attempted->hook_path);
    ++		    hook_path);
     +
     +	return 1;
     +}
    @@ hook.c: int hook_exists(const char *name)
     +int run_hooks(const char *hook_name, const char *hook_path,
     +	      struct run_hooks_opt *options)
     +{
    -+	struct hook my_hook = {
    -+		.hook_path = hook_path,
    -+	};
     +	struct hook_cb_data cb_data = {
     +		.rc = 0,
     +		.hook_name = hook_name,
    ++		.hook_path = hook_path,
     +		.options = options,
     +	};
     +	int jobs = 1;
    @@ hook.c: int hook_exists(const char *name)
     +	if (!options)
     +		BUG("a struct run_hooks_opt must be provided to run_hooks");
     +
    -+	cb_data.run_me = &my_hook;
    -+
     +	run_processes_parallel_tr2(jobs,
     +				   pick_next_hook,
     +				   notify_start_failure,
    @@ hook.h
      #define HOOK_H
     +#include "strvec.h"
     +
    -+struct hook {
    -+	/* The path to the hook */
    -+	const char *hook_path;
    -+};
    -+
     +struct run_hooks_opt
     +{
     +	/* Environment vars to be set for each hook */
    @@ hook.h
     +	/* rc reflects the cumulative failure state */
     +	int rc;
     +	const char *hook_name;
    -+	struct hook *run_me;
    ++	const char *hook_path;
     +	struct run_hooks_opt *options;
     +};
      
    @@ hook.h: const char *find_hook(const char *name);
      int hook_exists(const char *hookname);
      
     +/**
    -+ * Clear data from an initialized "struct run_hooks-opt".
    ++ * Clear data from an initialized "struct run_hooks_opt".
     + */
     +void run_hooks_opt_clear(struct run_hooks_opt *o);
     +
 2:  dbac4204f7b =  2:  e3dc0aed81b gc: use hook library for pre-auto-gc hook
 3:  ff306debcb8 =  3:  6227a1e644d rebase: convert pre-rebase to use hook.h
 4:  b1d529ca485 =  4:  0e34eb54054 am: convert applypatch to use hook.h
 5:  15d71fc210b !  5:  a4df96c1719 hooks: convert 'post-checkout' hook to hook library
    @@ hook.c: static int pick_next_hook(struct child_process *cp,
      	cp->trace2_hook_name = hook_cb->hook_name;
     +	cp->dir = hook_cb->options->dir;
      
    - 	/* add command */
    - 	strvec_push(&cp->args, run_me->hook_path);
    + 	strvec_push(&cp->args, hook_path);
    + 	strvec_pushv(&cp->args, hook_cb->options->args.v);
     @@ hook.c: static int notify_hook_finished(int result,
      int run_hooks(const char *hook_name, const char *hook_path,
      	      struct run_hooks_opt *options)
      {
     +	struct strbuf abs_path = STRBUF_INIT;
    - 	struct hook my_hook = {
    - 		.hook_path = hook_path,
    - 	};
    + 	struct hook_cb_data cb_data = {
    + 		.rc = 0,
    + 		.hook_name = hook_name,
     @@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,
      	if (!options)
      		BUG("a struct run_hooks_opt must be provided to run_hooks");
      
     +	if (options->absolute_path) {
     +		strbuf_add_absolute_path(&abs_path, hook_path);
    -+		my_hook.hook_path = abs_path.buf;
    ++		hook_path = abs_path.buf;
     +	}
    - 	cb_data.run_me = &my_hook;
    - 
    ++
      	run_processes_parallel_tr2(jobs,
    + 				   pick_next_hook,
    + 				   notify_start_failure,
     @@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,
      				   "hook",
      				   hook_name);
 6:  08f27f0d6be =  6:  327f916f8c3 merge: convert post-merge to use hook.h
 7:  107c14d740f !  7:  328767015b1 git hook run: add an --ignore-missing flag
    @@ Commit message
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## Documentation/git-hook.txt ##
    -@@ Documentation/git-hook.txt: git-hook - run git hooks
    +@@ Documentation/git-hook.txt: git-hook - Run git hooks
      SYNOPSIS
      --------
      [verse]
    @@ Documentation/git-hook.txt: git-hook - run git hooks
      
      DESCRIPTION
      -----------
    -@@ Documentation/git-hook.txt: optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The
    - arguments (if any) differ by hook name, see linkgit:githooks[5] for
    - what those are.
    +@@ Documentation/git-hook.txt: Any positional arguments to the hook should be passed after a
    + mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
    + linkgit:githooks[5] for arguments hooks might expect (if any).
      
     +OPTIONS
     +-------
    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)
      	};
      	int ret;
     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)
    - 	/*
    - 	 * We are not using a plain run_hooks() because we'd like to
    - 	 * detect missing hooks. Let's find it ourselves and call
    --	 * run_hooks() instead.
    -+	 * run_hooks() instead...
    - 	 */
    + 	git_config(git_default_config, NULL);
    + 
      	hook_name = argv[0];
     +	if (ignore_missing)
    -+		/* ... act like a plain run_hooks() under --ignore-missing */
     +		return run_hooks_oneshot(hook_name, &opt);
      	hook_path = find_hook(hook_name);
      	if (!hook_path) {
 8:  1d30e2dbbe0 =  8:  6c4ebd68d56 send-email: use 'git hook run' for 'sendemail-validate'
 9:  69cc447a1e1 =  9:  b1f52733e3c git-p4: use 'git hook' to run hooks
10:  1c22b2992cf = 10:  dc31d98acdf commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
11:  e762fce32af = 11:  58b7689e4af read-cache: convert post-index-change to use hook.h
12:  d63b91196ae = 12:  ae1e2a82147 receive-pack: convert push-to-checkout hook to hook.h
13:  fe8996dda3e = 13:  289d5a2d849 run-command: remove old run_hook_{le,ve}() hook API
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 01/13] hook: add 'run' subcommand

From: Emily Shaffer <emilyshaffer@google.com>

In order to enable hooks to be run as an external process, by a standalone Git command, or by tools which wrap Git, provide an external means to run all configured hook commands for a given hook event.

Most of our hooks require more complex functionality than this, but let's start with the bare minimum required to support our simplest hooks.

In terms of implementation the usage_with_options() and "goto usage" pattern here mirrors that of builtin/{commit-graph,multi-pack-index}.c.

Some of the implementation here, such as a function being named run_hooks() when it's tasked with running one hook, to using the run_processes_parallel_tr2() API to run with jobs=1 is somewhere between a bit odd and and an overkill for the current features of this "hook run" command and the hook.[ch] API.

This code will eventually be able to run multiple hooks declared in config in parallel, by starting out with these names and APIs we reduce the later churn of renaming functions, switching from the run_command() to run_processes_parallel_tr2() API etc.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  37 +++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/hook.c             |  85 ++++++++++++++++++++++++
 command-list.txt           |   1 +
 git.c                      |   1 +
 hook.c                     |  91 ++++++++++++++++++++++++++
 hook.h                     |  34 ++++++++++
 t/t1800-hook.sh            | 129 +++++++++++++++++++++++++++++++++++++
 11 files changed, 385 insertions(+)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Show changes to 11 files +385 −0

.gitignore, Documentation/git-hook.txt, Documentation/githooks.txt, Makefile, builtin.h, builtin/hook.c, command-list.txt, git.c, hook.c, hook.h, t/t1800-hook.sh

diff --git a/.gitignore b/.gitignore
index 80c6bf6f3a8..005be1415ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@
 /git-grep
 /git-hash-object
 /git-help
+/git-hook
 /git-http-backend
 /git-http-fetch
 /git-http-push
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
new file mode 100644
index 00000000000..e39b1b5d069
--- /dev/null
+++ b/Documentation/git-hook.txt
@@ -0,0 +1,37 @@
+git-hook(1)
+===========
+
+NAME
+----
+git-hook - Run git hooks
+
+SYNOPSIS
+--------
+[verse]
+'git hook' run <hook-name> [-- <hook-args>]
+
+DESCRIPTION
+-----------
+
+A command interface to running git hooks (see linkgit:githooks[5]),
+for use by other scripted git commands.
+
+SUBCOMMANDS
+-----------
+
+run::
+	Run the `<hook-name>` hook. See linkgit:githooks[5] for
+	supported hook names.
++
+
+Any positional arguments to the hook should be passed after a
+mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
+linkgit:githooks[5] for arguments hooks might expect (if any).
+
+SEE ALSO
+--------
+linkgit:githooks[5]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index b51959ff941..a16e62bc8c8 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -698,6 +698,10 @@ and "0" meaning they were not.
 Only one parameter should be set to "1" when the hook runs.  The hook
 running passing "1", "1" should not be possible.
 
+SEE ALSO
+--------
+linkgit:git-hook[1]
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 060a8c46475..c95997f8d21 100644
--- a/Makefile
+++ b/Makefile
@@ -1108,6 +1108,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
 BUILTIN_OBJS += builtin/grep.o
 BUILTIN_OBJS += builtin/hash-object.o
 BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/hook.o
 BUILTIN_OBJS += builtin/index-pack.o
 BUILTIN_OBJS += builtin/init-db.o
 BUILTIN_OBJS += builtin/interpret-trailers.o
diff --git a/builtin.h b/builtin.h
index 8a58743ed63..83379f3832c 100644
--- a/builtin.h
+++ b/builtin.h
@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);
 int cmd_grep(int argc, const char **argv, const char *prefix);
 int cmd_hash_object(int argc, const char **argv, const char *prefix);
 int cmd_help(int argc, const char **argv, const char *prefix);
+int cmd_hook(int argc, const char **argv, const char *prefix);
 int cmd_index_pack(int argc, const char **argv, const char *prefix);
 int cmd_init_db(int argc, const char **argv, const char *prefix);
 int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);
diff --git a/builtin/hook.c b/builtin/hook.c
new file mode 100644
index 00000000000..41dd15550cf
--- /dev/null
+++ b/builtin/hook.c
@@ -0,0 +1,85 @@
+#include "cache.h"
+#include "builtin.h"
+#include "config.h"
+#include "hook.h"
+#include "parse-options.h"
+#include "strbuf.h"
+#include "strvec.h"
+
+#define BUILTIN_HOOK_RUN_USAGE \
+	N_("git hook run <hook-name> [-- <hook-args>]")
+
+static const char * const builtin_hook_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static const char * const builtin_hook_run_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static int run(int argc, const char **argv, const char *prefix)
+{
+	int i;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	const char *hook_name;
+	const char *hook_path;
+	struct option run_options[] = {
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, run_options,
+			     builtin_hook_run_usage,
+			     PARSE_OPT_KEEP_DASHDASH);
+
+	if (!argc)
+		goto usage;
+
+	/*
+	 * Having a -- for "run" when providing <hook-args> is
+	 * mandatory.
+	 */
+	if (argc > 1 && strcmp(argv[1], "--") &&
+	    strcmp(argv[1], "--end-of-options"))
+		goto usage;
+
+	/* Add our arguments, start after -- */
+	for (i = 2 ; i < argc; i++)
+		strvec_push(&opt.args, argv[i]);
+
+	/* Need to take into account core.hooksPath */
+	git_config(git_default_config, NULL);
+
+	hook_name = argv[0];
+	hook_path = find_hook(hook_name);
+	if (!hook_path) {
+		error("cannot find a hook named %s", hook_name);
+		return 1;
+	}
+
+	ret = run_hooks(hook_name, hook_path, &opt);
+	run_hooks_opt_clear(&opt);
+	return ret;
+usage:
+	usage_with_options(builtin_hook_run_usage, run_options);
+}
+
+int cmd_hook(int argc, const char **argv, const char *prefix)
+{
+	struct option builtin_hook_options[] = {
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, NULL, builtin_hook_options,
+			     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+	if (!argc)
+		goto usage;
+
+	if (!strcmp(argv[0], "run"))
+		return run(argc, argv, prefix);
+
+usage:
+	usage_with_options(builtin_hook_usage, builtin_hook_options);
+}
diff --git a/command-list.txt b/command-list.txt
index a289f09ed6f..17c3958802e 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info
 git-gui                                 mainporcelain
 git-hash-object                         plumbingmanipulators
 git-help                                ancillaryinterrogators          complete
+git-hook                                purehelpers
 git-http-backend                        synchingrepositories
 git-http-fetch                          synchelpers
 git-http-push                           synchelpers
diff --git a/git.c b/git.c
index 60c2784be45..e5891e02021 100644
--- a/git.c
+++ b/git.c
@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {
 	{ "grep", cmd_grep, RUN_SETUP_GENTLY },
 	{ "hash-object", cmd_hash_object },
 	{ "help", cmd_help },
+	{ "hook", cmd_hook, RUN_SETUP },
 	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
 	{ "init", cmd_init_db },
 	{ "init-db", cmd_init_db },
diff --git a/hook.c b/hook.c
index 55e1145a4b7..20a1a4fec33 100644
--- a/hook.c
+++ b/hook.c
@@ -1,6 +1,7 @@
 #include "cache.h"
 #include "hook.h"
 #include "run-command.h"
+#include "config.h"
 
 const char *find_hook(const char *name)
 {
@@ -40,3 +41,93 @@ int hook_exists(const char *name)
 {
 	return !!find_hook(name);
 }
+
+void run_hooks_opt_clear(struct run_hooks_opt *o)
+{
+	strvec_clear(&o->env);
+	strvec_clear(&o->args);
+}
+
+static int pick_next_hook(struct child_process *cp,
+			  struct strbuf *out,
+			  void *pp_cb,
+			  void **pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = hook_cb->hook_path;
+
+	if (!hook_path)
+		return 0;
+
+	cp->no_stdin = 1;
+	cp->env = hook_cb->options->env.v;
+	cp->stdout_to_stderr = 1;
+	cp->trace2_hook_name = hook_cb->hook_name;
+
+	strvec_push(&cp->args, hook_path);
+	strvec_pushv(&cp->args, hook_cb->options->args.v);
+
+	/* Provide context for errors if necessary */
+	*pp_task_cb = (char *)hook_path;
+
+	/*
+	 * This pick_next_hook() will be called again, we're only
+	 * running one hook, so indicate that no more work will be
+	 * done.
+	 */
+	hook_cb->hook_path = NULL;
+
+	return 1;
+}
+
+static int notify_start_failure(struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cp)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = pp_task_cp;
+
+	hook_cb->rc |= 1;
+
+	strbuf_addf(out, _("Couldn't start hook '%s'\n"),
+		    hook_path);
+
+	return 1;
+}
+
+static int notify_hook_finished(int result,
+				struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+
+	hook_cb->rc |= result;
+
+	return 0;
+}
+
+int run_hooks(const char *hook_name, const char *hook_path,
+	      struct run_hooks_opt *options)
+{
+	struct hook_cb_data cb_data = {
+		.rc = 0,
+		.hook_name = hook_name,
+		.hook_path = hook_path,
+		.options = options,
+	};
+	int jobs = 1;
+
+	if (!options)
+		BUG("a struct run_hooks_opt must be provided to run_hooks");
+
+	run_processes_parallel_tr2(jobs,
+				   pick_next_hook,
+				   notify_start_failure,
+				   notify_hook_finished,
+				   &cb_data,
+				   "hook",
+				   hook_name);
+
+	return cb_data.rc;
+}
diff --git a/hook.h b/hook.h
index 6aa36fc7ff9..a9317c3f95e 100644
--- a/hook.h
+++ b/hook.h
@@ -1,5 +1,28 @@
 #ifndef HOOK_H
 #define HOOK_H
+#include "strvec.h"
+
+struct run_hooks_opt
+{
+	/* Environment vars to be set for each hook */
+	struct strvec env;
+
+	/* Args to be passed to each hook */
+	struct strvec args;
+};
+
+#define RUN_HOOKS_OPT_INIT { \
+	.env = STRVEC_INIT, \
+	.args = STRVEC_INIT, \
+}
+
+struct hook_cb_data {
+	/* rc reflects the cumulative failure state */
+	int rc;
+	const char *hook_name;
+	const char *hook_path;
+	struct run_hooks_opt *options;
+};
 
 /*
  * Returns the path to the hook file, or NULL if the hook is missing
@@ -13,4 +36,15 @@ const char *find_hook(const char *name);
  */
 int hook_exists(const char *hookname);
 
+/**
+ * Clear data from an initialized "struct run_hooks_opt".
+ */
+void run_hooks_opt_clear(struct run_hooks_opt *o);
+
+/**
+ * Takes an already resolved hook found via find_hook() and runs
+ * it. Does not call run_hooks_opt_clear() for you.
+ */
+int run_hooks(const char *hookname, const char *hook_path,
+	      struct run_hooks_opt *options);
 #endif
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
new file mode 100755
index 00000000000..3aea1b105f0
--- /dev/null
+++ b/t/t1800-hook.sh
@@ -0,0 +1,129 @@
+#!/bin/sh
+
+test_description='git-hook command'
+
+TEST_PASSES_SANITIZE_LEAK=true
+. ./test-lib.sh
+
+test_expect_success 'git hook usage' '
+	test_expect_code 129 git hook &&
+	test_expect_code 129 git hook run &&
+	test_expect_code 129 git hook run -h &&
+	test_expect_code 129 git hook run --unknown 2>err &&
+	grep "unknown option" err
+'
+
+test_expect_success 'git hook run: nonexistent hook' '
+	cat >stderr.expect <<-\EOF &&
+	error: cannot find a hook named test-hook
+	EOF
+	test_expect_code 1 git hook run test-hook 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual
+'
+
+test_expect_success 'git hook run: basic' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	EOF
+	git hook run test-hook 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run: stdout and stderr both write to our stderr' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo >&1 Will end up on stderr
+	echo >&2 Will end up on stderr
+	EOF
+
+	cat >stderr.expect <<-\EOF &&
+	Will end up on stderr
+	Will end up on stderr
+	EOF
+	git hook run test-hook >stdout.actual 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual &&
+	test_must_be_empty stdout.actual
+'
+
+test_expect_success 'git hook run: exit codes are passed along' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 1
+	EOF
+
+	test_expect_code 1 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 2
+	EOF
+
+	test_expect_code 2 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 128
+	EOF
+
+	test_expect_code 128 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 129
+	EOF
+
+	test_expect_code 129 git hook run test-hook
+'
+
+test_expect_success 'git hook run arg u ments without -- is not allowed' '
+	test_expect_code 129 git hook run test-hook arg u ments
+'
+
+test_expect_success 'git hook run -- pass arguments' '
+	write_script .git/hooks/test-hook <<-\EOF &&
+	echo $1
+	echo $2
+	EOF
+
+	cat >expect <<-EOF &&
+	arg
+	u ments
+	EOF
+
+	git hook run test-hook -- arg "u ments" 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run -- out-of-repo runs excluded' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	nongit test_must_fail git hook run test-hook
+'
+
+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
+	mkdir my-hooks &&
+	write_script my-hooks/test-hook <<-\EOF &&
+	echo Hook ran $1 >>actual
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	Hook ran one
+	Hook ran two
+	Hook ran three
+	Hook ran four
+	EOF
+
+	# Test various ways of specifying the path. See also
+	# t1350-config-hooks-path.sh
+	>actual &&
+	git hook run test-hook -- ignored 2>>actual &&
+	git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&
+	git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks/" hook run test-hook -- four 2>>actual &&
+	test_cmp expect actual
+'
+
+test_done
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 16:24 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 01/13] hook: add 'run' subcommand

On Tue, Oct 12, 2021 at 03:30:26PM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 41 quoted lines
> 
> 
> In order to enable hooks to be run as an external process, by a
> standalone Git command, or by tools which wrap Git, provide an external
> means to run all configured hook commands for a given hook event.
> 
> Most of our hooks require more complex functionality than this, but
> let's start with the bare minimum required to support our simplest
> hooks.
> 
> In terms of implementation the usage_with_options() and "goto usage"
> pattern here mirrors that of
> builtin/{commit-graph,multi-pack-index}.c.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
> new file mode 100644
> index 00000000000..660d6a992a0
> --- /dev/null
> +++ b/Documentation/git-hook.txt
> @@ -0,0 +1,38 @@
> +git-hook(1)
> +===========
> +
> +NAME
> +----
> +git-hook - run git hooks
> +
> +SYNOPSIS
> +--------
> +[verse]
> +'git hook' run <hook-name> [-- <hook-args>]
> +
> +DESCRIPTION
> +-----------
> +
> +This command is an interface to git hooks (see linkgit:githooks[5]).
> +Currently it only provides a convenience wrapper for running hooks for
> +use by git itself. In the future it might gain other functionality.

s/git/Git/g nit, no? I thought we were somewhat picky about capitalizing Git when we don't mean `git`?

Show 20 quoted lines
> +
> +SUBCOMMANDS
> +-----------
> +
> +run::
> +	Run the `<hook-name>` hook. See linkgit:githooks[5] for
> +	the hook names we support.
> ++
> +Any positional arguments to the hook should be passed after an
> +optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The
> +arguments (if any) differ by hook name, see linkgit:githooks[5] for
> +what those are.
> +
> +SEE ALSO
> +--------
> +linkgit:githooks[5]
> +
> +GIT
> +---
> +Part of the linkgit:git[1] suite
Show 26 quoted lines
> diff --git a/builtin/hook.c b/builtin/hook.c
> new file mode 100644
> index 00000000000..012a2973b38
> --- /dev/null
> +++ b/builtin/hook.c
> @@ -0,0 +1,90 @@
> +#include "cache.h"
> +#include "builtin.h"
> +#include "config.h"
> +#include "hook.h"
> +#include "parse-options.h"
> +#include "strbuf.h"
> +#include "strvec.h"
> +
> +#define BUILTIN_HOOK_RUN_USAGE \
> +	N_("git hook run <hook-name> [-- <hook-args>]")
> +
> +static const char * const builtin_hook_usage[] = {
> +	BUILTIN_HOOK_RUN_USAGE,
> +	NULL
> +};
> +
> +static const char * const builtin_hook_run_usage[] = {
> +	BUILTIN_HOOK_RUN_USAGE,
> +	NULL
> +};

While some reviewers might think this looks untidy, it does save us some repetition later on adding other subcommands to 'git hook'. Thanks.

Show 15 quoted lines
> +
> +static int run(int argc, const char **argv, const char *prefix)
> +{
> +	int i;
> +	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
> +	const char *hook_name;
> +	const char *hook_path;
> +	struct option run_options[] = {
> +		OPT_END(),
> +	};
> +	int ret;
> +
> +	argc = parse_options(argc, argv, prefix, run_options,
> +			     builtin_hook_run_usage,
> +			     PARSE_OPT_KEEP_DASHDASH);

We KEEP_DASHDASH so we can ensure someone did not send us some args they meant for 'git hook' instead of for the hook script itself. Ok.

Show 10 quoted lines
> +
> +	if (!argc)
> +		goto usage;
> +
> +	/*
> +	 * Having a -- for "run" when providing <hook-args> is
> +	 * mandatory.
> +	 */
> +	if (argc > 1 && strcmp(argv[1], "--") &&
> +	    strcmp(argv[1], "--end-of-options"))

--end-of-options isn't documented anywhere, right? Fine, I don't mind, but it's interesting to support it "secretly".

Show 8 quoted lines
> +		goto usage;
> +
> +	/* Add our arguments, start after -- */
> +	for (i = 2 ; i < argc; i++)
> +		strvec_push(&opt.args, argv[i]);
> +
> +	/* Need to take into account core.hooksPath */
> +	git_config(git_default_config, NULL);
..and git_default_config() sets it up for use in find_hook(). Great.
Show 6 quoted lines
> +
> +	/*
> +	 * We are not using a plain run_hooks() because we'd like to
> +	 * detect missing hooks. Let's find it ourselves and call
> +	 * run_hooks() instead.
> +	 */

Hmm. In practice, I wonder if this means that callers like 'git-send-email' still need to check whether there is a 'sendemail-validate' hook before they can call 'git hook run sendemail-validate'? I guess I'll have a look at the work there and see.

Show 31 quoted lines
> +	hook_name = argv[0];
> +	hook_path = find_hook(hook_name);
> +	if (!hook_path) {
> +		error("cannot find a hook named %s", hook_name);
> +		return 1;
> +	}
> +
> +	ret = run_hooks(hook_name, hook_path, &opt);
> +	run_hooks_opt_clear(&opt);
> +	return ret;
> +usage:
> +	usage_with_options(builtin_hook_run_usage, run_options);
> +}
> +
> +int cmd_hook(int argc, const char **argv, const char *prefix)
> +{
> +	struct option builtin_hook_options[] = {
> +		OPT_END(),
> +	};
> +
> +	argc = parse_options(argc, argv, NULL, builtin_hook_options,
> +			     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
> +	if (!argc)
> +		goto usage;
> +
> +	if (!strcmp(argv[0], "run"))
> +		return run(argc, argv, prefix);
> +
> +usage:
> +	usage_with_options(builtin_hook_usage, builtin_hook_options);
> +}
The rest of it is very straightforward boilerplate for a builtin. Thanks.
Show 21 quoted lines
> diff --git a/command-list.txt b/command-list.txt
> index a289f09ed6f..9ccd8e5aebe 100644
> --- a/command-list.txt
> +++ b/command-list.txt
> @@ -103,6 +103,7 @@ git-grep                                mainporcelain           info
>  git-gui                                 mainporcelain
>  git-hash-object                         plumbingmanipulators
>  git-help                                ancillaryinterrogators          complete
> +git-hook                                mainporcelain
>  git-http-backend                        synchingrepositories
>  git-http-fetch                          synchelpers
>  git-http-push                           synchelpers
> diff --git a/git.c b/git.c
> index 60c2784be45..e5891e02021 100644
> --- a/git.c
> +++ b/git.c
> @@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {
>  	{ "grep", cmd_grep, RUN_SETUP_GENTLY },
>  	{ "hash-object", cmd_hash_object },
>  	{ "help", cmd_help },
> +	{ "hook", cmd_hook, RUN_SETUP },

Ok. For now this is still requiring a gitdir. That makes sense to me, because we are still getting our hooks from $GITDIR/hooks/. Fine.

Show 36 quoted lines
>  	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
>  	{ "init", cmd_init_db },
>  	{ "init-db", cmd_init_db },
> diff --git a/hook.c b/hook.c
> index 55e1145a4b7..240270db64e 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -1,6 +1,7 @@
>  #include "cache.h"
>  #include "hook.h"
>  #include "run-command.h"
> +#include "config.h"
>  
>  const char *find_hook(const char *name)
>  {
> @@ -40,3 +41,103 @@ int hook_exists(const char *name)
>  {
>  	return !!find_hook(name);
>  }
> +
> +void run_hooks_opt_clear(struct run_hooks_opt *o)
> +{
> +	strvec_clear(&o->env);
> +	strvec_clear(&o->args);
> +}
> +
> +static int pick_next_hook(struct child_process *cp,
> +			  struct strbuf *out,
> +			  void *pp_cb,
> +			  void **pp_task_cb)
> +{
> +	struct hook_cb_data *hook_cb = pp_cb;
> +	struct hook *run_me = hook_cb->run_me;
> +
> +	if (!run_me)
> +		return 0;

Signal the run_processes_parallel() machinery if we don't have any hooks left to run. Right now the linked list is always going to be of size 1, but that's ok :)

Show 5 quoted lines
> +
> +	cp->no_stdin = 1;
> +	cp->env = hook_cb->options->env.v;
> +	cp->stdout_to_stderr = 1;
> +	cp->trace2_hook_name = hook_cb->hook_name;
These args match identically the ones set up in run_hook_ve. Ok.
Show 31 quoted lines
> +
> +	/* add command */
> +	strvec_push(&cp->args, run_me->hook_path);
> +
> +	/*
> +	 * add passed-in argv, without expanding - let the user get back
> +	 * exactly what they put in
> +	 */
> +	strvec_pushv(&cp->args, hook_cb->options->args.v);
> +
> +	/* Provide context for errors if necessary */
> +	*pp_task_cb = run_me;
> +
> +	/*
> +	 * This pick_next_hook() will be called again, we're only
> +	 * running one hook, so indicate that no more work will be
> +	 * done.
> +	 */
> +	hook_cb->run_me = NULL;
> +
> +	return 1;
> +}
> +
> +static int notify_start_failure(struct strbuf *out,
> +				void *pp_cb,
> +				void *pp_task_cp)
> +{
> +	struct hook_cb_data *hook_cb = pp_cb;
> +	struct hook *attempted = pp_task_cp;
> +
> +	hook_cb->rc |= 1;

The OR means this will continue to work if the list of hooks is greater than 1. Ok.

Show 5 quoted lines
> +
> +	strbuf_addf(out, _("Couldn't start hook '%s'\n"),
> +		    attempted->hook_path);
> +
> +	return 1;

By returning 1, we ask the run_processes_parallel() machinery to give up on the rest of the hooks. I think that is okay, because the Git operation will not complete successfully anyway, and because "failed to start process" has more serious implications for Git's ability to continue starting tasks, compared to a hook validation failure. Ok.

Show 12 quoted lines
> +}
> +
> +static int notify_hook_finished(int result,
> +				struct strbuf *out,
> +				void *pp_cb,
> +				void *pp_task_cb)
> +{
> +	struct hook_cb_data *hook_cb = pp_cb;
> +
> +	hook_cb->rc |= result;
> +
> +	return 0;

Conversely, here we could decide to terminate the task list early if we heard an unhappy hook's return code. However, it's probably more useful for a user to hear *all* the reasons that their operation failed, rather than just one, so they don't need to rerun their commit operation 5 times for 5 different hook failures... :) Fine.

Show 14 quoted lines
> +}
> +
> +int run_hooks(const char *hook_name, const char *hook_path,
> +	      struct run_hooks_opt *options)
> +{
> +	struct hook my_hook = {
> +		.hook_path = hook_path,
> +	};
> +	struct hook_cb_data cb_data = {
> +		.rc = 0,
> +		.hook_name = hook_name,
> +		.options = options,
> +	};
> +	int jobs = 1;

Even though this is a constant, I find the throwaway variable easier to read than using a magic 1 in the run_processes_parallel call. I imagine the compiler gets rid of it, anyways. Thanks.

Show 60 quoted lines
> +
> +	if (!options)
> +		BUG("a struct run_hooks_opt must be provided to run_hooks");
> +
> +	cb_data.run_me = &my_hook;
> +
> +	run_processes_parallel_tr2(jobs,
> +				   pick_next_hook,
> +				   notify_start_failure,
> +				   notify_hook_finished,
> +				   &cb_data,
> +				   "hook",
> +				   hook_name);
> +
> +	return cb_data.rc;
> +}
> diff --git a/hook.h b/hook.h
> index 6aa36fc7ff9..111c5cf9296 100644
> --- a/hook.h
> +++ b/hook.h
> @@ -1,5 +1,33 @@
>  #ifndef HOOK_H
>  #define HOOK_H
> +#include "strvec.h"
> +
> +struct hook {
> +	/* The path to the hook */
> +	const char *hook_path;
> +};
> +
> +struct run_hooks_opt
> +{
> +	/* Environment vars to be set for each hook */
> +	struct strvec env;
> +
> +	/* Args to be passed to each hook */
> +	struct strvec args;
> +};
> +
> +#define RUN_HOOKS_OPT_INIT { \
> +	.env = STRVEC_INIT, \
> +	.args = STRVEC_INIT, \
> +}
> +
> +struct hook_cb_data {
> +	/* rc reflects the cumulative failure state */
> +	int rc;
> +	const char *hook_name;
> +	struct hook *run_me;
> +	struct run_hooks_opt *options;
> +};
>  
>  /*
>   * Returns the path to the hook file, or NULL if the hook is missing
> @@ -13,4 +41,15 @@ const char *find_hook(const char *name);
>   */
>  int hook_exists(const char *hookname);
>  
> +/**
> + * Clear data from an initialized "struct run_hooks-opt".
Nit, s/run_hooks-opt/run_hooks_opt/
Show 37 quoted lines
> + */
> +void run_hooks_opt_clear(struct run_hooks_opt *o);
> +
> +/**
> + * Takes an already resolved hook found via find_hook() and runs
> + * it. Does not call run_hooks_opt_clear() for you.
> + */
> +int run_hooks(const char *hookname, const char *hook_path,
> +	      struct run_hooks_opt *options);
>  #endif
> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
> new file mode 100755
> index 00000000000..3aea1b105f0
> --- /dev/null
> +++ b/t/t1800-hook.sh
> @@ -0,0 +1,129 @@
> +#!/bin/sh
> +
> +test_description='git-hook command'
> +
> +TEST_PASSES_SANITIZE_LEAK=true
> +. ./test-lib.sh
> +
> +test_expect_success 'git hook usage' '
> +	test_expect_code 129 git hook &&
> +	test_expect_code 129 git hook run &&
> +	test_expect_code 129 git hook run -h &&
> +	test_expect_code 129 git hook run --unknown 2>err &&
> +	grep "unknown option" err
> +'
> +
> +test_expect_success 'git hook run: nonexistent hook' '
> +	cat >stderr.expect <<-\EOF &&
> +	error: cannot find a hook named test-hook
> +	EOF
> +	test_expect_code 1 git hook run test-hook 2>stderr.actual &&
> +	test_cmp stderr.expect stderr.actual
See my earlier worry about this meaning callers still have to check the hookdir.
Show 110 quoted lines
> +'
> +
> +test_expect_success 'git hook run: basic' '
> +	write_script .git/hooks/test-hook <<-EOF &&
> +	echo Test hook
> +	EOF
> +
> +	cat >expect <<-\EOF &&
> +	Test hook
> +	EOF
> +	git hook run test-hook 2>actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'git hook run: stdout and stderr both write to our stderr' '
> +	write_script .git/hooks/test-hook <<-EOF &&
> +	echo >&1 Will end up on stderr
> +	echo >&2 Will end up on stderr
> +	EOF
> +
> +	cat >stderr.expect <<-\EOF &&
> +	Will end up on stderr
> +	Will end up on stderr
> +	EOF
> +	git hook run test-hook >stdout.actual 2>stderr.actual &&
> +	test_cmp stderr.expect stderr.actual &&
> +	test_must_be_empty stdout.actual
> +'
> +
> +test_expect_success 'git hook run: exit codes are passed along' '
> +	write_script .git/hooks/test-hook <<-EOF &&
> +	exit 1
> +	EOF
> +
> +	test_expect_code 1 git hook run test-hook &&
> +
> +	write_script .git/hooks/test-hook <<-EOF &&
> +	exit 2
> +	EOF
> +
> +	test_expect_code 2 git hook run test-hook &&
> +
> +	write_script .git/hooks/test-hook <<-EOF &&
> +	exit 128
> +	EOF
> +
> +	test_expect_code 128 git hook run test-hook &&
> +
> +	write_script .git/hooks/test-hook <<-EOF &&
> +	exit 129
> +	EOF
> +
> +	test_expect_code 129 git hook run test-hook
> +'
> +
> +test_expect_success 'git hook run arg u ments without -- is not allowed' '
> +	test_expect_code 129 git hook run test-hook arg u ments
> +'
> +
> +test_expect_success 'git hook run -- pass arguments' '
> +	write_script .git/hooks/test-hook <<-\EOF &&
> +	echo $1
> +	echo $2
> +	EOF
> +
> +	cat >expect <<-EOF &&
> +	arg
> +	u ments
> +	EOF
> +
> +	git hook run test-hook -- arg "u ments" 2>actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'git hook run -- out-of-repo runs excluded' '
> +	write_script .git/hooks/test-hook <<-EOF &&
> +	echo Test hook
> +	EOF
> +
> +	nongit test_must_fail git hook run test-hook
> +'
> +
> +test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
> +	mkdir my-hooks &&
> +	write_script my-hooks/test-hook <<-\EOF &&
> +	echo Hook ran $1 >>actual
> +	EOF
> +
> +	cat >expect <<-\EOF &&
> +	Test hook
> +	Hook ran one
> +	Hook ran two
> +	Hook ran three
> +	Hook ran four
> +	EOF
> +
> +	# Test various ways of specifying the path. See also
> +	# t1350-config-hooks-path.sh
> +	>actual &&
> +	git hook run test-hook -- ignored 2>>actual &&
> +	git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&
> +	git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&
> +	git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&
> +	git -c core.hooksPath="$PWD/my-hooks/" hook run test-hook -- four 2>>actual &&
> +	test_cmp expect actual
> +'
> +
> +test_done
> -- 
> 2.33.0.1567.g7b23ce7ed9e

Thanks. Other than my concern about erroring when there is no hook executable, it looks fine - but that one is serious enough that I'll withhold the Reviewed-by line.

Eric Sunshine· Oct 15, 2021, 17:53 UTC · re: Emily Shaffer · lore

Re: [PATCH v2 01/13] hook: add 'run' subcommand

On Fri, Oct 15, 2021 at 12:24 PM Emily Shaffer <emilyshaffer@google.com> wrote:
Show 6 quoted lines
> On Tue, Oct 12, 2021 at 03:30:26PM +0200, Ævar Arnfjörð Bjarmason wrote:
> > +     if (argc > 1 && strcmp(argv[1], "--") &&
> > +         strcmp(argv[1], "--end-of-options"))
>
> --end-of-options isn't documented anywhere, right? Fine, I don't mind, but it's
> interesting to support it "secretly".
This option is documented in Documentation/gitcli.txt (i.e. `git help cli`).
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:08 UTC · re: Emily Shaffer · lore

Re: [PATCH v2 01/13] hook: add 'run' subcommand

On Fri, Oct 15 2021, Emily Shaffer wrote:
Show 45 quoted lines
> On Tue, Oct 12, 2021 at 03:30:26PM +0200, Ævar Arnfjörð Bjarmason wrote:
>> 
>> 
>> In order to enable hooks to be run as an external process, by a
>> standalone Git command, or by tools which wrap Git, provide an external
>> means to run all configured hook commands for a given hook event.
>> 
>> Most of our hooks require more complex functionality than this, but
>> let's start with the bare minimum required to support our simplest
>> hooks.
>> 
>> In terms of implementation the usage_with_options() and "goto usage"
>> pattern here mirrors that of
>> builtin/{commit-graph,multi-pack-index}.c.
>> 
>> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
>> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
>> ---
>> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
>> new file mode 100644
>> index 00000000000..660d6a992a0
>> --- /dev/null
>> +++ b/Documentation/git-hook.txt
>> @@ -0,0 +1,38 @@
>> +git-hook(1)
>> +===========
>> +
>> +NAME
>> +----
>> +git-hook - run git hooks
>> +
>> +SYNOPSIS
>> +--------
>> +[verse]
>> +'git hook' run <hook-name> [-- <hook-args>]
>> +
>> +DESCRIPTION
>> +-----------
>> +
>> +This command is an interface to git hooks (see linkgit:githooks[5]).
>> +Currently it only provides a convenience wrapper for running hooks for
>> +use by git itself. In the future it might gain other functionality.
>
> s/git/Git/g nit, no? I thought we were somewhat picky about capitalizing Git
> when we don't mean `git`?

I'm not sure how this got mixed up, but the text you've got quoted here is from the v1 at: https://lore.kernel.org/git/patch-01.13-a39c0748d3f-20211012T131934Z-avarab@gmail.com/

But you're replying to the v2 here, i.e. this, which has no such text: https://lore.kernel.org/git/patch-v2-01.13-ba64faf0580-20211015T093918Z-avarab@gmail.com/

I see I managed to omit the In-Reply-To link from v2 to v1 by mistake, maybe some funny mailer interaction with that?

Anyway for all the rest of the comments below & in this thread I tried to address them in some way, re-rolling soon...

Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 02/13] gc: use hook library for pre-auto-gc hook

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-auto-gc hook away from run-command.h to and over to the new hook.h library.

To do this introduce a simple run_hooks_oneshot() wrapper, we'll be using it extensively for these simple cases of wanting to run a single hook under a given name, and having it free the memory we allocate for us.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c |  3 ++-
 hook.c       | 20 ++++++++++++++++++++
 hook.h       | 13 +++++++++++++
 3 files changed, 35 insertions(+), 1 deletion(-)
Show changes to 3 files +35 −1

builtin/gc.c, hook.c, hook.h

diff --git a/builtin/gc.c b/builtin/gc.c
index 6b3de3dd514..95939e77f53 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -32,6 +32,7 @@
 #include "remote.h"
 #include "object-store.h"
 #include "exec-cmd.h"
+#include "hook.h"
 
 #define FAILED_RUN "failed to run %s"
 
@@ -394,7 +395,7 @@ static int need_to_gc(void)
 	else
 		return 0;
 
-	if (run_hook_le(NULL, "pre-auto-gc", NULL))
+	if (run_hooks_oneshot("pre-auto-gc", NULL))
 		return 0;
 	return 1;
 }
diff --git a/hook.c b/hook.c
index 20a1a4fec33..9951015dc66 100644
--- a/hook.c
+++ b/hook.c
@@ -131,3 +131,23 @@ int run_hooks(const char *hook_name, const char *hook_path,
 
 	return cb_data.rc;
 }
+
+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)
+{
+	const char *hook_path;
+	struct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;
+	int ret = 0;
+
+	if (!options)
+		options = &hook_opt_scratch;
+
+	hook_path = find_hook(hook_name);
+	if (!hook_path)
+		goto cleanup;
+
+	ret = run_hooks(hook_name, hook_path, options);
+cleanup:
+	run_hooks_opt_clear(options);
+
+	return ret;
+}
diff --git a/hook.h b/hook.h
index a9317c3f95e..9e4d10df63e 100644
--- a/hook.h
+++ b/hook.h
@@ -44,7 +44,20 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);
 /**
  * Takes an already resolved hook found via find_hook() and runs
  * it. Does not call run_hooks_opt_clear() for you.
+ *
+ * See run_hooks_oneshot() for the simpler one-shot API.
  */
 int run_hooks(const char *hookname, const char *hook_path,
 	      struct run_hooks_opt *options);
+
+/**
+ * Calls find_hook() on your "hook_name" and runs the hooks (if any)
+ * with run_hooks().
+ *
+ * If "options" is provided calls run_hooks_opt_clear() on it for
+ * you. If "options" is NULL the default options from
+ * RUN_HOOKS_OPT_INIT will be used.
+ */
+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);
+
 #endif
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 16:24 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 02/13] gc: use hook library for pre-auto-gc hook

On Tue, Oct 12, 2021 at 03:30:27PM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 36 quoted lines
> 
> 
> Move the pre-auto-gc hook away from run-command.h to and over to the
> new hook.h library.
> 
> To do this introduce a simple run_hooks_oneshot() wrapper, we'll be
> using it extensively for these simple cases of wanting to run a single
> hook under a given name, and having it free the memory we allocate for
> us.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  builtin/gc.c |  3 ++-
>  hook.c       | 20 ++++++++++++++++++++
>  hook.h       | 13 +++++++++++++
>  3 files changed, 35 insertions(+), 1 deletion(-)
> 
> diff --git a/builtin/gc.c b/builtin/gc.c
> index 6b3de3dd514..95939e77f53 100644
> --- a/builtin/gc.c
> +++ b/builtin/gc.c
> @@ -32,6 +32,7 @@
>  #include "remote.h"
>  #include "object-store.h"
>  #include "exec-cmd.h"
> +#include "hook.h"
>  
>  #define FAILED_RUN "failed to run %s"
>  
> @@ -394,7 +395,7 @@ static int need_to_gc(void)
>  	else
>  		return 0;
>  
> -	if (run_hook_le(NULL, "pre-auto-gc", NULL))
> +	if (run_hooks_oneshot("pre-auto-gc", NULL))
I like that the oneshot gives us such a simple replacement for run_hooks_le.
Show 31 quoted lines
>  		return 0;
>  	return 1;
>  }
> diff --git a/hook.c b/hook.c
> index 240270db64e..bfdc79e2f1a 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -141,3 +141,23 @@ int run_hooks(const char *hook_name, const char *hook_path,
>  
>  	return cb_data.rc;
>  }
> +
> +int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)
> +{
> +	const char *hook_path;
> +	struct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;
> +	int ret = 0;
> +
> +	if (!options)
> +		options = &hook_opt_scratch;
> +
> +	hook_path = find_hook(hook_name);
> +	if (!hook_path)
> +		goto cleanup;
> +
> +	ret = run_hooks(hook_name, hook_path, options);
> +cleanup:
> +	run_hooks_opt_clear(options);
> +
> +	return ret;
> +}
Reasonable enough - lookup, run, cleanup. Thanks.
Show 28 quoted lines
> diff --git a/hook.h b/hook.h
> index 111c5cf9296..a2b8d4fc6bd 100644
> --- a/hook.h
> +++ b/hook.h
> @@ -49,7 +49,20 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);
>  /**
>   * Takes an already resolved hook found via find_hook() and runs
>   * it. Does not call run_hooks_opt_clear() for you.
> + *
> + * See run_hooks_oneshot() for the simpler one-shot API.
>   */
>  int run_hooks(const char *hookname, const char *hook_path,
>  	      struct run_hooks_opt *options);
> +
> +/**
> + * Calls find_hook() on your "hook_name" and runs the hooks (if any)
> + * with run_hooks().
> + *
> + * If "options" is provided calls run_hooks_opt_clear() on it for
> + * you. If "options" is NULL the default options from
> + * RUN_HOOKS_OPT_INIT will be used.
> + */
> +int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);
> +
>  #endif
> -- 
> 2.33.0.1567.g7b23ce7ed9e
> 
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 04/13] am: convert applypatch to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach pre-applypatch, post-applypatch, and applypatch-msg to use the hook.h library instead of the run-command.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/am.c | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)
Show changes to builtin/am.c +5 −3
diff --git a/builtin/am.c b/builtin/am.c
index 8677ea2348a..5528f131a81 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -446,9 +446,11 @@ static void am_destroy(const struct am_state *state)
 static int run_applypatch_msg_hook(struct am_state *state)
 {
 	int ret;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 
 	assert(state->msg);
-	ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
+	strvec_push(&opt.args, am_path(state, "final-commit"));
+	ret = run_hooks_oneshot("applypatch-msg", &opt);
 
 	if (!ret) {
 		FREE_AND_NULL(state->msg);
@@ -1609,7 +1611,7 @@ static void do_commit(const struct am_state *state)
 	const char *reflog_msg, *author, *committer = NULL;
 	struct strbuf sb = STRBUF_INIT;
 
-	if (run_hook_le(NULL, "pre-applypatch", NULL))
+	if (run_hooks_oneshot("pre-applypatch", NULL))
 		exit(1);
 
 	if (write_cache_as_tree(&tree, 0, NULL))
@@ -1661,7 +1663,7 @@ static void do_commit(const struct am_state *state)
 		fclose(fp);
 	}
 
-	run_hook_le(NULL, "post-applypatch", NULL);
+	run_hooks_oneshot("post-applypatch", NULL);
 
 	strbuf_release(&sb);
 }
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 16:25 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 04/13] am: convert applypatch to use hook.h

On Tue, Oct 12, 2021 at 03:30:29PM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 7 quoted lines
> 
> 
> Teach pre-applypatch, post-applypatch, and applypatch-msg to use the
> hook.h library instead of the run-command.h library.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Show 42 quoted lines
> ---
>  builtin/am.c | 8 +++++---
>  1 file changed, 5 insertions(+), 3 deletions(-)
> 
> diff --git a/builtin/am.c b/builtin/am.c
> index 3527945a467..2cfe6451b6f 100644
> --- a/builtin/am.c
> +++ b/builtin/am.c
> @@ -446,9 +446,11 @@ static void am_destroy(const struct am_state *state)
>  static int run_applypatch_msg_hook(struct am_state *state)
>  {
>  	int ret;
> +	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
>  
>  	assert(state->msg);
> -	ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
> +	strvec_push(&opt.args, am_path(state, "final-commit"));
> +	ret = run_hooks_oneshot("applypatch-msg", &opt);
>  
>  	if (!ret) {
>  		FREE_AND_NULL(state->msg);
> @@ -1609,7 +1611,7 @@ static void do_commit(const struct am_state *state)
>  	const char *reflog_msg, *author, *committer = NULL;
>  	struct strbuf sb = STRBUF_INIT;
>  
> -	if (run_hook_le(NULL, "pre-applypatch", NULL))
> +	if (run_hooks_oneshot("pre-applypatch", NULL))
>  		exit(1);
>  
>  	if (write_cache_as_tree(&tree, 0, NULL))
> @@ -1661,7 +1663,7 @@ static void do_commit(const struct am_state *state)
>  		fclose(fp);
>  	}
>  
> -	run_hook_le(NULL, "post-applypatch", NULL);
> +	run_hooks_oneshot("post-applypatch", NULL);
>  
>  	strbuf_release(&sb);
>  }
> -- 
> 2.33.0.1567.g7b23ce7ed9e
> 
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 05/13] hooks: convert 'post-checkout' hook to hook library

From: Emily Shaffer <emilyshaffer@google.com>

Move the running of the 'post-checkout' hook away from run-command.h to the new hook.h library. For "worktree" this requires a change to it to run the hooks from a given directory.

We could strictly speaking skip the "absolute_path" flag and just check if "dir" is specified, but let's split them up for clarity, as well as for any future user who'd like to set "dir" but not implicitly change the argument to an absolute path.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/checkout.c | 14 +++++++++-----
 builtin/clone.c    |  7 +++++--
 builtin/worktree.c | 28 ++++++++++++----------------
 hook.c             | 10 ++++++++++
 hook.h             |  9 +++++++++
 read-cache.c       |  1 +
 reset.c            | 14 ++++++++++----
 7 files changed, 56 insertions(+), 27 deletions(-)
Show changes to 7 files +56 −27

builtin/checkout.c, builtin/clone.c, builtin/worktree.c, hook.c, hook.h, read-cache.c, reset.c

diff --git a/builtin/checkout.c b/builtin/checkout.c
index cbf73b8c9f6..54cbba821a8 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -9,6 +9,7 @@
 #include "config.h"
 #include "diff.h"
 #include "dir.h"
+#include "hook.h"
 #include "ll-merge.h"
 #include "lockfile.h"
 #include "merge-recursive.h"
@@ -106,13 +107,16 @@ struct branch_info {
 static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
 			      int changed)
 {
-	return run_hook_le(NULL, "post-checkout",
-			   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
-			   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
-			   changed ? "1" : "0", NULL);
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
 	/* "new_commit" can be NULL when checking out from the index before
 	   a commit exists. */
-
+	strvec_pushl(&opt.args,
+		     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
+		     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
+		     changed ? "1" : "0",
+		     NULL);
+	return run_hooks_oneshot("post-checkout", &opt);
 }
 
 static int update_some(const struct object_id *oid, struct strbuf *base,
diff --git a/builtin/clone.c b/builtin/clone.c
index 559acf9e036..53ca5ca0eb2 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -32,6 +32,7 @@
 #include "connected.h"
 #include "packfile.h"
 #include "list-objects-filter-options.h"
+#include "hook.h"
 
 /*
  * Overall FIXMEs:
@@ -659,6 +660,7 @@ static int checkout(int submodule_progress)
 	struct tree *tree;
 	struct tree_desc t;
 	int err = 0;
+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
 
 	if (option_no_checkout)
 		return 0;
@@ -705,8 +707,9 @@ static int checkout(int submodule_progress)
 	if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
 		die(_("unable to write new index file"));
 
-	err |= run_hook_le(NULL, "post-checkout", oid_to_hex(null_oid()),
-			   oid_to_hex(&oid), "1", NULL);
+	strvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid),
+		     "1", NULL);
+	err |= run_hooks_oneshot("post-checkout", &hook_opt);
 
 	if (!err && (option_recurse_submodules.nr > 0)) {
 		struct strvec args = STRVEC_INIT;
diff --git a/builtin/worktree.c b/builtin/worktree.c
index d22ece93e1a..330867c19bf 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -382,22 +382,18 @@ static int add_worktree(const char *path, const char *refname,
 	 * is_junk is cleared, but do return appropriate code when hook fails.
 	 */
 	if (!ret && opts->checkout) {
-		const char *hook = find_hook("post-checkout");
-		if (hook) {
-			const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
-			cp.git_cmd = 0;
-			cp.no_stdin = 1;
-			cp.stdout_to_stderr = 1;
-			cp.dir = path;
-			cp.env = env;
-			cp.argv = NULL;
-			cp.trace2_hook_name = "post-checkout";
-			strvec_pushl(&cp.args, absolute_path(hook),
-				     oid_to_hex(null_oid()),
-				     oid_to_hex(&commit->object.oid),
-				     "1", NULL);
-			ret = run_command(&cp);
-		}
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+		strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
+		strvec_pushl(&opt.args,
+			     oid_to_hex(null_oid()),
+			     oid_to_hex(&commit->object.oid),
+			     "1",
+			     NULL);
+		opt.dir = path;
+		opt.absolute_path = 1;
+
+		ret = run_hooks_oneshot("post-checkout", &opt);
 	}
 
 	strvec_clear(&child_env);
diff --git a/hook.c b/hook.c
index 9951015dc66..756f549e5b2 100644
--- a/hook.c
+++ b/hook.c
@@ -63,6 +63,7 @@ static int pick_next_hook(struct child_process *cp,
 	cp->env = hook_cb->options->env.v;
 	cp->stdout_to_stderr = 1;
 	cp->trace2_hook_name = hook_cb->hook_name;
+	cp->dir = hook_cb->options->dir;
 
 	strvec_push(&cp->args, hook_path);
 	strvec_pushv(&cp->args, hook_cb->options->args.v);
@@ -110,6 +111,7 @@ static int notify_hook_finished(int result,
 int run_hooks(const char *hook_name, const char *hook_path,
 	      struct run_hooks_opt *options)
 {
+	struct strbuf abs_path = STRBUF_INIT;
 	struct hook_cb_data cb_data = {
 		.rc = 0,
 		.hook_name = hook_name,
@@ -121,6 +123,11 @@ int run_hooks(const char *hook_name, const char *hook_path,
 	if (!options)
 		BUG("a struct run_hooks_opt must be provided to run_hooks");
 
+	if (options->absolute_path) {
+		strbuf_add_absolute_path(&abs_path, hook_path);
+		hook_path = abs_path.buf;
+	}
+
 	run_processes_parallel_tr2(jobs,
 				   pick_next_hook,
 				   notify_start_failure,
@@ -129,6 +136,9 @@ int run_hooks(const char *hook_name, const char *hook_path,
 				   "hook",
 				   hook_name);
 
+	if (options->absolute_path)
+		strbuf_release(&abs_path);
+
 	return cb_data.rc;
 }
 
diff --git a/hook.h b/hook.h
index 9e4d10df63e..252a605b125 100644
--- a/hook.h
+++ b/hook.h
@@ -9,6 +9,15 @@ struct run_hooks_opt
 
 	/* Args to be passed to each hook */
 	struct strvec args;
+
+	/*
+	 * Resolve and run the "absolute_path(hook)" instead of
+	 * "hook". Used for "git worktree" hooks
+	 */
+	int absolute_path;
+
+	/* Path to initial working directory for subprocess */
+	const char *dir;
 };
 
 #define RUN_HOOKS_OPT_INIT { \
diff --git a/read-cache.c b/read-cache.c
index a78b88a41bf..9773118d078 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -28,6 +28,7 @@
 #include "sparse-index.h"
 #include "csum-file.h"
 #include "promisor-remote.h"
+#include "hook.h"
 
 /* Mask for the name length in ce_flags in the on-disk index */
 
diff --git a/reset.c b/reset.c
index f214df3d96c..4234dd345db 100644
--- a/reset.c
+++ b/reset.c
@@ -7,6 +7,7 @@
 #include "tree-walk.h"
 #include "tree.h"
 #include "unpack-trees.h"
+#include "hook.h"
 
 int reset_head(struct repository *r, struct object_id *oid, const char *action,
 	       const char *switch_to_branch, unsigned flags,
@@ -126,10 +127,15 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,
 			ret = create_symref("HEAD", switch_to_branch,
 					    reflog_head);
 	}
-	if (run_hook)
-		run_hook_le(NULL, "post-checkout",
-			    oid_to_hex(orig ? orig : null_oid()),
-			    oid_to_hex(oid), "1", NULL);
+	if (run_hook) {
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+		strvec_pushl(&opt.args,
+			     oid_to_hex(orig ? orig : null_oid()),
+			     oid_to_hex(oid),
+			     "1",
+			     NULL);
+		run_hooks_oneshot("post-checkout", &opt);
+	}
 
 leave_reset_head:
 	strbuf_release(&msg);
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 16:25 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 05/13] hooks: convert 'post-checkout' hook to hook library

On Tue, Oct 12, 2021 at 03:30:30PM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 20 quoted lines
> 
> 
> Move the running of the 'post-checkout' hook away from run-command.h
> to the new hook.h library. For "worktree" this requires a change to it
> to run the hooks from a given directory.
> 
> We could strictly speaking skip the "absolute_path" flag and just
> check if "dir" is specified, but let's split them up for clarity, as
> well as for any future user who'd like to set "dir" but not implicitly
> change the argument to an absolute path.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> --- a/read-cache.c
> +++ b/read-cache.c
> @@ -28,6 +28,7 @@
>  #include "sparse-index.h"
>  #include "csum-file.h"
>  #include "promisor-remote.h"
> +#include "hook.h"
Hum, is this a stray line? I don't see any other change in read-cache.c?
Otherwise, the change looks reasonable to me.
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 03/13] rebase: convert pre-rebase to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-rebase hook away from run-command.h to and over to the new hook.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/rebase.c | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)
Show changes to builtin/rebase.c +6 −2
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 8c6393f6d78..9a44939d662 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -28,6 +28,7 @@
 #include "sequencer.h"
 #include "rebase-interactive.h"
 #include "reset.h"
+#include "hook.h"
 
 #define DEFAULT_REFLOG_ACTION "rebase"
 
@@ -1305,6 +1306,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	char *squash_onto_name = NULL;
 	int reschedule_failed_exec = -1;
 	int allow_preemptive_ff = 1;
+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
 	struct option builtin_rebase_options[] = {
 		OPT_STRING(0, "onto", &options.onto_name,
 			   N_("revision"),
@@ -2021,9 +2023,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	}
 
 	/* If a hook exists, give it a chance to interrupt*/
+	strvec_push(&hook_opt.args, options.upstream_arg);
+	if (argc)
+		strvec_push(&hook_opt.args, argv[0]);
 	if (!ok_to_skip_pre_rebase &&
-	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
-			argc ? argv[0] : NULL, NULL))
+	    run_hooks_oneshot("pre-rebase", &hook_opt))
 		die(_("The pre-rebase hook refused to rebase."));
 
 	if (options.flags & REBASE_DIFFSTAT) {
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 16:25 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 03/13] rebase: convert pre-rebase to use hook.h

On Tue, Oct 12, 2021 at 03:30:28PM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 7 quoted lines
> 
> 
> Move the pre-rebase hook away from run-command.h to and over to the
> new hook.h library.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Show 41 quoted lines
> ---
>  builtin/rebase.c | 8 ++++++--
>  1 file changed, 6 insertions(+), 2 deletions(-)
> 
> diff --git a/builtin/rebase.c b/builtin/rebase.c
> index 8c6393f6d78..9a44939d662 100644
> --- a/builtin/rebase.c
> +++ b/builtin/rebase.c
> @@ -28,6 +28,7 @@
>  #include "sequencer.h"
>  #include "rebase-interactive.h"
>  #include "reset.h"
> +#include "hook.h"
>  
>  #define DEFAULT_REFLOG_ACTION "rebase"
>  
> @@ -1305,6 +1306,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
>  	char *squash_onto_name = NULL;
>  	int reschedule_failed_exec = -1;
>  	int allow_preemptive_ff = 1;
> +	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
>  	struct option builtin_rebase_options[] = {
>  		OPT_STRING(0, "onto", &options.onto_name,
>  			   N_("revision"),
> @@ -2021,9 +2023,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
>  	}
>  
>  	/* If a hook exists, give it a chance to interrupt*/
> +	strvec_push(&hook_opt.args, options.upstream_arg);
> +	if (argc)
> +		strvec_push(&hook_opt.args, argv[0]);
>  	if (!ok_to_skip_pre_rebase &&
> -	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
> -			argc ? argv[0] : NULL, NULL))
> +	    run_hooks_oneshot("pre-rebase", &hook_opt))
>  		die(_("The pre-rebase hook refused to rebase."));
>  
>  	if (options.flags & REBASE_DIFFSTAT) {
> -- 
> 2.33.0.1567.g7b23ce7ed9e
> 
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 06/13] merge: convert post-merge to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach post-merge to use the hook.h library instead of the run-command.h library to run hooks.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/merge.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)
Show changes to builtin/merge.c +3 −1
diff --git a/builtin/merge.c b/builtin/merge.c
index cc4a910c69b..c48b875b82b 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -448,6 +448,7 @@ static void finish(struct commit *head_commit,
 		   const struct object_id *new_head, const char *msg)
 {
 	struct strbuf reflog_message = STRBUF_INIT;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 	const struct object_id *head = &head_commit->object.oid;
 
 	if (!msg)
@@ -488,7 +489,8 @@ static void finish(struct commit *head_commit,
 	}
 
 	/* Run a post-merge hook */
-	run_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL);
+	strvec_push(&opt.args, squash ? "1" : "0");
+	run_hooks_oneshot("post-merge", &opt);
 
 	apply_autostash(git_path_merge_autostash(the_repository));
 	strbuf_release(&reflog_message);
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 16:26 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 06/13] merge: convert post-merge to use hook.h

On Tue, Oct 12, 2021 at 03:30:31PM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 7 quoted lines
> 
> 
> Teach post-merge to use the hook.h library instead of the
> run-command.h library to run hooks.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 07/13] git hook run: add an --ignore-missing flag

For certain one-shot hooks we'd like to optimistically run them, and not complain if they don't exist. This will be used by send-email in a subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Documentation/git-hook.txt | 10 +++++++++-
 builtin/hook.c             |  7 ++++++-
 t/t1800-hook.sh            |  5 +++++
 3 files changed, 20 insertions(+), 2 deletions(-)
Show changes to 3 files +20 −2

Documentation/git-hook.txt, builtin/hook.c, t/t1800-hook.sh

diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index e39b1b5d069..77c3a8ad909 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -8,7 +8,7 @@ git-hook - Run git hooks
 SYNOPSIS
 --------
 [verse]
-'git hook' run <hook-name> [-- <hook-args>]
+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]
 
 DESCRIPTION
 -----------
@@ -28,6 +28,14 @@ Any positional arguments to the hook should be passed after a
 mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
 linkgit:githooks[5] for arguments hooks might expect (if any).
 
+OPTIONS
+-------
+
+--ignore-missing::
+	Ignore any missing hook by quietly returning zero. Used for
+	tools that want to do a blind one-shot run of a hook that may
+	or may not be present.
+
 SEE ALSO
 --------
 linkgit:githooks[5]
diff --git a/builtin/hook.c b/builtin/hook.c
index 41dd15550cf..fa26454990d 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -7,7 +7,7 @@
 #include "strvec.h"
 
 #define BUILTIN_HOOK_RUN_USAGE \
-	N_("git hook run <hook-name> [-- <hook-args>]")
+	N_("git hook run [--ignore-missing] <hook-name> [-- <hook-args>]")
 
 static const char * const builtin_hook_usage[] = {
 	BUILTIN_HOOK_RUN_USAGE,
@@ -23,9 +23,12 @@ static int run(int argc, const char **argv, const char *prefix)
 {
 	int i;
 	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	int ignore_missing = 0;
 	const char *hook_name;
 	const char *hook_path;
 	struct option run_options[] = {
+		OPT_BOOL(0, "ignore-missing", &ignore_missing,
+			 N_("silently ignore missing requested <hook-name>")),
 		OPT_END(),
 	};
 	int ret;
@@ -53,6 +56,8 @@ static int run(int argc, const char **argv, const char *prefix)
 	git_config(git_default_config, NULL);
 
 	hook_name = argv[0];
+	if (ignore_missing)
+		return run_hooks_oneshot(hook_name, &opt);
 	hook_path = find_hook(hook_name);
 	if (!hook_path) {
 		error("cannot find a hook named %s", hook_name);
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 3aea1b105f0..29718aa9913 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -21,6 +21,11 @@ test_expect_success 'git hook run: nonexistent hook' '
 	test_cmp stderr.expect stderr.actual
 '
 
+test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '
+	git hook run --ignore-missing does-not-exist 2>stderr.actual &&
+	test_must_be_empty stderr.actual
+'
+
 test_expect_success 'git hook run: basic' '
 	write_script .git/hooks/test-hook <<-EOF &&
 	echo Test hook
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 16:26 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 07/13] git hook run: add an --ignore-missing flag

On Tue, Oct 12, 2021 at 03:30:32PM +0200, Ævar Arnfjörð Bjarmason wrote:
> 
> For certain one-shot hooks we'd like to optimistically run them, and
> not complain if they don't exist. This will be used by send-email in a
> subsequent commit.

Aha, this is what I was looking for in the earlier commit. Feel free to disregard those complaints. ;)

> 
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 08/13] send-email: use 'git hook run' for 'sendemail-validate'

From: Emily Shaffer <emilyshaffer@google.com>

Change the "sendmail-validate" hook to be run via the "git hook run" wrapper instead of via a direct invocation.

This is the smallest possibly change to get "send-email" using "git hook run". We still check the hook itself with "-x", and set a "GIT_DIR" variable, both of which are asserted by our tests. We'll need to get rid of this special behavior if we start running N hooks, but for now let's be as close to bug-for-bug compatible as possible.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl   | 22 ++++++++++++++--------
 t/t9001-send-email.sh |  4 ++--
 2 files changed, 16 insertions(+), 10 deletions(-)
Show changes to 2 files +16 −10

git-send-email.perl, t/t9001-send-email.sh

diff --git a/git-send-email.perl b/git-send-email.perl
index 5262d88ee32..4c20c0bbb79 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -195,13 +195,13 @@ sub format_2822_time {
 my $editor;
 
 sub system_or_msg {
-	my ($args, $msg) = @_;
+	my ($args, $msg, $cmd_name) = @_;
 	system(@$args);
 	my $signalled = $? & 127;
 	my $exit_code = $? >> 8;
 	return unless $signalled or $exit_code;
 
-	my @sprintf_args = ($args->[0], $exit_code);
+	my @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);
 	if (defined $msg) {
 		# Quiet the 'redundant' warning category, except we
 		# need to support down to Perl 5.8, so we can't do a
@@ -2039,10 +2039,10 @@ sub validate_patch {
 	my ($fn, $xfer_encoding) = @_;
 
 	if ($repo) {
+		my $hook_name = 'sendemail-validate';
 		my $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');
 		require File::Spec;
-		my $validate_hook = File::Spec->catfile($hooks_path,
-					    'sendemail-validate');
+		my $validate_hook = File::Spec->catfile($hooks_path, $hook_name);
 		my $hook_error;
 		if (-x $validate_hook) {
 			require Cwd;
@@ -2052,13 +2052,19 @@ sub validate_patch {
 			chdir($repo->wc_path() or $repo->repo_path())
 				or die("chdir: $!");
 			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			$hook_error = system_or_msg([$validate_hook, $target]);
+			my @cmd = ("git", "hook", "run", "--ignore-missing",
+				    $hook_name, "--");
+			my @cmd_msg = (@cmd, "<patch>");
+			my @cmd_run = (@cmd, $target);
+			$hook_error = system_or_msg(\@cmd_run, undef, "@cmd_msg");
 			chdir($cwd_save) or die("chdir: $!");
 		}
 		if ($hook_error) {
-			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
-				       "%s\n" .
-				       "warning: no patches were sent\n"), $fn, $hook_error);
+			$hook_error = sprintf(__("fatal: %s: rejected by %s hook\n" .
+						 $hook_error . "\n" .
+						 "warning: no patches were sent\n"),
+					      $fn, $hook_name);
+			die $hook_error;
 		}
 	}
 
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index aa0c20499ba..84d0f40d76a 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -539,7 +539,7 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'my-hooks/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
@@ -558,7 +558,7 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 17:07 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 08/13] send-email: use 'git hook run' for 'sendemail-validate'

On Fri, Oct 15, 2021 at 11:43:36AM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 13 quoted lines
> 
> 
> Change the "sendmail-validate" hook to be run via the "git hook run"
> wrapper instead of via a direct invocation.
> 
> This is the smallest possibly change to get "send-email" using "git
> hook run". We still check the hook itself with "-x", and set a
> "GIT_DIR" variable, both of which are asserted by our tests. We'll
> need to get rid of this special behavior if we start running N hooks,
> but for now let's be as close to bug-for-bug compatible as possible.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>

Someday we can delete a bunch more in there and on that day we can celebrate. But the rationale for making the change minimal LGTM.

Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Show 87 quoted lines
> ---
>  git-send-email.perl   | 22 ++++++++++++++--------
>  t/t9001-send-email.sh |  4 ++--
>  2 files changed, 16 insertions(+), 10 deletions(-)
> 
> diff --git a/git-send-email.perl b/git-send-email.perl
> index 5262d88ee32..4c20c0bbb79 100755
> --- a/git-send-email.perl
> +++ b/git-send-email.perl
> @@ -195,13 +195,13 @@ sub format_2822_time {
>  my $editor;
>  
>  sub system_or_msg {
> -	my ($args, $msg) = @_;
> +	my ($args, $msg, $cmd_name) = @_;
>  	system(@$args);
>  	my $signalled = $? & 127;
>  	my $exit_code = $? >> 8;
>  	return unless $signalled or $exit_code;
>  
> -	my @sprintf_args = ($args->[0], $exit_code);
> +	my @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);
>  	if (defined $msg) {
>  		# Quiet the 'redundant' warning category, except we
>  		# need to support down to Perl 5.8, so we can't do a
> @@ -2039,10 +2039,10 @@ sub validate_patch {
>  	my ($fn, $xfer_encoding) = @_;
>  
>  	if ($repo) {
> +		my $hook_name = 'sendemail-validate';
>  		my $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');
>  		require File::Spec;
> -		my $validate_hook = File::Spec->catfile($hooks_path,
> -					    'sendemail-validate');
> +		my $validate_hook = File::Spec->catfile($hooks_path, $hook_name);
>  		my $hook_error;
>  		if (-x $validate_hook) {
>  			require Cwd;
> @@ -2052,13 +2052,19 @@ sub validate_patch {
>  			chdir($repo->wc_path() or $repo->repo_path())
>  				or die("chdir: $!");
>  			local $ENV{"GIT_DIR"} = $repo->repo_path();
> -			$hook_error = system_or_msg([$validate_hook, $target]);
> +			my @cmd = ("git", "hook", "run", "--ignore-missing",
> +				    $hook_name, "--");
> +			my @cmd_msg = (@cmd, "<patch>");
> +			my @cmd_run = (@cmd, $target);
> +			$hook_error = system_or_msg(\@cmd_run, undef, "@cmd_msg");
>  			chdir($cwd_save) or die("chdir: $!");
>  		}
>  		if ($hook_error) {
> -			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
> -				       "%s\n" .
> -				       "warning: no patches were sent\n"), $fn, $hook_error);
> +			$hook_error = sprintf(__("fatal: %s: rejected by %s hook\n" .
> +						 $hook_error . "\n" .
> +						 "warning: no patches were sent\n"),
> +					      $fn, $hook_name);
> +			die $hook_error;
>  		}
>  	}
>  
> diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
> index aa0c20499ba..84d0f40d76a 100755
> --- a/t/t9001-send-email.sh
> +++ b/t/t9001-send-email.sh
> @@ -539,7 +539,7 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
>  	test_path_is_file my-hooks.ran &&
>  	cat >expect <<-EOF &&
>  	fatal: longline.patch: rejected by sendemail-validate hook
> -	fatal: command '"'"'my-hooks/sendemail-validate'"'"' died with exit code 1
> +	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
>  	warning: no patches were sent
>  	EOF
>  	test_cmp expect actual
> @@ -558,7 +558,7 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
>  	test_path_is_file my-hooks.ran &&
>  	cat >expect <<-EOF &&
>  	fatal: longline.patch: rejected by sendemail-validate hook
> -	fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1
> +	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
>  	warning: no patches were sent
>  	EOF
>  	test_cmp expect actual
> -- 
> 2.33.1.1338.g20da966911a
> 
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 09/13] git-p4: use 'git hook' to run hooks

From: Emily Shaffer <emilyshaffer@google.com>

Instead of duplicating the behavior of run-command.h:run_hook_le() in Python, we can directly call 'git hook run'. We emulate the existence check with the --ignore-missing flag.

As this is the last hook execution in git.git to not go through "git hook run" or the hook.[ch] library we can now be absolutely sure that our assertion in hook.c that only hooks known by the generated (from githooks(5)) hook-list.h are permitted.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-p4.py | 72 ++++++-------------------------------------------------
 1 file changed, 7 insertions(+), 65 deletions(-)
Show changes to git-p4.py +7 −65
diff --git a/git-p4.py b/git-p4.py
index 2b4500226aa..1f24cbf0bca 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -207,71 +207,13 @@ def decode_path(path):
         return path
 
 def run_git_hook(cmd, param=[]):
-    """Execute a hook if the hook exists."""
-    if verbose:
-        sys.stderr.write("Looking for hook: %s\n" % cmd)
-        sys.stderr.flush()
-
-    hooks_path = gitConfig("core.hooksPath")
-    if len(hooks_path) <= 0:
-        hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
-
-    if not isinstance(param, list):
-        param=[param]
-
-    # resolve hook file name, OS depdenent
-    hook_file = os.path.join(hooks_path, cmd)
-    if platform.system() == 'Windows':
-        if not os.path.isfile(hook_file):
-            # look for the file with an extension
-            files = glob.glob(hook_file + ".*")
-            if not files:
-                return True
-            files.sort()
-            hook_file = files.pop()
-            while hook_file.upper().endswith(".SAMPLE"):
-                # The file is a sample hook. We don't want it
-                if len(files) > 0:
-                    hook_file = files.pop()
-                else:
-                    return True
-
-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
-        return True
-
-    return run_hook_command(hook_file, param) == 0
-
-def run_hook_command(cmd, param):
-    """Executes a git hook command
-       cmd = the command line file to be executed. This can be
-       a file that is run by OS association.
-
-       param = a list of parameters to pass to the cmd command
-
-       On windows, the extension is checked to see if it should
-       be run with the Git for Windows Bash shell.  If there
-       is no file extension, the file is deemed a bash shell
-       and will be handed off to sh.exe. Otherwise, Windows
-       will be called with the shell to handle the file assocation.
-
-       For non Windows operating systems, the file is called
-       as an executable.
-    """
-    cli = [cmd] + param
-    use_shell = False
-    if platform.system() == 'Windows':
-        (root,ext) = os.path.splitext(cmd)
-        if ext == "":
-            exe_path = os.environ.get("EXEPATH")
-            if exe_path is None:
-                exe_path = ""
-            else:
-                exe_path = os.path.join(exe_path, "bin")
-            cli = [os.path.join(exe_path, "SH.EXE")] + cli
-        else:
-            use_shell = True
-    return subprocess.call(cli, shell=use_shell)
-
+    """args are specified with -a <arg> -a <arg> -a <arg>"""
+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]
+    if param:
+        args.append("--")
+        for p in param:
+            args.append(p)
+    return subprocess.call(args) == 0
 
 def write_pipe(c, stdin):
     if verbose:
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 17:08 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 09/13] git-p4: use 'git hook' to run hooks

On Fri, Oct 15, 2021 at 11:43:37AM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 91 quoted lines
> 
> 
> Instead of duplicating the behavior of run-command.h:run_hook_le() in
> Python, we can directly call 'git hook run'. We emulate the existence
> check with the --ignore-missing flag.
> 
> As this is the last hook execution in git.git to not go through "git
> hook run" or the hook.[ch] library we can now be absolutely sure that
> our assertion in hook.c that only hooks known by the generated (from
> githooks(5)) hook-list.h are permitted.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  git-p4.py | 72 ++++++-------------------------------------------------
>  1 file changed, 7 insertions(+), 65 deletions(-)
> 
> diff --git a/git-p4.py b/git-p4.py
> index 2b4500226aa..1f24cbf0bca 100755
> --- a/git-p4.py
> +++ b/git-p4.py
> @@ -207,71 +207,13 @@ def decode_path(path):
>          return path
>  
>  def run_git_hook(cmd, param=[]):
> -    """Execute a hook if the hook exists."""
> -    if verbose:
> -        sys.stderr.write("Looking for hook: %s\n" % cmd)
> -        sys.stderr.flush()
> -
> -    hooks_path = gitConfig("core.hooksPath")
> -    if len(hooks_path) <= 0:
> -        hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
> -
> -    if not isinstance(param, list):
> -        param=[param]
> -
> -    # resolve hook file name, OS depdenent
> -    hook_file = os.path.join(hooks_path, cmd)
> -    if platform.system() == 'Windows':
> -        if not os.path.isfile(hook_file):
> -            # look for the file with an extension
> -            files = glob.glob(hook_file + ".*")
> -            if not files:
> -                return True
> -            files.sort()
> -            hook_file = files.pop()
> -            while hook_file.upper().endswith(".SAMPLE"):
> -                # The file is a sample hook. We don't want it
> -                if len(files) > 0:
> -                    hook_file = files.pop()
> -                else:
> -                    return True
> -
> -    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
> -        return True
> -
> -    return run_hook_command(hook_file, param) == 0
> -
> -def run_hook_command(cmd, param):
> -    """Executes a git hook command
> -       cmd = the command line file to be executed. This can be
> -       a file that is run by OS association.
> -
> -       param = a list of parameters to pass to the cmd command
> -
> -       On windows, the extension is checked to see if it should
> -       be run with the Git for Windows Bash shell.  If there
> -       is no file extension, the file is deemed a bash shell
> -       and will be handed off to sh.exe. Otherwise, Windows
> -       will be called with the shell to handle the file assocation.
> -
> -       For non Windows operating systems, the file is called
> -       as an executable.
> -    """
> -    cli = [cmd] + param
> -    use_shell = False
> -    if platform.system() == 'Windows':
> -        (root,ext) = os.path.splitext(cmd)
> -        if ext == "":
> -            exe_path = os.environ.get("EXEPATH")
> -            if exe_path is None:
> -                exe_path = ""
> -            else:
> -                exe_path = os.path.join(exe_path, "bin")
> -            cli = [os.path.join(exe_path, "SH.EXE")] + cli
> -        else:
> -            use_shell = True
> -    return subprocess.call(cli, shell=use_shell)
> -
> +    """args are specified with -a <arg> -a <arg> -a <arg>"""
Ah, the comment is misleading now, since we're adding args after the --.
Show 12 quoted lines
> +    args = ['git', 'hook', 'run', '--ignore-missing', cmd]
> +    if param:
> +        args.append("--")
> +        for p in param:
> +            args.append(p)
> +    return subprocess.call(args) == 0
>  
>  def write_pipe(c, stdin):
>      if verbose:
> -- 
> 2.33.1.1338.g20da966911a
> 
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 10/13] commit: convert {pre-commit,prepare-commit-msg} hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move these hooks hook away from run-command.h to and over to the new hook.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 commit.c | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)
Show changes to commit.c +8 −7
diff --git a/commit.c b/commit.c
index 551de4903c1..4e7cbd7585d 100644
--- a/commit.c
+++ b/commit.c
@@ -21,6 +21,7 @@
 #include "commit-reach.h"
 #include "run-command.h"
 #include "shallow.h"
+#include "hook.h"
 
 static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
 
@@ -1700,22 +1701,22 @@ size_t ignore_non_trailer(const char *buf, size_t len)
 int run_commit_hook(int editor_is_used, const char *index_file,
 		    const char *name, ...)
 {
-	struct strvec hook_env = STRVEC_INIT;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 	va_list args;
-	int ret;
+	const char *arg;
 
-	strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
+	strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s", index_file);
 
 	/*
 	 * Let the hook know that no editor will be launched.
 	 */
 	if (!editor_is_used)
-		strvec_push(&hook_env, "GIT_EDITOR=:");
+		strvec_push(&opt.env, "GIT_EDITOR=:");
 
 	va_start(args, name);
-	ret = run_hook_ve(hook_env.v, name, args);
+	while ((arg = va_arg(args, const char *)))
+		strvec_push(&opt.args, arg);
 	va_end(args);
-	strvec_clear(&hook_env);
 
-	return ret;
+	return run_hooks_oneshot(name, &opt);
 }
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 17:15 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 10/13] commit: convert {pre-commit,prepare-commit-msg} hook to hook.h

On Fri, Oct 15, 2021 at 11:43:38AM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 7 quoted lines
> 
> 
> Move these hooks hook away from run-command.h to and over to the new
> hook.h library.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Very straightforward.
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Show 49 quoted lines
> ---
>  commit.c | 15 ++++++++-------
>  1 file changed, 8 insertions(+), 7 deletions(-)
> 
> diff --git a/commit.c b/commit.c
> index 551de4903c1..4e7cbd7585d 100644
> --- a/commit.c
> +++ b/commit.c
> @@ -21,6 +21,7 @@
>  #include "commit-reach.h"
>  #include "run-command.h"
>  #include "shallow.h"
> +#include "hook.h"
>  
>  static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
>  
> @@ -1700,22 +1701,22 @@ size_t ignore_non_trailer(const char *buf, size_t len)
>  int run_commit_hook(int editor_is_used, const char *index_file,
>  		    const char *name, ...)
>  {
> -	struct strvec hook_env = STRVEC_INIT;
> +	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
>  	va_list args;
> -	int ret;
> +	const char *arg;
>  
> -	strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
> +	strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s", index_file);
>  
>  	/*
>  	 * Let the hook know that no editor will be launched.
>  	 */
>  	if (!editor_is_used)
> -		strvec_push(&hook_env, "GIT_EDITOR=:");
> +		strvec_push(&opt.env, "GIT_EDITOR=:");
>  
>  	va_start(args, name);
> -	ret = run_hook_ve(hook_env.v, name, args);
> +	while ((arg = va_arg(args, const char *)))
> +		strvec_push(&opt.args, arg);
>  	va_end(args);
> -	strvec_clear(&hook_env);
>  
> -	return ret;
> +	return run_hooks_oneshot(name, &opt);
>  }
> -- 
> 2.33.1.1338.g20da966911a
> 
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 11/13] read-cache: convert post-index-change to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the post-index-change hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of "run_hook_ve()" outside of run-command.c ("run_hook_le()" still uses it). So we can make the function static now. A subsequent commit will remove this code entirely when "run_hook_le()" itself goes away.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 read-cache.c  | 10 +++++++---
 run-command.c |  2 +-
 run-command.h |  1 -
 3 files changed, 8 insertions(+), 5 deletions(-)
Show changes to 3 files +8 −5

read-cache.c, run-command.c, run-command.h

diff --git a/read-cache.c b/read-cache.c
index 9773118d078..84cc3d88196 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -3087,6 +3087,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
 {
 	int ret;
 	int was_full = !istate->sparse_index;
+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
 
 	ret = convert_to_sparse(istate, 0);
 
@@ -3115,9 +3116,12 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
 	else
 		ret = close_lock_file_gently(lock);
 
-	run_hook_le(NULL, "post-index-change",
-			istate->updated_workdir ? "1" : "0",
-			istate->updated_skipworktree ? "1" : "0", NULL);
+	strvec_pushl(&hook_opt.args,
+		     istate->updated_workdir ? "1" : "0",
+		     istate->updated_skipworktree ? "1" : "0",
+		     NULL);
+	run_hooks_oneshot("post-index-change", &hook_opt);
+
 	istate->updated_workdir = 0;
 	istate->updated_skipworktree = 0;
 
diff --git a/run-command.c b/run-command.c
index 7ef5cc712a9..d92e670c8ed 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1323,7 +1323,7 @@ int async_with_fork(void)
 #endif
 }
 
-int run_hook_ve(const char *const *env, const char *name, va_list args)
+static int run_hook_ve(const char *const *env, const char *name, va_list args)
 {
 	struct child_process hook = CHILD_PROCESS_INIT;
 	const char *p;
diff --git a/run-command.h b/run-command.h
index 49878262584..3fa7454cf8a 100644
--- a/run-command.h
+++ b/run-command.h
@@ -239,7 +239,6 @@ int run_command(struct child_process *);
  */
 LAST_ARG_MUST_BE_NULL
 int run_hook_le(const char *const *env, const char *name, ...);
-int run_hook_ve(const char *const *env, const char *name, va_list args);
 
 /*
  * Trigger an auto-gc
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 17:17 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 11/13] read-cache: convert post-index-change to use hook.h

On Fri, Oct 15, 2021 at 11:43:39AM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 9 quoted lines
> 
> 
> Move the post-index-change hook away from run-command.h to and over to
> the new hook.h library.
> 
> This removes the last direct user of "run_hook_ve()" outside of
> run-command.c ("run_hook_le()" still uses it). So we can make the
> function static now. A subsequent commit will remove this code
> entirely when "run_hook_le()" itself goes away.
\o/
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Show 62 quoted lines
> ---
>  read-cache.c  | 10 +++++++---
>  run-command.c |  2 +-
>  run-command.h |  1 -
>  3 files changed, 8 insertions(+), 5 deletions(-)
> 
> diff --git a/read-cache.c b/read-cache.c
> index 9773118d078..84cc3d88196 100644
> --- a/read-cache.c
> +++ b/read-cache.c
> @@ -3087,6 +3087,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
>  {
>  	int ret;
>  	int was_full = !istate->sparse_index;
> +	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
>  
>  	ret = convert_to_sparse(istate, 0);
>  
> @@ -3115,9 +3116,12 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
>  	else
>  		ret = close_lock_file_gently(lock);
>  
> -	run_hook_le(NULL, "post-index-change",
> -			istate->updated_workdir ? "1" : "0",
> -			istate->updated_skipworktree ? "1" : "0", NULL);
> +	strvec_pushl(&hook_opt.args,
> +		     istate->updated_workdir ? "1" : "0",
> +		     istate->updated_skipworktree ? "1" : "0",
> +		     NULL);
> +	run_hooks_oneshot("post-index-change", &hook_opt);
> +
>  	istate->updated_workdir = 0;
>  	istate->updated_skipworktree = 0;
>  
> diff --git a/run-command.c b/run-command.c
> index 7ef5cc712a9..d92e670c8ed 100644
> --- a/run-command.c
> +++ b/run-command.c
> @@ -1323,7 +1323,7 @@ int async_with_fork(void)
>  #endif
>  }
>  
> -int run_hook_ve(const char *const *env, const char *name, va_list args)
> +static int run_hook_ve(const char *const *env, const char *name, va_list args)
>  {
>  	struct child_process hook = CHILD_PROCESS_INIT;
>  	const char *p;
> diff --git a/run-command.h b/run-command.h
> index 49878262584..3fa7454cf8a 100644
> --- a/run-command.h
> +++ b/run-command.h
> @@ -239,7 +239,6 @@ int run_command(struct child_process *);
>   */
>  LAST_ARG_MUST_BE_NULL
>  int run_hook_le(const char *const *env, const char *name, ...);
> -int run_hook_ve(const char *const *env, const char *name, va_list args);
>  
>  /*
>   * Trigger an auto-gc
> -- 
> 2.33.1.1338.g20da966911a
> 
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 12/13] receive-pack: convert push-to-checkout hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the push-to-checkout hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of run_hook_le(), so we could remove that function now, but let's leave that to a follow-up cleanup commit.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/receive-pack.c | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)
Show changes to builtin/receive-pack.c +5 −2
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 25cc0c907e1..22e7a431728 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1435,9 +1435,12 @@ static const char *push_to_checkout(unsigned char *hash,
 				    struct strvec *env,
 				    const char *work_tree)
 {
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
 	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
-	if (run_hook_le(env->v, push_to_checkout_hook,
-			hash_to_hex(hash), NULL))
+	strvec_pushv(&opt.env, env->v);
+	strvec_push(&opt.args, hash_to_hex(hash));
+	if (run_hooks_oneshot(push_to_checkout_hook, &opt))
 		return "push-to-checkout hook declined";
 	else
 		return NULL;
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 15, 2021, 09:43 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v2 13/13] run-command: remove old run_hook_{le,ve}() hook API

From: Emily Shaffer <emilyshaffer@google.com>

The new hook.h library has replaced all run-command.h hook-related functionality. So let's delete this dead code.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 run-command.c | 32 --------------------------------
 run-command.h | 16 ----------------
 2 files changed, 48 deletions(-)
Show changes to 2 files +0 −48

run-command.c, run-command.h

diff --git a/run-command.c b/run-command.c
index d92e670c8ed..8a21ff525f3 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1323,38 +1323,6 @@ int async_with_fork(void)
 #endif
 }
 
-static int run_hook_ve(const char *const *env, const char *name, va_list args)
-{
-	struct child_process hook = CHILD_PROCESS_INIT;
-	const char *p;
-
-	p = find_hook(name);
-	if (!p)
-		return 0;
-
-	strvec_push(&hook.args, p);
-	while ((p = va_arg(args, const char *)))
-		strvec_push(&hook.args, p);
-	hook.env = env;
-	hook.no_stdin = 1;
-	hook.stdout_to_stderr = 1;
-	hook.trace2_hook_name = name;
-
-	return run_command(&hook);
-}
-
-int run_hook_le(const char *const *env, const char *name, ...)
-{
-	va_list args;
-	int ret;
-
-	va_start(args, name);
-	ret = run_hook_ve(env, name, args);
-	va_end(args);
-
-	return ret;
-}
-
 struct io_pump {
 	/* initialized by caller */
 	int fd;
diff --git a/run-command.h b/run-command.h
index 3fa7454cf8a..59e1fbff64c 100644
--- a/run-command.h
+++ b/run-command.h
@@ -224,22 +224,6 @@ int finish_command_in_signal(struct child_process *);
  */
 int run_command(struct child_process *);
 
-/**
- * Run a hook.
- * The first argument is a pathname to an index file, or NULL
- * if the hook uses the default index file or no index is needed.
- * The second argument is the name of the hook.
- * The further arguments correspond to the hook arguments.
- * The last argument has to be NULL to terminate the arguments list.
- * If the hook does not exist or is not executable, the return
- * value will be zero.
- * If it is executable, the hook will be executed and the exit
- * status of the hook is returned.
- * On execution, .stdout_to_stderr and .no_stdin will be set.
- */
-LAST_ARG_MUST_BE_NULL
-int run_hook_le(const char *const *env, const char *name, ...);
-
 /*
  * Trigger an auto-gc
  */
-- 
2.33.1.1338.g20da966911a
Emily Shaffer· Oct 15, 2021, 17:18 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 13/13] run-command: remove old run_hook_{le,ve}() hook API

On Fri, Oct 15, 2021 at 11:43:41AM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 7 quoted lines
> 
> 
> The new hook.h library has replaced all run-command.h hook-related
> functionality. So let's delete this dead code.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Thanks.
Show 78 quoted lines
> ---
>  run-command.c | 32 --------------------------------
>  run-command.h | 16 ----------------
>  2 files changed, 48 deletions(-)
> 
> diff --git a/run-command.c b/run-command.c
> index d92e670c8ed..8a21ff525f3 100644
> --- a/run-command.c
> +++ b/run-command.c
> @@ -1323,38 +1323,6 @@ int async_with_fork(void)
>  #endif
>  }
>  
> -static int run_hook_ve(const char *const *env, const char *name, va_list args)
> -{
> -	struct child_process hook = CHILD_PROCESS_INIT;
> -	const char *p;
> -
> -	p = find_hook(name);
> -	if (!p)
> -		return 0;
> -
> -	strvec_push(&hook.args, p);
> -	while ((p = va_arg(args, const char *)))
> -		strvec_push(&hook.args, p);
> -	hook.env = env;
> -	hook.no_stdin = 1;
> -	hook.stdout_to_stderr = 1;
> -	hook.trace2_hook_name = name;
> -
> -	return run_command(&hook);
> -}
> -
> -int run_hook_le(const char *const *env, const char *name, ...)
> -{
> -	va_list args;
> -	int ret;
> -
> -	va_start(args, name);
> -	ret = run_hook_ve(env, name, args);
> -	va_end(args);
> -
> -	return ret;
> -}
> -
>  struct io_pump {
>  	/* initialized by caller */
>  	int fd;
> diff --git a/run-command.h b/run-command.h
> index 3fa7454cf8a..59e1fbff64c 100644
> --- a/run-command.h
> +++ b/run-command.h
> @@ -224,22 +224,6 @@ int finish_command_in_signal(struct child_process *);
>   */
>  int run_command(struct child_process *);
>  
> -/**
> - * Run a hook.
> - * The first argument is a pathname to an index file, or NULL
> - * if the hook uses the default index file or no index is needed.
> - * The second argument is the name of the hook.
> - * The further arguments correspond to the hook arguments.
> - * The last argument has to be NULL to terminate the arguments list.
> - * If the hook does not exist or is not executable, the return
> - * value will be zero.
> - * If it is executable, the hook will be executed and the exit
> - * status of the hook is returned.
> - * On execution, .stdout_to_stderr and .no_stdin will be set.
> - */
> -LAST_ARG_MUST_BE_NULL
> -int run_hook_le(const char *const *env, const char *name, ...);
> -
>  /*
>   * Trigger an auto-gc
>   */
> -- 
> 2.33.1.1338.g20da966911a
> 
Emily Shaffer· Oct 15, 2021, 16:23 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 00/13] hook.[ch]: new library to run hooks + simple hook conversion

On Tue, Oct 12, 2021 at 03:30:25PM +0200, Ævar Arnfjörð Bjarmason wrote:
Show 47 quoted lines
> 
> This series is part 2 of an incremental restart of the greater
> "config-based hooks" topic by Emily Shaffer that I've been helping
> with. See [1] for part 1, and [2] (search for "mark2") for a summary
> of parts to come.
> 
> This goes on top of the "ab/config-based-hooks-1" topic that's already
> in "next" and marked for graduation to "master".
> 
> In this topic we build upon the skeleton hook.[ch] library and build
> infrastructure added in part 1 and add a hook running library and new
> "git hook" command.
> 
> The new "git hook" command is (for now) only for the benefit of
> in-tree non-C programs that need to run hooks, and this series
> converts git-send-email and git-p4 to use it.
> 
> At the end of the series we remove
> "run_hook_{le,ve}()" from run-command.c, as we've migrated all its
> callers.
> 
> What we don't do is convert any of the more complex in-tree hooks that
> require input on stdin such as "pre-receive" and the like, that's in
> later parts once this lands.
> 
> This series is approximately patch 6-20/36 of the previous 36 patch
> ab/config-based-hooks-base topic. A range-diff to that v5[3] is
> included below.
> 
> The changes since that v5 are rather trivial, they are:
> 
>  * Formatting changes to reduce the diff to parts that come after this
>    (which Emily & I were juggling at the time), and re-flowing some
>    overly long lines.
> 
>  * The new test is now marked for SANITIZE=leak testing, with the new
>    test facility I've added recently (and which just landed in
>    "master"). They all pass, the new API doesn't leak.
> 
>  * I rewrote some of the git-send-email.perl code to avoid
>    de-duplication and hardcoding (just using intermediate variables).
> 
> Things to focus on reviewing:
> 
>  * This should all be pretty solid and well tested, but the git-p4.py
>    part in particular I've never tested for real (not having access to
>    p4), and think Emily hasn't either[4].

Correct. I seem to remember wanting to give this a test and not having access to real perforce to try it against.

Show 15 quoted lines
> 
>    The relevant patch looks trivially correct to me[5], and I've tried it
>    out in the Python REPL. But if any of the CC'd people active in
>    git-p4.py development could give it some end-to-end testing that
>    would be much appreciated.
> 
>  * Both Python and Perl code now calls the in-between "git hook run"
>    command rather than calling hooks directly. Will this behave
>    differently due to any special behavior running via a git built-in
>    adds?
> 
>    I vaguely recall a third-party "git-foo" program breaking in the
>    past when invoked as "git foo" but not "git-foo", due to git
>    squatting on SIGINT, but none of that should be relevant here
>    (we're not starting a pager etc.).

I would be very surprised, assuming that we invoke 'git hook' in the same way that those Python and Perl scripts invoke other 'git whatever' commands...

That said, once we invoke a user hook, all bets are off as far as "we're not starting a pager or anything". A determined user could find a reason to invoke a pager, editor, whatever during a hook. I'm curious to know more about this remembered breakage.

Last time I looked at the range-diff; this time I'll look at the individual patches, instead.

Note: I looked at a bunch of these offline, so sorry for the sudden
ingress of mails today ;)
 - Emily
Junio C Hamano· Oct 16, 2021, 00:58 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 00/13] hook.[ch]: new library to run hooks + simple hook conversion

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
Show 7 quoted lines
> Part 2 of the greater configurable hook saga, starting by converting
> some existing simple hooks to the new hook.[ch] library and "git hook
> run" utility.
>
> See the v1 [1] CL for more context. I've dropped the git-p4 folks from
> the CC list, your feedback on git-p4 would still be very appreciated,
> but I don't think I need to re-spam you for every re-roll of this.

This, when merged to 'seen', seems to fail so many tests to make me question my sanity. Something is not playing with it well.

Queued on top of a merge of ab/config-based-hooks-1 into 'master'; when tested alone, without any other topics in 'seen', it seems to fare much better. Only t2400-worktree-add.sh fails at 58-59 and 62.

Test Summary Report
-------------------
t1092-sparse-checkout-compatibility.sh           (Wstat: 256 Tests: 36 Failed: 2)
  Failed tests:  23, 25
  Non-zero exit status: 1
t5572-pull-submodule.sh                          (Wstat: 256 Tests: 64 Failed: 2)
  Failed tests:  61, 63
  Non-zero exit status: 1
t3432-rebase-fast-forward.sh                     (Wstat: 256 Tests: 225 Failed: 203)
  Failed tests:  15-85, 87-170, 172-201, 203, 205, 207, 209
                211, 213-225
  Non-zero exit status: 1
t7406-submodule-update.sh                        (Wstat: 256 Tests: 58 Failed: 9)
  Failed tests:  14-22
  Non-zero exit status: 1
t3404-rebase-interactive.sh                      (Wstat: 256 Tests: 119 Failed: 93)
  Failed tests:  4-6, 8-9, 14-21, 24-42, 44-47, 49, 52-59
                61, 63-69, 71-78, 80-86, 89, 92-104, 106
                108, 110-118
  Non-zero exit status: 1
t3426-rebase-submodule.sh                        (Wstat: 256 Tests: 29 Failed: 18)
  Failed tests:  1-6, 11-13, 15-20, 25-27
  Non-zero exit status: 1
t0021-conversion.sh                              (Wstat: 256 Tests: 41 Failed: 1)
  Failed test:  18
  Non-zero exit status: 1
t5520-pull.sh                                    (Wstat: 256 Tests: 70 Failed: 26)
  Failed tests:  19, 23-24, 26-29, 40-41, 43, 45-46, 49-52
                55, 57-62, 67, 69-70
  Non-zero exit status: 1
t5533-push-cas.sh                                (Wstat: 256 Tests: 23 Failed: 2)
  Failed tests:  21-22
  Non-zero exit status: 1
t3200-branch.sh                                  (Wstat: 256 Tests: 145 Failed: 2)
  Failed tests:  140-141
  Non-zero exit status: 1
t3421-rebase-topology-linear.sh                  (Wstat: 256 Tests: 64 Failed: 50)
  Failed tests:  2-4, 9-16, 22-27, 29-33, 36-42, 44-64
  Non-zero exit status: 1
t2400-worktree-add.sh                            (Wstat: 256 Tests: 71 Failed: 6)
  Failed tests:  41, 45-46, 58-59, 62
  Non-zero exit status: 1
t7512-status-help.sh                             (Wstat: 256 Tests: 44 Failed: 20)
  Failed tests:  5-6, 8-12, 14-22, 28, 41-43
  Non-zero exit status: 1
t3420-rebase-autostash.sh                        (Wstat: 256 Tests: 42 Failed: 38)
  Failed tests:  2-3, 5-36, 39-42
  Non-zero exit status: 1
t7601-merge-pull-config.sh                       (Wstat: 256 Tests: 62 Failed: 11)
  Failed tests:  15, 25-26, 28-29, 33-36, 42-43
  Non-zero exit status: 1
t3430-rebase-merges.sh                           (Wstat: 256 Tests: 25 Failed: 21)
  Failed tests:  2, 5-10, 12-25
  Non-zero exit status: 1
t9903-bash-prompt.sh                             (Wstat: 256 Tests: 66 Failed: 47)
  Failed tests:  14-35, 37, 40-52, 54-63, 66
  Non-zero exit status: 1
t3415-rebase-autosquash.sh                       (Wstat: 256 Tests: 24 Failed: 23)
  Failed tests:  2-24
  Non-zero exit status: 1
t3418-rebase-continue.sh                         (Wstat: 256 Tests: 29 Failed: 19)
  Failed tests:  2-3, 6-9, 11-19, 26-29
  Non-zero exit status: 1
t3433-rebase-across-mode-change.sh               (Wstat: 256 Tests: 4 Failed: 3)
  Failed tests:  2-4
  Non-zero exit status: 1
t3400-rebase.sh                                  (Wstat: 256 Tests: 35 Failed: 23)
  Failed tests:  4-6, 10-13, 18-33
  Non-zero exit status: 1
t7528-signed-commit-ssh.sh                       (Wstat: 256 Tests: 23 Failed: 14)
  Failed tests:  1-5, 7, 10-17
  Non-zero exit status: 1
t3435-rebase-gpg-sign.sh                         (Wstat: 256 Tests: 20 Failed: 17)
  Failed tests:  3-11, 13-20
  Non-zero exit status: 1
t7510-signed-commit.sh                           (Wstat: 256 Tests: 25 Failed: 19)
  Failed tests:  1-6, 8, 11-19, 23-25
  Non-zero exit status: 1
t3431-rebase-fork-point.sh                       (Wstat: 256 Tests: 26 Failed: 23)
  Failed tests:  2-19, 22-26
  Non-zero exit status: 1
t5407-post-rewrite-hook.sh                       (Wstat: 256 Tests: 16 Failed: 13)
  Failed tests:  4-16
  Non-zero exit status: 1
t7505-prepare-commit-msg-hook.sh                 (Wstat: 256 Tests: 20 Failed: 1)
  Failed test:  14
  Non-zero exit status: 1
t3901-i18n-patch.sh                              (Wstat: 256 Tests: 20 Failed: 8)
  Failed tests:  4-7, 12-15
  Non-zero exit status: 1
t3900-i18n-commit.sh                             (Wstat: 256 Tests: 38 Failed: 6)
  Failed tests:  33-38
  Non-zero exit status: 1
t2012-checkout-last.sh                           (Wstat: 256 Tests: 22 Failed: 4)
  Failed tests:  19-22
  Non-zero exit status: 1
t3436-rebase-more-options.sh                     (Wstat: 256 Tests: 17 Failed: 15)
  Failed tests:  2-16
  Non-zero exit status: 1
t3437-rebase-fixup-options.sh                    (Wstat: 256 Tests: 11 Failed: 10)
  Failed tests:  2-11
  Non-zero exit status: 1
t3407-rebase-abort.sh                            (Wstat: 256 Tests: 15 Failed: 5)
  Failed tests:  3, 5, 10-12
  Non-zero exit status: 1
t7504-commit-msg-hook.sh                         (Wstat: 256 Tests: 25 Failed: 1)
  Failed test:  25
  Non-zero exit status: 1
t7030-verify-tag.sh                              (Wstat: 256 Tests: 15 Failed: 6)
  Failed tests:  1, 3, 8-9, 11-12
  Non-zero exit status: 1
t3402-rebase-merge.sh                            (Wstat: 256 Tests: 14 Failed: 12)
  Failed tests:  3-14
  Non-zero exit status: 1
t3403-rebase-skip.sh                             (Wstat: 256 Tests: 20 Failed: 12)
  Failed tests:  4-5, 8, 11-19
  Non-zero exit status: 1
t3412-rebase-root.sh                             (Wstat: 256 Tests: 25 Failed: 12)
  Failed tests:  4, 6-8, 10-13, 19, 21-22, 24
  Non-zero exit status: 1
t3425-rebase-topology-merges.sh                  (Wstat: 256 Tests: 13 Failed: 12)
  Failed tests:  2-13
  Non-zero exit status: 1
t7031-verify-tag-signed-ssh.sh                   (Wstat: 256 Tests: 8 Failed: 6)
  Failed tests:  1-6
  Non-zero exit status: 1
t3424-rebase-empty.sh                            (Wstat: 256 Tests: 15 Failed: 13)
  Failed tests:  3-15
  Non-zero exit status: 1
t7517-per-repo-email.sh                          (Wstat: 256 Tests: 16 Failed: 3)
  Failed tests:  7, 9, 11
  Non-zero exit status: 1
t6427-diff3-conflict-markers.sh                  (Wstat: 256 Tests: 9 Failed: 2)
  Failed tests:  7-8
  Non-zero exit status: 1
t3416-rebase-onto-threedots.sh                   (Wstat: 256 Tests: 13 Failed: 6)
  Failed tests:  2-3, 5-6, 10, 12
  Non-zero exit status: 1
t3406-rebase-message.sh                          (Wstat: 256 Tests: 14 Failed: 13)
  Failed tests:  2-14
  Non-zero exit status: 1
t3413-rebase-hook.sh                             (Wstat: 256 Tests: 15 Failed: 10)
  Failed tests:  2-3, 5-10, 14-15
  Non-zero exit status: 1
t3401-rebase-and-am-rename.sh                    (Wstat: 256 Tests: 10 Failed: 5)
  Failed tests:  2, 4, 7-9
  Non-zero exit status: 1
t3434-rebase-i18n.sh                             (Wstat: 256 Tests: 6 Failed: 5)
  Failed tests:  2-6
  Non-zero exit status: 1
t7402-submodule-rebase.sh                        (Wstat: 256 Tests: 6 Failed: 3)
  Failed tests:  2-3, 6
  Non-zero exit status: 1
t3429-rebase-edit-todo.sh                        (Wstat: 256 Tests: 6 Failed: 2)
  Failed tests:  3-4
  Non-zero exit status: 1
t3419-rebase-patch-id.sh                         (Wstat: 256 Tests: 5 Failed: 1)
  Failed test:  4
  Non-zero exit status: 1
t3428-rebase-signoff.sh                          (Wstat: 256 Tests: 6 Failed: 6)
  Failed tests:  1-6
  Non-zero exit status: 1
t5403-post-checkout-hook.sh                      (Wstat: 256 Tests: 8 Failed: 2)
  Failed tests:  6-7
  Non-zero exit status: 1
t3427-rebase-subtree.sh                          (Wstat: 256 Tests: 3 Failed: 2)
  Failed tests:  2-3
  Non-zero exit status: 1
t3423-rebase-reword.sh                           (Wstat: 256 Tests: 3 Failed: 2)
  Failed tests:  2-3
  Non-zero exit status: 1
t3417-rebase-whitespace-fix.sh                   (Wstat: 256 Tests: 4 Failed: 4)
  Failed tests:  1-4
  Non-zero exit status: 1
t3405-rebase-malformed.sh                        (Wstat: 256 Tests: 5 Failed: 3)
  Failed tests:  3-5
  Non-zero exit status: 1
t3408-rebase-multi-line.sh                       (Wstat: 256 Tests: 2 Failed: 1)
  Failed test:  2
  Non-zero exit status: 1
Files=942, Tests=24498, 113 wallclock secs ( 8.74 usr  2.04 sys + 726.97 cusr 424.82 csys = 1162.57 CPU)
Result: FAIL
g
Eric Sunshine· Oct 16, 2021, 05:22 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 00/13] hook.[ch]: new library to run hooks + simple hook conversion

On Fri, Oct 15, 2021 at 8:58 PM Junio C Hamano <gitster@pobox.com> wrote:
Show 11 quoted lines
> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
> > Part 2 of the greater configurable hook saga, starting by converting
> > some existing simple hooks to the new hook.[ch] library and "git hook
> > run" utility.
>
> This, when merged to 'seen', seems to fail so many tests to make me
> question my sanity.  Something is not playing with it well.
>
> Queued on top of a merge of ab/config-based-hooks-1 into 'master';
> when tested alone, without any other topics in 'seen', it seems to
> fare much better.  Only t2400-worktree-add.sh fails at 58-59 and 62.

For what it's worth, `git worktree add` is special in that it has to run the `post-checkout` hook in the newly-created worktree, not in the worktree in which the `git worktree add` command was issued.

Ævar Arnfjörð Bjarmason· Oct 16, 2021, 05:47 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 00/13] hook.[ch]: new library to run hooks + simple hook conversion

On Fri, Oct 15 2021, Junio C Hamano wrote:
Show 17 quoted lines
> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
>
>> Part 2 of the greater configurable hook saga, starting by converting
>> some existing simple hooks to the new hook.[ch] library and "git hook
>> run" utility.
>>
>> See the v1 [1] CL for more context. I've dropped the git-p4 folks from
>> the CC list, your feedback on git-p4 would still be very appreciated,
>> but I don't think I need to re-spam you for every re-roll of this.
>
>
> This, when merged to 'seen', seems to fail so many tests to make me
> question my sanity.  Something is not playing with it well.
>
> Queued on top of a merge of ab/config-based-hooks-1 into 'master';
> when tested alone, without any other topics in 'seen', it seems to
> fare much better.  Only t2400-worktree-add.sh fails at 58-59 and 62.

I haven't looked into worktree-add.sh failure (and from what Eric says it seems unrelated), but all the rest is due to a mismerge of reset.c. The diff here at the end on top of "seen" fixes it[1].

I'm able to reproduce it with:
    $ git reference 978c287ca92 # just a "show" alias...
    978c287ca92 (hooks: convert 'post-checkout' hook to hook library, 2021-10-15)
    $ git checkout origin/seen^1; git -c rerere.enabled=false cherry-pick 978c287ca92
Which has a big conflict, including:
    
    @@ -158,10 -90,54 +159,58 @@@
                    goto leave_reset_head;
            }
      
    ++<<<<<<< HEAD
     +      if (oid != &head_oid || update_orig_head || switch_to_branch)
     +              ret = update_refs(opts, oid);
    ++=======
    + reset_head_refs:
    +       reflog_action = getenv(GIT_REFLOG_ACTION_ENVIRONMENT);
    +       strbuf_addf(&msg, "%s: ", reflog_action ? reflog_action : default_reflog_action);
I.e. the oid check there went away, but needs to be kept as well.
1.
Show changes to reset.c +2 −0
diff --git a/reset.c b/reset.c
index 6441b22eebc..602cba75c1c 100644
--- a/reset.c
+++ b/reset.c
@@ -165,6 +165,8 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts)
                goto leave_reset_head;
        }
 
+       if (oid != &head_oid || update_orig_head || switch_to_branch)
+               ret = update_refs(opts, oid);
 
 leave_reset_head:
        rollback_lock_file(&lock);
Junio C Hamano· Oct 16, 2021, 18:04 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v2 00/13] hook.[ch]: new library to run hooks + simple hook conversion

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
> I haven't looked into worktree-add.sh failure (and from what Eric says
> it seems unrelated), but all the rest is due to a mismerge of
> reset.c. The diff here at the end on top of "seen" fixes it[1].
Ahh, of course.

The "update_refs()" call is what replaces the big block that had a bit of conflict in the way how a hook is run, where the original made a call to run_hook_le() now we set up an opt structure and make a call to run_hooks_oneshot(). Dropping the call to it would of course mean no hooks would be run from there X-<.

Thanks.
Ævar Arnfjörð Bjarmason· Oct 16, 2021, 18:10 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 00/13] hook.[ch]: new library to run hooks + simple hook conversion

On Sat, Oct 16 2021, Junio C Hamano wrote:
Show 15 quoted lines
> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>
>> I haven't looked into worktree-add.sh failure (and from what Eric says
>> it seems unrelated), but all the rest is due to a mismerge of
>> reset.c. The diff here at the end on top of "seen" fixes it[1].
>
> Ahh, of course.
>
> The "update_refs()" call is what replaces the
> big block that had a bit of conflict in the way how a hook is run,
> where the original made a call to run_hook_le() now we set up an
> opt structure and make a call to run_hooks_oneshot().  Dropping the
> call to it would of course mean no hooks would be run from there X-<.
>
> Thanks.

I was thinking of re-rolling that series soon, hopefully with "last nits", would you prefer to just have the reset.c part of it ejected for now?

It's really nothing that needs to happen right now, we can do it in some subsequent round when activity in that area has hopefully quieted.

It also means dropping the "remove old run_hook_{le,ve}() hook API", but that can similarly come later...

Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 00/13] hook.[ch]: new library to run hooks + simple hook conversion

Part 2 of the greater configurable hook saga, starting by converting some existing simple hooks to the new hook.[ch] library and "git hook run" utility.

For more context:

See v2: https://lore.kernel.org/git/cover-v2-00.13-00000000000-20211015T093918Z-avarab@gmail.com and v1: https://lore.kernel.org/git/cover-00.13-00000000000-20211012T131934Z-avarab@gmail.com/

Changes since v2:
 * I got some of the s/struct hook/const char *hook_path/ conversion
   wrong in v1->v2, which is the cause of the bug in "seen" with
   t2400*.sh, this fixes that.
 * The commit message for the git-p4 change was also out of date (made
   sense before some previous re-ordering). I also updated a comment
   there.
 * Rebased on "master" for getting past a couple of small
   conflicts. (seen in the range-diff).
 * An include was happening in the wrong commit (was used later in the
   series), moved.
Emily Shaffer (12):
  hook: add 'run' subcommand
  gc: use hook library for pre-auto-gc hook
  rebase: convert pre-rebase to use hook.h
  am: convert applypatch to use hook.h
  hooks: convert 'post-checkout' hook to hook library
  merge: convert post-merge to use hook.h
  send-email: use 'git hook run' for 'sendemail-validate'
  git-p4: use 'git hook' to run hooks
  commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
  read-cache: convert post-index-change to use hook.h
  receive-pack: convert push-to-checkout hook to hook.h
  run-command: remove old run_hook_{le,ve}() hook API
Ævar Arnfjörð Bjarmason (1):
  git hook run: add an --ignore-missing flag
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  45 +++++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/am.c               |   8 ++-
 builtin/checkout.c         |  14 ++--
 builtin/clone.c            |   7 +-
 builtin/gc.c               |   3 +-
 builtin/hook.c             |  90 +++++++++++++++++++++++++
 builtin/merge.c            |   4 +-
 builtin/rebase.c           |   8 ++-
 builtin/receive-pack.c     |   7 +-
 builtin/worktree.c         |  28 ++++----
 command-list.txt           |   1 +
 commit.c                   |  15 +++--
 git-p4.py                  |  70 ++-----------------
 git-send-email.perl        |  22 +++---
 git.c                      |   1 +
 hook.c                     | 121 +++++++++++++++++++++++++++++++++
 hook.h                     |  56 ++++++++++++++++
 read-cache.c               |  11 ++-
 reset.c                    |  14 ++--
 run-command.c              |  32 ---------
 run-command.h              |  17 -----
 t/t1800-hook.sh            | 134 +++++++++++++++++++++++++++++++++++++
 t/t9001-send-email.sh      |   4 +-
 27 files changed, 550 insertions(+), 169 deletions(-)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Range-diff against v2:
 1:  ba64faf0580 =  1:  02fd699e699 hook: add 'run' subcommand
 2:  e3dc0aed81b =  2:  42cc4d2c3c6 gc: use hook library for pre-auto-gc hook
 3:  6227a1e644d !  3:  cbbfd77a4f6 rebase: convert pre-rebase to use hook.h
    @@ builtin/rebase.c
      #define DEFAULT_REFLOG_ACTION "rebase"
      
     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
    - 	char *squash_onto_name = NULL;
      	int reschedule_failed_exec = -1;
      	int allow_preemptive_ff = 1;
    + 	int preserve_merges_selected = 0;
     +	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
      	struct option builtin_rebase_options[] = {
      		OPT_STRING(0, "onto", &options.onto_name,
 4:  0e34eb54054 =  4:  b26cef24f39 am: convert applypatch to use hook.h
 5:  a4df96c1719 !  5:  2a747a65829 hooks: convert 'post-checkout' hook to hook library
    @@ hook.c: static int notify_hook_finished(int result,
      	struct hook_cb_data cb_data = {
      		.rc = 0,
      		.hook_name = hook_name,
    +-		.hook_path = hook_path,
    + 		.options = options,
    + 	};
    + 	int jobs = 1;
     @@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,
      	if (!options)
      		BUG("a struct run_hooks_opt must be provided to run_hooks");
    @@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,
     +		strbuf_add_absolute_path(&abs_path, hook_path);
     +		hook_path = abs_path.buf;
     +	}
    ++	cb_data.hook_path = hook_path;
     +
      	run_processes_parallel_tr2(jobs,
      				   pick_next_hook,
    @@ hook.h: struct run_hooks_opt
      
      #define RUN_HOOKS_OPT_INIT { \
     
    - ## read-cache.c ##
    -@@
    - #include "sparse-index.h"
    - #include "csum-file.h"
    - #include "promisor-remote.h"
    -+#include "hook.h"
    - 
    - /* Mask for the name length in ce_flags in the on-disk index */
    - 
    -
      ## reset.c ##
     @@
      #include "tree-walk.h"
 6:  327f916f8c3 =  6:  7a9fd8627cd merge: convert post-merge to use hook.h
 7:  328767015b1 =  7:  840fb530df3 git hook run: add an --ignore-missing flag
 8:  6c4ebd68d56 =  8:  716ebabd794 send-email: use 'git hook run' for 'sendemail-validate'
 9:  b1f52733e3c !  9:  95782109270 git-p4: use 'git hook' to run hooks
    @@ Commit message
         Python, we can directly call 'git hook run'. We emulate the existence
         check with the --ignore-missing flag.
     
    -    As this is the last hook execution in git.git to not go through "git
    -    hook run" or the hook.[ch] library we can now be absolutely sure that
    -    our assertion in hook.c that only hooks known by the generated (from
    -    githooks(5)) hook-list.h are permitted.
    +    We're dropping the "verbose" handling added in 9f59ca4d6af (git-p4:
    +    create new function run_git_hook, 2020-02-11), those who want
    +    diagnostic output about how hooks are run are now able to get that via
    +    e.g. the trace2 facility and GIT_TRACE=1.
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## git-p4.py ##
     @@ git-p4.py: def decode_path(path):
    -         return path
      
      def run_git_hook(cmd, param=[]):
    --    """Execute a hook if the hook exists."""
    +     """Execute a hook if the hook exists."""
     -    if verbose:
     -        sys.stderr.write("Looking for hook: %s\n" % cmd)
     -        sys.stderr.flush()
    @@ git-p4.py: def decode_path(path):
     -            use_shell = True
     -    return subprocess.call(cli, shell=use_shell)
     -
    -+    """args are specified with -a <arg> -a <arg> -a <arg>"""
     +    args = ['git', 'hook', 'run', '--ignore-missing', cmd]
     +    if param:
     +        args.append("--")
10:  dc31d98acdf = 10:  706426c8a79 commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
11:  58b7689e4af ! 11:  39069a9c3ff read-cache: convert post-index-change to use hook.h
    @@ Commit message
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## read-cache.c ##
    +@@
    + #include "sparse-index.h"
    + #include "csum-file.h"
    + #include "promisor-remote.h"
    ++#include "hook.h"
    + 
    + /* Mask for the name length in ce_flags in the on-disk index */
    + 
     @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struct lock_file *l
      {
      	int ret;
12:  ae1e2a82147 = 12:  9818078f1e5 receive-pack: convert push-to-checkout hook to hook.h
13:  289d5a2d849 = 13:  1bc080d3611 run-command: remove old run_hook_{le,ve}() hook API
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 01/13] hook: add 'run' subcommand

From: Emily Shaffer <emilyshaffer@google.com>

In order to enable hooks to be run as an external process, by a standalone Git command, or by tools which wrap Git, provide an external means to run all configured hook commands for a given hook event.

Most of our hooks require more complex functionality than this, but let's start with the bare minimum required to support our simplest hooks.

In terms of implementation the usage_with_options() and "goto usage" pattern here mirrors that of builtin/{commit-graph,multi-pack-index}.c.

Some of the implementation here, such as a function being named run_hooks() when it's tasked with running one hook, to using the run_processes_parallel_tr2() API to run with jobs=1 is somewhere between a bit odd and and an overkill for the current features of this "hook run" command and the hook.[ch] API.

This code will eventually be able to run multiple hooks declared in config in parallel, by starting out with these names and APIs we reduce the later churn of renaming functions, switching from the run_command() to run_processes_parallel_tr2() API etc.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  37 +++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/hook.c             |  85 ++++++++++++++++++++++++
 command-list.txt           |   1 +
 git.c                      |   1 +
 hook.c                     |  91 ++++++++++++++++++++++++++
 hook.h                     |  34 ++++++++++
 t/t1800-hook.sh            | 129 +++++++++++++++++++++++++++++++++++++
 11 files changed, 385 insertions(+)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Show changes to 11 files +385 −0

.gitignore, Documentation/git-hook.txt, Documentation/githooks.txt, Makefile, builtin.h, builtin/hook.c, command-list.txt, git.c, hook.c, hook.h, t/t1800-hook.sh

diff --git a/.gitignore b/.gitignore
index 054249b20a8..f817c509ec0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@
 /git-grep
 /git-hash-object
 /git-help
+/git-hook
 /git-http-backend
 /git-http-fetch
 /git-http-push
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
new file mode 100644
index 00000000000..e39b1b5d069
--- /dev/null
+++ b/Documentation/git-hook.txt
@@ -0,0 +1,37 @@
+git-hook(1)
+===========
+
+NAME
+----
+git-hook - Run git hooks
+
+SYNOPSIS
+--------
+[verse]
+'git hook' run <hook-name> [-- <hook-args>]
+
+DESCRIPTION
+-----------
+
+A command interface to running git hooks (see linkgit:githooks[5]),
+for use by other scripted git commands.
+
+SUBCOMMANDS
+-----------
+
+run::
+	Run the `<hook-name>` hook. See linkgit:githooks[5] for
+	supported hook names.
++
+
+Any positional arguments to the hook should be passed after a
+mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
+linkgit:githooks[5] for arguments hooks might expect (if any).
+
+SEE ALSO
+--------
+linkgit:githooks[5]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index b51959ff941..a16e62bc8c8 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -698,6 +698,10 @@ and "0" meaning they were not.
 Only one parameter should be set to "1" when the hook runs.  The hook
 running passing "1", "1" should not be possible.
 
+SEE ALSO
+--------
+linkgit:git-hook[1]
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 381bed2c1d2..1eef009def2 100644
--- a/Makefile
+++ b/Makefile
@@ -1107,6 +1107,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
 BUILTIN_OBJS += builtin/grep.o
 BUILTIN_OBJS += builtin/hash-object.o
 BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/hook.o
 BUILTIN_OBJS += builtin/index-pack.o
 BUILTIN_OBJS += builtin/init-db.o
 BUILTIN_OBJS += builtin/interpret-trailers.o
diff --git a/builtin.h b/builtin.h
index 8a58743ed63..83379f3832c 100644
--- a/builtin.h
+++ b/builtin.h
@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);
 int cmd_grep(int argc, const char **argv, const char *prefix);
 int cmd_hash_object(int argc, const char **argv, const char *prefix);
 int cmd_help(int argc, const char **argv, const char *prefix);
+int cmd_hook(int argc, const char **argv, const char *prefix);
 int cmd_index_pack(int argc, const char **argv, const char *prefix);
 int cmd_init_db(int argc, const char **argv, const char *prefix);
 int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);
diff --git a/builtin/hook.c b/builtin/hook.c
new file mode 100644
index 00000000000..41dd15550cf
--- /dev/null
+++ b/builtin/hook.c
@@ -0,0 +1,85 @@
+#include "cache.h"
+#include "builtin.h"
+#include "config.h"
+#include "hook.h"
+#include "parse-options.h"
+#include "strbuf.h"
+#include "strvec.h"
+
+#define BUILTIN_HOOK_RUN_USAGE \
+	N_("git hook run <hook-name> [-- <hook-args>]")
+
+static const char * const builtin_hook_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static const char * const builtin_hook_run_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static int run(int argc, const char **argv, const char *prefix)
+{
+	int i;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	const char *hook_name;
+	const char *hook_path;
+	struct option run_options[] = {
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, run_options,
+			     builtin_hook_run_usage,
+			     PARSE_OPT_KEEP_DASHDASH);
+
+	if (!argc)
+		goto usage;
+
+	/*
+	 * Having a -- for "run" when providing <hook-args> is
+	 * mandatory.
+	 */
+	if (argc > 1 && strcmp(argv[1], "--") &&
+	    strcmp(argv[1], "--end-of-options"))
+		goto usage;
+
+	/* Add our arguments, start after -- */
+	for (i = 2 ; i < argc; i++)
+		strvec_push(&opt.args, argv[i]);
+
+	/* Need to take into account core.hooksPath */
+	git_config(git_default_config, NULL);
+
+	hook_name = argv[0];
+	hook_path = find_hook(hook_name);
+	if (!hook_path) {
+		error("cannot find a hook named %s", hook_name);
+		return 1;
+	}
+
+	ret = run_hooks(hook_name, hook_path, &opt);
+	run_hooks_opt_clear(&opt);
+	return ret;
+usage:
+	usage_with_options(builtin_hook_run_usage, run_options);
+}
+
+int cmd_hook(int argc, const char **argv, const char *prefix)
+{
+	struct option builtin_hook_options[] = {
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, NULL, builtin_hook_options,
+			     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+	if (!argc)
+		goto usage;
+
+	if (!strcmp(argv[0], "run"))
+		return run(argc, argv, prefix);
+
+usage:
+	usage_with_options(builtin_hook_usage, builtin_hook_options);
+}
diff --git a/command-list.txt b/command-list.txt
index a289f09ed6f..17c3958802e 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info
 git-gui                                 mainporcelain
 git-hash-object                         plumbingmanipulators
 git-help                                ancillaryinterrogators          complete
+git-hook                                purehelpers
 git-http-backend                        synchingrepositories
 git-http-fetch                          synchelpers
 git-http-push                           synchelpers
diff --git a/git.c b/git.c
index 5ff21be21f3..a5be02b04b8 100644
--- a/git.c
+++ b/git.c
@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {
 	{ "grep", cmd_grep, RUN_SETUP_GENTLY },
 	{ "hash-object", cmd_hash_object },
 	{ "help", cmd_help },
+	{ "hook", cmd_hook, RUN_SETUP },
 	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
 	{ "init", cmd_init_db },
 	{ "init-db", cmd_init_db },
diff --git a/hook.c b/hook.c
index 55e1145a4b7..20a1a4fec33 100644
--- a/hook.c
+++ b/hook.c
@@ -1,6 +1,7 @@
 #include "cache.h"
 #include "hook.h"
 #include "run-command.h"
+#include "config.h"
 
 const char *find_hook(const char *name)
 {
@@ -40,3 +41,93 @@ int hook_exists(const char *name)
 {
 	return !!find_hook(name);
 }
+
+void run_hooks_opt_clear(struct run_hooks_opt *o)
+{
+	strvec_clear(&o->env);
+	strvec_clear(&o->args);
+}
+
+static int pick_next_hook(struct child_process *cp,
+			  struct strbuf *out,
+			  void *pp_cb,
+			  void **pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = hook_cb->hook_path;
+
+	if (!hook_path)
+		return 0;
+
+	cp->no_stdin = 1;
+	cp->env = hook_cb->options->env.v;
+	cp->stdout_to_stderr = 1;
+	cp->trace2_hook_name = hook_cb->hook_name;
+
+	strvec_push(&cp->args, hook_path);
+	strvec_pushv(&cp->args, hook_cb->options->args.v);
+
+	/* Provide context for errors if necessary */
+	*pp_task_cb = (char *)hook_path;
+
+	/*
+	 * This pick_next_hook() will be called again, we're only
+	 * running one hook, so indicate that no more work will be
+	 * done.
+	 */
+	hook_cb->hook_path = NULL;
+
+	return 1;
+}
+
+static int notify_start_failure(struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cp)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = pp_task_cp;
+
+	hook_cb->rc |= 1;
+
+	strbuf_addf(out, _("Couldn't start hook '%s'\n"),
+		    hook_path);
+
+	return 1;
+}
+
+static int notify_hook_finished(int result,
+				struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+
+	hook_cb->rc |= result;
+
+	return 0;
+}
+
+int run_hooks(const char *hook_name, const char *hook_path,
+	      struct run_hooks_opt *options)
+{
+	struct hook_cb_data cb_data = {
+		.rc = 0,
+		.hook_name = hook_name,
+		.hook_path = hook_path,
+		.options = options,
+	};
+	int jobs = 1;
+
+	if (!options)
+		BUG("a struct run_hooks_opt must be provided to run_hooks");
+
+	run_processes_parallel_tr2(jobs,
+				   pick_next_hook,
+				   notify_start_failure,
+				   notify_hook_finished,
+				   &cb_data,
+				   "hook",
+				   hook_name);
+
+	return cb_data.rc;
+}
diff --git a/hook.h b/hook.h
index 6aa36fc7ff9..a9317c3f95e 100644
--- a/hook.h
+++ b/hook.h
@@ -1,5 +1,28 @@
 #ifndef HOOK_H
 #define HOOK_H
+#include "strvec.h"
+
+struct run_hooks_opt
+{
+	/* Environment vars to be set for each hook */
+	struct strvec env;
+
+	/* Args to be passed to each hook */
+	struct strvec args;
+};
+
+#define RUN_HOOKS_OPT_INIT { \
+	.env = STRVEC_INIT, \
+	.args = STRVEC_INIT, \
+}
+
+struct hook_cb_data {
+	/* rc reflects the cumulative failure state */
+	int rc;
+	const char *hook_name;
+	const char *hook_path;
+	struct run_hooks_opt *options;
+};
 
 /*
  * Returns the path to the hook file, or NULL if the hook is missing
@@ -13,4 +36,15 @@ const char *find_hook(const char *name);
  */
 int hook_exists(const char *hookname);
 
+/**
+ * Clear data from an initialized "struct run_hooks_opt".
+ */
+void run_hooks_opt_clear(struct run_hooks_opt *o);
+
+/**
+ * Takes an already resolved hook found via find_hook() and runs
+ * it. Does not call run_hooks_opt_clear() for you.
+ */
+int run_hooks(const char *hookname, const char *hook_path,
+	      struct run_hooks_opt *options);
 #endif
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
new file mode 100755
index 00000000000..3aea1b105f0
--- /dev/null
+++ b/t/t1800-hook.sh
@@ -0,0 +1,129 @@
+#!/bin/sh
+
+test_description='git-hook command'
+
+TEST_PASSES_SANITIZE_LEAK=true
+. ./test-lib.sh
+
+test_expect_success 'git hook usage' '
+	test_expect_code 129 git hook &&
+	test_expect_code 129 git hook run &&
+	test_expect_code 129 git hook run -h &&
+	test_expect_code 129 git hook run --unknown 2>err &&
+	grep "unknown option" err
+'
+
+test_expect_success 'git hook run: nonexistent hook' '
+	cat >stderr.expect <<-\EOF &&
+	error: cannot find a hook named test-hook
+	EOF
+	test_expect_code 1 git hook run test-hook 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual
+'
+
+test_expect_success 'git hook run: basic' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	EOF
+	git hook run test-hook 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run: stdout and stderr both write to our stderr' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo >&1 Will end up on stderr
+	echo >&2 Will end up on stderr
+	EOF
+
+	cat >stderr.expect <<-\EOF &&
+	Will end up on stderr
+	Will end up on stderr
+	EOF
+	git hook run test-hook >stdout.actual 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual &&
+	test_must_be_empty stdout.actual
+'
+
+test_expect_success 'git hook run: exit codes are passed along' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 1
+	EOF
+
+	test_expect_code 1 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 2
+	EOF
+
+	test_expect_code 2 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 128
+	EOF
+
+	test_expect_code 128 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 129
+	EOF
+
+	test_expect_code 129 git hook run test-hook
+'
+
+test_expect_success 'git hook run arg u ments without -- is not allowed' '
+	test_expect_code 129 git hook run test-hook arg u ments
+'
+
+test_expect_success 'git hook run -- pass arguments' '
+	write_script .git/hooks/test-hook <<-\EOF &&
+	echo $1
+	echo $2
+	EOF
+
+	cat >expect <<-EOF &&
+	arg
+	u ments
+	EOF
+
+	git hook run test-hook -- arg "u ments" 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run -- out-of-repo runs excluded' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	nongit test_must_fail git hook run test-hook
+'
+
+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
+	mkdir my-hooks &&
+	write_script my-hooks/test-hook <<-\EOF &&
+	echo Hook ran $1 >>actual
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	Hook ran one
+	Hook ran two
+	Hook ran three
+	Hook ran four
+	EOF
+
+	# Test various ways of specifying the path. See also
+	# t1350-config-hooks-path.sh
+	>actual &&
+	git hook run test-hook -- ignored 2>>actual &&
+	git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&
+	git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks/" hook run test-hook -- four 2>>actual &&
+	test_cmp expect actual
+'
+
+test_done
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 02/13] gc: use hook library for pre-auto-gc hook

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-auto-gc hook away from run-command.h to and over to the new hook.h library.

To do this introduce a simple run_hooks_oneshot() wrapper, we'll be using it extensively for these simple cases of wanting to run a single hook under a given name, and having it free the memory we allocate for us.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c |  3 ++-
 hook.c       | 20 ++++++++++++++++++++
 hook.h       | 13 +++++++++++++
 3 files changed, 35 insertions(+), 1 deletion(-)
Show changes to 3 files +35 −1

builtin/gc.c, hook.c, hook.h

diff --git a/builtin/gc.c b/builtin/gc.c
index 6b3de3dd514..95939e77f53 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -32,6 +32,7 @@
 #include "remote.h"
 #include "object-store.h"
 #include "exec-cmd.h"
+#include "hook.h"
 
 #define FAILED_RUN "failed to run %s"
 
@@ -394,7 +395,7 @@ static int need_to_gc(void)
 	else
 		return 0;
 
-	if (run_hook_le(NULL, "pre-auto-gc", NULL))
+	if (run_hooks_oneshot("pre-auto-gc", NULL))
 		return 0;
 	return 1;
 }
diff --git a/hook.c b/hook.c
index 20a1a4fec33..9951015dc66 100644
--- a/hook.c
+++ b/hook.c
@@ -131,3 +131,23 @@ int run_hooks(const char *hook_name, const char *hook_path,
 
 	return cb_data.rc;
 }
+
+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)
+{
+	const char *hook_path;
+	struct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;
+	int ret = 0;
+
+	if (!options)
+		options = &hook_opt_scratch;
+
+	hook_path = find_hook(hook_name);
+	if (!hook_path)
+		goto cleanup;
+
+	ret = run_hooks(hook_name, hook_path, options);
+cleanup:
+	run_hooks_opt_clear(options);
+
+	return ret;
+}
diff --git a/hook.h b/hook.h
index a9317c3f95e..9e4d10df63e 100644
--- a/hook.h
+++ b/hook.h
@@ -44,7 +44,20 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);
 /**
  * Takes an already resolved hook found via find_hook() and runs
  * it. Does not call run_hooks_opt_clear() for you.
+ *
+ * See run_hooks_oneshot() for the simpler one-shot API.
  */
 int run_hooks(const char *hookname, const char *hook_path,
 	      struct run_hooks_opt *options);
+
+/**
+ * Calls find_hook() on your "hook_name" and runs the hooks (if any)
+ * with run_hooks().
+ *
+ * If "options" is provided calls run_hooks_opt_clear() on it for
+ * you. If "options" is NULL the default options from
+ * RUN_HOOKS_OPT_INIT will be used.
+ */
+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);
+
 #endif
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 03/13] rebase: convert pre-rebase to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-rebase hook away from run-command.h to and over to the new hook.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/rebase.c | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)
Show changes to builtin/rebase.c +6 −2
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 34b4744e5f3..758b5dfabe2 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -28,6 +28,7 @@
 #include "sequencer.h"
 #include "rebase-interactive.h"
 #include "reset.h"
+#include "hook.h"
 
 #define DEFAULT_REFLOG_ACTION "rebase"
 
@@ -1017,6 +1018,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	int reschedule_failed_exec = -1;
 	int allow_preemptive_ff = 1;
 	int preserve_merges_selected = 0;
+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
 	struct option builtin_rebase_options[] = {
 		OPT_STRING(0, "onto", &options.onto_name,
 			   N_("revision"),
@@ -1711,9 +1713,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	}
 
 	/* If a hook exists, give it a chance to interrupt*/
+	strvec_push(&hook_opt.args, options.upstream_arg);
+	if (argc)
+		strvec_push(&hook_opt.args, argv[0]);
 	if (!ok_to_skip_pre_rebase &&
-	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
-			argc ? argv[0] : NULL, NULL))
+	    run_hooks_oneshot("pre-rebase", &hook_opt))
 		die(_("The pre-rebase hook refused to rebase."));
 
 	if (options.flags & REBASE_DIFFSTAT) {
-- 
2.33.1.1338.g20da966911a
Junio C Hamano· Oct 29, 2021, 05:48 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v3 03/13] rebase: convert pre-rebase to use hook.h

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
Show 45 quoted lines
> From: Emily Shaffer <emilyshaffer@google.com>
>
> Move the pre-rebase hook away from run-command.h to and over to the
> new hook.h library.
>
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  builtin/rebase.c | 8 ++++++--
>  1 file changed, 6 insertions(+), 2 deletions(-)
>
> diff --git a/builtin/rebase.c b/builtin/rebase.c
> index 34b4744e5f3..758b5dfabe2 100644
> --- a/builtin/rebase.c
> +++ b/builtin/rebase.c
> @@ -28,6 +28,7 @@
>  #include "sequencer.h"
>  #include "rebase-interactive.h"
>  #include "reset.h"
> +#include "hook.h"
>  
>  #define DEFAULT_REFLOG_ACTION "rebase"
>  
> @@ -1017,6 +1018,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
>  	int reschedule_failed_exec = -1;
>  	int allow_preemptive_ff = 1;
>  	int preserve_merges_selected = 0;
> +	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
>  	struct option builtin_rebase_options[] = {
>  		OPT_STRING(0, "onto", &options.onto_name,
>  			   N_("revision"),
> @@ -1711,9 +1713,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
>  	}
>  
>  	/* If a hook exists, give it a chance to interrupt*/
> +	strvec_push(&hook_opt.args, options.upstream_arg);
> +	if (argc)
> +		strvec_push(&hook_opt.args, argv[0]);
>  	if (!ok_to_skip_pre_rebase &&
> -	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
> -			argc ? argv[0] : NULL, NULL))
> +	    run_hooks_oneshot("pre-rebase", &hook_opt))
>  		die(_("The pre-rebase hook refused to rebase."));
>  
>  	if (options.flags & REBASE_DIFFSTAT) {
OK, so the convention is
 * if there is no extra args the hook takes, you only give hook_name
   and NULL;
 * if there is even one arg the hook takes, you have to prepare the
   opt struct and strvec_push() the arg yourself.

That would work, and it might be merely the matter of taste, but it would have been nicer if the calling sequence to run_hooks_oneshot() were more like run_hook_le().

But patches were already written, so let's keep reading.
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 04/13] am: convert applypatch to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach pre-applypatch, post-applypatch, and applypatch-msg to use the hook.h library instead of the run-command.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/am.c | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)
Show changes to builtin/am.c +5 −3
diff --git a/builtin/am.c b/builtin/am.c
index 8677ea2348a..5528f131a81 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -446,9 +446,11 @@ static void am_destroy(const struct am_state *state)
 static int run_applypatch_msg_hook(struct am_state *state)
 {
 	int ret;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 
 	assert(state->msg);
-	ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
+	strvec_push(&opt.args, am_path(state, "final-commit"));
+	ret = run_hooks_oneshot("applypatch-msg", &opt);
 
 	if (!ret) {
 		FREE_AND_NULL(state->msg);
@@ -1609,7 +1611,7 @@ static void do_commit(const struct am_state *state)
 	const char *reflog_msg, *author, *committer = NULL;
 	struct strbuf sb = STRBUF_INIT;
 
-	if (run_hook_le(NULL, "pre-applypatch", NULL))
+	if (run_hooks_oneshot("pre-applypatch", NULL))
 		exit(1);
 
 	if (write_cache_as_tree(&tree, 0, NULL))
@@ -1661,7 +1663,7 @@ static void do_commit(const struct am_state *state)
 		fclose(fp);
 	}
 
-	run_hook_le(NULL, "post-applypatch", NULL);
+	run_hooks_oneshot("post-applypatch", NULL);
 
 	strbuf_release(&sb);
 }
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 05/13] hooks: convert 'post-checkout' hook to hook library

From: Emily Shaffer <emilyshaffer@google.com>

Move the running of the 'post-checkout' hook away from run-command.h to the new hook.h library. For "worktree" this requires a change to it to run the hooks from a given directory.

We could strictly speaking skip the "absolute_path" flag and just check if "dir" is specified, but let's split them up for clarity, as well as for any future user who'd like to set "dir" but not implicitly change the argument to an absolute path.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/checkout.c | 14 +++++++++-----
 builtin/clone.c    |  7 +++++--
 builtin/worktree.c | 28 ++++++++++++----------------
 hook.c             | 12 +++++++++++-
 hook.h             |  9 +++++++++
 reset.c            | 14 ++++++++++----
 6 files changed, 56 insertions(+), 28 deletions(-)
Show changes to 6 files +56 −28

builtin/checkout.c, builtin/clone.c, builtin/worktree.c, hook.c, hook.h, reset.c

diff --git a/builtin/checkout.c b/builtin/checkout.c
index cbf73b8c9f6..54cbba821a8 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -9,6 +9,7 @@
 #include "config.h"
 #include "diff.h"
 #include "dir.h"
+#include "hook.h"
 #include "ll-merge.h"
 #include "lockfile.h"
 #include "merge-recursive.h"
@@ -106,13 +107,16 @@ struct branch_info {
 static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
 			      int changed)
 {
-	return run_hook_le(NULL, "post-checkout",
-			   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
-			   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
-			   changed ? "1" : "0", NULL);
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
 	/* "new_commit" can be NULL when checking out from the index before
 	   a commit exists. */
-
+	strvec_pushl(&opt.args,
+		     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
+		     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
+		     changed ? "1" : "0",
+		     NULL);
+	return run_hooks_oneshot("post-checkout", &opt);
 }
 
 static int update_some(const struct object_id *oid, struct strbuf *base,
diff --git a/builtin/clone.c b/builtin/clone.c
index 559acf9e036..53ca5ca0eb2 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -32,6 +32,7 @@
 #include "connected.h"
 #include "packfile.h"
 #include "list-objects-filter-options.h"
+#include "hook.h"
 
 /*
  * Overall FIXMEs:
@@ -659,6 +660,7 @@ static int checkout(int submodule_progress)
 	struct tree *tree;
 	struct tree_desc t;
 	int err = 0;
+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
 
 	if (option_no_checkout)
 		return 0;
@@ -705,8 +707,9 @@ static int checkout(int submodule_progress)
 	if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
 		die(_("unable to write new index file"));
 
-	err |= run_hook_le(NULL, "post-checkout", oid_to_hex(null_oid()),
-			   oid_to_hex(&oid), "1", NULL);
+	strvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid),
+		     "1", NULL);
+	err |= run_hooks_oneshot("post-checkout", &hook_opt);
 
 	if (!err && (option_recurse_submodules.nr > 0)) {
 		struct strvec args = STRVEC_INIT;
diff --git a/builtin/worktree.c b/builtin/worktree.c
index d22ece93e1a..330867c19bf 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -382,22 +382,18 @@ static int add_worktree(const char *path, const char *refname,
 	 * is_junk is cleared, but do return appropriate code when hook fails.
 	 */
 	if (!ret && opts->checkout) {
-		const char *hook = find_hook("post-checkout");
-		if (hook) {
-			const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
-			cp.git_cmd = 0;
-			cp.no_stdin = 1;
-			cp.stdout_to_stderr = 1;
-			cp.dir = path;
-			cp.env = env;
-			cp.argv = NULL;
-			cp.trace2_hook_name = "post-checkout";
-			strvec_pushl(&cp.args, absolute_path(hook),
-				     oid_to_hex(null_oid()),
-				     oid_to_hex(&commit->object.oid),
-				     "1", NULL);
-			ret = run_command(&cp);
-		}
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+		strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
+		strvec_pushl(&opt.args,
+			     oid_to_hex(null_oid()),
+			     oid_to_hex(&commit->object.oid),
+			     "1",
+			     NULL);
+		opt.dir = path;
+		opt.absolute_path = 1;
+
+		ret = run_hooks_oneshot("post-checkout", &opt);
 	}
 
 	strvec_clear(&child_env);
diff --git a/hook.c b/hook.c
index 9951015dc66..bf34cd0a2c1 100644
--- a/hook.c
+++ b/hook.c
@@ -63,6 +63,7 @@ static int pick_next_hook(struct child_process *cp,
 	cp->env = hook_cb->options->env.v;
 	cp->stdout_to_stderr = 1;
 	cp->trace2_hook_name = hook_cb->hook_name;
+	cp->dir = hook_cb->options->dir;
 
 	strvec_push(&cp->args, hook_path);
 	strvec_pushv(&cp->args, hook_cb->options->args.v);
@@ -110,10 +111,10 @@ static int notify_hook_finished(int result,
 int run_hooks(const char *hook_name, const char *hook_path,
 	      struct run_hooks_opt *options)
 {
+	struct strbuf abs_path = STRBUF_INIT;
 	struct hook_cb_data cb_data = {
 		.rc = 0,
 		.hook_name = hook_name,
-		.hook_path = hook_path,
 		.options = options,
 	};
 	int jobs = 1;
@@ -121,6 +122,12 @@ int run_hooks(const char *hook_name, const char *hook_path,
 	if (!options)
 		BUG("a struct run_hooks_opt must be provided to run_hooks");
 
+	if (options->absolute_path) {
+		strbuf_add_absolute_path(&abs_path, hook_path);
+		hook_path = abs_path.buf;
+	}
+	cb_data.hook_path = hook_path;
+
 	run_processes_parallel_tr2(jobs,
 				   pick_next_hook,
 				   notify_start_failure,
@@ -129,6 +136,9 @@ int run_hooks(const char *hook_name, const char *hook_path,
 				   "hook",
 				   hook_name);
 
+	if (options->absolute_path)
+		strbuf_release(&abs_path);
+
 	return cb_data.rc;
 }
 
diff --git a/hook.h b/hook.h
index 9e4d10df63e..252a605b125 100644
--- a/hook.h
+++ b/hook.h
@@ -9,6 +9,15 @@ struct run_hooks_opt
 
 	/* Args to be passed to each hook */
 	struct strvec args;
+
+	/*
+	 * Resolve and run the "absolute_path(hook)" instead of
+	 * "hook". Used for "git worktree" hooks
+	 */
+	int absolute_path;
+
+	/* Path to initial working directory for subprocess */
+	const char *dir;
 };
 
 #define RUN_HOOKS_OPT_INIT { \
diff --git a/reset.c b/reset.c
index f214df3d96c..4234dd345db 100644
--- a/reset.c
+++ b/reset.c
@@ -7,6 +7,7 @@
 #include "tree-walk.h"
 #include "tree.h"
 #include "unpack-trees.h"
+#include "hook.h"
 
 int reset_head(struct repository *r, struct object_id *oid, const char *action,
 	       const char *switch_to_branch, unsigned flags,
@@ -126,10 +127,15 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,
 			ret = create_symref("HEAD", switch_to_branch,
 					    reflog_head);
 	}
-	if (run_hook)
-		run_hook_le(NULL, "post-checkout",
-			    oid_to_hex(orig ? orig : null_oid()),
-			    oid_to_hex(oid), "1", NULL);
+	if (run_hook) {
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+		strvec_pushl(&opt.args,
+			     oid_to_hex(orig ? orig : null_oid()),
+			     oid_to_hex(oid),
+			     "1",
+			     NULL);
+		run_hooks_oneshot("post-checkout", &opt);
+	}
 
 leave_reset_head:
 	strbuf_release(&msg);
-- 
2.33.1.1338.g20da966911a
Junio C Hamano· Oct 29, 2021, 05:55 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v3 05/13] hooks: convert 'post-checkout' hook to hook library

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
Show 29 quoted lines
>  	if (!ret && opts->checkout) {
> -		const char *hook = find_hook("post-checkout");
> -		if (hook) {
> -			const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
> -			cp.git_cmd = 0;
> -			cp.no_stdin = 1;
> -			cp.stdout_to_stderr = 1;
> -			cp.dir = path;
> -			cp.env = env;
> -			cp.argv = NULL;
> -			cp.trace2_hook_name = "post-checkout";
> -			strvec_pushl(&cp.args, absolute_path(hook),
> -				     oid_to_hex(null_oid()),
> -				     oid_to_hex(&commit->object.oid),
> -				     "1", NULL);
> -			ret = run_command(&cp);
> -		}
> +		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
> +
> +		strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
> +		strvec_pushl(&opt.args,
> +			     oid_to_hex(null_oid()),
> +			     oid_to_hex(&commit->object.oid),
> +			     "1",
> +			     NULL);
> +		opt.dir = path;
> +		opt.absolute_path = 1;
> +
> +		ret = run_hooks_oneshot("post-checkout", &opt);

I can see how passing opt from the caller gives more flexibility like allowing to pass arbitrary environments. The interface still looks a bit unwieldy as I expect passing command line args would be a much more common need than tweaking the environment, but let's keep reading.

Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 06/13] merge: convert post-merge to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach post-merge to use the hook.h library instead of the run-command.h library to run hooks.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/merge.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)
Show changes to builtin/merge.c +3 −1
diff --git a/builtin/merge.c b/builtin/merge.c
index cc4a910c69b..c48b875b82b 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -448,6 +448,7 @@ static void finish(struct commit *head_commit,
 		   const struct object_id *new_head, const char *msg)
 {
 	struct strbuf reflog_message = STRBUF_INIT;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 	const struct object_id *head = &head_commit->object.oid;
 
 	if (!msg)
@@ -488,7 +489,8 @@ static void finish(struct commit *head_commit,
 	}
 
 	/* Run a post-merge hook */
-	run_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL);
+	strvec_push(&opt.args, squash ? "1" : "0");
+	run_hooks_oneshot("post-merge", &opt);
 
 	apply_autostash(git_path_merge_autostash(the_repository));
 	strbuf_release(&reflog_message);
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 07/13] git hook run: add an --ignore-missing flag

For certain one-shot hooks we'd like to optimistically run them, and not complain if they don't exist. This will be used by send-email in a subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Documentation/git-hook.txt | 10 +++++++++-
 builtin/hook.c             |  7 ++++++-
 t/t1800-hook.sh            |  5 +++++
 3 files changed, 20 insertions(+), 2 deletions(-)
Show changes to 3 files +20 −2

Documentation/git-hook.txt, builtin/hook.c, t/t1800-hook.sh

diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index e39b1b5d069..77c3a8ad909 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -8,7 +8,7 @@ git-hook - Run git hooks
 SYNOPSIS
 --------
 [verse]
-'git hook' run <hook-name> [-- <hook-args>]
+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]
 
 DESCRIPTION
 -----------
@@ -28,6 +28,14 @@ Any positional arguments to the hook should be passed after a
 mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
 linkgit:githooks[5] for arguments hooks might expect (if any).
 
+OPTIONS
+-------
+
+--ignore-missing::
+	Ignore any missing hook by quietly returning zero. Used for
+	tools that want to do a blind one-shot run of a hook that may
+	or may not be present.
+
 SEE ALSO
 --------
 linkgit:githooks[5]
diff --git a/builtin/hook.c b/builtin/hook.c
index 41dd15550cf..fa26454990d 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -7,7 +7,7 @@
 #include "strvec.h"
 
 #define BUILTIN_HOOK_RUN_USAGE \
-	N_("git hook run <hook-name> [-- <hook-args>]")
+	N_("git hook run [--ignore-missing] <hook-name> [-- <hook-args>]")
 
 static const char * const builtin_hook_usage[] = {
 	BUILTIN_HOOK_RUN_USAGE,
@@ -23,9 +23,12 @@ static int run(int argc, const char **argv, const char *prefix)
 {
 	int i;
 	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	int ignore_missing = 0;
 	const char *hook_name;
 	const char *hook_path;
 	struct option run_options[] = {
+		OPT_BOOL(0, "ignore-missing", &ignore_missing,
+			 N_("silently ignore missing requested <hook-name>")),
 		OPT_END(),
 	};
 	int ret;
@@ -53,6 +56,8 @@ static int run(int argc, const char **argv, const char *prefix)
 	git_config(git_default_config, NULL);
 
 	hook_name = argv[0];
+	if (ignore_missing)
+		return run_hooks_oneshot(hook_name, &opt);
 	hook_path = find_hook(hook_name);
 	if (!hook_path) {
 		error("cannot find a hook named %s", hook_name);
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 3aea1b105f0..29718aa9913 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -21,6 +21,11 @@ test_expect_success 'git hook run: nonexistent hook' '
 	test_cmp stderr.expect stderr.actual
 '
 
+test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '
+	git hook run --ignore-missing does-not-exist 2>stderr.actual &&
+	test_must_be_empty stderr.actual
+'
+
 test_expect_success 'git hook run: basic' '
 	write_script .git/hooks/test-hook <<-EOF &&
 	echo Test hook
-- 
2.33.1.1338.g20da966911a
Junio C Hamano· Oct 29, 2021, 05:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v3 07/13] git hook run: add an --ignore-missing flag

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
Show 18 quoted lines
> +	int ignore_missing = 0;
>  	const char *hook_name;
>  	const char *hook_path;
>  	struct option run_options[] = {
> +		OPT_BOOL(0, "ignore-missing", &ignore_missing,
> +			 N_("silently ignore missing requested <hook-name>")),
>  		OPT_END(),
>  	};
>  	int ret;
> @@ -53,6 +56,8 @@ static int run(int argc, const char **argv, const char *prefix)
>  	git_config(git_default_config, NULL);
>  
>  	hook_name = argv[0];
> +	if (ignore_missing)
> +		return run_hooks_oneshot(hook_name, &opt);
>  	hook_path = find_hook(hook_name);
>  	if (!hook_path) {
>  		error("cannot find a hook named %s", hook_name);

Given that the rest of this "run()" function is more or less identical to run_hooks_oneshot(), I have to wonder why we do not give an .ignore_missing member to the hooks_opt structure to be done with this. Otehrwise, every time the underlying run_hooks() interface needs to change, we need to update two places, no?

Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 08/13] send-email: use 'git hook run' for 'sendemail-validate'

From: Emily Shaffer <emilyshaffer@google.com>

Change the "sendmail-validate" hook to be run via the "git hook run" wrapper instead of via a direct invocation.

This is the smallest possibly change to get "send-email" using "git hook run". We still check the hook itself with "-x", and set a "GIT_DIR" variable, both of which are asserted by our tests. We'll need to get rid of this special behavior if we start running N hooks, but for now let's be as close to bug-for-bug compatible as possible.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl   | 22 ++++++++++++++--------
 t/t9001-send-email.sh |  4 ++--
 2 files changed, 16 insertions(+), 10 deletions(-)
Show changes to 2 files +16 −10

git-send-email.perl, t/t9001-send-email.sh

diff --git a/git-send-email.perl b/git-send-email.perl
index 5262d88ee32..4c20c0bbb79 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -195,13 +195,13 @@ sub format_2822_time {
 my $editor;
 
 sub system_or_msg {
-	my ($args, $msg) = @_;
+	my ($args, $msg, $cmd_name) = @_;
 	system(@$args);
 	my $signalled = $? & 127;
 	my $exit_code = $? >> 8;
 	return unless $signalled or $exit_code;
 
-	my @sprintf_args = ($args->[0], $exit_code);
+	my @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);
 	if (defined $msg) {
 		# Quiet the 'redundant' warning category, except we
 		# need to support down to Perl 5.8, so we can't do a
@@ -2039,10 +2039,10 @@ sub validate_patch {
 	my ($fn, $xfer_encoding) = @_;
 
 	if ($repo) {
+		my $hook_name = 'sendemail-validate';
 		my $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');
 		require File::Spec;
-		my $validate_hook = File::Spec->catfile($hooks_path,
-					    'sendemail-validate');
+		my $validate_hook = File::Spec->catfile($hooks_path, $hook_name);
 		my $hook_error;
 		if (-x $validate_hook) {
 			require Cwd;
@@ -2052,13 +2052,19 @@ sub validate_patch {
 			chdir($repo->wc_path() or $repo->repo_path())
 				or die("chdir: $!");
 			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			$hook_error = system_or_msg([$validate_hook, $target]);
+			my @cmd = ("git", "hook", "run", "--ignore-missing",
+				    $hook_name, "--");
+			my @cmd_msg = (@cmd, "<patch>");
+			my @cmd_run = (@cmd, $target);
+			$hook_error = system_or_msg(\@cmd_run, undef, "@cmd_msg");
 			chdir($cwd_save) or die("chdir: $!");
 		}
 		if ($hook_error) {
-			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
-				       "%s\n" .
-				       "warning: no patches were sent\n"), $fn, $hook_error);
+			$hook_error = sprintf(__("fatal: %s: rejected by %s hook\n" .
+						 $hook_error . "\n" .
+						 "warning: no patches were sent\n"),
+					      $fn, $hook_name);
+			die $hook_error;
 		}
 	}
 
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index aa0c20499ba..84d0f40d76a 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -539,7 +539,7 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'my-hooks/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
@@ -558,7 +558,7 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 09/13] git-p4: use 'git hook' to run hooks

From: Emily Shaffer <emilyshaffer@google.com>

Instead of duplicating the behavior of run-command.h:run_hook_le() in Python, we can directly call 'git hook run'. We emulate the existence check with the --ignore-missing flag.

We're dropping the "verbose" handling added in 9f59ca4d6af (git-p4: create new function run_git_hook, 2020-02-11), those who want diagnostic output about how hooks are run are now able to get that via e.g. the trace2 facility and GIT_TRACE=1.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-p4.py | 70 +++++--------------------------------------------------
 1 file changed, 6 insertions(+), 64 deletions(-)
Show changes to git-p4.py +6 −64
diff --git a/git-p4.py b/git-p4.py
index 2b4500226aa..3b54168eb4a 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -208,70 +208,12 @@ def decode_path(path):
 
 def run_git_hook(cmd, param=[]):
     """Execute a hook if the hook exists."""
-    if verbose:
-        sys.stderr.write("Looking for hook: %s\n" % cmd)
-        sys.stderr.flush()
-
-    hooks_path = gitConfig("core.hooksPath")
-    if len(hooks_path) <= 0:
-        hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
-
-    if not isinstance(param, list):
-        param=[param]
-
-    # resolve hook file name, OS depdenent
-    hook_file = os.path.join(hooks_path, cmd)
-    if platform.system() == 'Windows':
-        if not os.path.isfile(hook_file):
-            # look for the file with an extension
-            files = glob.glob(hook_file + ".*")
-            if not files:
-                return True
-            files.sort()
-            hook_file = files.pop()
-            while hook_file.upper().endswith(".SAMPLE"):
-                # The file is a sample hook. We don't want it
-                if len(files) > 0:
-                    hook_file = files.pop()
-                else:
-                    return True
-
-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
-        return True
-
-    return run_hook_command(hook_file, param) == 0
-
-def run_hook_command(cmd, param):
-    """Executes a git hook command
-       cmd = the command line file to be executed. This can be
-       a file that is run by OS association.
-
-       param = a list of parameters to pass to the cmd command
-
-       On windows, the extension is checked to see if it should
-       be run with the Git for Windows Bash shell.  If there
-       is no file extension, the file is deemed a bash shell
-       and will be handed off to sh.exe. Otherwise, Windows
-       will be called with the shell to handle the file assocation.
-
-       For non Windows operating systems, the file is called
-       as an executable.
-    """
-    cli = [cmd] + param
-    use_shell = False
-    if platform.system() == 'Windows':
-        (root,ext) = os.path.splitext(cmd)
-        if ext == "":
-            exe_path = os.environ.get("EXEPATH")
-            if exe_path is None:
-                exe_path = ""
-            else:
-                exe_path = os.path.join(exe_path, "bin")
-            cli = [os.path.join(exe_path, "SH.EXE")] + cli
-        else:
-            use_shell = True
-    return subprocess.call(cli, shell=use_shell)
-
+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]
+    if param:
+        args.append("--")
+        for p in param:
+            args.append(p)
+    return subprocess.call(args) == 0
 
 def write_pipe(c, stdin):
     if verbose:
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 10/13] commit: convert {pre-commit,prepare-commit-msg} hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move these hooks hook away from run-command.h to and over to the new hook.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 commit.c | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)
Show changes to commit.c +8 −7
diff --git a/commit.c b/commit.c
index 551de4903c1..4e7cbd7585d 100644
--- a/commit.c
+++ b/commit.c
@@ -21,6 +21,7 @@
 #include "commit-reach.h"
 #include "run-command.h"
 #include "shallow.h"
+#include "hook.h"
 
 static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
 
@@ -1700,22 +1701,22 @@ size_t ignore_non_trailer(const char *buf, size_t len)
 int run_commit_hook(int editor_is_used, const char *index_file,
 		    const char *name, ...)
 {
-	struct strvec hook_env = STRVEC_INIT;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 	va_list args;
-	int ret;
+	const char *arg;
 
-	strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
+	strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s", index_file);
 
 	/*
 	 * Let the hook know that no editor will be launched.
 	 */
 	if (!editor_is_used)
-		strvec_push(&hook_env, "GIT_EDITOR=:");
+		strvec_push(&opt.env, "GIT_EDITOR=:");
 
 	va_start(args, name);
-	ret = run_hook_ve(hook_env.v, name, args);
+	while ((arg = va_arg(args, const char *)))
+		strvec_push(&opt.args, arg);
 	va_end(args);
-	strvec_clear(&hook_env);
 
-	return ret;
+	return run_hooks_oneshot(name, &opt);
 }
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 11/13] read-cache: convert post-index-change to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the post-index-change hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of "run_hook_ve()" outside of run-command.c ("run_hook_le()" still uses it). So we can make the function static now. A subsequent commit will remove this code entirely when "run_hook_le()" itself goes away.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 read-cache.c  | 11 ++++++++---
 run-command.c |  2 +-
 run-command.h |  1 -
 3 files changed, 9 insertions(+), 5 deletions(-)
Show changes to 3 files +9 −5

read-cache.c, run-command.c, run-command.h

diff --git a/read-cache.c b/read-cache.c
index 8a50ff66b30..6674df28ab3 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -28,6 +28,7 @@
 #include "sparse-index.h"
 #include "csum-file.h"
 #include "promisor-remote.h"
+#include "hook.h"
 
 /* Mask for the name length in ce_flags in the on-disk index */
 
@@ -3101,6 +3102,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
 {
 	int ret;
 	int was_full = !istate->sparse_index;
+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
 
 	ret = convert_to_sparse(istate, 0);
 
@@ -3129,9 +3131,12 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
 	else
 		ret = close_lock_file_gently(lock);
 
-	run_hook_le(NULL, "post-index-change",
-			istate->updated_workdir ? "1" : "0",
-			istate->updated_skipworktree ? "1" : "0", NULL);
+	strvec_pushl(&hook_opt.args,
+		     istate->updated_workdir ? "1" : "0",
+		     istate->updated_skipworktree ? "1" : "0",
+		     NULL);
+	run_hooks_oneshot("post-index-change", &hook_opt);
+
 	istate->updated_workdir = 0;
 	istate->updated_skipworktree = 0;
 
diff --git a/run-command.c b/run-command.c
index 7ef5cc712a9..d92e670c8ed 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1323,7 +1323,7 @@ int async_with_fork(void)
 #endif
 }
 
-int run_hook_ve(const char *const *env, const char *name, va_list args)
+static int run_hook_ve(const char *const *env, const char *name, va_list args)
 {
 	struct child_process hook = CHILD_PROCESS_INIT;
 	const char *p;
diff --git a/run-command.h b/run-command.h
index 49878262584..3fa7454cf8a 100644
--- a/run-command.h
+++ b/run-command.h
@@ -239,7 +239,6 @@ int run_command(struct child_process *);
  */
 LAST_ARG_MUST_BE_NULL
 int run_hook_le(const char *const *env, const char *name, ...);
-int run_hook_ve(const char *const *env, const char *name, va_list args);
 
 /*
  * Trigger an auto-gc
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 12/13] receive-pack: convert push-to-checkout hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the push-to-checkout hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of run_hook_le(), so we could remove that function now, but let's leave that to a follow-up cleanup commit.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/receive-pack.c | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)
Show changes to builtin/receive-pack.c +5 −2
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 25cc0c907e1..22e7a431728 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1435,9 +1435,12 @@ static const char *push_to_checkout(unsigned char *hash,
 				    struct strvec *env,
 				    const char *work_tree)
 {
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
 	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
-	if (run_hook_le(env->v, push_to_checkout_hook,
-			hash_to_hex(hash), NULL))
+	strvec_pushv(&opt.env, env->v);
+	strvec_push(&opt.args, hash_to_hex(hash));
+	if (run_hooks_oneshot(push_to_checkout_hook, &opt))
 		return "push-to-checkout hook declined";
 	else
 		return NULL;
-- 
2.33.1.1338.g20da966911a
Ævar Arnfjörð Bjarmason· Oct 19, 2021, 23:20 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v3 13/13] run-command: remove old run_hook_{le,ve}() hook API

From: Emily Shaffer <emilyshaffer@google.com>

The new hook.h library has replaced all run-command.h hook-related functionality. So let's delete this dead code.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 run-command.c | 32 --------------------------------
 run-command.h | 16 ----------------
 2 files changed, 48 deletions(-)
Show changes to 2 files +0 −48

run-command.c, run-command.h

diff --git a/run-command.c b/run-command.c
index d92e670c8ed..8a21ff525f3 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1323,38 +1323,6 @@ int async_with_fork(void)
 #endif
 }
 
-static int run_hook_ve(const char *const *env, const char *name, va_list args)
-{
-	struct child_process hook = CHILD_PROCESS_INIT;
-	const char *p;
-
-	p = find_hook(name);
-	if (!p)
-		return 0;
-
-	strvec_push(&hook.args, p);
-	while ((p = va_arg(args, const char *)))
-		strvec_push(&hook.args, p);
-	hook.env = env;
-	hook.no_stdin = 1;
-	hook.stdout_to_stderr = 1;
-	hook.trace2_hook_name = name;
-
-	return run_command(&hook);
-}
-
-int run_hook_le(const char *const *env, const char *name, ...)
-{
-	va_list args;
-	int ret;
-
-	va_start(args, name);
-	ret = run_hook_ve(env, name, args);
-	va_end(args);
-
-	return ret;
-}
-
 struct io_pump {
 	/* initialized by caller */
 	int fd;
diff --git a/run-command.h b/run-command.h
index 3fa7454cf8a..59e1fbff64c 100644
--- a/run-command.h
+++ b/run-command.h
@@ -224,22 +224,6 @@ int finish_command_in_signal(struct child_process *);
  */
 int run_command(struct child_process *);
 
-/**
- * Run a hook.
- * The first argument is a pathname to an index file, or NULL
- * if the hook uses the default index file or no index is needed.
- * The second argument is the name of the hook.
- * The further arguments correspond to the hook arguments.
- * The last argument has to be NULL to terminate the arguments list.
- * If the hook does not exist or is not executable, the return
- * value will be zero.
- * If it is executable, the hook will be executed and the exit
- * status of the hook is returned.
- * On execution, .stdout_to_stderr and .no_stdin will be set.
- */
-LAST_ARG_MUST_BE_NULL
-int run_hook_le(const char *const *env, const char *name, ...);
-
 /*
  * Trigger an auto-gc
  */
-- 
2.33.1.1338.g20da966911a
Junio C Hamano· Oct 29, 2021, 06:27 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v3 00/13] hook.[ch]: new library to run hooks + simple hook conversion

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
> Part 2 of the greater configurable hook saga, starting by converting
> some existing simple hooks to the new hook.[ch] library and "git hook
> run" utility.

These look mostly OK. Other than .ignore_missing stuff, I think I agree with the direction of the overall series.

Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 00/17] hook.[ch]: new library to run hooks + simple hook conversion

Part 2 of the greater configurable hook saga, starting by converting some existing simple hooks to the new hook.[ch] library and "git hook run" utility. See [1] for the last iteration.

This re-roll should address Junio's comments on v3, i.e. there's now a run_hooks_l() API similar to run_hook_le(). I also simplified the callers of run_hooks(), which is now called run_hooks_opt().

Since no callers needed anything but a one-shot call the memory public memory management API is gone, that'll eventually be needed for a "git hook list" with configurable hooks, but let's introduce those API bits then, not now.

This grew to 17 patches from 13 due to splitting up some commints for ease of review, the overall diffstat got smaller. I picked a function with the same width as the old function, so many of the diffs are now simply a s/run_hook_le(NULL, /run_hooks_l(/.

1. https://lore.kernel.org/git/cover-v3-00.13-00000000000-20211019T231647Z-avarab@gmail.com/
Emily Shaffer (14):
  hook: add 'run' subcommand
  gc: use hook library for pre-auto-gc hook
  am: convert {pre,post}-applypatch to use hook.h
  rebase: convert pre-rebase to use hook.h
  am: convert applypatch-msg to use hook.h
  merge: convert post-merge to use hook.h
  hooks: convert non-worktree 'post-checkout' hook to hook library
  hooks: convert worktree 'post-checkout' hook to hook library
  send-email: use 'git hook run' for 'sendemail-validate'
  git-p4: use 'git hook' to run hooks
  commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
  read-cache: convert post-index-change to use hook.h
  receive-pack: convert push-to-checkout hook to hook.h
  run-command: remove old run_hook_{le,ve}() hook API
Ævar Arnfjörð Bjarmason (3):
  hook API: add a run_hooks() wrapper
  hook API: add a run_hooks_l() wrapper
  git hook run: add an --ignore-missing flag
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  45 +++++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/am.c               |   6 +-
 builtin/checkout.c         |   3 +-
 builtin/clone.c            |   3 +-
 builtin/gc.c               |   3 +-
 builtin/hook.c             |  84 +++++++++++++++++++++++
 builtin/merge.c            |   2 +-
 builtin/rebase.c           |   3 +-
 builtin/receive-pack.c     |   7 +-
 builtin/worktree.c         |  27 +++-----
 command-list.txt           |   1 +
 commit.c                   |  15 +++--
 git-p4.py                  |  70 ++-----------------
 git-send-email.perl        |  22 +++---
 git.c                      |   1 +
 hook.c                     | 131 ++++++++++++++++++++++++++++++++++++
 hook.h                     |  57 ++++++++++++++++
 read-cache.c               |   3 +-
 reset.c                    |   3 +-
 run-command.c              |  32 ---------
 run-command.h              |  17 -----
 t/t1800-hook.sh            | 134 +++++++++++++++++++++++++++++++++++++
 t/t9001-send-email.sh      |   4 +-
 27 files changed, 522 insertions(+), 158 deletions(-)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Range-diff against v3:
 1:  02fd699e699 !  1:  d97d6734961 hook: add 'run' subcommand
    @@ Commit message
         builtin/{commit-graph,multi-pack-index}.c.
     
         Some of the implementation here, such as a function being named
    -    run_hooks() when it's tasked with running one hook, to using the
    +    run_hooks_opt() when it's tasked with running one hook, to using the
         run_processes_parallel_tr2() API to run with jobs=1 is somewhere
         between a bit odd and and an overkill for the current features of this
         "hook run" command and the hook.[ch] API.
    @@ builtin/hook.c (new)
     +	int i;
     +	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
     +	const char *hook_name;
    -+	const char *hook_path;
     +	struct option run_options[] = {
     +		OPT_END(),
     +	};
    @@ builtin/hook.c (new)
     +	git_config(git_default_config, NULL);
     +
     +	hook_name = argv[0];
    -+	hook_path = find_hook(hook_name);
    -+	if (!hook_path) {
    -+		error("cannot find a hook named %s", hook_name);
    -+		return 1;
    -+	}
    -+
    -+	ret = run_hooks(hook_name, hook_path, &opt);
    -+	run_hooks_opt_clear(&opt);
    ++	opt.error_if_missing = 1;
    ++	ret = run_hooks_opt(hook_name, &opt);
    ++	if (ret < 0) /* error() return */
    ++		ret = 1;
     +	return ret;
     +usage:
     +	usage_with_options(builtin_hook_run_usage, run_options);
    @@ hook.c: int hook_exists(const char *name)
      	return !!find_hook(name);
      }
     +
    -+void run_hooks_opt_clear(struct run_hooks_opt *o)
    -+{
    -+	strvec_clear(&o->env);
    -+	strvec_clear(&o->args);
    -+}
    -+
     +static int pick_next_hook(struct child_process *cp,
     +			  struct strbuf *out,
     +			  void *pp_cb,
    @@ hook.c: int hook_exists(const char *name)
     +	return 0;
     +}
     +
    -+int run_hooks(const char *hook_name, const char *hook_path,
    -+	      struct run_hooks_opt *options)
    ++static void run_hooks_opt_clear(struct run_hooks_opt *options)
    ++{
    ++	strvec_clear(&options->env);
    ++	strvec_clear(&options->args);
    ++}
    ++
    ++int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
     +{
     +	struct hook_cb_data cb_data = {
     +		.rc = 0,
     +		.hook_name = hook_name,
    -+		.hook_path = hook_path,
     +		.options = options,
     +	};
    ++	const char *const hook_path = find_hook(hook_name);
     +	int jobs = 1;
    ++	int ret = 0;
     +
     +	if (!options)
     +		BUG("a struct run_hooks_opt must be provided to run_hooks");
     +
    ++	if (!hook_path && !options->error_if_missing)
    ++		goto cleanup;
    ++
    ++	if (!hook_path) {
    ++		ret = error("cannot find a hook named %s", hook_name);
    ++		goto cleanup;
    ++	}
    ++
    ++	cb_data.hook_path = hook_path;
     +	run_processes_parallel_tr2(jobs,
     +				   pick_next_hook,
     +				   notify_start_failure,
    @@ hook.c: int hook_exists(const char *name)
     +				   &cb_data,
     +				   "hook",
     +				   hook_name);
    -+
    -+	return cb_data.rc;
    ++	ret = cb_data.rc;
    ++cleanup:
    ++	run_hooks_opt_clear(options);
    ++	return ret;
     +}
     
      ## hook.h ##
    @@ hook.h
     +
     +	/* Args to be passed to each hook */
     +	struct strvec args;
    ++
    ++	/* Emit an error if the hook is missing */
    ++	unsigned int error_if_missing:1;
     +};
     +
     +#define RUN_HOOKS_OPT_INIT { \
    @@ hook.h: const char *find_hook(const char *name);
      int hook_exists(const char *hookname);
      
     +/**
    -+ * Clear data from an initialized "struct run_hooks_opt".
    -+ */
    -+void run_hooks_opt_clear(struct run_hooks_opt *o);
    -+
    -+/**
    -+ * Takes an already resolved hook found via find_hook() and runs
    -+ * it. Does not call run_hooks_opt_clear() for you.
    ++ * Takes a `hook_name`, resolves it to a path with find_hook(), and
    ++ * runs the hook for you with the options specified in "struct
    ++ * run_hooks opt". Will free memory associated with the "struct run_hooks_opt".
    ++ *
    ++ * Returns the status code of the run hook, or a negative value on
    ++ * error().
     + */
    -+int run_hooks(const char *hookname, const char *hook_path,
    -+	      struct run_hooks_opt *options);
    ++int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
      #endif
     
      ## t/t1800-hook.sh (new) ##
 -:  ----------- >  2:  ca6464f7d5e hook API: add a run_hooks() wrapper
 2:  42cc4d2c3c6 !  3:  173860afca1 gc: use hook library for pre-auto-gc hook
    @@ Commit message
         gc: use hook library for pre-auto-gc hook
     
         Move the pre-auto-gc hook away from run-command.h to and over to the
    -    new hook.h library.
    -
    -    To do this introduce a simple run_hooks_oneshot() wrapper, we'll be
    -    using it extensively for these simple cases of wanting to run a single
    -    hook under a given name, and having it free the memory we allocate for
    -    us.
    +    new hook.h library. This uses the new run_hooks() wrapper.
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
    @@ builtin/gc.c: static int need_to_gc(void)
      		return 0;
      
     -	if (run_hook_le(NULL, "pre-auto-gc", NULL))
    -+	if (run_hooks_oneshot("pre-auto-gc", NULL))
    ++	if (run_hooks("pre-auto-gc"))
      		return 0;
      	return 1;
      }
    -
    - ## hook.c ##
    -@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,
    - 
    - 	return cb_data.rc;
    - }
    -+
    -+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)
    -+{
    -+	const char *hook_path;
    -+	struct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;
    -+	int ret = 0;
    -+
    -+	if (!options)
    -+		options = &hook_opt_scratch;
    -+
    -+	hook_path = find_hook(hook_name);
    -+	if (!hook_path)
    -+		goto cleanup;
    -+
    -+	ret = run_hooks(hook_name, hook_path, options);
    -+cleanup:
    -+	run_hooks_opt_clear(options);
    -+
    -+	return ret;
    -+}
    -
    - ## hook.h ##
    -@@ hook.h: void run_hooks_opt_clear(struct run_hooks_opt *o);
    - /**
    -  * Takes an already resolved hook found via find_hook() and runs
    -  * it. Does not call run_hooks_opt_clear() for you.
    -+ *
    -+ * See run_hooks_oneshot() for the simpler one-shot API.
    -  */
    - int run_hooks(const char *hookname, const char *hook_path,
    - 	      struct run_hooks_opt *options);
    -+
    -+/**
    -+ * Calls find_hook() on your "hook_name" and runs the hooks (if any)
    -+ * with run_hooks().
    -+ *
    -+ * If "options" is provided calls run_hooks_opt_clear() on it for
    -+ * you. If "options" is NULL the default options from
    -+ * RUN_HOOKS_OPT_INIT will be used.
    -+ */
    -+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);
    -+
    - #endif
 4:  b26cef24f39 !  4:  80a2171ddaf am: convert applypatch to use hook.h
    @@ Metadata
     Author: Emily Shaffer <emilyshaffer@google.com>
     
      ## Commit message ##
    -    am: convert applypatch to use hook.h
    +    am: convert {pre,post}-applypatch to use hook.h
     
    -    Teach pre-applypatch, post-applypatch, and applypatch-msg to use the
    -    hook.h library instead of the run-command.h library.
    +    Teach pre-applypatch and post-applypatch to use the hook.h library
    +    instead of the run-command.h library.
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## builtin/am.c ##
    -@@ builtin/am.c: static void am_destroy(const struct am_state *state)
    - static int run_applypatch_msg_hook(struct am_state *state)
    - {
    - 	int ret;
    -+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
    - 
    - 	assert(state->msg);
    --	ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
    -+	strvec_push(&opt.args, am_path(state, "final-commit"));
    -+	ret = run_hooks_oneshot("applypatch-msg", &opt);
    - 
    - 	if (!ret) {
    - 		FREE_AND_NULL(state->msg);
     @@ builtin/am.c: static void do_commit(const struct am_state *state)
      	const char *reflog_msg, *author, *committer = NULL;
      	struct strbuf sb = STRBUF_INIT;
      
     -	if (run_hook_le(NULL, "pre-applypatch", NULL))
    -+	if (run_hooks_oneshot("pre-applypatch", NULL))
    ++	if (run_hooks("pre-applypatch"))
      		exit(1);
      
      	if (write_cache_as_tree(&tree, 0, NULL))
    @@ builtin/am.c: static void do_commit(const struct am_state *state)
      	}
      
     -	run_hook_le(NULL, "post-applypatch", NULL);
    -+	run_hooks_oneshot("post-applypatch", NULL);
    ++	run_hooks("post-applypatch");
      
      	strbuf_release(&sb);
      }
 -:  ----------- >  5:  74f459db287 hook API: add a run_hooks_l() wrapper
 3:  cbbfd77a4f6 !  6:  1fd70c0e88a rebase: convert pre-rebase to use hook.h
    @@ Commit message
         Move the pre-rebase hook away from run-command.h to and over to the
         new hook.h library.
     
    +    Since this hook needs arguments introduce a run_hooksl() wrapper, like
    +    run_hooks(), but it takes varargs.
    +
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
    @@ builtin/rebase.c
      #define DEFAULT_REFLOG_ACTION "rebase"
      
     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
    - 	int reschedule_failed_exec = -1;
    - 	int allow_preemptive_ff = 1;
    - 	int preserve_merges_selected = 0;
    -+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
    - 	struct option builtin_rebase_options[] = {
    - 		OPT_STRING(0, "onto", &options.onto_name,
    - 			   N_("revision"),
    -@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
    - 	}
      
      	/* If a hook exists, give it a chance to interrupt*/
    -+	strvec_push(&hook_opt.args, options.upstream_arg);
    -+	if (argc)
    -+		strvec_push(&hook_opt.args, argv[0]);
      	if (!ok_to_skip_pre_rebase &&
     -	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
    --			argc ? argv[0] : NULL, NULL))
    -+	    run_hooks_oneshot("pre-rebase", &hook_opt))
    ++	    run_hooks_l("pre-rebase", options.upstream_arg,
    + 			argc ? argv[0] : NULL, NULL))
      		die(_("The pre-rebase hook refused to rebase."));
      
    - 	if (options.flags & REBASE_DIFFSTAT) {
 -:  ----------- >  7:  ccba3ddf52e am: convert applypatch-msg to use hook.h
 6:  7a9fd8627cd !  8:  2c23e8645ec merge: convert post-merge to use hook.h
    @@ Commit message
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
      ## builtin/merge.c ##
    -@@ builtin/merge.c: static void finish(struct commit *head_commit,
    - 		   const struct object_id *new_head, const char *msg)
    - {
    - 	struct strbuf reflog_message = STRBUF_INIT;
    -+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
    - 	const struct object_id *head = &head_commit->object.oid;
    - 
    - 	if (!msg)
     @@ builtin/merge.c: static void finish(struct commit *head_commit,
      	}
      
      	/* Run a post-merge hook */
     -	run_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL);
    -+	strvec_push(&opt.args, squash ? "1" : "0");
    -+	run_hooks_oneshot("post-merge", &opt);
    ++	run_hooks_l("post-merge", squash ? "1" : "0", NULL);
      
      	apply_autostash(git_path_merge_autostash(the_repository));
      	strbuf_release(&reflog_message);
 -:  ----------- >  9:  cb95c79093b hooks: convert non-worktree 'post-checkout' hook to hook library
 5:  2a747a65829 ! 10:  f330600fec8 hooks: convert 'post-checkout' hook to hook library
    @@ Metadata
     Author: Emily Shaffer <emilyshaffer@google.com>
     
      ## Commit message ##
    -    hooks: convert 'post-checkout' hook to hook library
    +    hooks: convert worktree 'post-checkout' hook to hook library
     
         Move the running of the 'post-checkout' hook away from run-command.h
    -    to the new hook.h library. For "worktree" this requires a change to it
    -    to run the hooks from a given directory.
    +    to the new hook.h library in builtin/worktree.c. For this special case
    +    we need a change to the hook API to teach it to run the hook from a
    +    given directory.
     
    -    We could strictly speaking skip the "absolute_path" flag and just
    -    check if "dir" is specified, but let's split them up for clarity, as
    -    well as for any future user who'd like to set "dir" but not implicitly
    -    change the argument to an absolute path.
    +    We cannot skip the "absolute_path" flag and just check if "dir" is
    +    specified as we'd then fail to find our hook in the new dir we'd
    +    chdir() to. We currently don't have a use-case for running a hook not
    +    in our "base" repository at a given absolute path, so let's have "dir"
    +    imply absolute_path(find_hook(hook_name)).
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
    - ## builtin/checkout.c ##
    -@@
    - #include "config.h"
    - #include "diff.h"
    - #include "dir.h"
    -+#include "hook.h"
    - #include "ll-merge.h"
    - #include "lockfile.h"
    - #include "merge-recursive.h"
    -@@ builtin/checkout.c: struct branch_info {
    - static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
    - 			      int changed)
    - {
    --	return run_hook_le(NULL, "post-checkout",
    --			   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
    --			   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
    --			   changed ? "1" : "0", NULL);
    -+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
    -+
    - 	/* "new_commit" can be NULL when checking out from the index before
    - 	   a commit exists. */
    --
    -+	strvec_pushl(&opt.args,
    -+		     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
    -+		     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
    -+		     changed ? "1" : "0",
    -+		     NULL);
    -+	return run_hooks_oneshot("post-checkout", &opt);
    - }
    - 
    - static int update_some(const struct object_id *oid, struct strbuf *base,
    -
    - ## builtin/clone.c ##
    -@@
    - #include "connected.h"
    - #include "packfile.h"
    - #include "list-objects-filter-options.h"
    -+#include "hook.h"
    - 
    - /*
    -  * Overall FIXMEs:
    -@@ builtin/clone.c: static int checkout(int submodule_progress)
    - 	struct tree *tree;
    - 	struct tree_desc t;
    - 	int err = 0;
    -+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
    - 
    - 	if (option_no_checkout)
    - 		return 0;
    -@@ builtin/clone.c: static int checkout(int submodule_progress)
    - 	if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
    - 		die(_("unable to write new index file"));
    - 
    --	err |= run_hook_le(NULL, "post-checkout", oid_to_hex(null_oid()),
    --			   oid_to_hex(&oid), "1", NULL);
    -+	strvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid),
    -+		     "1", NULL);
    -+	err |= run_hooks_oneshot("post-checkout", &hook_opt);
    - 
    - 	if (!err && (option_recurse_submodules.nr > 0)) {
    - 		struct strvec args = STRVEC_INIT;
    -
      ## builtin/worktree.c ##
     @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refname,
      	 * is_junk is cleared, but do return appropriate code when hook fails.
    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam
     +			     "1",
     +			     NULL);
     +		opt.dir = path;
    -+		opt.absolute_path = 1;
     +
    -+		ret = run_hooks_oneshot("post-checkout", &opt);
    ++		ret = run_hooks_opt("post-checkout", &opt);
      	}
      
      	strvec_clear(&child_env);
    @@ hook.c: static int pick_next_hook(struct child_process *cp,
      
      	strvec_push(&cp->args, hook_path);
      	strvec_pushv(&cp->args, hook_cb->options->args.v);
    -@@ hook.c: static int notify_hook_finished(int result,
    - int run_hooks(const char *hook_name, const char *hook_path,
    - 	      struct run_hooks_opt *options)
    +@@ hook.c: static void run_hooks_opt_clear(struct run_hooks_opt *options)
    + 
    + int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
      {
     +	struct strbuf abs_path = STRBUF_INIT;
      	struct hook_cb_data cb_data = {
      		.rc = 0,
      		.hook_name = hook_name,
    --		.hook_path = hook_path,
    - 		.options = options,
    - 	};
    - 	int jobs = 1;
    -@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,
    - 	if (!options)
    - 		BUG("a struct run_hooks_opt must be provided to run_hooks");
    +@@ hook.c: int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
    + 	}
      
    -+	if (options->absolute_path) {
    + 	cb_data.hook_path = hook_path;
    ++	if (options->dir) {
     +		strbuf_add_absolute_path(&abs_path, hook_path);
    -+		hook_path = abs_path.buf;
    ++		cb_data.hook_path = abs_path.buf;
     +	}
    -+	cb_data.hook_path = hook_path;
     +
      	run_processes_parallel_tr2(jobs,
      				   pick_next_hook,
      				   notify_start_failure,
    -@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,
    - 				   "hook",
    +@@ hook.c: int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
      				   hook_name);
    - 
    -+	if (options->absolute_path)
    -+		strbuf_release(&abs_path);
    -+
    - 	return cb_data.rc;
    + 	ret = cb_data.rc;
    + cleanup:
    ++	strbuf_release(&abs_path);
    + 	run_hooks_opt_clear(options);
    + 	return ret;
      }
    - 
     
      ## hook.h ##
     @@ hook.h: struct run_hooks_opt
      
    - 	/* Args to be passed to each hook */
    - 	struct strvec args;
    + 	/* Emit an error if the hook is missing */
    + 	unsigned int error_if_missing:1;
     +
    -+	/*
    -+	 * Resolve and run the "absolute_path(hook)" instead of
    -+	 * "hook". Used for "git worktree" hooks
    ++	/**
    ++	 * An optional initial working directory for the hook,
    ++	 * translates to "struct child_process"'s "dir" member.
     +	 */
    -+	int absolute_path;
    -+
    -+	/* Path to initial working directory for subprocess */
     +	const char *dir;
      };
      
      #define RUN_HOOKS_OPT_INIT { \
    -
    - ## reset.c ##
    -@@
    - #include "tree-walk.h"
    - #include "tree.h"
    - #include "unpack-trees.h"
    -+#include "hook.h"
    - 
    - int reset_head(struct repository *r, struct object_id *oid, const char *action,
    - 	       const char *switch_to_branch, unsigned flags,
    -@@ reset.c: int reset_head(struct repository *r, struct object_id *oid, const char *action,
    - 			ret = create_symref("HEAD", switch_to_branch,
    - 					    reflog_head);
    - 	}
    --	if (run_hook)
    --		run_hook_le(NULL, "post-checkout",
    --			    oid_to_hex(orig ? orig : null_oid()),
    --			    oid_to_hex(oid), "1", NULL);
    -+	if (run_hook) {
    -+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
    -+		strvec_pushl(&opt.args,
    -+			     oid_to_hex(orig ? orig : null_oid()),
    -+			     oid_to_hex(oid),
    -+			     "1",
    -+			     NULL);
    -+		run_hooks_oneshot("post-checkout", &opt);
    -+	}
    - 
    - leave_reset_head:
    - 	strbuf_release(&msg);
 7:  840fb530df3 ! 11:  a0b6818c766 git hook run: add an --ignore-missing flag
    @@ Commit message
         git hook run: add an --ignore-missing flag
     
         For certain one-shot hooks we'd like to optimistically run them, and
    -    not complain if they don't exist. This will be used by send-email in a
    -    subsequent commit.
    +    not complain if they don't exist.
    +
    +    This was already supported by the underlying hook.c library, but had
    +    not been exposed via "git hook run". The command version of this will
    +    be used by send-email in a subsequent commit.
     
         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
     
    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)
      	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
     +	int ignore_missing = 0;
      	const char *hook_name;
    - 	const char *hook_path;
      	struct option run_options[] = {
     +		OPT_BOOL(0, "ignore-missing", &ignore_missing,
     +			 N_("silently ignore missing requested <hook-name>")),
    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)
      	git_config(git_default_config, NULL);
      
      	hook_name = argv[0];
    -+	if (ignore_missing)
    -+		return run_hooks_oneshot(hook_name, &opt);
    - 	hook_path = find_hook(hook_name);
    - 	if (!hook_path) {
    - 		error("cannot find a hook named %s", hook_name);
    +-	opt.error_if_missing = 1;
    ++	if (!ignore_missing)
    ++		opt.error_if_missing = 1;
    + 	ret = run_hooks_opt(hook_name, &opt);
    + 	if (ret < 0) /* error() return */
    + 		ret = 1;
     
      ## t/t1800-hook.sh ##
     @@ t/t1800-hook.sh: test_expect_success 'git hook run: nonexistent hook' '
 8:  716ebabd794 = 12:  efa35971e9f send-email: use 'git hook run' for 'sendemail-validate'
 9:  95782109270 = 13:  98e0e3330fb git-p4: use 'git hook' to run hooks
10:  706426c8a79 ! 14:  79ea5a2a4f5 commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
    @@ commit.c: size_t ignore_non_trailer(const char *buf, size_t len)
     -	strvec_clear(&hook_env);
      
     -	return ret;
    -+	return run_hooks_oneshot(name, &opt);
    ++	return run_hooks_opt(name, &opt);
      }
11:  39069a9c3ff ! 15:  81612f94707 read-cache: convert post-index-change to use hook.h
    @@ read-cache.c
      
      /* Mask for the name length in ce_flags in the on-disk index */
      
    -@@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struct lock_file *l
    - {
    - 	int ret;
    - 	int was_full = !istate->sparse_index;
    -+	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;
    - 
    - 	ret = convert_to_sparse(istate, 0);
    - 
     @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struct lock_file *l
      	else
      		ret = close_lock_file_gently(lock);
      
     -	run_hook_le(NULL, "post-index-change",
    --			istate->updated_workdir ? "1" : "0",
    --			istate->updated_skipworktree ? "1" : "0", NULL);
    -+	strvec_pushl(&hook_opt.args,
    -+		     istate->updated_workdir ? "1" : "0",
    -+		     istate->updated_skipworktree ? "1" : "0",
    -+		     NULL);
    -+	run_hooks_oneshot("post-index-change", &hook_opt);
    -+
    ++	run_hooks_l("post-index-change",
    + 			istate->updated_workdir ? "1" : "0",
    + 			istate->updated_skipworktree ? "1" : "0", NULL);
      	istate->updated_workdir = 0;
    - 	istate->updated_skipworktree = 0;
    - 
     
      ## run-command.c ##
     @@ run-command.c: int async_with_fork(void)
12:  9818078f1e5 ! 16:  43ecd6697e0 receive-pack: convert push-to-checkout hook to hook.h
    @@ builtin/receive-pack.c: static const char *push_to_checkout(unsigned char *hash,
     -			hash_to_hex(hash), NULL))
     +	strvec_pushv(&opt.env, env->v);
     +	strvec_push(&opt.args, hash_to_hex(hash));
    -+	if (run_hooks_oneshot(push_to_checkout_hook, &opt))
    ++	if (run_hooks_opt(push_to_checkout_hook, &opt))
      		return "push-to-checkout hook declined";
      	else
      		return NULL;
13:  1bc080d3611 = 17:  9ef574fa30c run-command: remove old run_hook_{le,ve}() hook API
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 02/17] hook API: add a run_hooks() wrapper

Add a run_hooks() wrapper, we'll use it in subsequent commits for the simple cases of wanting to run a single hook under a given name, without providing options such as "env" or "args".

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 hook.c | 7 +++++++
 hook.h | 6 ++++++
 2 files changed, 13 insertions(+)
Show changes to 2 files +13 −0

hook.c, hook.h

diff --git a/hook.c b/hook.c
index 64d3608e45e..720994bf4f6 100644
--- a/hook.c
+++ b/hook.c
@@ -142,3 +142,10 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 	run_hooks_opt_clear(options);
 	return ret;
 }
+
+int run_hooks(const char *hook_name)
+{
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+	return run_hooks_opt(hook_name, &opt);
+}
diff --git a/hook.h b/hook.h
index 782385cc235..9c358789958 100644
--- a/hook.h
+++ b/hook.h
@@ -48,4 +48,10 @@ int hook_exists(const char *hookname);
  * error().
  */
 int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
+
+/**
+ * A wrapper for run_hooks_opt() which provides a dummy "struct
+ * run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
+ */
+int run_hooks(const char *hook_name);
 #endif
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 09/17] hooks: convert non-worktree 'post-checkout' hook to hook library

From: Emily Shaffer <emilyshaffer@google.com>

Move the running of the 'post-checkout' hook away from run-command.h to the new hook.h library, except in the case of builtin/worktree.c. That special-case will be handled in a subsequent commit.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/checkout.c | 3 ++-
 builtin/clone.c    | 3 ++-
 reset.c            | 3 ++-
 3 files changed, 6 insertions(+), 3 deletions(-)
Show changes to 3 files +6 −3

builtin/checkout.c, builtin/clone.c, reset.c

diff --git a/builtin/checkout.c b/builtin/checkout.c
index cbf73b8c9f6..4af17d17217 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -9,6 +9,7 @@
 #include "config.h"
 #include "diff.h"
 #include "dir.h"
+#include "hook.h"
 #include "ll-merge.h"
 #include "lockfile.h"
 #include "merge-recursive.h"
@@ -106,7 +107,7 @@ struct branch_info {
 static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
 			      int changed)
 {
-	return run_hook_le(NULL, "post-checkout",
+	return run_hooks_l("post-checkout",
 			   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
 			   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
 			   changed ? "1" : "0", NULL);
diff --git a/builtin/clone.c b/builtin/clone.c
index fb377b27657..ee27b9f8114 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -32,6 +32,7 @@
 #include "connected.h"
 #include "packfile.h"
 #include "list-objects-filter-options.h"
+#include "hook.h"
 
 /*
  * Overall FIXMEs:
@@ -705,7 +706,7 @@ static int checkout(int submodule_progress)
 	if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
 		die(_("unable to write new index file"));
 
-	err |= run_hook_le(NULL, "post-checkout", oid_to_hex(null_oid()),
+	err |= run_hooks_l("post-checkout", oid_to_hex(null_oid()),
 			   oid_to_hex(&oid), "1", NULL);
 
 	if (!err && (option_recurse_submodules.nr > 0)) {
diff --git a/reset.c b/reset.c
index f214df3d96c..0881e636915 100644
--- a/reset.c
+++ b/reset.c
@@ -7,6 +7,7 @@
 #include "tree-walk.h"
 #include "tree.h"
 #include "unpack-trees.h"
+#include "hook.h"
 
 int reset_head(struct repository *r, struct object_id *oid, const char *action,
 	       const char *switch_to_branch, unsigned flags,
@@ -127,7 +128,7 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,
 					    reflog_head);
 	}
 	if (run_hook)
-		run_hook_le(NULL, "post-checkout",
+		run_hooks_l("post-checkout",
 			    oid_to_hex(orig ? orig : null_oid()),
 			    oid_to_hex(oid), "1", NULL);
 
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 01/17] hook: add 'run' subcommand

From: Emily Shaffer <emilyshaffer@google.com>

In order to enable hooks to be run as an external process, by a standalone Git command, or by tools which wrap Git, provide an external means to run all configured hook commands for a given hook event.

Most of our hooks require more complex functionality than this, but let's start with the bare minimum required to support our simplest hooks.

In terms of implementation the usage_with_options() and "goto usage" pattern here mirrors that of builtin/{commit-graph,multi-pack-index}.c.

Some of the implementation here, such as a function being named run_hooks_opt() when it's tasked with running one hook, to using the run_processes_parallel_tr2() API to run with jobs=1 is somewhere between a bit odd and and an overkill for the current features of this "hook run" command and the hook.[ch] API.

This code will eventually be able to run multiple hooks declared in config in parallel, by starting out with these names and APIs we reduce the later churn of renaming functions, switching from the run_command() to run_processes_parallel_tr2() API etc.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  37 +++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/hook.c             |  80 +++++++++++++++++++++++
 command-list.txt           |   1 +
 git.c                      |   1 +
 hook.c                     | 102 +++++++++++++++++++++++++++++
 hook.h                     |  35 ++++++++++
 t/t1800-hook.sh            | 129 +++++++++++++++++++++++++++++++++++++
 11 files changed, 392 insertions(+)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Show changes to 11 files +392 −0

.gitignore, Documentation/git-hook.txt, Documentation/githooks.txt, Makefile, builtin.h, builtin/hook.c, command-list.txt, git.c, hook.c, hook.h, t/t1800-hook.sh

diff --git a/.gitignore b/.gitignore
index 054249b20a8..f817c509ec0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@
 /git-grep
 /git-hash-object
 /git-help
+/git-hook
 /git-http-backend
 /git-http-fetch
 /git-http-push
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
new file mode 100644
index 00000000000..e39b1b5d069
--- /dev/null
+++ b/Documentation/git-hook.txt
@@ -0,0 +1,37 @@
+git-hook(1)
+===========
+
+NAME
+----
+git-hook - Run git hooks
+
+SYNOPSIS
+--------
+[verse]
+'git hook' run <hook-name> [-- <hook-args>]
+
+DESCRIPTION
+-----------
+
+A command interface to running git hooks (see linkgit:githooks[5]),
+for use by other scripted git commands.
+
+SUBCOMMANDS
+-----------
+
+run::
+	Run the `<hook-name>` hook. See linkgit:githooks[5] for
+	supported hook names.
++
+
+Any positional arguments to the hook should be passed after a
+mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
+linkgit:githooks[5] for arguments hooks might expect (if any).
+
+SEE ALSO
+--------
+linkgit:githooks[5]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index b51959ff941..a16e62bc8c8 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -698,6 +698,10 @@ and "0" meaning they were not.
 Only one parameter should be set to "1" when the hook runs.  The hook
 running passing "1", "1" should not be possible.
 
+SEE ALSO
+--------
+linkgit:git-hook[1]
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 12be39ac497..6cd20534304 100644
--- a/Makefile
+++ b/Makefile
@@ -1107,6 +1107,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
 BUILTIN_OBJS += builtin/grep.o
 BUILTIN_OBJS += builtin/hash-object.o
 BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/hook.o
 BUILTIN_OBJS += builtin/index-pack.o
 BUILTIN_OBJS += builtin/init-db.o
 BUILTIN_OBJS += builtin/interpret-trailers.o
diff --git a/builtin.h b/builtin.h
index 8a58743ed63..83379f3832c 100644
--- a/builtin.h
+++ b/builtin.h
@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);
 int cmd_grep(int argc, const char **argv, const char *prefix);
 int cmd_hash_object(int argc, const char **argv, const char *prefix);
 int cmd_help(int argc, const char **argv, const char *prefix);
+int cmd_hook(int argc, const char **argv, const char *prefix);
 int cmd_index_pack(int argc, const char **argv, const char *prefix);
 int cmd_init_db(int argc, const char **argv, const char *prefix);
 int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);
diff --git a/builtin/hook.c b/builtin/hook.c
new file mode 100644
index 00000000000..9b67ff50cef
--- /dev/null
+++ b/builtin/hook.c
@@ -0,0 +1,80 @@
+#include "cache.h"
+#include "builtin.h"
+#include "config.h"
+#include "hook.h"
+#include "parse-options.h"
+#include "strbuf.h"
+#include "strvec.h"
+
+#define BUILTIN_HOOK_RUN_USAGE \
+	N_("git hook run <hook-name> [-- <hook-args>]")
+
+static const char * const builtin_hook_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static const char * const builtin_hook_run_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static int run(int argc, const char **argv, const char *prefix)
+{
+	int i;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	const char *hook_name;
+	struct option run_options[] = {
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, run_options,
+			     builtin_hook_run_usage,
+			     PARSE_OPT_KEEP_DASHDASH);
+
+	if (!argc)
+		goto usage;
+
+	/*
+	 * Having a -- for "run" when providing <hook-args> is
+	 * mandatory.
+	 */
+	if (argc > 1 && strcmp(argv[1], "--") &&
+	    strcmp(argv[1], "--end-of-options"))
+		goto usage;
+
+	/* Add our arguments, start after -- */
+	for (i = 2 ; i < argc; i++)
+		strvec_push(&opt.args, argv[i]);
+
+	/* Need to take into account core.hooksPath */
+	git_config(git_default_config, NULL);
+
+	hook_name = argv[0];
+	opt.error_if_missing = 1;
+	ret = run_hooks_opt(hook_name, &opt);
+	if (ret < 0) /* error() return */
+		ret = 1;
+	return ret;
+usage:
+	usage_with_options(builtin_hook_run_usage, run_options);
+}
+
+int cmd_hook(int argc, const char **argv, const char *prefix)
+{
+	struct option builtin_hook_options[] = {
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, NULL, builtin_hook_options,
+			     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+	if (!argc)
+		goto usage;
+
+	if (!strcmp(argv[0], "run"))
+		return run(argc, argv, prefix);
+
+usage:
+	usage_with_options(builtin_hook_usage, builtin_hook_options);
+}
diff --git a/command-list.txt b/command-list.txt
index eb9cee8dee9..0c9af14bdf3 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info
 git-gui                                 mainporcelain
 git-hash-object                         plumbingmanipulators
 git-help                                ancillaryinterrogators          complete
+git-hook                                purehelpers
 git-http-backend                        synchingrepositories
 git-http-fetch                          synchelpers
 git-http-push                           synchelpers
diff --git a/git.c b/git.c
index 5ff21be21f3..a5be02b04b8 100644
--- a/git.c
+++ b/git.c
@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {
 	{ "grep", cmd_grep, RUN_SETUP_GENTLY },
 	{ "hash-object", cmd_hash_object },
 	{ "help", cmd_help },
+	{ "hook", cmd_hook, RUN_SETUP },
 	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
 	{ "init", cmd_init_db },
 	{ "init-db", cmd_init_db },
diff --git a/hook.c b/hook.c
index 55e1145a4b7..64d3608e45e 100644
--- a/hook.c
+++ b/hook.c
@@ -1,6 +1,7 @@
 #include "cache.h"
 #include "hook.h"
 #include "run-command.h"
+#include "config.h"
 
 const char *find_hook(const char *name)
 {
@@ -40,3 +41,104 @@ int hook_exists(const char *name)
 {
 	return !!find_hook(name);
 }
+
+static int pick_next_hook(struct child_process *cp,
+			  struct strbuf *out,
+			  void *pp_cb,
+			  void **pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = hook_cb->hook_path;
+
+	if (!hook_path)
+		return 0;
+
+	cp->no_stdin = 1;
+	cp->env = hook_cb->options->env.v;
+	cp->stdout_to_stderr = 1;
+	cp->trace2_hook_name = hook_cb->hook_name;
+
+	strvec_push(&cp->args, hook_path);
+	strvec_pushv(&cp->args, hook_cb->options->args.v);
+
+	/* Provide context for errors if necessary */
+	*pp_task_cb = (char *)hook_path;
+
+	/*
+	 * This pick_next_hook() will be called again, we're only
+	 * running one hook, so indicate that no more work will be
+	 * done.
+	 */
+	hook_cb->hook_path = NULL;
+
+	return 1;
+}
+
+static int notify_start_failure(struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cp)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = pp_task_cp;
+
+	hook_cb->rc |= 1;
+
+	strbuf_addf(out, _("Couldn't start hook '%s'\n"),
+		    hook_path);
+
+	return 1;
+}
+
+static int notify_hook_finished(int result,
+				struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+
+	hook_cb->rc |= result;
+
+	return 0;
+}
+
+static void run_hooks_opt_clear(struct run_hooks_opt *options)
+{
+	strvec_clear(&options->env);
+	strvec_clear(&options->args);
+}
+
+int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
+{
+	struct hook_cb_data cb_data = {
+		.rc = 0,
+		.hook_name = hook_name,
+		.options = options,
+	};
+	const char *const hook_path = find_hook(hook_name);
+	int jobs = 1;
+	int ret = 0;
+
+	if (!options)
+		BUG("a struct run_hooks_opt must be provided to run_hooks");
+
+	if (!hook_path && !options->error_if_missing)
+		goto cleanup;
+
+	if (!hook_path) {
+		ret = error("cannot find a hook named %s", hook_name);
+		goto cleanup;
+	}
+
+	cb_data.hook_path = hook_path;
+	run_processes_parallel_tr2(jobs,
+				   pick_next_hook,
+				   notify_start_failure,
+				   notify_hook_finished,
+				   &cb_data,
+				   "hook",
+				   hook_name);
+	ret = cb_data.rc;
+cleanup:
+	run_hooks_opt_clear(options);
+	return ret;
+}
diff --git a/hook.h b/hook.h
index 6aa36fc7ff9..782385cc235 100644
--- a/hook.h
+++ b/hook.h
@@ -1,5 +1,31 @@
 #ifndef HOOK_H
 #define HOOK_H
+#include "strvec.h"
+
+struct run_hooks_opt
+{
+	/* Environment vars to be set for each hook */
+	struct strvec env;
+
+	/* Args to be passed to each hook */
+	struct strvec args;
+
+	/* Emit an error if the hook is missing */
+	unsigned int error_if_missing:1;
+};
+
+#define RUN_HOOKS_OPT_INIT { \
+	.env = STRVEC_INIT, \
+	.args = STRVEC_INIT, \
+}
+
+struct hook_cb_data {
+	/* rc reflects the cumulative failure state */
+	int rc;
+	const char *hook_name;
+	const char *hook_path;
+	struct run_hooks_opt *options;
+};
 
 /*
  * Returns the path to the hook file, or NULL if the hook is missing
@@ -13,4 +39,13 @@ const char *find_hook(const char *name);
  */
 int hook_exists(const char *hookname);
 
+/**
+ * Takes a `hook_name`, resolves it to a path with find_hook(), and
+ * runs the hook for you with the options specified in "struct
+ * run_hooks opt". Will free memory associated with the "struct run_hooks_opt".
+ *
+ * Returns the status code of the run hook, or a negative value on
+ * error().
+ */
+int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
 #endif
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
new file mode 100755
index 00000000000..3aea1b105f0
--- /dev/null
+++ b/t/t1800-hook.sh
@@ -0,0 +1,129 @@
+#!/bin/sh
+
+test_description='git-hook command'
+
+TEST_PASSES_SANITIZE_LEAK=true
+. ./test-lib.sh
+
+test_expect_success 'git hook usage' '
+	test_expect_code 129 git hook &&
+	test_expect_code 129 git hook run &&
+	test_expect_code 129 git hook run -h &&
+	test_expect_code 129 git hook run --unknown 2>err &&
+	grep "unknown option" err
+'
+
+test_expect_success 'git hook run: nonexistent hook' '
+	cat >stderr.expect <<-\EOF &&
+	error: cannot find a hook named test-hook
+	EOF
+	test_expect_code 1 git hook run test-hook 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual
+'
+
+test_expect_success 'git hook run: basic' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	EOF
+	git hook run test-hook 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run: stdout and stderr both write to our stderr' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo >&1 Will end up on stderr
+	echo >&2 Will end up on stderr
+	EOF
+
+	cat >stderr.expect <<-\EOF &&
+	Will end up on stderr
+	Will end up on stderr
+	EOF
+	git hook run test-hook >stdout.actual 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual &&
+	test_must_be_empty stdout.actual
+'
+
+test_expect_success 'git hook run: exit codes are passed along' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 1
+	EOF
+
+	test_expect_code 1 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 2
+	EOF
+
+	test_expect_code 2 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 128
+	EOF
+
+	test_expect_code 128 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 129
+	EOF
+
+	test_expect_code 129 git hook run test-hook
+'
+
+test_expect_success 'git hook run arg u ments without -- is not allowed' '
+	test_expect_code 129 git hook run test-hook arg u ments
+'
+
+test_expect_success 'git hook run -- pass arguments' '
+	write_script .git/hooks/test-hook <<-\EOF &&
+	echo $1
+	echo $2
+	EOF
+
+	cat >expect <<-EOF &&
+	arg
+	u ments
+	EOF
+
+	git hook run test-hook -- arg "u ments" 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run -- out-of-repo runs excluded' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	nongit test_must_fail git hook run test-hook
+'
+
+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
+	mkdir my-hooks &&
+	write_script my-hooks/test-hook <<-\EOF &&
+	echo Hook ran $1 >>actual
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	Hook ran one
+	Hook ran two
+	Hook ran three
+	Hook ran four
+	EOF
+
+	# Test various ways of specifying the path. See also
+	# t1350-config-hooks-path.sh
+	>actual &&
+	git hook run test-hook -- ignored 2>>actual &&
+	git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&
+	git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks/" hook run test-hook -- four 2>>actual &&
+	test_cmp expect actual
+'
+
+test_done
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 05/17] hook API: add a run_hooks_l() wrapper

Add a run_hooks_l() wrapper, we'll use it in subsequent commits for the simple cases of wanting to run a single hook under a given name along with a list of arguments.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 hook.c | 14 ++++++++++++++
 hook.h | 10 ++++++++++
 2 files changed, 24 insertions(+)
Show changes to 2 files +24 −0

hook.c, hook.h

diff --git a/hook.c b/hook.c
index 720994bf4f6..cf6d099f83a 100644
--- a/hook.c
+++ b/hook.c
@@ -149,3 +149,17 @@ int run_hooks(const char *hook_name)
 
 	return run_hooks_opt(hook_name, &opt);
 }
+
+int run_hooks_l(const char *hook_name, ...)
+{
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	va_list ap;
+	const char *arg;
+
+	va_start(ap, hook_name);
+	while ((arg = va_arg(ap, const char *)))
+		strvec_push(&opt.args, arg);
+	va_end(ap);
+
+	return run_hooks_opt(hook_name, &opt);
+}
diff --git a/hook.h b/hook.h
index 9c358789958..54528395953 100644
--- a/hook.h
+++ b/hook.h
@@ -54,4 +54,14 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
  * run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
  */
 int run_hooks(const char *hook_name);
+
+/**
+ * Like run_hooks(), a wrapper for run_hooks_opt().
+ *
+ * In addition to the wrapping behavior provided by run_hooks(), this
+ * wrapper takes a list of strings terminated by a NULL
+ * argument. These things will be used as positional arguments to the
+ * hook. This function behaves like the old run_hook_le() API.
+ */
+int run_hooks_l(const char *hook_name, ...);
 #endif
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 04/17] am: convert {pre,post}-applypatch to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach pre-applypatch and post-applypatch to use the hook.h library instead of the run-command.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/am.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Show changes to builtin/am.c +2 −2
diff --git a/builtin/am.c b/builtin/am.c
index 8677ea2348a..4b334cb7b12 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -1609,7 +1609,7 @@ static void do_commit(const struct am_state *state)
 	const char *reflog_msg, *author, *committer = NULL;
 	struct strbuf sb = STRBUF_INIT;
 
-	if (run_hook_le(NULL, "pre-applypatch", NULL))
+	if (run_hooks("pre-applypatch"))
 		exit(1);
 
 	if (write_cache_as_tree(&tree, 0, NULL))
@@ -1661,7 +1661,7 @@ static void do_commit(const struct am_state *state)
 		fclose(fp);
 	}
 
-	run_hook_le(NULL, "post-applypatch", NULL);
+	run_hooks("post-applypatch");
 
 	strbuf_release(&sb);
 }
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 03/17] gc: use hook library for pre-auto-gc hook

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-auto-gc hook away from run-command.h to and over to the new hook.h library. This uses the new run_hooks() wrapper.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
Show changes to builtin/gc.c +2 −1
diff --git a/builtin/gc.c b/builtin/gc.c
index 6b3de3dd514..f57cbd2d217 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -32,6 +32,7 @@
 #include "remote.h"
 #include "object-store.h"
 #include "exec-cmd.h"
+#include "hook.h"
 
 #define FAILED_RUN "failed to run %s"
 
@@ -394,7 +395,7 @@ static int need_to_gc(void)
 	else
 		return 0;
 
-	if (run_hook_le(NULL, "pre-auto-gc", NULL))
+	if (run_hooks("pre-auto-gc"))
 		return 0;
 	return 1;
 }
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 07/17] am: convert applypatch-msg to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach applypatch-msg to use the hook.h library instead of the run-command.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/am.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to builtin/am.c +1 −1
diff --git a/builtin/am.c b/builtin/am.c
index 4b334cb7b12..ae0c484dcba 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -448,7 +448,7 @@ static int run_applypatch_msg_hook(struct am_state *state)
 	int ret;
 
 	assert(state->msg);
-	ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
+	ret = run_hooks_l("applypatch-msg", am_path(state, "final-commit"), NULL);
 
 	if (!ret) {
 		FREE_AND_NULL(state->msg);
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 06/17] rebase: convert pre-rebase to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-rebase hook away from run-command.h to and over to the new hook.h library.

Since this hook needs arguments introduce a run_hooksl() wrapper, like run_hooks(), but it takes varargs.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/rebase.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
Show changes to builtin/rebase.c +2 −1
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 34b4744e5f3..ac4120013a0 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -28,6 +28,7 @@
 #include "sequencer.h"
 #include "rebase-interactive.h"
 #include "reset.h"
+#include "hook.h"
 
 #define DEFAULT_REFLOG_ACTION "rebase"
 
@@ -1712,7 +1713,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
 	/* If a hook exists, give it a chance to interrupt*/
 	if (!ok_to_skip_pre_rebase &&
-	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
+	    run_hooks_l("pre-rebase", options.upstream_arg,
 			argc ? argv[0] : NULL, NULL))
 		die(_("The pre-rebase hook refused to rebase."));
 
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 10/17] hooks: convert worktree 'post-checkout' hook to hook library

From: Emily Shaffer <emilyshaffer@google.com>

Move the running of the 'post-checkout' hook away from run-command.h to the new hook.h library in builtin/worktree.c. For this special case we need a change to the hook API to teach it to run the hook from a given directory.

We cannot skip the "absolute_path" flag and just check if "dir" is specified as we'd then fail to find our hook in the new dir we'd chdir() to. We currently don't have a use-case for running a hook not in our "base" repository at a given absolute path, so let's have "dir" imply absolute_path(find_hook(hook_name)).

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/worktree.c | 27 +++++++++++----------------
 hook.c             |  8 ++++++++
 hook.h             |  6 ++++++
 3 files changed, 25 insertions(+), 16 deletions(-)
Show changes to 3 files +25 −16

builtin/worktree.c, hook.c, hook.h

diff --git a/builtin/worktree.c b/builtin/worktree.c
index d22ece93e1a..4d9df5ecc4c 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -382,22 +382,17 @@ static int add_worktree(const char *path, const char *refname,
 	 * is_junk is cleared, but do return appropriate code when hook fails.
 	 */
 	if (!ret && opts->checkout) {
-		const char *hook = find_hook("post-checkout");
-		if (hook) {
-			const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
-			cp.git_cmd = 0;
-			cp.no_stdin = 1;
-			cp.stdout_to_stderr = 1;
-			cp.dir = path;
-			cp.env = env;
-			cp.argv = NULL;
-			cp.trace2_hook_name = "post-checkout";
-			strvec_pushl(&cp.args, absolute_path(hook),
-				     oid_to_hex(null_oid()),
-				     oid_to_hex(&commit->object.oid),
-				     "1", NULL);
-			ret = run_command(&cp);
-		}
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+		strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
+		strvec_pushl(&opt.args,
+			     oid_to_hex(null_oid()),
+			     oid_to_hex(&commit->object.oid),
+			     "1",
+			     NULL);
+		opt.dir = path;
+
+		ret = run_hooks_opt("post-checkout", &opt);
 	}
 
 	strvec_clear(&child_env);
diff --git a/hook.c b/hook.c
index cf6d099f83a..95f5b926354 100644
--- a/hook.c
+++ b/hook.c
@@ -57,6 +57,7 @@ static int pick_next_hook(struct child_process *cp,
 	cp->env = hook_cb->options->env.v;
 	cp->stdout_to_stderr = 1;
 	cp->trace2_hook_name = hook_cb->hook_name;
+	cp->dir = hook_cb->options->dir;
 
 	strvec_push(&cp->args, hook_path);
 	strvec_pushv(&cp->args, hook_cb->options->args.v);
@@ -109,6 +110,7 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
 
 int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 {
+	struct strbuf abs_path = STRBUF_INIT;
 	struct hook_cb_data cb_data = {
 		.rc = 0,
 		.hook_name = hook_name,
@@ -130,6 +132,11 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 	}
 
 	cb_data.hook_path = hook_path;
+	if (options->dir) {
+		strbuf_add_absolute_path(&abs_path, hook_path);
+		cb_data.hook_path = abs_path.buf;
+	}
+
 	run_processes_parallel_tr2(jobs,
 				   pick_next_hook,
 				   notify_start_failure,
@@ -139,6 +146,7 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 				   hook_name);
 	ret = cb_data.rc;
 cleanup:
+	strbuf_release(&abs_path);
 	run_hooks_opt_clear(options);
 	return ret;
 }
diff --git a/hook.h b/hook.h
index 54528395953..18d90aedf14 100644
--- a/hook.h
+++ b/hook.h
@@ -12,6 +12,12 @@ struct run_hooks_opt
 
 	/* Emit an error if the hook is missing */
 	unsigned int error_if_missing:1;
+
+	/**
+	 * An optional initial working directory for the hook,
+	 * translates to "struct child_process"'s "dir" member.
+	 */
+	const char *dir;
 };
 
 #define RUN_HOOKS_OPT_INIT { \
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 08/17] merge: convert post-merge to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach post-merge to use the hook.h library instead of the run-command.h library to run hooks.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/merge.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to builtin/merge.c +1 −1
diff --git a/builtin/merge.c b/builtin/merge.c
index ea3112e0c0b..e6facd1c95d 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -488,7 +488,7 @@ static void finish(struct commit *head_commit,
 	}
 
 	/* Run a post-merge hook */
-	run_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL);
+	run_hooks_l("post-merge", squash ? "1" : "0", NULL);
 
 	apply_autostash(git_path_merge_autostash(the_repository));
 	strbuf_release(&reflog_message);
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 14/17] commit: convert {pre-commit,prepare-commit-msg} hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move these hooks hook away from run-command.h to and over to the new hook.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 commit.c | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)
Show changes to commit.c +8 −7
diff --git a/commit.c b/commit.c
index 551de4903c1..581d7dc216c 100644
--- a/commit.c
+++ b/commit.c
@@ -21,6 +21,7 @@
 #include "commit-reach.h"
 #include "run-command.h"
 #include "shallow.h"
+#include "hook.h"
 
 static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
 
@@ -1700,22 +1701,22 @@ size_t ignore_non_trailer(const char *buf, size_t len)
 int run_commit_hook(int editor_is_used, const char *index_file,
 		    const char *name, ...)
 {
-	struct strvec hook_env = STRVEC_INIT;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 	va_list args;
-	int ret;
+	const char *arg;
 
-	strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
+	strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s", index_file);
 
 	/*
 	 * Let the hook know that no editor will be launched.
 	 */
 	if (!editor_is_used)
-		strvec_push(&hook_env, "GIT_EDITOR=:");
+		strvec_push(&opt.env, "GIT_EDITOR=:");
 
 	va_start(args, name);
-	ret = run_hook_ve(hook_env.v, name, args);
+	while ((arg = va_arg(args, const char *)))
+		strvec_push(&opt.args, arg);
 	va_end(args);
-	strvec_clear(&hook_env);
 
-	return ret;
+	return run_hooks_opt(name, &opt);
 }
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 17/17] run-command: remove old run_hook_{le,ve}() hook API

From: Emily Shaffer <emilyshaffer@google.com>

The new hook.h library has replaced all run-command.h hook-related functionality. So let's delete this dead code.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 run-command.c | 32 --------------------------------
 run-command.h | 16 ----------------
 2 files changed, 48 deletions(-)
Show changes to 2 files +0 −48

run-command.c, run-command.h

diff --git a/run-command.c b/run-command.c
index d92e670c8ed..8a21ff525f3 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1323,38 +1323,6 @@ int async_with_fork(void)
 #endif
 }
 
-static int run_hook_ve(const char *const *env, const char *name, va_list args)
-{
-	struct child_process hook = CHILD_PROCESS_INIT;
-	const char *p;
-
-	p = find_hook(name);
-	if (!p)
-		return 0;
-
-	strvec_push(&hook.args, p);
-	while ((p = va_arg(args, const char *)))
-		strvec_push(&hook.args, p);
-	hook.env = env;
-	hook.no_stdin = 1;
-	hook.stdout_to_stderr = 1;
-	hook.trace2_hook_name = name;
-
-	return run_command(&hook);
-}
-
-int run_hook_le(const char *const *env, const char *name, ...)
-{
-	va_list args;
-	int ret;
-
-	va_start(args, name);
-	ret = run_hook_ve(env, name, args);
-	va_end(args);
-
-	return ret;
-}
-
 struct io_pump {
 	/* initialized by caller */
 	int fd;
diff --git a/run-command.h b/run-command.h
index 3fa7454cf8a..59e1fbff64c 100644
--- a/run-command.h
+++ b/run-command.h
@@ -224,22 +224,6 @@ int finish_command_in_signal(struct child_process *);
  */
 int run_command(struct child_process *);
 
-/**
- * Run a hook.
- * The first argument is a pathname to an index file, or NULL
- * if the hook uses the default index file or no index is needed.
- * The second argument is the name of the hook.
- * The further arguments correspond to the hook arguments.
- * The last argument has to be NULL to terminate the arguments list.
- * If the hook does not exist or is not executable, the return
- * value will be zero.
- * If it is executable, the hook will be executed and the exit
- * status of the hook is returned.
- * On execution, .stdout_to_stderr and .no_stdin will be set.
- */
-LAST_ARG_MUST_BE_NULL
-int run_hook_le(const char *const *env, const char *name, ...);
-
 /*
  * Trigger an auto-gc
  */
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 13/17] git-p4: use 'git hook' to run hooks

From: Emily Shaffer <emilyshaffer@google.com>

Instead of duplicating the behavior of run-command.h:run_hook_le() in Python, we can directly call 'git hook run'. We emulate the existence check with the --ignore-missing flag.

We're dropping the "verbose" handling added in 9f59ca4d6af (git-p4: create new function run_git_hook, 2020-02-11), those who want diagnostic output about how hooks are run are now able to get that via e.g. the trace2 facility and GIT_TRACE=1.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-p4.py | 70 +++++--------------------------------------------------
 1 file changed, 6 insertions(+), 64 deletions(-)
Show changes to git-p4.py +6 −64
diff --git a/git-p4.py b/git-p4.py
index 2b4500226aa..3b54168eb4a 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -208,70 +208,12 @@ def decode_path(path):
 
 def run_git_hook(cmd, param=[]):
     """Execute a hook if the hook exists."""
-    if verbose:
-        sys.stderr.write("Looking for hook: %s\n" % cmd)
-        sys.stderr.flush()
-
-    hooks_path = gitConfig("core.hooksPath")
-    if len(hooks_path) <= 0:
-        hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
-
-    if not isinstance(param, list):
-        param=[param]
-
-    # resolve hook file name, OS depdenent
-    hook_file = os.path.join(hooks_path, cmd)
-    if platform.system() == 'Windows':
-        if not os.path.isfile(hook_file):
-            # look for the file with an extension
-            files = glob.glob(hook_file + ".*")
-            if not files:
-                return True
-            files.sort()
-            hook_file = files.pop()
-            while hook_file.upper().endswith(".SAMPLE"):
-                # The file is a sample hook. We don't want it
-                if len(files) > 0:
-                    hook_file = files.pop()
-                else:
-                    return True
-
-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
-        return True
-
-    return run_hook_command(hook_file, param) == 0
-
-def run_hook_command(cmd, param):
-    """Executes a git hook command
-       cmd = the command line file to be executed. This can be
-       a file that is run by OS association.
-
-       param = a list of parameters to pass to the cmd command
-
-       On windows, the extension is checked to see if it should
-       be run with the Git for Windows Bash shell.  If there
-       is no file extension, the file is deemed a bash shell
-       and will be handed off to sh.exe. Otherwise, Windows
-       will be called with the shell to handle the file assocation.
-
-       For non Windows operating systems, the file is called
-       as an executable.
-    """
-    cli = [cmd] + param
-    use_shell = False
-    if platform.system() == 'Windows':
-        (root,ext) = os.path.splitext(cmd)
-        if ext == "":
-            exe_path = os.environ.get("EXEPATH")
-            if exe_path is None:
-                exe_path = ""
-            else:
-                exe_path = os.path.join(exe_path, "bin")
-            cli = [os.path.join(exe_path, "SH.EXE")] + cli
-        else:
-            use_shell = True
-    return subprocess.call(cli, shell=use_shell)
-
+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]
+    if param:
+        args.append("--")
+        for p in param:
+            args.append(p)
+    return subprocess.call(args) == 0
 
 def write_pipe(c, stdin):
     if verbose:
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 15/17] read-cache: convert post-index-change to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the post-index-change hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of "run_hook_ve()" outside of run-command.c ("run_hook_le()" still uses it). So we can make the function static now. A subsequent commit will remove this code entirely when "run_hook_le()" itself goes away.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 read-cache.c  | 3 ++-
 run-command.c | 2 +-
 run-command.h | 1 -
 3 files changed, 3 insertions(+), 3 deletions(-)
Show changes to 3 files +3 −3

read-cache.c, run-command.c, run-command.h

diff --git a/read-cache.c b/read-cache.c
index f3986596623..4352661549f 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -28,6 +28,7 @@
 #include "sparse-index.h"
 #include "csum-file.h"
 #include "promisor-remote.h"
+#include "hook.h"
 
 /* Mask for the name length in ce_flags in the on-disk index */
 
@@ -3129,7 +3130,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
 	else
 		ret = close_lock_file_gently(lock);
 
-	run_hook_le(NULL, "post-index-change",
+	run_hooks_l("post-index-change",
 			istate->updated_workdir ? "1" : "0",
 			istate->updated_skipworktree ? "1" : "0", NULL);
 	istate->updated_workdir = 0;
diff --git a/run-command.c b/run-command.c
index 7ef5cc712a9..d92e670c8ed 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1323,7 +1323,7 @@ int async_with_fork(void)
 #endif
 }
 
-int run_hook_ve(const char *const *env, const char *name, va_list args)
+static int run_hook_ve(const char *const *env, const char *name, va_list args)
 {
 	struct child_process hook = CHILD_PROCESS_INIT;
 	const char *p;
diff --git a/run-command.h b/run-command.h
index 49878262584..3fa7454cf8a 100644
--- a/run-command.h
+++ b/run-command.h
@@ -239,7 +239,6 @@ int run_command(struct child_process *);
  */
 LAST_ARG_MUST_BE_NULL
 int run_hook_le(const char *const *env, const char *name, ...);
-int run_hook_ve(const char *const *env, const char *name, va_list args);
 
 /*
  * Trigger an auto-gc
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 16/17] receive-pack: convert push-to-checkout hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the push-to-checkout hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of run_hook_le(), so we could remove that function now, but let's leave that to a follow-up cleanup commit.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/receive-pack.c | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)
Show changes to builtin/receive-pack.c +5 −2
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 49b846d9605..5d6d8dd9a26 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1439,9 +1439,12 @@ static const char *push_to_checkout(unsigned char *hash,
 				    struct strvec *env,
 				    const char *work_tree)
 {
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
 	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
-	if (run_hook_le(env->v, push_to_checkout_hook,
-			hash_to_hex(hash), NULL))
+	strvec_pushv(&opt.env, env->v);
+	strvec_push(&opt.args, hash_to_hex(hash));
+	if (run_hooks_opt(push_to_checkout_hook, &opt))
 		return "push-to-checkout hook declined";
 	else
 		return NULL;
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 12/17] send-email: use 'git hook run' for 'sendemail-validate'

From: Emily Shaffer <emilyshaffer@google.com>

Change the "sendmail-validate" hook to be run via the "git hook run" wrapper instead of via a direct invocation.

This is the smallest possibly change to get "send-email" using "git hook run". We still check the hook itself with "-x", and set a "GIT_DIR" variable, both of which are asserted by our tests. We'll need to get rid of this special behavior if we start running N hooks, but for now let's be as close to bug-for-bug compatible as possible.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl   | 22 ++++++++++++++--------
 t/t9001-send-email.sh |  4 ++--
 2 files changed, 16 insertions(+), 10 deletions(-)
Show changes to 2 files +16 −10

git-send-email.perl, t/t9001-send-email.sh

diff --git a/git-send-email.perl b/git-send-email.perl
index 5262d88ee32..4c20c0bbb79 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -195,13 +195,13 @@ sub format_2822_time {
 my $editor;
 
 sub system_or_msg {
-	my ($args, $msg) = @_;
+	my ($args, $msg, $cmd_name) = @_;
 	system(@$args);
 	my $signalled = $? & 127;
 	my $exit_code = $? >> 8;
 	return unless $signalled or $exit_code;
 
-	my @sprintf_args = ($args->[0], $exit_code);
+	my @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);
 	if (defined $msg) {
 		# Quiet the 'redundant' warning category, except we
 		# need to support down to Perl 5.8, so we can't do a
@@ -2039,10 +2039,10 @@ sub validate_patch {
 	my ($fn, $xfer_encoding) = @_;
 
 	if ($repo) {
+		my $hook_name = 'sendemail-validate';
 		my $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');
 		require File::Spec;
-		my $validate_hook = File::Spec->catfile($hooks_path,
-					    'sendemail-validate');
+		my $validate_hook = File::Spec->catfile($hooks_path, $hook_name);
 		my $hook_error;
 		if (-x $validate_hook) {
 			require Cwd;
@@ -2052,13 +2052,19 @@ sub validate_patch {
 			chdir($repo->wc_path() or $repo->repo_path())
 				or die("chdir: $!");
 			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			$hook_error = system_or_msg([$validate_hook, $target]);
+			my @cmd = ("git", "hook", "run", "--ignore-missing",
+				    $hook_name, "--");
+			my @cmd_msg = (@cmd, "<patch>");
+			my @cmd_run = (@cmd, $target);
+			$hook_error = system_or_msg(\@cmd_run, undef, "@cmd_msg");
 			chdir($cwd_save) or die("chdir: $!");
 		}
 		if ($hook_error) {
-			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
-				       "%s\n" .
-				       "warning: no patches were sent\n"), $fn, $hook_error);
+			$hook_error = sprintf(__("fatal: %s: rejected by %s hook\n" .
+						 $hook_error . "\n" .
+						 "warning: no patches were sent\n"),
+					      $fn, $hook_name);
+			die $hook_error;
 		}
 	}
 
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index aa0c20499ba..84d0f40d76a 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -539,7 +539,7 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'my-hooks/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
@@ -558,7 +558,7 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 1, 2021, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v4 11/17] git hook run: add an --ignore-missing flag

For certain one-shot hooks we'd like to optimistically run them, and not complain if they don't exist.

This was already supported by the underlying hook.c library, but had not been exposed via "git hook run". The command version of this will be used by send-email in a subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Documentation/git-hook.txt | 10 +++++++++-
 builtin/hook.c             |  8 ++++++--
 t/t1800-hook.sh            |  5 +++++
 3 files changed, 20 insertions(+), 3 deletions(-)
Show changes to 3 files +20 −3

Documentation/git-hook.txt, builtin/hook.c, t/t1800-hook.sh

diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index e39b1b5d069..77c3a8ad909 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -8,7 +8,7 @@ git-hook - Run git hooks
 SYNOPSIS
 --------
 [verse]
-'git hook' run <hook-name> [-- <hook-args>]
+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]
 
 DESCRIPTION
 -----------
@@ -28,6 +28,14 @@ Any positional arguments to the hook should be passed after a
 mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
 linkgit:githooks[5] for arguments hooks might expect (if any).
 
+OPTIONS
+-------
+
+--ignore-missing::
+	Ignore any missing hook by quietly returning zero. Used for
+	tools that want to do a blind one-shot run of a hook that may
+	or may not be present.
+
 SEE ALSO
 --------
 linkgit:githooks[5]
diff --git a/builtin/hook.c b/builtin/hook.c
index 9b67ff50cef..54e5c6ec933 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -7,7 +7,7 @@
 #include "strvec.h"
 
 #define BUILTIN_HOOK_RUN_USAGE \
-	N_("git hook run <hook-name> [-- <hook-args>]")
+	N_("git hook run [--ignore-missing] <hook-name> [-- <hook-args>]")
 
 static const char * const builtin_hook_usage[] = {
 	BUILTIN_HOOK_RUN_USAGE,
@@ -23,8 +23,11 @@ static int run(int argc, const char **argv, const char *prefix)
 {
 	int i;
 	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	int ignore_missing = 0;
 	const char *hook_name;
 	struct option run_options[] = {
+		OPT_BOOL(0, "ignore-missing", &ignore_missing,
+			 N_("silently ignore missing requested <hook-name>")),
 		OPT_END(),
 	};
 	int ret;
@@ -52,7 +55,8 @@ static int run(int argc, const char **argv, const char *prefix)
 	git_config(git_default_config, NULL);
 
 	hook_name = argv[0];
-	opt.error_if_missing = 1;
+	if (!ignore_missing)
+		opt.error_if_missing = 1;
 	ret = run_hooks_opt(hook_name, &opt);
 	if (ret < 0) /* error() return */
 		ret = 1;
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 3aea1b105f0..29718aa9913 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -21,6 +21,11 @@ test_expect_success 'git hook run: nonexistent hook' '
 	test_cmp stderr.expect stderr.actual
 '
 
+test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '
+	git hook run --ignore-missing does-not-exist 2>stderr.actual &&
+	test_must_be_empty stderr.actual
+'
+
 test_expect_success 'git hook run: basic' '
 	write_script .git/hooks/test-hook <<-EOF &&
 	echo Test hook
-- 
2.33.1.1570.g069344fdd45
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:45 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 00/17] hook.[ch]: new library to run hooks + simple hook conversion

A trivial update to the v4 of the series to migrate hook execution to the new hook.c hook execution library. For a meaningful overall summary see v4's CL[1].

The only update here is to stop using the "env" member of "struct child_process" in favor of "env_array".

This is in preparation for a re-roll of another series[2] to remove "argv" (and soon in a re-roll, "env") from that API. Without this update the two would semantically conflict.

1. https://lore.kernel.org/git/cover-v4-00.17-00000000000-20211101T184938Z-avarab@gmail.com/
2. https://lore.kernel.org/git/cover-0.5-00000000000-20211122T153605Z-avarab@gmail.com/
Emily Shaffer (14):
  hook: add 'run' subcommand
  gc: use hook library for pre-auto-gc hook
  am: convert {pre,post}-applypatch to use hook.h
  rebase: convert pre-rebase to use hook.h
  am: convert applypatch-msg to use hook.h
  merge: convert post-merge to use hook.h
  hooks: convert non-worktree 'post-checkout' hook to hook library
  hooks: convert worktree 'post-checkout' hook to hook library
  send-email: use 'git hook run' for 'sendemail-validate'
  git-p4: use 'git hook' to run hooks
  commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
  read-cache: convert post-index-change to use hook.h
  receive-pack: convert push-to-checkout hook to hook.h
  run-command: remove old run_hook_{le,ve}() hook API
Ævar Arnfjörð Bjarmason (3):
  hook API: add a run_hooks() wrapper
  hook API: add a run_hooks_l() wrapper
  git hook run: add an --ignore-missing flag
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  45 +++++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/am.c               |   6 +-
 builtin/checkout.c         |   3 +-
 builtin/clone.c            |   3 +-
 builtin/gc.c               |   3 +-
 builtin/hook.c             |  84 +++++++++++++++++++++++
 builtin/merge.c            |   2 +-
 builtin/rebase.c           |   3 +-
 builtin/receive-pack.c     |   7 +-
 builtin/worktree.c         |  27 +++-----
 command-list.txt           |   1 +
 commit.c                   |  15 +++--
 git-p4.py                  |  70 ++-----------------
 git-send-email.perl        |  22 +++---
 git.c                      |   1 +
 hook.c                     | 131 ++++++++++++++++++++++++++++++++++++
 hook.h                     |  57 ++++++++++++++++
 read-cache.c               |   3 +-
 reset.c                    |   3 +-
 run-command.c              |  32 ---------
 run-command.h              |  17 -----
 t/t1800-hook.sh            | 134 +++++++++++++++++++++++++++++++++++++
 t/t9001-send-email.sh      |   4 +-
 27 files changed, 522 insertions(+), 158 deletions(-)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Range-diff against v4:
 1:  d97d6734961 !  1:  4ca52feebb8 hook: add 'run' subcommand
    @@ hook.c: int hook_exists(const char *name)
     +		return 0;
     +
     +	cp->no_stdin = 1;
    -+	cp->env = hook_cb->options->env.v;
    ++	strvec_pushv(&cp->env_array, hook_cb->options->env.v);
     +	cp->stdout_to_stderr = 1;
     +	cp->trace2_hook_name = hook_cb->hook_name;
     +
 2:  ca6464f7d5e =  2:  6275b97a306 hook API: add a run_hooks() wrapper
 3:  173860afca1 =  3:  b5b3051b2e5 gc: use hook library for pre-auto-gc hook
 4:  80a2171ddaf =  4:  c88eb5d4c25 am: convert {pre,post}-applypatch to use hook.h
 5:  74f459db287 =  5:  1d8f7b7e4c1 hook API: add a run_hooks_l() wrapper
 6:  1fd70c0e88a =  6:  d49a1444345 rebase: convert pre-rebase to use hook.h
 7:  ccba3ddf52e =  7:  191fdad0165 am: convert applypatch-msg to use hook.h
 8:  2c23e8645ec =  8:  119b92fbeae merge: convert post-merge to use hook.h
 9:  cb95c79093b =  9:  359ba416e84 hooks: convert non-worktree 'post-checkout' hook to hook library
10:  f330600fec8 ! 10:  b7599be95a7 hooks: convert worktree 'post-checkout' hook to hook library
    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam
     
      ## hook.c ##
     @@ hook.c: static int pick_next_hook(struct child_process *cp,
    - 	cp->env = hook_cb->options->env.v;
    + 	strvec_pushv(&cp->env_array, hook_cb->options->env.v);
      	cp->stdout_to_stderr = 1;
      	cp->trace2_hook_name = hook_cb->hook_name;
     +	cp->dir = hook_cb->options->dir;
11:  a0b6818c766 = 11:  f1c84d7f627 git hook run: add an --ignore-missing flag
12:  efa35971e9f = 12:  4e0f94d9102 send-email: use 'git hook run' for 'sendemail-validate'
13:  98e0e3330fb = 13:  e858f332a62 git-p4: use 'git hook' to run hooks
14:  79ea5a2a4f5 = 14:  9a5956cc028 commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
15:  81612f94707 = 15:  6fd47c4c499 read-cache: convert post-index-change to use hook.h
16:  43ecd6697e0 = 16:  b201ea46f4b receive-pack: convert push-to-checkout hook to hook.h
17:  9ef574fa30c = 17:  281d17b04db run-command: remove old run_hook_{le,ve}() hook API
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 01/17] hook: add 'run' subcommand

From: Emily Shaffer <emilyshaffer@google.com>

In order to enable hooks to be run as an external process, by a standalone Git command, or by tools which wrap Git, provide an external means to run all configured hook commands for a given hook event.

Most of our hooks require more complex functionality than this, but let's start with the bare minimum required to support our simplest hooks.

In terms of implementation the usage_with_options() and "goto usage" pattern here mirrors that of builtin/{commit-graph,multi-pack-index}.c.

Some of the implementation here, such as a function being named run_hooks_opt() when it's tasked with running one hook, to using the run_processes_parallel_tr2() API to run with jobs=1 is somewhere between a bit odd and and an overkill for the current features of this "hook run" command and the hook.[ch] API.

This code will eventually be able to run multiple hooks declared in config in parallel, by starting out with these names and APIs we reduce the later churn of renaming functions, switching from the run_command() to run_processes_parallel_tr2() API etc.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  37 +++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/hook.c             |  80 +++++++++++++++++++++++
 command-list.txt           |   1 +
 git.c                      |   1 +
 hook.c                     | 102 +++++++++++++++++++++++++++++
 hook.h                     |  35 ++++++++++
 t/t1800-hook.sh            | 129 +++++++++++++++++++++++++++++++++++++
 11 files changed, 392 insertions(+)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Show changes to 11 files +392 −0

.gitignore, Documentation/git-hook.txt, Documentation/githooks.txt, Makefile, builtin.h, builtin/hook.c, command-list.txt, git.c, hook.c, hook.h, t/t1800-hook.sh

diff --git a/.gitignore b/.gitignore
index 054249b20a8..f817c509ec0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@
 /git-grep
 /git-hash-object
 /git-help
+/git-hook
 /git-http-backend
 /git-http-fetch
 /git-http-push
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
new file mode 100644
index 00000000000..e39b1b5d069
--- /dev/null
+++ b/Documentation/git-hook.txt
@@ -0,0 +1,37 @@
+git-hook(1)
+===========
+
+NAME
+----
+git-hook - Run git hooks
+
+SYNOPSIS
+--------
+[verse]
+'git hook' run <hook-name> [-- <hook-args>]
+
+DESCRIPTION
+-----------
+
+A command interface to running git hooks (see linkgit:githooks[5]),
+for use by other scripted git commands.
+
+SUBCOMMANDS
+-----------
+
+run::
+	Run the `<hook-name>` hook. See linkgit:githooks[5] for
+	supported hook names.
++
+
+Any positional arguments to the hook should be passed after a
+mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
+linkgit:githooks[5] for arguments hooks might expect (if any).
+
+SEE ALSO
+--------
+linkgit:githooks[5]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index b51959ff941..a16e62bc8c8 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -698,6 +698,10 @@ and "0" meaning they were not.
 Only one parameter should be set to "1" when the hook runs.  The hook
 running passing "1", "1" should not be possible.
 
+SEE ALSO
+--------
+linkgit:git-hook[1]
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 12be39ac497..6cd20534304 100644
--- a/Makefile
+++ b/Makefile
@@ -1107,6 +1107,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
 BUILTIN_OBJS += builtin/grep.o
 BUILTIN_OBJS += builtin/hash-object.o
 BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/hook.o
 BUILTIN_OBJS += builtin/index-pack.o
 BUILTIN_OBJS += builtin/init-db.o
 BUILTIN_OBJS += builtin/interpret-trailers.o
diff --git a/builtin.h b/builtin.h
index 8a58743ed63..83379f3832c 100644
--- a/builtin.h
+++ b/builtin.h
@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);
 int cmd_grep(int argc, const char **argv, const char *prefix);
 int cmd_hash_object(int argc, const char **argv, const char *prefix);
 int cmd_help(int argc, const char **argv, const char *prefix);
+int cmd_hook(int argc, const char **argv, const char *prefix);
 int cmd_index_pack(int argc, const char **argv, const char *prefix);
 int cmd_init_db(int argc, const char **argv, const char *prefix);
 int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);
diff --git a/builtin/hook.c b/builtin/hook.c
new file mode 100644
index 00000000000..9b67ff50cef
--- /dev/null
+++ b/builtin/hook.c
@@ -0,0 +1,80 @@
+#include "cache.h"
+#include "builtin.h"
+#include "config.h"
+#include "hook.h"
+#include "parse-options.h"
+#include "strbuf.h"
+#include "strvec.h"
+
+#define BUILTIN_HOOK_RUN_USAGE \
+	N_("git hook run <hook-name> [-- <hook-args>]")
+
+static const char * const builtin_hook_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static const char * const builtin_hook_run_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static int run(int argc, const char **argv, const char *prefix)
+{
+	int i;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	const char *hook_name;
+	struct option run_options[] = {
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, run_options,
+			     builtin_hook_run_usage,
+			     PARSE_OPT_KEEP_DASHDASH);
+
+	if (!argc)
+		goto usage;
+
+	/*
+	 * Having a -- for "run" when providing <hook-args> is
+	 * mandatory.
+	 */
+	if (argc > 1 && strcmp(argv[1], "--") &&
+	    strcmp(argv[1], "--end-of-options"))
+		goto usage;
+
+	/* Add our arguments, start after -- */
+	for (i = 2 ; i < argc; i++)
+		strvec_push(&opt.args, argv[i]);
+
+	/* Need to take into account core.hooksPath */
+	git_config(git_default_config, NULL);
+
+	hook_name = argv[0];
+	opt.error_if_missing = 1;
+	ret = run_hooks_opt(hook_name, &opt);
+	if (ret < 0) /* error() return */
+		ret = 1;
+	return ret;
+usage:
+	usage_with_options(builtin_hook_run_usage, run_options);
+}
+
+int cmd_hook(int argc, const char **argv, const char *prefix)
+{
+	struct option builtin_hook_options[] = {
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, NULL, builtin_hook_options,
+			     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+	if (!argc)
+		goto usage;
+
+	if (!strcmp(argv[0], "run"))
+		return run(argc, argv, prefix);
+
+usage:
+	usage_with_options(builtin_hook_usage, builtin_hook_options);
+}
diff --git a/command-list.txt b/command-list.txt
index eb9cee8dee9..0c9af14bdf3 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info
 git-gui                                 mainporcelain
 git-hash-object                         plumbingmanipulators
 git-help                                ancillaryinterrogators          complete
+git-hook                                purehelpers
 git-http-backend                        synchingrepositories
 git-http-fetch                          synchelpers
 git-http-push                           synchelpers
diff --git a/git.c b/git.c
index 5ff21be21f3..a5be02b04b8 100644
--- a/git.c
+++ b/git.c
@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {
 	{ "grep", cmd_grep, RUN_SETUP_GENTLY },
 	{ "hash-object", cmd_hash_object },
 	{ "help", cmd_help },
+	{ "hook", cmd_hook, RUN_SETUP },
 	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
 	{ "init", cmd_init_db },
 	{ "init-db", cmd_init_db },
diff --git a/hook.c b/hook.c
index 55e1145a4b7..a0917cf877c 100644
--- a/hook.c
+++ b/hook.c
@@ -1,6 +1,7 @@
 #include "cache.h"
 #include "hook.h"
 #include "run-command.h"
+#include "config.h"
 
 const char *find_hook(const char *name)
 {
@@ -40,3 +41,104 @@ int hook_exists(const char *name)
 {
 	return !!find_hook(name);
 }
+
+static int pick_next_hook(struct child_process *cp,
+			  struct strbuf *out,
+			  void *pp_cb,
+			  void **pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = hook_cb->hook_path;
+
+	if (!hook_path)
+		return 0;
+
+	cp->no_stdin = 1;
+	strvec_pushv(&cp->env_array, hook_cb->options->env.v);
+	cp->stdout_to_stderr = 1;
+	cp->trace2_hook_name = hook_cb->hook_name;
+
+	strvec_push(&cp->args, hook_path);
+	strvec_pushv(&cp->args, hook_cb->options->args.v);
+
+	/* Provide context for errors if necessary */
+	*pp_task_cb = (char *)hook_path;
+
+	/*
+	 * This pick_next_hook() will be called again, we're only
+	 * running one hook, so indicate that no more work will be
+	 * done.
+	 */
+	hook_cb->hook_path = NULL;
+
+	return 1;
+}
+
+static int notify_start_failure(struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cp)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = pp_task_cp;
+
+	hook_cb->rc |= 1;
+
+	strbuf_addf(out, _("Couldn't start hook '%s'\n"),
+		    hook_path);
+
+	return 1;
+}
+
+static int notify_hook_finished(int result,
+				struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+
+	hook_cb->rc |= result;
+
+	return 0;
+}
+
+static void run_hooks_opt_clear(struct run_hooks_opt *options)
+{
+	strvec_clear(&options->env);
+	strvec_clear(&options->args);
+}
+
+int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
+{
+	struct hook_cb_data cb_data = {
+		.rc = 0,
+		.hook_name = hook_name,
+		.options = options,
+	};
+	const char *const hook_path = find_hook(hook_name);
+	int jobs = 1;
+	int ret = 0;
+
+	if (!options)
+		BUG("a struct run_hooks_opt must be provided to run_hooks");
+
+	if (!hook_path && !options->error_if_missing)
+		goto cleanup;
+
+	if (!hook_path) {
+		ret = error("cannot find a hook named %s", hook_name);
+		goto cleanup;
+	}
+
+	cb_data.hook_path = hook_path;
+	run_processes_parallel_tr2(jobs,
+				   pick_next_hook,
+				   notify_start_failure,
+				   notify_hook_finished,
+				   &cb_data,
+				   "hook",
+				   hook_name);
+	ret = cb_data.rc;
+cleanup:
+	run_hooks_opt_clear(options);
+	return ret;
+}
diff --git a/hook.h b/hook.h
index 6aa36fc7ff9..782385cc235 100644
--- a/hook.h
+++ b/hook.h
@@ -1,5 +1,31 @@
 #ifndef HOOK_H
 #define HOOK_H
+#include "strvec.h"
+
+struct run_hooks_opt
+{
+	/* Environment vars to be set for each hook */
+	struct strvec env;
+
+	/* Args to be passed to each hook */
+	struct strvec args;
+
+	/* Emit an error if the hook is missing */
+	unsigned int error_if_missing:1;
+};
+
+#define RUN_HOOKS_OPT_INIT { \
+	.env = STRVEC_INIT, \
+	.args = STRVEC_INIT, \
+}
+
+struct hook_cb_data {
+	/* rc reflects the cumulative failure state */
+	int rc;
+	const char *hook_name;
+	const char *hook_path;
+	struct run_hooks_opt *options;
+};
 
 /*
  * Returns the path to the hook file, or NULL if the hook is missing
@@ -13,4 +39,13 @@ const char *find_hook(const char *name);
  */
 int hook_exists(const char *hookname);
 
+/**
+ * Takes a `hook_name`, resolves it to a path with find_hook(), and
+ * runs the hook for you with the options specified in "struct
+ * run_hooks opt". Will free memory associated with the "struct run_hooks_opt".
+ *
+ * Returns the status code of the run hook, or a negative value on
+ * error().
+ */
+int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
 #endif
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
new file mode 100755
index 00000000000..3aea1b105f0
--- /dev/null
+++ b/t/t1800-hook.sh
@@ -0,0 +1,129 @@
+#!/bin/sh
+
+test_description='git-hook command'
+
+TEST_PASSES_SANITIZE_LEAK=true
+. ./test-lib.sh
+
+test_expect_success 'git hook usage' '
+	test_expect_code 129 git hook &&
+	test_expect_code 129 git hook run &&
+	test_expect_code 129 git hook run -h &&
+	test_expect_code 129 git hook run --unknown 2>err &&
+	grep "unknown option" err
+'
+
+test_expect_success 'git hook run: nonexistent hook' '
+	cat >stderr.expect <<-\EOF &&
+	error: cannot find a hook named test-hook
+	EOF
+	test_expect_code 1 git hook run test-hook 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual
+'
+
+test_expect_success 'git hook run: basic' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	EOF
+	git hook run test-hook 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run: stdout and stderr both write to our stderr' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo >&1 Will end up on stderr
+	echo >&2 Will end up on stderr
+	EOF
+
+	cat >stderr.expect <<-\EOF &&
+	Will end up on stderr
+	Will end up on stderr
+	EOF
+	git hook run test-hook >stdout.actual 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual &&
+	test_must_be_empty stdout.actual
+'
+
+test_expect_success 'git hook run: exit codes are passed along' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 1
+	EOF
+
+	test_expect_code 1 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 2
+	EOF
+
+	test_expect_code 2 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 128
+	EOF
+
+	test_expect_code 128 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 129
+	EOF
+
+	test_expect_code 129 git hook run test-hook
+'
+
+test_expect_success 'git hook run arg u ments without -- is not allowed' '
+	test_expect_code 129 git hook run test-hook arg u ments
+'
+
+test_expect_success 'git hook run -- pass arguments' '
+	write_script .git/hooks/test-hook <<-\EOF &&
+	echo $1
+	echo $2
+	EOF
+
+	cat >expect <<-EOF &&
+	arg
+	u ments
+	EOF
+
+	git hook run test-hook -- arg "u ments" 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run -- out-of-repo runs excluded' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	nongit test_must_fail git hook run test-hook
+'
+
+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
+	mkdir my-hooks &&
+	write_script my-hooks/test-hook <<-\EOF &&
+	echo Hook ran $1 >>actual
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	Hook ran one
+	Hook ran two
+	Hook ran three
+	Hook ran four
+	EOF
+
+	# Test various ways of specifying the path. See also
+	# t1350-config-hooks-path.sh
+	>actual &&
+	git hook run test-hook -- ignored 2>>actual &&
+	git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&
+	git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks/" hook run test-hook -- four 2>>actual &&
+	test_cmp expect actual
+'
+
+test_done
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 02/17] hook API: add a run_hooks() wrapper

Add a run_hooks() wrapper, we'll use it in subsequent commits for the simple cases of wanting to run a single hook under a given name, without providing options such as "env" or "args".

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 hook.c | 7 +++++++
 hook.h | 6 ++++++
 2 files changed, 13 insertions(+)
Show changes to 2 files +13 −0

hook.c, hook.h

diff --git a/hook.c b/hook.c
index a0917cf877c..d67a114e62d 100644
--- a/hook.c
+++ b/hook.c
@@ -142,3 +142,10 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 	run_hooks_opt_clear(options);
 	return ret;
 }
+
+int run_hooks(const char *hook_name)
+{
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+	return run_hooks_opt(hook_name, &opt);
+}
diff --git a/hook.h b/hook.h
index 782385cc235..9c358789958 100644
--- a/hook.h
+++ b/hook.h
@@ -48,4 +48,10 @@ int hook_exists(const char *hookname);
  * error().
  */
 int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
+
+/**
+ * A wrapper for run_hooks_opt() which provides a dummy "struct
+ * run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
+ */
+int run_hooks(const char *hook_name);
 #endif
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 03/17] gc: use hook library for pre-auto-gc hook

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-auto-gc hook away from run-command.h to and over to the new hook.h library. This uses the new run_hooks() wrapper.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
Show changes to builtin/gc.c +2 −1
diff --git a/builtin/gc.c b/builtin/gc.c
index bcef6a4c8da..4bbc58aae5b 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -32,6 +32,7 @@
 #include "remote.h"
 #include "object-store.h"
 #include "exec-cmd.h"
+#include "hook.h"
 
 #define FAILED_RUN "failed to run %s"
 
@@ -394,7 +395,7 @@ static int need_to_gc(void)
 	else
 		return 0;
 
-	if (run_hook_le(NULL, "pre-auto-gc", NULL))
+	if (run_hooks("pre-auto-gc"))
 		return 0;
 	return 1;
 }
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 06/17] rebase: convert pre-rebase to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-rebase hook away from run-command.h to and over to the new hook.h library.

Since this hook needs arguments introduce a run_hooksl() wrapper, like run_hooks(), but it takes varargs.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/rebase.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
Show changes to builtin/rebase.c +2 −1
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 34b4744e5f3..ac4120013a0 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -28,6 +28,7 @@
 #include "sequencer.h"
 #include "rebase-interactive.h"
 #include "reset.h"
+#include "hook.h"
 
 #define DEFAULT_REFLOG_ACTION "rebase"
 
@@ -1712,7 +1713,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
 	/* If a hook exists, give it a chance to interrupt*/
 	if (!ok_to_skip_pre_rebase &&
-	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
+	    run_hooks_l("pre-rebase", options.upstream_arg,
 			argc ? argv[0] : NULL, NULL))
 		die(_("The pre-rebase hook refused to rebase."));
 
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 04/17] am: convert {pre,post}-applypatch to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach pre-applypatch and post-applypatch to use the hook.h library instead of the run-command.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/am.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Show changes to builtin/am.c +2 −2
diff --git a/builtin/am.c b/builtin/am.c
index 8677ea2348a..4b334cb7b12 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -1609,7 +1609,7 @@ static void do_commit(const struct am_state *state)
 	const char *reflog_msg, *author, *committer = NULL;
 	struct strbuf sb = STRBUF_INIT;
 
-	if (run_hook_le(NULL, "pre-applypatch", NULL))
+	if (run_hooks("pre-applypatch"))
 		exit(1);
 
 	if (write_cache_as_tree(&tree, 0, NULL))
@@ -1661,7 +1661,7 @@ static void do_commit(const struct am_state *state)
 		fclose(fp);
 	}
 
-	run_hook_le(NULL, "post-applypatch", NULL);
+	run_hooks("post-applypatch");
 
 	strbuf_release(&sb);
 }
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 05/17] hook API: add a run_hooks_l() wrapper

Add a run_hooks_l() wrapper, we'll use it in subsequent commits for the simple cases of wanting to run a single hook under a given name along with a list of arguments.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 hook.c | 14 ++++++++++++++
 hook.h | 10 ++++++++++
 2 files changed, 24 insertions(+)
Show changes to 2 files +24 −0

hook.c, hook.h

diff --git a/hook.c b/hook.c
index d67a114e62d..1ad123422b2 100644
--- a/hook.c
+++ b/hook.c
@@ -149,3 +149,17 @@ int run_hooks(const char *hook_name)
 
 	return run_hooks_opt(hook_name, &opt);
 }
+
+int run_hooks_l(const char *hook_name, ...)
+{
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	va_list ap;
+	const char *arg;
+
+	va_start(ap, hook_name);
+	while ((arg = va_arg(ap, const char *)))
+		strvec_push(&opt.args, arg);
+	va_end(ap);
+
+	return run_hooks_opt(hook_name, &opt);
+}
diff --git a/hook.h b/hook.h
index 9c358789958..54528395953 100644
--- a/hook.h
+++ b/hook.h
@@ -54,4 +54,14 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
  * run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
  */
 int run_hooks(const char *hook_name);
+
+/**
+ * Like run_hooks(), a wrapper for run_hooks_opt().
+ *
+ * In addition to the wrapping behavior provided by run_hooks(), this
+ * wrapper takes a list of strings terminated by a NULL
+ * argument. These things will be used as positional arguments to the
+ * hook. This function behaves like the old run_hook_le() API.
+ */
+int run_hooks_l(const char *hook_name, ...);
 #endif
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 08/17] merge: convert post-merge to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach post-merge to use the hook.h library instead of the run-command.h library to run hooks.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/merge.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to builtin/merge.c +1 −1
diff --git a/builtin/merge.c b/builtin/merge.c
index ea3112e0c0b..e6facd1c95d 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -488,7 +488,7 @@ static void finish(struct commit *head_commit,
 	}
 
 	/* Run a post-merge hook */
-	run_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL);
+	run_hooks_l("post-merge", squash ? "1" : "0", NULL);
 
 	apply_autostash(git_path_merge_autostash(the_repository));
 	strbuf_release(&reflog_message);
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 07/17] am: convert applypatch-msg to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach applypatch-msg to use the hook.h library instead of the run-command.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/am.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to builtin/am.c +1 −1
diff --git a/builtin/am.c b/builtin/am.c
index 4b334cb7b12..ae0c484dcba 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -448,7 +448,7 @@ static int run_applypatch_msg_hook(struct am_state *state)
 	int ret;
 
 	assert(state->msg);
-	ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
+	ret = run_hooks_l("applypatch-msg", am_path(state, "final-commit"), NULL);
 
 	if (!ret) {
 		FREE_AND_NULL(state->msg);
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 10/17] hooks: convert worktree 'post-checkout' hook to hook library

From: Emily Shaffer <emilyshaffer@google.com>

Move the running of the 'post-checkout' hook away from run-command.h to the new hook.h library in builtin/worktree.c. For this special case we need a change to the hook API to teach it to run the hook from a given directory.

We cannot skip the "absolute_path" flag and just check if "dir" is specified as we'd then fail to find our hook in the new dir we'd chdir() to. We currently don't have a use-case for running a hook not in our "base" repository at a given absolute path, so let's have "dir" imply absolute_path(find_hook(hook_name)).

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/worktree.c | 27 +++++++++++----------------
 hook.c             |  8 ++++++++
 hook.h             |  6 ++++++
 3 files changed, 25 insertions(+), 16 deletions(-)
Show changes to 3 files +25 −16

builtin/worktree.c, hook.c, hook.h

diff --git a/builtin/worktree.c b/builtin/worktree.c
index d22ece93e1a..4d9df5ecc4c 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -382,22 +382,17 @@ static int add_worktree(const char *path, const char *refname,
 	 * is_junk is cleared, but do return appropriate code when hook fails.
 	 */
 	if (!ret && opts->checkout) {
-		const char *hook = find_hook("post-checkout");
-		if (hook) {
-			const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
-			cp.git_cmd = 0;
-			cp.no_stdin = 1;
-			cp.stdout_to_stderr = 1;
-			cp.dir = path;
-			cp.env = env;
-			cp.argv = NULL;
-			cp.trace2_hook_name = "post-checkout";
-			strvec_pushl(&cp.args, absolute_path(hook),
-				     oid_to_hex(null_oid()),
-				     oid_to_hex(&commit->object.oid),
-				     "1", NULL);
-			ret = run_command(&cp);
-		}
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+		strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
+		strvec_pushl(&opt.args,
+			     oid_to_hex(null_oid()),
+			     oid_to_hex(&commit->object.oid),
+			     "1",
+			     NULL);
+		opt.dir = path;
+
+		ret = run_hooks_opt("post-checkout", &opt);
 	}
 
 	strvec_clear(&child_env);
diff --git a/hook.c b/hook.c
index 1ad123422b2..69a215b2c3c 100644
--- a/hook.c
+++ b/hook.c
@@ -57,6 +57,7 @@ static int pick_next_hook(struct child_process *cp,
 	strvec_pushv(&cp->env_array, hook_cb->options->env.v);
 	cp->stdout_to_stderr = 1;
 	cp->trace2_hook_name = hook_cb->hook_name;
+	cp->dir = hook_cb->options->dir;
 
 	strvec_push(&cp->args, hook_path);
 	strvec_pushv(&cp->args, hook_cb->options->args.v);
@@ -109,6 +110,7 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
 
 int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 {
+	struct strbuf abs_path = STRBUF_INIT;
 	struct hook_cb_data cb_data = {
 		.rc = 0,
 		.hook_name = hook_name,
@@ -130,6 +132,11 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 	}
 
 	cb_data.hook_path = hook_path;
+	if (options->dir) {
+		strbuf_add_absolute_path(&abs_path, hook_path);
+		cb_data.hook_path = abs_path.buf;
+	}
+
 	run_processes_parallel_tr2(jobs,
 				   pick_next_hook,
 				   notify_start_failure,
@@ -139,6 +146,7 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 				   hook_name);
 	ret = cb_data.rc;
 cleanup:
+	strbuf_release(&abs_path);
 	run_hooks_opt_clear(options);
 	return ret;
 }
diff --git a/hook.h b/hook.h
index 54528395953..18d90aedf14 100644
--- a/hook.h
+++ b/hook.h
@@ -12,6 +12,12 @@ struct run_hooks_opt
 
 	/* Emit an error if the hook is missing */
 	unsigned int error_if_missing:1;
+
+	/**
+	 * An optional initial working directory for the hook,
+	 * translates to "struct child_process"'s "dir" member.
+	 */
+	const char *dir;
 };
 
 #define RUN_HOOKS_OPT_INIT { \
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 14/17] commit: convert {pre-commit,prepare-commit-msg} hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move these hooks hook away from run-command.h to and over to the new hook.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 commit.c | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)
Show changes to commit.c +8 −7
diff --git a/commit.c b/commit.c
index 551de4903c1..581d7dc216c 100644
--- a/commit.c
+++ b/commit.c
@@ -21,6 +21,7 @@
 #include "commit-reach.h"
 #include "run-command.h"
 #include "shallow.h"
+#include "hook.h"
 
 static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
 
@@ -1700,22 +1701,22 @@ size_t ignore_non_trailer(const char *buf, size_t len)
 int run_commit_hook(int editor_is_used, const char *index_file,
 		    const char *name, ...)
 {
-	struct strvec hook_env = STRVEC_INIT;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 	va_list args;
-	int ret;
+	const char *arg;
 
-	strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
+	strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s", index_file);
 
 	/*
 	 * Let the hook know that no editor will be launched.
 	 */
 	if (!editor_is_used)
-		strvec_push(&hook_env, "GIT_EDITOR=:");
+		strvec_push(&opt.env, "GIT_EDITOR=:");
 
 	va_start(args, name);
-	ret = run_hook_ve(hook_env.v, name, args);
+	while ((arg = va_arg(args, const char *)))
+		strvec_push(&opt.args, arg);
 	va_end(args);
-	strvec_clear(&hook_env);
 
-	return ret;
+	return run_hooks_opt(name, &opt);
 }
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 13/17] git-p4: use 'git hook' to run hooks

From: Emily Shaffer <emilyshaffer@google.com>

Instead of duplicating the behavior of run-command.h:run_hook_le() in Python, we can directly call 'git hook run'. We emulate the existence check with the --ignore-missing flag.

We're dropping the "verbose" handling added in 9f59ca4d6af (git-p4: create new function run_git_hook, 2020-02-11), those who want diagnostic output about how hooks are run are now able to get that via e.g. the trace2 facility and GIT_TRACE=1.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-p4.py | 70 +++++--------------------------------------------------
 1 file changed, 6 insertions(+), 64 deletions(-)
Show changes to git-p4.py +6 −64
diff --git a/git-p4.py b/git-p4.py
index 2b4500226aa..3b54168eb4a 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -208,70 +208,12 @@ def decode_path(path):
 
 def run_git_hook(cmd, param=[]):
     """Execute a hook if the hook exists."""
-    if verbose:
-        sys.stderr.write("Looking for hook: %s\n" % cmd)
-        sys.stderr.flush()
-
-    hooks_path = gitConfig("core.hooksPath")
-    if len(hooks_path) <= 0:
-        hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
-
-    if not isinstance(param, list):
-        param=[param]
-
-    # resolve hook file name, OS depdenent
-    hook_file = os.path.join(hooks_path, cmd)
-    if platform.system() == 'Windows':
-        if not os.path.isfile(hook_file):
-            # look for the file with an extension
-            files = glob.glob(hook_file + ".*")
-            if not files:
-                return True
-            files.sort()
-            hook_file = files.pop()
-            while hook_file.upper().endswith(".SAMPLE"):
-                # The file is a sample hook. We don't want it
-                if len(files) > 0:
-                    hook_file = files.pop()
-                else:
-                    return True
-
-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
-        return True
-
-    return run_hook_command(hook_file, param) == 0
-
-def run_hook_command(cmd, param):
-    """Executes a git hook command
-       cmd = the command line file to be executed. This can be
-       a file that is run by OS association.
-
-       param = a list of parameters to pass to the cmd command
-
-       On windows, the extension is checked to see if it should
-       be run with the Git for Windows Bash shell.  If there
-       is no file extension, the file is deemed a bash shell
-       and will be handed off to sh.exe. Otherwise, Windows
-       will be called with the shell to handle the file assocation.
-
-       For non Windows operating systems, the file is called
-       as an executable.
-    """
-    cli = [cmd] + param
-    use_shell = False
-    if platform.system() == 'Windows':
-        (root,ext) = os.path.splitext(cmd)
-        if ext == "":
-            exe_path = os.environ.get("EXEPATH")
-            if exe_path is None:
-                exe_path = ""
-            else:
-                exe_path = os.path.join(exe_path, "bin")
-            cli = [os.path.join(exe_path, "SH.EXE")] + cli
-        else:
-            use_shell = True
-    return subprocess.call(cli, shell=use_shell)
-
+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]
+    if param:
+        args.append("--")
+        for p in param:
+            args.append(p)
+    return subprocess.call(args) == 0
 
 def write_pipe(c, stdin):
     if verbose:
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 15/17] read-cache: convert post-index-change to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the post-index-change hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of "run_hook_ve()" outside of run-command.c ("run_hook_le()" still uses it). So we can make the function static now. A subsequent commit will remove this code entirely when "run_hook_le()" itself goes away.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 read-cache.c  | 3 ++-
 run-command.c | 2 +-
 run-command.h | 1 -
 3 files changed, 3 insertions(+), 3 deletions(-)
Show changes to 3 files +3 −3

read-cache.c, run-command.c, run-command.h

diff --git a/read-cache.c b/read-cache.c
index f3986596623..4352661549f 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -28,6 +28,7 @@
 #include "sparse-index.h"
 #include "csum-file.h"
 #include "promisor-remote.h"
+#include "hook.h"
 
 /* Mask for the name length in ce_flags in the on-disk index */
 
@@ -3129,7 +3130,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
 	else
 		ret = close_lock_file_gently(lock);
 
-	run_hook_le(NULL, "post-index-change",
+	run_hooks_l("post-index-change",
 			istate->updated_workdir ? "1" : "0",
 			istate->updated_skipworktree ? "1" : "0", NULL);
 	istate->updated_workdir = 0;
diff --git a/run-command.c b/run-command.c
index f40df01c772..d7d64701ede 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1323,7 +1323,7 @@ int async_with_fork(void)
 #endif
 }
 
-int run_hook_ve(const char *const *env, const char *name, va_list args)
+static int run_hook_ve(const char *const *env, const char *name, va_list args)
 {
 	struct child_process hook = CHILD_PROCESS_INIT;
 	const char *p;
diff --git a/run-command.h b/run-command.h
index 49878262584..3fa7454cf8a 100644
--- a/run-command.h
+++ b/run-command.h
@@ -239,7 +239,6 @@ int run_command(struct child_process *);
  */
 LAST_ARG_MUST_BE_NULL
 int run_hook_le(const char *const *env, const char *name, ...);
-int run_hook_ve(const char *const *env, const char *name, va_list args);
 
 /*
  * Trigger an auto-gc
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 16/17] receive-pack: convert push-to-checkout hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the push-to-checkout hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of run_hook_le(), so we could remove that function now, but let's leave that to a follow-up cleanup commit.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/receive-pack.c | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)
Show changes to builtin/receive-pack.c +5 −2
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 49b846d9605..5d6d8dd9a26 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1439,9 +1439,12 @@ static const char *push_to_checkout(unsigned char *hash,
 				    struct strvec *env,
 				    const char *work_tree)
 {
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
 	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
-	if (run_hook_le(env->v, push_to_checkout_hook,
-			hash_to_hex(hash), NULL))
+	strvec_pushv(&opt.env, env->v);
+	strvec_push(&opt.args, hash_to_hex(hash));
+	if (run_hooks_opt(push_to_checkout_hook, &opt))
 		return "push-to-checkout hook declined";
 	else
 		return NULL;
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 17/17] run-command: remove old run_hook_{le,ve}() hook API

From: Emily Shaffer <emilyshaffer@google.com>

The new hook.h library has replaced all run-command.h hook-related functionality. So let's delete this dead code.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 run-command.c | 32 --------------------------------
 run-command.h | 16 ----------------
 2 files changed, 48 deletions(-)
Show changes to 2 files +0 −48

run-command.c, run-command.h

diff --git a/run-command.c b/run-command.c
index d7d64701ede..7ecff26c1dc 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1323,38 +1323,6 @@ int async_with_fork(void)
 #endif
 }
 
-static int run_hook_ve(const char *const *env, const char *name, va_list args)
-{
-	struct child_process hook = CHILD_PROCESS_INIT;
-	const char *p;
-
-	p = find_hook(name);
-	if (!p)
-		return 0;
-
-	strvec_push(&hook.args, p);
-	while ((p = va_arg(args, const char *)))
-		strvec_push(&hook.args, p);
-	hook.env = env;
-	hook.no_stdin = 1;
-	hook.stdout_to_stderr = 1;
-	hook.trace2_hook_name = name;
-
-	return run_command(&hook);
-}
-
-int run_hook_le(const char *const *env, const char *name, ...)
-{
-	va_list args;
-	int ret;
-
-	va_start(args, name);
-	ret = run_hook_ve(env, name, args);
-	va_end(args);
-
-	return ret;
-}
-
 struct io_pump {
 	/* initialized by caller */
 	int fd;
diff --git a/run-command.h b/run-command.h
index 3fa7454cf8a..59e1fbff64c 100644
--- a/run-command.h
+++ b/run-command.h
@@ -224,22 +224,6 @@ int finish_command_in_signal(struct child_process *);
  */
 int run_command(struct child_process *);
 
-/**
- * Run a hook.
- * The first argument is a pathname to an index file, or NULL
- * if the hook uses the default index file or no index is needed.
- * The second argument is the name of the hook.
- * The further arguments correspond to the hook arguments.
- * The last argument has to be NULL to terminate the arguments list.
- * If the hook does not exist or is not executable, the return
- * value will be zero.
- * If it is executable, the hook will be executed and the exit
- * status of the hook is returned.
- * On execution, .stdout_to_stderr and .no_stdin will be set.
- */
-LAST_ARG_MUST_BE_NULL
-int run_hook_le(const char *const *env, const char *name, ...);
-
 /*
  * Trigger an auto-gc
  */
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 12/17] send-email: use 'git hook run' for 'sendemail-validate'

From: Emily Shaffer <emilyshaffer@google.com>

Change the "sendmail-validate" hook to be run via the "git hook run" wrapper instead of via a direct invocation.

This is the smallest possibly change to get "send-email" using "git hook run". We still check the hook itself with "-x", and set a "GIT_DIR" variable, both of which are asserted by our tests. We'll need to get rid of this special behavior if we start running N hooks, but for now let's be as close to bug-for-bug compatible as possible.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl   | 22 ++++++++++++++--------
 t/t9001-send-email.sh |  4 ++--
 2 files changed, 16 insertions(+), 10 deletions(-)
Show changes to 2 files +16 −10

git-send-email.perl, t/t9001-send-email.sh

diff --git a/git-send-email.perl b/git-send-email.perl
index 5262d88ee32..4c20c0bbb79 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -195,13 +195,13 @@ sub format_2822_time {
 my $editor;
 
 sub system_or_msg {
-	my ($args, $msg) = @_;
+	my ($args, $msg, $cmd_name) = @_;
 	system(@$args);
 	my $signalled = $? & 127;
 	my $exit_code = $? >> 8;
 	return unless $signalled or $exit_code;
 
-	my @sprintf_args = ($args->[0], $exit_code);
+	my @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);
 	if (defined $msg) {
 		# Quiet the 'redundant' warning category, except we
 		# need to support down to Perl 5.8, so we can't do a
@@ -2039,10 +2039,10 @@ sub validate_patch {
 	my ($fn, $xfer_encoding) = @_;
 
 	if ($repo) {
+		my $hook_name = 'sendemail-validate';
 		my $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');
 		require File::Spec;
-		my $validate_hook = File::Spec->catfile($hooks_path,
-					    'sendemail-validate');
+		my $validate_hook = File::Spec->catfile($hooks_path, $hook_name);
 		my $hook_error;
 		if (-x $validate_hook) {
 			require Cwd;
@@ -2052,13 +2052,19 @@ sub validate_patch {
 			chdir($repo->wc_path() or $repo->repo_path())
 				or die("chdir: $!");
 			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			$hook_error = system_or_msg([$validate_hook, $target]);
+			my @cmd = ("git", "hook", "run", "--ignore-missing",
+				    $hook_name, "--");
+			my @cmd_msg = (@cmd, "<patch>");
+			my @cmd_run = (@cmd, $target);
+			$hook_error = system_or_msg(\@cmd_run, undef, "@cmd_msg");
 			chdir($cwd_save) or die("chdir: $!");
 		}
 		if ($hook_error) {
-			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
-				       "%s\n" .
-				       "warning: no patches were sent\n"), $fn, $hook_error);
+			$hook_error = sprintf(__("fatal: %s: rejected by %s hook\n" .
+						 $hook_error . "\n" .
+						 "warning: no patches were sent\n"),
+					      $fn, $hook_name);
+			die $hook_error;
 		}
 	}
 
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index aa0c20499ba..84d0f40d76a 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -539,7 +539,7 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'my-hooks/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
@@ -558,7 +558,7 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 11/17] git hook run: add an --ignore-missing flag

For certain one-shot hooks we'd like to optimistically run them, and not complain if they don't exist.

This was already supported by the underlying hook.c library, but had not been exposed via "git hook run". The command version of this will be used by send-email in a subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Documentation/git-hook.txt | 10 +++++++++-
 builtin/hook.c             |  8 ++++++--
 t/t1800-hook.sh            |  5 +++++
 3 files changed, 20 insertions(+), 3 deletions(-)
Show changes to 3 files +20 −3

Documentation/git-hook.txt, builtin/hook.c, t/t1800-hook.sh

diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index e39b1b5d069..77c3a8ad909 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -8,7 +8,7 @@ git-hook - Run git hooks
 SYNOPSIS
 --------
 [verse]
-'git hook' run <hook-name> [-- <hook-args>]
+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]
 
 DESCRIPTION
 -----------
@@ -28,6 +28,14 @@ Any positional arguments to the hook should be passed after a
 mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
 linkgit:githooks[5] for arguments hooks might expect (if any).
 
+OPTIONS
+-------
+
+--ignore-missing::
+	Ignore any missing hook by quietly returning zero. Used for
+	tools that want to do a blind one-shot run of a hook that may
+	or may not be present.
+
 SEE ALSO
 --------
 linkgit:githooks[5]
diff --git a/builtin/hook.c b/builtin/hook.c
index 9b67ff50cef..54e5c6ec933 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -7,7 +7,7 @@
 #include "strvec.h"
 
 #define BUILTIN_HOOK_RUN_USAGE \
-	N_("git hook run <hook-name> [-- <hook-args>]")
+	N_("git hook run [--ignore-missing] <hook-name> [-- <hook-args>]")
 
 static const char * const builtin_hook_usage[] = {
 	BUILTIN_HOOK_RUN_USAGE,
@@ -23,8 +23,11 @@ static int run(int argc, const char **argv, const char *prefix)
 {
 	int i;
 	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	int ignore_missing = 0;
 	const char *hook_name;
 	struct option run_options[] = {
+		OPT_BOOL(0, "ignore-missing", &ignore_missing,
+			 N_("silently ignore missing requested <hook-name>")),
 		OPT_END(),
 	};
 	int ret;
@@ -52,7 +55,8 @@ static int run(int argc, const char **argv, const char *prefix)
 	git_config(git_default_config, NULL);
 
 	hook_name = argv[0];
-	opt.error_if_missing = 1;
+	if (!ignore_missing)
+		opt.error_if_missing = 1;
 	ret = run_hooks_opt(hook_name, &opt);
 	if (ret < 0) /* error() return */
 		ret = 1;
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 3aea1b105f0..29718aa9913 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -21,6 +21,11 @@ test_expect_success 'git hook run: nonexistent hook' '
 	test_cmp stderr.expect stderr.actual
 '
 
+test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '
+	git hook run --ignore-missing does-not-exist 2>stderr.actual &&
+	test_must_be_empty stderr.actual
+'
+
 test_expect_success 'git hook run: basic' '
 	write_script .git/hooks/test-hook <<-EOF &&
 	echo Test hook
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Nov 23, 2021, 11:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v5 09/17] hooks: convert non-worktree 'post-checkout' hook to hook library

From: Emily Shaffer <emilyshaffer@google.com>

Move the running of the 'post-checkout' hook away from run-command.h to the new hook.h library, except in the case of builtin/worktree.c. That special-case will be handled in a subsequent commit.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/checkout.c | 3 ++-
 builtin/clone.c    | 3 ++-
 reset.c            | 3 ++-
 3 files changed, 6 insertions(+), 3 deletions(-)
Show changes to 3 files +6 −3

builtin/checkout.c, builtin/clone.c, reset.c

diff --git a/builtin/checkout.c b/builtin/checkout.c
index cbf73b8c9f6..4af17d17217 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -9,6 +9,7 @@
 #include "config.h"
 #include "diff.h"
 #include "dir.h"
+#include "hook.h"
 #include "ll-merge.h"
 #include "lockfile.h"
 #include "merge-recursive.h"
@@ -106,7 +107,7 @@ struct branch_info {
 static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
 			      int changed)
 {
-	return run_hook_le(NULL, "post-checkout",
+	return run_hooks_l("post-checkout",
 			   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
 			   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
 			   changed ? "1" : "0", NULL);
diff --git a/builtin/clone.c b/builtin/clone.c
index fb377b27657..ee27b9f8114 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -32,6 +32,7 @@
 #include "connected.h"
 #include "packfile.h"
 #include "list-objects-filter-options.h"
+#include "hook.h"
 
 /*
  * Overall FIXMEs:
@@ -705,7 +706,7 @@ static int checkout(int submodule_progress)
 	if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
 		die(_("unable to write new index file"));
 
-	err |= run_hook_le(NULL, "post-checkout", oid_to_hex(null_oid()),
+	err |= run_hooks_l("post-checkout", oid_to_hex(null_oid()),
 			   oid_to_hex(&oid), "1", NULL);
 
 	if (!err && (option_recurse_submodules.nr > 0)) {
diff --git a/reset.c b/reset.c
index f214df3d96c..0881e636915 100644
--- a/reset.c
+++ b/reset.c
@@ -7,6 +7,7 @@
 #include "tree-walk.h"
 #include "tree.h"
 #include "unpack-trees.h"
+#include "hook.h"
 
 int reset_head(struct repository *r, struct object_id *oid, const char *action,
 	       const char *switch_to_branch, unsigned flags,
@@ -127,7 +128,7 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,
 					    reflog_head);
 	}
 	if (run_hook)
-		run_hook_le(NULL, "post-checkout",
+		run_hooks_l("post-checkout",
 			    oid_to_hex(orig ? orig : null_oid()),
 			    oid_to_hex(oid), "1", NULL);
 
-- 
2.34.0.831.gd33babec0d1
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 00/17] hook.[ch]: new library to run hooks + simple hook conversion

A trivial update to v5 to rebase it past conflicts with topics that recently landed on "master". For v5 see: https://lore.kernel.org/git/cover-v5-00.17-00000000000-20211123T114206Z-avarab@gmail.com/

Emily Shaffer (14):
  hook: add 'run' subcommand
  gc: use hook library for pre-auto-gc hook
  am: convert {pre,post}-applypatch to use hook.h
  rebase: convert pre-rebase to use hook.h
  am: convert applypatch-msg to use hook.h
  merge: convert post-merge to use hook.h
  hooks: convert non-worktree 'post-checkout' hook to hook library
  hooks: convert worktree 'post-checkout' hook to hook library
  send-email: use 'git hook run' for 'sendemail-validate'
  git-p4: use 'git hook' to run hooks
  commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
  read-cache: convert post-index-change to use hook.h
  receive-pack: convert push-to-checkout hook to hook.h
  run-command: remove old run_hook_{le,ve}() hook API
Ævar Arnfjörð Bjarmason (3):
  hook API: add a run_hooks() wrapper
  hook API: add a run_hooks_l() wrapper
  git hook run: add an --ignore-missing flag
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  45 +++++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/am.c               |   6 +-
 builtin/checkout.c         |   3 +-
 builtin/clone.c            |   3 +-
 builtin/gc.c               |   3 +-
 builtin/hook.c             |  84 +++++++++++++++++++++++
 builtin/merge.c            |   2 +-
 builtin/rebase.c           |   3 +-
 builtin/receive-pack.c     |   7 +-
 builtin/worktree.c         |  26 +++----
 command-list.txt           |   1 +
 commit.c                   |  15 +++--
 git-p4.py                  |  70 ++-----------------
 git-send-email.perl        |  22 +++---
 git.c                      |   1 +
 hook.c                     | 131 ++++++++++++++++++++++++++++++++++++
 hook.h                     |  57 ++++++++++++++++
 read-cache.c               |   3 +-
 reset.c                    |   3 +-
 run-command.c              |  33 ---------
 run-command.h              |  17 -----
 t/t1800-hook.sh            | 134 +++++++++++++++++++++++++++++++++++++
 t/t9001-send-email.sh      |   4 +-
 27 files changed, 522 insertions(+), 158 deletions(-)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Range-diff against v5:
 1:  4ca52feebb8 =  1:  ba6fd47482e hook: add 'run' subcommand
 2:  6275b97a306 =  2:  cfba5c139e7 hook API: add a run_hooks() wrapper
 3:  b5b3051b2e5 =  3:  a4cca074bcb gc: use hook library for pre-auto-gc hook
 4:  c88eb5d4c25 =  4:  ce57ce1adcb am: convert {pre,post}-applypatch to use hook.h
 5:  1d8f7b7e4c1 =  5:  d6162fbef80 hook API: add a run_hooks_l() wrapper
 6:  d49a1444345 =  6:  4c1a8951fc5 rebase: convert pre-rebase to use hook.h
 7:  191fdad0165 =  7:  d8aa5e8345f am: convert applypatch-msg to use hook.h
 8:  119b92fbeae =  8:  6f8d3754b4f merge: convert post-merge to use hook.h
 9:  359ba416e84 !  9:  d3107034806 hooks: convert non-worktree 'post-checkout' hook to hook library
    @@ builtin/checkout.c
      #include "ll-merge.h"
      #include "lockfile.h"
      #include "merge-recursive.h"
    -@@ builtin/checkout.c: struct branch_info {
    +@@ builtin/checkout.c: static void branch_info_release(struct branch_info *info)
      static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
      			      int changed)
      {
10:  b7599be95a7 ! 10:  bff7c1513ca hooks: convert worktree 'post-checkout' hook to hook library
    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam
     -		const char *hook = find_hook("post-checkout");
     -		if (hook) {
     -			const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
    --			cp.git_cmd = 0;
    +-			struct child_process cp = CHILD_PROCESS_INIT;
     -			cp.no_stdin = 1;
     -			cp.stdout_to_stderr = 1;
     -			cp.dir = path;
    --			cp.env = env;
    --			cp.argv = NULL;
    +-			strvec_pushv(&cp.env_array, env);
     -			cp.trace2_hook_name = "post-checkout";
     -			strvec_pushl(&cp.args, absolute_path(hook),
     -				     oid_to_hex(null_oid()),
11:  f1c84d7f627 = 11:  7d9c0a73568 git hook run: add an --ignore-missing flag
12:  4e0f94d9102 = 12:  8ea3b250dff send-email: use 'git hook run' for 'sendemail-validate'
13:  e858f332a62 = 13:  a184afd1ffd git-p4: use 'git hook' to run hooks
14:  9a5956cc028 = 14:  1a43e50617f commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
15:  6fd47c4c499 = 15:  08b7e63ba5b read-cache: convert post-index-change to use hook.h
16:  b201ea46f4b = 16:  c47b36ab41a receive-pack: convert push-to-checkout hook to hook.h
17:  281d17b04db ! 17:  7b99a4b633c run-command: remove old run_hook_{le,ve}() hook API
    @@ run-command.c: int async_with_fork(void)
     -	strvec_push(&hook.args, p);
     -	while ((p = va_arg(args, const char *)))
     -		strvec_push(&hook.args, p);
    --	hook.env = env;
    +-	if (env)
    +-		strvec_pushv(&hook.env_array, (const char **)env);
     -	hook.no_stdin = 1;
     -	hook.stdout_to_stderr = 1;
     -	hook.trace2_hook_name = name;
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 01/17] hook: add 'run' subcommand

From: Emily Shaffer <emilyshaffer@google.com>

In order to enable hooks to be run as an external process, by a standalone Git command, or by tools which wrap Git, provide an external means to run all configured hook commands for a given hook event.

Most of our hooks require more complex functionality than this, but let's start with the bare minimum required to support our simplest hooks.

In terms of implementation the usage_with_options() and "goto usage" pattern here mirrors that of builtin/{commit-graph,multi-pack-index}.c.

Some of the implementation here, such as a function being named run_hooks_opt() when it's tasked with running one hook, to using the run_processes_parallel_tr2() API to run with jobs=1 is somewhere between a bit odd and and an overkill for the current features of this "hook run" command and the hook.[ch] API.

This code will eventually be able to run multiple hooks declared in config in parallel, by starting out with these names and APIs we reduce the later churn of renaming functions, switching from the run_command() to run_processes_parallel_tr2() API etc.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 .gitignore                 |   1 +
 Documentation/git-hook.txt |  37 +++++++++++
 Documentation/githooks.txt |   4 ++
 Makefile                   |   1 +
 builtin.h                  |   1 +
 builtin/hook.c             |  80 +++++++++++++++++++++++
 command-list.txt           |   1 +
 git.c                      |   1 +
 hook.c                     | 102 +++++++++++++++++++++++++++++
 hook.h                     |  35 ++++++++++
 t/t1800-hook.sh            | 129 +++++++++++++++++++++++++++++++++++++
 11 files changed, 392 insertions(+)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1800-hook.sh
Show changes to 11 files +392 −0

.gitignore, Documentation/git-hook.txt, Documentation/githooks.txt, Makefile, builtin.h, builtin/hook.c, command-list.txt, git.c, hook.c, hook.h, t/t1800-hook.sh

diff --git a/.gitignore b/.gitignore
index 054249b20a8..f817c509ec0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@
 /git-grep
 /git-hash-object
 /git-help
+/git-hook
 /git-http-backend
 /git-http-fetch
 /git-http-push
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
new file mode 100644
index 00000000000..e39b1b5d069
--- /dev/null
+++ b/Documentation/git-hook.txt
@@ -0,0 +1,37 @@
+git-hook(1)
+===========
+
+NAME
+----
+git-hook - Run git hooks
+
+SYNOPSIS
+--------
+[verse]
+'git hook' run <hook-name> [-- <hook-args>]
+
+DESCRIPTION
+-----------
+
+A command interface to running git hooks (see linkgit:githooks[5]),
+for use by other scripted git commands.
+
+SUBCOMMANDS
+-----------
+
+run::
+	Run the `<hook-name>` hook. See linkgit:githooks[5] for
+	supported hook names.
++
+
+Any positional arguments to the hook should be passed after a
+mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
+linkgit:githooks[5] for arguments hooks might expect (if any).
+
+SEE ALSO
+--------
+linkgit:githooks[5]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index b51959ff941..a16e62bc8c8 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -698,6 +698,10 @@ and "0" meaning they were not.
 Only one parameter should be set to "1" when the hook runs.  The hook
 running passing "1", "1" should not be possible.
 
+SEE ALSO
+--------
+linkgit:git-hook[1]
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index 75ed168adbc..9a3cbc8c414 100644
--- a/Makefile
+++ b/Makefile
@@ -1109,6 +1109,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
 BUILTIN_OBJS += builtin/grep.o
 BUILTIN_OBJS += builtin/hash-object.o
 BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/hook.o
 BUILTIN_OBJS += builtin/index-pack.o
 BUILTIN_OBJS += builtin/init-db.o
 BUILTIN_OBJS += builtin/interpret-trailers.o
diff --git a/builtin.h b/builtin.h
index 8a58743ed63..83379f3832c 100644
--- a/builtin.h
+++ b/builtin.h
@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);
 int cmd_grep(int argc, const char **argv, const char *prefix);
 int cmd_hash_object(int argc, const char **argv, const char *prefix);
 int cmd_help(int argc, const char **argv, const char *prefix);
+int cmd_hook(int argc, const char **argv, const char *prefix);
 int cmd_index_pack(int argc, const char **argv, const char *prefix);
 int cmd_init_db(int argc, const char **argv, const char *prefix);
 int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);
diff --git a/builtin/hook.c b/builtin/hook.c
new file mode 100644
index 00000000000..9b67ff50cef
--- /dev/null
+++ b/builtin/hook.c
@@ -0,0 +1,80 @@
+#include "cache.h"
+#include "builtin.h"
+#include "config.h"
+#include "hook.h"
+#include "parse-options.h"
+#include "strbuf.h"
+#include "strvec.h"
+
+#define BUILTIN_HOOK_RUN_USAGE \
+	N_("git hook run <hook-name> [-- <hook-args>]")
+
+static const char * const builtin_hook_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static const char * const builtin_hook_run_usage[] = {
+	BUILTIN_HOOK_RUN_USAGE,
+	NULL
+};
+
+static int run(int argc, const char **argv, const char *prefix)
+{
+	int i;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	const char *hook_name;
+	struct option run_options[] = {
+		OPT_END(),
+	};
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, run_options,
+			     builtin_hook_run_usage,
+			     PARSE_OPT_KEEP_DASHDASH);
+
+	if (!argc)
+		goto usage;
+
+	/*
+	 * Having a -- for "run" when providing <hook-args> is
+	 * mandatory.
+	 */
+	if (argc > 1 && strcmp(argv[1], "--") &&
+	    strcmp(argv[1], "--end-of-options"))
+		goto usage;
+
+	/* Add our arguments, start after -- */
+	for (i = 2 ; i < argc; i++)
+		strvec_push(&opt.args, argv[i]);
+
+	/* Need to take into account core.hooksPath */
+	git_config(git_default_config, NULL);
+
+	hook_name = argv[0];
+	opt.error_if_missing = 1;
+	ret = run_hooks_opt(hook_name, &opt);
+	if (ret < 0) /* error() return */
+		ret = 1;
+	return ret;
+usage:
+	usage_with_options(builtin_hook_run_usage, run_options);
+}
+
+int cmd_hook(int argc, const char **argv, const char *prefix)
+{
+	struct option builtin_hook_options[] = {
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, NULL, builtin_hook_options,
+			     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+	if (!argc)
+		goto usage;
+
+	if (!strcmp(argv[0], "run"))
+		return run(argc, argv, prefix);
+
+usage:
+	usage_with_options(builtin_hook_usage, builtin_hook_options);
+}
diff --git a/command-list.txt b/command-list.txt
index 675c28f0bd0..9bd6f3c48f4 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info
 git-gui                                 mainporcelain
 git-hash-object                         plumbingmanipulators
 git-help                                ancillaryinterrogators          complete
+git-hook                                purehelpers
 git-http-backend                        synchingrepositories
 git-http-fetch                          synchelpers
 git-http-push                           synchelpers
diff --git a/git.c b/git.c
index 7edafd8ecff..7030f1180b3 100644
--- a/git.c
+++ b/git.c
@@ -541,6 +541,7 @@ static struct cmd_struct commands[] = {
 	{ "grep", cmd_grep, RUN_SETUP_GENTLY },
 	{ "hash-object", cmd_hash_object },
 	{ "help", cmd_help },
+	{ "hook", cmd_hook, RUN_SETUP },
 	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
 	{ "init", cmd_init_db },
 	{ "init-db", cmd_init_db },
diff --git a/hook.c b/hook.c
index 55e1145a4b7..a0917cf877c 100644
--- a/hook.c
+++ b/hook.c
@@ -1,6 +1,7 @@
 #include "cache.h"
 #include "hook.h"
 #include "run-command.h"
+#include "config.h"
 
 const char *find_hook(const char *name)
 {
@@ -40,3 +41,104 @@ int hook_exists(const char *name)
 {
 	return !!find_hook(name);
 }
+
+static int pick_next_hook(struct child_process *cp,
+			  struct strbuf *out,
+			  void *pp_cb,
+			  void **pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = hook_cb->hook_path;
+
+	if (!hook_path)
+		return 0;
+
+	cp->no_stdin = 1;
+	strvec_pushv(&cp->env_array, hook_cb->options->env.v);
+	cp->stdout_to_stderr = 1;
+	cp->trace2_hook_name = hook_cb->hook_name;
+
+	strvec_push(&cp->args, hook_path);
+	strvec_pushv(&cp->args, hook_cb->options->args.v);
+
+	/* Provide context for errors if necessary */
+	*pp_task_cb = (char *)hook_path;
+
+	/*
+	 * This pick_next_hook() will be called again, we're only
+	 * running one hook, so indicate that no more work will be
+	 * done.
+	 */
+	hook_cb->hook_path = NULL;
+
+	return 1;
+}
+
+static int notify_start_failure(struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cp)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+	const char *hook_path = pp_task_cp;
+
+	hook_cb->rc |= 1;
+
+	strbuf_addf(out, _("Couldn't start hook '%s'\n"),
+		    hook_path);
+
+	return 1;
+}
+
+static int notify_hook_finished(int result,
+				struct strbuf *out,
+				void *pp_cb,
+				void *pp_task_cb)
+{
+	struct hook_cb_data *hook_cb = pp_cb;
+
+	hook_cb->rc |= result;
+
+	return 0;
+}
+
+static void run_hooks_opt_clear(struct run_hooks_opt *options)
+{
+	strvec_clear(&options->env);
+	strvec_clear(&options->args);
+}
+
+int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
+{
+	struct hook_cb_data cb_data = {
+		.rc = 0,
+		.hook_name = hook_name,
+		.options = options,
+	};
+	const char *const hook_path = find_hook(hook_name);
+	int jobs = 1;
+	int ret = 0;
+
+	if (!options)
+		BUG("a struct run_hooks_opt must be provided to run_hooks");
+
+	if (!hook_path && !options->error_if_missing)
+		goto cleanup;
+
+	if (!hook_path) {
+		ret = error("cannot find a hook named %s", hook_name);
+		goto cleanup;
+	}
+
+	cb_data.hook_path = hook_path;
+	run_processes_parallel_tr2(jobs,
+				   pick_next_hook,
+				   notify_start_failure,
+				   notify_hook_finished,
+				   &cb_data,
+				   "hook",
+				   hook_name);
+	ret = cb_data.rc;
+cleanup:
+	run_hooks_opt_clear(options);
+	return ret;
+}
diff --git a/hook.h b/hook.h
index 6aa36fc7ff9..782385cc235 100644
--- a/hook.h
+++ b/hook.h
@@ -1,5 +1,31 @@
 #ifndef HOOK_H
 #define HOOK_H
+#include "strvec.h"
+
+struct run_hooks_opt
+{
+	/* Environment vars to be set for each hook */
+	struct strvec env;
+
+	/* Args to be passed to each hook */
+	struct strvec args;
+
+	/* Emit an error if the hook is missing */
+	unsigned int error_if_missing:1;
+};
+
+#define RUN_HOOKS_OPT_INIT { \
+	.env = STRVEC_INIT, \
+	.args = STRVEC_INIT, \
+}
+
+struct hook_cb_data {
+	/* rc reflects the cumulative failure state */
+	int rc;
+	const char *hook_name;
+	const char *hook_path;
+	struct run_hooks_opt *options;
+};
 
 /*
  * Returns the path to the hook file, or NULL if the hook is missing
@@ -13,4 +39,13 @@ const char *find_hook(const char *name);
  */
 int hook_exists(const char *hookname);
 
+/**
+ * Takes a `hook_name`, resolves it to a path with find_hook(), and
+ * runs the hook for you with the options specified in "struct
+ * run_hooks opt". Will free memory associated with the "struct run_hooks_opt".
+ *
+ * Returns the status code of the run hook, or a negative value on
+ * error().
+ */
+int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
 #endif
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
new file mode 100755
index 00000000000..3aea1b105f0
--- /dev/null
+++ b/t/t1800-hook.sh
@@ -0,0 +1,129 @@
+#!/bin/sh
+
+test_description='git-hook command'
+
+TEST_PASSES_SANITIZE_LEAK=true
+. ./test-lib.sh
+
+test_expect_success 'git hook usage' '
+	test_expect_code 129 git hook &&
+	test_expect_code 129 git hook run &&
+	test_expect_code 129 git hook run -h &&
+	test_expect_code 129 git hook run --unknown 2>err &&
+	grep "unknown option" err
+'
+
+test_expect_success 'git hook run: nonexistent hook' '
+	cat >stderr.expect <<-\EOF &&
+	error: cannot find a hook named test-hook
+	EOF
+	test_expect_code 1 git hook run test-hook 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual
+'
+
+test_expect_success 'git hook run: basic' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	EOF
+	git hook run test-hook 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run: stdout and stderr both write to our stderr' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo >&1 Will end up on stderr
+	echo >&2 Will end up on stderr
+	EOF
+
+	cat >stderr.expect <<-\EOF &&
+	Will end up on stderr
+	Will end up on stderr
+	EOF
+	git hook run test-hook >stdout.actual 2>stderr.actual &&
+	test_cmp stderr.expect stderr.actual &&
+	test_must_be_empty stdout.actual
+'
+
+test_expect_success 'git hook run: exit codes are passed along' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 1
+	EOF
+
+	test_expect_code 1 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 2
+	EOF
+
+	test_expect_code 2 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 128
+	EOF
+
+	test_expect_code 128 git hook run test-hook &&
+
+	write_script .git/hooks/test-hook <<-EOF &&
+	exit 129
+	EOF
+
+	test_expect_code 129 git hook run test-hook
+'
+
+test_expect_success 'git hook run arg u ments without -- is not allowed' '
+	test_expect_code 129 git hook run test-hook arg u ments
+'
+
+test_expect_success 'git hook run -- pass arguments' '
+	write_script .git/hooks/test-hook <<-\EOF &&
+	echo $1
+	echo $2
+	EOF
+
+	cat >expect <<-EOF &&
+	arg
+	u ments
+	EOF
+
+	git hook run test-hook -- arg "u ments" 2>actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git hook run -- out-of-repo runs excluded' '
+	write_script .git/hooks/test-hook <<-EOF &&
+	echo Test hook
+	EOF
+
+	nongit test_must_fail git hook run test-hook
+'
+
+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
+	mkdir my-hooks &&
+	write_script my-hooks/test-hook <<-\EOF &&
+	echo Hook ran $1 >>actual
+	EOF
+
+	cat >expect <<-\EOF &&
+	Test hook
+	Hook ran one
+	Hook ran two
+	Hook ran three
+	Hook ran four
+	EOF
+
+	# Test various ways of specifying the path. See also
+	# t1350-config-hooks-path.sh
+	>actual &&
+	git hook run test-hook -- ignored 2>>actual &&
+	git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&
+	git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&
+	git -c core.hooksPath="$PWD/my-hooks/" hook run test-hook -- four 2>>actual &&
+	test_cmp expect actual
+'
+
+test_done
-- 
2.34.1.1146.gb52885e7c44
Emily Shaffer· Jan 7, 2022, 21:53 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v6 01/17] hook: add 'run' subcommand

On Wed, Dec 22, 2021 at 04:59:27AM +0100, Ævar Arnfjörð Bjarmason wrote:
Show 11 quoted lines
> diff --git a/hook.h b/hook.h
> +struct run_hooks_opt
> +{
> +	/* Environment vars to be set for each hook */
> +	struct strvec env;
> +
> +	/* Args to be passed to each hook */
> +	struct strvec args;
> +
> +	/* Emit an error if the hook is missing */
> +	unsigned int error_if_missing:1;

I wonder if it's premature to include error_if_missing, if we will always set it to 1 until way down in patch 11.

But I do not care all that much - at this point, I'll be honest, I'd just like to see the series merged.

Show 9 quoted lines
> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
> new file mode 100755
> +test_expect_success 'git hook run: nonexistent hook' '
> +	cat >stderr.expect <<-\EOF &&
> +	error: cannot find a hook named test-hook
> +	EOF
> +	test_expect_code 1 git hook run test-hook 2>stderr.actual &&
> +	test_cmp stderr.expect stderr.actual
> +'

It's a little unclear to me what "nonexistent" means - does it mean that it's a hook unknown to Git (e.g. not in hook-list.h)? Does it mean that the hook path doesn't exist? Does it mean the hook path is not executable?

I had to poke in the code a little bit but it looks like this whole known_hook() distinction has disappeared, so maybe I only find it confusing because I reviewed previous iterations.

It might be nice to at least leave a comment on this test case and point out that it relies on nobody having created a test-hook script in an earlier test. Or to clean up that path just in case, at the beginning of this one. I could see someone well-meaning adding another test before this for something simple and breaking this test.

No serious concerns here though - just a couple thoughts.
 - Emily
Junio C Hamano· Jan 7, 2022, 23:08 UTC · re: Emily Shaffer · lore

Re: [PATCH v6 01/17] hook: add 'run' subcommand

Emily Shaffer <emilyshaffer@google.com> writes:
Show 33 quoted lines
> On Wed, Dec 22, 2021 at 04:59:27AM +0100, Ævar Arnfjörð Bjarmason wrote:
>> diff --git a/hook.h b/hook.h
>> +struct run_hooks_opt
>> +{
>> +	/* Environment vars to be set for each hook */
>> +	struct strvec env;
>> +
>> +	/* Args to be passed to each hook */
>> +	struct strvec args;
>> +
>> +	/* Emit an error if the hook is missing */
>> +	unsigned int error_if_missing:1;
>
> I wonder if it's premature to include error_if_missing, if we will
> always set it to 1 until way down in patch 11.
>
> But I do not care all that much - at this point, I'll be honest, I'd
> just like to see the series merged.
>
>> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
>> new file mode 100755
>> +test_expect_success 'git hook run: nonexistent hook' '
>> +	cat >stderr.expect <<-\EOF &&
>> +	error: cannot find a hook named test-hook
>> +	EOF
>> +	test_expect_code 1 git hook run test-hook 2>stderr.actual &&
>> +	test_cmp stderr.expect stderr.actual
>> +'
>
> It's a little unclear to me what "nonexistent" means - does it mean that
> it's a hook unknown to Git (e.g. not in hook-list.h)? Does it mean that
> the hook path doesn't exist? Does it mean the hook path is not
> executable?

I think I saw the same puzzlement from another reviewer. It needs to be clarified if the documentation patch does not do a good job.

> It might be nice to at least leave a comment on this test case and point
> out that it relies on nobody having created a test-hook script in an
> earlier test. Or to clean up that path just in case, at the beginning of
> this one.
Yup, I agree that the latter is a good practice.
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 03/17] gc: use hook library for pre-auto-gc hook

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-auto-gc hook away from run-command.h to and over to the new hook.h library. This uses the new run_hooks() wrapper.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/gc.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
Show changes to builtin/gc.c +2 −1
diff --git a/builtin/gc.c b/builtin/gc.c
index bcef6a4c8da..4bbc58aae5b 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -32,6 +32,7 @@
 #include "remote.h"
 #include "object-store.h"
 #include "exec-cmd.h"
+#include "hook.h"
 
 #define FAILED_RUN "failed to run %s"
 
@@ -394,7 +395,7 @@ static int need_to_gc(void)
 	else
 		return 0;
 
-	if (run_hook_le(NULL, "pre-auto-gc", NULL))
+	if (run_hooks("pre-auto-gc"))
 		return 0;
 	return 1;
 }
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 04/17] am: convert {pre,post}-applypatch to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach pre-applypatch and post-applypatch to use the hook.h library instead of the run-command.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/am.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Show changes to builtin/am.c +2 −2
diff --git a/builtin/am.c b/builtin/am.c
index 8677ea2348a..4b334cb7b12 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -1609,7 +1609,7 @@ static void do_commit(const struct am_state *state)
 	const char *reflog_msg, *author, *committer = NULL;
 	struct strbuf sb = STRBUF_INIT;
 
-	if (run_hook_le(NULL, "pre-applypatch", NULL))
+	if (run_hooks("pre-applypatch"))
 		exit(1);
 
 	if (write_cache_as_tree(&tree, 0, NULL))
@@ -1661,7 +1661,7 @@ static void do_commit(const struct am_state *state)
 		fclose(fp);
 	}
 
-	run_hook_le(NULL, "post-applypatch", NULL);
+	run_hooks("post-applypatch");
 
 	strbuf_release(&sb);
 }
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 06/17] rebase: convert pre-rebase to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the pre-rebase hook away from run-command.h to and over to the new hook.h library.

Since this hook needs arguments introduce a run_hooksl() wrapper, like run_hooks(), but it takes varargs.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/rebase.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
Show changes to builtin/rebase.c +2 −1
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 34b4744e5f3..ac4120013a0 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -28,6 +28,7 @@
 #include "sequencer.h"
 #include "rebase-interactive.h"
 #include "reset.h"
+#include "hook.h"
 
 #define DEFAULT_REFLOG_ACTION "rebase"
 
@@ -1712,7 +1713,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 
 	/* If a hook exists, give it a chance to interrupt*/
 	if (!ok_to_skip_pre_rebase &&
-	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
+	    run_hooks_l("pre-rebase", options.upstream_arg,
 			argc ? argv[0] : NULL, NULL))
 		die(_("The pre-rebase hook refused to rebase."));
 
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 05/17] hook API: add a run_hooks_l() wrapper

Add a run_hooks_l() wrapper, we'll use it in subsequent commits for the simple cases of wanting to run a single hook under a given name along with a list of arguments.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 hook.c | 14 ++++++++++++++
 hook.h | 10 ++++++++++
 2 files changed, 24 insertions(+)
Show changes to 2 files +24 −0

hook.c, hook.h

diff --git a/hook.c b/hook.c
index d67a114e62d..1ad123422b2 100644
--- a/hook.c
+++ b/hook.c
@@ -149,3 +149,17 @@ int run_hooks(const char *hook_name)
 
 	return run_hooks_opt(hook_name, &opt);
 }
+
+int run_hooks_l(const char *hook_name, ...)
+{
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	va_list ap;
+	const char *arg;
+
+	va_start(ap, hook_name);
+	while ((arg = va_arg(ap, const char *)))
+		strvec_push(&opt.args, arg);
+	va_end(ap);
+
+	return run_hooks_opt(hook_name, &opt);
+}
diff --git a/hook.h b/hook.h
index 9c358789958..54528395953 100644
--- a/hook.h
+++ b/hook.h
@@ -54,4 +54,14 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
  * run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
  */
 int run_hooks(const char *hook_name);
+
+/**
+ * Like run_hooks(), a wrapper for run_hooks_opt().
+ *
+ * In addition to the wrapping behavior provided by run_hooks(), this
+ * wrapper takes a list of strings terminated by a NULL
+ * argument. These things will be used as positional arguments to the
+ * hook. This function behaves like the old run_hook_le() API.
+ */
+int run_hooks_l(const char *hook_name, ...);
 #endif
-- 
2.34.1.1146.gb52885e7c44
Glen Choo· Jan 6, 2022, 18:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v6 05/17] hook API: add a run_hooks_l() wrapper

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:
Show 53 quoted lines
> Add a run_hooks_l() wrapper, we'll use it in subsequent commits for
> the simple cases of wanting to run a single hook under a given name
> along with a list of arguments.
>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  hook.c | 14 ++++++++++++++
>  hook.h | 10 ++++++++++
>  2 files changed, 24 insertions(+)
>
> diff --git a/hook.c b/hook.c
> index d67a114e62d..1ad123422b2 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -149,3 +149,17 @@ int run_hooks(const char *hook_name)
>  
>  	return run_hooks_opt(hook_name, &opt);
>  }
> +
> +int run_hooks_l(const char *hook_name, ...)
> +{
> +	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
> +	va_list ap;
> +	const char *arg;
> +
> +	va_start(ap, hook_name);
> +	while ((arg = va_arg(ap, const char *)))
> +		strvec_push(&opt.args, arg);
> +	va_end(ap);
> +
> +	return run_hooks_opt(hook_name, &opt);
> +}
> diff --git a/hook.h b/hook.h
> index 9c358789958..54528395953 100644
> --- a/hook.h
> +++ b/hook.h
> @@ -54,4 +54,14 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
>   * run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
>   */
>  int run_hooks(const char *hook_name);
> +
> +/**
> + * Like run_hooks(), a wrapper for run_hooks_opt().
> + *
> + * In addition to the wrapping behavior provided by run_hooks(), this
> + * wrapper takes a list of strings terminated by a NULL
> + * argument. These things will be used as positional arguments to the
> + * hook. This function behaves like the old run_hook_le() API.
> + */
> +int run_hooks_l(const char *hook_name, ...);
>  #endif
> -- 
> 2.34.1.1146.gb52885e7c44

Same comments as patch 2 [1], i.e. this looks good; it's technically dead code, but that's mostly a matter of taste.

[1] https://lore.kernel.org/git/kl6lmtk87l1t.fsf@chooglen-macbookpro.roam.corp.google.com
Emily Shaffer· Jan 7, 2022, 21:56 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v6 05/17] hook API: add a run_hooks_l() wrapper

On Wed, Dec 22, 2021 at 04:59:31AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> Add a run_hooks_l() wrapper, we'll use it in subsequent commits for
> the simple cases of wanting to run a single hook under a given name
> along with a list of arguments.

I've got a slight allergy to varargs, just as personal preference, but there's no denying that this makes the following callsite conversions extremely mechanical and easy to review.

Does this calling convention make it easier for us to try and inline results from something like oid_to_hex(), which may become stale, compared to needing to put together a struct with args? I think probably it's fine - because strvec_push() creates a copy, and run_hooks_l() is not something we'd want to call multiple times in parallel (I hope).

Anyway,
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
Show 51 quoted lines
> 
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  hook.c | 14 ++++++++++++++
>  hook.h | 10 ++++++++++
>  2 files changed, 24 insertions(+)
> 
> diff --git a/hook.c b/hook.c
> index d67a114e62d..1ad123422b2 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -149,3 +149,17 @@ int run_hooks(const char *hook_name)
>  
>  	return run_hooks_opt(hook_name, &opt);
>  }
> +
> +int run_hooks_l(const char *hook_name, ...)
> +{
> +	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
> +	va_list ap;
> +	const char *arg;
> +
> +	va_start(ap, hook_name);
> +	while ((arg = va_arg(ap, const char *)))
> +		strvec_push(&opt.args, arg);
> +	va_end(ap);
> +
> +	return run_hooks_opt(hook_name, &opt);
> +}
> diff --git a/hook.h b/hook.h
> index 9c358789958..54528395953 100644
> --- a/hook.h
> +++ b/hook.h
> @@ -54,4 +54,14 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
>   * run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
>   */
>  int run_hooks(const char *hook_name);
> +
> +/**
> + * Like run_hooks(), a wrapper for run_hooks_opt().
> + *
> + * In addition to the wrapping behavior provided by run_hooks(), this
> + * wrapper takes a list of strings terminated by a NULL
> + * argument. These things will be used as positional arguments to the
> + * hook. This function behaves like the old run_hook_le() API.
> + */
> +int run_hooks_l(const char *hook_name, ...);
>  #endif
> -- 
> 2.34.1.1146.gb52885e7c44
> 
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 02/17] hook API: add a run_hooks() wrapper

Add a run_hooks() wrapper, we'll use it in subsequent commits for the simple cases of wanting to run a single hook under a given name, without providing options such as "env" or "args".

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 hook.c | 7 +++++++
 hook.h | 6 ++++++
 2 files changed, 13 insertions(+)
Show changes to 2 files +13 −0

hook.c, hook.h

diff --git a/hook.c b/hook.c
index a0917cf877c..d67a114e62d 100644
--- a/hook.c
+++ b/hook.c
@@ -142,3 +142,10 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 	run_hooks_opt_clear(options);
 	return ret;
 }
+
+int run_hooks(const char *hook_name)
+{
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+	return run_hooks_opt(hook_name, &opt);
+}
diff --git a/hook.h b/hook.h
index 782385cc235..9c358789958 100644
--- a/hook.h
+++ b/hook.h
@@ -48,4 +48,10 @@ int hook_exists(const char *hookname);
  * error().
  */
 int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
+
+/**
+ * A wrapper for run_hooks_opt() which provides a dummy "struct
+ * run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
+ */
+int run_hooks(const char *hook_name);
 #endif
-- 
2.34.1.1146.gb52885e7c44
Glen Choo· Jan 6, 2022, 18:53 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v6 02/17] hook API: add a run_hooks() wrapper

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:
Show 42 quoted lines
> Add a run_hooks() wrapper, we'll use it in subsequent commits for the
> simple cases of wanting to run a single hook under a given name,
> without providing options such as "env" or "args".
>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  hook.c | 7 +++++++
>  hook.h | 6 ++++++
>  2 files changed, 13 insertions(+)
>
> diff --git a/hook.c b/hook.c
> index a0917cf877c..d67a114e62d 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -142,3 +142,10 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
>  	run_hooks_opt_clear(options);
>  	return ret;
>  }
> +
> +int run_hooks(const char *hook_name)
> +{
> +	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
> +
> +	return run_hooks_opt(hook_name, &opt);
> +}
> diff --git a/hook.h b/hook.h
> index 782385cc235..9c358789958 100644
> --- a/hook.h
> +++ b/hook.h
> @@ -48,4 +48,10 @@ int hook_exists(const char *hookname);
>   * error().
>   */
>  int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
> +
> +/**
> + * A wrapper for run_hooks_opt() which provides a dummy "struct
> + * run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
> + */
> +int run_hooks(const char *hook_name);
>  #endif
> -- 
> 2.34.1.1146.gb52885e7c44

This patch looks good. As a matter of personal taste, it's not ideal that this is dead code, but it gets tested immediately in the subsequent patches anyway, and those patches are pretty clear.

Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 07/17] am: convert applypatch-msg to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach applypatch-msg to use the hook.h library instead of the run-command.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/am.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to builtin/am.c +1 −1
diff --git a/builtin/am.c b/builtin/am.c
index 4b334cb7b12..ae0c484dcba 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -448,7 +448,7 @@ static int run_applypatch_msg_hook(struct am_state *state)
 	int ret;
 
 	assert(state->msg);
-	ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
+	ret = run_hooks_l("applypatch-msg", am_path(state, "final-commit"), NULL);
 
 	if (!ret) {
 		FREE_AND_NULL(state->msg);
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 10/17] hooks: convert worktree 'post-checkout' hook to hook library

From: Emily Shaffer <emilyshaffer@google.com>

Move the running of the 'post-checkout' hook away from run-command.h to the new hook.h library in builtin/worktree.c. For this special case we need a change to the hook API to teach it to run the hook from a given directory.

We cannot skip the "absolute_path" flag and just check if "dir" is specified as we'd then fail to find our hook in the new dir we'd chdir() to. We currently don't have a use-case for running a hook not in our "base" repository at a given absolute path, so let's have "dir" imply absolute_path(find_hook(hook_name)).

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/worktree.c | 26 +++++++++++---------------
 hook.c             |  8 ++++++++
 hook.h             |  6 ++++++
 3 files changed, 25 insertions(+), 15 deletions(-)
Show changes to 3 files +25 −15

builtin/worktree.c, hook.c, hook.h

diff --git a/builtin/worktree.c b/builtin/worktree.c
index a396cfdc64e..6f03afad974 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -382,21 +382,17 @@ static int add_worktree(const char *path, const char *refname,
 	 * is_junk is cleared, but do return appropriate code when hook fails.
 	 */
 	if (!ret && opts->checkout) {
-		const char *hook = find_hook("post-checkout");
-		if (hook) {
-			const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
-			struct child_process cp = CHILD_PROCESS_INIT;
-			cp.no_stdin = 1;
-			cp.stdout_to_stderr = 1;
-			cp.dir = path;
-			strvec_pushv(&cp.env_array, env);
-			cp.trace2_hook_name = "post-checkout";
-			strvec_pushl(&cp.args, absolute_path(hook),
-				     oid_to_hex(null_oid()),
-				     oid_to_hex(&commit->object.oid),
-				     "1", NULL);
-			ret = run_command(&cp);
-		}
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
+		strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
+		strvec_pushl(&opt.args,
+			     oid_to_hex(null_oid()),
+			     oid_to_hex(&commit->object.oid),
+			     "1",
+			     NULL);
+		opt.dir = path;
+
+		ret = run_hooks_opt("post-checkout", &opt);
 	}
 
 	strvec_clear(&child_env);
diff --git a/hook.c b/hook.c
index 1ad123422b2..69a215b2c3c 100644
--- a/hook.c
+++ b/hook.c
@@ -57,6 +57,7 @@ static int pick_next_hook(struct child_process *cp,
 	strvec_pushv(&cp->env_array, hook_cb->options->env.v);
 	cp->stdout_to_stderr = 1;
 	cp->trace2_hook_name = hook_cb->hook_name;
+	cp->dir = hook_cb->options->dir;
 
 	strvec_push(&cp->args, hook_path);
 	strvec_pushv(&cp->args, hook_cb->options->args.v);
@@ -109,6 +110,7 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
 
 int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 {
+	struct strbuf abs_path = STRBUF_INIT;
 	struct hook_cb_data cb_data = {
 		.rc = 0,
 		.hook_name = hook_name,
@@ -130,6 +132,11 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 	}
 
 	cb_data.hook_path = hook_path;
+	if (options->dir) {
+		strbuf_add_absolute_path(&abs_path, hook_path);
+		cb_data.hook_path = abs_path.buf;
+	}
+
 	run_processes_parallel_tr2(jobs,
 				   pick_next_hook,
 				   notify_start_failure,
@@ -139,6 +146,7 @@ int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
 				   hook_name);
 	ret = cb_data.rc;
 cleanup:
+	strbuf_release(&abs_path);
 	run_hooks_opt_clear(options);
 	return ret;
 }
diff --git a/hook.h b/hook.h
index 54528395953..18d90aedf14 100644
--- a/hook.h
+++ b/hook.h
@@ -12,6 +12,12 @@ struct run_hooks_opt
 
 	/* Emit an error if the hook is missing */
 	unsigned int error_if_missing:1;
+
+	/**
+	 * An optional initial working directory for the hook,
+	 * translates to "struct child_process"'s "dir" member.
+	 */
+	const char *dir;
 };
 
 #define RUN_HOOKS_OPT_INIT { \
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 09/17] hooks: convert non-worktree 'post-checkout' hook to hook library

From: Emily Shaffer <emilyshaffer@google.com>

Move the running of the 'post-checkout' hook away from run-command.h to the new hook.h library, except in the case of builtin/worktree.c. That special-case will be handled in a subsequent commit.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/checkout.c | 3 ++-
 builtin/clone.c    | 3 ++-
 reset.c            | 3 ++-
 3 files changed, 6 insertions(+), 3 deletions(-)
Show changes to 3 files +6 −3

builtin/checkout.c, builtin/clone.c, reset.c

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 72beeb49aa9..e2e95445407 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -9,6 +9,7 @@
 #include "config.h"
 #include "diff.h"
 #include "dir.h"
+#include "hook.h"
 #include "ll-merge.h"
 #include "lockfile.h"
 #include "merge-recursive.h"
@@ -114,7 +115,7 @@ static void branch_info_release(struct branch_info *info)
 static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
 			      int changed)
 {
-	return run_hook_le(NULL, "post-checkout",
+	return run_hooks_l("post-checkout",
 			   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
 			   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
 			   changed ? "1" : "0", NULL);
diff --git a/builtin/clone.c b/builtin/clone.c
index fb377b27657..ee27b9f8114 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -32,6 +32,7 @@
 #include "connected.h"
 #include "packfile.h"
 #include "list-objects-filter-options.h"
+#include "hook.h"
 
 /*
  * Overall FIXMEs:
@@ -705,7 +706,7 @@ static int checkout(int submodule_progress)
 	if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
 		die(_("unable to write new index file"));
 
-	err |= run_hook_le(NULL, "post-checkout", oid_to_hex(null_oid()),
+	err |= run_hooks_l("post-checkout", oid_to_hex(null_oid()),
 			   oid_to_hex(&oid), "1", NULL);
 
 	if (!err && (option_recurse_submodules.nr > 0)) {
diff --git a/reset.c b/reset.c
index f214df3d96c..0881e636915 100644
--- a/reset.c
+++ b/reset.c
@@ -7,6 +7,7 @@
 #include "tree-walk.h"
 #include "tree.h"
 #include "unpack-trees.h"
+#include "hook.h"
 
 int reset_head(struct repository *r, struct object_id *oid, const char *action,
 	       const char *switch_to_branch, unsigned flags,
@@ -127,7 +128,7 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,
 					    reflog_head);
 	}
 	if (run_hook)
-		run_hook_le(NULL, "post-checkout",
+		run_hooks_l("post-checkout",
 			    oid_to_hex(orig ? orig : null_oid()),
 			    oid_to_hex(oid), "1", NULL);
 
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 08/17] merge: convert post-merge to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Teach post-merge to use the hook.h library instead of the run-command.h library to run hooks.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/merge.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to builtin/merge.c +1 −1
diff --git a/builtin/merge.c b/builtin/merge.c
index 5f0476b0b76..5be3009c2a2 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -487,7 +487,7 @@ static void finish(struct commit *head_commit,
 	}
 
 	/* Run a post-merge hook */
-	run_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL);
+	run_hooks_l("post-merge", squash ? "1" : "0", NULL);
 
 	apply_autostash(git_path_merge_autostash(the_repository));
 	strbuf_release(&reflog_message);
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 11/17] git hook run: add an --ignore-missing flag

For certain one-shot hooks we'd like to optimistically run them, and not complain if they don't exist.

This was already supported by the underlying hook.c library, but had not been exposed via "git hook run". The command version of this will be used by send-email in a subsequent commit.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 Documentation/git-hook.txt | 10 +++++++++-
 builtin/hook.c             |  8 ++++++--
 t/t1800-hook.sh            |  5 +++++
 3 files changed, 20 insertions(+), 3 deletions(-)
Show changes to 3 files +20 −3

Documentation/git-hook.txt, builtin/hook.c, t/t1800-hook.sh

diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index e39b1b5d069..77c3a8ad909 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -8,7 +8,7 @@ git-hook - Run git hooks
 SYNOPSIS
 --------
 [verse]
-'git hook' run <hook-name> [-- <hook-args>]
+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]
 
 DESCRIPTION
 -----------
@@ -28,6 +28,14 @@ Any positional arguments to the hook should be passed after a
 mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
 linkgit:githooks[5] for arguments hooks might expect (if any).
 
+OPTIONS
+-------
+
+--ignore-missing::
+	Ignore any missing hook by quietly returning zero. Used for
+	tools that want to do a blind one-shot run of a hook that may
+	or may not be present.
+
 SEE ALSO
 --------
 linkgit:githooks[5]
diff --git a/builtin/hook.c b/builtin/hook.c
index 9b67ff50cef..54e5c6ec933 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -7,7 +7,7 @@
 #include "strvec.h"
 
 #define BUILTIN_HOOK_RUN_USAGE \
-	N_("git hook run <hook-name> [-- <hook-args>]")
+	N_("git hook run [--ignore-missing] <hook-name> [-- <hook-args>]")
 
 static const char * const builtin_hook_usage[] = {
 	BUILTIN_HOOK_RUN_USAGE,
@@ -23,8 +23,11 @@ static int run(int argc, const char **argv, const char *prefix)
 {
 	int i;
 	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	int ignore_missing = 0;
 	const char *hook_name;
 	struct option run_options[] = {
+		OPT_BOOL(0, "ignore-missing", &ignore_missing,
+			 N_("silently ignore missing requested <hook-name>")),
 		OPT_END(),
 	};
 	int ret;
@@ -52,7 +55,8 @@ static int run(int argc, const char **argv, const char *prefix)
 	git_config(git_default_config, NULL);
 
 	hook_name = argv[0];
-	opt.error_if_missing = 1;
+	if (!ignore_missing)
+		opt.error_if_missing = 1;
 	ret = run_hooks_opt(hook_name, &opt);
 	if (ret < 0) /* error() return */
 		ret = 1;
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 3aea1b105f0..29718aa9913 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -21,6 +21,11 @@ test_expect_success 'git hook run: nonexistent hook' '
 	test_cmp stderr.expect stderr.actual
 '
 
+test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '
+	git hook run --ignore-missing does-not-exist 2>stderr.actual &&
+	test_must_be_empty stderr.actual
+'
+
 test_expect_success 'git hook run: basic' '
 	write_script .git/hooks/test-hook <<-EOF &&
 	echo Test hook
-- 
2.34.1.1146.gb52885e7c44
Glen Choo· Jan 6, 2022, 19:33 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v6 11/17] git hook run: add an --ignore-missing flag

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:
Show 38 quoted lines
> For certain one-shot hooks we'd like to optimistically run them, and
> not complain if they don't exist.
>
> This was already supported by the underlying hook.c library, but had
> not been exposed via "git hook run". The command version of this will
> be used by send-email in a subsequent commit.
>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  Documentation/git-hook.txt | 10 +++++++++-
>  builtin/hook.c             |  8 ++++++--
>  t/t1800-hook.sh            |  5 +++++
>  3 files changed, 20 insertions(+), 3 deletions(-)
>
> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
> index e39b1b5d069..77c3a8ad909 100644
> --- a/Documentation/git-hook.txt
> +++ b/Documentation/git-hook.txt
> @@ -8,7 +8,7 @@ git-hook - Run git hooks
>  SYNOPSIS
>  --------
>  [verse]
> -'git hook' run <hook-name> [-- <hook-args>]
> +'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]
>  
>  DESCRIPTION
>  -----------
> @@ -28,6 +28,14 @@ Any positional arguments to the hook should be passed after a
>  mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
>  linkgit:githooks[5] for arguments hooks might expect (if any).
>  
> +OPTIONS
> +-------
> +
> +--ignore-missing::
> +	Ignore any missing hook by quietly returning zero. Used for
> +	tools that want to do a blind one-shot run of a hook that may
> +	or may not be present.

The most obvious question whenever a new option is introduced is "Is this a descriptive and consistent option name?" The answer we came to in review club is yes, "--ignore-missing" is good :)

However, the description isn't 100% clear about what it means for a hook to be missing (possible confusion: is a missing hook a hook that isn't documented in Documentation/githooks.txt?). I suspect that your wording is intentionally open-ended because Git will eventually look for hooks in the config as well as `.git/hooks`. Nevertheless, it would help to get a precise definition in the docs so that readers know what to expect and we can advise() users consistently.

Show 36 quoted lines
> +
>  SEE ALSO
>  --------
>  linkgit:githooks[5]
> diff --git a/builtin/hook.c b/builtin/hook.c
> index 9b67ff50cef..54e5c6ec933 100644
> --- a/builtin/hook.c
> +++ b/builtin/hook.c
> @@ -7,7 +7,7 @@
>  #include "strvec.h"
>  
>  #define BUILTIN_HOOK_RUN_USAGE \
> -	N_("git hook run <hook-name> [-- <hook-args>]")
> +	N_("git hook run [--ignore-missing] <hook-name> [-- <hook-args>]")
>  
>  static const char * const builtin_hook_usage[] = {
>  	BUILTIN_HOOK_RUN_USAGE,
> @@ -23,8 +23,11 @@ static int run(int argc, const char **argv, const char *prefix)
>  {
>  	int i;
>  	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
> +	int ignore_missing = 0;
>  	const char *hook_name;
>  	struct option run_options[] = {
> +		OPT_BOOL(0, "ignore-missing", &ignore_missing,
> +			 N_("silently ignore missing requested <hook-name>")),
>  		OPT_END(),
>  	};
>  	int ret;
> @@ -52,7 +55,8 @@ static int run(int argc, const char **argv, const char *prefix)
>  	git_config(git_default_config, NULL);
>  
>  	hook_name = argv[0];
> -	opt.error_if_missing = 1;
> +	if (!ignore_missing)
> +		opt.error_if_missing = 1;

It's not clear why --ignore-missing isn't just the default behavior (and judging from the lines above, it looks like it used to be?). After some debate, most of the Google reviewers liked this new default, but it was difficult to assess the merits of the decision because the thought process wasn't obvious.

Here's the line of reasoning: if we only consider the hooks in-tree, it certainly seems like none of the callers actually want to error out if the hook is missing. This suggests that the default behavior of ignoring missing hooks is actually what we want, so why change the default behavior and add another option to worry about?

I can propose at least one reason, which is that "git hook" is also meant to be invoked by end users in the shell and not just by programs. I used to use npm quite a lot, and a pattern I am quite fond of is to have an npm script called "precommit" which runs my linters and formatters, and I'd make `npm run precommit` part of my pre-commit hook. But a nice benefit is that I can also run `npm run precommit` at any time to run all of my tooling, regardless of whether or not I am making a commit. It would be pretty neat to be able to do away with npm and just use `git hook run pre-commit'. In this scenario, I would certainly expect `git hook run pre-commit` to fail if there is no `pre-commit` hook, so yes, this new default behavior can make sense.

But I can't tell if this is something you had intended all along, or if you had something else in mind. If anything, the original cover letter [1] _almost_ seems to suggest otherwise.

  The new "git hook" command is (for now) only for the benefit of
  in-tree non-C programs that need to run hooks, and this series
  converts git-send-email and git-p4 to use it.
It's worth explaining the rationale in the commit message.
>  	ret = run_hooks_opt(hook_name, &opt);
>  	if (ret < 0) /* error() return */
>  		ret = 1;
Show 13 quoted lines
> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
> index 3aea1b105f0..29718aa9913 100755
> --- a/t/t1800-hook.sh
> +++ b/t/t1800-hook.sh
> @@ -21,6 +21,11 @@ test_expect_success 'git hook run: nonexistent hook' '
>  	test_cmp stderr.expect stderr.actual
>  '
>  
> +test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '
> +	git hook run --ignore-missing does-not-exist 2>stderr.actual &&
> +	test_must_be_empty stderr.actual
> +'
> +

For completeness, this should include a test for the non-exceptional case i.e. when the hook isn't missing.

Show 5 quoted lines
>  test_expect_success 'git hook run: basic' '
>  	write_script .git/hooks/test-hook <<-EOF &&
>  	echo Test hook
> -- 
> 2.34.1.1146.gb52885e7c44
[1] https://lore.kernel.org/git/cover-00.13-00000000000-20211012T131934Z-avarab@gmail.com/
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 12/17] send-email: use 'git hook run' for 'sendemail-validate'

From: Emily Shaffer <emilyshaffer@google.com>

Change the "sendmail-validate" hook to be run via the "git hook run" wrapper instead of via a direct invocation.

This is the smallest possibly change to get "send-email" using "git hook run". We still check the hook itself with "-x", and set a "GIT_DIR" variable, both of which are asserted by our tests. We'll need to get rid of this special behavior if we start running N hooks, but for now let's be as close to bug-for-bug compatible as possible.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl   | 22 ++++++++++++++--------
 t/t9001-send-email.sh |  4 ++--
 2 files changed, 16 insertions(+), 10 deletions(-)
Show changes to 2 files +16 −10

git-send-email.perl, t/t9001-send-email.sh

diff --git a/git-send-email.perl b/git-send-email.perl
index 04087221aa7..a98460bdb96 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -225,13 +225,13 @@ sub format_2822_time {
 my $editor;
 
 sub system_or_msg {
-	my ($args, $msg) = @_;
+	my ($args, $msg, $cmd_name) = @_;
 	system(@$args);
 	my $signalled = $? & 127;
 	my $exit_code = $? >> 8;
 	return unless $signalled or $exit_code;
 
-	my @sprintf_args = ($args->[0], $exit_code);
+	my @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);
 	if (defined $msg) {
 		# Quiet the 'redundant' warning category, except we
 		# need to support down to Perl 5.8, so we can't do a
@@ -2075,10 +2075,10 @@ sub validate_patch {
 	my ($fn, $xfer_encoding) = @_;
 
 	if ($repo) {
+		my $hook_name = 'sendemail-validate';
 		my $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');
 		require File::Spec;
-		my $validate_hook = File::Spec->catfile($hooks_path,
-					    'sendemail-validate');
+		my $validate_hook = File::Spec->catfile($hooks_path, $hook_name);
 		my $hook_error;
 		if (-x $validate_hook) {
 			require Cwd;
@@ -2088,13 +2088,19 @@ sub validate_patch {
 			chdir($repo->wc_path() or $repo->repo_path())
 				or die("chdir: $!");
 			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			$hook_error = system_or_msg([$validate_hook, $target]);
+			my @cmd = ("git", "hook", "run", "--ignore-missing",
+				    $hook_name, "--");
+			my @cmd_msg = (@cmd, "<patch>");
+			my @cmd_run = (@cmd, $target);
+			$hook_error = system_or_msg(\@cmd_run, undef, "@cmd_msg");
 			chdir($cwd_save) or die("chdir: $!");
 		}
 		if ($hook_error) {
-			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
-				       "%s\n" .
-				       "warning: no patches were sent\n"), $fn, $hook_error);
+			$hook_error = sprintf(__("fatal: %s: rejected by %s hook\n" .
+						 $hook_error . "\n" .
+						 "warning: no patches were sent\n"),
+					      $fn, $hook_name);
+			die $hook_error;
 		}
 	}
 
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index aa0c20499ba..84d0f40d76a 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -539,7 +539,7 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'my-hooks/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
@@ -558,7 +558,7 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
 	test_path_is_file my-hooks.ran &&
 	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
-	fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1
+	fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 13/17] git-p4: use 'git hook' to run hooks

From: Emily Shaffer <emilyshaffer@google.com>

Instead of duplicating the behavior of run-command.h:run_hook_le() in Python, we can directly call 'git hook run'. We emulate the existence check with the --ignore-missing flag.

We're dropping the "verbose" handling added in 9f59ca4d6af (git-p4: create new function run_git_hook, 2020-02-11), those who want diagnostic output about how hooks are run are now able to get that via e.g. the trace2 facility and GIT_TRACE=1.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-p4.py | 70 +++++--------------------------------------------------
 1 file changed, 6 insertions(+), 64 deletions(-)
Show changes to git-p4.py +6 −64
diff --git a/git-p4.py b/git-p4.py
index 2b4500226aa..3b54168eb4a 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -208,70 +208,12 @@ def decode_path(path):
 
 def run_git_hook(cmd, param=[]):
     """Execute a hook if the hook exists."""
-    if verbose:
-        sys.stderr.write("Looking for hook: %s\n" % cmd)
-        sys.stderr.flush()
-
-    hooks_path = gitConfig("core.hooksPath")
-    if len(hooks_path) <= 0:
-        hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
-
-    if not isinstance(param, list):
-        param=[param]
-
-    # resolve hook file name, OS depdenent
-    hook_file = os.path.join(hooks_path, cmd)
-    if platform.system() == 'Windows':
-        if not os.path.isfile(hook_file):
-            # look for the file with an extension
-            files = glob.glob(hook_file + ".*")
-            if not files:
-                return True
-            files.sort()
-            hook_file = files.pop()
-            while hook_file.upper().endswith(".SAMPLE"):
-                # The file is a sample hook. We don't want it
-                if len(files) > 0:
-                    hook_file = files.pop()
-                else:
-                    return True
-
-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
-        return True
-
-    return run_hook_command(hook_file, param) == 0
-
-def run_hook_command(cmd, param):
-    """Executes a git hook command
-       cmd = the command line file to be executed. This can be
-       a file that is run by OS association.
-
-       param = a list of parameters to pass to the cmd command
-
-       On windows, the extension is checked to see if it should
-       be run with the Git for Windows Bash shell.  If there
-       is no file extension, the file is deemed a bash shell
-       and will be handed off to sh.exe. Otherwise, Windows
-       will be called with the shell to handle the file assocation.
-
-       For non Windows operating systems, the file is called
-       as an executable.
-    """
-    cli = [cmd] + param
-    use_shell = False
-    if platform.system() == 'Windows':
-        (root,ext) = os.path.splitext(cmd)
-        if ext == "":
-            exe_path = os.environ.get("EXEPATH")
-            if exe_path is None:
-                exe_path = ""
-            else:
-                exe_path = os.path.join(exe_path, "bin")
-            cli = [os.path.join(exe_path, "SH.EXE")] + cli
-        else:
-            use_shell = True
-    return subprocess.call(cli, shell=use_shell)
-
+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]
+    if param:
+        args.append("--")
+        for p in param:
+            args.append(p)
+    return subprocess.call(args) == 0
 
 def write_pipe(c, stdin):
     if verbose:
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 14/17] commit: convert {pre-commit,prepare-commit-msg} hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move these hooks hook away from run-command.h to and over to the new hook.h library.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 commit.c | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)
Show changes to commit.c +8 −7
diff --git a/commit.c b/commit.c
index a348f085b2b..931f65d56d6 100644
--- a/commit.c
+++ b/commit.c
@@ -21,6 +21,7 @@
 #include "commit-reach.h"
 #include "run-command.h"
 #include "shallow.h"
+#include "hook.h"
 
 static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
 
@@ -1702,22 +1703,22 @@ size_t ignore_non_trailer(const char *buf, size_t len)
 int run_commit_hook(int editor_is_used, const char *index_file,
 		    const char *name, ...)
 {
-	struct strvec hook_env = STRVEC_INIT;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
 	va_list args;
-	int ret;
+	const char *arg;
 
-	strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
+	strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s", index_file);
 
 	/*
 	 * Let the hook know that no editor will be launched.
 	 */
 	if (!editor_is_used)
-		strvec_push(&hook_env, "GIT_EDITOR=:");
+		strvec_push(&opt.env, "GIT_EDITOR=:");
 
 	va_start(args, name);
-	ret = run_hook_ve(hook_env.v, name, args);
+	while ((arg = va_arg(args, const char *)))
+		strvec_push(&opt.args, arg);
 	va_end(args);
-	strvec_clear(&hook_env);
 
-	return ret;
+	return run_hooks_opt(name, &opt);
 }
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 15/17] read-cache: convert post-index-change to use hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the post-index-change hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of "run_hook_ve()" outside of run-command.c ("run_hook_le()" still uses it). So we can make the function static now. A subsequent commit will remove this code entirely when "run_hook_le()" itself goes away.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 read-cache.c  | 3 ++-
 run-command.c | 2 +-
 run-command.h | 1 -
 3 files changed, 3 insertions(+), 3 deletions(-)
Show changes to 3 files +3 −3

read-cache.c, run-command.c, run-command.h

diff --git a/read-cache.c b/read-cache.c
index cbe73f14e5e..a7fbf144fec 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -28,6 +28,7 @@
 #include "sparse-index.h"
 #include "csum-file.h"
 #include "promisor-remote.h"
+#include "hook.h"
 
 /* Mask for the name length in ce_flags in the on-disk index */
 
@@ -3150,7 +3151,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
 	else
 		ret = close_lock_file_gently(lock);
 
-	run_hook_le(NULL, "post-index-change",
+	run_hooks_l("post-index-change",
 			istate->updated_workdir ? "1" : "0",
 			istate->updated_skipworktree ? "1" : "0", NULL);
 	istate->updated_workdir = 0;
diff --git a/run-command.c b/run-command.c
index 3ea2c2fc101..ea09de040a7 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1313,7 +1313,7 @@ int async_with_fork(void)
 #endif
 }
 
-int run_hook_ve(const char *const *env, const char *name, va_list args)
+static int run_hook_ve(const char *const *env, const char *name, va_list args)
 {
 	struct child_process hook = CHILD_PROCESS_INIT;
 	const char *p;
diff --git a/run-command.h b/run-command.h
index 2be5f5d6422..88a398c300a 100644
--- a/run-command.h
+++ b/run-command.h
@@ -235,7 +235,6 @@ int run_command(struct child_process *);
  */
 LAST_ARG_MUST_BE_NULL
 int run_hook_le(const char *const *env, const char *name, ...);
-int run_hook_ve(const char *const *env, const char *name, va_list args);
 
 /*
  * Trigger an auto-gc
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 16/17] receive-pack: convert push-to-checkout hook to hook.h

From: Emily Shaffer <emilyshaffer@google.com>

Move the push-to-checkout hook away from run-command.h to and over to the new hook.h library.

This removes the last direct user of run_hook_le(), so we could remove that function now, but let's leave that to a follow-up cleanup commit.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 builtin/receive-pack.c | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)
Show changes to builtin/receive-pack.c +5 −2
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 4f92e6f059d..e99b1ecd103 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1424,9 +1424,12 @@ static const char *push_to_checkout(unsigned char *hash,
 				    struct strvec *env,
 				    const char *work_tree)
 {
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+
 	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
-	if (run_hook_le(env->v, push_to_checkout_hook,
-			hash_to_hex(hash), NULL))
+	strvec_pushv(&opt.env, env->v);
+	strvec_push(&opt.args, hash_to_hex(hash));
+	if (run_hooks_opt(push_to_checkout_hook, &opt))
 		return "push-to-checkout hook declined";
 	else
 		return NULL;
-- 
2.34.1.1146.gb52885e7c44
Ævar Arnfjörð Bjarmason· Dec 22, 2021, 03:59 UTC · re: Ævar Arnfjörð Bjarmason · lore

[PATCH v6 17/17] run-command: remove old run_hook_{le,ve}() hook API

From: Emily Shaffer <emilyshaffer@google.com>

The new hook.h library has replaced all run-command.h hook-related functionality. So let's delete this dead code.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 run-command.c | 33 ---------------------------------
 run-command.h | 16 ----------------
 2 files changed, 49 deletions(-)
Show changes to 2 files +0 −49

run-command.c, run-command.h

diff --git a/run-command.c b/run-command.c
index ea09de040a7..f32e4fe1326 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1313,39 +1313,6 @@ int async_with_fork(void)
 #endif
 }
 
-static int run_hook_ve(const char *const *env, const char *name, va_list args)
-{
-	struct child_process hook = CHILD_PROCESS_INIT;
-	const char *p;
-
-	p = find_hook(name);
-	if (!p)
-		return 0;
-
-	strvec_push(&hook.args, p);
-	while ((p = va_arg(args, const char *)))
-		strvec_push(&hook.args, p);
-	if (env)
-		strvec_pushv(&hook.env_array, (const char **)env);
-	hook.no_stdin = 1;
-	hook.stdout_to_stderr = 1;
-	hook.trace2_hook_name = name;
-
-	return run_command(&hook);
-}
-
-int run_hook_le(const char *const *env, const char *name, ...)
-{
-	va_list args;
-	int ret;
-
-	va_start(args, name);
-	ret = run_hook_ve(env, name, args);
-	va_end(args);
-
-	return ret;
-}
-
 struct io_pump {
 	/* initialized by caller */
 	int fd;
diff --git a/run-command.h b/run-command.h
index 88a398c300a..07bed6c31b4 100644
--- a/run-command.h
+++ b/run-command.h
@@ -220,22 +220,6 @@ int finish_command_in_signal(struct child_process *);
  */
 int run_command(struct child_process *);
 
-/**
- * Run a hook.
- * The first argument is a pathname to an index file, or NULL
- * if the hook uses the default index file or no index is needed.
- * The second argument is the name of the hook.
- * The further arguments correspond to the hook arguments.
- * The last argument has to be NULL to terminate the arguments list.
- * If the hook does not exist or is not executable, the return
- * value will be zero.
- * If it is executable, the hook will be executed and the exit
- * status of the hook is returned.
- * On execution, .stdout_to_stderr and .no_stdin will be set.
- */
-LAST_ARG_MUST_BE_NULL
-int run_hook_le(const char *const *env, const char *name, ...);
-
 /*
  * Trigger an auto-gc
  */
-- 
2.34.1.1146.gb52885e7c44
Glen Choo· Jan 6, 2022, 18:47 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v6 00/17] hook.[ch]: new library to run hooks + simple hook conversion

Ævar Arnfjörð Bjarmason         <avarab@gmail.com> writes:
Show 111 quoted lines
> A trivial update to v5 to rebase it past conflicts with topics that
> recently landed on "master". For v5 see:
> https://lore.kernel.org/git/cover-v5-00.17-00000000000-20211123T114206Z-avarab@gmail.com/
>
> Emily Shaffer (14):
>   hook: add 'run' subcommand
>   gc: use hook library for pre-auto-gc hook
>   am: convert {pre,post}-applypatch to use hook.h
>   rebase: convert pre-rebase to use hook.h
>   am: convert applypatch-msg to use hook.h
>   merge: convert post-merge to use hook.h
>   hooks: convert non-worktree 'post-checkout' hook to hook library
>   hooks: convert worktree 'post-checkout' hook to hook library
>   send-email: use 'git hook run' for 'sendemail-validate'
>   git-p4: use 'git hook' to run hooks
>   commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
>   read-cache: convert post-index-change to use hook.h
>   receive-pack: convert push-to-checkout hook to hook.h
>   run-command: remove old run_hook_{le,ve}() hook API
>
> Ævar Arnfjörð Bjarmason (3):
>   hook API: add a run_hooks() wrapper
>   hook API: add a run_hooks_l() wrapper
>   git hook run: add an --ignore-missing flag
>
>  .gitignore                 |   1 +
>  Documentation/git-hook.txt |  45 +++++++++++++
>  Documentation/githooks.txt |   4 ++
>  Makefile                   |   1 +
>  builtin.h                  |   1 +
>  builtin/am.c               |   6 +-
>  builtin/checkout.c         |   3 +-
>  builtin/clone.c            |   3 +-
>  builtin/gc.c               |   3 +-
>  builtin/hook.c             |  84 +++++++++++++++++++++++
>  builtin/merge.c            |   2 +-
>  builtin/rebase.c           |   3 +-
>  builtin/receive-pack.c     |   7 +-
>  builtin/worktree.c         |  26 +++----
>  command-list.txt           |   1 +
>  commit.c                   |  15 +++--
>  git-p4.py                  |  70 ++-----------------
>  git-send-email.perl        |  22 +++---
>  git.c                      |   1 +
>  hook.c                     | 131 ++++++++++++++++++++++++++++++++++++
>  hook.h                     |  57 ++++++++++++++++
>  read-cache.c               |   3 +-
>  reset.c                    |   3 +-
>  run-command.c              |  33 ---------
>  run-command.h              |  17 -----
>  t/t1800-hook.sh            | 134 +++++++++++++++++++++++++++++++++++++
>  t/t9001-send-email.sh      |   4 +-
>  27 files changed, 522 insertions(+), 158 deletions(-)
>  create mode 100644 Documentation/git-hook.txt
>  create mode 100644 builtin/hook.c
>  create mode 100755 t/t1800-hook.sh
>
> Range-diff against v5:
>  1:  4ca52feebb8 =  1:  ba6fd47482e hook: add 'run' subcommand
>  2:  6275b97a306 =  2:  cfba5c139e7 hook API: add a run_hooks() wrapper
>  3:  b5b3051b2e5 =  3:  a4cca074bcb gc: use hook library for pre-auto-gc hook
>  4:  c88eb5d4c25 =  4:  ce57ce1adcb am: convert {pre,post}-applypatch to use hook.h
>  5:  1d8f7b7e4c1 =  5:  d6162fbef80 hook API: add a run_hooks_l() wrapper
>  6:  d49a1444345 =  6:  4c1a8951fc5 rebase: convert pre-rebase to use hook.h
>  7:  191fdad0165 =  7:  d8aa5e8345f am: convert applypatch-msg to use hook.h
>  8:  119b92fbeae =  8:  6f8d3754b4f merge: convert post-merge to use hook.h
>  9:  359ba416e84 !  9:  d3107034806 hooks: convert non-worktree 'post-checkout' hook to hook library
>     @@ builtin/checkout.c
>       #include "ll-merge.h"
>       #include "lockfile.h"
>       #include "merge-recursive.h"
>     -@@ builtin/checkout.c: struct branch_info {
>     +@@ builtin/checkout.c: static void branch_info_release(struct branch_info *info)
>       static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
>       			      int changed)
>       {
> 10:  b7599be95a7 ! 10:  bff7c1513ca hooks: convert worktree 'post-checkout' hook to hook library
>     @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam
>      -		const char *hook = find_hook("post-checkout");
>      -		if (hook) {
>      -			const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
>     --			cp.git_cmd = 0;
>     +-			struct child_process cp = CHILD_PROCESS_INIT;
>      -			cp.no_stdin = 1;
>      -			cp.stdout_to_stderr = 1;
>      -			cp.dir = path;
>     --			cp.env = env;
>     --			cp.argv = NULL;
>     +-			strvec_pushv(&cp.env_array, env);
>      -			cp.trace2_hook_name = "post-checkout";
>      -			strvec_pushl(&cp.args, absolute_path(hook),
>      -				     oid_to_hex(null_oid()),
> 11:  f1c84d7f627 = 11:  7d9c0a73568 git hook run: add an --ignore-missing flag
> 12:  4e0f94d9102 = 12:  8ea3b250dff send-email: use 'git hook run' for 'sendemail-validate'
> 13:  e858f332a62 = 13:  a184afd1ffd git-p4: use 'git hook' to run hooks
> 14:  9a5956cc028 = 14:  1a43e50617f commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
> 15:  6fd47c4c499 = 15:  08b7e63ba5b read-cache: convert post-index-change to use hook.h
> 16:  b201ea46f4b = 16:  c47b36ab41a receive-pack: convert push-to-checkout hook to hook.h
> 17:  281d17b04db ! 17:  7b99a4b633c run-command: remove old run_hook_{le,ve}() hook API
>     @@ run-command.c: int async_with_fork(void)
>      -	strvec_push(&hook.args, p);
>      -	while ((p = va_arg(args, const char *)))
>      -		strvec_push(&hook.args, p);
>     --	hook.env = env;
>     +-	if (env)
>     +-		strvec_pushv(&hook.env_array, (const char **)env);
>      -	hook.no_stdin = 1;
>      -	hook.stdout_to_stderr = 1;
>      -	hook.trace2_hook_name = name;
> -- 
> 2.34.1.1146.gb52885e7c44

The Google folks picked up this series for our "review club" sessions (which is open to the public, as you know ;)). As Junio mentioned in the last "What's Cooking" [1], he would like serious review on the patches you contributed, so that is where we focused most of our attention.

The code itself looks quite good, but one concern that came up was that the motivation behind this series doesn't seem immediately obvious to first time readers. This series has existed in some form for a very long time; the first patches for config-based hooks I could find date back to 2019! [2]. So keeping everyone on the same page is definitely hard, but it's worthwhile if it means that someone can pick up the series without having to be familiar with the prior work.

With that in mind, your original cover letter [3] does a good job at describing the "what" of the series - what it does (extend the hook API and replace calls), what it is (an incremental restart of an ejected topic). However, it's hard to recreate the motivation behind this series based on the cover letter and its papertrail. Much of the Google team is fairly familiar with the feature, but newcomers (including myself) have a hard time piecing together the story based on this thread alone.

I found myself asking some questions when reading this cover letter, and here are some suggestions to make this friendlier for new readers:

* Why do we want config based hooks in the first place?
  Convincing us that we want this is Emily's job, not yours, but it
  helps to know how this fits in with the whole thing. As a convenience,
  linking to Emily's original series would save the reader a lot of
  thread-following.
* How does this series contribute to config based hooks?
* Why did we need this reroll in the first place?
* How does this series meet the goals of the reroll? 
  These are pretty difficult to pin down without being familiar with the
  original series too. Emily probably knows the anwers to these, so you
  have at least one person who can review the series based on these
  questions, but it's pretty challenging for others to figure this out.
  This question is probably best answered in the cover letter, or at the
  very least, links to suggested reading and the order in which to read
  them.
* What benefit does this series bring irrespective of config based
  hooks?
  This is best answered in the cover letter as well, but the payoff is
  way bigger than the previous set of questions. If you can sell
  reviewers on the idea that this series independently mergeable, is
  good for the project on its own (I think that it is!), _and_ it
  contributes to config based hooks, then reviewers have a good reason
  to look at it even if they aren't particularly invested in config
  based hooks. I think this is even more important because we want
  reviews saying "yes this series is good for the project".

TL;DR this is basically a long-winded way of saying that for a long-running work in progress like this one, new reviewers are probably scared off by having to recreate the context leading up to this series, and anything we can do to lessen that burden will be greatly appreciated :)

[1] https://lore.kernel.org/git/xmqq1r1lfwyq.fsf@gitster.g [2] https://lore.kernel.org/git/20191210023335.49987-1-emilyshaffer@google.com/ [3] https://lore.kernel.org/git/cover-00.13-00000000000-20211012T131934Z-avarab@gmail.com/

Emily Shaffer· Jan 7, 2022, 21:48 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [PATCH v6 00/17] hook.[ch]: new library to run hooks + simple hook conversion

On Wed, Dec 22, 2021 at 04:59:26AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> A trivial update to v5 to rebase it past conflicts with topics that
> recently landed on "master". For v5 see:
> https://lore.kernel.org/git/cover-v5-00.17-00000000000-20211123T114206Z-avarab@gmail.com/

One note, which is not so useful for this time, but might be useful in the future:

It seems that in order to understand the purpose of this series and what it's building for, you need to be me or be you (and therefore be aware of the history of the series), or you need to go back through the breadcrumbs and eventually read my huge config-based-hooks 30-patch series (or at least the cover letter). Which sort of defeats the purpose of splitting the series up into smaller reviewable chunks.

Next time it might be nice to add a quick summary of the whole goal, which part of that goal this series is, and a link to the longer cover letter/older series? But again, for this time, I think it is probably too late, especially since this series looked pretty good to me.

Per-patch reviews to follow momentarily.
 - Emily
Show 109 quoted lines
> 
> Emily Shaffer (14):
>   hook: add 'run' subcommand
>   gc: use hook library for pre-auto-gc hook
>   am: convert {pre,post}-applypatch to use hook.h
>   rebase: convert pre-rebase to use hook.h
>   am: convert applypatch-msg to use hook.h
>   merge: convert post-merge to use hook.h
>   hooks: convert non-worktree 'post-checkout' hook to hook library
>   hooks: convert worktree 'post-checkout' hook to hook library
>   send-email: use 'git hook run' for 'sendemail-validate'
>   git-p4: use 'git hook' to run hooks
>   commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
>   read-cache: convert post-index-change to use hook.h
>   receive-pack: convert push-to-checkout hook to hook.h
>   run-command: remove old run_hook_{le,ve}() hook API
> 
> Ævar Arnfjörð Bjarmason (3):
>   hook API: add a run_hooks() wrapper
>   hook API: add a run_hooks_l() wrapper
>   git hook run: add an --ignore-missing flag
> 
>  .gitignore                 |   1 +
>  Documentation/git-hook.txt |  45 +++++++++++++
>  Documentation/githooks.txt |   4 ++
>  Makefile                   |   1 +
>  builtin.h                  |   1 +
>  builtin/am.c               |   6 +-
>  builtin/checkout.c         |   3 +-
>  builtin/clone.c            |   3 +-
>  builtin/gc.c               |   3 +-
>  builtin/hook.c             |  84 +++++++++++++++++++++++
>  builtin/merge.c            |   2 +-
>  builtin/rebase.c           |   3 +-
>  builtin/receive-pack.c     |   7 +-
>  builtin/worktree.c         |  26 +++----
>  command-list.txt           |   1 +
>  commit.c                   |  15 +++--
>  git-p4.py                  |  70 ++-----------------
>  git-send-email.perl        |  22 +++---
>  git.c                      |   1 +
>  hook.c                     | 131 ++++++++++++++++++++++++++++++++++++
>  hook.h                     |  57 ++++++++++++++++
>  read-cache.c               |   3 +-
>  reset.c                    |   3 +-
>  run-command.c              |  33 ---------
>  run-command.h              |  17 -----
>  t/t1800-hook.sh            | 134 +++++++++++++++++++++++++++++++++++++
>  t/t9001-send-email.sh      |   4 +-
>  27 files changed, 522 insertions(+), 158 deletions(-)
>  create mode 100644 Documentation/git-hook.txt
>  create mode 100644 builtin/hook.c
>  create mode 100755 t/t1800-hook.sh
> 
> Range-diff against v5:
>  1:  4ca52feebb8 =  1:  ba6fd47482e hook: add 'run' subcommand
>  2:  6275b97a306 =  2:  cfba5c139e7 hook API: add a run_hooks() wrapper
>  3:  b5b3051b2e5 =  3:  a4cca074bcb gc: use hook library for pre-auto-gc hook
>  4:  c88eb5d4c25 =  4:  ce57ce1adcb am: convert {pre,post}-applypatch to use hook.h
>  5:  1d8f7b7e4c1 =  5:  d6162fbef80 hook API: add a run_hooks_l() wrapper
>  6:  d49a1444345 =  6:  4c1a8951fc5 rebase: convert pre-rebase to use hook.h
>  7:  191fdad0165 =  7:  d8aa5e8345f am: convert applypatch-msg to use hook.h
>  8:  119b92fbeae =  8:  6f8d3754b4f merge: convert post-merge to use hook.h
>  9:  359ba416e84 !  9:  d3107034806 hooks: convert non-worktree 'post-checkout' hook to hook library
>     @@ builtin/checkout.c
>       #include "ll-merge.h"
>       #include "lockfile.h"
>       #include "merge-recursive.h"
>     -@@ builtin/checkout.c: struct branch_info {
>     +@@ builtin/checkout.c: static void branch_info_release(struct branch_info *info)
>       static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
>       			      int changed)
>       {
> 10:  b7599be95a7 ! 10:  bff7c1513ca hooks: convert worktree 'post-checkout' hook to hook library
>     @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam
>      -		const char *hook = find_hook("post-checkout");
>      -		if (hook) {
>      -			const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
>     --			cp.git_cmd = 0;
>     +-			struct child_process cp = CHILD_PROCESS_INIT;
>      -			cp.no_stdin = 1;
>      -			cp.stdout_to_stderr = 1;
>      -			cp.dir = path;
>     --			cp.env = env;
>     --			cp.argv = NULL;
>     +-			strvec_pushv(&cp.env_array, env);
>      -			cp.trace2_hook_name = "post-checkout";
>      -			strvec_pushl(&cp.args, absolute_path(hook),
>      -				     oid_to_hex(null_oid()),
> 11:  f1c84d7f627 = 11:  7d9c0a73568 git hook run: add an --ignore-missing flag
> 12:  4e0f94d9102 = 12:  8ea3b250dff send-email: use 'git hook run' for 'sendemail-validate'
> 13:  e858f332a62 = 13:  a184afd1ffd git-p4: use 'git hook' to run hooks
> 14:  9a5956cc028 = 14:  1a43e50617f commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
> 15:  6fd47c4c499 = 15:  08b7e63ba5b read-cache: convert post-index-change to use hook.h
> 16:  b201ea46f4b = 16:  c47b36ab41a receive-pack: convert push-to-checkout hook to hook.h
> 17:  281d17b04db ! 17:  7b99a4b633c run-command: remove old run_hook_{le,ve}() hook API
>     @@ run-command.c: int async_with_fork(void)
>      -	strvec_push(&hook.args, p);
>      -	while ((p = va_arg(args, const char *)))
>      -		strvec_push(&hook.args, p);
>     --	hook.env = env;
>     +-	if (env)
>     +-		strvec_pushv(&hook.env_array, (const char **)env);
>      -	hook.no_stdin = 1;
>      -	hook.stdout_to_stderr = 1;
>      -	hook.trace2_hook_name = name;
> -- 
> 2.34.1.1146.gb52885e7c44
> 
Emily Shaffer· Jan 7, 2022, 22:01 UTC · re: Emily Shaffer · lore

Re: [PATCH v6 00/17] hook.[ch]: new library to run hooks + simple hook conversion

On Fri, Jan 07, 2022 at 01:48:16PM -0800, Emily Shaffer wrote:
Show 23 quoted lines
> 
> On Wed, Dec 22, 2021 at 04:59:26AM +0100, Ævar Arnfjörð Bjarmason wrote:
> > 
> > A trivial update to v5 to rebase it past conflicts with topics that
> > recently landed on "master". For v5 see:
> > https://lore.kernel.org/git/cover-v5-00.17-00000000000-20211123T114206Z-avarab@gmail.com/
> 
> One note, which is not so useful for this time, but might be useful in
> the future:
> 
> It seems that in order to understand the purpose of this series and what
> it's building for, you need to be me or be you (and therefore be aware
> of the history of the series), or you need to go back through the
> breadcrumbs and eventually read my huge config-based-hooks 30-patch
> series (or at least the cover letter). Which sort of defeats the purpose
> of splitting the series up into smaller reviewable chunks.
> 
> Next time it might be nice to add a quick summary of the whole goal,
> which part of that goal this series is, and a link to the longer cover
> letter/older series? But again, for this time, I think it is probably
> too late, especially since this series looked pretty good to me.
> 
> Per-patch reviews to follow momentarily.

Left a couple nits here and there, but it all looks fine for me. Some comments that I thought I had turned out to be invalid once I read the series a second time... ;)

For all patches, but especially Ævar's,
Reviewed-by: Emily Shaffer <emilyshaffer@google.com>
I think that's stronger than an ACK for mine, but either way, for my
own patches:
Acked-by: Emily Shaffer <emilyshaffer@google.com>
Thanks. I'd be excited to see this go in.
 - Emily

← back to recent threads