git@vger.kernel.org list mirror (unofficial, one of many)
 help / color / mirror / code / Atom feed
* [PATCH v8 00/37] config-based hooks
@ 2021-03-11  2:10 Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 01/37] doc: propose hooks managed by the config Emily Shaffer
                   ` (40 more replies)
  0 siblings, 41 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git
  Cc: Emily Shaffer, Jeff King, Junio C Hamano, James Ramsay,
	Jonathan Nieder, brian m. carlson,
	Ævar Arnfjörð Bjarmason, Phillip Wood,
	Josh Steadmon, Johannes Schindelin, Jonathan Tan

Since v7:
- Addressed Jonathan Tan's review of part I
- Addressed Junio's review of part I and II
- Combined parts I and II

I think the updates to patch 1 between the rest of the work I've been
doing probably have covered Ævar's comments.

More details about per-patch changes found in the notes on each mail (I
hope).

I know that Junio was talking about merging v7 after Josh Steadmon's
review and I asked him not to - this reroll has those changes from
Jonathan Tan's review that I was wanting to wait for.

Thanks!
 - Emily

Emily Shaffer (37):
  doc: propose hooks managed by the config
  hook: scaffolding for git-hook subcommand
  hook: add list command
  hook: include hookdir hook in list
  hook: teach hook.runHookDir
  hook: implement hookcmd.<name>.skip
  parse-options: parse into strvec
  hook: add 'run' subcommand
  hook: introduce hook_exists()
  hook: support passing stdin to hooks
  run-command: allow stdin for run_processes_parallel
  hook: allow parallel hook execution
  hook: allow specifying working directory for hooks
  run-command: add stdin callback for parallelization
  hook: provide stdin by string_list or callback
  run-command: allow capturing of collated output
  hooks: allow callers to capture output
  commit: use config-based hooks
  am: convert applypatch hooks to use config
  merge: use config-based hooks for post-merge hook
  gc: use hook library for pre-auto-gc hook
  rebase: teach pre-rebase to use hook.h
  read-cache: convert post-index-change hook to use config
  receive-pack: convert push-to-checkout hook to hook.h
  git-p4: use 'git hook' to run hooks
  hooks: convert 'post-checkout' hook to hook library
  hook: convert 'post-rewrite' hook to config
  transport: convert pre-push hook to use config
  reference-transaction: look for hooks in config
  receive-pack: convert 'update' hook to hook.h
  proc-receive: acquire hook list from hook.h
  post-update: use hook.h library
  receive-pack: convert receive hooks to hook.h
  bugreport: use hook_exists instead of find_hook
  git-send-email: use 'git hook run' for 'sendemail-validate'
  run-command: stop thinking about hooks
  docs: unify githooks and git-hook manpages

 .gitignore                                    |   1 +
 Documentation/Makefile                        |   1 +
 Documentation/config/hook.txt                 |  27 +
 Documentation/git-hook.txt                    | 161 ++++
 Documentation/githooks.txt                    | 655 +---------------
 Documentation/native-hooks.txt                | 708 ++++++++++++++++++
 Documentation/technical/api-parse-options.txt |   7 +
 .../technical/config-based-hooks.txt          | 369 +++++++++
 Makefile                                      |   2 +
 builtin.h                                     |   1 +
 builtin/am.c                                  |  33 +-
 builtin/bugreport.c                           |   4 +-
 builtin/checkout.c                            |  19 +-
 builtin/clone.c                               |   8 +-
 builtin/commit.c                              |  11 +-
 builtin/fetch.c                               |   1 +
 builtin/gc.c                                  |   5 +-
 builtin/hook.c                                | 176 +++++
 builtin/merge.c                               |  15 +-
 builtin/rebase.c                              |   9 +-
 builtin/receive-pack.c                        | 329 ++++----
 builtin/submodule--helper.c                   |   2 +-
 builtin/worktree.c                            |  31 +-
 command-list.txt                              |   1 +
 commit.c                                      |  22 +-
 commit.h                                      |   3 +-
 git-p4.py                                     |  67 +-
 git-send-email.perl                           |  21 +-
 git.c                                         |   1 +
 hook.c                                        | 480 ++++++++++++
 hook.h                                        | 138 ++++
 parse-options-cb.c                            |  16 +
 parse-options.h                               |   4 +
 read-cache.c                                  |  13 +-
 refs.c                                        |  43 +-
 reset.c                                       |  16 +-
 run-command.c                                 | 156 ++--
 run-command.h                                 |  55 +-
 sequencer.c                                   |  90 +--
 submodule.c                                   |   1 +
 t/helper/test-run-command.c                   |  46 +-
 t/t0061-run-command.sh                        |  37 +
 t/t1360-config-based-hooks.sh                 | 303 ++++++++
 t/t1416-ref-transaction-hooks.sh              |  12 +-
 t/t5411/test-0015-too-many-hooks-error.sh     |  47 ++
 ...3-pre-commit-and-pre-merge-commit-hooks.sh |  17 +-
 t/t9001-send-email.sh                         |  11 +-
 transport.c                                   |  59 +-
 48 files changed, 3052 insertions(+), 1182 deletions(-)
 create mode 100644 Documentation/config/hook.txt
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 Documentation/native-hooks.txt
 create mode 100644 Documentation/technical/config-based-hooks.txt
 create mode 100644 builtin/hook.c
 create mode 100644 hook.c
 create mode 100644 hook.h
 create mode 100755 t/t1360-config-based-hooks.sh
 create mode 100644 t/t5411/test-0015-too-many-hooks-error.sh

-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 01/37] doc: propose hooks managed by the config
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 02/37] hook: scaffolding for git-hook subcommand Emily Shaffer
                   ` (39 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Begin a design document for config-based hooks, managed via git-hook.
Focus on an overview of the implementation and motivation for design
decisions. Briefly discuss the alternatives considered before this
point. Also, attempt to redefine terms to fit into a multihook world.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Since v7, made some wording changes based on reviewer comments
    (mostly Junio's, I think).

 - Emily

 Documentation/Makefile                        |   1 +
 .../technical/config-based-hooks.txt          | 369 ++++++++++++++++++
 2 files changed, 370 insertions(+)
 create mode 100644 Documentation/technical/config-based-hooks.txt

diff --git a/Documentation/Makefile b/Documentation/Makefile
index 81d1bf7a04..2743de8995 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -82,6 +82,7 @@ SP_ARTICLES += $(API_DOCS)
 TECH_DOCS += MyFirstContribution
 TECH_DOCS += MyFirstObjectWalk
 TECH_DOCS += SubmittingPatches
+TECH_DOCS += technical/config-based-hooks
 TECH_DOCS += technical/hash-function-transition
 TECH_DOCS += technical/http-protocol
 TECH_DOCS += technical/index-format
diff --git a/Documentation/technical/config-based-hooks.txt b/Documentation/technical/config-based-hooks.txt
new file mode 100644
index 0000000000..1f973117e4
--- /dev/null
+++ b/Documentation/technical/config-based-hooks.txt
@@ -0,0 +1,369 @@
+Configuration-based hook management
+===================================
+:sectanchors:
+
+[[motivation]]
+== Motivation
+
+Replace the `.git/hook/hookname` path as the only source of hooks to execute;
+allow users to define hooks using config files, in a way which is friendly to
+users with multiple repos which have similar needs - hooks can be easily shared
+between multiple Git repos.
+
+Redefine "hook" as an event rather than a single script, allowing users to
+perform multiple unrelated actions on a single event.
+
+Make it easier for users to discover Git's hook feature and automate their
+workflows.
+
+[[user-interfaces]]
+== User interfaces
+
+[[config-schema]]
+=== Config schema
+
+Hooks can be introduced by editing the configuration manually. There are two new
+sections added, `hook` and `hookcmd`.
+
+[[config-schema-hook]]
+==== `hook`
+
+Primarily contains subsections for each hook event. The order of variables in
+these subsections defines the hook command execution order; hook commands can be
+specified by setting the value directly to the command if no additional
+configuration is needed, or by setting the value as the name of a `hookcmd`. If
+Git does not find a `hookcmd` whose subsection matches the value of the given
+command string, Git will try to execute the string directly. Hooks are executed
+by passing the resolved command string to the shell. In the future, hook event
+subsections could also contain per-hook-event settings; see
+<<per-hook-event-settings,the section in Future Work>> for more details.
+
+Also contains top-level hook execution settings, for example, `hook.runHookDir`.
+(These settings are described more in <<library,Library>>.)
+
+----
+[hook "pre-commit"]
+  command = perl-linter
+  command = /usr/bin/git-secrets --pre-commit
+
+[hook "pre-applypatch"]
+  command = perl-linter
+  # for illustration purposes; error behavior isn't planned yet
+  error = ignore
+
+[hook]
+  runHookDir = interactive
+----
+
+[[config-schema-hookcmd]]
+==== `hookcmd`
+
+Defines a hook command and its attributes, which will be used when a hook event
+occurs. Unqualified attributes are assumed to apply to this hook during all hook
+events, but event-specific attributes can also be supplied. The example runs
+`/usr/bin/lint-it --language=perl <args passed by Git>`, but for repos which
+include this config, the hook command will be skipped for all events.
+Theoretically, the last line could be used to "un-skip" the hook command for
+`pre-commit` hooks, but this hasn't been scoped or implemented yet.
+
+----
+[hookcmd "perl-linter"]
+  command = /usr/bin/lint-it --language=perl
+  skip = true
+  # for illustration purposes; below hasn't been defined yet
+  pre-commit-skip = false
+----
+
+[[command-line-api]]
+=== Command-line API
+
+Users should be able to view, run, reorder, and create hook commands via the
+command line. External tools should be able to view a list of hooks in the
+correct order to run. Modifier commands (`edit` and `add`) have not been
+implemented yet and may not be if manually editing the config proves usable
+enough.
+
+*`git hook list <hook-event>`*
+
+*`git hook run <hook-event> [-a <arg>]... [-e <env-var>]...`*
+
+*`git hook edit <hook-event>`*
+
+*`git hook add <hook-command> <hook-event> <options...>`*
+
+[[hook-editor]]
+=== Hook editor
+
+The tool which is presented by `git hook edit <hook-command>`. Ideally, this
+tool should be easier to use than manually editing the config, and then produce
+a concise config afterwards. It may take a form similar to `git rebase
+--interactive`. This has not been designed or implemented yet and may not be if
+the config proves usable enough.
+
+[[implementation]]
+== Implementation
+
+[[library]]
+=== Library
+
+`hook.c` and `hook.h` are responsible for interacting with the config files. The
+hook library provides a basic API to call all hooks in config order with more
+complex options passed via `struct run_hooks_opt`:
+
+*`int run_hooks(const char *hookname, struct run_hooks_opt *options)`*
+
+`struct run_hooks_opt` allows callers to set:
+
+- environment variables
+- command-line arguments
+- behavior for the hook command provided by `run-command.h:find_hook()` (see
+  below)
+- a method to provide stdin to each hook, either via a file containing stdin, a
+  `struct string_list` containing a list of lines to print, or a callback
+  function to allow the caller to populate stdin manually
+- a method to process stdout from each hook, e.g. for printing to sideband
+  during a network operation
+- parallelism
+- a custom working directory for hooks to execute in
+
+And this struct can be extended with more options as necessary in the future.
+
+The "legacy" hook provided by `run-command.h:find_hook()` - that is, the hook
+present in `.git/hooks/<hookname>` or
+`$(git config --get core.hooksPath)/<hookname>` - can be handled in a number of
+ways, providing an avenue to deprecate these "legacy" hooks if desired. The
+handling is based on a config `hook.runHookDir`, which is checked against a
+number of cases:
+
+- "no": the legacy hook will not be run
+- "error": Git will print a warning to stderr before ignoring the legacy hook
+- "interactive": Git will prompt the user before running the legacy hook
+- "warn": Git will print a warning to stderr before running the legacy hook
+- "yes" (default): Git will silently run the legacy hook
+
+In case this list is expanded in the future, if a value for `hook.runHookDir` is
+given which Git does not recognize, Git should discard that config entry. For
+example, if "warn" was specified at system level and "junk" was specified at
+global level, Git would resolve the value to "warn"; if the only time the config
+was set was to "junk", Git would use the default value of "yes" (but print a
+warning to the user first to let them know their value is wrong).
+
+`struct hookcmd` is expected to grow in size over time as more functionality is
+added to hooks; so that other parts of the code don't need to understand the
+config schema, `struct hookcmd` should contain logical values instead of string
+pairs.
+
+By default, hook parallelism is chosen based on the semantics of each hook;
+callsites initialize their `struct run_hooks_opt` via one of two macros,
+`RUN_HOOKS_OPT_INIT_SYNC` or `RUN_HOOKS_OPT_INIT_ASYNC`. The default number of
+jobs can be configured in `hook.jobs`; this config applies across all hook
+events. If unset, the value of `online_cpus()` (equivalent to `nproc`) is used.
+
+[[builtin]]
+=== Builtin
+
+`builtin/hook.c` is responsible for providing the frontend. It's responsible for
+formatting user-provided data and then calling the library API to set the
+configs as appropriate. The builtin frontend is not responsible for calling the
+config directly, so that other areas of Git can rely on the hook library to
+understand the most recent config schema for hooks.
+
+[[migration]]
+=== Migration path
+
+[[stage-0]]
+==== Stage 0
+
+Hooks are called by running `run-command.h:find_hook()` with the hookname and
+executing the result. The hook library and builtin do not exist. Hooks only
+exist as specially named scripts within `.git/hooks/`.
+
+[[stage-1]]
+==== Stage 1
+
+`git hook list --porcelain <hook-event>` is implemented. `hook.h:run_hooks()` is
+taught to include `run-command.h:find_hook()` at the end; calls to `find_hook()`
+are replaced with calls to `run_hooks()`. Users can opt-in to config-based hooks
+simply by creating some in their config; otherwise users should remain
+unaffected by the change.
+
+[[stage-2]]
+==== Stage 2
+
+The call to `find_hook()` inside of `run_hooks()` learns to check for a config,
+`hook.runHookDir`. Users can opt into managing their hooks completely via the
+config this way.
+
+[[stage-3]]
+==== Stage 3
+
+`.git/hooks` is removed from the template and the hook directory is considered
+deprecated. To avoid breaking older repos, the default of `hook.runHookDir` is
+not changed, and `find_hook()` is not removed.
+
+[[caveats]]
+== Caveats
+
+[[security]]
+=== Security and repo config
+
+Part of the motivation behind this refactor is to mitigate hooks as an attack
+vector.footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/]
+However, as the design stands, users can still provide hooks in the repo-level
+config, which is included when a repo is zipped and sent elsewhere. The
+security of the repo-level config is still under discussion; this design
+generally assumes the repo-level config is secure, which is not true yet. This
+assumption was made to avoid overcomplicating the design. So, this series
+doesn't particularly improve security or resistance to zip attacks.
+
+[[ease-of-use]]
+=== Ease of use
+
+The config schema is nontrivial; that's why it's important for the `git hook`
+modifier commands to be usable. Contributors with UX expertise are encouraged to
+share their suggestions.
+
+[[alternatives]]
+== Alternative approaches
+
+A previous summary of alternatives exists in the
+archives.footnote:[https://lore.kernel.org/git/20191116011125.GG22855@google.com]
+
+The table below shows a number of goals and how they might be achieved with
+config-based hooks, by implementing directory support (i.e.
+'.git/hooks/pre-commit.d'), or as hooks are run today.
+
+.Comparison of alternatives
+|===
+|Feature |Config-based hooks |Hook directories |Status quo
+
+|Supports multiple hooks
+|Natively
+|Natively
+|With user effort
+
+|Supports parallelization
+|Natively
+|Natively
+|No (user's multihook trampoline script would need to handle parallelism)
+
+|Safer for zipped repos
+|A little
+|No
+|No
+
+|Previous hooks just work
+|If configured
+|Yes
+|Yes
+
+|Can install one hook to many repos
+|Yes
+|With symlinks or core.hooksPath
+|With symlinks or core.hooksPath
+
+|Discoverability
+|Findable with 'git help git' or tab-completion via 'git hook' subcommand
+|Findable via improved documentation
+|Same as before
+
+|Hard to run unexpected hook
+|If configured
+|Could be made to warn or look for a config
+|No
+|===
+
+[[status-quo]]
+=== Status quo
+
+Today users can implement multihooks themselves by using a "trampoline script"
+as their hook, and pointing that script to a directory or list of other scripts
+they wish to run.
+
+[[hook-directories]]
+=== Hook directories
+
+Other contributors have suggested Git learn about the existence of a directory
+such as `.git/hooks/<hookname>.d` and execute those hooks in alphabetical order.
+
+[[future-work]]
+== Future work
+
+[[execution-ordering]]
+=== Execution ordering
+
+We may find that config order is insufficient for some users; for example,
+config order makes it difficult to add a new hook to the system or global config
+which runs at the end of the hook list. A new ordering schema should be:
+
+1) Specified by a `hook.order` config, so that users will not unexpectedly see
+their order change;
+
+2) Either dependency or numerically based.
+
+Dependency-based ordering is prone to classic linked-list problems, like a
+cycles and handling of missing dependencies. But, it paves the way for enabling
+parallelization if some tasks truly depend on others.
+
+Numerical ordering makes it tricky for Git to generate suggested ordering
+numbers for each command, but is easy to determine a definitive order.
+
+[[parallelization]]
+=== Parallelization with dependencies
+
+Currently hooks use a naive parallelization scheme or are run in series.  But if
+one hook depends on another's output, then users will want to specify those
+dependencies. If we decide to solve this problem, we may want to look to modern
+build systems for inspiration on how to manage dependencies and parallel tasks.
+
+[[nontrivial-hooks]]
+=== Multihooks and nontrivial output
+
+Some hooks - like 'proc-receive' - don't lend themselves well to multihooks at
+all. In the case of 'proc-receive', for now, multiple hook definitions are
+disallowed. In the future we might be able to conceive a better approach, for
+example, running the hooks in series and using the output from one hook as the
+input to the next.
+
+[[securing-hookdir-hooks]]
+=== Securing hookdir hooks
+
+With the design as written in this doc, it's still possible for a malicious user
+to modify `.git/config` to include `hook.pre-receive.command = rm -rf /`, then
+zip their repo and send it to another user. It may be necessary to teach Git to
+only allow inlined hooks like this if they were configured outside of the local
+scope (in other words, only run hookcmds, and only allow hookcmds to be
+configured in global or system scope); or another approach, like a list of safe
+projects, might be useful. It may also be sufficient (or at least useful) to
+teach a `hook.disableAll` config or similar flag to the Git executable.
+
+[[submodule-inheritance]]
+=== Submodule inheritance
+
+It's possible some submodules may want to run the identical set of hooks that
+their superrepo runs. While a globally-configured hook set is helpful, it's not
+a great solution for users who have multiple repos-with-submodules under the
+same user. It would be useful for submodules to learn how to run hooks from
+their superrepo's config, or inherit that hook setting.
+
+[[per-hook-event-settings]]
+=== Per-hook-event settings
+
+It might be desirable to keep settings specifically for some hook events, but
+not for others - for example, a user may wish to disable hookdir hooks for all
+events but pre-commit, which they haven't had time to convert yet; or, a user
+may wish for execution order settings to differ based on hook event. In that
+case, it would be useful to set something like `hook.pre-commit.executionOrder`
+which would not apply to the 'prepare-commit-msg' hook, for example.
+
+[[glossary]]
+== Glossary
+
+*hook event*
+
+A point during Git's execution where user scripts may be run, for example,
+_prepare-commit-msg_ or _pre-push_.
+
+*hook command*
+
+A user script or executable which will be run on one or more hook events.
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 02/37] hook: scaffolding for git-hook subcommand
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 01/37] doc: propose hooks managed by the config Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 03/37] hook: add list command Emily Shaffer
                   ` (38 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Introduce infrastructure for a new subcommand, git-hook, which will be
used to ease config-based hook management. This command will handle
parsing configs to compose a list of hooks to run for a given event, as
well as adding or modifying hook configs in an interactive fashion.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
No change since v7.

 .gitignore                    |  1 +
 Documentation/git-hook.txt    | 20 ++++++++++++++++++++
 Makefile                      |  1 +
 builtin.h                     |  1 +
 builtin/hook.c                | 21 +++++++++++++++++++++
 command-list.txt              |  1 +
 git.c                         |  1 +
 t/t1360-config-based-hooks.sh | 11 +++++++++++
 8 files changed, 57 insertions(+)
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100755 t/t1360-config-based-hooks.sh

diff --git a/.gitignore b/.gitignore
index 3dcdb6bb5a..3608c35b73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,6 +76,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 0000000000..9eeab0009d
--- /dev/null
+++ b/Documentation/git-hook.txt
@@ -0,0 +1,20 @@
+git-hook(1)
+===========
+
+NAME
+----
+git-hook - Manage configured hooks
+
+SYNOPSIS
+--------
+[verse]
+'git hook'
+
+DESCRIPTION
+-----------
+A placeholder command. Later, you will be able to list, add, and modify hooks
+with this command.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index dfb0f1000f..8e904a1ab5 100644
--- a/Makefile
+++ b/Makefile
@@ -1087,6 +1087,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 b6ce981b73..8df1d36a7a 100644
--- a/builtin.h
+++ b/builtin.h
@@ -163,6 +163,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 0000000000..b2bbc84d4d
--- /dev/null
+++ b/builtin/hook.c
@@ -0,0 +1,21 @@
+#include "cache.h"
+
+#include "builtin.h"
+#include "parse-options.h"
+
+static const char * const builtin_hook_usage[] = {
+	N_("git hook"),
+	NULL
+};
+
+int cmd_hook(int argc, const char **argv, const char *prefix)
+{
+	struct option builtin_hook_options[] = {
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, builtin_hook_options,
+			     builtin_hook_usage, 0);
+
+	return 0;
+}
diff --git a/command-list.txt b/command-list.txt
index a289f09ed6..9ccd8e5aeb 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 9bc077a025..14adac716f 100644
--- a/git.c
+++ b/git.c
@@ -528,6 +528,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_GENTLY },
 	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
 	{ "init", cmd_init_db },
 	{ "init-db", cmd_init_db },
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
new file mode 100755
index 0000000000..34b0df5216
--- /dev/null
+++ b/t/t1360-config-based-hooks.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+test_description='config-managed multihooks, including git-hook command'
+
+. ./test-lib.sh
+
+test_expect_success 'git hook command does not crash' '
+	git hook
+'
+
+test_done
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 03/37] hook: add list command
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 01/37] doc: propose hooks managed by the config Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 02/37] hook: scaffolding for git-hook subcommand Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  8:20   ` Ævar Arnfjörð Bjarmason
  2021-03-11  2:10 ` [PATCH v8 04/37] hook: include hookdir hook in list Emily Shaffer
                   ` (37 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Teach 'git hook list <hookname>', which checks the known configs in
order to create an ordered list of hooks to run on a given hook event.

Multiple commands can be specified for a given hook by providing
multiple "hook.<hookname>.command = <path-to-hook>" lines. Hooks will be
run in config order. If more properties need to be set on a given hook
in the future, commands can also be specified by providing
"hook.<hookname>.command = <hookcmd-name>", as well as a "[hookcmd
<hookcmd-name>]" subsection; this subsection should contain a
"hookcmd.<hookcmd-name>.command = <path-to-hook>" line.

For example:

  $ git config --list | grep ^hook
  hook.pre-commit.command=baz
  hook.pre-commit.command=~/bar.sh
  hookcmd.baz.command=~/baz/from/hookcmd.sh

  $ git hook list pre-commit
  global: ~/baz/from/hookcmd.sh
  local: ~/bar.sh

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Since v7, fixed some nits from Jonathan Tan, one of which revealed a bug in
    how I was adding hooks to the list.

 Documentation/config/hook.txt |   9 +++
 Documentation/git-hook.txt    |  59 ++++++++++++++++-
 Makefile                      |   1 +
 builtin/hook.c                |  56 ++++++++++++++--
 hook.c                        | 120 ++++++++++++++++++++++++++++++++++
 hook.h                        |  25 +++++++
 t/t1360-config-based-hooks.sh |  81 ++++++++++++++++++++++-
 7 files changed, 341 insertions(+), 10 deletions(-)
 create mode 100644 Documentation/config/hook.txt
 create mode 100644 hook.c
 create mode 100644 hook.h

diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
new file mode 100644
index 0000000000..71449ecbc7
--- /dev/null
+++ b/Documentation/config/hook.txt
@@ -0,0 +1,9 @@
+hook.<command>.command::
+	A command to execute during the <command> hook event. This can be an
+	executable on your device, a oneliner for your shell, or the name of a
+	hookcmd. See linkgit:git-hook[1].
+
+hookcmd.<name>.command::
+	A command to execute during a hook for which <name> has been specified
+	as a command. This can be an executable on your device or a oneliner for
+	your shell. See linkgit:git-hook[1].
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index 9eeab0009d..f19875ed68 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -8,12 +8,65 @@ git-hook - Manage configured hooks
 SYNOPSIS
 --------
 [verse]
-'git hook'
+'git hook' list <hook-name>
 
 DESCRIPTION
 -----------
-A placeholder command. Later, you will be able to list, add, and modify hooks
-with this command.
+You can list configured hooks with this command. Later, you will be able to run,
+add, and modify hooks with this command.
+
+This command parses the default configuration files for sections `hook` and
+`hookcmd`. `hook` is used to describe the commands which will be run during a
+particular hook event; commands are run in the order Git encounters them during
+the configuration parse (see linkgit:git-config[1]). `hookcmd` is used to
+describe attributes of a specific command. If additional attributes don't need
+to be specified, a command to run can be specified directly in the `hook`
+section; if a `hookcmd` by that name isn't found, Git will attempt to run the
+provided value directly. For example:
+
+Global config
+----
+  [hook "post-commit"]
+    command = "linter"
+    command = "~/typocheck.sh"
+
+  [hookcmd "linter"]
+    command = "/bin/linter --c"
+----
+
+Local config
+----
+  [hook "prepare-commit-msg"]
+    command = "linter"
+  [hook "post-commit"]
+    command = "python ~/run-test-suite.py"
+----
+
+With these configs, you'd then see:
+
+----
+$ git hook list "post-commit"
+global: /bin/linter --c
+global: ~/typocheck.sh
+local: python ~/run-test-suite.py
+
+$ git hook list "prepare-commit-msg"
+local: /bin/linter --c
+----
+
+COMMANDS
+--------
+
+list `<hook-name>`::
+
+List the hooks which have been configured for `<hook-name>`. Hooks appear
+in the order they should be run, and print the config scope where the relevant
+`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).
+This output is human-readable and the format is subject to change over time.
+
+CONFIGURATION
+-------------
+include::config/hook.txt[]
 
 GIT
 ---
diff --git a/Makefile b/Makefile
index 8e904a1ab5..3fa51597d8 100644
--- a/Makefile
+++ b/Makefile
@@ -891,6 +891,7 @@ LIB_OBJS += hash-lookup.o
 LIB_OBJS += hashmap.o
 LIB_OBJS += help.o
 LIB_OBJS += hex.o
+LIB_OBJS += hook.o
 LIB_OBJS += ident.o
 LIB_OBJS += json-writer.o
 LIB_OBJS += kwset.o
diff --git a/builtin/hook.c b/builtin/hook.c
index b2bbc84d4d..bb64cd77ca 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -1,21 +1,67 @@
 #include "cache.h"
-
 #include "builtin.h"
+#include "config.h"
+#include "hook.h"
 #include "parse-options.h"
+#include "strbuf.h"
 
 static const char * const builtin_hook_usage[] = {
-	N_("git hook"),
+	N_("git hook list <hookname>"),
 	NULL
 };
 
-int cmd_hook(int argc, const char **argv, const char *prefix)
+static int list(int argc, const char **argv, const char *prefix)
 {
-	struct option builtin_hook_options[] = {
+	struct list_head *head, *pos;
+	struct strbuf hookname = STRBUF_INIT;
+
+	struct option list_options[] = {
 		OPT_END(),
 	};
 
-	argc = parse_options(argc, argv, prefix, builtin_hook_options,
+	argc = parse_options(argc, argv, prefix, list_options,
 			     builtin_hook_usage, 0);
 
+	if (argc < 1) {
+		usage_msg_opt(_("You must specify a hook event name to list."),
+			      builtin_hook_usage, list_options);
+	}
+
+	strbuf_addstr(&hookname, argv[0]);
+
+	head = hook_list(&hookname);
+
+	if (list_empty(head)) {
+		printf(_("no commands configured for hook '%s'\n"),
+		       hookname.buf);
+		strbuf_release(&hookname);
+		return 0;
+	}
+
+	list_for_each(pos, head) {
+		struct hook *item = list_entry(pos, struct hook, list);
+		if (item)
+			printf("%s: %s\n",
+			       config_scope_name(item->origin),
+			       item->command.buf);
+	}
+
+	clear_hook_list(head);
+	strbuf_release(&hookname);
+
 	return 0;
 }
+
+int cmd_hook(int argc, const char **argv, const char *prefix)
+{
+	struct option builtin_hook_options[] = {
+		OPT_END(),
+	};
+	if (argc < 2)
+		usage_with_options(builtin_hook_usage, builtin_hook_options);
+
+	if (!strcmp(argv[1], "list"))
+		return list(argc - 1, argv + 1, prefix);
+
+	usage_with_options(builtin_hook_usage, builtin_hook_options);
+}
diff --git a/hook.c b/hook.c
new file mode 100644
index 0000000000..fede40e925
--- /dev/null
+++ b/hook.c
@@ -0,0 +1,120 @@
+#include "cache.h"
+
+#include "hook.h"
+#include "config.h"
+
+void free_hook(struct hook *ptr)
+{
+	if (ptr) {
+		strbuf_release(&ptr->command);
+		free(ptr);
+	}
+}
+
+static void append_or_move_hook(struct list_head *head, const char *command)
+{
+	struct list_head *pos = NULL, *tmp = NULL;
+	struct hook *to_add = NULL;
+
+	/*
+	 * remove the prior entry with this command; we'll replace it at the
+	 * end.
+	 */
+	list_for_each_safe(pos, tmp, head) {
+		struct hook *it = list_entry(pos, struct hook, list);
+		if (!strcmp(it->command.buf, command)) {
+		    list_del(pos);
+		    /* we'll simply move the hook to the end */
+		    to_add = it;
+		    break;
+		}
+	}
+
+	if (!to_add) {
+		/* adding a new hook, not moving an old one */
+		to_add = xmalloc(sizeof(*to_add));
+		strbuf_init(&to_add->command, 0);
+		strbuf_addstr(&to_add->command, command);
+	}
+
+	/* re-set the scope so we show where an override was specified */
+	to_add->origin = current_config_scope();
+
+	list_add_tail(&to_add->list, head);
+}
+
+static void remove_hook(struct list_head *to_remove)
+{
+	struct hook *hook_to_remove = list_entry(to_remove, struct hook, list);
+	list_del(to_remove);
+	free_hook(hook_to_remove);
+}
+
+void clear_hook_list(struct list_head *head)
+{
+	struct list_head *pos, *tmp;
+	list_for_each_safe(pos, tmp, head)
+		remove_hook(pos);
+}
+
+struct hook_config_cb
+{
+	struct strbuf *hookname;
+	struct list_head *list;
+};
+
+static int hook_config_lookup(const char *key, const char *value, void *cb_data)
+{
+	struct hook_config_cb *data = cb_data;
+	const char *hook_key = data->hookname->buf;
+	struct list_head *head = data->list;
+
+	if (!strcmp(key, hook_key)) {
+		const char *command = value;
+		struct strbuf hookcmd_name = STRBUF_INIT;
+
+		/*
+		 * Check if a hookcmd with that name exists. If it doesn't,
+		 * 'git_config_get_value()' is documented not to touch &command,
+		 * so we don't need to do anything.
+		 */
+		strbuf_addf(&hookcmd_name, "hookcmd.%s.command", command);
+		git_config_get_value(hookcmd_name.buf, &command);
+
+		if (!command) {
+			strbuf_release(&hookcmd_name);
+			BUG("git_config_get_value overwrote a string it shouldn't have");
+		}
+
+		/*
+		 * TODO: implement an option-getting callback, e.g.
+		 *   get configs by pattern hookcmd.$value.*
+		 *   for each key+value, do_callback(key, value, cb_data)
+		 */
+
+		append_or_move_hook(head, command);
+
+		strbuf_release(&hookcmd_name);
+	}
+
+	return 0;
+}
+
+struct list_head* hook_list(const struct strbuf* hookname)
+{
+	struct strbuf hook_key = STRBUF_INIT;
+	struct list_head *hook_head = xmalloc(sizeof(struct list_head));
+	struct hook_config_cb cb_data = { &hook_key, hook_head };
+
+	INIT_LIST_HEAD(hook_head);
+
+	if (!hookname)
+		return NULL;
+
+	strbuf_addf(&hook_key, "hook.%s.command", hookname->buf);
+
+	git_config(hook_config_lookup, &cb_data);
+
+	strbuf_release(&hook_key);
+	return hook_head;
+}
diff --git a/hook.h b/hook.h
new file mode 100644
index 0000000000..e48dfc6d27
--- /dev/null
+++ b/hook.h
@@ -0,0 +1,25 @@
+#include "config.h"
+#include "list.h"
+#include "strbuf.h"
+
+struct hook {
+	struct list_head list;
+	/*
+	 * Config file which holds the hook.*.command definition.
+	 * (This has nothing to do with the hookcmd.<name>.* configs.)
+	 */
+	enum config_scope origin;
+	/* The literal command to run. */
+	struct strbuf command;
+};
+
+/*
+ * Provides a linked list of 'struct hook' detailing commands which should run
+ * in response to the 'hookname' event, in execution order.
+ */
+struct list_head* hook_list(const struct strbuf *hookname);
+
+/* Free memory associated with a 'struct hook' */
+void free_hook(struct hook *ptr);
+/* Empties the list at 'head', calling 'free_hook()' on each entry */
+void clear_hook_list(struct list_head *head);
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
index 34b0df5216..6e4a3e763f 100755
--- a/t/t1360-config-based-hooks.sh
+++ b/t/t1360-config-based-hooks.sh
@@ -4,8 +4,85 @@ test_description='config-managed multihooks, including git-hook command'
 
 . ./test-lib.sh
 
-test_expect_success 'git hook command does not crash' '
-	git hook
+ROOT=
+if test_have_prereq MINGW
+then
+	# In Git for Windows, Unix-like paths work only in shell scripts;
+	# `git.exe`, however, will prefix them with the pseudo root directory
+	# (of the Unix shell). Let's accommodate for that.
+	ROOT="$(cd / && pwd)"
+fi
+
+setup_hooks () {
+	test_config hook.pre-commit.command "/path/ghi" --add
+	test_config_global hook.pre-commit.command "/path/def" --add
+}
+
+setup_hookcmd () {
+	test_config hook.pre-commit.command "abc" --add
+	test_config_global hookcmd.abc.command "/path/abc" --add
+}
+
+test_expect_success 'git hook rejects commands without a mode' '
+	test_must_fail git hook pre-commit
+'
+
+
+test_expect_success 'git hook rejects commands without a hookname' '
+	test_must_fail git hook list
+'
+
+test_expect_success 'git hook runs outside of a repo' '
+	setup_hooks &&
+
+	cat >expected <<-EOF &&
+	global: $ROOT/path/def
+	EOF
+
+	nongit git config --list --global &&
+
+	nongit git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git hook list orders by config order' '
+	setup_hooks &&
+
+	cat >expected <<-EOF &&
+	global: $ROOT/path/def
+	local: $ROOT/path/ghi
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git hook list dereferences a hookcmd' '
+	setup_hooks &&
+	setup_hookcmd &&
+
+	cat >expected <<-EOF &&
+	global: $ROOT/path/def
+	local: $ROOT/path/ghi
+	local: $ROOT/path/abc
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git hook list reorders on duplicate commands' '
+	setup_hooks &&
+
+	test_config hook.pre-commit.command "/path/def" --add &&
+
+	cat >expected <<-EOF &&
+	local: $ROOT/path/ghi
+	local: $ROOT/path/def
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
 '
 
 test_done
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 04/37] hook: include hookdir hook in list
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (2 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 03/37] hook: add list command Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  8:30   ` Ævar Arnfjörð Bjarmason
  2021-03-11  2:10 ` [PATCH v8 05/37] hook: teach hook.runHookDir Emily Shaffer
                   ` (36 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Historically, hooks are declared by placing an executable into
$GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken
from the config are more featureful than hooks placed in the $HOOKDIR,
those hooks should not stop working for users who already have them.
Let's list them to the user, but instead of displaying a config scope
(e.g. "global: blah") we can prefix them with "hookdir:".

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Since v7, fix some nits from Jonathan Tan. The largest is to move reference to
    "hookdir annotation" from this commit to the next one which introduces the
    hook.runHookDir option.

 builtin/hook.c                | 11 +++++++++--
 hook.c                        | 17 +++++++++++++++++
 hook.h                        |  1 +
 t/t1360-config-based-hooks.sh | 19 +++++++++++++++++++
 4 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/builtin/hook.c b/builtin/hook.c
index bb64cd77ca..c8fbfbb39d 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -40,10 +40,15 @@ static int list(int argc, const char **argv, const char *prefix)
 
 	list_for_each(pos, head) {
 		struct hook *item = list_entry(pos, struct hook, list);
-		if (item)
+		item = list_entry(pos, struct hook, list);
+		if (item) {
+			/* Don't translate 'hookdir' - it matches the config */
 			printf("%s: %s\n",
-			       config_scope_name(item->origin),
+			       (item->from_hookdir
+				? "hookdir"
+				: config_scope_name(item->origin)),
 			       item->command.buf);
+		}
 	}
 
 	clear_hook_list(head);
@@ -60,6 +65,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)
 	if (argc < 2)
 		usage_with_options(builtin_hook_usage, builtin_hook_options);
 
+	git_config(git_default_config, NULL);
+
 	if (!strcmp(argv[1], "list"))
 		return list(argc - 1, argv + 1, prefix);
 
diff --git a/hook.c b/hook.c
index fede40e925..080e25696b 100644
--- a/hook.c
+++ b/hook.c
@@ -2,6 +2,7 @@
 
 #include "hook.h"
 #include "config.h"
+#include "run-command.h"
 
 void free_hook(struct hook *ptr)
 {
@@ -35,6 +36,7 @@ static void append_or_move_hook(struct list_head *head, const char *command)
 		to_add = xmalloc(sizeof(*to_add));
 		strbuf_init(&to_add->command, 0);
 		strbuf_addstr(&to_add->command, command);
+		to_add->from_hookdir = 0;
 	}
 
 	/* re-set the scope so we show where an override was specified */
@@ -115,6 +117,21 @@ struct list_head* hook_list(const struct strbuf* hookname)
 
 	git_config(hook_config_lookup, &cb_data);
 
+	if (have_git_dir()) {
+		const char *legacy_hook_path = find_hook(hookname->buf);
+
+		/* Unconditionally add legacy hook, but annotate it. */
+		if (legacy_hook_path) {
+			struct hook *legacy_hook;
+
+			append_or_move_hook(hook_head,
+					    absolute_path(legacy_hook_path));
+			legacy_hook = list_entry(hook_head->prev, struct hook,
+						 list);
+			legacy_hook->from_hookdir = 1;
+		}
+	}
+
 	strbuf_release(&hook_key);
 	return hook_head;
 }
diff --git a/hook.h b/hook.h
index e48dfc6d27..a97d43670d 100644
--- a/hook.h
+++ b/hook.h
@@ -11,6 +11,7 @@ struct hook {
 	enum config_scope origin;
 	/* The literal command to run. */
 	struct strbuf command;
+	unsigned from_hookdir : 1;
 };
 
 /*
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
index 6e4a3e763f..0f12af4659 100755
--- a/t/t1360-config-based-hooks.sh
+++ b/t/t1360-config-based-hooks.sh
@@ -23,6 +23,14 @@ setup_hookcmd () {
 	test_config_global hookcmd.abc.command "/path/abc" --add
 }
 
+setup_hookdir () {
+	mkdir .git/hooks
+	write_script .git/hooks/pre-commit <<-EOF
+	echo \"Legacy Hook\"
+	EOF
+	test_when_finished rm -rf .git/hooks
+}
+
 test_expect_success 'git hook rejects commands without a mode' '
 	test_must_fail git hook pre-commit
 '
@@ -85,4 +93,15 @@ test_expect_success 'git hook list reorders on duplicate commands' '
 	test_cmp expected actual
 '
 
+test_expect_success 'git hook list shows hooks from the hookdir' '
+	setup_hookdir &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
 test_done
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 05/37] hook: teach hook.runHookDir
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (3 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 04/37] hook: include hookdir hook in list Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  8:33   ` Ævar Arnfjörð Bjarmason
  2021-03-11  2:10 ` [PATCH v8 06/37] hook: implement hookcmd.<name>.skip Emily Shaffer
                   ` (35 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

For now, just give a hint about how these hooks will be run in 'git hook
list'. Later on, though, we will pay attention to this enum when running
the hooks.
---

Notes:
    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to
    enforce it - now it matches the design doc much better.
    
    Also, thanks Jonathan Tan for pointing out that the commit message made no sense
    and was targeted for a different change. Rewrote the commit message now.
    
    Plus, added HOOK_ERROR flag per Junio and Jonathan Nieder.

 - Emily

 Documentation/config/hook.txt |  5 +++
 builtin/hook.c                | 69 +++++++++++++++++++++++++++++++---
 hook.c                        | 24 ++++++++++++
 hook.h                        | 16 ++++++++
 t/t1360-config-based-hooks.sh | 71 +++++++++++++++++++++++++++++++++++
 5 files changed, 180 insertions(+), 5 deletions(-)

diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
index 71449ecbc7..75312754ae 100644
--- a/Documentation/config/hook.txt
+++ b/Documentation/config/hook.txt
@@ -7,3 +7,8 @@ hookcmd.<name>.command::
 	A command to execute during a hook for which <name> has been specified
 	as a command. This can be an executable on your device or a oneliner for
 	your shell. See linkgit:git-hook[1].
+
+hook.runHookDir::
+	Controls how hooks contained in your hookdir are executed. Can be any of
+	"yes", "warn", "interactive", or "no". Defaults to "yes". See
+	linkgit:git-hook[1] and linkgit:git-config[1] "core.hooksPath").
diff --git a/builtin/hook.c b/builtin/hook.c
index c8fbfbb39d..310f696ebf 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -10,10 +10,13 @@ static const char * const builtin_hook_usage[] = {
 	NULL
 };
 
+static enum hookdir_opt should_run_hookdir;
+
 static int list(int argc, const char **argv, const char *prefix)
 {
 	struct list_head *head, *pos;
 	struct strbuf hookname = STRBUF_INIT;
+	struct strbuf hookdir_annotation = STRBUF_INIT;
 
 	struct option list_options[] = {
 		OPT_END(),
@@ -38,20 +41,48 @@ static int list(int argc, const char **argv, const char *prefix)
 		return 0;
 	}
 
+	switch (should_run_hookdir) {
+		case HOOKDIR_NO:
+			strbuf_addstr(&hookdir_annotation, _(" (will not run)"));
+			break;
+		case HOOKDIR_ERROR:
+			strbuf_addstr(&hookdir_annotation, _(" (will error and not run)"));
+			break;
+		case HOOKDIR_INTERACTIVE:
+			strbuf_addstr(&hookdir_annotation, _(" (will prompt)"));
+			break;
+		case HOOKDIR_WARN:
+			strbuf_addstr(&hookdir_annotation, _(" (will warn but run)"));
+			break;
+		case HOOKDIR_YES:
+		/*
+		 * The default behavior should agree with
+		 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
+		 * do the default behavior.
+		 */
+		case HOOKDIR_UNKNOWN:
+		default:
+			break;
+	}
+
 	list_for_each(pos, head) {
 		struct hook *item = list_entry(pos, struct hook, list);
 		item = list_entry(pos, struct hook, list);
 		if (item) {
 			/* Don't translate 'hookdir' - it matches the config */
-			printf("%s: %s\n",
+			printf("%s: %s%s\n",
 			       (item->from_hookdir
 				? "hookdir"
 				: config_scope_name(item->origin)),
-			       item->command.buf);
+			       item->command.buf,
+			       (item->from_hookdir
+				? hookdir_annotation.buf
+				: ""));
 		}
 	}
 
 	clear_hook_list(head);
+	strbuf_release(&hookdir_annotation);
 	strbuf_release(&hookname);
 
 	return 0;
@@ -59,16 +90,44 @@ static int list(int argc, const char **argv, const char *prefix)
 
 int cmd_hook(int argc, const char **argv, const char *prefix)
 {
+	const char *run_hookdir = NULL;
+
 	struct option builtin_hook_options[] = {
+		OPT_STRING(0, "run-hookdir", &run_hookdir, N_("option"),
+			   N_("what to do with hooks found in the hookdir")),
 		OPT_END(),
 	};
-	if (argc < 2)
+
+	argc = parse_options(argc, argv, prefix, builtin_hook_options,
+			     builtin_hook_usage, 0);
+
+	/* after the parse, we should have "<command> <hookname> <args...>" */
+	if (argc < 1)
 		usage_with_options(builtin_hook_usage, builtin_hook_options);
 
 	git_config(git_default_config, NULL);
 
-	if (!strcmp(argv[1], "list"))
-		return list(argc - 1, argv + 1, prefix);
+
+	/* argument > config */
+	if (run_hookdir)
+		if (!strcmp(run_hookdir, "no"))
+			should_run_hookdir = HOOKDIR_NO;
+		else if (!strcmp(run_hookdir, "error"))
+			should_run_hookdir = HOOKDIR_ERROR;
+		else if (!strcmp(run_hookdir, "yes"))
+			should_run_hookdir = HOOKDIR_YES;
+		else if (!strcmp(run_hookdir, "warn"))
+			should_run_hookdir = HOOKDIR_WARN;
+		else if (!strcmp(run_hookdir, "interactive"))
+			should_run_hookdir = HOOKDIR_INTERACTIVE;
+		else
+			die(_("'%s' is not a valid option for --run-hookdir "
+			      "(yes, warn, interactive, no)"), run_hookdir);
+	else
+		should_run_hookdir = configured_hookdir_opt();
+
+	if (!strcmp(argv[0], "list"))
+		return list(argc, argv, prefix);
 
 	usage_with_options(builtin_hook_usage, builtin_hook_options);
 }
diff --git a/hook.c b/hook.c
index 080e25696b..039ff0a378 100644
--- a/hook.c
+++ b/hook.c
@@ -102,6 +102,30 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)
 	return 0;
 }
 
+enum hookdir_opt configured_hookdir_opt(void)
+{
+	const char *key;
+	if (git_config_get_value("hook.runhookdir", &key))
+		return HOOKDIR_YES; /* by default, just run it. */
+
+	if (!strcmp(key, "no"))
+		return HOOKDIR_NO;
+
+	if (!strcmp(key, "error"))
+		return HOOKDIR_ERROR;
+
+	if (!strcmp(key, "yes"))
+		return HOOKDIR_YES;
+
+	if (!strcmp(key, "warn"))
+		return HOOKDIR_WARN;
+
+	if (!strcmp(key, "interactive"))
+		return HOOKDIR_INTERACTIVE;
+
+	return HOOKDIR_UNKNOWN;
+}
+
 struct list_head* hook_list(const struct strbuf* hookname)
 {
 	struct strbuf hook_key = STRBUF_INIT;
diff --git a/hook.h b/hook.h
index a97d43670d..1c4b953aec 100644
--- a/hook.h
+++ b/hook.h
@@ -20,6 +20,22 @@ struct hook {
  */
 struct list_head* hook_list(const struct strbuf *hookname);
 
+enum hookdir_opt
+{
+	HOOKDIR_NO,
+	HOOKDIR_ERROR,
+	HOOKDIR_WARN,
+	HOOKDIR_INTERACTIVE,
+	HOOKDIR_YES,
+	HOOKDIR_UNKNOWN,
+};
+
+/*
+ * Provides the hookdir_opt specified in the config without consulting any
+ * command line arguments.
+ */
+enum hookdir_opt configured_hookdir_opt(void);
+
 /* Free memory associated with a 'struct hook' */
 void free_hook(struct hook *ptr);
 /* Empties the list at 'head', calling 'free_hook()' on each entry */
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
index 0f12af4659..66b0b6b7ad 100755
--- a/t/t1360-config-based-hooks.sh
+++ b/t/t1360-config-based-hooks.sh
@@ -104,4 +104,75 @@ test_expect_success 'git hook list shows hooks from the hookdir' '
 	test_cmp expected actual
 '
 
+test_expect_success 'hook.runHookDir = no is respected by list' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "no" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit (will not run)
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_i18ncmp expected actual
+'
+
+test_expect_success 'hook.runHookDir = error is respected by list' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "error" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit (will error and not run)
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_i18ncmp expected actual
+'
+
+test_expect_success 'hook.runHookDir = warn is respected by list' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "warn" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit (will warn but run)
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_i18ncmp expected actual
+'
+
+
+test_expect_success 'hook.runHookDir = interactive is respected by list' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "interactive" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit (will prompt)
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_i18ncmp expected actual
+'
+
+test_expect_success 'hook.runHookDir is tolerant to unknown values' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "junk" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_i18ncmp expected actual
+'
+
 test_done
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 06/37] hook: implement hookcmd.<name>.skip
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (4 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 05/37] hook: teach hook.runHookDir Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  8:49   ` Ævar Arnfjörð Bjarmason
  2021-03-11  2:10 ` [PATCH v8 07/37] parse-options: parse into strvec Emily Shaffer
                   ` (34 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

If a user wants a specific repo to skip execution of a hook which is set
at a global or system level, they will be able to do so by specifying
'skip' in their repo config:

~/.gitconfig
  [hook.pre-commit]
    command = skippable-oneliner
    command = skippable-hookcmd

  [hookcmd.skippable-hookcmd]
    command = foo.sh

$GIT_DIR/.git/config
  [hookcmd.skippable-oneliner]
    skip = true
  [hookcmd.skippable-hookcmd]
    skip = true

Later it may make sense to add an option like
"hookcmd.<name>.<hook-event>-skip" - but for simplicity, let's start
with a universal skip setting like this.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

 Documentation/config/hook.txt |  8 ++++++++
 Documentation/git-hook.txt    | 33 +++++++++++++++++++++++++++++++++
 hook.c                        | 35 ++++++++++++++++++++++++++---------
 t/t1360-config-based-hooks.sh | 35 +++++++++++++++++++++++++++++++++++
 4 files changed, 102 insertions(+), 9 deletions(-)

diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
index 75312754ae..8b12512e33 100644
--- a/Documentation/config/hook.txt
+++ b/Documentation/config/hook.txt
@@ -8,6 +8,14 @@ hookcmd.<name>.command::
 	as a command. This can be an executable on your device or a oneliner for
 	your shell. See linkgit:git-hook[1].
 
+hookcmd.<name>.skip::
+	Specify this boolean to remove a command from earlier in the execution
+	order. Useful if you want to make a single repo an exception to hook
+	configured at the system or global scope. If there is no hookcmd
+	specified for the command you want to skip, you can use the value of
+	`hook.<command>.command` as <name> as a shortcut. The "skip" setting
+	must be specified after the "hook.<command>.command" to have an effect.
+
 hook.runHookDir::
 	Controls how hooks contained in your hookdir are executed. Can be any of
 	"yes", "warn", "interactive", or "no". Defaults to "yes". See
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index f19875ed68..c84520cb38 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -54,6 +54,39 @@ $ git hook list "prepare-commit-msg"
 local: /bin/linter --c
 ----
 
+If there is a command you wish to run in most cases but have one or two
+exceptional repos where it should be skipped, you can use specify
+`hookcmd.<name>.skip`, for example:
+
+System config
+----
+  [hook "pre-commit"]
+    command = check-for-secrets
+
+  [hookcmd "check-for-secrets"]
+    command = /bin/secret-checker --aggressive
+----
+
+Local config
+----
+  [hookcmd "check-for-secrets"]
+    skip = true
+  # This works for inlined hook commands, too:
+  [hookcmd "~/typocheck.sh"]
+    skip = true
+----
+
+After these configs are added, the hook list becomes:
+
+----
+$ git hook list "post-commit"
+global: /bin/linter --c
+local: python ~/run-test-suite.py
+
+$ git hook list "pre-commit"
+no commands configured for hook 'pre-commit'
+----
+
 COMMANDS
 --------
 
diff --git a/hook.c b/hook.c
index 039ff0a378..37b740d58d 100644
--- a/hook.c
+++ b/hook.c
@@ -12,24 +12,25 @@ void free_hook(struct hook *ptr)
 	}
 }
 
-static void append_or_move_hook(struct list_head *head, const char *command)
+static struct hook * find_hook_by_command(struct list_head *head, const char *command)
 {
 	struct list_head *pos = NULL, *tmp = NULL;
-	struct hook *to_add = NULL;
+	struct hook *found = NULL;
 
-	/*
-	 * remove the prior entry with this command; we'll replace it at the
-	 * end.
-	 */
 	list_for_each_safe(pos, tmp, head) {
 		struct hook *it = list_entry(pos, struct hook, list);
 		if (!strcmp(it->command.buf, command)) {
 		    list_del(pos);
-		    /* we'll simply move the hook to the end */
-		    to_add = it;
+		    found = it;
 		    break;
 		}
 	}
+	return found;
+}
+
+static void append_or_move_hook(struct list_head *head, const char *command)
+{
+	struct hook *to_add = find_hook_by_command(head, command);
 
 	if (!to_add) {
 		/* adding a new hook, not moving an old one */
@@ -74,12 +75,22 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)
 	if (!strcmp(key, hook_key)) {
 		const char *command = value;
 		struct strbuf hookcmd_name = STRBUF_INIT;
+		int skip = 0;
+
+		/*
+		 * Check if we're removing that hook instead. Hookcmds are
+		 * removed by name, and inlined hooks are removed by command
+		 * content.
+		 */
+		strbuf_addf(&hookcmd_name, "hookcmd.%s.skip", command);
+		git_config_get_bool(hookcmd_name.buf, &skip);
 
 		/*
 		 * Check if a hookcmd with that name exists. If it doesn't,
 		 * 'git_config_get_value()' is documented not to touch &command,
 		 * so we don't need to do anything.
 		 */
+		strbuf_reset(&hookcmd_name);
 		strbuf_addf(&hookcmd_name, "hookcmd.%s.command", command);
 		git_config_get_value(hookcmd_name.buf, &command);
 
@@ -94,7 +105,13 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)
 		 *   for each key+value, do_callback(key, value, cb_data)
 		 */
 
-		append_or_move_hook(head, command);
+		if (skip) {
+			struct hook *to_remove = find_hook_by_command(head, command);
+			if (to_remove)
+				remove_hook(&(to_remove->list));
+		} else {
+			append_or_move_hook(head, command);
+		}
 
 		strbuf_release(&hookcmd_name);
 	}
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
index 66b0b6b7ad..a9b1b046c1 100755
--- a/t/t1360-config-based-hooks.sh
+++ b/t/t1360-config-based-hooks.sh
@@ -146,6 +146,41 @@ test_expect_success 'hook.runHookDir = warn is respected by list' '
 	test_i18ncmp expected actual
 '
 
+test_expect_success 'git hook list removes skipped hookcmd' '
+	setup_hookcmd &&
+	test_config hookcmd.abc.skip "true" --add &&
+
+	cat >expected <<-EOF &&
+	no commands configured for hook '\''pre-commit'\''
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_i18ncmp expected actual
+'
+
+test_expect_success 'git hook list ignores skip referring to unused hookcmd' '
+	test_config hookcmd.abc.command "/path/abc" --add &&
+	test_config hookcmd.abc.skip "true" --add &&
+
+	cat >expected <<-EOF &&
+	no commands configured for hook '\''pre-commit'\''
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_i18ncmp expected actual
+'
+
+test_expect_success 'git hook list removes skipped inlined hook' '
+	setup_hooks &&
+	test_config hookcmd."$ROOT/path/ghi".skip "true" --add &&
+
+	cat >expected <<-EOF &&
+	global: $ROOT/path/def
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
 
 test_expect_success 'hook.runHookDir = interactive is respected by list' '
 	setup_hookdir &&
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 07/37] parse-options: parse into strvec
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (5 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 06/37] hook: implement hookcmd.<name>.skip Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  8:50   ` Ævar Arnfjörð Bjarmason
  2021-03-11  2:10 ` [PATCH v8 08/37] hook: add 'run' subcommand Emily Shaffer
                   ` (33 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

parse-options already knows how to read into a string_list, and it knows
how to read into an strvec as a passthrough (that is, including the
argument as well as its value). string_list and strvec serve similar
purposes but are somewhat painful to convert between; so, let's teach
parse-options to read values of string arguments directly into an
strvec without preserving the argument name.

This is useful if collecting generic arguments to pass through to
another command, for example, 'git hook run --arg "--quiet" --arg
"--format=pretty" some-hook'. The resulting strvec would contain
{ "--quiet", "--format=pretty" }.

The implementation is based on that of OPT_STRING_LIST.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Since v7, updated the reference doc to make the intended usage for OPT_STRVEC
    more clear.
    
    Since v4, fixed one or two more places where I missed the argv_array->strvec
    rename.

 Documentation/technical/api-parse-options.txt |  7 +++++++
 parse-options-cb.c                            | 16 ++++++++++++++++
 parse-options.h                               |  4 ++++
 3 files changed, 27 insertions(+)

diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt
index 5a60bbfa7f..f79b17e7fc 100644
--- a/Documentation/technical/api-parse-options.txt
+++ b/Documentation/technical/api-parse-options.txt
@@ -173,6 +173,13 @@ There are some macros to easily define options:
 	The string argument is stored as an element in `string_list`.
 	Use of `--no-option` will clear the list of preceding values.
 
+`OPT_STRVEC(short, long, &struct strvec, arg_str, description)`::
+	Introduce an option with a string argument, meant to be specified
+	multiple times.
+	The string argument is stored as an element in `strvec`, and later
+	arguments are added to the same `strvec`.
+	Use of `--no-option` will clear the list of preceding values.
+
 `OPT_INTEGER(short, long, &int_var, description)`::
 	Introduce an option with integer argument.
 	The integer is put into `int_var`.
diff --git a/parse-options-cb.c b/parse-options-cb.c
index 4542d4d3f9..c2451dfb1b 100644
--- a/parse-options-cb.c
+++ b/parse-options-cb.c
@@ -207,6 +207,22 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset)
 	return 0;
 }
 
+int parse_opt_strvec(const struct option *opt, const char *arg, int unset)
+{
+	struct strvec *v = opt->value;
+
+	if (unset) {
+		strvec_clear(v);
+		return 0;
+	}
+
+	if (!arg)
+		return -1;
+
+	strvec_push(v, arg);
+	return 0;
+}
+
 int parse_opt_noop_cb(const struct option *opt, const char *arg, int unset)
 {
 	return 0;
diff --git a/parse-options.h b/parse-options.h
index ff6506a504..44c4ac08e9 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -177,6 +177,9 @@ struct option {
 #define OPT_STRING_LIST(s, l, v, a, h) \
 				    { OPTION_CALLBACK, (s), (l), (v), (a), \
 				      (h), 0, &parse_opt_string_list }
+#define OPT_STRVEC(s, l, v, a, h) \
+				    { OPTION_CALLBACK, (s), (l), (v), (a), \
+				      (h), 0, &parse_opt_strvec }
 #define OPT_UYN(s, l, v, h)         { OPTION_CALLBACK, (s), (l), (v), NULL, \
 				      (h), PARSE_OPT_NOARG, &parse_opt_tertiary }
 #define OPT_EXPIRY_DATE(s, l, v, h) \
@@ -296,6 +299,7 @@ int parse_opt_commits(const struct option *, const char *, int);
 int parse_opt_commit(const struct option *, const char *, int);
 int parse_opt_tertiary(const struct option *, const char *, int);
 int parse_opt_string_list(const struct option *, const char *, int);
+int parse_opt_strvec(const struct option *, const char *, int);
 int parse_opt_noop_cb(const struct option *, const char *, int);
 enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx,
 					   const struct option *,
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 08/37] hook: add 'run' subcommand
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (6 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 07/37] parse-options: parse into strvec Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  8:54   ` Ævar Arnfjörð Bjarmason
  2021-03-12 10:22   ` Junio C Hamano
  2021-03-11  2:10 ` [PATCH v8 09/37] hook: introduce hook_exists() Emily Shaffer
                   ` (32 subsequent siblings)
  40 siblings, 2 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

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.

For now, the hook commands will run in config order, in series. As
alternate ordering or parallelism is supported in the future, we should
add knobs to use those to the command line as well.

As with the legacy hook implementation, all stdout generated by hook
commands is redirected to stderr. Piping from stdin is not yet
supported.

Legacy hooks (those present in $GITDIR/hooks) are run at the end of the
execution list. They can be disabled, or made to print warnings, or to
prompt before running, with the 'hook.runHookDir' config.

Users may wish to provide hook commands like 'git config
hook.pre-commit.command "~/linter.sh --pre-commit"'. To enable this,
config-defined hooks are run in a shell. (Since hooks in $GITDIR/hooks
can't be specified with included arguments or paths which need expansion
like this, they are run without a shell instead.)

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Since v7, added support for "error" hook.runHookDir setting.
    
    Since v4, updated the docs, and did less local application of single
    quotes. In order for hookdir hooks to run successfully with a space in
    the path, though, they must not be run with 'sh -c'. So we can treat the
    hookdir hooks specially, and warn users via doc about special
    considerations for configured hooks with spaces in their path.

 Documentation/git-hook.txt    |  31 +++++++-
 builtin/hook.c                |  42 ++++++++++-
 hook.c                        | 128 ++++++++++++++++++++++++++++++++++
 hook.h                        |  26 +++++++
 t/t1360-config-based-hooks.sh |  72 ++++++++++++++++++-
 5 files changed, 292 insertions(+), 7 deletions(-)

diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index c84520cb38..8f96c347ea 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -9,11 +9,12 @@ SYNOPSIS
 --------
 [verse]
 'git hook' list <hook-name>
+'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>
 
 DESCRIPTION
 -----------
-You can list configured hooks with this command. Later, you will be able to run,
-add, and modify hooks with this command.
+You can list and run configured hooks with this command. Later, you will be able
+to add and modify hooks with this command.
 
 This command parses the default configuration files for sections `hook` and
 `hookcmd`. `hook` is used to describe the commands which will be run during a
@@ -97,6 +98,32 @@ in the order they should be run, and print the config scope where the relevant
 `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).
 This output is human-readable and the format is subject to change over time.
 
+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::
+
+Runs hooks configured for `<hook-name>`, in the same order displayed by `git
+hook list`. Hooks configured this way may be run prepended with `sh -c`, so
+paths containing special characters or spaces should be wrapped in single
+quotes: `command = '/my/path with spaces/script.sh' some args`.
+
+OPTIONS
+-------
+--run-hookdir::
+	Overrides the hook.runHookDir config. Must be 'yes', 'warn',
+	'interactive', or 'no'. Specifies how to handle hooks located in the Git
+	hook directory (core.hooksPath).
+
+-a::
+--arg::
+	Only valid for `run`.
++
+Specify arguments to pass to every hook that is run.
+
+-e::
+--env::
+	Only valid for `run`.
++
+Specify environment variables to set for every hook that is run.
+
 CONFIGURATION
 -------------
 include::config/hook.txt[]
diff --git a/builtin/hook.c b/builtin/hook.c
index 310f696ebf..e823a96238 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -4,9 +4,11 @@
 #include "hook.h"
 #include "parse-options.h"
 #include "strbuf.h"
+#include "strvec.h"
 
 static const char * const builtin_hook_usage[] = {
 	N_("git hook list <hookname>"),
+	N_("git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hookname>"),
 	NULL
 };
 
@@ -88,6 +90,40 @@ static int list(int argc, const char **argv, const char *prefix)
 	return 0;
 }
 
+static int run(int argc, const char **argv, const char *prefix)
+{
+	struct strbuf hookname = STRBUF_INIT;
+	struct run_hooks_opt opt;
+	int rc = 0;
+
+	struct option run_options[] = {
+		OPT_STRVEC('e', "env", &opt.env, N_("var"),
+			   N_("environment variables for hook to use")),
+		OPT_STRVEC('a', "arg", &opt.args, N_("args"),
+			   N_("argument to pass to hook")),
+		OPT_END(),
+	};
+
+	run_hooks_opt_init(&opt);
+
+	argc = parse_options(argc, argv, prefix, run_options,
+			     builtin_hook_usage, 0);
+
+	if (argc < 1)
+		usage_msg_opt(_("You must specify a hook event to run."),
+			      builtin_hook_usage, run_options);
+
+	strbuf_addstr(&hookname, argv[0]);
+	opt.run_hookdir = should_run_hookdir;
+
+	rc = run_hooks(hookname.buf, &opt);
+
+	strbuf_release(&hookname);
+	run_hooks_opt_clear(&opt);
+
+	return rc;
+}
+
 int cmd_hook(int argc, const char **argv, const char *prefix)
 {
 	const char *run_hookdir = NULL;
@@ -99,10 +135,10 @@ int cmd_hook(int argc, const char **argv, const char *prefix)
 	};
 
 	argc = parse_options(argc, argv, prefix, builtin_hook_options,
-			     builtin_hook_usage, 0);
+			     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN);
 
 	/* after the parse, we should have "<command> <hookname> <args...>" */
-	if (argc < 1)
+	if (argc < 2)
 		usage_with_options(builtin_hook_usage, builtin_hook_options);
 
 	git_config(git_default_config, NULL);
@@ -128,6 +164,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)
 
 	if (!strcmp(argv[0], "list"))
 		return list(argc, argv, prefix);
+	if (!strcmp(argv[0], "run"))
+		return run(argc, argv, prefix);
 
 	usage_with_options(builtin_hook_usage, builtin_hook_options);
 }
diff --git a/hook.c b/hook.c
index 37b740d58d..d166d17fb0 100644
--- a/hook.c
+++ b/hook.c
@@ -3,6 +3,7 @@
 #include "hook.h"
 #include "config.h"
 #include "run-command.h"
+#include "prompt.h"
 
 void free_hook(struct hook *ptr)
 {
@@ -143,6 +144,64 @@ enum hookdir_opt configured_hookdir_opt(void)
 	return HOOKDIR_UNKNOWN;
 }
 
+static int should_include_hookdir(const char *path, enum hookdir_opt cfg)
+{
+	struct strbuf prompt = STRBUF_INIT;
+	/*
+	 * If the path doesn't exist, don't bother adding the empty hook and
+	 * don't bother checking the config or prompting the user.
+	 */
+	if (!path)
+		return 0;
+
+	switch (cfg)
+	{
+		case HOOKDIR_ERROR:
+			fprintf(stderr, _("Skipping legacy hook at '%s'\n"),
+				path);
+			/* FALLTHROUGH */
+		case HOOKDIR_NO:
+			return 0;
+		case HOOKDIR_WARN:
+			fprintf(stderr, _("Running legacy hook at '%s'\n"),
+				path);
+			return 1;
+		case HOOKDIR_INTERACTIVE:
+			do {
+				/*
+				 * TRANSLATORS: Make sure to include [Y] and [n]
+				 * in your translation. Only English input is
+				 * accepted. Default option is "yes".
+				 */
+				fprintf(stderr, _("Run '%s'? [Yn] "), path);
+				git_read_line_interactively(&prompt);
+				strbuf_tolower(&prompt);
+				if (starts_with(prompt.buf, "n")) {
+					strbuf_release(&prompt);
+					return 0;
+				} else if (starts_with(prompt.buf, "y")) {
+					strbuf_release(&prompt);
+					return 1;
+				}
+				/* otherwise, we didn't understand the input */
+			} while (prompt.len); /* an empty reply means "Yes" */
+			strbuf_release(&prompt);
+			return 1;
+		/*
+		 * HOOKDIR_UNKNOWN should match the default behavior, but let's
+		 * give a heads up to the user.
+		 */
+		case HOOKDIR_UNKNOWN:
+			fprintf(stderr,
+				_("Unrecognized value for 'hook.runHookDir'. "
+				  "Is there a typo? "));
+			/* FALLTHROUGH */
+		case HOOKDIR_YES:
+		default:
+			return 1;
+	}
+}
+
 struct list_head* hook_list(const struct strbuf* hookname)
 {
 	struct strbuf hook_key = STRBUF_INIT;
@@ -176,3 +235,72 @@ struct list_head* hook_list(const struct strbuf* hookname)
 	strbuf_release(&hook_key);
 	return hook_head;
 }
+
+void run_hooks_opt_init(struct run_hooks_opt *o)
+{
+	strvec_init(&o->env);
+	strvec_init(&o->args);
+	o->run_hookdir = configured_hookdir_opt();
+}
+
+void run_hooks_opt_clear(struct run_hooks_opt *o)
+{
+	strvec_clear(&o->env);
+	strvec_clear(&o->args);
+}
+
+static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
+			    struct child_process *cp)
+{
+	if (!hook)
+		return;
+
+	cp->no_stdin = 1;
+	cp->env = options->env.v;
+	cp->stdout_to_stderr = 1;
+	cp->trace2_hook_name = hook->command.buf;
+
+	/*
+	 * Commands from the config could be oneliners, but we know
+	 * for certain that hookdir commands are not.
+	 */
+	cp->use_shell = !hook->from_hookdir;
+
+	/* add command */
+	strvec_push(&cp->args, hook->command.buf);
+
+	/*
+	 * add passed-in argv, without expanding - let the user get back
+	 * exactly what they put in
+	 */
+	strvec_pushv(&cp->args, options->args.v);
+}
+
+int run_hooks(const char *hookname, struct run_hooks_opt *options)
+{
+	struct strbuf hookname_str = STRBUF_INIT;
+	struct list_head *to_run, *pos = NULL, *tmp = NULL;
+	int rc = 0;
+
+	if (!options)
+		BUG("a struct run_hooks_opt must be provided to run_hooks");
+
+	strbuf_addstr(&hookname_str, hookname);
+
+	to_run = hook_list(&hookname_str);
+
+	list_for_each_safe(pos, tmp, to_run) {
+		struct child_process hook_proc = CHILD_PROCESS_INIT;
+		struct hook *hook = list_entry(pos, struct hook, list);
+
+		if (hook->from_hookdir &&
+		    !should_include_hookdir(hook->command.buf, options->run_hookdir))
+			continue;
+
+		prepare_hook_cp(hook, options, &hook_proc);
+
+		rc |= run_command(&hook_proc);
+	}
+
+	return rc;
+}
diff --git a/hook.h b/hook.h
index 1c4b953aec..c24b2c9ecd 100644
--- a/hook.h
+++ b/hook.h
@@ -1,6 +1,7 @@
 #include "config.h"
 #include "list.h"
 #include "strbuf.h"
+#include "strvec.h"
 
 struct hook {
 	struct list_head list;
@@ -36,6 +37,31 @@ enum hookdir_opt
  */
 enum hookdir_opt configured_hookdir_opt(void);
 
+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;
+
+	/*
+	 * How should the hookdir be handled?
+	 * Leave the RUN_HOOKS_OPT_INIT default in most cases; this only needs
+	 * to be overridden if the user can override it at the command line.
+	 */
+	enum hookdir_opt run_hookdir;
+};
+
+void run_hooks_opt_init(struct run_hooks_opt *o);
+void run_hooks_opt_clear(struct run_hooks_opt *o);
+
+/*
+ * Runs all hooks associated to the 'hookname' event in order. Each hook will be
+ * passed 'env' and 'args'.
+ */
+int run_hooks(const char *hookname, struct run_hooks_opt *options);
+
 /* Free memory associated with a 'struct hook' */
 void free_hook(struct hook *ptr);
 /* Empties the list at 'head', calling 'free_hook()' on each entry */
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
index a9b1b046c1..1fca83d536 100755
--- a/t/t1360-config-based-hooks.sh
+++ b/t/t1360-config-based-hooks.sh
@@ -115,7 +115,10 @@ test_expect_success 'hook.runHookDir = no is respected by list' '
 
 	git hook list pre-commit >actual &&
 	# the hookdir annotation is translated
-	test_i18ncmp expected actual
+	test_i18ncmp expected actual &&
+
+	git hook run pre-commit 2>actual &&
+	test_must_be_empty actual
 '
 
 test_expect_success 'hook.runHookDir = error is respected by list' '
@@ -129,6 +132,13 @@ test_expect_success 'hook.runHookDir = error is respected by list' '
 
 	git hook list pre-commit >actual &&
 	# the hookdir annotation is translated
+	test_i18ncmp expected actual &&
+
+	cat >expected <<-EOF &&
+	Skipping legacy hook at '\''$(pwd)/.git/hooks/pre-commit'\''
+	EOF
+
+	git hook run pre-commit 2>actual &&
 	test_i18ncmp expected actual
 '
 
@@ -143,6 +153,14 @@ test_expect_success 'hook.runHookDir = warn is respected by list' '
 
 	git hook list pre-commit >actual &&
 	# the hookdir annotation is translated
+	test_i18ncmp expected actual &&
+
+	cat >expected <<-EOF &&
+	Running legacy hook at '\''$(pwd)/.git/hooks/pre-commit'\''
+	"Legacy Hook"
+	EOF
+
+	git hook run pre-commit 2>actual &&
 	test_i18ncmp expected actual
 '
 
@@ -182,7 +200,7 @@ test_expect_success 'git hook list removes skipped inlined hook' '
 	test_cmp expected actual
 '
 
-test_expect_success 'hook.runHookDir = interactive is respected by list' '
+test_expect_success 'hook.runHookDir = interactive is respected by list and run' '
 	setup_hookdir &&
 
 	test_config hook.runHookDir "interactive" &&
@@ -193,7 +211,55 @@ test_expect_success 'hook.runHookDir = interactive is respected by list' '
 
 	git hook list pre-commit >actual &&
 	# the hookdir annotation is translated
-	test_i18ncmp expected actual
+	test_i18ncmp expected actual &&
+
+	test_write_lines n | git hook run pre-commit 2>actual &&
+	! grep "Legacy Hook" actual &&
+
+	test_write_lines y | git hook run pre-commit 2>actual &&
+	grep "Legacy Hook" actual
+'
+
+test_expect_success 'inline hook definitions execute oneliners' '
+	test_config hook.pre-commit.command "echo \"Hello World\"" &&
+
+	echo "Hello World" >expected &&
+
+	# hooks are run with stdout_to_stderr = 1
+	git hook run pre-commit 2>actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'inline hook definitions resolve paths' '
+	write_script sample-hook.sh <<-EOF &&
+	echo \"Sample Hook\"
+	EOF
+
+	test_when_finished "rm sample-hook.sh" &&
+
+	test_config hook.pre-commit.command "\"$(pwd)/sample-hook.sh\"" &&
+
+	echo \"Sample Hook\" >expected &&
+
+	# hooks are run with stdout_to_stderr = 1
+	git hook run pre-commit 2>actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'hookdir hook included in git hook run' '
+	setup_hookdir &&
+
+	echo \"Legacy Hook\" >expected &&
+
+	# hooks are run with stdout_to_stderr = 1
+	git hook run pre-commit 2>actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'out-of-repo runs excluded' '
+	setup_hooks &&
+
+	nongit test_must_fail git hook run pre-commit
 '
 
 test_expect_success 'hook.runHookDir is tolerant to unknown values' '
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 09/37] hook: introduce hook_exists()
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (7 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 08/37] hook: add 'run' subcommand Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 10/37] hook: support passing stdin to hooks Emily Shaffer
                   ` (31 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Add a helper to easily determine whether any hooks exist for a given
hook event.

Many callers want to check whether some state could be modified by a
hook; that check should include the config-based hooks as well. Optimize
by checking the config directly. Since commands which execute hooks
might want to take args to replace 'hook.runHookDir', let
'hook_exists()' take a hookdir_opt to override that config.

In some cases, external callers today use find_hook() to discover the
location of a hook and then run it manually with run-command.h (that is,
not with run_hook_le()). Later, those cases will call hook.h:run_hook()
directly instead.

Once the entire codebase is using hook_exists() instead of find_hook(),
find_hook() can be safely rolled into hook_exists() and removed from
run-command.h.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 hook.c | 19 +++++++++++++++++++
 hook.h | 10 ++++++++++
 2 files changed, 29 insertions(+)

diff --git a/hook.c b/hook.c
index d166d17fb0..118931f273 100644
--- a/hook.c
+++ b/hook.c
@@ -243,6 +243,25 @@ void run_hooks_opt_init(struct run_hooks_opt *o)
 	o->run_hookdir = configured_hookdir_opt();
 }
 
+int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)
+{
+	const char *value = NULL; /* throwaway */
+	struct strbuf hook_key = STRBUF_INIT;
+	int could_run_hookdir;
+
+	if (should_run_hookdir == HOOKDIR_USE_CONFIG)
+		should_run_hookdir = configured_hookdir_opt();
+
+	could_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||
+				should_run_hookdir == HOOKDIR_WARN ||
+				should_run_hookdir == HOOKDIR_YES)
+				&& !!find_hook(hookname);
+
+	strbuf_addf(&hook_key, "hook.%s.command", hookname);
+
+	return (!git_config_get_value(hook_key.buf, &value)) || could_run_hookdir;
+}
+
 void run_hooks_opt_clear(struct run_hooks_opt *o)
 {
 	strvec_clear(&o->env);
diff --git a/hook.h b/hook.h
index c24b2c9ecd..0df785add5 100644
--- a/hook.h
+++ b/hook.h
@@ -23,6 +23,7 @@ struct list_head* hook_list(const struct strbuf *hookname);
 
 enum hookdir_opt
 {
+	HOOKDIR_USE_CONFIG,
 	HOOKDIR_NO,
 	HOOKDIR_ERROR,
 	HOOKDIR_WARN,
@@ -56,6 +57,15 @@ struct run_hooks_opt
 void run_hooks_opt_init(struct run_hooks_opt *o);
 void run_hooks_opt_clear(struct run_hooks_opt *o);
 
+/*
+ * Returns 1 if any hooks are specified in the config or if a hook exists in the
+ * hookdir. Typically, invoke hook_exsts() like:
+ *   hook_exists(hookname, configured_hookdir_opt());
+ * Like with run_hooks, if you take a --run-hookdir flag, reflect that
+ * user-specified behavior here instead.
+ */
+int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);
+
 /*
  * Runs all hooks associated to the 'hookname' event in order. Each hook will be
  * passed 'env' and 'args'.
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 10/37] hook: support passing stdin to hooks
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (8 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 09/37] hook: introduce hook_exists() Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  9:00   ` Ævar Arnfjörð Bjarmason
  2021-03-12 10:22   ` Junio C Hamano
  2021-03-11  2:10 ` [PATCH v8 11/37] run-command: allow stdin for run_processes_parallel Emily Shaffer
                   ` (30 subsequent siblings)
  40 siblings, 2 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Some hooks (such as post-rewrite) need to take input via stdin.
Previously, callers provided stdin to hooks by setting
run-command.h:child_process.in, which takes a FD. Callers would open the
file in question themselves before calling run-command(). However, since
we will now need to seek to the front of the file and read it again for
every hook which runs, hook.h:run_command() takes a path and handles FD
management itself. Since this file is opened for read only, it should
not prevent later parallel execution support.

On the frontend, this is supported by asking for a file path, rather
than by reading stdin. Reading directly from stdin would involve caching
the entire stdin (to memory or to disk) and reading it back from the
beginning to each hook. We'd want to support cases like insufficient
memory or storage for the file. While this may prove useful later, for
now the path of least resistance is to just ask the user to make this
interim file themselves.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/git-hook.txt    | 11 +++++++++--
 builtin/hook.c                |  5 ++++-
 hook.c                        |  8 +++++++-
 hook.h                        |  6 +++++-
 t/t1360-config-based-hooks.sh | 24 ++++++++++++++++++++++++
 5 files changed, 49 insertions(+), 5 deletions(-)

diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index 8f96c347ea..96a857c682 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -9,7 +9,8 @@ SYNOPSIS
 --------
 [verse]
 'git hook' list <hook-name>
-'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>
+'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>]
+	<hook-name>
 
 DESCRIPTION
 -----------
@@ -98,7 +99,7 @@ in the order they should be run, and print the config scope where the relevant
 `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).
 This output is human-readable and the format is subject to change over time.
 
-run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::
+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] `<hook-name>`::
 
 Runs hooks configured for `<hook-name>`, in the same order displayed by `git
 hook list`. Hooks configured this way may be run prepended with `sh -c`, so
@@ -124,6 +125,12 @@ Specify arguments to pass to every hook that is run.
 +
 Specify environment variables to set for every hook that is run.
 
+--to-stdin::
+	Only valid for `run`.
++
+Specify a file which will be streamed into stdin for every hook that is run.
+Each hook will receive the entire file from beginning to EOF.
+
 CONFIGURATION
 -------------
 include::config/hook.txt[]
diff --git a/builtin/hook.c b/builtin/hook.c
index e823a96238..38a4555e05 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -8,7 +8,8 @@
 
 static const char * const builtin_hook_usage[] = {
 	N_("git hook list <hookname>"),
-	N_("git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hookname>"),
+	N_("git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...]"
+	   "[--to-stdin=<path>] <hookname>"),
 	NULL
 };
 
@@ -101,6 +102,8 @@ static int run(int argc, const char **argv, const char *prefix)
 			   N_("environment variables for hook to use")),
 		OPT_STRVEC('a', "arg", &opt.args, N_("args"),
 			   N_("argument to pass to hook")),
+		OPT_STRING(0, "to-stdin", &opt.path_to_stdin, N_("path"),
+			   N_("file to read into hooks' stdin")),
 		OPT_END(),
 	};
 
diff --git a/hook.c b/hook.c
index 118931f273..f906e8c61c 100644
--- a/hook.c
+++ b/hook.c
@@ -240,6 +240,7 @@ void run_hooks_opt_init(struct run_hooks_opt *o)
 {
 	strvec_init(&o->env);
 	strvec_init(&o->args);
+	o->path_to_stdin = NULL;
 	o->run_hookdir = configured_hookdir_opt();
 }
 
@@ -274,7 +275,12 @@ static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
 	if (!hook)
 		return;
 
-	cp->no_stdin = 1;
+	/* reopen the file for stdin; run_command closes it. */
+	if (options->path_to_stdin)
+		cp->in = xopen(options->path_to_stdin, O_RDONLY);
+	else
+		cp->no_stdin = 1;
+
 	cp->env = options->env.v;
 	cp->stdout_to_stderr = 1;
 	cp->trace2_hook_name = hook->command.buf;
diff --git a/hook.h b/hook.h
index 0df785add5..2314ec5962 100644
--- a/hook.h
+++ b/hook.h
@@ -52,6 +52,9 @@ struct run_hooks_opt
 	 * to be overridden if the user can override it at the command line.
 	 */
 	enum hookdir_opt run_hookdir;
+
+	/* Path to file which should be piped to stdin for each hook */
+	const char *path_to_stdin;
 };
 
 void run_hooks_opt_init(struct run_hooks_opt *o);
@@ -68,7 +71,8 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);
 
 /*
  * Runs all hooks associated to the 'hookname' event in order. Each hook will be
- * passed 'env' and 'args'.
+ * passed 'env' and 'args'. The file at 'stdin_path' will be closed and reopened
+ * for each hook that runs.
  */
 int run_hooks(const char *hookname, struct run_hooks_opt *options);
 
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
index 1fca83d536..cace5a23c1 100755
--- a/t/t1360-config-based-hooks.sh
+++ b/t/t1360-config-based-hooks.sh
@@ -276,4 +276,28 @@ test_expect_success 'hook.runHookDir is tolerant to unknown values' '
 	test_i18ncmp expected actual
 '
 
+test_expect_success 'stdin to multiple hooks' '
+	git config --add hook.test.command "xargs -P1 -I% echo a%" &&
+	git config --add hook.test.command "xargs -P1 -I% echo b%" &&
+	test_when_finished "test_unconfig hook.test.command" &&
+
+	cat >input <<-EOF &&
+	1
+	2
+	3
+	EOF
+
+	cat >expected <<-EOF &&
+	a1
+	a2
+	a3
+	b1
+	b2
+	b3
+	EOF
+
+	git hook run --to-stdin=input test 2>actual &&
+	test_cmp expected actual
+'
+
 test_done
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 11/37] run-command: allow stdin for run_processes_parallel
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (9 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 10/37] hook: support passing stdin to hooks Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 12/37] hook: allow parallel hook execution Emily Shaffer
                   ` (29 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

While it makes sense not to inherit stdin from the parent process to
avoid deadlocking, it's not necessary to completely ban stdin to
children. An informed user should be able to configure stdin safely. By
setting `some_child.process.no_stdin=1` before calling `get_next_task()`
we provide a reasonable default behavior but enable users to set up
stdin streaming for themselves during the callback.

`some_child.process.stdout_to_stderr`, however, remains unmodifiable by
`get_next_task()` - the rest of the run_processes_parallel() API depends
on child output in stderr.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 run-command.c | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/run-command.c b/run-command.c
index 4e34623e2e..e6d7541b84 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1693,6 +1693,14 @@ static int pp_start_one(struct parallel_processes *pp)
 	if (i == pp->max_processes)
 		BUG("bookkeeping is hard");
 
+	/*
+	 * By default, do not inherit stdin from the parent process - otherwise,
+	 * all children would share stdin! Users may overwrite this to provide
+	 * something to the child's stdin by having their 'get_next_task'
+	 * callback assign 0 to .no_stdin and an appropriate integer to .in.
+	 */
+	pp->children[i].process.no_stdin = 1;
+
 	code = pp->get_next_task(&pp->children[i].process,
 				 &pp->children[i].err,
 				 pp->data,
@@ -1704,7 +1712,6 @@ static int pp_start_one(struct parallel_processes *pp)
 	}
 	pp->children[i].process.err = -1;
 	pp->children[i].process.stdout_to_stderr = 1;
-	pp->children[i].process.no_stdin = 1;
 
 	if (start_command(&pp->children[i].process)) {
 		code = pp->start_failure(&pp->children[i].err,
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 12/37] hook: allow parallel hook execution
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (10 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 11/37] run-command: allow stdin for run_processes_parallel Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 13/37] hook: allow specifying working directory for hooks Emily Shaffer
                   ` (28 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

In many cases, there's no reason not to allow hooks to execute in
parallel. run_processes_parallel() is well-suited - it's a task queue
that runs its housekeeping in series, which means users don't
need to worry about thread safety on their callback data. True
multithreaded execution with the async_* functions isn't necessary here.
Synchronous hook execution can be achieved by only allowing 1 job to run
at a time.

Teach run_hooks() to use that function for simple hooks which don't
require stdin or capture of stderr.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Per AEvar's request - parallel hook execution on day zero.
    
    In most ways run_processes_parallel() worked great for me - but it didn't
    have great support for hooks where we pipe to and from. I had to add this
    support later in the series.
    
    Since I modified an existing and in-use library I'd appreciate a keen look on
    these patches.
    
     - Emily

 Documentation/config/hook.txt |   5 ++
 Documentation/git-hook.txt    |  14 ++++-
 builtin/hook.c                |   6 +-
 hook.c                        | 108 +++++++++++++++++++++++++++++-----
 hook.h                        |  21 ++++++-
 5 files changed, 132 insertions(+), 22 deletions(-)

diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
index 8b12512e33..4f66bb35cf 100644
--- a/Documentation/config/hook.txt
+++ b/Documentation/config/hook.txt
@@ -20,3 +20,8 @@ hook.runHookDir::
 	Controls how hooks contained in your hookdir are executed. Can be any of
 	"yes", "warn", "interactive", or "no". Defaults to "yes". See
 	linkgit:git-hook[1] and linkgit:git-config[1] "core.hooksPath").
+
+hook.jobs::
+	Specifies how many hooks can be run simultaneously during parallelized
+	hook execution. If unspecified, defaults to the number of processors on
+	the current system.
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index 96a857c682..81b8e94994 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -10,7 +10,7 @@ SYNOPSIS
 [verse]
 'git hook' list <hook-name>
 'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>]
-	<hook-name>
+	[(-j|--jobs) <n>] <hook-name>
 
 DESCRIPTION
 -----------
@@ -99,7 +99,7 @@ in the order they should be run, and print the config scope where the relevant
 `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).
 This output is human-readable and the format is subject to change over time.
 
-run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] `<hook-name>`::
+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] [(-j|--jobs)<n>] `<hook-name>`::
 
 Runs hooks configured for `<hook-name>`, in the same order displayed by `git
 hook list`. Hooks configured this way may be run prepended with `sh -c`, so
@@ -131,6 +131,16 @@ Specify environment variables to set for every hook that is run.
 Specify a file which will be streamed into stdin for every hook that is run.
 Each hook will receive the entire file from beginning to EOF.
 
+-j::
+--jobs::
+	Only valid for `run`.
++
+Specify how many hooks to run simultaneously. If this flag is not specified, use
+the value of the `hook.jobs` config. If the config is not specified, use the
+number of CPUs on the current system. Some hooks may be ineligible for
+parallelization: for example, 'commit-msg' intends hooks modify the commit
+message body and cannot be parallelized.
+
 CONFIGURATION
 -------------
 include::config/hook.txt[]
diff --git a/builtin/hook.c b/builtin/hook.c
index 38a4555e05..b4f4adb1de 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -9,7 +9,7 @@
 static const char * const builtin_hook_usage[] = {
 	N_("git hook list <hookname>"),
 	N_("git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...]"
-	   "[--to-stdin=<path>] <hookname>"),
+	   "[--to-stdin=<path>] [(-j|--jobs) <count>] <hookname>"),
 	NULL
 };
 
@@ -104,10 +104,12 @@ static int run(int argc, const char **argv, const char *prefix)
 			   N_("argument to pass to hook")),
 		OPT_STRING(0, "to-stdin", &opt.path_to_stdin, N_("path"),
 			   N_("file to read into hooks' stdin")),
+		OPT_INTEGER('j', "jobs", &opt.jobs,
+			    N_("run up to <n> hooks simultaneously")),
 		OPT_END(),
 	};
 
-	run_hooks_opt_init(&opt);
+	run_hooks_opt_init_async(&opt);
 
 	argc = parse_options(argc, argv, prefix, run_options,
 			     builtin_hook_usage, 0);
diff --git a/hook.c b/hook.c
index f906e8c61c..fe8860860b 100644
--- a/hook.c
+++ b/hook.c
@@ -144,6 +144,14 @@ enum hookdir_opt configured_hookdir_opt(void)
 	return HOOKDIR_UNKNOWN;
 }
 
+int configured_hook_jobs(void)
+{
+	int n = online_cpus();
+	git_config_get_int("hook.jobs", &n);
+
+	return n;
+}
+
 static int should_include_hookdir(const char *path, enum hookdir_opt cfg)
 {
 	struct strbuf prompt = STRBUF_INIT;
@@ -236,12 +244,19 @@ struct list_head* hook_list(const struct strbuf* hookname)
 	return hook_head;
 }
 
-void run_hooks_opt_init(struct run_hooks_opt *o)
+void run_hooks_opt_init_sync(struct run_hooks_opt *o)
 {
 	strvec_init(&o->env);
 	strvec_init(&o->args);
 	o->path_to_stdin = NULL;
 	o->run_hookdir = configured_hookdir_opt();
+	o->jobs = 1;
+}
+
+void run_hooks_opt_init_async(struct run_hooks_opt *o)
+{
+	run_hooks_opt_init_sync(o);
+	o->jobs = configured_hook_jobs();
 }
 
 int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)
@@ -269,19 +284,26 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)
 	strvec_clear(&o->args);
 }
 
-static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
-			    struct child_process *cp)
+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 *hook = hook_cb->run_me;
+
 	if (!hook)
-		return;
+		return 0;
 
 	/* reopen the file for stdin; run_command closes it. */
-	if (options->path_to_stdin)
-		cp->in = xopen(options->path_to_stdin, O_RDONLY);
-	else
+	if (hook_cb->options->path_to_stdin) {
+		cp->no_stdin = 0;
+		cp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);
+	} else {
 		cp->no_stdin = 1;
+	}
 
-	cp->env = options->env.v;
+	cp->env = hook_cb->options->env.v;
 	cp->stdout_to_stderr = 1;
 	cp->trace2_hook_name = hook->command.buf;
 
@@ -298,14 +320,59 @@ static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
 	 * add passed-in argv, without expanding - let the user get back
 	 * exactly what they put in
 	 */
-	strvec_pushv(&cp->args, options->args.v);
+	strvec_pushv(&cp->args, hook_cb->options->args.v);
+
+	/* Provide context for errors if necessary */
+	*pp_task_cb = hook;
+
+	/* Get the next entry ready */
+	if (hook_cb->run_me->list.next == hook_cb->head)
+		hook_cb->run_me = NULL;
+	else
+		hook_cb->run_me = list_entry(hook_cb->run_me->list.next,
+					     struct hook, list);
+
+	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;
+
+	/* |= rc in cb */
+	hook_cb->rc |= 1;
+
+	strbuf_addf(out, _("Couldn't start '%s', configured in '%s'\n"),
+		    attempted->command.buf,
+		    attempted->from_hookdir ? "hookdir"
+			: config_scope_name(attempted->origin));
+
+	/* NEEDSWORK: if halt_on_error is desired, do it here. */
+	return 0;
+}
+
+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;
+
+	/* |= rc in cb */
+	hook_cb->rc |= result;
+
+	/* NEEDSWORK: if halt_on_error is desired, do it here. */
+	return 0;
 }
 
 int run_hooks(const char *hookname, struct run_hooks_opt *options)
 {
 	struct strbuf hookname_str = STRBUF_INIT;
 	struct list_head *to_run, *pos = NULL, *tmp = NULL;
-	int rc = 0;
+	struct hook_cb_data cb_data = { 0, NULL, NULL, options };
 
 	if (!options)
 		BUG("a struct run_hooks_opt must be provided to run_hooks");
@@ -315,17 +382,26 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
 	to_run = hook_list(&hookname_str);
 
 	list_for_each_safe(pos, tmp, to_run) {
-		struct child_process hook_proc = CHILD_PROCESS_INIT;
 		struct hook *hook = list_entry(pos, struct hook, list);
 
 		if (hook->from_hookdir &&
 		    !should_include_hookdir(hook->command.buf, options->run_hookdir))
-			continue;
+			    list_del(pos);
+	}
+
+	if (list_empty(to_run))
+		return 0;
 
-		prepare_hook_cp(hook, options, &hook_proc);
+	cb_data.head = to_run;
+	cb_data.run_me = list_entry(to_run->next, struct hook, list);
 
-		rc |= run_command(&hook_proc);
-	}
+	run_processes_parallel_tr2(options->jobs,
+				   pick_next_hook,
+				   notify_start_failure,
+				   notify_hook_finished,
+				   &cb_data,
+				   "hook",
+				   hookname);
 
-	return rc;
+	return cb_data.rc;
 }
diff --git a/hook.h b/hook.h
index 2314ec5962..2593f932c0 100644
--- a/hook.h
+++ b/hook.h
@@ -38,6 +38,9 @@ enum hookdir_opt
  */
 enum hookdir_opt configured_hookdir_opt(void);
 
+/* Provides the number of threads to use for parallel hook execution. */
+int configured_hook_jobs(void);
+
 struct run_hooks_opt
 {
 	/* Environment vars to be set for each hook */
@@ -48,16 +51,30 @@ struct run_hooks_opt
 
 	/*
 	 * How should the hookdir be handled?
-	 * Leave the RUN_HOOKS_OPT_INIT default in most cases; this only needs
+	 * Leave the run_hooks_opt_init_*() default in most cases; this only needs
 	 * to be overridden if the user can override it at the command line.
 	 */
 	enum hookdir_opt run_hookdir;
 
 	/* Path to file which should be piped to stdin for each hook */
 	const char *path_to_stdin;
+
+	/* Number of threads to parallelize across */
+	int jobs;
+};
+
+/*
+ * Callback provided to feed_pipe_fn and consume_sideband_fn.
+ */
+struct hook_cb_data {
+	int rc;
+	struct list_head *head;
+	struct hook *run_me;
+	struct run_hooks_opt *options;
 };
 
-void run_hooks_opt_init(struct run_hooks_opt *o);
+void run_hooks_opt_init_sync(struct run_hooks_opt *o);
+void run_hooks_opt_init_async(struct run_hooks_opt *o);
 void run_hooks_opt_clear(struct run_hooks_opt *o);
 
 /*
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 13/37] hook: allow specifying working directory for hooks
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (11 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 12/37] hook: allow parallel hook execution Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 14/37] run-command: add stdin callback for parallelization Emily Shaffer
                   ` (27 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Hooks like "post-checkout" require that hooks have a different working
directory than the initial process. Pipe that directly through to struct
child_process.

Because we can just run 'git -C <some-dir> hook run ...' it shouldn't be
necessary to pipe this option through the frontend. In fact, this
reduces the possibility of users running hooks which affect some part of
the filesystem outside of the repo in question.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Needed later for "post-checkout" conversion.

 hook.c | 2 ++
 hook.h | 3 +++
 2 files changed, 5 insertions(+)

diff --git a/hook.c b/hook.c
index fe8860860b..67ad3aa747 100644
--- a/hook.c
+++ b/hook.c
@@ -251,6 +251,7 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)
 	o->path_to_stdin = NULL;
 	o->run_hookdir = configured_hookdir_opt();
 	o->jobs = 1;
+	o->dir = NULL;
 }
 
 void run_hooks_opt_init_async(struct run_hooks_opt *o)
@@ -306,6 +307,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->command.buf;
+	cp->dir = hook_cb->options->dir;
 
 	/*
 	 * Commands from the config could be oneliners, but we know
diff --git a/hook.h b/hook.h
index 2593f932c0..fcd8e99e39 100644
--- a/hook.h
+++ b/hook.h
@@ -61,6 +61,9 @@ struct run_hooks_opt
 
 	/* Number of threads to parallelize across */
 	int jobs;
+
+	/* Path to initial working directory for subprocess */
+	const char *dir;
 };
 
 /*
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 14/37] run-command: add stdin callback for parallelization
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (12 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 13/37] hook: allow specifying working directory for hooks Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 15/37] hook: provide stdin by string_list or callback Emily Shaffer
                   ` (26 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

If a user of the run_processes_parallel() API wants to pipe a large
amount of information to stdin of each parallel command, that
information could exceed the buffer of the pipe allocated for that
process's stdin.  Generally this is solved by repeatedly writing to
child_process.in between calls to start_command() and finish_command();
run_processes_parallel() did not provide users an opportunity to access
child_process at that time.

Because the data might be extremely large (for example, a list of all
refs received during a push from a client) simply taking a string_list
or strbuf is not as scalable as using a callback; the rest of the
run_processes_parallel() API also uses callbacks, so making this feature
match the rest of the API reduces mental load on the user.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 builtin/fetch.c             |  1 +
 builtin/submodule--helper.c |  2 +-
 hook.c                      |  1 +
 run-command.c               | 54 +++++++++++++++++++++++++++++++++++--
 run-command.h               | 17 +++++++++++-
 submodule.c                 |  1 +
 t/helper/test-run-command.c | 31 ++++++++++++++++++---
 t/t0061-run-command.sh      | 30 +++++++++++++++++++++
 8 files changed, 129 insertions(+), 8 deletions(-)

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 0b90de87c7..d8e798dc69 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1757,6 +1757,7 @@ static int fetch_multiple(struct string_list *list, int max_children)
 		result = run_processes_parallel_tr2(max_children,
 						    &fetch_next_remote,
 						    &fetch_failed_to_start,
+						    NULL,
 						    &fetch_finished,
 						    &state,
 						    "fetch", "parallel/fetch");
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 9d505a6329..14f6e4ee8c 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -2294,7 +2294,7 @@ static int update_submodules(struct submodule_update_clone *suc)
 	int i;
 
 	run_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,
-				   update_clone_start_failure,
+				   update_clone_start_failure, NULL,
 				   update_clone_task_finished, suc, "submodule",
 				   "parallel/update");
 
diff --git a/hook.c b/hook.c
index 67ad3aa747..9088b520f3 100644
--- a/hook.c
+++ b/hook.c
@@ -400,6 +400,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
 	run_processes_parallel_tr2(options->jobs,
 				   pick_next_hook,
 				   notify_start_failure,
+				   NULL,
 				   notify_hook_finished,
 				   &cb_data,
 				   "hook",
diff --git a/run-command.c b/run-command.c
index e6d7541b84..e7eeb6c49b 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1558,6 +1558,7 @@ struct parallel_processes {
 
 	get_next_task_fn get_next_task;
 	start_failure_fn start_failure;
+	feed_pipe_fn feed_pipe;
 	task_finished_fn task_finished;
 
 	struct {
@@ -1585,6 +1586,13 @@ static int default_start_failure(struct strbuf *out,
 	return 0;
 }
 
+static int default_feed_pipe(struct strbuf *pipe,
+			     void *pp_cb,
+			     void *pp_task_cb)
+{
+	return 1;
+}
+
 static int default_task_finished(int result,
 				 struct strbuf *out,
 				 void *pp_cb,
@@ -1615,6 +1623,7 @@ static void pp_init(struct parallel_processes *pp,
 		    int n,
 		    get_next_task_fn get_next_task,
 		    start_failure_fn start_failure,
+		    feed_pipe_fn feed_pipe,
 		    task_finished_fn task_finished,
 		    void *data)
 {
@@ -1633,6 +1642,7 @@ static void pp_init(struct parallel_processes *pp,
 	pp->get_next_task = get_next_task;
 
 	pp->start_failure = start_failure ? start_failure : default_start_failure;
+	pp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;
 	pp->task_finished = task_finished ? task_finished : default_task_finished;
 
 	pp->nr_processes = 0;
@@ -1730,6 +1740,37 @@ static int pp_start_one(struct parallel_processes *pp)
 	return 0;
 }
 
+static void pp_buffer_stdin(struct parallel_processes *pp)
+{
+	int i;
+	struct strbuf sb = STRBUF_INIT;
+
+	/* Buffer stdin for each pipe. */
+	for (i = 0; i < pp->max_processes; i++) {
+		if (pp->children[i].state == GIT_CP_WORKING &&
+		    pp->children[i].process.in > 0) {
+			int done;
+			strbuf_reset(&sb);
+			done = pp->feed_pipe(&sb, pp->data,
+					      pp->children[i].data);
+			if (sb.len) {
+				if (write_in_full(pp->children[i].process.in,
+					      sb.buf, sb.len) < 0) {
+					if (errno != EPIPE)
+						die_errno("write");
+					done = 1;
+				}
+			}
+			if (done) {
+				close(pp->children[i].process.in);
+				pp->children[i].process.in = 0;
+			}
+		}
+	}
+
+	strbuf_release(&sb);
+}
+
 static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)
 {
 	int i;
@@ -1794,6 +1835,7 @@ static int pp_collect_finished(struct parallel_processes *pp)
 		pp->nr_processes--;
 		pp->children[i].state = GIT_CP_FREE;
 		pp->pfd[i].fd = -1;
+		pp->children[i].process.in = 0;
 		child_process_init(&pp->children[i].process);
 
 		if (i != pp->output_owner) {
@@ -1827,6 +1869,7 @@ static int pp_collect_finished(struct parallel_processes *pp)
 int run_processes_parallel(int n,
 			   get_next_task_fn get_next_task,
 			   start_failure_fn start_failure,
+			   feed_pipe_fn feed_pipe,
 			   task_finished_fn task_finished,
 			   void *pp_cb)
 {
@@ -1835,7 +1878,9 @@ int run_processes_parallel(int n,
 	int spawn_cap = 4;
 	struct parallel_processes pp;
 
-	pp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb);
+	sigchain_push(SIGPIPE, SIG_IGN);
+
+	pp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);
 	while (1) {
 		for (i = 0;
 		    i < spawn_cap && !pp.shutdown &&
@@ -1852,6 +1897,7 @@ int run_processes_parallel(int n,
 		}
 		if (!pp.nr_processes)
 			break;
+		pp_buffer_stdin(&pp);
 		pp_buffer_stderr(&pp, output_timeout);
 		pp_output(&pp);
 		code = pp_collect_finished(&pp);
@@ -1863,11 +1909,15 @@ int run_processes_parallel(int n,
 	}
 
 	pp_cleanup(&pp);
+
+	sigchain_pop(SIGPIPE);
+
 	return 0;
 }
 
 int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,
 			       start_failure_fn start_failure,
+			       feed_pipe_fn feed_pipe,
 			       task_finished_fn task_finished, void *pp_cb,
 			       const char *tr2_category, const char *tr2_label)
 {
@@ -1877,7 +1927,7 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,
 				   ((n < 1) ? online_cpus() : n));
 
 	result = run_processes_parallel(n, get_next_task, start_failure,
-					task_finished, pp_cb);
+					feed_pipe, task_finished, pp_cb);
 
 	trace2_region_leave(tr2_category, tr2_label, NULL);
 
diff --git a/run-command.h b/run-command.h
index d08414a92e..1e3cf0999f 100644
--- a/run-command.h
+++ b/run-command.h
@@ -443,6 +443,20 @@ typedef int (*start_failure_fn)(struct strbuf *out,
 				void *pp_cb,
 				void *pp_task_cb);
 
+/**
+ * This callback is called repeatedly on every child process who requests
+ * start_command() to create a pipe by setting child_process.in < 0.
+ *
+ * pp_cb is the callback cookie as passed into run_processes_parallel, and
+ * pp_task_cb is the callback cookie as passed into get_next_task_fn.
+ * The contents of 'send' will be read into the pipe and passed to the pipe.
+ *
+ * Return nonzero to close the pipe.
+ */
+typedef int (*feed_pipe_fn)(struct strbuf *pipe,
+			    void *pp_cb,
+			    void *pp_task_cb);
+
 /**
  * This callback is called on every child process that finished processing.
  *
@@ -477,10 +491,11 @@ typedef int (*task_finished_fn)(int result,
 int run_processes_parallel(int n,
 			   get_next_task_fn,
 			   start_failure_fn,
+			   feed_pipe_fn,
 			   task_finished_fn,
 			   void *pp_cb);
 int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,
-			       task_finished_fn, void *pp_cb,
+			       feed_pipe_fn, task_finished_fn, void *pp_cb,
 			       const char *tr2_category, const char *tr2_label);
 
 #endif
diff --git a/submodule.c b/submodule.c
index 9767ba9893..dc4a6a60f4 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1644,6 +1644,7 @@ int fetch_populated_submodules(struct repository *r,
 	run_processes_parallel_tr2(max_parallel_jobs,
 				   get_next_submodule,
 				   fetch_start_failure,
+				   NULL,
 				   fetch_finish,
 				   &spf,
 				   "submodule", "parallel/fetch");
diff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c
index 7ae03dc712..9348184d30 100644
--- a/t/helper/test-run-command.c
+++ b/t/helper/test-run-command.c
@@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,
 		return 0;
 
 	strvec_pushv(&cp->args, d->argv);
+	cp->in = d->in;
+	cp->no_stdin = d->no_stdin;
 	strbuf_addstr(err, "preloaded output of a child\n");
 	number_callbacks++;
+
+	*task_cb = xmalloc(sizeof(int));
+	*(int*)(*task_cb) = 2;
 	return 1;
 }
 
@@ -55,6 +60,17 @@ static int task_finished(int result,
 	return 1;
 }
 
+static int test_stdin(struct strbuf *pipe, void *cb, void *task_cb)
+{
+	int *lines_remaining = task_cb;
+
+	if (*lines_remaining)
+		strbuf_addf(pipe, "sample stdin %d\n", --(*lines_remaining));
+
+	return !(*lines_remaining);
+}
+
+
 struct testsuite {
 	struct string_list tests, failed;
 	int next;
@@ -185,7 +201,7 @@ static int testsuite(int argc, const char **argv)
 		suite.tests.nr, max_jobs);
 
 	ret = run_processes_parallel(max_jobs, next_test, test_failed,
-				     test_finished, &suite);
+				     test_stdin, test_finished, &suite);
 
 	if (suite.failed.nr > 0) {
 		ret = 1;
@@ -413,15 +429,22 @@ int cmd__run_command(int argc, const char **argv)
 
 	if (!strcmp(argv[1], "run-command-parallel"))
 		exit(run_processes_parallel(jobs, parallel_next,
-					    NULL, NULL, &proc));
+					    NULL, NULL, NULL, &proc));
 
 	if (!strcmp(argv[1], "run-command-abort"))
 		exit(run_processes_parallel(jobs, parallel_next,
-					    NULL, task_finished, &proc));
+					    NULL, NULL, task_finished, &proc));
 
 	if (!strcmp(argv[1], "run-command-no-jobs"))
 		exit(run_processes_parallel(jobs, no_job,
-					    NULL, task_finished, &proc));
+					    NULL, NULL, task_finished, &proc));
+
+	if (!strcmp(argv[1], "run-command-stdin")) {
+		proc.in = -1;
+		proc.no_stdin = 0;
+		exit (run_processes_parallel(jobs, parallel_next, NULL,
+					     test_stdin, NULL, &proc));
+	}
 
 	fprintf(stderr, "check usage\n");
 	return 1;
diff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh
index 7d599675e3..87759482ad 100755
--- a/t/t0061-run-command.sh
+++ b/t/t0061-run-command.sh
@@ -143,6 +143,36 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai
 	test_cmp expect actual
 '
 
+cat >expect <<-EOF
+preloaded output of a child
+listening for stdin:
+sample stdin 1
+sample stdin 0
+preloaded output of a child
+listening for stdin:
+sample stdin 1
+sample stdin 0
+preloaded output of a child
+listening for stdin:
+sample stdin 1
+sample stdin 0
+preloaded output of a child
+listening for stdin:
+sample stdin 1
+sample stdin 0
+EOF
+
+test_expect_success 'run_command listens to stdin' '
+	write_script stdin-script <<-\EOF &&
+	echo "listening for stdin:"
+	while read line; do
+		echo "$line"
+	done
+	EOF
+	test-tool run-command run-command-stdin 2 ./stdin-script 2>actual &&
+	test_cmp expect actual
+'
+
 cat >expect <<-EOF
 preloaded output of a child
 asking for a quick stop
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 15/37] hook: provide stdin by string_list or callback
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (13 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 14/37] run-command: add stdin callback for parallelization Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 16/37] run-command: allow capturing of collated output Emily Shaffer
                   ` (25 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

In cases where a hook requires only a small amount of information via
stdin, it should be simple for users to provide a string_list alone. But
in more complicated cases where the stdin is too large to hold in
memory, let's provide a callback the users can populate line after line
with instead.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 hook.c | 35 ++++++++++++++++++++++++++++++++++-
 hook.h | 28 ++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 1 deletion(-)

diff --git a/hook.c b/hook.c
index 9088b520f3..a509d2d80e 100644
--- a/hook.c
+++ b/hook.c
@@ -9,6 +9,7 @@ void free_hook(struct hook *ptr)
 {
 	if (ptr) {
 		strbuf_release(&ptr->command);
+		free(ptr->feed_pipe_cb_data);
 		free(ptr);
 	}
 }
@@ -39,6 +40,7 @@ static void append_or_move_hook(struct list_head *head, const char *command)
 		strbuf_init(&to_add->command, 0);
 		strbuf_addstr(&to_add->command, command);
 		to_add->from_hookdir = 0;
+		to_add->feed_pipe_cb_data = NULL;
 	}
 
 	/* re-set the scope so we show where an override was specified */
@@ -252,6 +254,8 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)
 	o->run_hookdir = configured_hookdir_opt();
 	o->jobs = 1;
 	o->dir = NULL;
+	o->feed_pipe = NULL;
+	o->feed_pipe_ctx = NULL;
 }
 
 void run_hooks_opt_init_async(struct run_hooks_opt *o)
@@ -285,6 +289,28 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)
 	strvec_clear(&o->args);
 }
 
+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)
+{
+	int *item_idx;
+	struct hook *ctx = pp_task_cb;
+	struct string_list *to_pipe = ((struct hook_cb_data*)pp_cb)->options->feed_pipe_ctx;
+
+	/* Bootstrap the state manager if necessary. */
+	if (!ctx->feed_pipe_cb_data) {
+		ctx->feed_pipe_cb_data = xmalloc(sizeof(unsigned int));
+		*(int*)ctx->feed_pipe_cb_data = 0;
+	}
+
+	item_idx = ctx->feed_pipe_cb_data;
+
+	if (*item_idx < to_pipe->nr) {
+		strbuf_addf(pipe, "%s\n", to_pipe->items[*item_idx].string);
+		(*item_idx)++;
+		return 0;
+	}
+	return 1;
+}
+
 static int pick_next_hook(struct child_process *cp,
 			  struct strbuf *out,
 			  void *pp_cb,
@@ -300,6 +326,10 @@ static int pick_next_hook(struct child_process *cp,
 	if (hook_cb->options->path_to_stdin) {
 		cp->no_stdin = 0;
 		cp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);
+	} else if (hook_cb->options->feed_pipe) {
+		/* ask for start_command() to make a pipe for us */
+		cp->in = -1;
+		cp->no_stdin = 0;
 	} else {
 		cp->no_stdin = 1;
 	}
@@ -379,6 +409,9 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
 	if (!options)
 		BUG("a struct run_hooks_opt must be provided to run_hooks");
 
+	if (options->path_to_stdin && options->feed_pipe)
+		BUG("choose only one method to populate stdin");
+
 	strbuf_addstr(&hookname_str, hookname);
 
 	to_run = hook_list(&hookname_str);
@@ -400,7 +433,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
 	run_processes_parallel_tr2(options->jobs,
 				   pick_next_hook,
 				   notify_start_failure,
-				   NULL,
+				   options->feed_pipe,
 				   notify_hook_finished,
 				   &cb_data,
 				   "hook",
diff --git a/hook.h b/hook.h
index fcd8e99e39..ecf0228a46 100644
--- a/hook.h
+++ b/hook.h
@@ -2,6 +2,7 @@
 #include "list.h"
 #include "strbuf.h"
 #include "strvec.h"
+#include "run-command.h"
 
 struct hook {
 	struct list_head list;
@@ -13,6 +14,12 @@ struct hook {
 	/* The literal command to run. */
 	struct strbuf command;
 	unsigned from_hookdir : 1;
+
+	/*
+	 * Use this to keep state for your feed_pipe_fn if you are using
+	 * run_hooks_opt.feed_pipe. Otherwise, do not touch it.
+	 */
+	void *feed_pipe_cb_data;
 };
 
 /*
@@ -58,14 +65,35 @@ struct run_hooks_opt
 
 	/* Path to file which should be piped to stdin for each hook */
 	const char *path_to_stdin;
+	/*
+	 * Callback and state pointer to ask for more content to pipe to stdin.
+	 * Will be called repeatedly, for each hook. See
+	 * hook.c:pipe_from_stdin() for an example. Keep per-hook state in
+	 * hook.feed_pipe_cb_data (per process). Keep initialization context in
+	 * feed_pipe_ctx (shared by all processes).
+	 *
+	 * See 'pipe_from_string_list()' for info about how to specify a
+	 * string_list as the stdin input instead of writing your own handler.
+	 */
+	feed_pipe_fn feed_pipe;
+	void *feed_pipe_ctx;
 
 	/* Number of threads to parallelize across */
 	int jobs;
 
 	/* Path to initial working directory for subprocess */
 	const char *dir;
+
 };
 
+/*
+ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the
+ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.
+ * This will pipe each string in the list to stdin, separated by newlines.  (Do
+ * not inject your own newlines.)
+ */
+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);
+
 /*
  * Callback provided to feed_pipe_fn and consume_sideband_fn.
  */
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 16/37] run-command: allow capturing of collated output
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (14 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 15/37] hook: provide stdin by string_list or callback Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 17/37] hooks: allow callers to capture output Emily Shaffer
                   ` (24 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Some callers, for example server-side hooks which wish to relay hook
output to clients across a transport, want to capture what would
normally print to stderr and do something else with it. Allow that via a
callback.

By calling the callback regardless of whether there's output available,
we allow clients to send e.g. a keepalive if necessary.

Because we expose a strbuf, not a fd or FILE*, there's no need to create
a temporary pipe or similar - we can just skip the print to stderr and
instead hand it to the caller.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

 builtin/fetch.c             |  2 +-
 builtin/submodule--helper.c |  2 +-
 hook.c                      |  1 +
 run-command.c               | 33 +++++++++++++++++++++++++--------
 run-command.h               | 18 +++++++++++++++++-
 submodule.c                 |  2 +-
 t/helper/test-run-command.c | 25 ++++++++++++++++++++-----
 t/t0061-run-command.sh      |  7 +++++++
 8 files changed, 73 insertions(+), 17 deletions(-)

diff --git a/builtin/fetch.c b/builtin/fetch.c
index d8e798dc69..b6d45f8359 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1757,7 +1757,7 @@ static int fetch_multiple(struct string_list *list, int max_children)
 		result = run_processes_parallel_tr2(max_children,
 						    &fetch_next_remote,
 						    &fetch_failed_to_start,
-						    NULL,
+						    NULL, NULL,
 						    &fetch_finished,
 						    &state,
 						    "fetch", "parallel/fetch");
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 14f6e4ee8c..136e09a016 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -2294,7 +2294,7 @@ static int update_submodules(struct submodule_update_clone *suc)
 	int i;
 
 	run_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,
-				   update_clone_start_failure, NULL,
+				   update_clone_start_failure, NULL, NULL,
 				   update_clone_task_finished, suc, "submodule",
 				   "parallel/update");
 
diff --git a/hook.c b/hook.c
index a509d2d80e..e16b082cbd 100644
--- a/hook.c
+++ b/hook.c
@@ -434,6 +434,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
 				   pick_next_hook,
 				   notify_start_failure,
 				   options->feed_pipe,
+				   NULL,
 				   notify_hook_finished,
 				   &cb_data,
 				   "hook",
diff --git a/run-command.c b/run-command.c
index e7eeb6c49b..36a4edbacf 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1559,6 +1559,7 @@ struct parallel_processes {
 	get_next_task_fn get_next_task;
 	start_failure_fn start_failure;
 	feed_pipe_fn feed_pipe;
+	consume_sideband_fn consume_sideband;
 	task_finished_fn task_finished;
 
 	struct {
@@ -1624,6 +1625,7 @@ static void pp_init(struct parallel_processes *pp,
 		    get_next_task_fn get_next_task,
 		    start_failure_fn start_failure,
 		    feed_pipe_fn feed_pipe,
+		    consume_sideband_fn consume_sideband,
 		    task_finished_fn task_finished,
 		    void *data)
 {
@@ -1644,6 +1646,7 @@ static void pp_init(struct parallel_processes *pp,
 	pp->start_failure = start_failure ? start_failure : default_start_failure;
 	pp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;
 	pp->task_finished = task_finished ? task_finished : default_task_finished;
+	pp->consume_sideband = consume_sideband;
 
 	pp->nr_processes = 0;
 	pp->output_owner = 0;
@@ -1680,7 +1683,10 @@ static void pp_cleanup(struct parallel_processes *pp)
 	 * When get_next_task added messages to the buffer in its last
 	 * iteration, the buffered output is non empty.
 	 */
-	strbuf_write(&pp->buffered_output, stderr);
+	if (pp->consume_sideband)
+		pp->consume_sideband(&pp->buffered_output, pp->data);
+	else
+		strbuf_write(&pp->buffered_output, stderr);
 	strbuf_release(&pp->buffered_output);
 
 	sigchain_pop_common();
@@ -1801,9 +1807,13 @@ static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)
 static void pp_output(struct parallel_processes *pp)
 {
 	int i = pp->output_owner;
+
 	if (pp->children[i].state == GIT_CP_WORKING &&
 	    pp->children[i].err.len) {
-		strbuf_write(&pp->children[i].err, stderr);
+		if (pp->consume_sideband)
+			pp->consume_sideband(&pp->children[i].err, pp->data);
+		else
+			strbuf_write(&pp->children[i].err, stderr);
 		strbuf_reset(&pp->children[i].err);
 	}
 }
@@ -1842,11 +1852,15 @@ static int pp_collect_finished(struct parallel_processes *pp)
 			strbuf_addbuf(&pp->buffered_output, &pp->children[i].err);
 			strbuf_reset(&pp->children[i].err);
 		} else {
-			strbuf_write(&pp->children[i].err, stderr);
+			/* Output errors, then all other finished child processes */
+			if (pp->consume_sideband) {
+				pp->consume_sideband(&pp->children[i].err, pp->data);
+				pp->consume_sideband(&pp->buffered_output, pp->data);
+			} else {
+				strbuf_write(&pp->children[i].err, stderr);
+				strbuf_write(&pp->buffered_output, stderr);
+			}
 			strbuf_reset(&pp->children[i].err);
-
-			/* Output all other finished child processes */
-			strbuf_write(&pp->buffered_output, stderr);
 			strbuf_reset(&pp->buffered_output);
 
 			/*
@@ -1870,6 +1884,7 @@ int run_processes_parallel(int n,
 			   get_next_task_fn get_next_task,
 			   start_failure_fn start_failure,
 			   feed_pipe_fn feed_pipe,
+			   consume_sideband_fn consume_sideband,
 			   task_finished_fn task_finished,
 			   void *pp_cb)
 {
@@ -1880,7 +1895,7 @@ int run_processes_parallel(int n,
 
 	sigchain_push(SIGPIPE, SIG_IGN);
 
-	pp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);
+	pp_init(&pp, n, get_next_task, start_failure, feed_pipe, consume_sideband, task_finished, pp_cb);
 	while (1) {
 		for (i = 0;
 		    i < spawn_cap && !pp.shutdown &&
@@ -1918,6 +1933,7 @@ int run_processes_parallel(int n,
 int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,
 			       start_failure_fn start_failure,
 			       feed_pipe_fn feed_pipe,
+			       consume_sideband_fn consume_sideband,
 			       task_finished_fn task_finished, void *pp_cb,
 			       const char *tr2_category, const char *tr2_label)
 {
@@ -1927,7 +1943,8 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,
 				   ((n < 1) ? online_cpus() : n));
 
 	result = run_processes_parallel(n, get_next_task, start_failure,
-					feed_pipe, task_finished, pp_cb);
+					feed_pipe, consume_sideband,
+					task_finished, pp_cb);
 
 	trace2_region_leave(tr2_category, tr2_label, NULL);
 
diff --git a/run-command.h b/run-command.h
index 1e3cf0999f..ebc4a95a94 100644
--- a/run-command.h
+++ b/run-command.h
@@ -457,6 +457,20 @@ typedef int (*feed_pipe_fn)(struct strbuf *pipe,
 			    void *pp_cb,
 			    void *pp_task_cb);
 
+/**
+ * If this callback is provided, instead of collating process output to stderr,
+ * they will be collated into a new pipe. consume_sideband_fn will be called
+ * repeatedly. When output is available on that pipe, it will be contained in
+ * 'output'. But it will be called with an empty 'output' too, to allow for
+ * keepalives or similar operations if necessary.
+ *
+ * pp_cb is the callback cookie as passed into run_processes_parallel.
+ *
+ * Since this callback is provided with the collated output, no task cookie is
+ * provided.
+ */
+typedef void (*consume_sideband_fn)(struct strbuf *output, void *pp_cb);
+
 /**
  * This callback is called on every child process that finished processing.
  *
@@ -492,10 +506,12 @@ int run_processes_parallel(int n,
 			   get_next_task_fn,
 			   start_failure_fn,
 			   feed_pipe_fn,
+			   consume_sideband_fn,
 			   task_finished_fn,
 			   void *pp_cb);
 int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,
-			       feed_pipe_fn, task_finished_fn, void *pp_cb,
+			       feed_pipe_fn, consume_sideband_fn,
+			       task_finished_fn, void *pp_cb,
 			       const char *tr2_category, const char *tr2_label);
 
 #endif
diff --git a/submodule.c b/submodule.c
index dc4a6a60f4..4926642451 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1644,7 +1644,7 @@ int fetch_populated_submodules(struct repository *r,
 	run_processes_parallel_tr2(max_parallel_jobs,
 				   get_next_submodule,
 				   fetch_start_failure,
-				   NULL,
+				   NULL, NULL,
 				   fetch_finish,
 				   &spf,
 				   "submodule", "parallel/fetch");
diff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c
index 9348184d30..d53db6d11c 100644
--- a/t/helper/test-run-command.c
+++ b/t/helper/test-run-command.c
@@ -51,6 +51,16 @@ static int no_job(struct child_process *cp,
 	return 0;
 }
 
+static void test_consume_sideband(struct strbuf *output, void *cb)
+{
+	FILE *sideband;
+
+	sideband = fopen("./sideband", "a");
+
+	strbuf_write(output, sideband);
+	fclose(sideband);
+}
+
 static int task_finished(int result,
 			 struct strbuf *err,
 			 void *pp_cb,
@@ -201,7 +211,7 @@ static int testsuite(int argc, const char **argv)
 		suite.tests.nr, max_jobs);
 
 	ret = run_processes_parallel(max_jobs, next_test, test_failed,
-				     test_stdin, test_finished, &suite);
+				     test_stdin, NULL, test_finished, &suite);
 
 	if (suite.failed.nr > 0) {
 		ret = 1;
@@ -429,23 +439,28 @@ int cmd__run_command(int argc, const char **argv)
 
 	if (!strcmp(argv[1], "run-command-parallel"))
 		exit(run_processes_parallel(jobs, parallel_next,
-					    NULL, NULL, NULL, &proc));
+					    NULL, NULL, NULL, NULL, &proc));
 
 	if (!strcmp(argv[1], "run-command-abort"))
 		exit(run_processes_parallel(jobs, parallel_next,
-					    NULL, NULL, task_finished, &proc));
+					    NULL, NULL, NULL, task_finished, &proc));
 
 	if (!strcmp(argv[1], "run-command-no-jobs"))
 		exit(run_processes_parallel(jobs, no_job,
-					    NULL, NULL, task_finished, &proc));
+					    NULL, NULL, NULL, task_finished, &proc));
 
 	if (!strcmp(argv[1], "run-command-stdin")) {
 		proc.in = -1;
 		proc.no_stdin = 0;
 		exit (run_processes_parallel(jobs, parallel_next, NULL,
-					     test_stdin, NULL, &proc));
+					     test_stdin, NULL, NULL, &proc));
 	}
 
+	if (!strcmp(argv[1], "run-command-sideband"))
+		exit(run_processes_parallel(jobs, parallel_next, NULL, NULL,
+					    test_consume_sideband, NULL,
+					    &proc));
+
 	fprintf(stderr, "check usage\n");
 	return 1;
 }
diff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh
index 87759482ad..e99f6c7f44 100755
--- a/t/t0061-run-command.sh
+++ b/t/t0061-run-command.sh
@@ -143,6 +143,13 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai
 	test_cmp expect actual
 '
 
+test_expect_success 'run_command can divert output' '
+	test_when_finished rm sideband &&
+	test-tool run-command run-command-sideband 3 sh -c "printf \"%s\n%s\n\" Hello World" 2>actual &&
+	test_must_be_empty actual &&
+	test_cmp expect sideband
+'
+
 cat >expect <<-EOF
 preloaded output of a child
 listening for stdin:
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 17/37] hooks: allow callers to capture output
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (15 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 16/37] run-command: allow capturing of collated output Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  9:08   ` Ævar Arnfjörð Bjarmason
  2021-03-11  2:10 ` [PATCH v8 18/37] commit: use config-based hooks Emily Shaffer
                   ` (23 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Some server-side hooks will require capturing output to send over
sideband instead of printing directly to stderr. Expose that capability.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    You can see this in practice in the conversions for some of the push hooks,
    like 'receive-pack'.

 hook.c | 3 ++-
 hook.h | 8 ++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/hook.c b/hook.c
index e16b082cbd..2322720ffe 100644
--- a/hook.c
+++ b/hook.c
@@ -256,6 +256,7 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)
 	o->dir = NULL;
 	o->feed_pipe = NULL;
 	o->feed_pipe_ctx = NULL;
+	o->consume_sideband = NULL;
 }
 
 void run_hooks_opt_init_async(struct run_hooks_opt *o)
@@ -434,7 +435,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
 				   pick_next_hook,
 				   notify_start_failure,
 				   options->feed_pipe,
-				   NULL,
+				   options->consume_sideband,
 				   notify_hook_finished,
 				   &cb_data,
 				   "hook",
diff --git a/hook.h b/hook.h
index ecf0228a46..4ff9999b04 100644
--- a/hook.h
+++ b/hook.h
@@ -78,6 +78,14 @@ struct run_hooks_opt
 	feed_pipe_fn feed_pipe;
 	void *feed_pipe_ctx;
 
+	/*
+	 * Populate this to capture output and prevent it from being printed to
+	 * stderr. This will be passed directly through to
+	 * run_command:run_parallel_processes(). See t/helper/test-run-command.c
+	 * for an example.
+	 */
+	consume_sideband_fn consume_sideband;
+
 	/* Number of threads to parallelize across */
 	int jobs;
 
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 18/37] commit: use config-based hooks
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (16 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 17/37] hooks: allow callers to capture output Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12 10:22   ` Junio C Hamano
  2021-03-11  2:10 ` [PATCH v8 19/37] am: convert applypatch hooks to use config Emily Shaffer
                   ` (22 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

As part of the adoption of config-based hooks, teach run_commit_hook()
to call hook.h instead of run-command.h. This covers 'pre-commit',
'commit-msg', and 'prepare-commit-msg'. Additionally, ask the hook
library - not run-command - whether any hooks will be run, as it's
possible hooks may exist in the config but not the hookdir.

Because all but 'post-commit' hooks are expected to make some state
change, force all but 'post-commit' hook to run in series. 'post-commit'
"is meant primarily for notification, and cannot affect the outcome of
`git commit`," so it is fine to run in parallel.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt                    | 13 +++++++++++
 builtin/commit.c                              | 11 +++++-----
 builtin/merge.c                               |  9 ++++----
 commit.c                                      | 22 ++++++++++++++-----
 commit.h                                      |  3 ++-
 sequencer.c                                   |  7 +++---
 ...3-pre-commit-and-pre-merge-commit-hooks.sh | 17 ++++++++++++--
 7 files changed, 61 insertions(+), 21 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 1f3b57d04d..984fb998b2 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -103,6 +103,8 @@ The default 'pre-commit' hook, when enabled--and with the
 `hooks.allownonascii` config option unset or set to false--prevents
 the use of non-ASCII filenames.
 
+Hooks executed during 'pre-commit' will not be parallelized.
+
 pre-merge-commit
 ~~~~~~~~~~~~~~~~
 
@@ -125,6 +127,8 @@ need to be resolved and the result committed separately (see
 linkgit:git-merge[1]). At that point, this hook will not be executed,
 but the 'pre-commit' hook will, if it is enabled.
 
+Hooks executed during 'pre-merge-commit' will not be parallelized.
+
 prepare-commit-msg
 ~~~~~~~~~~~~~~~~~~
 
@@ -150,6 +154,9 @@ be used as replacement for pre-commit hook.
 The sample `prepare-commit-msg` hook that comes with Git removes the
 help message found in the commented portion of the commit template.
 
+Hooks executed during 'prepare-commit-msg' will not be parallelized, because
+hooks are expected to edit the file containing the commit log message.
+
 commit-msg
 ~~~~~~~~~~
 
@@ -166,6 +173,9 @@ file.
 The default 'commit-msg' hook, when enabled, detects duplicate
 `Signed-off-by` trailers, and aborts the commit if one is found.
 
+Hooks executed during 'commit-msg' will not be parallelized, because hooks are
+expected to edit the file containing the proposed commit log message.
+
 post-commit
 ~~~~~~~~~~~
 
@@ -175,6 +185,9 @@ invoked after a commit is made.
 This hook is meant primarily for notification, and cannot affect
 the outcome of `git commit`.
 
+Hooks executed during 'post-commit' will run in parallel, unless hook.jobs is
+configured to 1.
+
 pre-rebase
 ~~~~~~~~~~
 
diff --git a/builtin/commit.c b/builtin/commit.c
index 739110c5a7..39f387e8f7 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -36,6 +36,7 @@
 #include "help.h"
 #include "commit-reach.h"
 #include "commit-graph.h"
+#include "hook.h"
 
 static const char * const builtin_commit_usage[] = {
 	N_("git commit [<options>] [--] <pathspec>..."),
@@ -699,7 +700,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 	/* This checks and barfs if author is badly specified */
 	determine_author_info(author_ident);
 
-	if (!no_verify && run_commit_hook(use_editor, index_file, "pre-commit", NULL))
+	if (!no_verify && run_commit_hook(use_editor, 0, index_file, "pre-commit", NULL))
 		return 0;
 
 	if (squash_message) {
@@ -983,7 +984,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 		return 0;
 	}
 
-	if (!no_verify && find_hook("pre-commit")) {
+	if (!no_verify && hook_exists("pre-commit", HOOKDIR_USE_CONFIG)) {
 		/*
 		 * Re-read the index as pre-commit hook could have updated it,
 		 * and write it out as a tree.  We must do this before we invoke
@@ -998,7 +999,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 		return 0;
 	}
 
-	if (run_commit_hook(use_editor, index_file, "prepare-commit-msg",
+	if (run_commit_hook(use_editor, 0, index_file, "prepare-commit-msg",
 			    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))
 		return 0;
 
@@ -1015,7 +1016,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 	}
 
 	if (!no_verify &&
-	    run_commit_hook(use_editor, index_file, "commit-msg", git_path_commit_editmsg(), NULL)) {
+	    run_commit_hook(use_editor, 0, index_file, "commit-msg", git_path_commit_editmsg(), NULL)) {
 		return 0;
 	}
 
@@ -1701,7 +1702,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
 
 	repo_rerere(the_repository, 0);
 	run_auto_maintenance(quiet);
-	run_commit_hook(use_editor, get_index_file(), "post-commit", NULL);
+	run_commit_hook(use_editor, 1, get_index_file(), "post-commit", NULL);
 	if (amend && !no_post_rewrite) {
 		commit_post_rewrite(the_repository, current_head, &oid);
 	}
diff --git a/builtin/merge.c b/builtin/merge.c
index eb00b273e6..33df744ab0 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -43,6 +43,7 @@
 #include "commit-reach.h"
 #include "wt-status.h"
 #include "commit-graph.h"
+#include "hook.h"
 
 #define DEFAULT_TWOHEAD (1<<0)
 #define DEFAULT_OCTOPUS (1<<1)
@@ -837,14 +838,14 @@ static void prepare_to_commit(struct commit_list *remoteheads)
 	struct strbuf msg = STRBUF_INIT;
 	const char *index_file = get_index_file();
 
-	if (!no_verify && run_commit_hook(0 < option_edit, index_file, "pre-merge-commit", NULL))
+	if (!no_verify && run_commit_hook(0 < option_edit, 0, index_file, "pre-merge-commit", NULL))
 		abort_commit(remoteheads, NULL);
 	/*
 	 * Re-read the index as pre-merge-commit hook could have updated it,
 	 * and write it out as a tree.  We must do this before we invoke
 	 * the editor and after we invoke run_status above.
 	 */
-	if (find_hook("pre-merge-commit"))
+	if (hook_exists("pre-merge-commit", HOOKDIR_USE_CONFIG))
 		discard_cache();
 	read_cache_from(index_file);
 	strbuf_addbuf(&msg, &merge_msg);
@@ -865,7 +866,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)
 		append_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);
 	write_merge_heads(remoteheads);
 	write_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);
-	if (run_commit_hook(0 < option_edit, get_index_file(), "prepare-commit-msg",
+	if (run_commit_hook(0 < option_edit, 0, get_index_file(), "prepare-commit-msg",
 			    git_path_merge_msg(the_repository), "merge", NULL))
 		abort_commit(remoteheads, NULL);
 	if (0 < option_edit) {
@@ -873,7 +874,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)
 			abort_commit(remoteheads, NULL);
 	}
 
-	if (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),
+	if (!no_verify && run_commit_hook(0 < option_edit, 0, get_index_file(),
 					  "commit-msg",
 					  git_path_merge_msg(the_repository), NULL))
 		abort_commit(remoteheads, NULL);
diff --git a/commit.c b/commit.c
index 6ccd774841..b72158cb34 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 **);
 
@@ -1681,25 +1682,34 @@ size_t ignore_non_trailer(const char *buf, size_t len)
 	return boc ? len - boc : len - cutoff;
 }
 
-int run_commit_hook(int editor_is_used, const char *index_file,
+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,
 		    const char *name, ...)
 {
-	struct strvec hook_env = STRVEC_INIT;
+	struct run_hooks_opt opt;
 	va_list args;
+	const char *arg;
 	int ret;
 
-	strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
+	run_hooks_opt_init_sync(&opt);
+
+	if (parallelize)
+		opt.jobs = configured_hook_jobs();
+
+	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);
+
+	ret = run_hooks(name, &opt);
+	run_hooks_opt_clear(&opt);
 
 	return ret;
 }
diff --git a/commit.h b/commit.h
index 49c0f50396..abea90a3f9 100644
--- a/commit.h
+++ b/commit.h
@@ -360,7 +360,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)
 int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);
 
 LAST_ARG_MUST_BE_NULL
-int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);
+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,
+		    const char *name, ...);
 
 /* Sign a commit or tag buffer, storing the result in a header. */
 int sign_with_header(struct strbuf *buf, const char *keyid);
diff --git a/sequencer.c b/sequencer.c
index d2332d3e17..e3a951fbeb 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -34,6 +34,7 @@
 #include "commit-reach.h"
 #include "rebase-interactive.h"
 #include "reset.h"
+#include "hook.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -1206,7 +1207,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,
 	} else {
 		arg1 = "message";
 	}
-	if (run_commit_hook(0, r->index_file, "prepare-commit-msg", name,
+	if (run_commit_hook(0, 0, r->index_file, "prepare-commit-msg", name,
 			    arg1, arg2, NULL))
 		ret = error(_("'prepare-commit-msg' hook failed"));
 
@@ -1444,7 +1445,7 @@ static int try_to_commit(struct repository *r,
 		}
 	}
 
-	if (find_hook("prepare-commit-msg")) {
+	if (hook_exists("prepare-commit-msg", HOOKDIR_USE_CONFIG)) {
 		res = run_prepare_commit_msg_hook(r, msg, hook_commit);
 		if (res)
 			goto out;
@@ -1536,7 +1537,7 @@ static int try_to_commit(struct repository *r,
 		goto out;
 	}
 
-	run_commit_hook(0, r->index_file, "post-commit", NULL);
+	run_commit_hook(0, 1, r->index_file, "post-commit", NULL);
 	if (flags & AMEND_MSG)
 		commit_post_rewrite(r, current_head, oid);
 
diff --git a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh
index 606d8d0f08..e9e3713033 100755
--- a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh
+++ b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh
@@ -8,8 +8,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 . ./test-lib.sh
 
 HOOKDIR="$(git rev-parse --git-dir)/hooks"
-PRECOMMIT="$HOOKDIR/pre-commit"
-PREMERGE="$HOOKDIR/pre-merge-commit"
+PRECOMMIT="$(pwd)/$HOOKDIR/pre-commit"
+PREMERGE="$(pwd)/$HOOKDIR/pre-merge-commit"
 
 # Prepare sample scripts that write their $0 to actual_hooks
 test_expect_success 'sample script setup' '
@@ -106,6 +106,19 @@ test_expect_success 'with succeeding hook' '
 	test_cmp expected_hooks actual_hooks
 '
 
+# NEEDSWORK: when 'git hook add' and 'git hook remove' have been added, use that
+# instead
+test_expect_success 'with succeeding hook (config-based)' '
+	test_when_finished "git config --unset hook.pre-commit.command success.sample" &&
+	test_when_finished "rm -f expected_hooks actual_hooks" &&
+	git config hook.pre-commit.command "$HOOKDIR/success.sample" &&
+	echo "$HOOKDIR/success.sample" >expected_hooks &&
+	echo "more" >>file &&
+	git add file &&
+	git commit -m "more" &&
+	test_cmp expected_hooks actual_hooks
+'
+
 test_expect_success 'with succeeding hook (merge)' '
 	test_when_finished "rm -f \"$PREMERGE\" expected_hooks actual_hooks" &&
 	cp "$HOOKDIR/success.sample" "$PREMERGE" &&
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 19/37] am: convert applypatch hooks to use config
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (17 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 18/37] commit: use config-based hooks Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12 10:23   ` Junio C Hamano
  2021-03-11  2:10 ` [PATCH v8 20/37] merge: use config-based hooks for post-merge hook Emily Shaffer
                   ` (21 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Teach pre-applypatch, post-applypatch, and applypatch-msg to use the
hook.h library instead of the run-command.h library. This enables use of
hooks specified in the config, in addition to those in the hookdir.
These three hooks are called only by builtin/am.c.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt |  9 +++++++++
 builtin/am.c               | 14 +++++++++++---
 2 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 984fb998b2..0e7eb972ab 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -58,6 +58,9 @@ the message file.
 The default 'applypatch-msg' hook, when enabled, runs the
 'commit-msg' hook, if the latter is enabled.
 
+Hooks run during 'applypatch-msg' will not be parallelized, because hooks are
+expected to edit the file holding the commit log message.
+
 pre-applypatch
 ~~~~~~~~~~~~~~
 
@@ -73,6 +76,9 @@ make a commit if it does not pass certain test.
 The default 'pre-applypatch' hook, when enabled, runs the
 'pre-commit' hook, if the latter is enabled.
 
+Hooks run during 'pre-applypatch' will be run in parallel, unless hook.jobs is
+configured to 1.
+
 post-applypatch
 ~~~~~~~~~~~~~~~
 
@@ -82,6 +88,9 @@ and is invoked after the patch is applied and a commit is made.
 This hook is meant primarily for notification, and cannot affect
 the outcome of `git am`.
 
+Hooks run during 'post-applypatch' will be run in parallel, unless hook.jobs is
+configured to 1.
+
 pre-commit
 ~~~~~~~~~~
 
diff --git a/builtin/am.c b/builtin/am.c
index 8355e3566f..4467fd9e63 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -33,6 +33,7 @@
 #include "string-list.h"
 #include "packfile.h"
 #include "repository.h"
+#include "hook.h"
 
 /**
  * Returns the length of the first line of msg.
@@ -426,9 +427,13 @@ 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_sync(&opt);
 
 	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("applypatch-msg", &opt);
+	run_hooks_opt_clear(&opt);
 
 	if (!ret) {
 		FREE_AND_NULL(state->msg);
@@ -1558,8 +1563,10 @@ static void do_commit(const struct am_state *state)
 	struct commit_list *parents = NULL;
 	const char *reflog_msg, *author, *committer = NULL;
 	struct strbuf sb = STRBUF_INIT;
+	struct run_hooks_opt hook_opt;
+	run_hooks_opt_init_async(&hook_opt);
 
-	if (run_hook_le(NULL, "pre-applypatch", NULL))
+	if (run_hooks("pre-applypatch", &hook_opt))
 		exit(1);
 
 	if (write_cache_as_tree(&tree, 0, NULL))
@@ -1611,8 +1618,9 @@ static void do_commit(const struct am_state *state)
 		fclose(fp);
 	}
 
-	run_hook_le(NULL, "post-applypatch", NULL);
+	run_hooks("post-applypatch", &hook_opt);
 
+	run_hooks_opt_clear(&hook_opt);
 	strbuf_release(&sb);
 }
 
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 20/37] merge: use config-based hooks for post-merge hook
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (18 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 19/37] am: convert applypatch hooks to use config Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 21/37] gc: use hook library for pre-auto-gc hook Emily Shaffer
                   ` (20 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Teach post-merge to use the hook.h library instead of the run-command.h
library to run hooks. This means that post-merge hooks can come from the
config as well as from the hookdir. post-merge is invoked only from
builtin/merge.c.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt | 3 +++
 builtin/merge.c            | 6 +++++-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 0e7eb972ab..664ad4803e 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -242,6 +242,9 @@ save and restore any form of metadata associated with the working tree
 (e.g.: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl
 for an example of how to do this.
 
+Hooks executed during 'post-merge' will run in parallel, unless hook.jobs is
+configured to 1.
+
 pre-push
 ~~~~~~~~
 
diff --git a/builtin/merge.c b/builtin/merge.c
index 33df744ab0..b473c8c5d3 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -444,7 +444,9 @@ 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;
 	const struct object_id *head = &head_commit->object.oid;
+	run_hooks_opt_init_async(&opt);
 
 	if (!msg)
 		strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION"));
@@ -485,7 +487,9 @@ 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("post-merge", &opt);
+	run_hooks_opt_clear(&opt);
 
 	apply_autostash(git_path_merge_autostash(the_repository));
 	strbuf_release(&reflog_message);
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 21/37] gc: use hook library for pre-auto-gc hook
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (19 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 20/37] merge: use config-based hooks for post-merge hook Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 22/37] rebase: teach pre-rebase to use hook.h Emily Shaffer
                   ` (19 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Using the hook.h library instead of the run-command.h library to run
pre-auto-gc means that those hooks can be set up in config files, as
well as in the hookdir. pre-auto-gc is called only from builtin/gc.c.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt | 3 +++
 builtin/gc.c               | 5 ++++-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 664ad4803e..00f88912cd 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -560,6 +560,9 @@ This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It
 takes no parameter, and exiting with non-zero status from this script
 causes the `git gc --auto` to abort.
 
+Hooks run during 'pre-auto-gc' will be run in parallel, unless hook.jobs is
+configured to 1.
+
 post-rewrite
 ~~~~~~~~~~~~
 
diff --git a/builtin/gc.c b/builtin/gc.c
index ef7226d7bc..e62cb510ee 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"
 
@@ -348,6 +349,8 @@ static void add_repack_incremental_option(void)
 
 static int need_to_gc(void)
 {
+	struct run_hooks_opt hook_opt;
+	run_hooks_opt_init_async(&hook_opt);
 	/*
 	 * Setting gc.auto to 0 or negative can disable the
 	 * automatic gc.
@@ -394,7 +397,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", &hook_opt))
 		return 0;
 	return 1;
 }
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 22/37] rebase: teach pre-rebase to use hook.h
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (20 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 21/37] gc: use hook library for pre-auto-gc hook Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12 10:24   ` Junio C Hamano
  2021-03-11  2:10 ` [PATCH v8 23/37] read-cache: convert post-index-change hook to use config Emily Shaffer
                   ` (18 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using hook.h instead of run-command.h to run hooks, pre-rebase hooks
can now be specified in the config as well as in the hookdir. pre-rebase
is not called anywhere besides builtin/rebase.c.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt | 3 +++
 builtin/rebase.c           | 9 +++++++--
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 00f88912cd..e3a0375827 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -206,6 +206,9 @@ two parameters.  The first parameter is the upstream from which
 the series was forked.  The second parameter is the branch being
 rebased, and is not set when rebasing the current branch.
 
+Hooks executed during 'pre-rebase' will run in parallel, unless hook.jobs is
+configured to 1.
+
 post-checkout
 ~~~~~~~~~~~~~
 
diff --git a/builtin/rebase.c b/builtin/rebase.c
index de400f9a19..c35b5ba452 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"
 
@@ -1318,6 +1319,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;
 	struct option builtin_rebase_options[] = {
 		OPT_STRING(0, "onto", &options.onto_name,
 			   N_("revision"),
@@ -1431,6 +1433,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	};
 	int i;
 
+	run_hooks_opt_init_async(&hook_opt);
+
 	if (argc == 2 && !strcmp(argv[1], "-h"))
 		usage_with_options(builtin_rebase_usage,
 				   builtin_rebase_options);
@@ -2032,9 +2036,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	}
 
 	/* If a hook exists, give it a chance to interrupt*/
+	strvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);
 	if (!ok_to_skip_pre_rebase &&
-	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
-			argc ? argv[0] : NULL, NULL))
+	    run_hooks("pre-rebase", &hook_opt))
 		die(_("The pre-rebase hook refused to rebase."));
 
 	if (options.flags & REBASE_DIFFSTAT) {
@@ -2114,6 +2118,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 	ret = !!run_specific_rebase(&options, action);
 
 cleanup:
+	run_hooks_opt_clear(&hook_opt);
 	strbuf_release(&buf);
 	strbuf_release(&revisions);
 	free(options.head_name);
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 23/37] read-cache: convert post-index-change hook to use config
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (21 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 22/37] rebase: teach pre-rebase to use hook.h Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12 10:22   ` Junio C Hamano
  2021-03-11  2:10 ` [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h Emily Shaffer
                   ` (17 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using hook.h instead of run-command.h to run, post-index-change hooks
can now be specified in the config in addition to the hookdir.
post-index-change is not run anywhere besides in read-cache.c.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt |  3 +++
 read-cache.c               | 13 ++++++++++---
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index e3a0375827..e5c2cef271 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -720,6 +720,9 @@ 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.
 
+Hooks run during 'post-index-change' will be run in parallel, unless hook.jobs
+is configured to 1.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/read-cache.c b/read-cache.c
index 1e9a50c6c7..fd6c111372 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -25,6 +25,7 @@
 #include "fsmonitor.h"
 #include "thread-utils.h"
 #include "progress.h"
+#include "hook.h"
 
 /* Mask for the name length in ce_flags in the on-disk index */
 
@@ -3070,6 +3071,8 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
 				 unsigned flags)
 {
 	int ret;
+	struct run_hooks_opt hook_opt;
+	run_hooks_opt_init_async(&hook_opt);
 
 	/*
 	 * TODO trace2: replace "the_repository" with the actual repo instance
@@ -3088,9 +3091,13 @@ 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("post-index-change", &hook_opt);
+	run_hooks_opt_clear(&hook_opt);
+
 	istate->updated_workdir = 0;
 	istate->updated_skipworktree = 0;
 
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (22 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 23/37] read-cache: convert post-index-change hook to use config Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12 10:24   ` Junio C Hamano
  2021-03-11  2:10 ` [PATCH v8 25/37] git-p4: use 'git hook' to run hooks Emily Shaffer
                   ` (16 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using hook.h instead of run-command.h to invoke push-to-checkout,
hooks can now be specified in the config as well as in the hookdir.
push-to-checkout is not called anywhere but in builtin/receive-pack.c.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt |  1 +
 builtin/receive-pack.c     | 16 ++++++++++++----
 2 files changed, 13 insertions(+), 4 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index e5c2cef271..f2178dbc83 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -555,6 +555,7 @@ that switches branches while
 keeping the local changes in the working tree that do not interfere
 with the difference between the branches.
 
+Hooks executed during 'push-to-checkout' will not be parallelized.
 
 pre-auto-gc
 ~~~~~~~~~~~
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index d26040c477..234b70f0d1 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -29,6 +29,7 @@
 #include "commit-reach.h"
 #include "worktree.h"
 #include "shallow.h"
+#include "hook.h"
 
 static const char * const receive_pack_usage[] = {
 	N_("git receive-pack <git-dir>"),
@@ -1435,12 +1436,19 @@ 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_sync(&opt);
+
 	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(push_to_checkout_hook, &opt)) {
+		run_hooks_opt_clear(&opt);
 		return "push-to-checkout hook declined";
-	else
+	} else {
+		run_hooks_opt_clear(&opt);
 		return NULL;
+	}
 }
 
 static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree)
@@ -1464,7 +1472,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w
 
 	strvec_pushf(&env, "GIT_DIR=%s", absolute_path(git_dir));
 
-	if (!find_hook(push_to_checkout_hook))
+	if (!hook_exists(push_to_checkout_hook, HOOKDIR_USE_CONFIG))
 		retval = push_to_deploy(sha1, &env, work_tree);
 	else
 		retval = push_to_checkout(sha1, &env, work_tree);
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 25/37] git-p4: use 'git hook' to run hooks
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (23 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 26/37] hooks: convert 'post-checkout' hook to hook library Emily Shaffer
                   ` (15 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Instead of duplicating the behavior of run-command.h:run_hook_le() in
Python, we can directly call 'git hook run'. As a bonus, this means
git-p4 learns how to find hook specifications from the Git config as
well as from the hookdir.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Maybe there is a better way to do this - I had a hard time getting this to run
    locally, and Python is not my forte, so if anybody has a better approach I'd
    love to just take that patch instead :)
    
    Since v6, removed the developer debug print statements.... :X
    
    Maybe there is a better way to do this - I had a hard time getting this to run
    locally, and Python is not my forte, so if anybody has a better approach I'd
    love to just take that patch instead :)

 git-p4.py | 67 +++++--------------------------------------------------
 1 file changed, 6 insertions(+), 61 deletions(-)

diff --git a/git-p4.py b/git-p4.py
index 09c9e93ac4..4b1c69822c 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -208,70 +208,15 @@ 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):
+    if not cmd:
         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'] +
+	    ["-a" + arg for arg in param] +
+	    [cmd])
 
+    return subprocess.call(args) == 0
 
 def write_pipe(c, stdin):
     if verbose:
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 26/37] hooks: convert 'post-checkout' hook to hook library
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (24 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 25/37] git-p4: use 'git hook' to run hooks Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 27/37] hook: convert 'post-rewrite' hook to config Emily Shaffer
                   ` (14 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using the 'hook.h' library, 'post-checkout' hooks can now be
specified in the config as well as in the hook directory.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt |  2 ++
 builtin/checkout.c         | 19 ++++++++++++++-----
 builtin/clone.c            |  8 ++++++--
 builtin/worktree.c         | 31 +++++++++++++++----------------
 reset.c                    | 16 ++++++++++++----
 5 files changed, 49 insertions(+), 27 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index f2178dbc83..362224a03b 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -231,6 +231,8 @@ This hook can be used to perform repository validity checks, auto-display
 differences from the previous HEAD if different, or set working dir metadata
 properties.
 
+Hooks executed during 'post-checkout' will not be parallelized.
+
 post-merge
 ~~~~~~~~~~
 
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 2d6550bc3c..f287b5e643 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"
@@ -104,13 +105,21 @@ 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;
+	int rc;
+
+	run_hooks_opt_init_sync(&opt);
+
 	/* "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);
+	rc = run_hooks("post-checkout", &opt);
+	run_hooks_opt_clear(&opt);
+	return rc;
 }
 
 static int update_some(const struct object_id *oid, struct strbuf *base,
diff --git a/builtin/clone.c b/builtin/clone.c
index 51e844a2de..52f2a5ecb4 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:
@@ -771,6 +772,8 @@ 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_sync(&hook_opt);
 
 	if (option_no_checkout)
 		return 0;
@@ -816,8 +819,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("post-checkout", &hook_opt);
+	run_hooks_opt_clear(&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 1cd5c2016e..8b06d121e5 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -13,6 +13,7 @@
 #include "utf8.h"
 #include "worktree.h"
 #include "quote.h"
+#include "hook.h"
 
 static const char * const worktree_usage[] = {
 	N_("git worktree add [<options>] <path> [<commit-ish>]"),
@@ -383,22 +384,20 @@ 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_sync(&opt);
+
+		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("post-checkout", &opt);
+
+		run_hooks_opt_clear(&opt);
 	}
 
 	strvec_clear(&child_env);
diff --git a/reset.c b/reset.c
index 2f4fbd07c5..85ee75f7fd 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,17 @@ 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_sync(&opt);
+		strvec_pushl(&opt.args,
+			     oid_to_hex(orig ? orig : &null_oid),
+			     oid_to_hex(oid),
+			     "1",
+			     NULL);
+		run_hooks("post-checkout", &opt);
+		run_hooks_opt_clear(&opt);
+	}
 
 leave_reset_head:
 	strbuf_release(&msg);
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 27/37] hook: convert 'post-rewrite' hook to config
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (25 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 26/37] hooks: convert 'post-checkout' hook to hook library Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 28/37] transport: convert pre-push hook to use config Emily Shaffer
                   ` (13 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using 'hook.h' for 'post-rewrite', we simplify hook invocations by
not needing to put together our own 'struct child_process' and we also
learn to run hooks specified in the config as well as the hook dir.

The signal handling that's being removed by this commit now takes place
in run-command.h:run_processes_parallel(), so it is OK to remove them
here.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt |  3 ++
 builtin/am.c               | 19 +++------
 sequencer.c                | 83 +++++++++++++++++---------------------
 3 files changed, 45 insertions(+), 60 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 362224a03b..544238b381 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -594,6 +594,9 @@ The hook always runs after the automatic note copying (see
 "notes.rewrite.<command>" in linkgit:git-config[1]) has happened, and
 thus has access to these notes.
 
+Hooks run during 'post-rewrite' will be run in parallel, unless hook.jobs is
+configured to 1.
+
 The following command-specific comments apply:
 
 rebase::
diff --git a/builtin/am.c b/builtin/am.c
index 4467fd9e63..45425105e8 100644
--- a/builtin/am.c
+++ b/builtin/am.c
@@ -450,23 +450,16 @@ static int run_applypatch_msg_hook(struct am_state *state)
  */
 static int run_post_rewrite_hook(const struct am_state *state)
 {
-	struct child_process cp = CHILD_PROCESS_INIT;
-	const char *hook = find_hook("post-rewrite");
+	struct run_hooks_opt opt;
 	int ret;
+	run_hooks_opt_init_async(&opt);
 
-	if (!hook)
-		return 0;
-
-	strvec_push(&cp.args, hook);
-	strvec_push(&cp.args, "rebase");
-
-	cp.in = xopen(am_path(state, "rewritten"), O_RDONLY);
-	cp.stdout_to_stderr = 1;
-	cp.trace2_hook_name = "post-rewrite";
+	strvec_push(&opt.args, "rebase");
+	opt.path_to_stdin = am_path(state, "rewritten");
 
-	ret = run_command(&cp);
+	ret = run_hooks("post-rewrite", &opt);
 
-	close(cp.in);
+	run_hooks_opt_clear(&opt);
 	return ret;
 }
 
diff --git a/sequencer.c b/sequencer.c
index e3a951fbeb..8280ba828b 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -35,6 +35,7 @@
 #include "rebase-interactive.h"
 #include "reset.h"
 #include "hook.h"
+#include "string-list.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -1146,33 +1147,29 @@ int update_head_with_reflog(const struct commit *old_head,
 static int run_rewrite_hook(const struct object_id *oldoid,
 			    const struct object_id *newoid)
 {
-	struct child_process proc = CHILD_PROCESS_INIT;
-	const char *argv[3];
+	struct run_hooks_opt opt;
+	struct strbuf tmp = STRBUF_INIT;
+	struct string_list to_stdin = STRING_LIST_INIT_DUP;
 	int code;
-	struct strbuf sb = STRBUF_INIT;
+	run_hooks_opt_init_async(&opt);
 
-	argv[0] = find_hook("post-rewrite");
-	if (!argv[0])
-		return 0;
+	strvec_push(&opt.args, "amend");
 
-	argv[1] = "amend";
-	argv[2] = NULL;
-
-	proc.argv = argv;
-	proc.in = -1;
-	proc.stdout_to_stderr = 1;
-	proc.trace2_hook_name = "post-rewrite";
-
-	code = start_command(&proc);
-	if (code)
-		return code;
-	strbuf_addf(&sb, "%s %s\n", oid_to_hex(oldoid), oid_to_hex(newoid));
-	sigchain_push(SIGPIPE, SIG_IGN);
-	write_in_full(proc.in, sb.buf, sb.len);
-	close(proc.in);
-	strbuf_release(&sb);
-	sigchain_pop(SIGPIPE);
-	return finish_command(&proc);
+	strbuf_addf(&tmp,
+		    "%s %s",
+		    oid_to_hex(oldoid),
+		    oid_to_hex(newoid));
+	string_list_append(&to_stdin, tmp.buf);
+
+	opt.feed_pipe = pipe_from_string_list;
+	opt.feed_pipe_ctx = &to_stdin;
+
+	code = run_hooks("post-rewrite", &opt);
+
+	run_hooks_opt_clear(&opt);
+	strbuf_release(&tmp);
+	string_list_clear(&to_stdin, 0);
+	return code;
 }
 
 void commit_post_rewrite(struct repository *r,
@@ -4325,30 +4322,22 @@ static int pick_commits(struct repository *r,
 		flush_rewritten_pending();
 		if (!stat(rebase_path_rewritten_list(), &st) &&
 				st.st_size > 0) {
-			struct child_process child = CHILD_PROCESS_INIT;
-			const char *post_rewrite_hook =
-				find_hook("post-rewrite");
-
-			child.in = open(rebase_path_rewritten_list(), O_RDONLY);
-			child.git_cmd = 1;
-			strvec_push(&child.args, "notes");
-			strvec_push(&child.args, "copy");
-			strvec_push(&child.args, "--for-rewrite=rebase");
+			struct child_process notes_cp = CHILD_PROCESS_INIT;
+			struct run_hooks_opt hook_opt;
+			run_hooks_opt_init_async(&hook_opt);
+
+			notes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);
+			notes_cp.git_cmd = 1;
+			strvec_push(&notes_cp.args, "notes");
+			strvec_push(&notes_cp.args, "copy");
+			strvec_push(&notes_cp.args, "--for-rewrite=rebase");
 			/* we don't care if this copying failed */
-			run_command(&child);
-
-			if (post_rewrite_hook) {
-				struct child_process hook = CHILD_PROCESS_INIT;
-
-				hook.in = open(rebase_path_rewritten_list(),
-					O_RDONLY);
-				hook.stdout_to_stderr = 1;
-				hook.trace2_hook_name = "post-rewrite";
-				strvec_push(&hook.args, post_rewrite_hook);
-				strvec_push(&hook.args, "rebase");
-				/* we don't care if this hook failed */
-				run_command(&hook);
-			}
+			run_command(&notes_cp);
+
+			hook_opt.path_to_stdin = rebase_path_rewritten_list();
+			strvec_push(&hook_opt.args, "rebase");
+			run_hooks("post-rewrite", &hook_opt);
+			run_hooks_opt_clear(&hook_opt);
 		}
 		apply_autostash(rebase_path_autostash());
 
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 28/37] transport: convert pre-push hook to use config
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (26 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 27/37] hook: convert 'post-rewrite' hook to config Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 29/37] reference-transaction: look for hooks in config Emily Shaffer
                   ` (12 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using the hook.h:run_hooks API, pre-push hooks can be specified in
the config as well as in the hookdir.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt |  3 ++
 transport.c                | 59 +++++++++++---------------------------
 2 files changed, 20 insertions(+), 42 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 544238b381..489c93a7cb 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -279,6 +279,9 @@ If this hook exits with a non-zero status, `git push` will abort without
 pushing anything.  Information about why the push is rejected may be sent
 to the user by writing to standard error.
 
+Hooks executed during 'pre-push' will run in parallel, unless hook.jobs is
+configured to 1.
+
 [[pre-receive]]
 pre-receive
 ~~~~~~~~~~~
diff --git a/transport.c b/transport.c
index b13fab5dc3..286b73881b 100644
--- a/transport.c
+++ b/transport.c
@@ -22,6 +22,7 @@
 #include "protocol.h"
 #include "object-store.h"
 #include "color.h"
+#include "hook.h"
 
 static int transport_use_color = -1;
 static char transport_colors[][COLOR_MAXLEN] = {
@@ -1172,31 +1173,15 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)
 static int run_pre_push_hook(struct transport *transport,
 			     struct ref *remote_refs)
 {
-	int ret = 0, x;
+	int ret = 0;
+	struct run_hooks_opt opt;
+	struct strbuf tmp = STRBUF_INIT;
 	struct ref *r;
-	struct child_process proc = CHILD_PROCESS_INIT;
-	struct strbuf buf;
-	const char *argv[4];
-
-	if (!(argv[0] = find_hook("pre-push")))
-		return 0;
-
-	argv[1] = transport->remote->name;
-	argv[2] = transport->url;
-	argv[3] = NULL;
-
-	proc.argv = argv;
-	proc.in = -1;
-	proc.trace2_hook_name = "pre-push";
-
-	if (start_command(&proc)) {
-		finish_command(&proc);
-		return -1;
-	}
-
-	sigchain_push(SIGPIPE, SIG_IGN);
+	struct string_list to_stdin = STRING_LIST_INIT_DUP;
+	run_hooks_opt_init_async(&opt);
 
-	strbuf_init(&buf, 256);
+	strvec_push(&opt.args, transport->remote->name);
+	strvec_push(&opt.args, transport->url);
 
 	for (r = remote_refs; r; r = r->next) {
 		if (!r->peer_ref) continue;
@@ -1205,30 +1190,20 @@ static int run_pre_push_hook(struct transport *transport,
 		if (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;
 		if (r->status == REF_STATUS_UPTODATE) continue;
 
-		strbuf_reset(&buf);
-		strbuf_addf( &buf, "%s %s %s %s\n",
+		strbuf_reset(&tmp);
+		strbuf_addf(&tmp, "%s %s %s %s",
 			 r->peer_ref->name, oid_to_hex(&r->new_oid),
 			 r->name, oid_to_hex(&r->old_oid));
-
-		if (write_in_full(proc.in, buf.buf, buf.len) < 0) {
-			/* We do not mind if a hook does not read all refs. */
-			if (errno != EPIPE)
-				ret = -1;
-			break;
-		}
+		string_list_append(&to_stdin, tmp.buf);
 	}
 
-	strbuf_release(&buf);
-
-	x = close(proc.in);
-	if (!ret)
-		ret = x;
-
-	sigchain_pop(SIGPIPE);
+	opt.feed_pipe = pipe_from_string_list;
+	opt.feed_pipe_ctx = &to_stdin;
 
-	x = finish_command(&proc);
-	if (!ret)
-		ret = x;
+	ret = run_hooks("pre-push", &opt);
+	run_hooks_opt_clear(&opt);
+	strbuf_release(&tmp);
+	string_list_clear(&to_stdin, 0);
 
 	return ret;
 }
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 29/37] reference-transaction: look for hooks in config
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (27 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 28/37] transport: convert pre-push hook to use config Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 30/37] receive-pack: convert 'update' hook to hook.h Emily Shaffer
                   ` (11 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using the hook.h library, reference-transaction hooks can be
specified in the config instead.

The expected output of the test is not fully updated to reflect the
absolute path of the hook called because the 'update' hook has not yet
been converted to use hook.h.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt       |  3 +++
 refs.c                           | 43 +++++++++++++-------------------
 t/t1416-ref-transaction-hooks.sh |  8 +++---
 3 files changed, 24 insertions(+), 30 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 489c93a7cb..dc8b7111d5 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -530,6 +530,9 @@ The exit status of the hook is ignored for any state except for the
 cause the transaction to be aborted. The hook will not be called with
 "aborted" state in that case.
 
+Hooks run during 'reference-transaction' will be run in parallel, unless
+hook.jobs is configured to 1.
+
 push-to-checkout
 ~~~~~~~~~~~~~~~~
 
diff --git a/refs.c b/refs.c
index a665ed5e10..4fccbac3e6 100644
--- a/refs.c
+++ b/refs.c
@@ -18,6 +18,7 @@
 #include "strvec.h"
 #include "repository.h"
 #include "sigchain.h"
+#include "hook.h"
 
 /*
  * List of all available backends
@@ -2061,47 +2062,37 @@ int ref_update_reject_duplicates(struct string_list *refnames,
 static int run_transaction_hook(struct ref_transaction *transaction,
 				const char *state)
 {
-	struct child_process proc = CHILD_PROCESS_INIT;
 	struct strbuf buf = STRBUF_INIT;
-	const char *hook;
+	struct run_hooks_opt opt;
+	struct string_list to_stdin = STRING_LIST_INIT_DUP;
 	int ret = 0, i;
+	char o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];
 
-	hook = find_hook("reference-transaction");
-	if (!hook)
-		return ret;
-
-	strvec_pushl(&proc.args, hook, state, NULL);
-	proc.in = -1;
-	proc.stdout_to_stderr = 1;
-	proc.trace2_hook_name = "reference-transaction";
+	run_hooks_opt_init_async(&opt);
 
-	ret = start_command(&proc);
-	if (ret)
+	if (!hook_exists("reference-transaction", HOOKDIR_USE_CONFIG))
 		return ret;
 
-	sigchain_push(SIGPIPE, SIG_IGN);
+	strvec_push(&opt.args, state);
 
 	for (i = 0; i < transaction->nr; i++) {
 		struct ref_update *update = transaction->updates[i];
+		oid_to_hex_r(o, &update->old_oid);
+		oid_to_hex_r(n, &update->new_oid);
 
 		strbuf_reset(&buf);
-		strbuf_addf(&buf, "%s %s %s\n",
-			    oid_to_hex(&update->old_oid),
-			    oid_to_hex(&update->new_oid),
-			    update->refname);
-
-		if (write_in_full(proc.in, buf.buf, buf.len) < 0) {
-			if (errno != EPIPE)
-				ret = -1;
-			break;
-		}
+		strbuf_addf(&buf, "%s %s %s", o, n, update->refname);
+		string_list_append(&to_stdin, buf.buf);
 	}
 
-	close(proc.in);
-	sigchain_pop(SIGPIPE);
+	opt.feed_pipe = pipe_from_string_list;
+	opt.feed_pipe_ctx = &to_stdin;
+
+	ret = run_hooks("reference-transaction", &opt);
+	run_hooks_opt_clear(&opt);
 	strbuf_release(&buf);
+	string_list_clear(&to_stdin, 0);
 
-	ret |= finish_command(&proc);
 	return ret;
 }
 
diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh
index 6c941027a8..3a90a59143 100755
--- a/t/t1416-ref-transaction-hooks.sh
+++ b/t/t1416-ref-transaction-hooks.sh
@@ -125,11 +125,11 @@ test_expect_success 'interleaving hook calls succeed' '
 
 	cat >expect <<-EOF &&
 		hooks/update refs/tags/PRE $ZERO_OID $PRE_OID
-		hooks/reference-transaction prepared
-		hooks/reference-transaction committed
+		$(pwd)/target-repo.git/hooks/reference-transaction prepared
+		$(pwd)/target-repo.git/hooks/reference-transaction committed
 		hooks/update refs/tags/POST $ZERO_OID $POST_OID
-		hooks/reference-transaction prepared
-		hooks/reference-transaction committed
+		$(pwd)/target-repo.git/hooks/reference-transaction prepared
+		$(pwd)/target-repo.git/hooks/reference-transaction committed
 	EOF
 
 	git push ./target-repo.git PRE POST &&
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 30/37] receive-pack: convert 'update' hook to hook.h
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (28 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 29/37] reference-transaction: look for hooks in config Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 31/37] proc-receive: acquire hook list from hook.h Emily Shaffer
                   ` (10 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using hook.h to invoke the 'update' hook, now hooks can be specified
in the config in addition to the hookdir.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt       |  3 ++
 builtin/receive-pack.c           | 66 ++++++++++++++++++++++----------
 t/t1416-ref-transaction-hooks.sh |  4 +-
 3 files changed, 50 insertions(+), 23 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index dc8b7111d5..60fd43d1da 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -368,6 +368,9 @@ The default 'update' hook, when enabled--and with
 `hooks.allowunannotated` config option unset or set to false--prevents
 unannotated tags to be pushed.
 
+Hooks executed during 'update' are run in parallel, unless hook.jobs is
+configured to 1.
+
 [[proc-receive]]
 proc-receive
 ~~~~~~~~~~~~
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 234b70f0d1..b34a27a303 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -938,33 +938,57 @@ static int run_receive_hook(struct command *commands,
 	return status;
 }
 
-static int run_update_hook(struct command *cmd)
+static void hook_output_to_sideband(struct strbuf *output, void *cb_data)
 {
-	const char *argv[5];
-	struct child_process proc = CHILD_PROCESS_INIT;
-	int code;
+	int keepalive_active = 0;
 
-	argv[0] = find_hook("update");
-	if (!argv[0])
-		return 0;
+	if (keepalive_in_sec <= 0)
+		use_keepalive = KEEPALIVE_NEVER;
+	if (use_keepalive == KEEPALIVE_ALWAYS)
+		keepalive_active = 1;
 
-	argv[1] = cmd->ref_name;
-	argv[2] = oid_to_hex(&cmd->old_oid);
-	argv[3] = oid_to_hex(&cmd->new_oid);
-	argv[4] = NULL;
+	/* send a keepalive if there is no data to write */
+	if (keepalive_active && !output->len) {
+		static const char buf[] = "0005\1";
+		write_or_die(1, buf, sizeof(buf) - 1);
+		return;
+	}
 
-	proc.no_stdin = 1;
-	proc.stdout_to_stderr = 1;
-	proc.err = use_sideband ? -1 : 0;
-	proc.argv = argv;
-	proc.trace2_hook_name = "update";
+	if (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {
+		const char *first_null = memchr(output->buf, '\0', output->len);
+		if (first_null) {
+			/* The null bit is excluded. */
+			size_t before_null = first_null - output->buf;
+			size_t after_null = output->len - (before_null + 1);
+			keepalive_active = 1;
+			send_sideband(1, 2, output->buf, before_null, use_sideband);
+			send_sideband(1, 2, first_null + 1, after_null, use_sideband);
+
+			return;
+		}
+	}
+
+	send_sideband(1, 2, output->buf, output->len, use_sideband);
+}
+
+static int run_update_hook(struct command *cmd)
+{
+	struct run_hooks_opt opt;
+	int code;
+	run_hooks_opt_init_async(&opt);
+
+	strvec_pushl(&opt.args,
+		     cmd->ref_name,
+		     oid_to_hex(&cmd->old_oid),
+		     oid_to_hex(&cmd->new_oid),
+		     NULL);
 
-	code = start_command(&proc);
-	if (code)
-		return code;
 	if (use_sideband)
-		copy_to_sideband(proc.err, -1, NULL);
-	return finish_command(&proc);
+		opt.consume_sideband = hook_output_to_sideband;
+
+	code = run_hooks("update", &opt);
+	run_hooks_opt_clear(&opt);
+	return code;
 }
 
 static struct command *find_command_by_refname(struct command *list,
diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh
index 3a90a59143..0a3c3e4a86 100755
--- a/t/t1416-ref-transaction-hooks.sh
+++ b/t/t1416-ref-transaction-hooks.sh
@@ -124,10 +124,10 @@ test_expect_success 'interleaving hook calls succeed' '
 	EOF
 
 	cat >expect <<-EOF &&
-		hooks/update refs/tags/PRE $ZERO_OID $PRE_OID
+		$(pwd)/target-repo.git/hooks/update refs/tags/PRE $ZERO_OID $PRE_OID
 		$(pwd)/target-repo.git/hooks/reference-transaction prepared
 		$(pwd)/target-repo.git/hooks/reference-transaction committed
-		hooks/update refs/tags/POST $ZERO_OID $POST_OID
+		$(pwd)/target-repo.git/hooks/update refs/tags/POST $ZERO_OID $POST_OID
 		$(pwd)/target-repo.git/hooks/reference-transaction prepared
 		$(pwd)/target-repo.git/hooks/reference-transaction committed
 	EOF
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 31/37] proc-receive: acquire hook list from hook.h
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (29 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 30/37] receive-pack: convert 'update' hook to hook.h Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 32/37] post-update: use hook.h library Emily Shaffer
                   ` (9 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

The proc-receive hook differs from most other hooks Git invokes because
the hook and the parent Git process engage in bidirectional
communication via stdin/stdout. This bidirectional communication is
unsuitable for multiple hooks, whether they are in series or in
parallel, and is incompatible with run-command.h:run_processes_parallel:

- The proc-receive hook is intended to modify the state of the Git repo.
  From 'git help githooks':
    This [proc-receive] hook is responsible for updating the relevant
    references and reporting the results back to 'receive-pack'.
  This prevents parallelization and implies, at least, specific ordering
  of hook execution.
- The proc-receive hook can reject a push by aborting early with an
  error code. If a former hook ran through the entire push contents
  successfully but a later hook rejects some of the push, the repo may
  be left in a partially-updated (and corrupt) state.
- The callback model of the run_processes_parallel() API is unsuited to
  the current implementation of proc-receive, which loops through
  "send-receive-consider" with the child process. proc-receive today
  relies on stateful communication with the child process, which would be
  unwieldy to implement with callbacks and saved state.
- Additionally, run_processes_parallel() is designed to collate the
  output of many child processes into a single output (stderr or callback),
  and would require significant work to tell the caller which process sent
  the output, and indeed to collect any output before the child process
  has exited.

So, rather than using hook.h:run_hooks() to invoke the proc-receive
hook, receive-pack.c can learn to ask hook.h:hook_list() for the
location of a hook to run. This allows users to configure their
proc-receive in a global config for all repos if they want, or a local
config if they just don't want to use the hookdir. Because running more
than one proc-receive hook doesn't make sense from a repo state
perspective, we can explicitly ban configuring more than one
proc-receive hook at a time.

If a user wants to globally configure one proc-receive hook for most of
their repos, but override that hook in a single repo, they should use
'skip' to manually remove the global hook in their special repo:

~/.gitconfig:
[hook.proc-receive]
  command = /usr/bin/usual-proc-receive

~/special-repo/.git/config:
[hookcmd./usr/bin/usual-proc-receive]
  skip = true
[hook.proc-receive]
  command = /usr/bin/special-proc-receive

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt                |  4 ++
 builtin/receive-pack.c                    | 33 +++++++++++++++-
 t/t5411/test-0015-too-many-hooks-error.sh | 47 +++++++++++++++++++++++
 3 files changed, 82 insertions(+), 2 deletions(-)
 create mode 100644 t/t5411/test-0015-too-many-hooks-error.sh

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index 60fd43d1da..c16353be2d 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -433,6 +433,10 @@ the input.  The exit status of the 'proc-receive' hook only determines
 the success or failure of the group of commands sent to it, unless
 atomic push is in use.
 
+It is forbidden to specify more than one hook for 'proc-receive'. If a
+globally-configured 'proc-receive' must be overridden, use
+'hookcmd.<global-hook>.skip = true' to ignore it.
+
 [[post-receive]]
 post-receive
 ~~~~~~~~~~~~
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index b34a27a303..e448956a32 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1146,11 +1146,40 @@ static int run_proc_receive_hook(struct command *commands,
 	int version = 0;
 	int code;
 
-	argv[0] = find_hook("proc-receive");
-	if (!argv[0]) {
+	struct strbuf hookname = STRBUF_INIT;
+	struct hook *proc_receive = NULL;
+	struct list_head *pos, *hooks;
+
+	strbuf_addstr(&hookname, "proc-receive");
+	hooks = hook_list(&hookname);
+
+	list_for_each(pos, hooks) {
+		if (proc_receive) {
+			rp_error("only one 'proc-receive' hook can be specified");
+			return -1;
+		}
+		proc_receive = list_entry(pos, struct hook, list);
+		/* check if the hookdir hook should be ignored */
+		if (proc_receive->from_hookdir) {
+			switch (configured_hookdir_opt()) {
+			case HOOKDIR_INTERACTIVE:
+			case HOOKDIR_NO:
+				proc_receive = NULL;
+				break;
+			default:
+				break;
+			}
+		}
+
+	}
+
+	if (!proc_receive) {
 		rp_error("cannot find hook 'proc-receive'");
 		return -1;
 	}
+
+
+	argv[0] = proc_receive->command.buf;
 	argv[1] = NULL;
 
 	proc.argv = argv;
diff --git a/t/t5411/test-0015-too-many-hooks-error.sh b/t/t5411/test-0015-too-many-hooks-error.sh
new file mode 100644
index 0000000000..2d64534510
--- /dev/null
+++ b/t/t5411/test-0015-too-many-hooks-error.sh
@@ -0,0 +1,47 @@
+test_expect_success "setup too  many proc-receive hooks (ok, $PROTOCOL)" '
+	write_script "proc-receive" <<-EOF &&
+	printf >&2 "# proc-receive hook\n"
+	test-tool proc-receive -v \
+		-r "ok refs/for/main/topic"
+	EOF
+
+	git -C "$upstream" config --add "hook.proc-receive.command" proc-receive &&
+	cp proc-receive "$upstream/hooks/proc-receive"
+'
+
+# Refs of upstream : main(A)
+# Refs of workbench: main(A)  tags/v123
+# git push         :                       next(A)  refs/for/main/topic(A)
+test_expect_success "proc-receive: reject more than one configured hook" '
+	test_must_fail git -C workbench push origin \
+		HEAD:next \
+		HEAD:refs/for/main/topic \
+		>out 2>&1 &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	remote: # pre-receive hook
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	remote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic
+	remote: error: only one "proc-receive" hook can be specified
+	remote: # post-receive hook
+	remote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next
+	To <URL/of/upstream.git>
+	 * [new branch] HEAD -> next
+	 ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)
+	EOF
+	test_cmp expect actual &&
+	git -C "$upstream" show-ref >out &&
+	make_user_friendly_and_stable_output <out >actual &&
+	cat >expect <<-EOF &&
+	<COMMIT-A> refs/heads/main
+	<COMMIT-A> refs/heads/next
+	EOF
+	test_cmp expect actual
+'
+
+# Refs of upstream : main(A)             next(A)
+# Refs of workbench: main(A)  tags/v123
+test_expect_success "cleanup ($PROTOCOL)" '
+	git -C "$upstream" config --unset "hook.proc-receive.command" "proc-receive" &&
+	git -C "$upstream" update-ref -d refs/heads/next
+'
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 32/37] post-update: use hook.h library
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (30 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 31/37] proc-receive: acquire hook list from hook.h Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  9:14   ` Ævar Arnfjörð Bjarmason
  2021-03-11  2:10 ` [PATCH v8 33/37] receive-pack: convert receive hooks to hook.h Emily Shaffer
                   ` (8 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using run_hooks() instead of run_hook_le(), 'post-update' hooks can
be specified in the config as well as the hookdir.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt |  3 +++
 builtin/receive-pack.c     | 27 ++++++++-------------------
 2 files changed, 11 insertions(+), 19 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index c16353be2d..fe5381b95b 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -508,6 +508,9 @@ Both standard output and standard error output are forwarded to
 `git send-pack` on the other end, so you can simply `echo` messages
 for the user.
 
+Hooks run during 'post-update' will be run in parallel, unless hook.jobs is
+configured to 1.
+
 reference-transaction
 ~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index e448956a32..955efbdf6d 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1688,33 +1688,22 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 static void run_update_post_hook(struct command *commands)
 {
 	struct command *cmd;
-	struct child_process proc = CHILD_PROCESS_INIT;
-	const char *hook;
-
-	hook = find_hook("post-update");
-	if (!hook)
-		return;
+	struct run_hooks_opt opt;
+	run_hooks_opt_init_async(&opt);
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
 		if (cmd->error_string || cmd->did_not_exist)
 			continue;
-		if (!proc.args.nr)
-			strvec_push(&proc.args, hook);
-		strvec_push(&proc.args, cmd->ref_name);
+		strvec_push(&opt.args, cmd->ref_name);
 	}
-	if (!proc.args.nr)
+	if (!opt.args.nr)
 		return;
 
-	proc.no_stdin = 1;
-	proc.stdout_to_stderr = 1;
-	proc.err = use_sideband ? -1 : 0;
-	proc.trace2_hook_name = "post-update";
+	if (use_sideband)
+		opt.consume_sideband = hook_output_to_sideband;
 
-	if (!start_command(&proc)) {
-		if (use_sideband)
-			copy_to_sideband(proc.err, -1, NULL);
-		finish_command(&proc);
-	}
+	run_hooks("post-update", &opt);
+	run_hooks_opt_clear(&opt);
 }
 
 static void check_aliased_update_internal(struct command *cmd,
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 33/37] receive-pack: convert receive hooks to hook.h
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (31 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 32/37] post-update: use hook.h library Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 34/37] bugreport: use hook_exists instead of find_hook Emily Shaffer
                   ` (7 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using the hook.h library to run receive hooks, they can be specified
in the config as well as in the hookdir.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/githooks.txt |   5 +
 builtin/receive-pack.c     | 199 +++++++++++++++++--------------------
 2 files changed, 97 insertions(+), 107 deletions(-)

diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index fe5381b95b..b63054b947 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -323,6 +323,8 @@ will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.
 See the section on "Quarantine Environment" in
 linkgit:git-receive-pack[1] for some caveats.
 
+Hooks executed during 'pre-receive' will not be parallelized.
+
 [[update]]
 update
 ~~~~~~
@@ -476,6 +478,9 @@ environment variables will not be set. If the client selects
 to use push options, but doesn't transmit any, the count variable
 will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.
 
+Hooks executed during 'post-receive' are run in parallel, unless hook.jobs is
+configured to 1.
+
 [[post-update]]
 post-update
 ~~~~~~~~~~~
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 955efbdf6d..d124718d0b 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -748,7 +748,7 @@ static int check_cert_push_options(const struct string_list *push_options)
 	return retval;
 }
 
-static void prepare_push_cert_sha1(struct child_process *proc)
+static void prepare_push_cert_sha1(struct run_hooks_opt *opt)
 {
 	static int already_done;
 
@@ -772,110 +772,42 @@ static void prepare_push_cert_sha1(struct child_process *proc)
 		nonce_status = check_nonce(push_cert.buf, bogs);
 	}
 	if (!is_null_oid(&push_cert_oid)) {
-		strvec_pushf(&proc->env_array, "GIT_PUSH_CERT=%s",
+		strvec_pushf(&opt->env, "GIT_PUSH_CERT=%s",
 			     oid_to_hex(&push_cert_oid));
-		strvec_pushf(&proc->env_array, "GIT_PUSH_CERT_SIGNER=%s",
+		strvec_pushf(&opt->env, "GIT_PUSH_CERT_SIGNER=%s",
 			     sigcheck.signer ? sigcheck.signer : "");
-		strvec_pushf(&proc->env_array, "GIT_PUSH_CERT_KEY=%s",
+		strvec_pushf(&opt->env, "GIT_PUSH_CERT_KEY=%s",
 			     sigcheck.key ? sigcheck.key : "");
-		strvec_pushf(&proc->env_array, "GIT_PUSH_CERT_STATUS=%c",
+		strvec_pushf(&opt->env, "GIT_PUSH_CERT_STATUS=%c",
 			     sigcheck.result);
 		if (push_cert_nonce) {
-			strvec_pushf(&proc->env_array,
+			strvec_pushf(&opt->env,
 				     "GIT_PUSH_CERT_NONCE=%s",
 				     push_cert_nonce);
-			strvec_pushf(&proc->env_array,
+			strvec_pushf(&opt->env,
 				     "GIT_PUSH_CERT_NONCE_STATUS=%s",
 				     nonce_status);
 			if (nonce_status == NONCE_SLOP)
-				strvec_pushf(&proc->env_array,
+				strvec_pushf(&opt->env,
 					     "GIT_PUSH_CERT_NONCE_SLOP=%ld",
 					     nonce_stamp_slop);
 		}
 	}
 }
 
+struct receive_hook_feed_context {
+	struct command *cmd;
+	int skip_broken;
+};
+
 struct receive_hook_feed_state {
 	struct command *cmd;
 	struct ref_push_report *report;
 	int skip_broken;
 	struct strbuf buf;
-	const struct string_list *push_options;
 };
 
-typedef int (*feed_fn)(void *, const char **, size_t *);
-static int run_and_feed_hook(const char *hook_name, feed_fn feed,
-			     struct receive_hook_feed_state *feed_state)
-{
-	struct child_process proc = CHILD_PROCESS_INIT;
-	struct async muxer;
-	const char *argv[2];
-	int code;
-
-	argv[0] = find_hook(hook_name);
-	if (!argv[0])
-		return 0;
-
-	argv[1] = NULL;
-
-	proc.argv = argv;
-	proc.in = -1;
-	proc.stdout_to_stderr = 1;
-	proc.trace2_hook_name = hook_name;
-
-	if (feed_state->push_options) {
-		int i;
-		for (i = 0; i < feed_state->push_options->nr; i++)
-			strvec_pushf(&proc.env_array,
-				     "GIT_PUSH_OPTION_%d=%s", i,
-				     feed_state->push_options->items[i].string);
-		strvec_pushf(&proc.env_array, "GIT_PUSH_OPTION_COUNT=%d",
-			     feed_state->push_options->nr);
-	} else
-		strvec_pushf(&proc.env_array, "GIT_PUSH_OPTION_COUNT");
-
-	if (tmp_objdir)
-		strvec_pushv(&proc.env_array, tmp_objdir_env(tmp_objdir));
-
-	if (use_sideband) {
-		memset(&muxer, 0, sizeof(muxer));
-		muxer.proc = copy_to_sideband;
-		muxer.in = -1;
-		code = start_async(&muxer);
-		if (code)
-			return code;
-		proc.err = muxer.in;
-	}
-
-	prepare_push_cert_sha1(&proc);
-
-	code = start_command(&proc);
-	if (code) {
-		if (use_sideband)
-			finish_async(&muxer);
-		return code;
-	}
-
-	sigchain_push(SIGPIPE, SIG_IGN);
-
-	while (1) {
-		const char *buf;
-		size_t n;
-		if (feed(feed_state, &buf, &n))
-			break;
-		if (write_in_full(proc.in, buf, n) < 0)
-			break;
-	}
-	close(proc.in);
-	if (use_sideband)
-		finish_async(&muxer);
-
-	sigchain_pop(SIGPIPE);
-
-	return finish_command(&proc);
-}
-
-static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
+static int feed_receive_hook(void *state_)
 {
 	struct receive_hook_feed_state *state = state_;
 	struct command *cmd = state->cmd;
@@ -884,9 +816,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 	       state->skip_broken && (cmd->error_string || cmd->did_not_exist))
 		cmd = cmd->next;
 	if (!cmd)
-		return -1; /* EOF */
-	if (!bufp)
-		return 0; /* OK, can feed something. */
+		return 1; /* EOF - close the pipe*/
 	strbuf_reset(&state->buf);
 	if (!state->report)
 		state->report = cmd->report;
@@ -910,32 +840,36 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
 			    cmd->ref_name);
 		state->cmd = cmd->next;
 	}
-	if (bufp) {
-		*bufp = state->buf.buf;
-		*sizep = state->buf.len;
-	}
 	return 0;
 }
 
-static int run_receive_hook(struct command *commands,
-			    const char *hook_name,
-			    int skip_broken,
-			    const struct string_list *push_options)
+static int feed_receive_hook_cb(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)
 {
-	struct receive_hook_feed_state state;
-	int status;
-
-	strbuf_init(&state.buf, 0);
-	state.cmd = commands;
-	state.skip_broken = skip_broken;
-	state.report = NULL;
-	if (feed_receive_hook(&state, NULL, NULL))
-		return 0;
-	state.cmd = commands;
-	state.push_options = push_options;
-	status = run_and_feed_hook(hook_name, feed_receive_hook, &state);
-	strbuf_release(&state.buf);
-	return status;
+	struct hook *hook = pp_task_cb;
+	struct receive_hook_feed_state *feed_state = hook->feed_pipe_cb_data;
+	int rc;
+
+	/* first-time setup */
+	if (!feed_state) {
+		struct hook_cb_data *hook_cb = pp_cb;
+		struct run_hooks_opt *opt = hook_cb->options;
+		struct receive_hook_feed_context *ctx = opt->feed_pipe_ctx;
+		if (!ctx)
+			BUG("run_hooks_opt.feed_pipe_ctx required for receive hook");
+
+		feed_state = xmalloc(sizeof(struct receive_hook_feed_state));
+		strbuf_init(&feed_state->buf, 0);
+		feed_state->cmd = ctx->cmd;
+		feed_state->skip_broken = ctx->skip_broken;
+		feed_state->report = NULL;
+
+		hook->feed_pipe_cb_data = feed_state;
+	}
+
+	rc = feed_receive_hook(feed_state);
+	if (!rc)
+		strbuf_addbuf(pipe, &feed_state->buf);
+	return rc;
 }
 
 static void hook_output_to_sideband(struct strbuf *output, void *cb_data)
@@ -971,6 +905,57 @@ static void hook_output_to_sideband(struct strbuf *output, void *cb_data)
 	send_sideband(1, 2, output->buf, output->len, use_sideband);
 }
 
+static int run_receive_hook(struct command *commands,
+			    const char *hook_name,
+			    int skip_broken,
+			    const struct string_list *push_options)
+{
+	struct run_hooks_opt opt;
+	struct receive_hook_feed_context ctx;
+	int rc;
+	struct command *iter = commands;
+
+	run_hooks_opt_init_async(&opt);
+
+	/* if there are no valid commands, don't invoke the hook at all. */
+	while (iter && skip_broken && (iter->error_string || iter->did_not_exist))
+		iter = iter->next;
+	if (!iter)
+		return 0;
+
+	/* pre-receive hooks should run in series as the hook updates refs */
+	if (!strcmp(hook_name, "pre-receive"))
+		opt.jobs = 1;
+
+	if (push_options) {
+		int i;
+		for (i = 0; i < push_options->nr; i++)
+			strvec_pushf(&opt.env, "GIT_PUSH_OPTION_%d=%s", i,
+				     push_options->items[i].string);
+		strvec_pushf(&opt.env, "GIT_PUSH_OPTION_COUNT=%d", push_options->nr);
+	} else
+		strvec_push(&opt.env, "GIT_PUSH_OPTION_COUNT");
+
+	if (tmp_objdir)
+		strvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));
+
+	prepare_push_cert_sha1(&opt);
+
+	/* set up sideband printer */
+	if (use_sideband)
+		opt.consume_sideband = hook_output_to_sideband;
+
+	/* set up stdin callback */
+	ctx.cmd = commands;
+	ctx.skip_broken = skip_broken;
+	opt.feed_pipe = feed_receive_hook_cb;
+	opt.feed_pipe_ctx = &ctx;
+
+	rc = run_hooks(hook_name, &opt);
+	run_hooks_opt_clear(&opt);
+	return rc;
+}
+
 static int run_update_hook(struct command *cmd)
 {
 	struct run_hooks_opt opt;
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 34/37] bugreport: use hook_exists instead of find_hook
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (32 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 33/37] receive-pack: convert receive hooks to hook.h Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate' Emily Shaffer
                   ` (6 subsequent siblings)
  40 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using the helper in hook.h instead of the one in run-command.h, we
can also check whether a hook exists in the config - not just whether it
exists in the hookdir.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 builtin/bugreport.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/builtin/bugreport.c b/builtin/bugreport.c
index ad3cc9c02f..eac3726527 100644
--- a/builtin/bugreport.c
+++ b/builtin/bugreport.c
@@ -3,7 +3,7 @@
 #include "strbuf.h"
 #include "help.h"
 #include "compat/compiler.h"
-#include "run-command.h"
+#include "hook.h"
 
 
 static void get_system_info(struct strbuf *sys_info)
@@ -82,7 +82,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)
 	}
 
 	for (i = 0; i < ARRAY_SIZE(hook); i++)
-		if (find_hook(hook[i]))
+		if (hook_exists(hook[i], HOOKDIR_USE_CONFIG))
 			strbuf_addf(hook_info, "%s\n", hook[i]);
 }
 
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (33 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 34/37] bugreport: use hook_exists instead of find_hook Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  9:21   ` Ævar Arnfjörð Bjarmason
  2021-03-12 23:29   ` [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate' Emily Shaffer
  2021-03-11  2:10 ` [PATCH v8 36/37] run-command: stop thinking about hooks Emily Shaffer
                   ` (5 subsequent siblings)
  40 siblings, 2 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By using the new 'git hook run' subcommand to run 'sendemail-validate',
we can reduce the boilerplate needed to run this hook in perl. Using
config-based hooks also allows us to run 'sendemail-validate' hooks that
were configured globally when running 'git send-email' from outside of a
Git directory, alongside other benefits like multihooks and
parallelization.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 git-send-email.perl   | 21 ++++-----------------
 t/t9001-send-email.sh | 11 +----------
 2 files changed, 5 insertions(+), 27 deletions(-)

diff --git a/git-send-email.perl b/git-send-email.perl
index 1f425c0809..73e1e0b51a 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -1941,23 +1941,10 @@ sub unique_email_list {
 sub validate_patch {
 	my ($fn, $xfer_encoding) = @_;
 
-	if ($repo) {
-		my $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),
-					    'sendemail-validate');
-		my $hook_error;
-		if (-x $validate_hook) {
-			my $target = abs_path($fn);
-			# The hook needs a correct cwd and GIT_DIR.
-			my $cwd_save = cwd();
-			chdir($repo->wc_path() or $repo->repo_path())
-				or die("chdir: $!");
-			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			$hook_error = "rejected by sendemail-validate hook"
-				if system($validate_hook, $target);
-			chdir($cwd_save) or die("chdir: $!");
-		}
-		return $hook_error if $hook_error;
-	}
+	my $target = abs_path($fn);
+	return "rejected by sendemail-validate hook"
+		if system(("git", "hook", "run", "sendemail-validate", "-a",
+				$target));
 
 	# Any long lines will be automatically fixed if we use a suitable transfer
 	# encoding.
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index 4eee9c3dcb..456b471c5c 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2101,16 +2101,7 @@ test_expect_success $PREREQ 'invoke hook' '
 	mkdir -p .git/hooks &&
 
 	write_script .git/hooks/sendemail-validate <<-\EOF &&
-	# test that we have the correct environment variable, pwd, and
-	# argument
-	case "$GIT_DIR" in
-	*.git)
-		true
-		;;
-	*)
-		false
-		;;
-	esac &&
+	# test that we have the correct argument
 	test -f 0001-add-main.patch &&
 	grep "add main" "$1"
 	EOF
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 36/37] run-command: stop thinking about hooks
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (34 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate' Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  9:23   ` Ævar Arnfjörð Bjarmason
  2021-03-11  2:10 ` [PATCH v8 37/37] docs: unify githooks and git-hook manpages Emily Shaffer
                   ` (4 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

hook.h has replaced all run-command.h hook-related functionality.
run-command.h:run_hooks_le/ve and find_hook are no longer used anywhere
in the codebase. So, let's delete the dead code - or, in the one case
where it's still needed, move it to an internal function in hook.c.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 hook.c        | 39 ++++++++++++++++++++++++++++--
 run-command.c | 66 ---------------------------------------------------
 run-command.h | 24 -------------------
 3 files changed, 37 insertions(+), 92 deletions(-)

diff --git a/hook.c b/hook.c
index 2322720ffe..7f6f3b9a61 100644
--- a/hook.c
+++ b/hook.c
@@ -212,6 +212,41 @@ static int should_include_hookdir(const char *path, enum hookdir_opt cfg)
 	}
 }
 
+static const char *find_legacy_hook(const char *name)
+{
+	static struct strbuf path = STRBUF_INIT;
+
+	strbuf_reset(&path);
+	strbuf_git_path(&path, "hooks/%s", name);
+	if (access(path.buf, X_OK) < 0) {
+		int err = errno;
+
+#ifdef STRIP_EXTENSION
+		strbuf_addstr(&path, STRIP_EXTENSION);
+		if (access(path.buf, X_OK) >= 0)
+			return path.buf;
+		if (errno == EACCES)
+			err = errno;
+#endif
+
+		if (err == EACCES && advice_ignored_hook) {
+			static struct string_list advise_given = STRING_LIST_INIT_DUP;
+
+			if (!string_list_lookup(&advise_given, name)) {
+				string_list_insert(&advise_given, name);
+				advise(_("The '%s' hook was ignored because "
+					 "it's not set as executable.\n"
+					 "You can disable this warning with "
+					 "`git config advice.ignoredHook false`."),
+				       path.buf);
+			}
+		}
+		return NULL;
+	}
+	return path.buf;
+}
+
+
 struct list_head* hook_list(const struct strbuf* hookname)
 {
 	struct strbuf hook_key = STRBUF_INIT;
@@ -228,7 +263,7 @@ struct list_head* hook_list(const struct strbuf* hookname)
 	git_config(hook_config_lookup, &cb_data);
 
 	if (have_git_dir()) {
-		const char *legacy_hook_path = find_hook(hookname->buf);
+		const char *legacy_hook_path = find_legacy_hook(hookname->buf);
 
 		/* Unconditionally add legacy hook, but annotate it. */
 		if (legacy_hook_path) {
@@ -277,7 +312,7 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)
 	could_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||
 				should_run_hookdir == HOOKDIR_WARN ||
 				should_run_hookdir == HOOKDIR_YES)
-				&& !!find_hook(hookname);
+				&& !!find_legacy_hook(hookname);
 
 	strbuf_addf(&hook_key, "hook.%s.command", hookname);
 
diff --git a/run-command.c b/run-command.c
index 36a4edbacf..837415131d 100644
--- a/run-command.c
+++ b/run-command.c
@@ -1320,72 +1320,6 @@ int async_with_fork(void)
 #endif
 }
 
-const char *find_hook(const char *name)
-{
-	static struct strbuf path = STRBUF_INIT;
-
-	strbuf_reset(&path);
-	strbuf_git_path(&path, "hooks/%s", name);
-	if (access(path.buf, X_OK) < 0) {
-		int err = errno;
-
-#ifdef STRIP_EXTENSION
-		strbuf_addstr(&path, STRIP_EXTENSION);
-		if (access(path.buf, X_OK) >= 0)
-			return path.buf;
-		if (errno == EACCES)
-			err = errno;
-#endif
-
-		if (err == EACCES && advice_ignored_hook) {
-			static struct string_list advise_given = STRING_LIST_INIT_DUP;
-
-			if (!string_list_lookup(&advise_given, name)) {
-				string_list_insert(&advise_given, name);
-				advise(_("The '%s' hook was ignored because "
-					 "it's not set as executable.\n"
-					 "You can disable this warning with "
-					 "`git config advice.ignoredHook false`."),
-				       path.buf);
-			}
-		}
-		return NULL;
-	}
-	return path.buf;
-}
-
-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 ebc4a95a94..7150da851a 100644
--- a/run-command.h
+++ b/run-command.h
@@ -201,30 +201,6 @@ int finish_command_in_signal(struct child_process *);
  */
 int run_command(struct child_process *);
 
-/*
- * Returns the path to the hook file, or NULL if the hook is missing
- * or disabled. Note that this points to static storage that will be
- * overwritten by further calls to find_hook and run_hook_*.
- */
-const char *find_hook(const char *name);
-
-/**
- * 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, ...);
-int run_hook_ve(const char *const *env, const char *name, va_list args);
-
 /*
  * Trigger an auto-gc
  */
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v8 37/37] docs: unify githooks and git-hook manpages
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (35 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 36/37] run-command: stop thinking about hooks Emily Shaffer
@ 2021-03-11  2:10 ` Emily Shaffer
  2021-03-12  9:29   ` Ævar Arnfjörð Bjarmason
  2021-04-07  2:36   ` Junio C Hamano
  2021-03-11 22:26 ` [PATCH v8 00/37] config-based hooks Junio C Hamano
                   ` (3 subsequent siblings)
  40 siblings, 2 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-11  2:10 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

By showing the list of all hooks in 'git help hook' for users to refer
to, 'git help hook' becomes a one-stop shop for hook authorship. Since
some may still have muscle memory for 'git help githooks', though,
reference the 'git hook' commands and otherwise don't remove content.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/git-hook.txt     |  11 +
 Documentation/githooks.txt     | 716 +--------------------------------
 Documentation/native-hooks.txt | 708 ++++++++++++++++++++++++++++++++
 3 files changed, 724 insertions(+), 711 deletions(-)
 create mode 100644 Documentation/native-hooks.txt

diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index 81b8e94994..4ad31ac360 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -17,6 +17,13 @@ DESCRIPTION
 You can list and run configured hooks with this command. Later, you will be able
 to add and modify hooks with this command.
 
+In general, when instructions suggest adding a script to
+`.git/hooks/<something>`, you can specify it in the config instead by running
+`git config --add hook.<something>.command <path-to-script>` - this way you can
+share the script between multiple repos. That is, `cp ~/my-script.sh
+~/project/.git/hooks/pre-commit` would become `git config --add
+hook.pre-commit.command ~/my-script.sh`.
+
 This command parses the default configuration files for sections `hook` and
 `hookcmd`. `hook` is used to describe the commands which will be run during a
 particular hook event; commands are run in the order Git encounters them during
@@ -145,6 +152,10 @@ CONFIGURATION
 -------------
 include::config/hook.txt[]
 
+HOOKS
+-----
+include::native-hooks.txt[]
+
 GIT
 ---
 Part of the linkgit:git[1] suite
diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt
index b63054b947..9a25dfdc3f 100644
--- a/Documentation/githooks.txt
+++ b/Documentation/githooks.txt
@@ -7,15 +7,16 @@ githooks - Hooks used by Git
 
 SYNOPSIS
 --------
+'git hook'
 $GIT_DIR/hooks/* (or \`git config core.hooksPath`/*)
 
 
 DESCRIPTION
 -----------
 
-Hooks are programs you can place in a hooks directory to trigger
-actions at certain points in git's execution. Hooks that don't have
-the executable bit set are ignored.
+Hooks are programs you can specify in your config (see linkgit:git-hook[1]) or
+place in a hooks directory to trigger actions at certain points in git's
+execution. Hooks that don't have the executable bit set are ignored.
 
 By default the hooks directory is `$GIT_DIR/hooks`, but that can be
 changed via the `core.hooksPath` configuration variable (see
@@ -41,714 +42,7 @@ The currently supported hooks are described below.
 
 HOOKS
 -----
-
-applypatch-msg
-~~~~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-am[1].  It takes a single
-parameter, the name of the file that holds the proposed commit
-log message.  Exiting with a non-zero status causes `git am` to abort
-before applying the patch.
-
-The hook is allowed to edit the message file in place, and can
-be used to normalize the message into some project standard
-format. It can also be used to refuse the commit after inspecting
-the message file.
-
-The default 'applypatch-msg' hook, when enabled, runs the
-'commit-msg' hook, if the latter is enabled.
-
-Hooks run during 'applypatch-msg' will not be parallelized, because hooks are
-expected to edit the file holding the commit log message.
-
-pre-applypatch
-~~~~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-am[1].  It takes no parameter, and is
-invoked after the patch is applied, but before a commit is made.
-
-If it exits with non-zero status, then the working tree will not be
-committed after applying the patch.
-
-It can be used to inspect the current working tree and refuse to
-make a commit if it does not pass certain test.
-
-The default 'pre-applypatch' hook, when enabled, runs the
-'pre-commit' hook, if the latter is enabled.
-
-Hooks run during 'pre-applypatch' will be run in parallel, unless hook.jobs is
-configured to 1.
-
-post-applypatch
-~~~~~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-am[1].  It takes no parameter,
-and is invoked after the patch is applied and a commit is made.
-
-This hook is meant primarily for notification, and cannot affect
-the outcome of `git am`.
-
-Hooks run during 'post-applypatch' will be run in parallel, unless hook.jobs is
-configured to 1.
-
-pre-commit
-~~~~~~~~~~
-
-This hook is invoked by linkgit:git-commit[1], and can be bypassed
-with the `--no-verify` option.  It takes no parameters, and is
-invoked before obtaining the proposed commit log message and
-making a commit.  Exiting with a non-zero status from this script
-causes the `git commit` command to abort before creating a commit.
-
-The default 'pre-commit' hook, when enabled, catches introduction
-of lines with trailing whitespaces and aborts the commit when
-such a line is found.
-
-All the `git commit` hooks are invoked with the environment
-variable `GIT_EDITOR=:` if the command will not bring up an editor
-to modify the commit message.
-
-The default 'pre-commit' hook, when enabled--and with the
-`hooks.allownonascii` config option unset or set to false--prevents
-the use of non-ASCII filenames.
-
-Hooks executed during 'pre-commit' will not be parallelized.
-
-pre-merge-commit
-~~~~~~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-merge[1], and can be bypassed
-with the `--no-verify` option.  It takes no parameters, and is
-invoked after the merge has been carried out successfully and before
-obtaining the proposed commit log message to
-make a commit.  Exiting with a non-zero status from this script
-causes the `git merge` command to abort before creating a commit.
-
-The default 'pre-merge-commit' hook, when enabled, runs the
-'pre-commit' hook, if the latter is enabled.
-
-This hook is invoked with the environment variable
-`GIT_EDITOR=:` if the command will not bring up an editor
-to modify the commit message.
-
-If the merge cannot be carried out automatically, the conflicts
-need to be resolved and the result committed separately (see
-linkgit:git-merge[1]). At that point, this hook will not be executed,
-but the 'pre-commit' hook will, if it is enabled.
-
-Hooks executed during 'pre-merge-commit' will not be parallelized.
-
-prepare-commit-msg
-~~~~~~~~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-commit[1] right after preparing the
-default log message, and before the editor is started.
-
-It takes one to three parameters.  The first is the name of the file
-that contains the commit log message.  The second is the source of the commit
-message, and can be: `message` (if a `-m` or `-F` option was
-given); `template` (if a `-t` option was given or the
-configuration option `commit.template` is set); `merge` (if the
-commit is a merge or a `.git/MERGE_MSG` file exists); `squash`
-(if a `.git/SQUASH_MSG` file exists); or `commit`, followed by
-a commit SHA-1 (if a `-c`, `-C` or `--amend` option was given).
-
-If the exit status is non-zero, `git commit` will abort.
-
-The purpose of the hook is to edit the message file in place, and
-it is not suppressed by the `--no-verify` option.  A non-zero exit
-means a failure of the hook and aborts the commit.  It should not
-be used as replacement for pre-commit hook.
-
-The sample `prepare-commit-msg` hook that comes with Git removes the
-help message found in the commented portion of the commit template.
-
-Hooks executed during 'prepare-commit-msg' will not be parallelized, because
-hooks are expected to edit the file containing the commit log message.
-
-commit-msg
-~~~~~~~~~~
-
-This hook is invoked by linkgit:git-commit[1] and linkgit:git-merge[1], and can be
-bypassed with the `--no-verify` option.  It takes a single parameter,
-the name of the file that holds the proposed commit log message.
-Exiting with a non-zero status causes the command to abort.
-
-The hook is allowed to edit the message file in place, and can be used
-to normalize the message into some project standard format. It
-can also be used to refuse the commit after inspecting the message
-file.
-
-The default 'commit-msg' hook, when enabled, detects duplicate
-`Signed-off-by` trailers, and aborts the commit if one is found.
-
-Hooks executed during 'commit-msg' will not be parallelized, because hooks are
-expected to edit the file containing the proposed commit log message.
-
-post-commit
-~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-commit[1]. It takes no parameters, and is
-invoked after a commit is made.
-
-This hook is meant primarily for notification, and cannot affect
-the outcome of `git commit`.
-
-Hooks executed during 'post-commit' will run in parallel, unless hook.jobs is
-configured to 1.
-
-pre-rebase
-~~~~~~~~~~
-
-This hook is called by linkgit:git-rebase[1] and can be used to prevent a
-branch from getting rebased.  The hook may be called with one or
-two parameters.  The first parameter is the upstream from which
-the series was forked.  The second parameter is the branch being
-rebased, and is not set when rebasing the current branch.
-
-Hooks executed during 'pre-rebase' will run in parallel, unless hook.jobs is
-configured to 1.
-
-post-checkout
-~~~~~~~~~~~~~
-
-This hook is invoked when a linkgit:git-checkout[1] or
-linkgit:git-switch[1] is run after having updated the
-worktree.  The hook is given three parameters: the ref of the previous HEAD,
-the ref of the new HEAD (which may or may not have changed), and a flag
-indicating whether the checkout was a branch checkout (changing branches,
-flag=1) or a file checkout (retrieving a file from the index, flag=0).
-This hook cannot affect the outcome of `git switch` or `git checkout`,
-other than that the hook's exit status becomes the exit status of
-these two commands.
-
-It is also run after linkgit:git-clone[1], unless the `--no-checkout` (`-n`) option is
-used. The first parameter given to the hook is the null-ref, the second the
-ref of the new HEAD and the flag is always 1. Likewise for `git worktree add`
-unless `--no-checkout` is used.
-
-This hook can be used to perform repository validity checks, auto-display
-differences from the previous HEAD if different, or set working dir metadata
-properties.
-
-Hooks executed during 'post-checkout' will not be parallelized.
-
-post-merge
-~~~~~~~~~~
-
-This hook is invoked by linkgit:git-merge[1], which happens when a `git pull`
-is done on a local repository.  The hook takes a single parameter, a status
-flag specifying whether or not the merge being done was a squash merge.
-This hook cannot affect the outcome of `git merge` and is not executed,
-if the merge failed due to conflicts.
-
-This hook can be used in conjunction with a corresponding pre-commit hook to
-save and restore any form of metadata associated with the working tree
-(e.g.: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl
-for an example of how to do this.
-
-Hooks executed during 'post-merge' will run in parallel, unless hook.jobs is
-configured to 1.
-
-pre-push
-~~~~~~~~
-
-This hook is called by linkgit:git-push[1] and can be used to prevent
-a push from taking place.  The hook is called with two parameters
-which provide the name and location of the destination remote, if a
-named remote is not being used both values will be the same.
-
-Information about what is to be pushed is provided on the hook's standard
-input with lines of the form:
-
-  <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
-
-For instance, if the command +git push origin master:foreign+ were run the
-hook would receive a line like the following:
-
-  refs/heads/master 67890 refs/heads/foreign 12345
-
-although the full, 40-character SHA-1s would be supplied.  If the foreign ref
-does not yet exist the `<remote SHA-1>` will be 40 `0`.  If a ref is to be
-deleted, the `<local ref>` will be supplied as `(delete)` and the `<local
-SHA-1>` will be 40 `0`.  If the local commit was specified by something other
-than a name which could be expanded (such as `HEAD~`, or a SHA-1) it will be
-supplied as it was originally given.
-
-If this hook exits with a non-zero status, `git push` will abort without
-pushing anything.  Information about why the push is rejected may be sent
-to the user by writing to standard error.
-
-Hooks executed during 'pre-push' will run in parallel, unless hook.jobs is
-configured to 1.
-
-[[pre-receive]]
-pre-receive
-~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
-`git push` and updates reference(s) in its repository.
-Just before starting to update refs on the remote repository, the
-pre-receive hook is invoked.  Its exit status determines the success
-or failure of the update.
-
-This hook executes once for the receive operation. It takes no
-arguments, but for each ref to be updated it receives on standard
-input a line of the format:
-
-  <old-value> SP <new-value> SP <ref-name> LF
-
-where `<old-value>` is the old object name stored in the ref,
-`<new-value>` is the new object name to be stored in the ref and
-`<ref-name>` is the full name of the ref.
-When creating a new ref, `<old-value>` is 40 `0`.
-
-If the hook exits with non-zero status, none of the refs will be
-updated. If the hook exits with zero, updating of individual refs can
-still be prevented by the <<update,'update'>> hook.
-
-Both standard output and standard error output are forwarded to
-`git send-pack` on the other end, so you can simply `echo` messages
-for the user.
-
-The number of push options given on the command line of
-`git push --push-option=...` can be read from the environment
-variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are
-found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...
-If it is negotiated to not use the push options phase, the
-environment variables will not be set. If the client selects
-to use push options, but doesn't transmit any, the count variable
-will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.
-
-See the section on "Quarantine Environment" in
-linkgit:git-receive-pack[1] for some caveats.
-
-Hooks executed during 'pre-receive' will not be parallelized.
-
-[[update]]
-update
-~~~~~~
-
-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
-`git push` and updates reference(s) in its repository.
-Just before updating the ref on the remote repository, the update hook
-is invoked.  Its exit status determines the success or failure of
-the ref update.
-
-The hook executes once for each ref to be updated, and takes
-three parameters:
-
- - the name of the ref being updated,
- - the old object name stored in the ref,
- - and the new object name to be stored in the ref.
-
-A zero exit from the update hook allows the ref to be updated.
-Exiting with a non-zero status prevents `git receive-pack`
-from updating that ref.
-
-This hook can be used to prevent 'forced' update on certain refs by
-making sure that the object name is a commit object that is a
-descendant of the commit object named by the old object name.
-That is, to enforce a "fast-forward only" policy.
-
-It could also be used to log the old..new status.  However, it
-does not know the entire set of branches, so it would end up
-firing one e-mail per ref when used naively, though.  The
-<<post-receive,'post-receive'>> hook is more suited to that.
-
-In an environment that restricts the users' access only to git
-commands over the wire, this hook can be used to implement access
-control without relying on filesystem ownership and group
-membership. See linkgit:git-shell[1] for how you might use the login
-shell to restrict the user's access to only git commands.
-
-Both standard output and standard error output are forwarded to
-`git send-pack` on the other end, so you can simply `echo` messages
-for the user.
-
-The default 'update' hook, when enabled--and with
-`hooks.allowunannotated` config option unset or set to false--prevents
-unannotated tags to be pushed.
-
-Hooks executed during 'update' are run in parallel, unless hook.jobs is
-configured to 1.
-
-[[proc-receive]]
-proc-receive
-~~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-receive-pack[1].  If the server has
-set the multi-valued config variable `receive.procReceiveRefs`, and the
-commands sent to 'receive-pack' have matching reference names, these
-commands will be executed by this hook, instead of by the internal
-`execute_commands()` function.  This hook is responsible for updating
-the relevant references and reporting the results back to 'receive-pack'.
-
-This hook executes once for the receive operation.  It takes no
-arguments, but uses a pkt-line format protocol to communicate with
-'receive-pack' to read commands, push-options and send results.  In the
-following example for the protocol, the letter 'S' stands for
-'receive-pack' and the letter 'H' stands for this hook.
-
-    # Version and features negotiation.
-    S: PKT-LINE(version=1\0push-options atomic...)
-    S: flush-pkt
-    H: PKT-LINE(version=1\0push-options...)
-    H: flush-pkt
-
-    # Send commands from server to the hook.
-    S: PKT-LINE(<old-oid> <new-oid> <ref>)
-    S: ... ...
-    S: flush-pkt
-    # Send push-options only if the 'push-options' feature is enabled.
-    S: PKT-LINE(push-option)
-    S: ... ...
-    S: flush-pkt
-
-    # Receive result from the hook.
-    # OK, run this command successfully.
-    H: PKT-LINE(ok <ref>)
-    # NO, I reject it.
-    H: PKT-LINE(ng <ref> <reason>)
-    # Fall through, let 'receive-pack' to execute it.
-    H: PKT-LINE(ok <ref>)
-    H: PKT-LINE(option fall-through)
-    # OK, but has an alternate reference.  The alternate reference name
-    # and other status can be given in option directives.
-    H: PKT-LINE(ok <ref>)
-    H: PKT-LINE(option refname <refname>)
-    H: PKT-LINE(option old-oid <old-oid>)
-    H: PKT-LINE(option new-oid <new-oid>)
-    H: PKT-LINE(option forced-update)
-    H: ... ...
-    H: flush-pkt
-
-Each command for the 'proc-receive' hook may point to a pseudo-reference
-and always has a zero-old as its old-oid, while the 'proc-receive' hook
-may update an alternate reference and the alternate reference may exist
-already with a non-zero old-oid.  For this case, this hook will use
-"option" directives to report extended attributes for the reference given
-by the leading "ok" directive.
-
-The report of the commands of this hook should have the same order as
-the input.  The exit status of the 'proc-receive' hook only determines
-the success or failure of the group of commands sent to it, unless
-atomic push is in use.
-
-It is forbidden to specify more than one hook for 'proc-receive'. If a
-globally-configured 'proc-receive' must be overridden, use
-'hookcmd.<global-hook>.skip = true' to ignore it.
-
-[[post-receive]]
-post-receive
-~~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
-`git push` and updates reference(s) in its repository.
-It executes on the remote repository once after all the refs have
-been updated.
-
-This hook executes once for the receive operation.  It takes no
-arguments, but gets the same information as the
-<<pre-receive,'pre-receive'>>
-hook does on its standard input.
-
-This hook does not affect the outcome of `git receive-pack`, as it
-is called after the real work is done.
-
-This supersedes the <<post-update,'post-update'>> hook in that it gets
-both old and new values of all the refs in addition to their
-names.
-
-Both standard output and standard error output are forwarded to
-`git send-pack` on the other end, so you can simply `echo` messages
-for the user.
-
-The default 'post-receive' hook is empty, but there is
-a sample script `post-receive-email` provided in the `contrib/hooks`
-directory in Git distribution, which implements sending commit
-emails.
-
-The number of push options given on the command line of
-`git push --push-option=...` can be read from the environment
-variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are
-found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...
-If it is negotiated to not use the push options phase, the
-environment variables will not be set. If the client selects
-to use push options, but doesn't transmit any, the count variable
-will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.
-
-Hooks executed during 'post-receive' are run in parallel, unless hook.jobs is
-configured to 1.
-
-[[post-update]]
-post-update
-~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
-`git push` and updates reference(s) in its repository.
-It executes on the remote repository once after all the refs have
-been updated.
-
-It takes a variable number of parameters, each of which is the
-name of ref that was actually updated.
-
-This hook is meant primarily for notification, and cannot affect
-the outcome of `git receive-pack`.
-
-The 'post-update' hook can tell what are the heads that were pushed,
-but it does not know what their original and updated values are,
-so it is a poor place to do log old..new. The
-<<post-receive,'post-receive'>> hook does get both original and
-updated values of the refs. You might consider it instead if you need
-them.
-
-When enabled, the default 'post-update' hook runs
-`git update-server-info` to keep the information used by dumb
-transports (e.g., HTTP) up to date.  If you are publishing
-a Git repository that is accessible via HTTP, you should
-probably enable this hook.
-
-Both standard output and standard error output are forwarded to
-`git send-pack` on the other end, so you can simply `echo` messages
-for the user.
-
-Hooks run during 'post-update' will be run in parallel, unless hook.jobs is
-configured to 1.
-
-reference-transaction
-~~~~~~~~~~~~~~~~~~~~~
-
-This hook is invoked by any Git command that performs reference
-updates. It executes whenever a reference transaction is prepared,
-committed or aborted and may thus get called multiple times.
-
-The hook takes exactly one argument, which is the current state the
-given reference transaction is in:
-
-    - "prepared": All reference updates have been queued to the
-      transaction and references were locked on disk.
-
-    - "committed": The reference transaction was committed and all
-      references now have their respective new value.
-
-    - "aborted": The reference transaction was aborted, no changes
-      were performed and the locks have been released.
-
-For each reference update that was added to the transaction, the hook
-receives on standard input a line of the format:
-
-  <old-value> SP <new-value> SP <ref-name> LF
-
-The exit status of the hook is ignored for any state except for the
-"prepared" state. In the "prepared" state, a non-zero exit status will
-cause the transaction to be aborted. The hook will not be called with
-"aborted" state in that case.
-
-Hooks run during 'reference-transaction' will be run in parallel, unless
-hook.jobs is configured to 1.
-
-push-to-checkout
-~~~~~~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
-`git push` and updates reference(s) in its repository, and when
-the push tries to update the branch that is currently checked out
-and the `receive.denyCurrentBranch` configuration variable is set to
-`updateInstead`.  Such a push by default is refused if the working
-tree and the index of the remote repository has any difference from
-the currently checked out commit; when both the working tree and the
-index match the current commit, they are updated to match the newly
-pushed tip of the branch.  This hook is to be used to override the
-default behaviour.
-
-The hook receives the commit with which the tip of the current
-branch is going to be updated.  It can exit with a non-zero status
-to refuse the push (when it does so, it must not modify the index or
-the working tree).  Or it can make any necessary changes to the
-working tree and to the index to bring them to the desired state
-when the tip of the current branch is updated to the new commit, and
-exit with a zero status.
-
-For example, the hook can simply run `git read-tree -u -m HEAD "$1"`
-in order to emulate `git fetch` that is run in the reverse direction
-with `git push`, as the two-tree form of `git read-tree -u -m` is
-essentially the same as `git switch` or `git checkout`
-that switches branches while
-keeping the local changes in the working tree that do not interfere
-with the difference between the branches.
-
-Hooks executed during 'push-to-checkout' will not be parallelized.
-
-pre-auto-gc
-~~~~~~~~~~~
-
-This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It
-takes no parameter, and exiting with non-zero status from this script
-causes the `git gc --auto` to abort.
-
-Hooks run during 'pre-auto-gc' will be run in parallel, unless hook.jobs is
-configured to 1.
-
-post-rewrite
-~~~~~~~~~~~~
-
-This hook is invoked by commands that rewrite commits
-(linkgit:git-commit[1] when called with `--amend` and
-linkgit:git-rebase[1]; however, full-history (re)writing tools like
-linkgit:git-fast-import[1] or
-https://github.com/newren/git-filter-repo[git-filter-repo] typically
-do not call it!).  Its first argument denotes the command it was
-invoked by: currently one of `amend` or `rebase`.  Further
-command-dependent arguments may be passed in the future.
-
-The hook receives a list of the rewritten commits on stdin, in the
-format
-
-  <old-sha1> SP <new-sha1> [ SP <extra-info> ] LF
-
-The 'extra-info' is again command-dependent.  If it is empty, the
-preceding SP is also omitted.  Currently, no commands pass any
-'extra-info'.
-
-The hook always runs after the automatic note copying (see
-"notes.rewrite.<command>" in linkgit:git-config[1]) has happened, and
-thus has access to these notes.
-
-Hooks run during 'post-rewrite' will be run in parallel, unless hook.jobs is
-configured to 1.
-
-The following command-specific comments apply:
-
-rebase::
-	For the 'squash' and 'fixup' operation, all commits that were
-	squashed are listed as being rewritten to the squashed commit.
-	This means that there will be several lines sharing the same
-	'new-sha1'.
-+
-The commits are guaranteed to be listed in the order that they were
-processed by rebase.
-
-sendemail-validate
-~~~~~~~~~~~~~~~~~~
-
-This hook is invoked by linkgit:git-send-email[1].  It takes a single parameter,
-the name of the file that holds the e-mail to be sent.  Exiting with a
-non-zero status causes `git send-email` to abort before sending any
-e-mails.
-
-fsmonitor-watchman
-~~~~~~~~~~~~~~~~~~
-
-This hook is invoked when the configuration option `core.fsmonitor` is
-set to `.git/hooks/fsmonitor-watchman` or `.git/hooks/fsmonitor-watchmanv2`
-depending on the version of the hook to use.
-
-Version 1 takes two arguments, a version (1) and the time in elapsed
-nanoseconds since midnight, January 1, 1970.
-
-Version 2 takes two arguments, a version (2) and a token that is used
-for identifying changes since the token. For watchman this would be
-a clock id. This version must output to stdout the new token followed
-by a NUL before the list of files.
-
-The hook should output to stdout the list of all files in the working
-directory that may have changed since the requested time.  The logic
-should be inclusive so that it does not miss any potential changes.
-The paths should be relative to the root of the working directory
-and be separated by a single NUL.
-
-It is OK to include files which have not actually changed.  All changes
-including newly-created and deleted files should be included. When
-files are renamed, both the old and the new name should be included.
-
-Git will limit what files it checks for changes as well as which
-directories are checked for untracked files based on the path names
-given.
-
-An optimized way to tell git "all files have changed" is to return
-the filename `/`.
-
-The exit status determines whether git will use the data from the
-hook to limit its search.  On error, it will fall back to verifying
-all files and folders.
-
-p4-changelist
-~~~~~~~~~~~~~
-
-This hook is invoked by `git-p4 submit`.
-
-The `p4-changelist` hook is executed after the changelist
-message has been edited by the user. It can be bypassed with the
-`--no-verify` option. It takes a single parameter, the name
-of the file that holds the proposed changelist text. Exiting
-with a non-zero status causes the command to abort.
-
-The hook is allowed to edit the changelist file and can be used
-to normalize the text into some project standard format. It can
-also be used to refuse the Submit after inspect the message file.
-
-Run `git-p4 submit --help` for details.
-
-p4-prepare-changelist
-~~~~~~~~~~~~~~~~~~~~~
-
-This hook is invoked by `git-p4 submit`.
-
-The `p4-prepare-changelist` hook is executed right after preparing
-the default changelist message and before the editor is started.
-It takes one parameter, the name of the file that contains the
-changelist text. Exiting with a non-zero status from the script
-will abort the process.
-
-The purpose of the hook is to edit the message file in place,
-and it is not suppressed by the `--no-verify` option. This hook
-is called even if `--prepare-p4-only` is set.
-
-Run `git-p4 submit --help` for details.
-
-p4-post-changelist
-~~~~~~~~~~~~~~~~~~
-
-This hook is invoked by `git-p4 submit`.
-
-The `p4-post-changelist` hook is invoked after the submit has
-successfully occurred in P4. It takes no parameters and is meant
-primarily for notification and cannot affect the outcome of the
-git p4 submit action.
-
-Run `git-p4 submit --help` for details.
-
-p4-pre-submit
-~~~~~~~~~~~~~
-
-This hook is invoked by `git-p4 submit`. It takes no parameters and nothing
-from standard input. Exiting with non-zero status from this script prevent
-`git-p4 submit` from launching. It can be bypassed with the `--no-verify`
-command line option. Run `git-p4 submit --help` for details.
-
-
-
-post-index-change
-~~~~~~~~~~~~~~~~~
-
-This hook is invoked when the index is written in read-cache.c
-do_write_locked_index.
-
-The first parameter passed to the hook is the indicator for the
-working directory being updated.  "1" meaning working directory
-was updated or "0" when the working directory was not updated.
-
-The second parameter passed to the hook is the indicator for whether
-or not the index was updated and the skip-worktree bit could have
-changed.  "1" meaning skip-worktree bits could have been updated
-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.
-
-Hooks run during 'post-index-change' will be run in parallel, unless hook.jobs
-is configured to 1.
+include::native-hooks.txt[]
 
 GIT
 ---
diff --git a/Documentation/native-hooks.txt b/Documentation/native-hooks.txt
new file mode 100644
index 0000000000..6c4aad83e1
--- /dev/null
+++ b/Documentation/native-hooks.txt
@@ -0,0 +1,708 @@
+applypatch-msg
+~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-am[1].  It takes a single
+parameter, the name of the file that holds the proposed commit
+log message.  Exiting with a non-zero status causes `git am` to abort
+before applying the patch.
+
+The hook is allowed to edit the message file in place, and can
+be used to normalize the message into some project standard
+format. It can also be used to refuse the commit after inspecting
+the message file.
+
+The default 'applypatch-msg' hook, when enabled, runs the
+'commit-msg' hook, if the latter is enabled.
+
+Hooks run during 'applypatch-msg' will not be parallelized, because hooks are
+expected to edit the file holding the commit log message.
+
+pre-applypatch
+~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-am[1].  It takes no parameter, and is
+invoked after the patch is applied, but before a commit is made.
+
+If it exits with non-zero status, then the working tree will not be
+committed after applying the patch.
+
+It can be used to inspect the current working tree and refuse to
+make a commit if it does not pass certain test.
+
+The default 'pre-applypatch' hook, when enabled, runs the
+'pre-commit' hook, if the latter is enabled.
+
+Hooks run during 'pre-applypatch' will be run in parallel, unless hook.jobs is
+configured to 1.
+
+post-applypatch
+~~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-am[1].  It takes no parameter,
+and is invoked after the patch is applied and a commit is made.
+
+This hook is meant primarily for notification, and cannot affect
+the outcome of `git am`.
+
+Hooks run during 'post-applypatch' will be run in parallel, unless hook.jobs is
+configured to 1.
+
+pre-commit
+~~~~~~~~~~
+
+This hook is invoked by linkgit:git-commit[1], and can be bypassed
+with the `--no-verify` option.  It takes no parameters, and is
+invoked before obtaining the proposed commit log message and
+making a commit.  Exiting with a non-zero status from this script
+causes the `git commit` command to abort before creating a commit.
+
+The default 'pre-commit' hook, when enabled, catches introduction
+of lines with trailing whitespaces and aborts the commit when
+such a line is found.
+
+All the `git commit` hooks are invoked with the environment
+variable `GIT_EDITOR=:` if the command will not bring up an editor
+to modify the commit message.
+
+The default 'pre-commit' hook, when enabled--and with the
+`hooks.allownonascii` config option unset or set to false--prevents
+the use of non-ASCII filenames.
+
+Hooks executed during 'pre-commit' will not be parallelized.
+
+pre-merge-commit
+~~~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-merge[1], and can be bypassed
+with the `--no-verify` option.  It takes no parameters, and is
+invoked after the merge has been carried out successfully and before
+obtaining the proposed commit log message to
+make a commit.  Exiting with a non-zero status from this script
+causes the `git merge` command to abort before creating a commit.
+
+The default 'pre-merge-commit' hook, when enabled, runs the
+'pre-commit' hook, if the latter is enabled.
+
+This hook is invoked with the environment variable
+`GIT_EDITOR=:` if the command will not bring up an editor
+to modify the commit message.
+
+If the merge cannot be carried out automatically, the conflicts
+need to be resolved and the result committed separately (see
+linkgit:git-merge[1]). At that point, this hook will not be executed,
+but the 'pre-commit' hook will, if it is enabled.
+
+Hooks executed during 'pre-merge-commit' will not be parallelized.
+
+prepare-commit-msg
+~~~~~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-commit[1] right after preparing the
+default log message, and before the editor is started.
+
+It takes one to three parameters.  The first is the name of the file
+that contains the commit log message.  The second is the source of the commit
+message, and can be: `message` (if a `-m` or `-F` option was
+given); `template` (if a `-t` option was given or the
+configuration option `commit.template` is set); `merge` (if the
+commit is a merge or a `.git/MERGE_MSG` file exists); `squash`
+(if a `.git/SQUASH_MSG` file exists); or `commit`, followed by
+a commit SHA-1 (if a `-c`, `-C` or `--amend` option was given).
+
+If the exit status is non-zero, `git commit` will abort.
+
+The purpose of the hook is to edit the message file in place, and
+it is not suppressed by the `--no-verify` option.  A non-zero exit
+means a failure of the hook and aborts the commit.  It should not
+be used as replacement for pre-commit hook.
+
+The sample `prepare-commit-msg` hook that comes with Git removes the
+help message found in the commented portion of the commit template.
+
+Hooks executed during 'prepare-commit-msg' will not be parallelized, because
+hooks are expected to edit the file containing the commit log message.
+
+commit-msg
+~~~~~~~~~~
+
+This hook is invoked by linkgit:git-commit[1] and linkgit:git-merge[1], and can be
+bypassed with the `--no-verify` option.  It takes a single parameter,
+the name of the file that holds the proposed commit log message.
+Exiting with a non-zero status causes the command to abort.
+
+The hook is allowed to edit the message file in place, and can be used
+to normalize the message into some project standard format. It
+can also be used to refuse the commit after inspecting the message
+file.
+
+The default 'commit-msg' hook, when enabled, detects duplicate
+`Signed-off-by` trailers, and aborts the commit if one is found.
+
+Hooks executed during 'commit-msg' will not be parallelized, because hooks are
+expected to edit the file containing the proposed commit log message.
+
+post-commit
+~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-commit[1]. It takes no parameters, and is
+invoked after a commit is made.
+
+This hook is meant primarily for notification, and cannot affect
+the outcome of `git commit`.
+
+Hooks executed during 'post-commit' will run in parallel, unless hook.jobs is
+configured to 1.
+
+pre-rebase
+~~~~~~~~~~
+
+This hook is called by linkgit:git-rebase[1] and can be used to prevent a
+branch from getting rebased.  The hook may be called with one or
+two parameters.  The first parameter is the upstream from which
+the series was forked.  The second parameter is the branch being
+rebased, and is not set when rebasing the current branch.
+
+Hooks executed during 'pre-rebase' will run in parallel, unless hook.jobs is
+configured to 1.
+
+post-checkout
+~~~~~~~~~~~~~
+
+This hook is invoked when a linkgit:git-checkout[1] or
+linkgit:git-switch[1] is run after having updated the
+worktree.  The hook is given three parameters: the ref of the previous HEAD,
+the ref of the new HEAD (which may or may not have changed), and a flag
+indicating whether the checkout was a branch checkout (changing branches,
+flag=1) or a file checkout (retrieving a file from the index, flag=0).
+This hook cannot affect the outcome of `git switch` or `git checkout`,
+other than that the hook's exit status becomes the exit status of
+these two commands.
+
+It is also run after linkgit:git-clone[1], unless the `--no-checkout` (`-n`) option is
+used. The first parameter given to the hook is the null-ref, the second the
+ref of the new HEAD and the flag is always 1. Likewise for `git worktree add`
+unless `--no-checkout` is used.
+
+This hook can be used to perform repository validity checks, auto-display
+differences from the previous HEAD if different, or set working dir metadata
+properties.
+
+Hooks executed during 'post-checkout' will not be parallelized.
+
+post-merge
+~~~~~~~~~~
+
+This hook is invoked by linkgit:git-merge[1], which happens when a `git pull`
+is done on a local repository.  The hook takes a single parameter, a status
+flag specifying whether or not the merge being done was a squash merge.
+This hook cannot affect the outcome of `git merge` and is not executed,
+if the merge failed due to conflicts.
+
+This hook can be used in conjunction with a corresponding pre-commit hook to
+save and restore any form of metadata associated with the working tree
+(e.g.: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl
+for an example of how to do this.
+
+Hooks executed during 'post-merge' will run in parallel, unless hook.jobs is
+configured to 1.
+
+pre-push
+~~~~~~~~
+
+This hook is called by linkgit:git-push[1] and can be used to prevent
+a push from taking place.  The hook is called with two parameters
+which provide the name and location of the destination remote, if a
+named remote is not being used both values will be the same.
+
+Information about what is to be pushed is provided on the hook's standard
+input with lines of the form:
+
+  <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
+
+For instance, if the command +git push origin master:foreign+ were run the
+hook would receive a line like the following:
+
+  refs/heads/master 67890 refs/heads/foreign 12345
+
+although the full, 40-character SHA-1s would be supplied.  If the foreign ref
+does not yet exist the `<remote SHA-1>` will be 40 `0`.  If a ref is to be
+deleted, the `<local ref>` will be supplied as `(delete)` and the `<local
+SHA-1>` will be 40 `0`.  If the local commit was specified by something other
+than a name which could be expanded (such as `HEAD~`, or a SHA-1) it will be
+supplied as it was originally given.
+
+If this hook exits with a non-zero status, `git push` will abort without
+pushing anything.  Information about why the push is rejected may be sent
+to the user by writing to standard error.
+
+Hooks executed during 'pre-push' will run in parallel, unless hook.jobs is
+configured to 1.
+
+[[pre-receive]]
+pre-receive
+~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository.
+Just before starting to update refs on the remote repository, the
+pre-receive hook is invoked.  Its exit status determines the success
+or failure of the update.
+
+This hook executes once for the receive operation. It takes no
+arguments, but for each ref to be updated it receives on standard
+input a line of the format:
+
+  <old-value> SP <new-value> SP <ref-name> LF
+
+where `<old-value>` is the old object name stored in the ref,
+`<new-value>` is the new object name to be stored in the ref and
+`<ref-name>` is the full name of the ref.
+When creating a new ref, `<old-value>` is 40 `0`.
+
+If the hook exits with non-zero status, none of the refs will be
+updated. If the hook exits with zero, updating of individual refs can
+still be prevented by the <<update,'update'>> hook.
+
+Both standard output and standard error output are forwarded to
+`git send-pack` on the other end, so you can simply `echo` messages
+for the user.
+
+The number of push options given on the command line of
+`git push --push-option=...` can be read from the environment
+variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are
+found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...
+If it is negotiated to not use the push options phase, the
+environment variables will not be set. If the client selects
+to use push options, but doesn't transmit any, the count variable
+will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.
+
+See the section on "Quarantine Environment" in
+linkgit:git-receive-pack[1] for some caveats.
+
+Hooks executed during 'pre-receive' will not be parallelized.
+
+[[update]]
+update
+~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository.
+Just before updating the ref on the remote repository, the update hook
+is invoked.  Its exit status determines the success or failure of
+the ref update.
+
+The hook executes once for each ref to be updated, and takes
+three parameters:
+
+ - the name of the ref being updated,
+ - the old object name stored in the ref,
+ - and the new object name to be stored in the ref.
+
+A zero exit from the update hook allows the ref to be updated.
+Exiting with a non-zero status prevents `git receive-pack`
+from updating that ref.
+
+This hook can be used to prevent 'forced' update on certain refs by
+making sure that the object name is a commit object that is a
+descendant of the commit object named by the old object name.
+That is, to enforce a "fast-forward only" policy.
+
+It could also be used to log the old..new status.  However, it
+does not know the entire set of branches, so it would end up
+firing one e-mail per ref when used naively, though.  The
+<<post-receive,'post-receive'>> hook is more suited to that.
+
+In an environment that restricts the users' access only to git
+commands over the wire, this hook can be used to implement access
+control without relying on filesystem ownership and group
+membership. See linkgit:git-shell[1] for how you might use the login
+shell to restrict the user's access to only git commands.
+
+Both standard output and standard error output are forwarded to
+`git send-pack` on the other end, so you can simply `echo` messages
+for the user.
+
+The default 'update' hook, when enabled--and with
+`hooks.allowunannotated` config option unset or set to false--prevents
+unannotated tags to be pushed.
+
+Hooks executed during 'update' are run in parallel, unless hook.jobs is
+configured to 1.
+
+[[proc-receive]]
+proc-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1].  If the server has
+set the multi-valued config variable `receive.procReceiveRefs`, and the
+commands sent to 'receive-pack' have matching reference names, these
+commands will be executed by this hook, instead of by the internal
+`execute_commands()` function.  This hook is responsible for updating
+the relevant references and reporting the results back to 'receive-pack'.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but uses a pkt-line format protocol to communicate with
+'receive-pack' to read commands, push-options and send results.  In the
+following example for the protocol, the letter 'S' stands for
+'receive-pack' and the letter 'H' stands for this hook.
+
+    # Version and features negotiation.
+    S: PKT-LINE(version=1\0push-options atomic...)
+    S: flush-pkt
+    H: PKT-LINE(version=1\0push-options...)
+    H: flush-pkt
+
+    # Send commands from server to the hook.
+    S: PKT-LINE(<old-oid> <new-oid> <ref>)
+    S: ... ...
+    S: flush-pkt
+    # Send push-options only if the 'push-options' feature is enabled.
+    S: PKT-LINE(push-option)
+    S: ... ...
+    S: flush-pkt
+
+    # Receive result from the hook.
+    # OK, run this command successfully.
+    H: PKT-LINE(ok <ref>)
+    # NO, I reject it.
+    H: PKT-LINE(ng <ref> <reason>)
+    # Fall through, let 'receive-pack' to execute it.
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option fall-through)
+    # OK, but has an alternate reference.  The alternate reference name
+    # and other status can be given in option directives.
+    H: PKT-LINE(ok <ref>)
+    H: PKT-LINE(option refname <refname>)
+    H: PKT-LINE(option old-oid <old-oid>)
+    H: PKT-LINE(option new-oid <new-oid>)
+    H: PKT-LINE(option forced-update)
+    H: ... ...
+    H: flush-pkt
+
+Each command for the 'proc-receive' hook may point to a pseudo-reference
+and always has a zero-old as its old-oid, while the 'proc-receive' hook
+may update an alternate reference and the alternate reference may exist
+already with a non-zero old-oid.  For this case, this hook will use
+"option" directives to report extended attributes for the reference given
+by the leading "ok" directive.
+
+The report of the commands of this hook should have the same order as
+the input.  The exit status of the 'proc-receive' hook only determines
+the success or failure of the group of commands sent to it, unless
+atomic push is in use.
+
+It is forbidden to specify more than one hook for 'proc-receive'. If a
+globally-configured 'proc-receive' must be overridden, use
+'hookcmd.<global-hook>.skip = true' to ignore it.
+
+[[post-receive]]
+post-receive
+~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository.
+It executes on the remote repository once after all the refs have
+been updated.
+
+This hook executes once for the receive operation.  It takes no
+arguments, but gets the same information as the
+<<pre-receive,'pre-receive'>>
+hook does on its standard input.
+
+This hook does not affect the outcome of `git receive-pack`, as it
+is called after the real work is done.
+
+This supersedes the <<post-update,'post-update'>> hook in that it gets
+both old and new values of all the refs in addition to their
+names.
+
+Both standard output and standard error output are forwarded to
+`git send-pack` on the other end, so you can simply `echo` messages
+for the user.
+
+The default 'post-receive' hook is empty, but there is
+a sample script `post-receive-email` provided in the `contrib/hooks`
+directory in Git distribution, which implements sending commit
+emails.
+
+The number of push options given on the command line of
+`git push --push-option=...` can be read from the environment
+variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are
+found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...
+If it is negotiated to not use the push options phase, the
+environment variables will not be set. If the client selects
+to use push options, but doesn't transmit any, the count variable
+will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.
+
+Hooks executed during 'post-receive' are run in parallel, unless hook.jobs is
+configured to 1.
+
+[[post-update]]
+post-update
+~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository.
+It executes on the remote repository once after all the refs have
+been updated.
+
+It takes a variable number of parameters, each of which is the
+name of ref that was actually updated.
+
+This hook is meant primarily for notification, and cannot affect
+the outcome of `git receive-pack`.
+
+The 'post-update' hook can tell what are the heads that were pushed,
+but it does not know what their original and updated values are,
+so it is a poor place to do log old..new. The
+<<post-receive,'post-receive'>> hook does get both original and
+updated values of the refs. You might consider it instead if you need
+them.
+
+When enabled, the default 'post-update' hook runs
+`git update-server-info` to keep the information used by dumb
+transports (e.g., HTTP) up to date.  If you are publishing
+a Git repository that is accessible via HTTP, you should
+probably enable this hook.
+
+Both standard output and standard error output are forwarded to
+`git send-pack` on the other end, so you can simply `echo` messages
+for the user.
+
+Hooks run during 'post-update' will be run in parallel, unless hook.jobs is
+configured to 1.
+
+reference-transaction
+~~~~~~~~~~~~~~~~~~~~~
+
+This hook is invoked by any Git command that performs reference
+updates. It executes whenever a reference transaction is prepared,
+committed or aborted and may thus get called multiple times.
+
+The hook takes exactly one argument, which is the current state the
+given reference transaction is in:
+
+    - "prepared": All reference updates have been queued to the
+      transaction and references were locked on disk.
+
+    - "committed": The reference transaction was committed and all
+      references now have their respective new value.
+
+    - "aborted": The reference transaction was aborted, no changes
+      were performed and the locks have been released.
+
+For each reference update that was added to the transaction, the hook
+receives on standard input a line of the format:
+
+  <old-value> SP <new-value> SP <ref-name> LF
+
+The exit status of the hook is ignored for any state except for the
+"prepared" state. In the "prepared" state, a non-zero exit status will
+cause the transaction to be aborted. The hook will not be called with
+"aborted" state in that case.
+
+Hooks run during 'reference-transaction' will be run in parallel, unless
+hook.jobs is configured to 1.
+
+push-to-checkout
+~~~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository, and when
+the push tries to update the branch that is currently checked out
+and the `receive.denyCurrentBranch` configuration variable is set to
+`updateInstead`.  Such a push by default is refused if the working
+tree and the index of the remote repository has any difference from
+the currently checked out commit; when both the working tree and the
+index match the current commit, they are updated to match the newly
+pushed tip of the branch.  This hook is to be used to override the
+default behaviour.
+
+The hook receives the commit with which the tip of the current
+branch is going to be updated.  It can exit with a non-zero status
+to refuse the push (when it does so, it must not modify the index or
+the working tree).  Or it can make any necessary changes to the
+working tree and to the index to bring them to the desired state
+when the tip of the current branch is updated to the new commit, and
+exit with a zero status.
+
+For example, the hook can simply run `git read-tree -u -m HEAD "$1"`
+in order to emulate `git fetch` that is run in the reverse direction
+with `git push`, as the two-tree form of `git read-tree -u -m` is
+essentially the same as `git switch` or `git checkout`
+that switches branches while
+keeping the local changes in the working tree that do not interfere
+with the difference between the branches.
+
+Hooks executed during 'push-to-checkout' will not be parallelized.
+
+pre-auto-gc
+~~~~~~~~~~~
+
+This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It
+takes no parameter, and exiting with non-zero status from this script
+causes the `git gc --auto` to abort.
+
+Hooks run during 'pre-auto-gc' will be run in parallel, unless hook.jobs is
+configured to 1.
+
+post-rewrite
+~~~~~~~~~~~~
+
+This hook is invoked by commands that rewrite commits
+(linkgit:git-commit[1] when called with `--amend` and
+linkgit:git-rebase[1]; however, full-history (re)writing tools like
+linkgit:git-fast-import[1] or
+https://github.com/newren/git-filter-repo[git-filter-repo] typically
+do not call it!).  Its first argument denotes the command it was
+invoked by: currently one of `amend` or `rebase`.  Further
+command-dependent arguments may be passed in the future.
+
+The hook receives a list of the rewritten commits on stdin, in the
+format
+
+  <old-sha1> SP <new-sha1> [ SP <extra-info> ] LF
+
+The 'extra-info' is again command-dependent.  If it is empty, the
+preceding SP is also omitted.  Currently, no commands pass any
+'extra-info'.
+
+The hook always runs after the automatic note copying (see
+"notes.rewrite.<command>" in linkgit:git-config[1]) has happened, and
+thus has access to these notes.
+
+Hooks run during 'post-rewrite' will be run in parallel, unless hook.jobs is
+configured to 1.
+
+The following command-specific comments apply:
+
+rebase::
+	For the 'squash' and 'fixup' operation, all commits that were
+	squashed are listed as being rewritten to the squashed commit.
+	This means that there will be several lines sharing the same
+	'new-sha1'.
++
+The commits are guaranteed to be listed in the order that they were
+processed by rebase.
+
+sendemail-validate
+~~~~~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-send-email[1].  It takes a single parameter,
+the name of the file that holds the e-mail to be sent.  Exiting with a
+non-zero status causes `git send-email` to abort before sending any
+e-mails.
+
+fsmonitor-watchman
+~~~~~~~~~~~~~~~~~~
+
+This hook is invoked when the configuration option `core.fsmonitor` is
+set to `.git/hooks/fsmonitor-watchman` or `.git/hooks/fsmonitor-watchmanv2`
+depending on the version of the hook to use.
+
+Version 1 takes two arguments, a version (1) and the time in elapsed
+nanoseconds since midnight, January 1, 1970.
+
+Version 2 takes two arguments, a version (2) and a token that is used
+for identifying changes since the token. For watchman this would be
+a clock id. This version must output to stdout the new token followed
+by a NUL before the list of files.
+
+The hook should output to stdout the list of all files in the working
+directory that may have changed since the requested time.  The logic
+should be inclusive so that it does not miss any potential changes.
+The paths should be relative to the root of the working directory
+and be separated by a single NUL.
+
+It is OK to include files which have not actually changed.  All changes
+including newly-created and deleted files should be included. When
+files are renamed, both the old and the new name should be included.
+
+Git will limit what files it checks for changes as well as which
+directories are checked for untracked files based on the path names
+given.
+
+An optimized way to tell git "all files have changed" is to return
+the filename `/`.
+
+The exit status determines whether git will use the data from the
+hook to limit its search.  On error, it will fall back to verifying
+all files and folders.
+
+p4-changelist
+~~~~~~~~~~~~~
+
+This hook is invoked by `git-p4 submit`.
+
+The `p4-changelist` hook is executed after the changelist
+message has been edited by the user. It can be bypassed with the
+`--no-verify` option. It takes a single parameter, the name
+of the file that holds the proposed changelist text. Exiting
+with a non-zero status causes the command to abort.
+
+The hook is allowed to edit the changelist file and can be used
+to normalize the text into some project standard format. It can
+also be used to refuse the Submit after inspect the message file.
+
+Run `git-p4 submit --help` for details.
+
+p4-prepare-changelist
+~~~~~~~~~~~~~~~~~~~~~
+
+This hook is invoked by `git-p4 submit`.
+
+The `p4-prepare-changelist` hook is executed right after preparing
+the default changelist message and before the editor is started.
+It takes one parameter, the name of the file that contains the
+changelist text. Exiting with a non-zero status from the script
+will abort the process.
+
+The purpose of the hook is to edit the message file in place,
+and it is not suppressed by the `--no-verify` option. This hook
+is called even if `--prepare-p4-only` is set.
+
+Run `git-p4 submit --help` for details.
+
+p4-post-changelist
+~~~~~~~~~~~~~~~~~~
+
+This hook is invoked by `git-p4 submit`.
+
+The `p4-post-changelist` hook is invoked after the submit has
+successfully occurred in P4. It takes no parameters and is meant
+primarily for notification and cannot affect the outcome of the
+git p4 submit action.
+
+Run `git-p4 submit --help` for details.
+
+p4-pre-submit
+~~~~~~~~~~~~~
+
+This hook is invoked by `git-p4 submit`. It takes no parameters and nothing
+from standard input. Exiting with non-zero status from this script prevent
+`git-p4 submit` from launching. It can be bypassed with the `--no-verify`
+command line option. Run `git-p4 submit --help` for details.
+
+
+
+post-index-change
+~~~~~~~~~~~~~~~~~
+
+This hook is invoked when the index is written in read-cache.c
+do_write_locked_index.
+
+The first parameter passed to the hook is the indicator for the
+working directory being updated.  "1" meaning working directory
+was updated or "0" when the working directory was not updated.
+
+The second parameter passed to the hook is the indicator for whether
+or not the index was updated and the skip-worktree bit could have
+changed.  "1" meaning skip-worktree bits could have been updated
+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.
+
+Hooks run during 'post-index-change' will be run in parallel, unless hook.jobs
+is configured to 1.
+
-- 
2.31.0.rc2.261.g7f71774620-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 00/37] config-based hooks
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (36 preceding siblings ...)
  2021-03-11  2:10 ` [PATCH v8 37/37] docs: unify githooks and git-hook manpages Emily Shaffer
@ 2021-03-11 22:26 ` Junio C Hamano
  2021-03-12 23:27   ` Emily Shaffer
  2021-03-12  9:49 ` Ævar Arnfjörð Bjarmason
                   ` (2 subsequent siblings)
  40 siblings, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-03-11 22:26 UTC (permalink / raw)
  To: Emily Shaffer
  Cc: git, Jeff King, James Ramsay, Jonathan Nieder, brian m. carlson,
	Ævar Arnfjörð Bjarmason, Phillip Wood,
	Josh Steadmon, Johannes Schindelin, Jonathan Tan

Emily Shaffer <emilyshaffer@google.com> writes:

> Since v7:
> - Addressed Jonathan Tan's review of part I
> - Addressed Junio's review of part I and II
> - Combined parts I and II
>
> I think the updates to patch 1 between the rest of the work I've been
> doing probably have covered Ævar's comments.
>
> More details about per-patch changes found in the notes on each mail (I
> hope).
>
> I know that Junio was talking about merging v7 after Josh Steadmon's
> review and I asked him not to - this reroll has those changes from
> Jonathan Tan's review that I was wanting to wait for.

I picked it up and replaced, not necessarily because it is an urgent
thing to do during the pre-release period, but primarily because I
wanted to be prepared for any nasty surprises by unmanageable
conflicts I may have to face once the current cycle is over.

It turns out that it was a bit painful to merge to 'seen' as there
are in-flight topics that touch the hooks documentation, and the
changes they make must be carried forward to the new file.

But it was not too bad.  

The merge into 'seen' is 3cdeaeab (Merge branch 'es/config-hooks'
into seen, 2021-03-11) as of this writing, and the output of

    $ git diff 3cdeaeab3a^:Documentation/githooks.txt \
               3cdeaeab3a:Documentation/native-hooks.txt

    (i.e. the version of the file before the merge, where your topic
    being merged took material to edit to produce the new "native-hooks"
    document, is compared with the result)

looks reasonable to me, but please double check.

Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 03/37] hook: add list command
  2021-03-11  2:10 ` [PATCH v8 03/37] hook: add list command Emily Shaffer
@ 2021-03-12  8:20   ` Ævar Arnfjörð Bjarmason
  2021-03-24 17:31     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  8:20 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
> new file mode 100644
> index 0000000000..71449ecbc7
> --- /dev/null
> +++ b/Documentation/config/hook.txt
> @@ -0,0 +1,9 @@
> +hook.<command>.command::
> +	A command to execute during the <command> hook event. This can be an
> +	executable on your device, a oneliner for your shell, or the name of a
> +	hookcmd. See linkgit:git-hook[1].
> +
> +hookcmd.<name>.command::
> +	A command to execute during a hook for which <name> has been specified
> +	as a command. This can be an executable on your device or a oneliner for
> +	your shell. See linkgit:git-hook[1].
> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
> index 9eeab0009d..f19875ed68 100644
> --- a/Documentation/git-hook.txt
> +++ b/Documentation/git-hook.txt
> @@ -8,12 +8,65 @@ git-hook - Manage configured hooks
>  SYNOPSIS
>  --------
>  [verse]
> -'git hook'
> +'git hook' list <hook-name>

Having just read this far (maybe this pattern is shared in the rest of
the series): Let's just squash this and the 2nd patch together.

Sometimes it's worth doing the scaffolding first, but adding a new
built-in is so trivial that I don't think it's worth it, and it just
results in back & forth churn like the above...

>  DESCRIPTION
>  -----------
> -A placeholder command. Later, you will be able to list, add, and modify hooks
> -with this command.

...and this...

> +You can list configured hooks with this command. Later, you will be able to run,
> +add, and modify hooks with this command.
> +
> +This command parses the default configuration files for sections `hook` and
> +`hookcmd`. `hook` is used to describe the commands which will be run during a
> +particular hook event; commands are run in the order Git encounters them during
> +the configuration parse (see linkgit:git-config[1]). `hookcmd` is used to
> +describe attributes of a specific command. If additional attributes don't need
> +to be specified, a command to run can be specified directly in the `hook`
> +section; if a `hookcmd` by that name isn't found, Git will attempt to run the
> +provided value directly. For example:
> +
> +Global config
> +----
> +  [hook "post-commit"]
> +    command = "linter"
> +    command = "~/typocheck.sh"
> +
> +  [hookcmd "linter"]
> +    command = "/bin/linter --c"
> +----
> +
> +Local config
> +----
> +  [hook "prepare-commit-msg"]
> +    command = "linter"
> +  [hook "post-commit"]
> +    command = "python ~/run-test-suite.py"
> +----
> +
> +With these configs, you'd then see:
> +
> +----
> +$ git hook list "post-commit"
> +global: /bin/linter --c
> +global: ~/typocheck.sh
> +local: python ~/run-test-suite.py
> +
> +$ git hook list "prepare-commit-msg"
> +local: /bin/linter --c
> +----
> +
> +COMMANDS
> +--------
> +
> +list `<hook-name>`::
> +
> +List the hooks which have been configured for `<hook-name>`. Hooks appear
> +in the order they should be run, and print the config scope where the relevant
> +`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).
> +This output is human-readable and the format is subject to change over time.
> +
> +CONFIGURATION
> +-------------
> +include::config/hook.txt[]
>  
>  GIT
>  ---
> diff --git a/Makefile b/Makefile
> index 8e904a1ab5..3fa51597d8 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -891,6 +891,7 @@ LIB_OBJS += hash-lookup.o
>  LIB_OBJS += hashmap.o
>  LIB_OBJS += help.o
>  LIB_OBJS += hex.o
> +LIB_OBJS += hook.o
>  LIB_OBJS += ident.o
>  LIB_OBJS += json-writer.o
>  LIB_OBJS += kwset.o
> diff --git a/builtin/hook.c b/builtin/hook.c
> index b2bbc84d4d..bb64cd77ca 100644
> --- a/builtin/hook.c
> +++ b/builtin/hook.c
> @@ -1,21 +1,67 @@
>  #include "cache.h"
> -

Stray back & forth whitespace churn?

>  #include "builtin.h"
> +#include "config.h"
> +#include "hook.h"
>  #include "parse-options.h"
> +#include "strbuf.h"
>  
>  static const char * const builtin_hook_usage[] = {
> -	N_("git hook"),
> +	N_("git hook list <hookname>"),
>  	NULL
>  };
>  
> -int cmd_hook(int argc, const char **argv, const char *prefix)
> +static int list(int argc, const char **argv, const char *prefix)

...and here the cmd_hook() function being replaced (not really, just
moved below, but you get my drift...)

>  {
> -	struct option builtin_hook_options[] = {
> +	struct list_head *head, *pos;
> +	struct strbuf hookname = STRBUF_INIT;
> +
> +	struct option list_options[] = {
>  		OPT_END(),
>  	};
>  
> -	argc = parse_options(argc, argv, prefix, builtin_hook_options,
> +	argc = parse_options(argc, argv, prefix, list_options,
>  			     builtin_hook_usage, 0);
>  
> +	if (argc < 1) {
> +		usage_msg_opt(_("You must specify a hook event name to list."),
> +			      builtin_hook_usage, list_options);
> +	}
> +
> +	strbuf_addstr(&hookname, argv[0]);
> +
> +	head = hook_list(&hookname);
> +

More on strbuf usage later in another soon-to-be-sent E-Mail.

> +	if (list_empty(head)) {
> +		printf(_("no commands configured for hook '%s'\n"),
> +		       hookname.buf);
> +		strbuf_release(&hookname);
> +		return 0;
> +	}
> +
> +	list_for_each(pos, head) {
> +		struct hook *item = list_entry(pos, struct hook, list);
> +		if (item)
> +			printf("%s: %s\n",
> +			       config_scope_name(item->origin),
> +			       item->command.buf);
> +	}
> +
> +	clear_hook_list(head);
> +	strbuf_release(&hookname);
> +
>  	return 0;
>  }
> +
> +int cmd_hook(int argc, const char **argv, const char *prefix)
> +{
> +	struct option builtin_hook_options[] = {
> +		OPT_END(),
> +	};
> +	if (argc < 2)
> +		usage_with_options(builtin_hook_usage, builtin_hook_options);
> +
> +	if (!strcmp(argv[1], "list"))
> +		return list(argc - 1, argv + 1, prefix);
> +
> +	usage_with_options(builtin_hook_usage, builtin_hook_options);
> +}
> diff --git a/hook.c b/hook.c
> new file mode 100644
> index 0000000000..fede40e925
> --- /dev/null
> +++ b/hook.c
> @@ -0,0 +1,120 @@
> +#include "cache.h"
> +
> +#include "hook.h"
> +#include "config.h"
> +
> +void free_hook(struct hook *ptr)
> +{
> +	if (ptr) {
> +		strbuf_release(&ptr->command);
> +		free(ptr);
> +	}
> +}

Neither strbuf_release() nor free() need or should have a "if (ptr)" guard.

> +
> +static void append_or_move_hook(struct list_head *head, const char *command)
> +{
> +	struct list_head *pos = NULL, *tmp = NULL;
> +	struct hook *to_add = NULL;
> +
> +	/*
> +	 * remove the prior entry with this command; we'll replace it at the
> +	 * end.
> +	 */
> +	list_for_each_safe(pos, tmp, head) {
> +		struct hook *it = list_entry(pos, struct hook, list);
> +		if (!strcmp(it->command.buf, command)) {
> +		    list_del(pos);
> +		    /* we'll simply move the hook to the end */
> +		    to_add = it;
> +		    break;
> +		}
> +	}
> +
> +	if (!to_add) {
> +		/* adding a new hook, not moving an old one */
> +		to_add = xmalloc(sizeof(*to_add));
> +		strbuf_init(&to_add->command, 0);
> +		strbuf_addstr(&to_add->command, command);
> +	}
> +
> +	/* re-set the scope so we show where an override was specified */
> +	to_add->origin = current_config_scope();
> +
> +	list_add_tail(&to_add->list, head);
> +}
> +
> +static void remove_hook(struct list_head *to_remove)
> +{
> +	struct hook *hook_to_remove = list_entry(to_remove, struct hook, list);
> +	list_del(to_remove);
> +	free_hook(hook_to_remove);
> +}
> +
> +void clear_hook_list(struct list_head *head)
> +{
> +	struct list_head *pos, *tmp;
> +	list_for_each_safe(pos, tmp, head)
> +		remove_hook(pos);
> +}
> +
> +struct hook_config_cb
> +{
> +	struct strbuf *hookname;
> +	struct list_head *list;
> +};
> +
> +static int hook_config_lookup(const char *key, const char *value, void *cb_data)
> +{
> +	struct hook_config_cb *data = cb_data;
> +	const char *hook_key = data->hookname->buf;
> +	struct list_head *head = data->list;
> +
> +	if (!strcmp(key, hook_key)) {
> +		const char *command = value;
> +		struct strbuf hookcmd_name = STRBUF_INIT;
> +
> +		/*
> +		 * Check if a hookcmd with that name exists. If it doesn't,
> +		 * 'git_config_get_value()' is documented not to touch &command,
> +		 * so we don't need to do anything.
> +		 */
> +		strbuf_addf(&hookcmd_name, "hookcmd.%s.command", command);
> +		git_config_get_value(hookcmd_name.buf, &command);
> +
> +		if (!command) {
> +			strbuf_release(&hookcmd_name);
> +			BUG("git_config_get_value overwrote a string it shouldn't have");
> +		}
> +
> +		/*
> +		 * TODO: implement an option-getting callback, e.g.
> +		 *   get configs by pattern hookcmd.$value.*
> +		 *   for each key+value, do_callback(key, value, cb_data)
> +		 */
> +
> +		append_or_move_hook(head, command);
> +
> +		strbuf_release(&hookcmd_name);
> +	}
> +
> +	return 0;
> +}
> +
> +struct list_head* hook_list(const struct strbuf* hookname)
> +{
> +	struct strbuf hook_key = STRBUF_INIT;
> +	struct list_head *hook_head = xmalloc(sizeof(struct list_head));
> +	struct hook_config_cb cb_data = { &hook_key, hook_head };
> +
> +	INIT_LIST_HEAD(hook_head);
> +
> +	if (!hookname)
> +		return NULL;

...if a strbuf being passed in is NULL?

> [...]
> +ROOT=
> +if test_have_prereq MINGW
> +then
> +	# In Git for Windows, Unix-like paths work only in shell scripts;
> +	# `git.exe`, however, will prefix them with the pseudo root directory
> +	# (of the Unix shell). Let's accommodate for that.
> +	ROOT="$(cd / && pwd)"
> +fi

I didn't read up on previous rounds, but if we're squashing this into 02
having a seperate commit summarizing this little hack would be most
welcome, or have it in this commit message.

Isn't this sort of thing generally usable, maybe we can add it under a
longer variable name to test-lib.sh?

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 04/37] hook: include hookdir hook in list
  2021-03-11  2:10 ` [PATCH v8 04/37] hook: include hookdir hook in list Emily Shaffer
@ 2021-03-12  8:30   ` Ævar Arnfjörð Bjarmason
  2021-03-24 17:56     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  8:30 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> Historically, hooks are declared by placing an executable into
> $GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken
> from the config are more featureful than hooks placed in the $HOOKDIR,
> those hooks should not stop working for users who already have them.
> Let's list them to the user, but instead of displaying a config scope
> (e.g. "global: blah") we can prefix them with "hookdir:".
>
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> ---
>
> Notes:
>     Since v7, fix some nits from Jonathan Tan. The largest is to move reference to
>     "hookdir annotation" from this commit to the next one which introduces the
>     hook.runHookDir option.
>
>  builtin/hook.c                | 11 +++++++++--
>  hook.c                        | 17 +++++++++++++++++
>  hook.h                        |  1 +
>  t/t1360-config-based-hooks.sh | 19 +++++++++++++++++++
>  4 files changed, 46 insertions(+), 2 deletions(-)
>
> diff --git a/builtin/hook.c b/builtin/hook.c
> index bb64cd77ca..c8fbfbb39d 100644
> --- a/builtin/hook.c
> +++ b/builtin/hook.c
> @@ -40,10 +40,15 @@ static int list(int argc, const char **argv, const char *prefix)
>  
>  	list_for_each(pos, head) {
>  		struct hook *item = list_entry(pos, struct hook, list);
> -		if (item)
> +		item = list_entry(pos, struct hook, list);
> +		if (item) {
> +			/* Don't translate 'hookdir' - it matches the config */

Let's prefix comments for translators with /* TRANSLATORS: .., see the
coding style doc. That's what they'll see, and this is useful to them.

Better yet have a note here about the first argument being 'system',
'local' etc., which I had to source spelunge for, and translators won't
have any idea about unless the magic parameter is documented.

> +setup_hookdir () {
> +	mkdir .git/hooks
> +	write_script .git/hooks/pre-commit <<-EOF
> +	echo \"Legacy Hook\"

Nit, "'s not needed, but it also seems nothing uses this, so if it's
just a pass-through script either "exit 0", or actually check if it's
run or something?

> [...]
> +test_expect_success 'git hook list shows hooks from the hookdir' '
> +	setup_hookdir &&
> +
> +	cat >expected <<-EOF &&
> +	hookdir: $(pwd)/.git/hooks/pre-commit
> +	EOF
> +
> +	git hook list pre-commit >actual &&
> +	test_cmp expected actual
> +'

Ah, so it's just checking if it exists...

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 05/37] hook: teach hook.runHookDir
  2021-03-11  2:10 ` [PATCH v8 05/37] hook: teach hook.runHookDir Emily Shaffer
@ 2021-03-12  8:33   ` Ævar Arnfjörð Bjarmason
  2021-03-24 18:46     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  8:33 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> +	switch (should_run_hookdir) {
> +		case HOOKDIR_NO:

Style: case shouldn't be indented

> +			strbuf_addstr(&hookdir_annotation, _(" (will not run)"));
> +			break;
> +		case HOOKDIR_ERROR:
> +			strbuf_addstr(&hookdir_annotation, _(" (will error and not run)"));
> +			break;
> +		case HOOKDIR_INTERACTIVE:
> +			strbuf_addstr(&hookdir_annotation, _(" (will prompt)"));
> +			break;
> +		case HOOKDIR_WARN:
> +			strbuf_addstr(&hookdir_annotation, _(" (will warn but run)"));
> +			break;
> +		case HOOKDIR_YES:
> +		/*
> +		 * The default behavior should agree with
> +		 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
> +		 * do the default behavior.
> +		 */
> +		case HOOKDIR_UNKNOWN:
> +		default:
> +			break;

We should avoid this sort of translation lego.

> +	}
> +
>  	list_for_each(pos, head) {
>  		struct hook *item = list_entry(pos, struct hook, list);
>  		item = list_entry(pos, struct hook, list);
>  		if (item) {
>  			/* Don't translate 'hookdir' - it matches the config */
> -			printf("%s: %s\n",
> +			printf("%s: %s%s\n",

native speakers in some languages to read the sentance backwards.
Because if you concatenate strings like this you force.

(We don't currently have a RTL language in po/, still, but let's not
create churn for if/when we do if we can help it)>


I have a patch on top to fix this, will send it as some general reply of
proposed fixup.s

>  			       (item->from_hookdir
> +	git hook list pre-commit >actual &&
> +	# the hookdir annotation is translated
> +	test_i18ncmp expected actual

This (and the rest of test_i18ncmp in this series) can and should just
be "test_cmp" or "test_i18ncmp", the poison mode is dead. See my recent
patches to search/replace test_i18ncmp.

The reason the function isn't gone entirely was to help a series like
yours in "seen", but if we're re-rolling...

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 06/37] hook: implement hookcmd.<name>.skip
  2021-03-11  2:10 ` [PATCH v8 06/37] hook: implement hookcmd.<name>.skip Emily Shaffer
@ 2021-03-12  8:49   ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  8:49 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> +	cat >expected <<-EOF &&
> +	no commands configured for hook '\''pre-commit'\''
> +	EOF
> +
> +	git hook list pre-commit >actual &&
> +	test_i18ncmp expected actual
> +'
> +
> +test_expect_success 'git hook list ignores skip referring to unused hookcmd' '
> +	test_config hookcmd.abc.command "/path/abc" --add &&
> +	test_config hookcmd.abc.skip "true" --add &&
> +
> +	cat >expected <<-EOF &&
> +	no commands configured for hook '\''pre-commit'\''

ditto on the "echo" comment in a previous mail, looks like we can avoid
both of these entirely.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 07/37] parse-options: parse into strvec
  2021-03-11  2:10 ` [PATCH v8 07/37] parse-options: parse into strvec Emily Shaffer
@ 2021-03-12  8:50   ` Ævar Arnfjörð Bjarmason
  2021-03-24 20:34     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  8:50 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt
> index 5a60bbfa7f..f79b17e7fc 100644
> --- a/Documentation/technical/api-parse-options.txt
> +++ b/Documentation/technical/api-parse-options.txt
> @@ -173,6 +173,13 @@ There are some macros to easily define options:
>  	The string argument is stored as an element in `string_list`.
>  	Use of `--no-option` will clear the list of preceding values.
>  
> +`OPT_STRVEC(short, long, &struct strvec, arg_str, description)`::
> +	Introduce an option with a string argument, meant to be specified
> +	multiple times.
> +	The string argument is stored as an element in `strvec`, and later
> +	arguments are added to the same `strvec`.
> +	Use of `--no-option` will clear the list of preceding values.
> +
>  `OPT_INTEGER(short, long, &int_var, description)`::
>  	Introduce an option with integer argument.
>  	The integer is put into `int_var`.
> diff --git a/parse-options-cb.c b/parse-options-cb.c
> index 4542d4d3f9..c2451dfb1b 100644
> --- a/parse-options-cb.c
> +++ b/parse-options-cb.c
> @@ -207,6 +207,22 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset)
>  	return 0;
>  }
>  
> +int parse_opt_strvec(const struct option *opt, const char *arg, int unset)
> +{
> +	struct strvec *v = opt->value;
> +
> +	if (unset) {
> +		strvec_clear(v);
> +		return 0;
> +	}
> +
> +	if (!arg)
> +		return -1;
> +
> +	strvec_push(v, arg);
> +	return 0;
> +}
> +
>  int parse_opt_noop_cb(const struct option *opt, const char *arg, int unset)
>  {
>  	return 0;
> diff --git a/parse-options.h b/parse-options.h
> index ff6506a504..44c4ac08e9 100644
> --- a/parse-options.h
> +++ b/parse-options.h
> @@ -177,6 +177,9 @@ struct option {
>  #define OPT_STRING_LIST(s, l, v, a, h) \
>  				    { OPTION_CALLBACK, (s), (l), (v), (a), \
>  				      (h), 0, &parse_opt_string_list }
> +#define OPT_STRVEC(s, l, v, a, h) \
> +				    { OPTION_CALLBACK, (s), (l), (v), (a), \
> +				      (h), 0, &parse_opt_strvec }
>  #define OPT_UYN(s, l, v, h)         { OPTION_CALLBACK, (s), (l), (v), NULL, \
>  				      (h), PARSE_OPT_NOARG, &parse_opt_tertiary }
>  #define OPT_EXPIRY_DATE(s, l, v, h) \
> @@ -296,6 +299,7 @@ int parse_opt_commits(const struct option *, const char *, int);
>  int parse_opt_commit(const struct option *, const char *, int);
>  int parse_opt_tertiary(const struct option *, const char *, int);
>  int parse_opt_string_list(const struct option *, const char *, int);
> +int parse_opt_strvec(const struct option *, const char *, int);
>  int parse_opt_noop_cb(const struct option *, const char *, int);
>  enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx,
>  					   const struct option *,

Nice, seems very useful.

But let's add a test in test-parse-options.c like we have for
string_list?

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 08/37] hook: add 'run' subcommand
  2021-03-11  2:10 ` [PATCH v8 08/37] hook: add 'run' subcommand Emily Shaffer
@ 2021-03-12  8:54   ` Ævar Arnfjörð Bjarmason
  2021-03-24 21:29     ` Emily Shaffer
  2021-03-12 10:22   ` Junio C Hamano
  1 sibling, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  8:54 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

>  'git hook' list <hook-name>
> +'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>

[...]

> +	switch (cfg)
> +	{
> +		case HOOKDIR_ERROR:

Overly indented case statements again.

> +			fprintf(stderr, _("Skipping legacy hook at '%s'\n"),
> +				path);
> +			/* FALLTHROUGH */
> +		case HOOKDIR_NO:
> +			return 0;
> +		case HOOKDIR_WARN:
> +			fprintf(stderr, _("Running legacy hook at '%s'\n"),
> +				path);
> +			return 1;
> +		case HOOKDIR_INTERACTIVE:
> +			do {
> +				/*
> +				 * TRANSLATORS: Make sure to include [Y] and [n]
> +				 * in your translation. Only English input is
> +				 * accepted. Default option is "yes".
> +				 */
> +				fprintf(stderr, _("Run '%s'? [Yn] "), path);

Nit: [Y/n]

> +				} else if (starts_with(prompt.buf, "y")) {

So also "Y", "yes" and "yellow"...

> [...]
>  	git hook list pre-commit >actual &&
>  	# the hookdir annotation is translated
> -	test_i18ncmp expected actual
> +	test_i18ncmp expected actual &&
> +
> +	test_write_lines n | git hook run pre-commit 2>actual &&
> +	! grep "Legacy Hook" actual &&
> +
> +	test_write_lines y | git hook run pre-commit 2>actual &&
> +	grep "Legacy Hook" actual
> +'
> +
> +test_expect_success 'inline hook definitions execute oneliners' '
> +	test_config hook.pre-commit.command "echo \"Hello World\"" &&
> +
> +	echo "Hello World" >expected &&
> +
> +	# hooks are run with stdout_to_stderr = 1
> +	git hook run pre-commit 2>actual &&
> +	test_cmp expected actual
> +'
> +
> +test_expect_success 'inline hook definitions resolve paths' '
> +	write_script sample-hook.sh <<-EOF &&
> +	echo \"Sample Hook\"
> +	EOF
> +
> +	test_when_finished "rm sample-hook.sh" &&
> +
> +	test_config hook.pre-commit.command "\"$(pwd)/sample-hook.sh\"" &&
> +
> +	echo \"Sample Hook\" >expected &&
> +
> +	# hooks are run with stdout_to_stderr = 1
> +	git hook run pre-commit 2>actual &&
> +	test_cmp expected actual
> +'
> +
> +test_expect_success 'hookdir hook included in git hook run' '
> +	setup_hookdir &&
> +
> +	echo \"Legacy Hook\" >expected &&
> +
> +	# hooks are run with stdout_to_stderr = 1
> +	git hook run pre-commit 2>actual &&
> +	test_cmp expected actual
> +'
> +
> +test_expect_success 'out-of-repo runs excluded' '
> +	setup_hooks &&
> +
> +	nongit test_must_fail git hook run pre-commit
>  '
>  
>  test_expect_success 'hook.runHookDir is tolerant to unknown values' '

No tests for --env or --arg?

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 10/37] hook: support passing stdin to hooks
  2021-03-11  2:10 ` [PATCH v8 10/37] hook: support passing stdin to hooks Emily Shaffer
@ 2021-03-12  9:00   ` Ævar Arnfjörð Bjarmason
  2021-03-12 10:22   ` Junio C Hamano
  1 sibling, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  9:00 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> On the frontend, this is supported by asking for a file path, rather
> than by reading stdin. Reading directly from stdin would involve caching
> the entire stdin (to memory or to disk) and reading it back from the
> beginning to each hook. We'd want to support cases like insufficient
> memory or storage for the file. While this may prove useful later, for
> now the path of least resistance is to just ask the user to make this
> interim file themselves.

We need to worry about cases where we wouldn't have enough memory to
buffer the stdin, but still need to then do repo operations on the input
from such a file, presumably some giant pre-receive update or something.

Seems unlikely, and the convenience of having stdin just work by just
allocating that seems appealing, but let's read on to the rest of the
series...

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 17/37] hooks: allow callers to capture output
  2021-03-11  2:10 ` [PATCH v8 17/37] hooks: allow callers to capture output Emily Shaffer
@ 2021-03-12  9:08   ` Ævar Arnfjörð Bjarmason
  2021-03-24 21:54     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  9:08 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> Some server-side hooks will require capturing output to send over
> sideband instead of printing directly to stderr. Expose that capability.

So added here in 17/37 and not used until 30/37. As a point on
readability (this isn't the first such patch) I think it would be better
to just squash those together with some "since we now need access to
consume_sideband in hooks, do that ...".

If there's a much larger API it makes sense to do it as another step...

>  hook.c | 3 ++-
>  hook.h | 8 ++++++++
>  2 files changed, 10 insertions(+), 1 deletion(-)
>
> diff --git a/hook.c b/hook.c
> index e16b082cbd..2322720ffe 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -256,6 +256,7 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)
>  	o->dir = NULL;
>  	o->feed_pipe = NULL;
>  	o->feed_pipe_ctx = NULL;
> +	o->consume_sideband = NULL;
>  }
>  
>  void run_hooks_opt_init_async(struct run_hooks_opt *o)
> @@ -434,7 +435,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
>  				   pick_next_hook,
>  				   notify_start_failure,
>  				   options->feed_pipe,
> -				   NULL,
> +				   options->consume_sideband,
>  				   notify_hook_finished,
>  				   &cb_data,
>  				   "hook",
> diff --git a/hook.h b/hook.h
> index ecf0228a46..4ff9999b04 100644
> --- a/hook.h
> +++ b/hook.h
> @@ -78,6 +78,14 @@ struct run_hooks_opt
>  	feed_pipe_fn feed_pipe;
>  	void *feed_pipe_ctx;
>  
> +	/*
> +	 * Populate this to capture output and prevent it from being printed to
> +	 * stderr. This will be passed directly through to
> +	 * run_command:run_parallel_processes(). See t/helper/test-run-command.c
> +	 * for an example.
> +	 */
> +	consume_sideband_fn consume_sideband;
> +
>  	/* Number of threads to parallelize across */
>  	int jobs;

...but this scaffolding is rather trivial.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 32/37] post-update: use hook.h library
  2021-03-11  2:10 ` [PATCH v8 32/37] post-update: use hook.h library Emily Shaffer
@ 2021-03-12  9:14   ` Ævar Arnfjörð Bjarmason
  2021-03-30  0:01     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  9:14 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> By using run_hooks() instead of run_hook_le(), 'post-update' hooks can
> be specified in the config as well as the hookdir.

Looking ahead in the series no tests for this, seems like a good thing
to have some at least trivial tests for each hook and their config
invocation.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'
  2021-03-11  2:10 ` [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate' Emily Shaffer
@ 2021-03-12  9:21   ` Ævar Arnfjörð Bjarmason
  2021-03-30  0:03     ` Emily Shaffer
  2021-03-31 21:47     ` Emily Shaffer
  2021-03-12 23:29   ` [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate' Emily Shaffer
  1 sibling, 2 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  9:21 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> By using the new 'git hook run' subcommand to run 'sendemail-validate',
> we can reduce the boilerplate needed to run this hook in perl. Using
> config-based hooks also allows us to run 'sendemail-validate' hooks that
> were configured globally when running 'git send-email' from outside of a
> Git directory, alongside other benefits like multihooks and
> parallelization.
>
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> ---
>  git-send-email.perl   | 21 ++++-----------------
>  t/t9001-send-email.sh | 11 +----------
>  2 files changed, 5 insertions(+), 27 deletions(-)
>
> diff --git a/git-send-email.perl b/git-send-email.perl
> index 1f425c0809..73e1e0b51a 100755
> --- a/git-send-email.perl
> +++ b/git-send-email.perl
> @@ -1941,23 +1941,10 @@ sub unique_email_list {
>  sub validate_patch {
>  	my ($fn, $xfer_encoding) = @_;
>  
> -	if ($repo) {
> -		my $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),
> -					    'sendemail-validate');
> -		my $hook_error;
> -		if (-x $validate_hook) {
> -			my $target = abs_path($fn);
> -			# The hook needs a correct cwd and GIT_DIR.
> -			my $cwd_save = cwd();
> -			chdir($repo->wc_path() or $repo->repo_path())
> -				or die("chdir: $!");
> -			local $ENV{"GIT_DIR"} = $repo->repo_path();
> -			$hook_error = "rejected by sendemail-validate hook"
> -				if system($validate_hook, $target);
> -			chdir($cwd_save) or die("chdir: $!");
> -		}
> -		return $hook_error if $hook_error;
> -	}
> +	my $target = abs_path($fn);
> +	return "rejected by sendemail-validate hook"
> +		if system(("git", "hook", "run", "sendemail-validate", "-a",
> +				$target));

I see it's just moving code around, but since we're touching this:

This conflates the hook exit code with a general failure to invoke it,
Perl's system().

Not a big deal in this case, but there's two other existing system()
invocations which use the right blurb for it:


	system('sh', '-c', $editor.' "$@"', $editor, $_);
	if (($? & 127) || ($? >> 8)) {
		die(__("the editor exited uncleanly, aborting everything"));
	}

Makes sense to do something similar here for consistency. See "perldoc
-f system" for an example.

>  
>  	# Any long lines will be automatically fixed if we use a suitable transfer
>  	# encoding.
> diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
> index 4eee9c3dcb..456b471c5c 100755
> --- a/t/t9001-send-email.sh
> +++ b/t/t9001-send-email.sh
> @@ -2101,16 +2101,7 @@ test_expect_success $PREREQ 'invoke hook' '
>  	mkdir -p .git/hooks &&
>  
>  	write_script .git/hooks/sendemail-validate <<-\EOF &&
> -	# test that we have the correct environment variable, pwd, and
> -	# argument
> -	case "$GIT_DIR" in
> -	*.git)
> -		true
> -		;;
> -	*)
> -		false
> -		;;
> -	esac &&
> +	# test that we have the correct argument

This and getting rid of these Perl/Python/whatever special cases is very
nice.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 36/37] run-command: stop thinking about hooks
  2021-03-11  2:10 ` [PATCH v8 36/37] run-command: stop thinking about hooks Emily Shaffer
@ 2021-03-12  9:23   ` Ævar Arnfjörð Bjarmason
  2021-03-30  0:07     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  9:23 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> hook.h has replaced all run-command.h hook-related functionality.
> run-command.h:run_hooks_le/ve and find_hook are no longer used anywhere
> in the codebase. So, let's delete the dead code - or, in the one case
> where it's still needed, move it to an internal function in hook.c.

Similar to other comments about squashing, I think just having this
happen incrementally as we remove whatever is the last user of the
function would be better.

E.g. find_hook() is last used in one commit, run_hook*() in another...

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages
  2021-03-11  2:10 ` [PATCH v8 37/37] docs: unify githooks and git-hook manpages Emily Shaffer
@ 2021-03-12  9:29   ` Ævar Arnfjörð Bjarmason
  2021-03-30  0:10     ` Emily Shaffer
  2021-04-07  2:36   ` Junio C Hamano
  1 sibling, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  9:29 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Thu, Mar 11 2021, Emily Shaffer wrote:

> By showing the list of all hooks in 'git help hook' for users to refer
> to, 'git help hook' becomes a one-stop shop for hook authorship. Since
> some may still have muscle memory for 'git help githooks', though,
> reference the 'git hook' commands and otherwise don't remove content.

I think this should at least have something like what my b6a8d09f6d8 (gc
docs: include the "gc.*" section from "config" in "gc", 2019-04-07) has
on top, i.e.:
    
    diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
    index 4ad31ac360a..5c9af30b43e 100644
    --- a/Documentation/git-hook.txt
    +++ b/Documentation/git-hook.txt
    @@ -150,10 +150,18 @@ message body and cannot be parallelized.
     
     CONFIGURATION
     -------------
    +
    +The below documentation is the same as what's found in
    +linkgit:git-config[1]:
    +
     include::config/hook.txt[]
     
     HOOKS
     -----
    +
    +The below documentation is the same as what's found in
    +linkgit:githooks[5]:
    +
     include::native-hooks.txt[]
     
     GIT

But I also don't think we should demote githooks(5) as the canonical doc
page for the hooks themselves.

If you run this in your terminal:

    man 5 git<TAB>

You'll get:

    gitattributes         gitignore             gitmailmap            gitrepository-layout  
    githooks              git-lfs-config        gitmodules            gitweb.conf 

(Well, maybe not the lfs-part, but whatever...).

We should move more in the direction of splitting up our "file format"
docs from implementation, like the git-hook runner.

I'm somewhat negative on including it at all in git-hook(1). For the
config section it makes sense, and it's consistent with established doc
convention.

But including githooks(5) is around 2/3 of the resulting manpage, I
think just a link is better.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 00/37] config-based hooks
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (37 preceding siblings ...)
  2021-03-11 22:26 ` [PATCH v8 00/37] config-based hooks Junio C Hamano
@ 2021-03-12  9:49 ` Ævar Arnfjörð Bjarmason
  2021-03-17 18:41   ` Emily Shaffer
  2021-03-12 11:13 ` Ævar Arnfjörð Bjarmason
  2021-05-27  0:08 ` [PATCH v9 00/37] propose " Emily Shaffer
  40 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12  9:49 UTC (permalink / raw)
  To: Emily Shaffer
  Cc: git, Jeff King, Junio C Hamano, James Ramsay, Jonathan Nieder,
	brian m. carlson, Phillip Wood, Josh Steadmon,
	Johannes Schindelin, Jonathan Tan


On Thu, Mar 11 2021, Emily Shaffer wrote:

> Since v7:
> - Addressed Jonathan Tan's review of part I
> - Addressed Junio's review of part I and II
> - Combined parts I and II
>
> I think the updates to patch 1 between the rest of the work I've been
> doing probably have covered Ævar's comments.

A range-diff between iterations of such a large series would be most
useful. Do you have a public repo with tags or whatever the different
versions, for those who'd like an easier way to follow along the
differing versions than scraping the ML archive?

While reading this I came up with the following fixup patches on top,
for discussion, maybe not something you want as-is:
	
	 Documentation/git-hook.txt |  8 +++++
	 builtin/bugreport.c        |  8 +++--
	 builtin/commit.c           |  3 +-
	 builtin/hook.c             | 79 ++++++++++++++++++++--------------------------
	 builtin/merge.c            |  3 +-
	 builtin/receive-pack.c     | 11 +++----
	 hook.c                     | 21 +++++-------
	 hook.h                     |  5 +--
	 refs.c                     |  4 ++-
	 sequencer.c                |  4 ++-
	 10 files changed, 73 insertions(+), 73 deletions(-)
	
	diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
	index 4ad31ac360a..5c9af30b43e 100644
	--- a/Documentation/git-hook.txt
	+++ b/Documentation/git-hook.txt
	@@ -150,10 +150,18 @@ message body and cannot be parallelized.
	 
	 CONFIGURATION
	 -------------
	+
	+The below documentation is the same as what's found in
	+linkgit:git-config[1]:
	+
	 include::config/hook.txt[]
	 
	 HOOKS
	 -----
	+
	+The below documentation is the same as what's found in
	+linkgit:githooks[5]:
	+
	 include::native-hooks.txt[]
	 
Noted in another reply, including it here for completeness.

	 GIT
	diff --git a/builtin/bugreport.c b/builtin/bugreport.c
	index 04467cd1d3a..b64e53fd625 100644
	--- a/builtin/bugreport.c
	+++ b/builtin/bugreport.c
	@@ -81,9 +81,13 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)
	 		return;
	 	}
	 
	-	for (i = 0; i < ARRAY_SIZE(hook); i++)
	-		if (hook_exists(hook[i], HOOKDIR_USE_CONFIG))
	+	for (i = 0; i < ARRAY_SIZE(hook); i++) {
	+		struct strbuf config;
	+		strbuf_addf(&config, "hook.%s.config", hook[i]);
	+		if (hook_exists(hook[i], config.buf, HOOKDIR_USE_CONFIG))
	 			strbuf_addf(hook_info, "%s\n", hook[i]);
	+		strbuf_release(&config);
	+	}
	 }

Less strbuf, see below.
	 
	 static const char * const bugreport_usage[] = {
	diff --git a/builtin/commit.c b/builtin/commit.c
	index 31df571f123..fc9f1f5ee58 100644
	--- a/builtin/commit.c
	+++ b/builtin/commit.c
	@@ -984,7 +984,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
	 		return 0;
	 	}
	 
	-	if (!no_verify && hook_exists("pre-commit", HOOKDIR_USE_CONFIG)) {
	+	if (!no_verify && hook_exists("pre-commit", "hook.pre-commit.command",
	+				      HOOKDIR_USE_CONFIG)) {
	 		/*
	 		 * Re-read the index as pre-commit hook could have updated it,
	 		 * and write it out as a tree.  We must do this before we invoke


..ditto.

	diff --git a/builtin/hook.c b/builtin/hook.c
	index b4f4adb1dea..d0b56ee47f8 100644
	--- a/builtin/hook.c
	+++ b/builtin/hook.c
	@@ -18,8 +18,6 @@ static enum hookdir_opt should_run_hookdir;
	 static int list(int argc, const char **argv, const char *prefix)
	 {
	 	struct list_head *head, *pos;
	-	struct strbuf hookname = STRBUF_INIT;
	-	struct strbuf hookdir_annotation = STRBUF_INIT;
	 
	 	struct option list_options[] = {
	 		OPT_END(),
	@@ -33,67 +31,60 @@ static int list(int argc, const char **argv, const char *prefix)
	 			      builtin_hook_usage, list_options);
	 	}
	 
	-	strbuf_addstr(&hookname, argv[0]);
	-
	-	head = hook_list(&hookname);
	+	head = hook_list(argv[0]);
	 
	 	if (list_empty(head)) {
	 		printf(_("no commands configured for hook '%s'\n"),
	-		       hookname.buf);
	-		strbuf_release(&hookname);
	+		       argv[0]);
	 		return 0;
	 	}
	 
	-	switch (should_run_hookdir) {
	-		case HOOKDIR_NO:
	-			strbuf_addstr(&hookdir_annotation, _(" (will not run)"));
	-			break;
	-		case HOOKDIR_ERROR:
	-			strbuf_addstr(&hookdir_annotation, _(" (will error and not run)"));
	-			break;
	-		case HOOKDIR_INTERACTIVE:
	-			strbuf_addstr(&hookdir_annotation, _(" (will prompt)"));
	-			break;
	-		case HOOKDIR_WARN:
	-			strbuf_addstr(&hookdir_annotation, _(" (will warn but run)"));
	-			break;
	-		case HOOKDIR_YES:
	-		/*
	-		 * The default behavior should agree with
	-		 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
	-		 * do the default behavior.
	-		 */
	-		case HOOKDIR_UNKNOWN:
	-		default:
	-			break;
	-	}
	-
	 	list_for_each(pos, head) {
	 		struct hook *item = list_entry(pos, struct hook, list);
	 		item = list_entry(pos, struct hook, list);
	 		if (item) {
	-			/* Don't translate 'hookdir' - it matches the config */
	-			printf("%s: %s%s\n",
	-			       (item->from_hookdir
	+			const char *scope = item->from_hookdir
	 				? "hookdir"
	-				: config_scope_name(item->origin)),
	-			       item->command.buf,
	-			       (item->from_hookdir
	-				? hookdir_annotation.buf
	-				: ""));
	+				: config_scope_name(item->origin);
	+			switch (should_run_hookdir) {
	+			case HOOKDIR_NO:
	+				printf(_("%s: %s (will not run)\n"),
	+				       scope, item->command.buf);
	+				break;
	+			case HOOKDIR_ERROR:
	+				printf(_("%s: %s (will error and not run)\n"),
	+				       scope, item->command.buf);
	+				break;
	+			case HOOKDIR_INTERACTIVE:
	+				printf(_("%s: %s (will prompt)\n"),
	+				       scope, item->command.buf);
	+				break;
	+			case HOOKDIR_WARN:
	+				printf(_("%s: %s (will warn but run)\n"),
	+				       scope, item->command.buf);
	+				break;
	+			case HOOKDIR_YES:
	+				/*
	+				 * The default behavior should agree with
	+				 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
	+				 * do the default behavior.
	+				 */
	+			case HOOKDIR_UNKNOWN:
	+			default:
	+				printf(_("%s: %s\n"),
	+				       scope, item->command.buf);
	+				break;
	+			}
	 		}
	 	}
	 
	 	clear_hook_list(head);
	-	strbuf_release(&hookdir_annotation);
	-	strbuf_release(&hookname);
	 
	 	return 0;
	 }

I think this is better to avoid i18n lego, as noted in another reply
(but I didn't include the patch).

More on strbuf below:
	 
	 static int run(int argc, const char **argv, const char *prefix)
	 {
	-	struct strbuf hookname = STRBUF_INIT;
	 	struct run_hooks_opt opt;
	 	int rc = 0;
	 
	@@ -118,12 +109,10 @@ static int run(int argc, const char **argv, const char *prefix)
	 		usage_msg_opt(_("You must specify a hook event to run."),
	 			      builtin_hook_usage, run_options);
	 
	-	strbuf_addstr(&hookname, argv[0]);
	 	opt.run_hookdir = should_run_hookdir;
	 
	-	rc = run_hooks(hookname.buf, &opt);
	+	rc = run_hooks(argv[0], &opt);
	 
	-	strbuf_release(&hookname);
	 	run_hooks_opt_clear(&opt);
	 
	 	return rc;
	diff --git a/builtin/merge.c b/builtin/merge.c
	index 3a2af257a6b..df4ff72fbc7 100644
	--- a/builtin/merge.c
	+++ b/builtin/merge.c
	@@ -848,7 +848,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)
	 	 * and write it out as a tree.  We must do this before we invoke
	 	 * the editor and after we invoke run_status above.
	 	 */
	-	if (hook_exists("pre-merge-commit", HOOKDIR_USE_CONFIG))
	+	if (hook_exists("pre-merge-commit", "hook.pre-merge-commit.command",
	+			HOOKDIR_USE_CONFIG))
	 		discard_cache();
	 	read_cache_from(index_file);
	 	strbuf_addbuf(&msg, &merge_msg);
	diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
	index eaedeeb1e8b..a76069ea592 100644
	--- a/builtin/receive-pack.c
	+++ b/builtin/receive-pack.c
	@@ -1123,12 +1123,10 @@ static int run_proc_receive_hook(struct command *commands,
	 	int version = 0;
	 	int code;
	 
	-	struct strbuf hookname = STRBUF_INIT;
	 	struct hook *proc_receive = NULL;
	 	struct list_head *pos, *hooks;
	 
	-	strbuf_addstr(&hookname, "proc-receive");
	-	hooks = hook_list(&hookname);
	+	hooks = hook_list("proc-receive");
	 
	 	list_for_each(pos, hooks) {
	 		if (proc_receive) {
	@@ -1460,8 +1458,6 @@ static const char *push_to_deploy(unsigned char *sha1,
	 	return NULL;
	 }
	 
	-static const char *push_to_checkout_hook = "push-to-checkout";
	-
	 static const char *push_to_checkout(unsigned char *hash,
	 				    struct strvec *env,
	 				    const char *work_tree)
	@@ -1472,7 +1468,7 @@ static const char *push_to_checkout(unsigned char *hash,
	 	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
	 	strvec_pushv(&opt.env, env->v);
	 	strvec_push(&opt.args, hash_to_hex(hash));
	-	if (run_hooks(push_to_checkout_hook, &opt)) {
	+	if (run_hooks("push-to-checkout", &opt)) {
	 		run_hooks_opt_clear(&opt);
	 		return "push-to-checkout hook declined";
	 	} else {
	@@ -1502,7 +1498,8 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w
	 
	 	strvec_pushf(&env, "GIT_DIR=%s", absolute_path(git_dir));
	 
	-	if (!hook_exists(push_to_checkout_hook, HOOKDIR_USE_CONFIG))
	+	if (!hook_exists("push-to-checkout", "hook.push-to-checkout.command",
	+			 HOOKDIR_USE_CONFIG))
	 		retval = push_to_deploy(sha1, &env, work_tree);
	 	else
	 		retval = push_to_checkout(sha1, &env, work_tree);
	diff --git a/hook.c b/hook.c
	index 7f6f3b9a616..49c3861ce00 100644
	--- a/hook.c
	+++ b/hook.c
	@@ -247,7 +247,7 @@ static const char *find_legacy_hook(const char *name)
	 }
	 
	 
	-struct list_head* hook_list(const struct strbuf* hookname)
	+struct list_head* hook_list(const char *hookname)
	 {
	 	struct strbuf hook_key = STRBUF_INIT;
	 	struct list_head *hook_head = xmalloc(sizeof(struct list_head));
	@@ -256,14 +256,14 @@ struct list_head* hook_list(const struct strbuf* hookname)
	 	INIT_LIST_HEAD(hook_head);
	 
	 	if (!hookname)
	-		return NULL;
	+		BUG("???");;
	 
	-	strbuf_addf(&hook_key, "hook.%s.command", hookname->buf);
	+	strbuf_addf(&hook_key, "hook.%s.command", hookname);
	 
	 	git_config(hook_config_lookup, &cb_data);
	 
	 	if (have_git_dir()) {
	-		const char *legacy_hook_path = find_legacy_hook(hookname->buf);
	+		const char *legacy_hook_path = find_legacy_hook(hookname);
	 
	 		/* Unconditionally add legacy hook, but annotate it. */
	 		if (legacy_hook_path) {
	@@ -300,10 +300,10 @@ void run_hooks_opt_init_async(struct run_hooks_opt *o)
	 	o->jobs = configured_hook_jobs();
	 }
	 
	-int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)
	+int hook_exists(const char *hookname, const char *hook_config,
	+		enum hookdir_opt should_run_hookdir)
	 {
	 	const char *value = NULL; /* throwaway */
	-	struct strbuf hook_key = STRBUF_INIT;
	 	int could_run_hookdir;
	 
	 	if (should_run_hookdir == HOOKDIR_USE_CONFIG)
	@@ -314,9 +314,7 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)
	 				should_run_hookdir == HOOKDIR_YES)
	 				&& !!find_legacy_hook(hookname);
	 
	-	strbuf_addf(&hook_key, "hook.%s.command", hookname);
	-
	-	return (!git_config_get_value(hook_key.buf, &value)) || could_run_hookdir;
	+	return (!git_config_get_value(hook_config, &value)) || could_run_hookdir;
	 }
	 
	 void run_hooks_opt_clear(struct run_hooks_opt *o)
	@@ -438,7 +436,6 @@ static int notify_hook_finished(int result,
	 
	 int run_hooks(const char *hookname, struct run_hooks_opt *options)
	 {
	-	struct strbuf hookname_str = STRBUF_INIT;
	 	struct list_head *to_run, *pos = NULL, *tmp = NULL;
	 	struct hook_cb_data cb_data = { 0, NULL, NULL, options };
	 
	@@ -448,9 +445,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
	 	if (options->path_to_stdin && options->feed_pipe)
	 		BUG("choose only one method to populate stdin");
	 
	-	strbuf_addstr(&hookname_str, hookname);
	-
	-	to_run = hook_list(&hookname_str);
	+	to_run = hook_list(hookname);
	 
	 	list_for_each_safe(pos, tmp, to_run) {
	 		struct hook *hook = list_entry(pos, struct hook, list);
	diff --git a/hook.h b/hook.h
	index 4ff9999b049..bfbbf36882d 100644
	--- a/hook.h
	+++ b/hook.h
	@@ -26,7 +26,7 @@ struct hook {
	  * Provides a linked list of 'struct hook' detailing commands which should run
	  * in response to the 'hookname' event, in execution order.
	  */
	-struct list_head* hook_list(const struct strbuf *hookname);
	+struct list_head* hook_list(const char *hookname);
	 
	 enum hookdir_opt
	 {
	@@ -123,7 +123,8 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);
	  * Like with run_hooks, if you take a --run-hookdir flag, reflect that
	  * user-specified behavior here instead.
	  */
	-int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);
	+int hook_exists(const char *hookname, const char *hook_config,
	+		enum hookdir_opt should_run_hookdir);
	 
	 /*
	  * Runs all hooks associated to the 'hookname' event in order. Each hook will be
	diff --git a/refs.c b/refs.c
	index 334fdd9103c..f01995fe64f 100644
	--- a/refs.c
	+++ b/refs.c
	@@ -1966,7 +1966,9 @@ static int run_transaction_hook(struct ref_transaction *transaction,
	 
	 	run_hooks_opt_init_async(&opt);
	 
	-	if (!hook_exists("reference-transaction", HOOKDIR_USE_CONFIG))
	+	if (!hook_exists("reference-transaction",
	+			 "hook.reference-transaction.command",
	+			 HOOKDIR_USE_CONFIG))
	 		return ret;
	 
	 	strvec_push(&opt.args, state);
	diff --git a/sequencer.c b/sequencer.c
	index 34ff275f0d1..52c067c1688 100644
	--- a/sequencer.c
	+++ b/sequencer.c
	@@ -1436,7 +1436,9 @@ static int try_to_commit(struct repository *r,
	 		}
	 	}
	 
	-	if (hook_exists("prepare-commit-msg", HOOKDIR_USE_CONFIG)) {
	+	if (hook_exists("prepare-commit-msg",
	+			"hook.prepare-commit-msg.command",
	+			HOOKDIR_USE_CONFIG)) {
	 		res = run_prepare_commit_msg_hook(r, msg, hook_commit);
	 		if (res)
	 			goto out;

There was another reply (from JT I believe, but didn't go back and look
it up) about the over use of strbuf.

I tend to agree, as much as I love the API it's really not better to
write C with it if all you need is a const char* that's never modified,
particularly if you get it from elsewhere.

So it's really not meant for or good for "everything we need a const
char*", but to avoid verbose realloc() dances all over the place, and
for things like getline() loops without a hardcoded buffer size.

E.g. in the first hunk here we're creating a strbuf just to copy argv[0]
to it, and then throwing it away, let's just pass down argv[0].

For hook_exists I think just having the code more grep-able and having
the config value inline is better, but I admit that's a matter of taste.

I didn't try to find all such strbuf() occurrences, anyway, in the
overall scheme of things it's a relatively small nit.

I'm hoping to do some deeper diving into this series, in particular the
parallelism, but just sending the shallow-ish comments I have for now.

Thanks for working on this!

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 23/37] read-cache: convert post-index-change hook to use config
  2021-03-11  2:10 ` [PATCH v8 23/37] read-cache: convert post-index-change hook to use config Emily Shaffer
@ 2021-03-12 10:22   ` Junio C Hamano
  2021-03-29 23:56     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-03-12 10:22 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git

Emily Shaffer <emilyshaffer@google.com> writes:

> @@ -3070,6 +3071,8 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
>  				 unsigned flags)
>  {
>  	int ret;
> +	struct run_hooks_opt hook_opt;
> +	run_hooks_opt_init_async(&hook_opt);
>  

Nit. blank line between the last of decls and the first stmt (many
identical nits exist everywhere in this series).

>  	/*
>  	 * TODO trace2: replace "the_repository" with the actual repo instance
> @@ -3088,9 +3091,13 @@ static int do_write_locked_index(s
>  	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("post-index-change", &hook_opt);
> +	run_hooks_opt_clear(&hook_opt);

There is one early return before the precontext of this hunk that
bypasses this opt_clear() call.  It is before any member of hook_opt
structure that was opt_init()'ed gets touched, so with the current
code, there is no leak, but it probably is laying a landmine for the
future, where opt_init() may allocate some resource to its member,
with the expectation that all users of the API would call
opt_clear() to release.  Or the caller of the API (like this one) may
start mucking with the opt structure before the existing early return,
at which point the current assumption that it is safe to return from
that point without opt_clear() would be broken.

I saw that there are other early returns in the series that are safe
right now but may become unsafe when the API implementation gets
extended that way.  If it does not involve too much code churning,
we may want to restructure the code to make these early returns into
"goto"s that jump to a single exit point, so that we can always
match opt_init() with opt_clear(), like the structure of the
existing code allowed cmd_rebase() to use the hooks API cleanly in
[v8 22/37].

Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 08/37] hook: add 'run' subcommand
  2021-03-11  2:10 ` [PATCH v8 08/37] hook: add 'run' subcommand Emily Shaffer
  2021-03-12  8:54   ` Ævar Arnfjörð Bjarmason
@ 2021-03-12 10:22   ` Junio C Hamano
  1 sibling, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-03-12 10:22 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git

Emily Shaffer <emilyshaffer@google.com> writes:

> +static int run(int argc, const char **argv, const char *prefix)
> +{
> +	struct strbuf hookname = STRBUF_INIT;
> +	struct run_hooks_opt opt;
> +	int rc = 0;
> +
> +	struct option run_options[] = {
> +		OPT_STRVEC('e', "env", &opt.env, N_("var"),
> +			   N_("environment variables for hook to use")),
> +		OPT_STRVEC('a', "arg", &opt.args, N_("args"),
> +			   N_("argument to pass to hook")),
> +		OPT_END(),
> +	};
> +
> +	run_hooks_opt_init(&opt);
> +
> +	argc = parse_options(argc, argv, prefix, run_options,
> +			     builtin_hook_usage, 0);
> +
> +	if (argc < 1)
> +		usage_msg_opt(_("You must specify a hook event to run."),
> +			      builtin_hook_usage, run_options);
> +
> +	strbuf_addstr(&hookname, argv[0]);
> +	opt.run_hookdir = should_run_hookdir;
> +
> +	rc = run_hooks(hookname.buf, &opt);
> +
> +	strbuf_release(&hookname);
> +	run_hooks_opt_clear(&opt);
> +
> +	return rc;
> +}

This looks like a small and clean example that is good for people to
emulate when using the new run-hooks API.  You opt_init(), futz with
the its fields, call run_hooks(), and finally opt_clear() to release
the resources.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 10/37] hook: support passing stdin to hooks
  2021-03-11  2:10 ` [PATCH v8 10/37] hook: support passing stdin to hooks Emily Shaffer
  2021-03-12  9:00   ` Ævar Arnfjörð Bjarmason
@ 2021-03-12 10:22   ` Junio C Hamano
  1 sibling, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-03-12 10:22 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git

Emily Shaffer <emilyshaffer@google.com> writes:

> diff --git a/hook.c b/hook.c
> index 118931f273..f906e8c61c 100644
> --- a/hook.c
> +++ b/hook.c
> @@ -240,6 +240,7 @@ void run_hooks_opt_init(struct run_hooks_opt *o)
>  {
>  	strvec_init(&o->env);
>  	strvec_init(&o->args);
> +	o->path_to_stdin = NULL;
>  	o->run_hookdir = configured_hookdir_opt();
>  }

The new member is initialized to NULL, and presumably the user of
the API would point an existing string with it.  Since there is no
free() in opt_clear() introduced by this patch, the member is
obviously a pointer to a borrowed piece of memory.

> diff --git a/hook.h b/hook.h
> index 0df785add5..2314ec5962 100644
> --- a/hook.h
> +++ b/hook.h
> @@ -52,6 +52,9 @@ struct run_hooks_opt
>  	 * to be overridden if the user can override it at the command line.
>  	 */
>  	enum hookdir_opt run_hookdir;
> +
> +	/* Path to file which should be piped to stdin for each hook */
> +	const char *path_to_stdin;
>  };

And we mark the fact that hook subsystem does not own it by making
it "const char *".  Looks quite consistent.  Good.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 18/37] commit: use config-based hooks
  2021-03-11  2:10 ` [PATCH v8 18/37] commit: use config-based hooks Emily Shaffer
@ 2021-03-12 10:22   ` Junio C Hamano
  0 siblings, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-03-12 10:22 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git

Emily Shaffer <emilyshaffer@google.com> writes:

> -int run_commit_hook(int editor_is_used, const char *index_file,
> +int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,
>  		    const char *name, ...)
>  {
> -	struct strvec hook_env = STRVEC_INIT;
> +	struct run_hooks_opt opt;
>  	va_list args;
> +	const char *arg;
>  	int ret;
>  
> -	strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
> +	run_hooks_opt_init_sync(&opt);
> +
> +	if (parallelize)
> +		opt.jobs = configured_hook_jobs();
> +
> +	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);
> +
> +	ret = run_hooks(name, &opt);
> +	run_hooks_opt_clear(&opt);
>  
>  	return ret;
>  }

This follows the textbook pattern established earlier and
demonstrated in [v8 08/37].  opt_init() to initialize, populate its
members, call run_hooks() and finally opt_clear().

Quite nicely demonstrated.


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 19/37] am: convert applypatch hooks to use config
  2021-03-11  2:10 ` [PATCH v8 19/37] am: convert applypatch hooks to use config Emily Shaffer
@ 2021-03-12 10:23   ` Junio C Hamano
  2021-03-29 23:39     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-03-12 10:23 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git

Emily Shaffer <emilyshaffer@google.com> writes:

> @@ -1558,8 +1563,10 @@ static void do_commit(const struct am_state *state)
>  	struct commit_list *parents = NULL;
>  	const char *reflog_msg, *author, *committer = NULL;
>  	struct strbuf sb = STRBUF_INIT;
> +	struct run_hooks_opt hook_opt;
> +	run_hooks_opt_init_async(&hook_opt);
>  
> -	if (run_hook_le(NULL, "pre-applypatch", NULL))
> +	if (run_hooks("pre-applypatch", &hook_opt))
>  		exit(1);
>  
>  	if (write_cache_as_tree(&tree, 0, NULL))
> @@ -1611,8 +1618,9 @@ static void do_commit(const struct am_state *state)
>  		fclose(fp);
>  	}
>  
> -	run_hook_le(NULL, "post-applypatch", NULL);
> +	run_hooks("post-applypatch", &hook_opt);
>  
> +	run_hooks_opt_clear(&hook_opt);
>  	strbuf_release(&sb);
>  }

This one does opt_init(), run_hooks(), and another run_hooks() and
then opt_clear().  If run_hooks() is a read-only operation on the
hook_opt, then that would be alright, but it just smells iffy that
it is not done as two separate opt_init(), run_hooks(), opt_clear()
sequences for two separate run_hooks() invocations.  The same worry
about future safety I meantioned elsewhere in the series also
applies.

Thanks.



^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 22/37] rebase: teach pre-rebase to use hook.h
  2021-03-11  2:10 ` [PATCH v8 22/37] rebase: teach pre-rebase to use hook.h Emily Shaffer
@ 2021-03-12 10:24   ` Junio C Hamano
  0 siblings, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-03-12 10:24 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git

Emily Shaffer <emilyshaffer@google.com> writes:

> @@ -1318,6 +1319,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;
>  	struct option builtin_rebase_options[] = {
>  		OPT_STRING(0, "onto", &options.onto_name,
>  			   N_("revision"),
> @@ -1431,6 +1433,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
>  	};
>  	int i;
>  
> +	run_hooks_opt_init_async(&hook_opt);
> +
>  	if (argc == 2 && !strcmp(argv[1], "-h"))
>  		usage_with_options(builtin_rebase_usage,
>  				   builtin_rebase_options);
> @@ -2032,9 +2036,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
>  	}
>  
>  	/* If a hook exists, give it a chance to interrupt*/
> +	strvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);
>  	if (!ok_to_skip_pre_rebase &&
> -	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
> -			argc ? argv[0] : NULL, NULL))
> +	    run_hooks("pre-rebase", &hook_opt))
>  		die(_("The pre-rebase hook refused to rebase."));

This may needlessly populate hook_opt.args even when run_hooks() is
not triggered, but that probably is OK.  Except for a place or two
where we call die(), the exit path from this function after this
point all eventually passes ...

>  	if (options.flags & REBASE_DIFFSTAT) {
> @@ -2114,6 +2118,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
>  	ret = !!run_specific_rebase(&options, action);
>  
>  cleanup:

... this label, so everybody calls opt_clear() at the end, which is
good.

> +	run_hooks_opt_clear(&hook_opt);
>  	strbuf_release(&buf);
>  	strbuf_release(&revisions);
>  	free(options.head_name);

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h
  2021-03-11  2:10 ` [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h Emily Shaffer
@ 2021-03-12 10:24   ` Junio C Hamano
  2021-03-29 23:59     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-03-12 10:24 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git

Emily Shaffer <emilyshaffer@google.com> writes:

> @@ -1435,12 +1436,19 @@ 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_sync(&opt);
> +
>  	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(push_to_checkout_hook, &opt)) {
> +		run_hooks_opt_clear(&opt);
>  		return "push-to-checkout hook declined";
> -	else
> +	} else {
> +		run_hooks_opt_clear(&opt);
>  		return NULL;
> +	}
>  }

OK, we opt_init(), futz with opt, call run_hooks() and opt_clear()
regardless of the outcome from run_hooks().  Narrow-sighted me
wonders if it makes the use of the API easier if run_hooks() did the
opt_clear() before it returns, but I haven't yet seen enough use at
this point to judge.

Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 00/37] config-based hooks
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (38 preceding siblings ...)
  2021-03-12  9:49 ` Ævar Arnfjörð Bjarmason
@ 2021-03-12 11:13 ` Ævar Arnfjörð Bjarmason
  2021-03-25 12:41   ` Ævar Arnfjörð Bjarmason
  2021-05-27  0:08 ` [PATCH v9 00/37] propose " Emily Shaffer
  40 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-12 11:13 UTC (permalink / raw)
  To: Emily Shaffer
  Cc: git, Jeff King, Junio C Hamano, James Ramsay, Jonathan Nieder,
	brian m. carlson, Phillip Wood, Josh Steadmon,
	Johannes Schindelin, Jonathan Tan


On Thu, Mar 11 2021, Emily Shaffer wrote:

> Since v7:
> - Addressed Jonathan Tan's review of part I
> - Addressed Junio's review of part I and II
> - Combined parts I and II
>

Comments on the overall design / goals (I don't just have strbuf nits):

I think I mentioned this in earlier rounds, but I'm still very skeptical
of the need for a "git hook" command for anything except the "run" case
(which is very useful).

So I tried patching it with this on top:
	
	diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
	index 4f66bb35cf8..eb48da1dcf0 100644
	--- a/Documentation/config/hook.txt
	+++ b/Documentation/config/hook.txt
	@@ -1,20 +1,17 @@
	-hook.<command>.command::
	-	A command to execute during the <command> hook event. This can be an
	-	executable on your device, a oneliner for your shell, or the name of a
	-	hookcmd. See linkgit:git-hook[1].
	-
	-hookcmd.<name>.command::
	-	A command to execute during a hook for which <name> has been specified
	-	as a command. This can be an executable on your device or a oneliner for
	-	your shell. See linkgit:git-hook[1].
	-
	-hookcmd.<name>.skip::
	-	Specify this boolean to remove a command from earlier in the execution
	-	order. Useful if you want to make a single repo an exception to hook
	-	configured at the system or global scope. If there is no hookcmd
	-	specified for the command you want to skip, you can use the value of
	-	`hook.<command>.command` as <name> as a shortcut. The "skip" setting
	-	must be specified after the "hook.<command>.command" to have an effect.
	+hook.<name>.event::
	+hook.<name>.command::
	+	A command to execute during a given hook event for which
	+	<name> has been specified This can be an executable on your
	+	device or a oneliner for your shell. See linkgit:git-hook[1].
	++
	+As a convention setting this to the string `true` will clobber and
	+omit a command from earlier in the execution order. Similarly to the
	+"cat" special-case for `pager.<cmd>` we won't execute the hook at all
	+in that case.
	++
	+To have a single hook handle multiple types of events (such as
	+`pre-receive` and `post-receive`) specify `hook.<name>.event` multiple
	+times.
	 
	 hook.runHookDir::
	 	Controls how hooks contained in your hookdir are executed. Can be any of

I didn't finish that WIP patch, but I have yet to see any reason for why
it wouldn't work.

In experimenting with it further I tried just adding a "git config
--show-hook" as a convenience alias for "git config --show-origin
--show-scope --get-regexp '^hook\.<name>\.'", something like:
	
	diff --git a/builtin/config.c b/builtin/config.c
	index 963d65fd3fc..f62356b923a 100644
	--- a/builtin/config.c
	+++ b/builtin/config.c
	@@ -33,6 +33,7 @@ static int end_nul;
	 static int respect_includes_opt = -1;
	 static struct config_options config_options;
	 static int show_origin;
	+static int show_hook;
	 static int show_scope;
	 
	 #define ACTION_GET (1<<0)
	@@ -159,6 +160,7 @@ static struct option builtin_config_options[] = {
	 	OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")),
	 	OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")),
	 	OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")),
	+	OPT_BOOL(0, "show-hook", &show_hook, N_("show configuration for a given hook (convenience alias for --show-origin --show-scope --get-regexp '^hook\\.<name>\\.')")),
	 	OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")),
	 	OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")),
	 	OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")),
	@@ -631,6 +633,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)
	 {
	 	int nongit = !startup_info->have_repository;
	 	char *value;
	+	struct strbuf show_hook_arg = STRBUF_INIT;
	 
	 	given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));
	 
	@@ -645,6 +648,14 @@ int cmd_config(int argc, const char **argv, const char *prefix)
	 		usage_builtin_config();
	 	}
	 
	+	if (show_hook) {
	+		strbuf_addf(&show_hook_arg, "^hook\\.%s\\.", argv[0]);
	+		actions = ACTION_GET_REGEXP;
	+		show_scope = 1;
	+		argv[0] = show_hook_arg.buf;
	+	}
	+		
	+
	 	if (nongit) {
	 		if (use_local_config)
	 			die(_("--local can only be used inside a git repository"));
	@@ -915,5 +926,8 @@ int cmd_config(int argc, const char **argv, const char *prefix)
	 		return get_colorbool(argv[0], argc == 2);
	 	}
	 
	+	/* TODO: Memory leak on non-zero return, do we care? */
	+	strbuf_release(&show_hook_arg);
	+
	 	return 0;
	 }

So the reason that naïve approach doesn't work is that the current
design has both a hook.<command>.command, *or* a
hookcmd.<command>.<cfg>. So it can't be just a single --get-regexp, you
need to statefully parse it, as indeed your implementation does.

But this seems like a bad idea to me for at least these reasons I've
thought of so far:

 1. If we just change the design a bit we can make this a much simpler
    git-config wrapper, or point to that directly.

 2. You're sticking full paths in the git config key, which is
    case-insensitive, and a feature of this format is being able to
    configure/override previously configured hooks.

    So the behavior of this feature depends on git's interaction with
    the case-sensitivity of filesystems, and not just one fs, any fs
    we're walking in our various config sources, and where the hook
    itself lives.

    As recent CVEs have shown that's a big can of worms, particularly
    for something whose goal is to address the security aspect of
    running hooks from other config.

    Arguably the case-sensitivity issue is just confusing since we
    canonicalize it anyway. But once you add in FS path canonicalization
    it becomes a real big can of worms. See the .gitmodules fsck code.

    Even if it wasn't for that it's relatively nastier to edit/maintain
    full paths and the appropriate escaping in the double-quoted key in
    the config file v.s. having it as an optionally quoted value.

 3. We're left with this "*.command = cmd", and "*.skip = true"
    special-case syntax. I can't see any reason for why it's needed over
    simply having "*.command = true" clobber earlier hooks as noted in
    the proposed docs above.

    And that doesn't require any magic to support, just like our
    existing "core.pager=cat" case.

    I mean, I suppose it's magical in that we might otherwise error on
    non-consumed stdin (do we?), anyway, documenting it as a synonym for
    "cat >/dev/null" would get around that :)

 4. It makes the common case of having the same hooks for N commands
    needlessly verbose, if you can just support "type" (or whatever we
    should call it) you can add that N times...

 5. At the end of this series we're left with the start of the docs
    saying:

      You can list and run configured hooks with this command. Later,
      you will be able to add and modify hooks with this command.

    But those patches have yet to land, and looking at the design
    document I'm skeptical of that being a good addition v.s. just
    adding the same thing to "git config".

    As just one exmaple; surely "git config edit <name>" would need to
    run around and find config files to edit, then open them in a loop
    for you, no?

    Which we'd eventually want for "git config" in general with an
    --edit-regexp option or whatever, which brings us (well, at least
    me) back to "then let's just add it to git-config?".

 6. The whole 'git hook' config special-casing doesn't help other
    commands or the security issue that seemed to have prompted (at
    least some of) its existence

    In the design doc we mention the "core.pager = rm -rf /" case for a
    .git/config.

    This series doesn't implement, but the design docs note a future
    want for solving that issue for the hooks.

    To me that's another case where we should just have general config
    syntax, not something hook-specific, e.g. if I could do this in my
    ~/.gitconfig:

       ;; We consider 'config.ignore' in reverse order, so e.g setting
       ;; it in. ~/.gitconfig will ignore any such keys for repo-level
       ;; config
       [config "ignore"]
       key = core.pager
       keyRegexp = "^hook\."

    We'd address both any hook security concerns, as well as core.pager
    etc. We could then just have e.g. some syntax sugar of:

       [include]
       path = built-in://gimme-safe-config

    Which would just be a thin layer of magit to include
    <path-to-git-prefix>/config-templates/gimme-safe-config or whatever.

    We'd thus address the issue for all config types without
    hook-specific magic.

Anyway. I'm very willing to be convinced otherwise. I just think that
for a first-draft implementation leaving aside 'hook.<command>.command'
and the whole 'list' thing makes sense.

We can consider the core code changes relatively separately from any
future aspirations, particularly with a 40-some patch series, and the
end-state of *this series* IMO not really justifying, that part of the
implementation, and thus requiring reviewers to look ahead beyond the
40-some patches.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 00/37] config-based hooks
  2021-03-11 22:26 ` [PATCH v8 00/37] config-based hooks Junio C Hamano
@ 2021-03-12 23:27   ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-12 23:27 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: git, Jeff King, James Ramsay, Jonathan Nieder, brian m. carlson,
	Ævar Arnfjörð Bjarmason, Phillip Wood,
	Josh Steadmon, Johannes Schindelin, Jonathan Tan

On Thu, Mar 11, 2021 at 02:26:10PM -0800, Junio C Hamano wrote:
> 
> Emily Shaffer <emilyshaffer@google.com> writes:
> 
> > Since v7:
> > - Addressed Jonathan Tan's review of part I
> > - Addressed Junio's review of part I and II
> > - Combined parts I and II
> >
> > I think the updates to patch 1 between the rest of the work I've been
> > doing probably have covered Ævar's comments.
> >
> > More details about per-patch changes found in the notes on each mail (I
> > hope).
> >
> > I know that Junio was talking about merging v7 after Josh Steadmon's
> > review and I asked him not to - this reroll has those changes from
> > Jonathan Tan's review that I was wanting to wait for.
> 
> I picked it up and replaced, not necessarily because it is an urgent
> thing to do during the pre-release period, but primarily because I
> wanted to be prepared for any nasty surprises by unmanageable
> conflicts I may have to face once the current cycle is over.
> 
> It turns out that it was a bit painful to merge to 'seen' as there
> are in-flight topics that touch the hooks documentation, and the
> changes they make must be carried forward to the new file.
> 
> But it was not too bad.  
> 
> The merge into 'seen' is 3cdeaeab (Merge branch 'es/config-hooks'
> into seen, 2021-03-11) as of this writing, and the output of
> 
>     $ git diff 3cdeaeab3a^:Documentation/githooks.txt \
>                3cdeaeab3a:Documentation/native-hooks.txt
> 
>     (i.e. the version of the file before the merge, where your topic
>     being merged took material to edit to produce the new "native-hooks"
>     document, is compared with the result)
> 
> looks reasonable to me, but please double check.

I had a look at that diff (but targeting 6da6893c, which is what I see
for "Merge branch 'es/config-hooks' into seen" when I fetch from
gitster/git today) and it looks fine to me, very reasonable. Thanks for
doing that.

> 
> Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'
  2021-03-11  2:10 ` [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate' Emily Shaffer
  2021-03-12  9:21   ` Ævar Arnfjörð Bjarmason
@ 2021-03-12 23:29   ` Emily Shaffer
  1 sibling, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-12 23:29 UTC (permalink / raw)
  To: git

On Wed, Mar 10, 2021 at 06:10:35PM -0800, Emily Shaffer wrote:
> 
> By using the new 'git hook run' subcommand to run 'sendemail-validate',
> we can reduce the boilerplate needed to run this hook in perl. Using
> config-based hooks also allows us to run 'sendemail-validate' hooks that
> were configured globally when running 'git send-email' from outside of a
> Git directory, alongside other benefits like multihooks and
> parallelization.
> 
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> ---

Without having had time to look at reviews to this (or the rest of the
series) yet - it occurred to me that this hook should be run in series
instead. That is, I should invoke 'git hook run' with '-j1'.

>  git-send-email.perl   | 21 ++++-----------------
>  t/t9001-send-email.sh | 11 +----------
>  2 files changed, 5 insertions(+), 27 deletions(-)
> 
> diff --git a/git-send-email.perl b/git-send-email.perl
> index 1f425c0809..73e1e0b51a 100755
> --- a/git-send-email.perl
> +++ b/git-send-email.perl
> @@ -1941,23 +1941,10 @@ sub unique_email_list {
>  sub validate_patch {
>  	my ($fn, $xfer_encoding) = @_;
>  
> -	if ($repo) {
> -		my $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),
> -					    'sendemail-validate');
> -		my $hook_error;
> -		if (-x $validate_hook) {
> -			my $target = abs_path($fn);
> -			# The hook needs a correct cwd and GIT_DIR.
> -			my $cwd_save = cwd();
> -			chdir($repo->wc_path() or $repo->repo_path())
> -				or die("chdir: $!");
> -			local $ENV{"GIT_DIR"} = $repo->repo_path();
> -			$hook_error = "rejected by sendemail-validate hook"
> -				if system($validate_hook, $target);
> -			chdir($cwd_save) or die("chdir: $!");
> -		}
> -		return $hook_error if $hook_error;
> -	}
> +	my $target = abs_path($fn);
> +	return "rejected by sendemail-validate hook"
> +		if system(("git", "hook", "run", "sendemail-validate", "-a",
> +				$target));
>  
>  	# Any long lines will be automatically fixed if we use a suitable transfer
>  	# encoding.
> diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
> index 4eee9c3dcb..456b471c5c 100755
> --- a/t/t9001-send-email.sh
> +++ b/t/t9001-send-email.sh
> @@ -2101,16 +2101,7 @@ test_expect_success $PREREQ 'invoke hook' '
>  	mkdir -p .git/hooks &&
>  
>  	write_script .git/hooks/sendemail-validate <<-\EOF &&
> -	# test that we have the correct environment variable, pwd, and
> -	# argument
> -	case "$GIT_DIR" in
> -	*.git)
> -		true
> -		;;
> -	*)
> -		false
> -		;;
> -	esac &&
> +	# test that we have the correct argument
>  	test -f 0001-add-main.patch &&
>  	grep "add main" "$1"
>  	EOF
> -- 
> 2.31.0.rc2.261.g7f71774620-goog
> 

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 00/37] config-based hooks
  2021-03-12  9:49 ` Ævar Arnfjörð Bjarmason
@ 2021-03-17 18:41   ` Emily Shaffer
  2021-03-17 19:16     ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-17 18:41 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Jeff King, Junio C Hamano, James Ramsay, Jonathan Nieder,
	brian m. carlson, Phillip Wood, Josh Steadmon,
	Johannes Schindelin, Jonathan Tan

On Fri, Mar 12, 2021 at 10:49:38AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> > Since v7:
> > - Addressed Jonathan Tan's review of part I
> > - Addressed Junio's review of part I and II
> > - Combined parts I and II
> >
> > I think the updates to patch 1 between the rest of the work I've been
> > doing probably have covered Ævar's comments.
> 
> A range-diff between iterations of such a large series would be most
> useful. Do you have a public repo with tags or whatever the different
> versions, for those who'd like an easier way to follow along the
> differing versions than scraping the ML archive?

I am really embarrassed to say that I don't have the
branches/tags/whatever up. I have not succeeded in building that habit
yet. I'll generate one from my local patches today and send it here.

> 
> While reading this I came up with the following fixup patches on top,
> for discussion, maybe not something you want as-is:

I was a little bit confused reading this fixup without seeing the rest of your
review, so I'll revisit this once I get through what else you wrote.

> 	
> 	 Documentation/git-hook.txt |  8 +++++
> 	 builtin/bugreport.c        |  8 +++--
> 	 builtin/commit.c           |  3 +-
> 	 builtin/hook.c             | 79 ++++++++++++++++++++--------------------------
> 	 builtin/merge.c            |  3 +-
> 	 builtin/receive-pack.c     | 11 +++----
> 	 hook.c                     | 21 +++++-------
> 	 hook.h                     |  5 +--
> 	 refs.c                     |  4 ++-
> 	 sequencer.c                |  4 ++-
> 	 10 files changed, 73 insertions(+), 73 deletions(-)
> 	
> 	diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
> 	index 4ad31ac360a..5c9af30b43e 100644
> 	--- a/Documentation/git-hook.txt
> 	+++ b/Documentation/git-hook.txt
> 	@@ -150,10 +150,18 @@ message body and cannot be parallelized.
> 	 
> 	 CONFIGURATION
> 	 -------------
> 	+
> 	+The below documentation is the same as what's found in
> 	+linkgit:git-config[1]:
> 	+
> 	 include::config/hook.txt[]
> 	 
> 	 HOOKS
> 	 -----
> 	+
> 	+The below documentation is the same as what's found in
> 	+linkgit:githooks[5]:
> 	+
> 	 include::native-hooks.txt[]
> 	 
> Noted in another reply, including it here for completeness.
> 
> 	 GIT
> 	diff --git a/builtin/bugreport.c b/builtin/bugreport.c
> 	index 04467cd1d3a..b64e53fd625 100644
> 	--- a/builtin/bugreport.c
> 	+++ b/builtin/bugreport.c
> 	@@ -81,9 +81,13 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)
> 	 		return;
> 	 	}
> 	 
> 	-	for (i = 0; i < ARRAY_SIZE(hook); i++)
> 	-		if (hook_exists(hook[i], HOOKDIR_USE_CONFIG))
> 	+	for (i = 0; i < ARRAY_SIZE(hook); i++) {
> 	+		struct strbuf config;
> 	+		strbuf_addf(&config, "hook.%s.config", hook[i]);
> 	+		if (hook_exists(hook[i], config.buf, HOOKDIR_USE_CONFIG))
> 	 			strbuf_addf(hook_info, "%s\n", hook[i]);
> 	+		strbuf_release(&config);
> 	+	}
> 	 }
> 
> Less strbuf, see below.
> 	 
> 	 static const char * const bugreport_usage[] = {
> 	diff --git a/builtin/commit.c b/builtin/commit.c
> 	index 31df571f123..fc9f1f5ee58 100644
> 	--- a/builtin/commit.c
> 	+++ b/builtin/commit.c
> 	@@ -984,7 +984,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
> 	 		return 0;
> 	 	}
> 	 
> 	-	if (!no_verify && hook_exists("pre-commit", HOOKDIR_USE_CONFIG)) {
> 	+	if (!no_verify && hook_exists("pre-commit", "hook.pre-commit.command",
> 	+				      HOOKDIR_USE_CONFIG)) {
> 	 		/*
> 	 		 * Re-read the index as pre-commit hook could have updated it,
> 	 		 * and write it out as a tree.  We must do this before we invoke
> 
> 
> ..ditto.
> 
> 	diff --git a/builtin/hook.c b/builtin/hook.c
> 	index b4f4adb1dea..d0b56ee47f8 100644
> 	--- a/builtin/hook.c
> 	+++ b/builtin/hook.c
> 	@@ -18,8 +18,6 @@ static enum hookdir_opt should_run_hookdir;
> 	 static int list(int argc, const char **argv, const char *prefix)
> 	 {
> 	 	struct list_head *head, *pos;
> 	-	struct strbuf hookname = STRBUF_INIT;
> 	-	struct strbuf hookdir_annotation = STRBUF_INIT;
> 	 
> 	 	struct option list_options[] = {
> 	 		OPT_END(),
> 	@@ -33,67 +31,60 @@ static int list(int argc, const char **argv, const char *prefix)
> 	 			      builtin_hook_usage, list_options);
> 	 	}
> 	 
> 	-	strbuf_addstr(&hookname, argv[0]);
> 	-
> 	-	head = hook_list(&hookname);
> 	+	head = hook_list(argv[0]);
> 	 
> 	 	if (list_empty(head)) {
> 	 		printf(_("no commands configured for hook '%s'\n"),
> 	-		       hookname.buf);
> 	-		strbuf_release(&hookname);
> 	+		       argv[0]);
> 	 		return 0;
> 	 	}
> 	 
> 	-	switch (should_run_hookdir) {
> 	-		case HOOKDIR_NO:
> 	-			strbuf_addstr(&hookdir_annotation, _(" (will not run)"));
> 	-			break;
> 	-		case HOOKDIR_ERROR:
> 	-			strbuf_addstr(&hookdir_annotation, _(" (will error and not run)"));
> 	-			break;
> 	-		case HOOKDIR_INTERACTIVE:
> 	-			strbuf_addstr(&hookdir_annotation, _(" (will prompt)"));
> 	-			break;
> 	-		case HOOKDIR_WARN:
> 	-			strbuf_addstr(&hookdir_annotation, _(" (will warn but run)"));
> 	-			break;
> 	-		case HOOKDIR_YES:
> 	-		/*
> 	-		 * The default behavior should agree with
> 	-		 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
> 	-		 * do the default behavior.
> 	-		 */
> 	-		case HOOKDIR_UNKNOWN:
> 	-		default:
> 	-			break;
> 	-	}
> 	-
> 	 	list_for_each(pos, head) {
> 	 		struct hook *item = list_entry(pos, struct hook, list);
> 	 		item = list_entry(pos, struct hook, list);
> 	 		if (item) {
> 	-			/* Don't translate 'hookdir' - it matches the config */
> 	-			printf("%s: %s%s\n",
> 	-			       (item->from_hookdir
> 	+			const char *scope = item->from_hookdir
> 	 				? "hookdir"
> 	-				: config_scope_name(item->origin)),
> 	-			       item->command.buf,
> 	-			       (item->from_hookdir
> 	-				? hookdir_annotation.buf
> 	-				: ""));
> 	+				: config_scope_name(item->origin);
> 	+			switch (should_run_hookdir) {
> 	+			case HOOKDIR_NO:
> 	+				printf(_("%s: %s (will not run)\n"),
> 	+				       scope, item->command.buf);
> 	+				break;
> 	+			case HOOKDIR_ERROR:
> 	+				printf(_("%s: %s (will error and not run)\n"),
> 	+				       scope, item->command.buf);
> 	+				break;
> 	+			case HOOKDIR_INTERACTIVE:
> 	+				printf(_("%s: %s (will prompt)\n"),
> 	+				       scope, item->command.buf);
> 	+				break;
> 	+			case HOOKDIR_WARN:
> 	+				printf(_("%s: %s (will warn but run)\n"),
> 	+				       scope, item->command.buf);
> 	+				break;
> 	+			case HOOKDIR_YES:
> 	+				/*
> 	+				 * The default behavior should agree with
> 	+				 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
> 	+				 * do the default behavior.
> 	+				 */
> 	+			case HOOKDIR_UNKNOWN:
> 	+			default:
> 	+				printf(_("%s: %s\n"),
> 	+				       scope, item->command.buf);
> 	+				break;
> 	+			}
> 	 		}
> 	 	}
> 	 
> 	 	clear_hook_list(head);
> 	-	strbuf_release(&hookdir_annotation);
> 	-	strbuf_release(&hookname);
> 	 
> 	 	return 0;
> 	 }
> 
> I think this is better to avoid i18n lego, as noted in another reply
> (but I didn't include the patch).
> 
> More on strbuf below:
> 	 
> 	 static int run(int argc, const char **argv, const char *prefix)
> 	 {
> 	-	struct strbuf hookname = STRBUF_INIT;
> 	 	struct run_hooks_opt opt;
> 	 	int rc = 0;
> 	 
> 	@@ -118,12 +109,10 @@ static int run(int argc, const char **argv, const char *prefix)
> 	 		usage_msg_opt(_("You must specify a hook event to run."),
> 	 			      builtin_hook_usage, run_options);
> 	 
> 	-	strbuf_addstr(&hookname, argv[0]);
> 	 	opt.run_hookdir = should_run_hookdir;
> 	 
> 	-	rc = run_hooks(hookname.buf, &opt);
> 	+	rc = run_hooks(argv[0], &opt);
> 	 
> 	-	strbuf_release(&hookname);
> 	 	run_hooks_opt_clear(&opt);
> 	 
> 	 	return rc;
> 	diff --git a/builtin/merge.c b/builtin/merge.c
> 	index 3a2af257a6b..df4ff72fbc7 100644
> 	--- a/builtin/merge.c
> 	+++ b/builtin/merge.c
> 	@@ -848,7 +848,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)
> 	 	 * and write it out as a tree.  We must do this before we invoke
> 	 	 * the editor and after we invoke run_status above.
> 	 	 */
> 	-	if (hook_exists("pre-merge-commit", HOOKDIR_USE_CONFIG))
> 	+	if (hook_exists("pre-merge-commit", "hook.pre-merge-commit.command",
> 	+			HOOKDIR_USE_CONFIG))
> 	 		discard_cache();
> 	 	read_cache_from(index_file);
> 	 	strbuf_addbuf(&msg, &merge_msg);
> 	diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
> 	index eaedeeb1e8b..a76069ea592 100644
> 	--- a/builtin/receive-pack.c
> 	+++ b/builtin/receive-pack.c
> 	@@ -1123,12 +1123,10 @@ static int run_proc_receive_hook(struct command *commands,
> 	 	int version = 0;
> 	 	int code;
> 	 
> 	-	struct strbuf hookname = STRBUF_INIT;
> 	 	struct hook *proc_receive = NULL;
> 	 	struct list_head *pos, *hooks;
> 	 
> 	-	strbuf_addstr(&hookname, "proc-receive");
> 	-	hooks = hook_list(&hookname);
> 	+	hooks = hook_list("proc-receive");
> 	 
> 	 	list_for_each(pos, hooks) {
> 	 		if (proc_receive) {
> 	@@ -1460,8 +1458,6 @@ static const char *push_to_deploy(unsigned char *sha1,
> 	 	return NULL;
> 	 }
> 	 
> 	-static const char *push_to_checkout_hook = "push-to-checkout";
> 	-
> 	 static const char *push_to_checkout(unsigned char *hash,
> 	 				    struct strvec *env,
> 	 				    const char *work_tree)
> 	@@ -1472,7 +1468,7 @@ static const char *push_to_checkout(unsigned char *hash,
> 	 	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
> 	 	strvec_pushv(&opt.env, env->v);
> 	 	strvec_push(&opt.args, hash_to_hex(hash));
> 	-	if (run_hooks(push_to_checkout_hook, &opt)) {
> 	+	if (run_hooks("push-to-checkout", &opt)) {
> 	 		run_hooks_opt_clear(&opt);
> 	 		return "push-to-checkout hook declined";
> 	 	} else {
> 	@@ -1502,7 +1498,8 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w
> 	 
> 	 	strvec_pushf(&env, "GIT_DIR=%s", absolute_path(git_dir));
> 	 
> 	-	if (!hook_exists(push_to_checkout_hook, HOOKDIR_USE_CONFIG))
> 	+	if (!hook_exists("push-to-checkout", "hook.push-to-checkout.command",
> 	+			 HOOKDIR_USE_CONFIG))
> 	 		retval = push_to_deploy(sha1, &env, work_tree);
> 	 	else
> 	 		retval = push_to_checkout(sha1, &env, work_tree);
> 	diff --git a/hook.c b/hook.c
> 	index 7f6f3b9a616..49c3861ce00 100644
> 	--- a/hook.c
> 	+++ b/hook.c
> 	@@ -247,7 +247,7 @@ static const char *find_legacy_hook(const char *name)
> 	 }
> 	 
> 	 
> 	-struct list_head* hook_list(const struct strbuf* hookname)
> 	+struct list_head* hook_list(const char *hookname)
> 	 {
> 	 	struct strbuf hook_key = STRBUF_INIT;
> 	 	struct list_head *hook_head = xmalloc(sizeof(struct list_head));
> 	@@ -256,14 +256,14 @@ struct list_head* hook_list(const struct strbuf* hookname)
> 	 	INIT_LIST_HEAD(hook_head);
> 	 
> 	 	if (!hookname)
> 	-		return NULL;
> 	+		BUG("???");;
> 	 
> 	-	strbuf_addf(&hook_key, "hook.%s.command", hookname->buf);
> 	+	strbuf_addf(&hook_key, "hook.%s.command", hookname);
> 	 
> 	 	git_config(hook_config_lookup, &cb_data);
> 	 
> 	 	if (have_git_dir()) {
> 	-		const char *legacy_hook_path = find_legacy_hook(hookname->buf);
> 	+		const char *legacy_hook_path = find_legacy_hook(hookname);
> 	 
> 	 		/* Unconditionally add legacy hook, but annotate it. */
> 	 		if (legacy_hook_path) {
> 	@@ -300,10 +300,10 @@ void run_hooks_opt_init_async(struct run_hooks_opt *o)
> 	 	o->jobs = configured_hook_jobs();
> 	 }
> 	 
> 	-int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)
> 	+int hook_exists(const char *hookname, const char *hook_config,
> 	+		enum hookdir_opt should_run_hookdir)
> 	 {
> 	 	const char *value = NULL; /* throwaway */
> 	-	struct strbuf hook_key = STRBUF_INIT;
> 	 	int could_run_hookdir;
> 	 
> 	 	if (should_run_hookdir == HOOKDIR_USE_CONFIG)
> 	@@ -314,9 +314,7 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)
> 	 				should_run_hookdir == HOOKDIR_YES)
> 	 				&& !!find_legacy_hook(hookname);
> 	 
> 	-	strbuf_addf(&hook_key, "hook.%s.command", hookname);
> 	-
> 	-	return (!git_config_get_value(hook_key.buf, &value)) || could_run_hookdir;
> 	+	return (!git_config_get_value(hook_config, &value)) || could_run_hookdir;
> 	 }
> 	 
> 	 void run_hooks_opt_clear(struct run_hooks_opt *o)
> 	@@ -438,7 +436,6 @@ static int notify_hook_finished(int result,
> 	 
> 	 int run_hooks(const char *hookname, struct run_hooks_opt *options)
> 	 {
> 	-	struct strbuf hookname_str = STRBUF_INIT;
> 	 	struct list_head *to_run, *pos = NULL, *tmp = NULL;
> 	 	struct hook_cb_data cb_data = { 0, NULL, NULL, options };
> 	 
> 	@@ -448,9 +445,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)
> 	 	if (options->path_to_stdin && options->feed_pipe)
> 	 		BUG("choose only one method to populate stdin");
> 	 
> 	-	strbuf_addstr(&hookname_str, hookname);
> 	-
> 	-	to_run = hook_list(&hookname_str);
> 	+	to_run = hook_list(hookname);
> 	 
> 	 	list_for_each_safe(pos, tmp, to_run) {
> 	 		struct hook *hook = list_entry(pos, struct hook, list);
> 	diff --git a/hook.h b/hook.h
> 	index 4ff9999b049..bfbbf36882d 100644
> 	--- a/hook.h
> 	+++ b/hook.h
> 	@@ -26,7 +26,7 @@ struct hook {
> 	  * Provides a linked list of 'struct hook' detailing commands which should run
> 	  * in response to the 'hookname' event, in execution order.
> 	  */
> 	-struct list_head* hook_list(const struct strbuf *hookname);
> 	+struct list_head* hook_list(const char *hookname);
> 	 
> 	 enum hookdir_opt
> 	 {
> 	@@ -123,7 +123,8 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);
> 	  * Like with run_hooks, if you take a --run-hookdir flag, reflect that
> 	  * user-specified behavior here instead.
> 	  */
> 	-int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);
> 	+int hook_exists(const char *hookname, const char *hook_config,
> 	+		enum hookdir_opt should_run_hookdir);
> 	 
> 	 /*
> 	  * Runs all hooks associated to the 'hookname' event in order. Each hook will be
> 	diff --git a/refs.c b/refs.c
> 	index 334fdd9103c..f01995fe64f 100644
> 	--- a/refs.c
> 	+++ b/refs.c
> 	@@ -1966,7 +1966,9 @@ static int run_transaction_hook(struct ref_transaction *transaction,
> 	 
> 	 	run_hooks_opt_init_async(&opt);
> 	 
> 	-	if (!hook_exists("reference-transaction", HOOKDIR_USE_CONFIG))
> 	+	if (!hook_exists("reference-transaction",
> 	+			 "hook.reference-transaction.command",
> 	+			 HOOKDIR_USE_CONFIG))
> 	 		return ret;
> 	 
> 	 	strvec_push(&opt.args, state);
> 	diff --git a/sequencer.c b/sequencer.c
> 	index 34ff275f0d1..52c067c1688 100644
> 	--- a/sequencer.c
> 	+++ b/sequencer.c
> 	@@ -1436,7 +1436,9 @@ static int try_to_commit(struct repository *r,
> 	 		}
> 	 	}
> 	 
> 	-	if (hook_exists("prepare-commit-msg", HOOKDIR_USE_CONFIG)) {
> 	+	if (hook_exists("prepare-commit-msg",
> 	+			"hook.prepare-commit-msg.command",
> 	+			HOOKDIR_USE_CONFIG)) {
> 	 		res = run_prepare_commit_msg_hook(r, msg, hook_commit);
> 	 		if (res)
> 	 			goto out;
> 
> There was another reply (from JT I believe, but didn't go back and look
> it up) about the over use of strbuf.
> 
> I tend to agree, as much as I love the API it's really not better to
> write C with it if all you need is a const char* that's never modified,
> particularly if you get it from elsewhere.
> 
> So it's really not meant for or good for "everything we need a const
> char*", but to avoid verbose realloc() dances all over the place, and
> for things like getline() loops without a hardcoded buffer size.
> 
> E.g. in the first hunk here we're creating a strbuf just to copy argv[0]
> to it, and then throwing it away, let's just pass down argv[0].
> 
> For hook_exists I think just having the code more grep-able and having
> the config value inline is better, but I admit that's a matter of taste.
> 
> I didn't try to find all such strbuf() occurrences, anyway, in the
> overall scheme of things it's a relatively small nit.
> 
> I'm hoping to do some deeper diving into this series, in particular the
> parallelism, but just sending the shallow-ish comments I have for now.
> 
> Thanks for working on this!

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 00/37] config-based hooks
  2021-03-17 18:41   ` Emily Shaffer
@ 2021-03-17 19:16     ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-17 19:16 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: git, Jeff King, Junio C Hamano, James Ramsay, Jonathan Nieder,
	brian m. carlson, Phillip Wood, Josh Steadmon,
	Johannes Schindelin, Jonathan Tan

On Wed, Mar 17, 2021 at 11:41:59AM -0700, Emily Shaffer wrote:
> 
> On Fri, Mar 12, 2021 at 10:49:38AM +0100, �var Arnfj�r� Bjarmason wrote:
> > 
> > 
> > On Thu, Mar 11 2021, Emily Shaffer wrote:
> > 
> > > Since v7:
> > > - Addressed Jonathan Tan's review of part I
> > > - Addressed Junio's review of part I and II
> > > - Combined parts I and II
> > >
> > > I think the updates to patch 1 between the rest of the work I've been
> > > doing probably have covered �var's comments.
> > 
> > A range-diff between iterations of such a large series would be most
> > useful. Do you have a public repo with tags or whatever the different
> > versions, for those who'd like an easier way to follow along the
> > differing versions than scraping the ML archive?
> 
> I am really embarrassed to say that I don't have the
> branches/tags/whatever up. I have not succeeded in building that habit
> yet. I'll generate one from my local patches today and send it here.

 1:  be907f68b9 !  1:  a5e8c233c3 doc: propose hooks managed by the config
    @@ Documentation/technical/config-based-hooks.txt (new)
     +[[motivation]]
     +== Motivation
     +
    -+Replace the .git/hook/hookname path as the only source of hooks to execute;
    ++Replace the `.git/hook/hookname` path as the only source of hooks to execute;
     +allow users to define hooks using config files, in a way which is friendly to
    -+users with multiple repos which have similar needs.
    ++users with multiple repos which have similar needs - hooks can be easily shared
    ++between multiple Git repos.
     +
     +Redefine "hook" as an event rather than a single script, allowing users to
     +perform multiple unrelated actions on a single event.
     +
    -+Take a step closer to safety when copying zipped Git repositories from untrusted
    -+users by making it more apparent to users which scripts will be run during
    -+normal Git operations.
    -+
     +Make it easier for users to discover Git's hook feature and automate their
     +workflows.
     +
    @@ Documentation/technical/config-based-hooks.txt (new)
     +number of cases:
     +
     +- "no": the legacy hook will not be run
    ++- "error": Git will print a warning to stderr before ignoring the legacy hook
     +- "interactive": Git will prompt the user before running the legacy hook
     +- "warn": Git will print a warning to stderr before running the legacy hook
     +- "yes" (default): Git will silently run the legacy hook
    @@ Documentation/technical/config-based-hooks.txt (new)
     +given which Git does not recognize, Git should discard that config entry. For
     +example, if "warn" was specified at system level and "junk" was specified at
     +global level, Git would resolve the value to "warn"; if the only time the config
    -+was set was to "junk", Git would use the default value of "yes".
    ++was set was to "junk", Git would use the default value of "yes" (but print a
    ++warning to the user first to let them know their value is wrong).
     +
     +`struct hookcmd` is expected to grow in size over time as more functionality is
     +added to hooks; so that other parts of the code don't need to understand the
    @@ Documentation/technical/config-based-hooks.txt (new)
     +=== Security and repo config
     +
     +Part of the motivation behind this refactor is to mitigate hooks as an attack
    -+vector;footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/]
    -+however, as the design stands, users can still provide hooks in the repo-level
    -+config, which is included when a repo is zipped and sent elsewhere.  The
    ++vector.footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/]
    ++However, as the design stands, users can still provide hooks in the repo-level
    ++config, which is included when a repo is zipped and sent elsewhere. The
     +security of the repo-level config is still under discussion; this design
    -+generally assumes the repo-level config is secure, which is not true yet. The
    -+goal is to avoid an overcomplicated design to work around a problem which has
    -+ceased to exist.
    ++generally assumes the repo-level config is secure, which is not true yet. This
    ++assumption was made to avoid overcomplicating the design. So, this series
    ++doesn't particularly improve security or resistance to zip attacks.
     +
     +[[ease-of-use]]
     +=== Ease of use
    @@ Documentation/technical/config-based-hooks.txt (new)
     +A previous summary of alternatives exists in the
     +archives.footnote:[https://lore.kernel.org/git/20191116011125.GG22855@google.com]
     +
    -+[[status-quo]]
    -+=== Status quo
    -+
    -+Today users can implement multihooks themselves by using a "trampoline script"
    -+as their hook, and pointing that script to a directory or list of other scripts
    -+they wish to run.
    -+
    -+[[hook-directories]]
    -+=== Hook directories
    -+
    -+Other contributors have suggested Git learn about the existence of a directory
    -+such as `.git/hooks/<hookname>.d` and execute those hooks in alphabetical order.
    -+
    -+[[comparison]]
    -+=== Comparison table
    ++The table below shows a number of goals and how they might be achieved with
    ++config-based hooks, by implementing directory support (i.e.
    ++'.git/hooks/pre-commit.d'), or as hooks are run today.
     +
     +.Comparison of alternatives
     +|===
    @@ Documentation/technical/config-based-hooks.txt (new)
     +|Natively
     +|With user effort
     +
    ++|Supports parallelization
    ++|Natively
    ++|Natively
    ++|No (user's multihook trampoline script would need to handle parallelism)
    ++
     +|Safer for zipped repos
     +|A little
     +|No
    @@ Documentation/technical/config-based-hooks.txt (new)
     +
     +|Can install one hook to many repos
     +|Yes
    -+|No
    -+|No
    ++|With symlinks or core.hooksPath
    ++|With symlinks or core.hooksPath
     +
     +|Discoverability
    -+|Better (in `git help git`)
    -+|Same as before
    ++|Findable with 'git help git' or tab-completion via 'git hook' subcommand
    ++|Findable via improved documentation
     +|Same as before
     +
     +|Hard to run unexpected hook
     +|If configured
    -+|No
    ++|Could be made to warn or look for a config
     +|No
     +|===
     +
    ++[[status-quo]]
    ++=== Status quo
    ++
    ++Today users can implement multihooks themselves by using a "trampoline script"
    ++as their hook, and pointing that script to a directory or list of other scripts
    ++they wish to run.
    ++
    ++[[hook-directories]]
    ++=== Hook directories
    ++
    ++Other contributors have suggested Git learn about the existence of a directory
    ++such as `.git/hooks/<hookname>.d` and execute those hooks in alphabetical order.
    ++
     +[[future-work]]
     +== Future work
     +
    @@ Documentation/technical/config-based-hooks.txt (new)
     +dependencies. If we decide to solve this problem, we may want to look to modern
     +build systems for inspiration on how to manage dependencies and parallel tasks.
     +
    ++[[nontrivial-hooks]]
    ++=== Multihooks and nontrivial output
    ++
    ++Some hooks - like 'proc-receive' - don't lend themselves well to multihooks at
    ++all. In the case of 'proc-receive', for now, multiple hook definitions are
    ++disallowed. In the future we might be able to conceive a better approach, for
    ++example, running the hooks in series and using the output from one hook as the
    ++input to the next.
    ++
     +[[securing-hookdir-hooks]]
     +=== Securing hookdir hooks
     +
 2:  b1d37c3911 =  2:  a3e858d056 hook: scaffolding for git-hook subcommand
 3:  fea411c598 !  3:  60b28a652b hook: add list command
    @@ Commit message
         run in config order. If more properties need to be set on a given hook
         in the future, commands can also be specified by providing
         "hook.<hookname>.command = <hookcmd-name>", as well as a "[hookcmd
    -    <hookcmd-name>]" subsection; at minimum, this subsection must contain a
    +    <hookcmd-name>]" subsection; this subsection should contain a
         "hookcmd.<hookcmd-name>.command = <path-to-hook>" line.
     
         For example:
    @@ Makefile: LIB_OBJS += hash-lookup.o
      ## builtin/hook.c ##
     @@
      #include "cache.h"
    - 
    +-
      #include "builtin.h"
     +#include "config.h"
     +#include "hook.h"
    @@ builtin/hook.c
      {
     -	struct option builtin_hook_options[] = {
     +	struct list_head *head, *pos;
    -+	struct hook *item;
     +	struct strbuf hookname = STRBUF_INIT;
     +
     +	struct option list_options[] = {
    @@ builtin/hook.c
     +	}
     +
     +	list_for_each(pos, head) {
    -+		item = list_entry(pos, struct hook, list);
    ++		struct hook *item = list_entry(pos, struct hook, list);
     +		if (item)
     +			printf("%s: %s\n",
     +			       config_scope_name(item->origin),
    @@ hook.c (new)
     +		    list_del(pos);
     +		    /* we'll simply move the hook to the end */
     +		    to_add = it;
    ++		    break;
     +		}
     +	}
     +
     +	if (!to_add) {
     +		/* adding a new hook, not moving an old one */
    -+		to_add = xmalloc(sizeof(struct hook));
    ++		to_add = xmalloc(sizeof(*to_add));
     +		strbuf_init(&to_add->command, 0);
     +		strbuf_addstr(&to_add->command, command);
     +	}
    @@ hook.c (new)
     +	/* re-set the scope so we show where an override was specified */
     +	to_add->origin = current_config_scope();
     +
    -+	list_add_tail(&to_add->list, pos);
    ++	list_add_tail(&to_add->list, head);
     +}
     +
     +static void remove_hook(struct list_head *to_remove)
    @@ hook.c (new)
     +		const char *command = value;
     +		struct strbuf hookcmd_name = STRBUF_INIT;
     +
    -+		/* Check if a hookcmd with that name exists. */
    ++		/*
    ++		 * Check if a hookcmd with that name exists. If it doesn't,
    ++		 * 'git_config_get_value()' is documented not to touch &command,
    ++		 * so we don't need to do anything.
    ++		 */
     +		strbuf_addf(&hookcmd_name, "hookcmd.%s.command", command);
     +		git_config_get_value(hookcmd_name.buf, &command);
     +
    @@ hook.c (new)
     +
     +	strbuf_addf(&hook_key, "hook.%s.command", hookname->buf);
     +
    -+	git_config(hook_config_lookup, (void*)&cb_data);
    ++	git_config(hook_config_lookup, &cb_data);
     +
     +	strbuf_release(&hook_key);
     +	return hook_head;
    @@ hook.h (new)
     +#include "list.h"
     +#include "strbuf.h"
     +
    -+struct hook
    -+{
    ++struct hook {
     +	struct list_head list;
     +	/*
     +	 * Config file which holds the hook.*.command definition.
 4:  89f1adf34d !  4:  d8232a8254 hook: include hookdir hook in list
    @@ Commit message
         $GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken
         from the config are more featureful than hooks placed in the $HOOKDIR,
         those hooks should not stop working for users who already have them.
    -
    -    Legacy hooks should be run directly, not in shell. We know that they are
    -    a path to an executable, not a oneliner script - and running them
    -    directly takes care of path quoting concerns for us for free.
    +    Let's list them to the user, but instead of displaying a config scope
    +    (e.g. "global: blah") we can prefix them with "hookdir:".
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
      ## builtin/hook.c ##
     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
    - 	struct list_head *head, *pos;
    - 	struct hook *item;
    - 	struct strbuf hookname = STRBUF_INIT;
    -+	struct strbuf hookdir_annotation = STRBUF_INIT;
    - 
    - 	struct option list_options[] = {
    - 		OPT_END(),
    -@@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
      
      	list_for_each(pos, head) {
    - 		item = list_entry(pos, struct hook, list);
    + 		struct hook *item = list_entry(pos, struct hook, list);
     -		if (item)
    --			printf("%s: %s\n",
    --			       config_scope_name(item->origin),
    --			       item->command.buf);
    ++		item = list_entry(pos, struct hook, list);
     +		if (item) {
     +			/* Don't translate 'hookdir' - it matches the config */
    -+			printf("%s: %s%s\n",
    + 			printf("%s: %s\n",
    +-			       config_scope_name(item->origin),
     +			       (item->from_hookdir
     +				? "hookdir"
     +				: config_scope_name(item->origin)),
    -+			       item->command.buf,
    -+			       (item->from_hookdir
    -+				? hookdir_annotation.buf
    -+				: ""));
    + 			       item->command.buf);
     +		}
      	}
      
    @@ hook.c
      void free_hook(struct hook *ptr)
      {
     @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *command)
    - 		to_add = xmalloc(sizeof(struct hook));
    + 		to_add = xmalloc(sizeof(*to_add));
      		strbuf_init(&to_add->command, 0);
      		strbuf_addstr(&to_add->command, command);
     +		to_add->from_hookdir = 0;
    @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *comm
      
      	/* re-set the scope so we show where an override was specified */
     @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
    - 	struct strbuf hook_key = STRBUF_INIT;
    - 	struct list_head *hook_head = xmalloc(sizeof(struct list_head));
    - 	struct hook_config_cb cb_data = { &hook_key, hook_head };
    -+	const char *legacy_hook_path = NULL;
    - 
    - 	INIT_LIST_HEAD(hook_head);
      
    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
    + 	git_config(hook_config_lookup, &cb_data);
      
    - 	git_config(hook_config_lookup, (void*)&cb_data);
    - 
    -+	if (have_git_dir())
    -+		legacy_hook_path = find_hook(hookname->buf);
    ++	if (have_git_dir()) {
    ++		const char *legacy_hook_path = find_hook(hookname->buf);
     +
    -+	/* Unconditionally add legacy hook, but annotate it. */
    -+	if (legacy_hook_path) {
    -+		struct hook *legacy_hook;
    ++		/* Unconditionally add legacy hook, but annotate it. */
    ++		if (legacy_hook_path) {
    ++			struct hook *legacy_hook;
     +
    -+		append_or_move_hook(hook_head, absolute_path(legacy_hook_path));
    -+		legacy_hook = list_entry(hook_head->prev, struct hook, list);
    -+		legacy_hook->from_hookdir = 1;
    ++			append_or_move_hook(hook_head,
    ++					    absolute_path(legacy_hook_path));
    ++			legacy_hook = list_entry(hook_head->prev, struct hook,
    ++						 list);
    ++			legacy_hook->from_hookdir = 1;
    ++		}
     +	}
     +
      	strbuf_release(&hook_key);
    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
      }
     
      ## hook.h ##
    -@@ hook.h: struct hook
    +@@ hook.h: struct hook {
      	enum config_scope origin;
      	/* The literal command to run. */
      	struct strbuf command;
    -+	int from_hookdir;
    ++	unsigned from_hookdir : 1;
      };
      
      /*
 5:  723edcd785 !  5:  96c0a4838f hook: respect hook.runHookDir
    @@ Metadata
     Author: Emily Shaffer <emilyshaffer@google.com>
     
      ## Commit message ##
    -    hook: respect hook.runHookDir
    +    hook: teach hook.runHookDir
     
    -    Include hooks specified in the hook directory in the list of hooks to
    -    run. These hooks do need to be treated differently from config-specified
    -    ones - they do not need to run in a shell, and later on may be disabled
    -    or warned about based on a config setting.
    -
    -    Because they are at least as local as the local config, we'll run them
    -    last - to keep the hook execution order from global to local.
    -
    -    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
    +    For now, just give a hint about how these hooks will be run in 'git hook
    +    list'. Later on, though, we will pay attention to this enum when running
    +    the hooks.
     
      ## Documentation/config/hook.txt ##
     @@ Documentation/config/hook.txt: hookcmd.<name>.command::
    @@ builtin/hook.c: static const char * const builtin_hook_usage[] = {
      static int list(int argc, const char **argv, const char *prefix)
      {
      	struct list_head *head, *pos;
    + 	struct strbuf hookname = STRBUF_INIT;
    ++	struct strbuf hookdir_annotation = STRBUF_INIT;
    + 
    + 	struct option list_options[] = {
    + 		OPT_END(),
     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
      		return 0;
      	}
    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
     +		case HOOKDIR_NO:
     +			strbuf_addstr(&hookdir_annotation, _(" (will not run)"));
     +			break;
    ++		case HOOKDIR_ERROR:
    ++			strbuf_addstr(&hookdir_annotation, _(" (will error and not run)"));
    ++			break;
     +		case HOOKDIR_INTERACTIVE:
     +			strbuf_addstr(&hookdir_annotation, _(" (will prompt)"));
     +			break;
     +		case HOOKDIR_WARN:
    -+		case HOOKDIR_UNKNOWN:
    -+			strbuf_addstr(&hookdir_annotation, _(" (will warn)"));
    ++			strbuf_addstr(&hookdir_annotation, _(" (will warn but run)"));
     +			break;
     +		case HOOKDIR_YES:
     +		/*
     +		 * The default behavior should agree with
    -+		 * hook.c:configured_hookdir_opt().
    ++		 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
    ++		 * do the default behavior.
     +		 */
    ++		case HOOKDIR_UNKNOWN:
     +		default:
     +			break;
     +	}
     +
      	list_for_each(pos, head) {
    + 		struct hook *item = list_entry(pos, struct hook, list);
      		item = list_entry(pos, struct hook, list);
      		if (item) {
    + 			/* Don't translate 'hookdir' - it matches the config */
    +-			printf("%s: %s\n",
    ++			printf("%s: %s%s\n",
    + 			       (item->from_hookdir
    + 				? "hookdir"
    + 				: config_scope_name(item->origin)),
    +-			       item->command.buf);
    ++			       item->command.buf,
    ++			       (item->from_hookdir
    ++				? hookdir_annotation.buf
    ++				: ""));
    + 		}
    + 	}
    + 
    + 	clear_hook_list(head);
    ++	strbuf_release(&hookdir_annotation);
    + 	strbuf_release(&hookname);
    + 
    + 	return 0;
     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
      
      int cmd_hook(int argc, const char **argv, const char *prefix)
    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
     +	if (run_hookdir)
     +		if (!strcmp(run_hookdir, "no"))
     +			should_run_hookdir = HOOKDIR_NO;
    ++		else if (!strcmp(run_hookdir, "error"))
    ++			should_run_hookdir = HOOKDIR_ERROR;
     +		else if (!strcmp(run_hookdir, "yes"))
     +			should_run_hookdir = HOOKDIR_YES;
     +		else if (!strcmp(run_hookdir, "warn"))
    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *
     +	if (!strcmp(key, "no"))
     +		return HOOKDIR_NO;
     +
    ++	if (!strcmp(key, "error"))
    ++		return HOOKDIR_ERROR;
    ++
     +	if (!strcmp(key, "yes"))
     +		return HOOKDIR_YES;
     +
    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *
      	struct strbuf hook_key = STRBUF_INIT;
     
      ## hook.h ##
    -@@ hook.h: struct hook
    +@@ hook.h: struct hook {
       */
      struct list_head* hook_list(const struct strbuf *hookname);
      
     +enum hookdir_opt
     +{
     +	HOOKDIR_NO,
    ++	HOOKDIR_ERROR,
     +	HOOKDIR_WARN,
     +	HOOKDIR_INTERACTIVE,
     +	HOOKDIR_YES,
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr
     +	test_i18ncmp expected actual
     +'
     +
    ++test_expect_success 'hook.runHookDir = error is respected by list' '
    ++	setup_hookdir &&
    ++
    ++	test_config hook.runHookDir "error" &&
    ++
    ++	cat >expected <<-EOF &&
    ++	hookdir: $(pwd)/.git/hooks/pre-commit (will error and not run)
    ++	EOF
    ++
    ++	git hook list pre-commit >actual &&
    ++	# the hookdir annotation is translated
    ++	test_i18ncmp expected actual
    ++'
    ++
     +test_expect_success 'hook.runHookDir = warn is respected by list' '
     +	setup_hookdir &&
     +
     +	test_config hook.runHookDir "warn" &&
     +
     +	cat >expected <<-EOF &&
    -+	hookdir: $(pwd)/.git/hooks/pre-commit (will warn)
    ++	hookdir: $(pwd)/.git/hooks/pre-commit (will warn but run)
     +	EOF
     +
     +	git hook list pre-commit >actual &&
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr
     +	# the hookdir annotation is translated
     +	test_i18ncmp expected actual
     +'
    ++
    ++test_expect_success 'hook.runHookDir is tolerant to unknown values' '
    ++	setup_hookdir &&
    ++
    ++	test_config hook.runHookDir "junk" &&
    ++
    ++	cat >expected <<-EOF &&
    ++	hookdir: $(pwd)/.git/hooks/pre-commit
    ++	EOF
    ++
    ++	git hook list pre-commit >actual &&
    ++	# the hookdir annotation is translated
    ++	test_i18ncmp expected actual
    ++'
     +
      test_done
 6:  567f6d9d00 !  6:  9068e11679 hook: implement hookcmd.<name>.skip
    @@ Commit message
         hook: implement hookcmd.<name>.skip
     
         If a user wants a specific repo to skip execution of a hook which is set
    -    at a global or system level, they can now do so by specifying 'skip' in
    -    their repo config:
    +    at a global or system level, they will be able to do so by specifying
    +    'skip' in their repo config:
     
         ~/.gitconfig
           [hook.pre-commit]
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    + ## Documentation/config/hook.txt ##
    +@@ Documentation/config/hook.txt: hookcmd.<name>.command::
    + 	as a command. This can be an executable on your device or a oneliner for
    + 	your shell. See linkgit:git-hook[1].
    + 
    ++hookcmd.<name>.skip::
    ++	Specify this boolean to remove a command from earlier in the execution
    ++	order. Useful if you want to make a single repo an exception to hook
    ++	configured at the system or global scope. If there is no hookcmd
    ++	specified for the command you want to skip, you can use the value of
    ++	`hook.<command>.command` as <name> as a shortcut. The "skip" setting
    ++	must be specified after the "hook.<command>.command" to have an effect.
    ++
    + hook.runHookDir::
    + 	Controls how hooks contained in your hookdir are executed. Can be any of
    + 	"yes", "warn", "interactive", or "no". Defaults to "yes". See
    +
    + ## Documentation/git-hook.txt ##
    +@@ Documentation/git-hook.txt: $ git hook list "prepare-commit-msg"
    + local: /bin/linter --c
    + ----
    + 
    ++If there is a command you wish to run in most cases but have one or two
    ++exceptional repos where it should be skipped, you can use specify
    ++`hookcmd.<name>.skip`, for example:
    ++
    ++System config
    ++----
    ++  [hook "pre-commit"]
    ++    command = check-for-secrets
    ++
    ++  [hookcmd "check-for-secrets"]
    ++    command = /bin/secret-checker --aggressive
    ++----
    ++
    ++Local config
    ++----
    ++  [hookcmd "check-for-secrets"]
    ++    skip = true
    ++  # This works for inlined hook commands, too:
    ++  [hookcmd "~/typocheck.sh"]
    ++    skip = true
    ++----
    ++
    ++After these configs are added, the hook list becomes:
    ++
    ++----
    ++$ git hook list "post-commit"
    ++global: /bin/linter --c
    ++local: python ~/run-test-suite.py
    ++
    ++$ git hook list "pre-commit"
    ++no commands configured for hook 'pre-commit'
    ++----
    ++
    + COMMANDS
    + --------
    + 
    +
      ## hook.c ##
     @@ hook.c: void free_hook(struct hook *ptr)
      	}
      }
      
     -static void append_or_move_hook(struct list_head *head, const char *command)
    -+static struct hook* find_hook_by_command(struct list_head *head, const char *command)
    ++static struct hook * find_hook_by_command(struct list_head *head, const char *command)
      {
      	struct list_head *pos = NULL, *tmp = NULL;
     -	struct hook *to_add = NULL;
    @@ hook.c: void free_hook(struct hook *ptr)
     -		    /* we'll simply move the hook to the end */
     -		    to_add = it;
     +		    found = it;
    + 		    break;
      		}
      	}
     +	return found;
    @@ hook.c: void free_hook(struct hook *ptr)
      
      	if (!to_add) {
      		/* adding a new hook, not moving an old one */
    -@@ hook.c: static void append_or_move_hook(struct list_head *head, const char *command)
    - 	/* re-set the scope so we show where an override was specified */
    - 	to_add->origin = current_config_scope();
    - 
    --	list_add_tail(&to_add->list, pos);
    -+	list_add_tail(&to_add->list, head);
    - }
    - 
    - static void remove_hook(struct list_head *to_remove)
     @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *cb_data)
      	if (!strcmp(key, hook_key)) {
      		const char *command = value;
    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *
     +		strbuf_addf(&hookcmd_name, "hookcmd.%s.skip", command);
     +		git_config_get_bool(hookcmd_name.buf, &skip);
      
    - 		/* Check if a hookcmd with that name exists. */
    + 		/*
    + 		 * Check if a hookcmd with that name exists. If it doesn't,
    + 		 * 'git_config_get_value()' is documented not to touch &command,
    + 		 * so we don't need to do anything.
    + 		 */
     +		strbuf_reset(&hookcmd_name);
      		strbuf_addf(&hookcmd_name, "hookcmd.%s.command", command);
      		git_config_get_value(hookcmd_name.buf, &command);
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is re
     +	test_i18ncmp expected actual
     +'
     +
    ++test_expect_success 'git hook list ignores skip referring to unused hookcmd' '
    ++	test_config hookcmd.abc.command "/path/abc" --add &&
    ++	test_config hookcmd.abc.skip "true" --add &&
    ++
    ++	cat >expected <<-EOF &&
    ++	no commands configured for hook '\''pre-commit'\''
    ++	EOF
    ++
    ++	git hook list pre-commit >actual &&
    ++	test_i18ncmp expected actual
    ++'
    ++
     +test_expect_success 'git hook list removes skipped inlined hook' '
     +	setup_hooks &&
     +	test_config hookcmd."$ROOT/path/ghi".skip "true" --add &&
 7:  a1c02b6758 !  7:  a2867ab8c0 parse-options: parse into strvec
    @@ Documentation/technical/api-parse-options.txt: There are some macros to easily d
      	Use of `--no-option` will clear the list of preceding values.
      
     +`OPT_STRVEC(short, long, &struct strvec, arg_str, description)`::
    -+	Introduce an option with a string argument.
    -+	The string argument is stored as an element in `strvec`.
    ++	Introduce an option with a string argument, meant to be specified
    ++	multiple times.
    ++	The string argument is stored as an element in `strvec`, and later
    ++	arguments are added to the same `strvec`.
     +	Use of `--no-option` will clear the list of preceding values.
     +
      `OPT_INTEGER(short, long, &int_var, description)`::
 8:  d865772ebc !  8:  8848eeddf2 hook: add 'run' subcommand
    @@ Commit message
         supported.
     
         Legacy hooks (those present in $GITDIR/hooks) are run at the end of the
    -    execution list. For now, there is no way to disable them.
    +    execution list. They can be disabled, or made to print warnings, or to
    +    prompt before running, with the 'hook.runHookDir' config.
     
         Users may wish to provide hook commands like 'git config
         hook.pre-commit.command "~/linter.sh --pre-commit"'. To enable this,
    @@ Documentation/git-hook.txt: in the order they should be run, and print the confi
     +run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::
     +
     +Runs hooks configured for `<hook-name>`, in the same order displayed by `git
    -+hook list`. Hooks configured this way are run prepended with `sh -c`, so paths
    -+containing special characters or spaces should be wrapped in single quotes:
    -+`command = '/my/path with spaces/script.sh' some args`.
    ++hook list`. Hooks configured this way may be run prepended with `sh -c`, so
    ++paths containing special characters or spaces should be wrapped in single
    ++quotes: `command = '/my/path with spaces/script.sh' some args`.
     +
     +OPTIONS
     +-------
    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
     +static int run(int argc, const char **argv, const char *prefix)
     +{
     +	struct strbuf hookname = STRBUF_INIT;
    -+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
    ++	struct run_hooks_opt opt;
     +	int rc = 0;
     +
     +	struct option run_options[] = {
    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
     +		OPT_END(),
     +	};
     +
    -+	/*
    -+	 * While it makes sense to list hooks out-of-repo, it doesn't make sense
    -+	 * to execute them. Hooks usually want to look at repository artifacts.
    -+	 */
    -+	if (!have_git_dir())
    -+		usage_msg_opt(_("You must be in a Git repo to execute hooks."),
    -+			      builtin_hook_usage, run_options);
    ++	run_hooks_opt_init(&opt);
     +
     +	argc = parse_options(argc, argv, prefix, run_options,
     +			     builtin_hook_usage, 0);
    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)
     +
     +	switch (cfg)
     +	{
    ++		case HOOKDIR_ERROR:
    ++			fprintf(stderr, _("Skipping legacy hook at '%s'\n"),
    ++				path);
    ++			/* FALLTHROUGH */
     +		case HOOKDIR_NO:
     +			return 0;
    -+		case HOOKDIR_UNKNOWN:
    -+			fprintf(stderr,
    -+				_("Unrecognized value for 'hook.runHookDir'. "
    -+				  "Is there a typo? "));
    -+			/* FALLTHROUGH */
     +		case HOOKDIR_WARN:
     +			fprintf(stderr, _("Running legacy hook at '%s'\n"),
     +				path);
    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)
     +			} while (prompt.len); /* an empty reply means "Yes" */
     +			strbuf_release(&prompt);
     +			return 1;
    ++		/*
    ++		 * HOOKDIR_UNKNOWN should match the default behavior, but let's
    ++		 * give a heads up to the user.
    ++		 */
    ++		case HOOKDIR_UNKNOWN:
    ++			fprintf(stderr,
    ++				_("Unrecognized value for 'hook.runHookDir'. "
    ++				  "Is there a typo? "));
    ++			/* FALLTHROUGH */
     +		case HOOKDIR_YES:
     +		default:
     +			return 1;
    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
     +	strvec_clear(&o->args);
     +}
     +
    ++static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
    ++			    struct child_process *cp)
    ++{
    ++	if (!hook)
    ++		return;
    ++
    ++	cp->no_stdin = 1;
    ++	cp->env = options->env.v;
    ++	cp->stdout_to_stderr = 1;
    ++	cp->trace2_hook_name = hook->command.buf;
    ++
    ++	/*
    ++	 * Commands from the config could be oneliners, but we know
    ++	 * for certain that hookdir commands are not.
    ++	 */
    ++	cp->use_shell = !hook->from_hookdir;
    ++
    ++	/* add command */
    ++	strvec_push(&cp->args, hook->command.buf);
    ++
    ++	/*
    ++	 * add passed-in argv, without expanding - let the user get back
    ++	 * exactly what they put in
    ++	 */
    ++	strvec_pushv(&cp->args, options->args.v);
    ++}
    ++
     +int run_hooks(const char *hookname, struct run_hooks_opt *options)
     +{
     +	struct strbuf hookname_str = STRBUF_INIT;
    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
     +		struct child_process hook_proc = CHILD_PROCESS_INIT;
     +		struct hook *hook = list_entry(pos, struct hook, list);
     +
    -+		hook_proc.env = options->env.v;
    -+		hook_proc.no_stdin = 1;
    -+		hook_proc.stdout_to_stderr = 1;
    -+		hook_proc.trace2_hook_name = hook->command.buf;
    -+		hook_proc.use_shell = 1;
    -+
    -+		if (hook->from_hookdir) {
    -+		    if (!should_include_hookdir(hook->command.buf, options->run_hookdir))
    ++		if (hook->from_hookdir &&
    ++		    !should_include_hookdir(hook->command.buf, options->run_hookdir))
     +			continue;
    -+		    /*
    -+		     * Commands from the config could be oneliners, but we know
    -+		     * for certain that hookdir commands are not.
    -+		     */
    -+		    hook_proc.use_shell = 0;
    -+		}
    -+
    -+		/* add command */
    -+		strvec_push(&hook_proc.args, hook->command.buf);
     +
    -+		/*
    -+		 * add passed-in argv, without expanding - let the user get back
    -+		 * exactly what they put in
    -+		 */
    -+		strvec_pushv(&hook_proc.args, options->args.v);
    ++		prepare_hook_cp(hook, options, &hook_proc);
     +
     +		rc |= run_command(&hook_proc);
     +	}
    @@ hook.h
      #include "strbuf.h"
     +#include "strvec.h"
      
    - struct hook
    - {
    + struct hook {
    + 	struct list_head list;
     @@ hook.h: enum hookdir_opt
       */
      enum hookdir_opt configured_hookdir_opt(void);
    @@ hook.h: enum hookdir_opt
     +	enum hookdir_opt run_hookdir;
     +};
     +
    -+#define RUN_HOOKS_OPT_INIT  {   		\
    -+	.env = STRVEC_INIT, 				\
    -+	.args = STRVEC_INIT, 			\
    -+	.run_hookdir = configured_hookdir_opt()	\
    -+}
    -+
     +void run_hooks_opt_init(struct run_hooks_opt *o);
     +void run_hooks_opt_clear(struct run_hooks_opt *o);
     +
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = no is resp
     +	test_must_be_empty actual
      '
      
    - test_expect_success 'hook.runHookDir = warn is respected by list' '
    + test_expect_success 'hook.runHookDir = error is respected by list' '
    +@@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = error is respected by list' '
    + 
    + 	git hook list pre-commit >actual &&
    + 	# the hookdir annotation is translated
    ++	test_i18ncmp expected actual &&
    ++
    ++	cat >expected <<-EOF &&
    ++	Skipping legacy hook at '\''$(pwd)/.git/hooks/pre-commit'\''
    ++	EOF
    ++
    ++	git hook run pre-commit 2>actual &&
    + 	test_i18ncmp expected actual
    + '
    + 
     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is respected by list' '
      
      	git hook list pre-commit >actual &&
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = interactiv
     +	nongit test_must_fail git hook run pre-commit
      '
      
    - test_done
    + test_expect_success 'hook.runHookDir is tolerant to unknown values' '
 9:  53a655ed2c !  9:  452f7eea89 hook: replace find_hook() with hook_exists()
    @@ Metadata
     Author: Emily Shaffer <emilyshaffer@google.com>
     
      ## Commit message ##
    -    hook: replace find_hook() with hook_exists()
    +    hook: introduce hook_exists()
     
         Add a helper to easily determine whether any hooks exist for a given
         hook event.
    @@ Commit message
         hook; that check should include the config-based hooks as well. Optimize
         by checking the config directly. Since commands which execute hooks
         might want to take args to replace 'hook.runHookDir', let
    -    'hook_exists()' mirror the behavior of 'hook.runHookDir'.
    +    'hook_exists()' take a hookdir_opt to override that config.
     
    -    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
    +    In some cases, external callers today use find_hook() to discover the
    +    location of a hook and then run it manually with run-command.h (that is,
    +    not with run_hook_le()). Later, those cases will call hook.h:run_hook()
    +    directly instead.
     
    - ## builtin/bugreport.c ##
    -@@
    - #include "strbuf.h"
    - #include "help.h"
    - #include "compat/compiler.h"
    --#include "run-command.h"
    -+#include "hook.h"
    - 
    - 
    - static void get_system_info(struct strbuf *sys_info)
    -@@ builtin/bugreport.c: static void get_populated_hooks(struct strbuf *hook_info, int nongit)
    - 	}
    - 
    - 	for (i = 0; i < ARRAY_SIZE(hook); i++)
    --		if (find_hook(hook[i]))
    -+		if (hook_exists(hook[i], configured_hookdir_opt()))
    - 			strbuf_addf(hook_info, "%s\n", hook[i]);
    - }
    - 
    +    Once the entire codebase is using hook_exists() instead of find_hook(),
    +    find_hook() can be safely rolled into hook_exists() and removed from
    +    run-command.h.
    +
    +    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
      ## hook.c ##
     @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)
    @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)
     +{
     +	const char *value = NULL; /* throwaway */
     +	struct strbuf hook_key = STRBUF_INIT;
    ++	int could_run_hookdir;
    ++
    ++	if (should_run_hookdir == HOOKDIR_USE_CONFIG)
    ++		should_run_hookdir = configured_hookdir_opt();
     +
    -+	int could_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||
    ++	could_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||
     +				should_run_hookdir == HOOKDIR_WARN ||
     +				should_run_hookdir == HOOKDIR_YES)
     +				&& !!find_hook(hookname);
    @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)
      	strvec_clear(&o->env);
     
      ## hook.h ##
    +@@ hook.h: struct list_head* hook_list(const struct strbuf *hookname);
    + 
    + enum hookdir_opt
    + {
    ++	HOOKDIR_USE_CONFIG,
    + 	HOOKDIR_NO,
    + 	HOOKDIR_ERROR,
    + 	HOOKDIR_WARN,
     @@ hook.h: struct run_hooks_opt
      void run_hooks_opt_init(struct run_hooks_opt *o);
      void run_hooks_opt_clear(struct run_hooks_opt *o);
10:  13abc6ce24 ! 10:  e76507b290 hook: support passing stdin to hooks
    @@ Documentation/git-hook.txt: in the order they should be run, and print the confi
     +run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] `<hook-name>`::
      
      Runs hooks configured for `<hook-name>`, in the same order displayed by `git
    - hook list`. Hooks configured this way are run prepended with `sh -c`, so paths
    + hook list`. Hooks configured this way may be run prepended with `sh -c`, so
     @@ Documentation/git-hook.txt: Specify arguments to pass to every hook that is run.
      +
      Specify environment variables to set for every hook that is run.
    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)
      
     
      ## hook.c ##
    -@@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)
    - 		struct child_process hook_proc = CHILD_PROCESS_INIT;
    - 		struct hook *hook = list_entry(pos, struct hook, list);
    +@@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)
    + {
    + 	strvec_init(&o->env);
    + 	strvec_init(&o->args);
    ++	o->path_to_stdin = NULL;
    + 	o->run_hookdir = configured_hookdir_opt();
    + }
    + 
    +@@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
    + 	if (!hook)
    + 		return;
      
    -+		/* reopen the file for stdin; run_command closes it. */
    -+		if (options->path_to_stdin)
    -+			hook_proc.in = xopen(options->path_to_stdin, O_RDONLY);
    -+		else
    -+			hook_proc.no_stdin = 1;
    +-	cp->no_stdin = 1;
    ++	/* reopen the file for stdin; run_command closes it. */
    ++	if (options->path_to_stdin)
    ++		cp->in = xopen(options->path_to_stdin, O_RDONLY);
    ++	else
    ++		cp->no_stdin = 1;
     +
    - 		hook_proc.env = options->env.v;
    --		hook_proc.no_stdin = 1;
    - 		hook_proc.stdout_to_stderr = 1;
    - 		hook_proc.trace2_hook_name = hook->command.buf;
    - 		hook_proc.use_shell = 1;
    + 	cp->env = options->env.v;
    + 	cp->stdout_to_stderr = 1;
    + 	cp->trace2_hook_name = hook->command.buf;
     
      ## hook.h ##
     @@ hook.h: struct run_hooks_opt
    @@ hook.h: struct run_hooks_opt
     +	const char *path_to_stdin;
      };
      
    - #define RUN_HOOKS_OPT_INIT  {   		\
    --	.env = STRVEC_INIT, 				\
    -+	.env = STRVEC_INIT, 			\
    - 	.args = STRVEC_INIT, 			\
    -+	.path_to_stdin = NULL,			\
    - 	.run_hookdir = configured_hookdir_opt()	\
    - }
    - 
    + void run_hooks_opt_init(struct run_hooks_opt *o);
     @@ hook.h: int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);
      
      /*
    @@ hook.h: int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdi
      
     
      ## t/t1360-config-based-hooks.sh ##
    -@@ t/t1360-config-based-hooks.sh: test_expect_success 'out-of-repo runs excluded' '
    - 	nongit test_must_fail git hook run pre-commit
    +@@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir is tolerant to unknown values' '
    + 	test_i18ncmp expected actual
      '
      
     +test_expect_success 'stdin to multiple hooks' '
11:  0465a9ec94 ! 11:  5f41555e49 run-command: allow stdin for run_processes_parallel
    @@ run-command.c: static int pp_start_one(struct parallel_processes *pp)
      	if (i == pp->max_processes)
      		BUG("bookkeeping is hard");
      
    -+	/* disallow by default, but allow users to set up stdin if they wish */
    ++	/*
    ++	 * By default, do not inherit stdin from the parent process - otherwise,
    ++	 * all children would share stdin! Users may overwrite this to provide
    ++	 * something to the child's stdin by having their 'get_next_task'
    ++	 * callback assign 0 to .no_stdin and an appropriate integer to .in.
    ++	 */
     +	pp->children[i].process.no_stdin = 1;
     +
      	code = pp->get_next_task(&pp->children[i].process,
12:  83eb7805a4 ! 12:  a3bf826304 hook: allow parallel hook execution
    @@ Documentation/git-hook.txt: in the order they should be run, and print the confi
     +run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] [(-j|--jobs)<n>] `<hook-name>`::
      
      Runs hooks configured for `<hook-name>`, in the same order displayed by `git
    - hook list`. Hooks configured this way are run prepended with `sh -c`, so paths
    + hook list`. Hooks configured this way may be run prepended with `sh -c`, so
     @@ Documentation/git-hook.txt: Specify environment variables to set for every hook that is run.
      Specify a file which will be streamed into stdin for every hook that is run.
      Each hook will receive the entire file from beginning to EOF.
    @@ builtin/hook.c
      	NULL
      };
      
    -@@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
    - static int run(int argc, const char **argv, const char *prefix)
    - {
    - 	struct strbuf hookname = STRBUF_INIT;
    --	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
    -+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;
    - 	int rc = 0;
    - 
    - 	struct option run_options[] = {
     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)
      			   N_("argument to pass to hook")),
      		OPT_STRING(0, "to-stdin", &opt.path_to_stdin, N_("path"),
    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)
      		OPT_END(),
      	};
      
    +-	run_hooks_opt_init(&opt);
    ++	run_hooks_opt_init_async(&opt);
    + 
    + 	argc = parse_options(argc, argv, prefix, run_options,
    + 			     builtin_hook_usage, 0);
     
      ## hook.c ##
     @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)
    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)
      static int should_include_hookdir(const char *path, enum hookdir_opt cfg)
      {
      	struct strbuf prompt = STRBUF_INIT;
    -@@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)
    +@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
    + 	return hook_head;
    + }
    + 
    +-void run_hooks_opt_init(struct run_hooks_opt *o)
    ++void run_hooks_opt_init_sync(struct run_hooks_opt *o)
    + {
      	strvec_init(&o->env);
      	strvec_init(&o->args);
    + 	o->path_to_stdin = NULL;
      	o->run_hookdir = configured_hookdir_opt();
    ++	o->jobs = 1;
    ++}
    ++
    ++void run_hooks_opt_init_async(struct run_hooks_opt *o)
    ++{
    ++	run_hooks_opt_init_sync(o);
     +	o->jobs = configured_hook_jobs();
      }
      
    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)
      	strvec_clear(&o->args);
      }
      
    -+
    +-static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
    +-			    struct child_process *cp)
     +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 *hook = hook_cb->run_me;
     +
    -+	struct hook *hook = list_entry(hook_cb->run_me, struct hook, list);
    -+
    -+	if (hook_cb->head == hook_cb->run_me)
    + 	if (!hook)
    +-		return;
     +		return 0;
    -+
    -+	cp->env = hook_cb->options->env.v;
    -+	cp->stdout_to_stderr = 1;
    -+	cp->trace2_hook_name = hook->command.buf;
    -+
    -+	/* reopen the file for stdin; run_command closes it. */
    + 
    + 	/* reopen the file for stdin; run_command closes it. */
    +-	if (options->path_to_stdin)
    +-		cp->in = xopen(options->path_to_stdin, O_RDONLY);
    +-	else
     +	if (hook_cb->options->path_to_stdin) {
     +		cp->no_stdin = 0;
     +		cp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);
     +	} else {
    -+		cp->no_stdin = 1;
    + 		cp->no_stdin = 1;
     +	}
    -+
    -+	/*
    -+	 * Commands from the config could be oneliners, but we know
    -+	 * for certain that hookdir commands are not.
    -+	 */
    -+	if (hook->from_hookdir)
    -+		cp->use_shell = 0;
    -+	else
    -+		cp->use_shell = 1;
    -+
    -+	/* add command */
    -+	strvec_push(&cp->args, hook->command.buf);
    -+
    -+	/*
    -+	 * add passed-in argv, without expanding - let the user get back
    -+	 * exactly what they put in
    -+	 */
    + 
    +-	cp->env = options->env.v;
    ++	cp->env = hook_cb->options->env.v;
    + 	cp->stdout_to_stderr = 1;
    + 	cp->trace2_hook_name = hook->command.buf;
    + 
    +@@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
    + 	 * add passed-in argv, without expanding - let the user get back
    + 	 * exactly what they put in
    + 	 */
    +-	strvec_pushv(&cp->args, options->args.v);
     +	strvec_pushv(&cp->args, hook_cb->options->args.v);
     +
     +	/* Provide context for errors if necessary */
     +	*pp_task_cb = hook;
     +
     +	/* Get the next entry ready */
    -+	hook_cb->run_me = hook_cb->run_me->next;
    ++	if (hook_cb->run_me->list.next == hook_cb->head)
    ++		hook_cb->run_me = NULL;
    ++	else
    ++		hook_cb->run_me = list_entry(hook_cb->run_me->list.next,
    ++					     struct hook, list);
     +
     +	return 1;
     +}
    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)
     +	strbuf_addf(out, _("Couldn't start '%s', configured in '%s'\n"),
     +		    attempted->command.buf,
     +		    attempted->from_hookdir ? "hookdir"
    -+		    	: config_scope_name(attempted->origin));
    ++			: config_scope_name(attempted->origin));
     +
     +	/* NEEDSWORK: if halt_on_error is desired, do it here. */
     +	return 0;
    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)
     +
     +	/* NEEDSWORK: if halt_on_error is desired, do it here. */
     +	return 0;
    -+}
    -+
    + }
    + 
      int run_hooks(const char *hookname, struct run_hooks_opt *options)
      {
      	struct strbuf hookname_str = STRBUF_INIT;
    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)
     -		struct child_process hook_proc = CHILD_PROCESS_INIT;
      		struct hook *hook = list_entry(pos, struct hook, list);
      
    --		/* reopen the file for stdin; run_command closes it. */
    --		if (options->path_to_stdin)
    --			hook_proc.in = xopen(options->path_to_stdin, O_RDONLY);
    --		else
    --			hook_proc.no_stdin = 1;
    --
    --		hook_proc.env = options->env.v;
    --		hook_proc.stdout_to_stderr = 1;
    --		hook_proc.trace2_hook_name = hook->command.buf;
    --		hook_proc.use_shell = 1;
    --
    --		if (hook->from_hookdir) {
    --		    if (!should_include_hookdir(hook->command.buf, options->run_hookdir))
    + 		if (hook->from_hookdir &&
    + 		    !should_include_hookdir(hook->command.buf, options->run_hookdir))
     -			continue;
    --		    /*
    --		     * Commands from the config could be oneliners, but we know
    --		     * for certain that hookdir commands are not.
    --		     */
    --		    hook_proc.use_shell = 0;
    --		}
    --
    --		/* add command */
    --		strvec_push(&hook_proc.args, hook->command.buf);
    -+		if (hook->from_hookdir &&
    -+		    !should_include_hookdir(hook->command.buf, options->run_hookdir))
     +			    list_del(pos);
     +	}
    ++
    ++	if (list_empty(to_run))
    ++		return 0;
      
    --		/*
    --		 * add passed-in argv, without expanding - let the user get back
    --		 * exactly what they put in
    --		 */
    --		strvec_pushv(&hook_proc.args, options->args.v);
    +-		prepare_hook_cp(hook, options, &hook_proc);
     +	cb_data.head = to_run;
    -+	cb_data.run_me = to_run->next;
    ++	cb_data.run_me = list_entry(to_run->next, struct hook, list);
      
     -		rc |= run_command(&hook_proc);
     -	}
    @@ hook.h: enum hookdir_opt
      	/* Environment vars to be set for each hook */
     @@ hook.h: struct run_hooks_opt
      
    + 	/*
    + 	 * How should the hookdir be handled?
    +-	 * Leave the RUN_HOOKS_OPT_INIT default in most cases; this only needs
    ++	 * Leave the run_hooks_opt_init_*() default in most cases; this only needs
    + 	 * to be overridden if the user can override it at the command line.
    + 	 */
    + 	enum hookdir_opt run_hookdir;
    + 
      	/* Path to file which should be piped to stdin for each hook */
      	const char *path_to_stdin;
     +
     +	/* Number of threads to parallelize across */
     +	int jobs;
    - };
    - 
    --#define RUN_HOOKS_OPT_INIT  {   		\
    ++};
    ++
     +/*
     + * Callback provided to feed_pipe_fn and consume_sideband_fn.
     + */
     +struct hook_cb_data {
     +	int rc;
     +	struct list_head *head;
    -+	struct list_head *run_me;
    ++	struct hook *run_me;
     +	struct run_hooks_opt *options;
    -+};
    -+
    -+#define RUN_HOOKS_OPT_INIT_SYNC  {   		\
    - 	.env = STRVEC_INIT, 			\
    - 	.args = STRVEC_INIT, 			\
    - 	.path_to_stdin = NULL,			\
    -+	.jobs = 1,				\
    - 	.run_hookdir = configured_hookdir_opt()	\
    - }
    + };
      
    -+#define RUN_HOOKS_OPT_INIT_ASYNC {		\
    -+	.env = STRVEC_INIT, 			\
    -+	.args = STRVEC_INIT, 			\
    -+	.path_to_stdin = NULL,			\
    -+	.jobs = configured_hook_jobs(),		\
    -+	.run_hookdir = configured_hookdir_opt()	\
    -+}
    -+
    -+
    - void run_hooks_opt_init(struct run_hooks_opt *o);
    +-void run_hooks_opt_init(struct run_hooks_opt *o);
    ++void run_hooks_opt_init_sync(struct run_hooks_opt *o);
    ++void run_hooks_opt_init_async(struct run_hooks_opt *o);
      void run_hooks_opt_clear(struct run_hooks_opt *o);
      
    + /*
13:  f84c879d5a <  -:  ---------- hook: allow specifying working directory for hooks
 -:  ---------- > 13:  0c4add98a4 hook: allow specifying working directory for hooks
14:  ac9cec6587 = 14:  1847c4c675 run-command: add stdin callback for parallelization
15:  71fca28ccf ! 15:  ab781c94d7 hook: provide stdin by string_list or callback
    @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *comm
      	}
      
      	/* re-set the scope so we show where an override was specified */
    +@@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)
    + 	o->run_hookdir = configured_hookdir_opt();
    + 	o->jobs = 1;
    + 	o->dir = NULL;
    ++	o->feed_pipe = NULL;
    ++	o->feed_pipe_ctx = NULL;
    + }
    + 
    + void run_hooks_opt_init_async(struct run_hooks_opt *o)
     @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)
    - {
    - 	strvec_clear(&o->env);
      	strvec_clear(&o->args);
    -+	string_list_clear(&o->str_stdin, 0);
      }
      
    - 
    -+static int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)
    ++int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)
     +{
     +	int *item_idx;
     +	struct hook *ctx = pp_task_cb;
    -+	struct string_list *to_pipe = &((struct hook_cb_data*)pp_cb)->options->str_stdin;
    ++	struct string_list *to_pipe = ((struct hook_cb_data*)pp_cb)->options->feed_pipe_ctx;
     +
     +	/* Bootstrap the state manager if necessary. */
     +	if (!ctx->feed_pipe_cb_data) {
    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)
      	if (!options)
      		BUG("a struct run_hooks_opt must be provided to run_hooks");
      
    -+	if ((options->path_to_stdin && options->str_stdin.nr) ||
    -+	    (options->path_to_stdin && options->feed_pipe) ||
    -+	    (options->str_stdin.nr && options->feed_pipe))
    ++	if (options->path_to_stdin && options->feed_pipe)
     +		BUG("choose only one method to populate stdin");
    -+
    -+	if (options->str_stdin.nr)
    -+		options->feed_pipe = &pipe_from_string_list;
     +
      	strbuf_addstr(&hookname_str, hookname);
      
    @@ hook.h
      #include "strvec.h"
     +#include "run-command.h"
      
    - struct hook
    - {
    -@@ hook.h: struct hook
    + struct hook {
    + 	struct list_head list;
    +@@ hook.h: struct hook {
      	/* The literal command to run. */
      	struct strbuf command;
    - 	int from_hookdir;
    + 	unsigned from_hookdir : 1;
     +
     +	/*
     +	 * Use this to keep state for your feed_pipe_fn if you are using
    @@ hook.h: struct run_hooks_opt
      
      	/* Path to file which should be piped to stdin for each hook */
      	const char *path_to_stdin;
    -+	/* Pipe each string to stdin, separated by newlines */
    -+	struct string_list str_stdin;
     +	/*
     +	 * Callback and state pointer to ask for more content to pipe to stdin.
     +	 * Will be called repeatedly, for each hook. See
     +	 * hook.c:pipe_from_stdin() for an example. Keep per-hook state in
     +	 * hook.feed_pipe_cb_data (per process). Keep initialization context in
     +	 * feed_pipe_ctx (shared by all processes).
    ++	 *
    ++	 * See 'pipe_from_string_list()' for info about how to specify a
    ++	 * string_list as the stdin input instead of writing your own handler.
     +	 */
     +	feed_pipe_fn feed_pipe;
     +	void *feed_pipe_ctx;
    @@ hook.h: struct run_hooks_opt
     +
      };
      
    ++/*
    ++ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the
    ++ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.
    ++ * This will pipe each string in the list to stdin, separated by newlines.  (Do
    ++ * not inject your own newlines.)
    ++ */
    ++int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);
    ++
      /*
    -@@ hook.h: struct hook_cb_data {
    - 	.path_to_stdin = NULL,			\
    - 	.jobs = 1,				\
    - 	.dir = NULL,				\
    -+	.str_stdin = STRING_LIST_INIT_DUP,	\
    -+	.feed_pipe = NULL,			\
    -+	.feed_pipe_ctx = NULL,			\
    - 	.run_hookdir = configured_hookdir_opt()	\
    - }
    - 
    -@@ hook.h: struct hook_cb_data {
    - 	.path_to_stdin = NULL,			\
    - 	.jobs = configured_hook_jobs(),		\
    - 	.dir = NULL,				\
    -+	.str_stdin = STRING_LIST_INIT_DUP,	\
    -+	.feed_pipe = NULL,			\
    -+	.feed_pipe_ctx = NULL,			\
    - 	.run_hookdir = configured_hookdir_opt()	\
    - }
    - 
    +  * Callback provided to feed_pipe_fn and consume_sideband_fn.
    +  */
16:  98253fa8fd = 16:  c51bf46e8d run-command: allow capturing of collated output
17:  9505812b74 ! 17:  b90a4ee79b hooks: allow callers to capture output
    @@ Commit message
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
      ## hook.c ##
    +@@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)
    + 	o->dir = NULL;
    + 	o->feed_pipe = NULL;
    + 	o->feed_pipe_ctx = NULL;
    ++	o->consume_sideband = NULL;
    + }
    + 
    + void run_hooks_opt_init_async(struct run_hooks_opt *o)
     @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)
      				   pick_next_hook,
      				   notify_start_failure,
    @@ hook.h: struct run_hooks_opt
      	/* Number of threads to parallelize across */
      	int jobs;
      
    -@@ hook.h: struct hook_cb_data {
    - 	.str_stdin = STRING_LIST_INIT_DUP,	\
    - 	.feed_pipe = NULL,			\
    - 	.feed_pipe_ctx = NULL,			\
    -+	.consume_sideband = NULL,		\
    - 	.run_hookdir = configured_hookdir_opt()	\
    - }
    - 
    -@@ hook.h: struct hook_cb_data {
    - 	.str_stdin = STRING_LIST_INIT_DUP,	\
    - 	.feed_pipe = NULL,			\
    - 	.feed_pipe_ctx = NULL,			\
    -+	.consume_sideband = NULL,		\
    - 	.run_hookdir = configured_hookdir_opt()	\
    - }
    - 

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 03/37] hook: add list command
  2021-03-12  8:20   ` Ævar Arnfjörð Bjarmason
@ 2021-03-24 17:31     ` Emily Shaffer
  2021-03-25 12:36       ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-24 17:31 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 09:20:05AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
> > new file mode 100644
> > index 0000000000..71449ecbc7
> > --- /dev/null
> > +++ b/Documentation/config/hook.txt
> > @@ -0,0 +1,9 @@
> > +hook.<command>.command::
> > +	A command to execute during the <command> hook event. This can be an
> > +	executable on your device, a oneliner for your shell, or the name of a
> > +	hookcmd. See linkgit:git-hook[1].
> > +
> > +hookcmd.<name>.command::
> > +	A command to execute during a hook for which <name> has been specified
> > +	as a command. This can be an executable on your device or a oneliner for
> > +	your shell. See linkgit:git-hook[1].
> > diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
> > index 9eeab0009d..f19875ed68 100644
> > --- a/Documentation/git-hook.txt
> > +++ b/Documentation/git-hook.txt
> > @@ -8,12 +8,65 @@ git-hook - Manage configured hooks
> >  SYNOPSIS
> >  --------
> >  [verse]
> > -'git hook'
> > +'git hook' list <hook-name>
> 
> Having just read this far (maybe this pattern is shared in the rest of
> the series): Let's just squash this and the 2nd patch together.
> 
> Sometimes it's worth doing the scaffolding first, but adding a new
> built-in is so trivial that I don't think it's worth it, and it just
> results in back & forth churn like the above...

Yeah, I think you are right here :)

> > +void free_hook(struct hook *ptr)
> > +{
> > +	if (ptr) {
> > +		strbuf_release(&ptr->command);
> > +		free(ptr);
> > +	}
> > +}
> 
> Neither strbuf_release() nor free() need or should have a "if (ptr)" guard.

I'll take free() out of the if guard, but I think
'strbuf_release(&<null>->command)' will go poorly - dereferencing the
NULL to even invoke strbuf_release will not be a happy time, and
strbuf_release internally is not NULL-resistant.

> > +struct list_head* hook_list(const struct strbuf* hookname)
> > +{
> > +	struct strbuf hook_key = STRBUF_INIT;
> > +	struct list_head *hook_head = xmalloc(sizeof(struct list_head));
> > +	struct hook_config_cb cb_data = { &hook_key, hook_head };
> > +
> > +	INIT_LIST_HEAD(hook_head);
> > +
> > +	if (!hookname)
> > +		return NULL;
> 
> ...if a strbuf being passed in is NULL?

Yeah, I think this is misplaced. But since it sounds like generally
folks don't like having the strbuf at the input here, I will address the
error checking then also.

> 
> > [...]
> > +ROOT=
> > +if test_have_prereq MINGW
> > +then
> > +	# In Git for Windows, Unix-like paths work only in shell scripts;
> > +	# `git.exe`, however, will prefix them with the pseudo root directory
> > +	# (of the Unix shell). Let's accommodate for that.
> > +	ROOT="$(cd / && pwd)"
> > +fi
> 
> I didn't read up on previous rounds, but if we're squashing this into 02
> having a seperate commit summarizing this little hack would be most
> welcome, or have it in this commit message.

Sure. I squashed it in from a commit dscho sent, so I can preserve that
commit in tree instead.

> 
> Isn't this sort of thing generally usable, maybe we can add it under a
> longer variable name to test-lib.sh?

I wonder. `git grep cd \/ &&` shows me that this hack also happens in
t1509-root-work-tree.sh. I think most tests must use relative paths, so
this must not be in broad use? But since it's not used elsewhere I feel
ambivalent about adding a helper to test-lib.sh. I can if you feel
strongly :)

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 04/37] hook: include hookdir hook in list
  2021-03-12  8:30   ` Ævar Arnfjörð Bjarmason
@ 2021-03-24 17:56     ` Emily Shaffer
  2021-03-24 19:11       ` Junio C Hamano
  0 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-24 17:56 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 09:30:04AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> > Historically, hooks are declared by placing an executable into
> > $GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken
> > from the config are more featureful than hooks placed in the $HOOKDIR,
> > those hooks should not stop working for users who already have them.
> > Let's list them to the user, but instead of displaying a config scope
> > (e.g. "global: blah") we can prefix them with "hookdir:".
> >
> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> > ---
> >
> > Notes:
> >     Since v7, fix some nits from Jonathan Tan. The largest is to move reference to
> >     "hookdir annotation" from this commit to the next one which introduces the
> >     hook.runHookDir option.
> >
> >  builtin/hook.c                | 11 +++++++++--
> >  hook.c                        | 17 +++++++++++++++++
> >  hook.h                        |  1 +
> >  t/t1360-config-based-hooks.sh | 19 +++++++++++++++++++
> >  4 files changed, 46 insertions(+), 2 deletions(-)
> >
> > diff --git a/builtin/hook.c b/builtin/hook.c
> > index bb64cd77ca..c8fbfbb39d 100644
> > --- a/builtin/hook.c
> > +++ b/builtin/hook.c
> > @@ -40,10 +40,15 @@ static int list(int argc, const char **argv, const char *prefix)
> >  
> >  	list_for_each(pos, head) {
> >  		struct hook *item = list_entry(pos, struct hook, list);
> > -		if (item)
> > +		item = list_entry(pos, struct hook, list);
> > +		if (item) {
> > +			/* Don't translate 'hookdir' - it matches the config */
> 
> Let's prefix comments for translators with /* TRANSLATORS: .., see the
> coding style doc. That's what they'll see, and this is useful to them.
> 
> Better yet have a note here about the first argument being 'system',
> 'local' etc., which I had to source spelunge for, and translators won't
> have any idea about unless the magic parameter is documented.

It's not a comment for translators. It's a comment for someone helpful
who comes later and says "oh, none of this is marked for translation,
I'd better fix that."

> 
> > +setup_hookdir () {
> > +	mkdir .git/hooks
> > +	write_script .git/hooks/pre-commit <<-EOF
> > +	echo \"Legacy Hook\"
> 
> Nit, "'s not needed, but it also seems nothing uses this, so if it's
> just a pass-through script either "exit 0", or actually check if it's
> run or something?

The output is checked in the run tests later on. I can remove it for
this commit if you want.

> 
> > [...]
> > +test_expect_success 'git hook list shows hooks from the hookdir' '
> > +	setup_hookdir &&
> > +
> > +	cat >expected <<-EOF &&
> > +	hookdir: $(pwd)/.git/hooks/pre-commit
> > +	EOF
> > +
> > +	git hook list pre-commit >actual &&
> > +	test_cmp expected actual
> > +'
> 
> Ah, so it's just checking if it exists...

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 05/37] hook: teach hook.runHookDir
  2021-03-12  8:33   ` Ævar Arnfjörð Bjarmason
@ 2021-03-24 18:46     ` Emily Shaffer
  2021-03-24 22:38       ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-24 18:46 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 09:33:46AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> > +	switch (should_run_hookdir) {
> > +		case HOOKDIR_NO:
> 
> Style: case shouldn't be indented

Done, thanks.

> 
> > +			strbuf_addstr(&hookdir_annotation, _(" (will not run)"));
> > +			break;
> > +		case HOOKDIR_ERROR:
> > +			strbuf_addstr(&hookdir_annotation, _(" (will error and not run)"));
> > +			break;
> > +		case HOOKDIR_INTERACTIVE:
> > +			strbuf_addstr(&hookdir_annotation, _(" (will prompt)"));
> > +			break;
> > +		case HOOKDIR_WARN:
> > +			strbuf_addstr(&hookdir_annotation, _(" (will warn but run)"));
> > +			break;
> > +		case HOOKDIR_YES:
> > +		/*
> > +		 * The default behavior should agree with
> > +		 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
> > +		 * do the default behavior.
> > +		 */
> > +		case HOOKDIR_UNKNOWN:
> > +		default:
> > +			break;
> 
> We should avoid this sort of translation lego.
> 
> > +	}
> > +
> >  	list_for_each(pos, head) {
> >  		struct hook *item = list_entry(pos, struct hook, list);
> >  		item = list_entry(pos, struct hook, list);
> >  		if (item) {
> >  			/* Don't translate 'hookdir' - it matches the config */
> > -			printf("%s: %s\n",
> > +			printf("%s: %s%s\n",
> 
> native speakers in some languages to read the sentance backwards.
> Because if you concatenate strings like this you force.
> 
> (We don't currently have a RTL language in po/, still, but let's not
> create churn for if/when we do if we can help it)>

Yeah, you are absolutely right. I'll take a look at your suggestion,
thanks.

> 
> 
> I have a patch on top to fix this, will send it as some general reply of
> proposed fixup.s

FWIW, I found this format of suggestions really hard to navigate. I had
to go find your fixup (and I think you sent two different ones) and then
had to scroll around and find what you're referring to from here. If I
were to apply the fixup directly, I'd have to split it up and find the
appropriate commits to associate each part of the diff with. I know
generating scissors patches for each review is a pain, but I'd even
prefer a prose "How about printing in each part of the case instead, so
each string can be translated in full" or a non-formatted example inline
to the catchall fixups patch you sent.

> 
> >  			       (item->from_hookdir
> > +	git hook list pre-commit >actual &&
> > +	# the hookdir annotation is translated
> > +	test_i18ncmp expected actual
> 
> This (and the rest of test_i18ncmp in this series) can and should just
> be "test_cmp" or "test_i18ncmp", the poison mode is dead. See my recent
> patches to search/replace test_i18ncmp.
> 
> The reason the function isn't gone entirely was to help a series like
> yours in "seen", but if we're re-rolling...

Oh cool, thanks, will do.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 04/37] hook: include hookdir hook in list
  2021-03-24 17:56     ` Emily Shaffer
@ 2021-03-24 19:11       ` Junio C Hamano
  2021-03-24 19:23         ` Eric Sunshine
  0 siblings, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-03-24 19:11 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: Ævar Arnfjörð Bjarmason, git

Emily Shaffer <emilyshaffer@google.com> writes:

>> > @@ -40,10 +40,15 @@ static int list(int argc, const char **argv, const char *prefix)
>> >  
>> >  	list_for_each(pos, head) {
>> >  		struct hook *item = list_entry(pos, struct hook, list);
>> > -		if (item)
>> > +		item = list_entry(pos, struct hook, list);
>> > +		if (item) {
>> > +			/* Don't translate 'hookdir' - it matches the config */
>> 
>> Let's prefix comments for translators with /* TRANSLATORS: .., see the
>> coding style doc. That's what they'll see, and this is useful to them.
>> 
>> Better yet have a note here about the first argument being 'system',
>> 'local' etc., which I had to source spelunge for, and translators won't
>> have any idea about unless the magic parameter is documented.
>
> It's not a comment for translators. It's a comment for someone helpful
> who comes later and says "oh, none of this is marked for translation,
> I'd better fix that."

Then, it is not limited to "hookdir", is it?  Resurrecting the
elided part back here:

Not just we do not want "hookdir" placed inside _(),

 			printf("%s: %s\n",
+			       (item->from_hookdir
+				? "hookdir"
+				: config_scope_name(item->origin)),
 			       item->command.buf);

we do not want the "%s: %s\n" to be placed inside _() and get munged
into "%2$s: %1$s\n" for languages that want the order swapped, for
example.

So perhaps the comment should be about the entire output, i.e.
"don't translate the output from this helper, as it is meant to be
machine parseable", or something?


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 04/37] hook: include hookdir hook in list
  2021-03-24 19:11       ` Junio C Hamano
@ 2021-03-24 19:23         ` Eric Sunshine
  2021-03-24 20:07           ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Eric Sunshine @ 2021-03-24 19:23 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Emily Shaffer, Ævar Arnfjörð Bjarmason, Git List

On Wed, Mar 24, 2021 at 3:12 PM Junio C Hamano <gitster@pobox.com> wrote:
> Not just we do not want "hookdir" placed inside _(),
>
>                         printf("%s: %s\n",
> +                              (item->from_hookdir
> +                               ? "hookdir"
> +                               : config_scope_name(item->origin)),
>                                item->command.buf);
>
> we do not want the "%s: %s\n" to be placed inside _() and get munged
> into "%2$s: %1$s\n" for languages that want the order swapped, for
> example.
>
> So perhaps the comment should be about the entire output, i.e.
> "don't translate the output from this helper, as it is meant to be
> machine parseable", or something?

Having the word "translate" in the comment automatically implies
localization, which confuses the issue. It would be clearer to avoid
that word altogether. Perhaps something along the lines of:

    /* machine-parseable output; do not apply _() localization */

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 04/37] hook: include hookdir hook in list
  2021-03-24 19:23         ` Eric Sunshine
@ 2021-03-24 20:07           ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-24 20:07 UTC (permalink / raw)
  To: Eric Sunshine
  Cc: Junio C Hamano, Ævar Arnfjörð Bjarmason, Git List

On Wed, Mar 24, 2021 at 03:23:30PM -0400, Eric Sunshine wrote:
> 
> On Wed, Mar 24, 2021 at 3:12 PM Junio C Hamano <gitster@pobox.com> wrote:
> > Not just we do not want "hookdir" placed inside _(),
> >
> >                         printf("%s: %s\n",
> > +                              (item->from_hookdir
> > +                               ? "hookdir"
> > +                               : config_scope_name(item->origin)),
> >                                item->command.buf);
> >
> > we do not want the "%s: %s\n" to be placed inside _() and get munged
> > into "%2$s: %1$s\n" for languages that want the order swapped, for
> > example.
> >
> > So perhaps the comment should be about the entire output, i.e.
> > "don't translate the output from this helper, as it is meant to be
> > machine parseable", or something?
> 
> Having the word "translate" in the comment automatically implies
> localization, which confuses the issue. It would be clearer to avoid
> that word altogether. Perhaps something along the lines of:
> 
>     /* machine-parseable output; do not apply _() localization */

After I read Ævar's comments on the next patch in this series, I decided
to rework the comments and translation markers for this whole section.

  if (item) {
          if (item->from_hookdir) {
                  /*
                   * TRANSLATORS: do not translate 'hookdir' as
                   * it matches the config setting.
                   */
                  switch (should_run_hookdir) {
                  case HOOKDIR_NO:
                          printf(_("hookdir: %s (will not run)\n"),
                                 item->command.buf);
                          break;
                  case HOOKDIR_ERROR:
                          printf(_("hookdir: %s (will error and not run)\n"),
                                 item->command.buf);
                          break;
                  case HOOKDIR_INTERACTIVE:
                          printf(_("hookdir: %s (will prompt)\n"),
                                 item->command.buf);
                          break;
                  case HOOKDIR_WARN:
                          printf(_("hookdir: %s (will warn but run)\n"),
                                 item->command.buf);
                          break;
                  case HOOKDIR_YES:
                  /*
                   * The default behavior should agree with
                   * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
                   * do the default behavior.
                   */
                  case HOOKDIR_UNKNOWN:
                  default:
                          printf(_("hookdir: %s\n"),
                                   item->command.buf);
                          break;
                  }
          } else {
                  /*
                   * TRANSLATORS: "<config scope>: <path>". Both fields
                   * should be left untranslated; config scope matches the
                   * output of 'git config --show-scope'. Marked for
                   * translation to provide better RTL support later.
                   */
                  printf(_("%s: %s\n"),
                          config_scope_name(item->origin),
                          item->command.buf);
          }
  }

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 07/37] parse-options: parse into strvec
  2021-03-12  8:50   ` Ævar Arnfjörð Bjarmason
@ 2021-03-24 20:34     ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-24 20:34 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 09:50:58AM +0100, Ævar Arnfjörð Bjarmason wrote:
[snip]
> Nice, seems very useful.
> 
> But let's add a test in test-parse-options.c like we have for
> string_list?

Sure, done. Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 08/37] hook: add 'run' subcommand
  2021-03-12  8:54   ` Ævar Arnfjörð Bjarmason
@ 2021-03-24 21:29     ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-24 21:29 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 09:54:28AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> >  'git hook' list <hook-name>
> > +'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>
> 
> [...]
> 
> > +	switch (cfg)
> > +	{
> > +		case HOOKDIR_ERROR:
> 
> Overly indented case statements again.
Thanks. Probably need to fix my autoformatter or something.
> 
> > +			fprintf(stderr, _("Skipping legacy hook at '%s'\n"),
> > +				path);
> > +			/* FALLTHROUGH */
> > +		case HOOKDIR_NO:
> > +			return 0;
> > +		case HOOKDIR_WARN:
> > +			fprintf(stderr, _("Running legacy hook at '%s'\n"),
> > +				path);
> > +			return 1;
> > +		case HOOKDIR_INTERACTIVE:
> > +			do {
> > +				/*
> > +				 * TRANSLATORS: Make sure to include [Y] and [n]
> > +				 * in your translation. Only English input is
> > +				 * accepted. Default option is "yes".
> > +				 */
> > +				fprintf(stderr, _("Run '%s'? [Yn] "), path);
> 
> Nit: [Y/n]
ACK

> 
> > +				} else if (starts_with(prompt.buf, "y")) {
> 
> So also "Y", "yes" and "yellow"...
That's also how add-patch.c:prompt_yesno(),
builtin/bisect--helper.c:decide_next(), and
git-add--interactive.perl:prompt_yesno (assuming I'm grokking the perl
correctly) work. builtin/clean.c:ask_each_cmd checks that the user's
reply matches a substring anchored to the beginning (e.g. "y", "ye",
"yes").  git-diftool--helper.sh:launch_merge_tool just checks for "not
'n'". And git-send-email.perl:ask just checks for "'y' or not".

So I think there's a little flexibility :) But I like builtin/clean.c's
approach most out of these, so I'll switch.

> 
> > [...]
> >  	git hook list pre-commit >actual &&
> >  	# the hookdir annotation is translated
> > -	test_i18ncmp expected actual
> > +	test_i18ncmp expected actual &&
> > +
> > +	test_write_lines n | git hook run pre-commit 2>actual &&
> > +	! grep "Legacy Hook" actual &&
> > +
> > +	test_write_lines y | git hook run pre-commit 2>actual &&
> > +	grep "Legacy Hook" actual
> > +'
> > +
> > +test_expect_success 'inline hook definitions execute oneliners' '
> > +	test_config hook.pre-commit.command "echo \"Hello World\"" &&
> > +
> > +	echo "Hello World" >expected &&
> > +
> > +	# hooks are run with stdout_to_stderr = 1
> > +	git hook run pre-commit 2>actual &&
> > +	test_cmp expected actual
> > +'
> > +
> > +test_expect_success 'inline hook definitions resolve paths' '
> > +	write_script sample-hook.sh <<-EOF &&
> > +	echo \"Sample Hook\"
> > +	EOF
> > +
> > +	test_when_finished "rm sample-hook.sh" &&
> > +
> > +	test_config hook.pre-commit.command "\"$(pwd)/sample-hook.sh\"" &&
> > +
> > +	echo \"Sample Hook\" >expected &&
> > +
> > +	# hooks are run with stdout_to_stderr = 1
> > +	git hook run pre-commit 2>actual &&
> > +	test_cmp expected actual
> > +'
> > +
> > +test_expect_success 'hookdir hook included in git hook run' '
> > +	setup_hookdir &&
> > +
> > +	echo \"Legacy Hook\" >expected &&
> > +
> > +	# hooks are run with stdout_to_stderr = 1
> > +	git hook run pre-commit 2>actual &&
> > +	test_cmp expected actual
> > +'
> > +
> > +test_expect_success 'out-of-repo runs excluded' '
> > +	setup_hooks &&
> > +
> > +	nongit test_must_fail git hook run pre-commit
> >  '
> >  
> >  test_expect_success 'hook.runHookDir is tolerant to unknown values' '
> 
> No tests for --env or --arg?

Yikes, I guess not. Thanks, will add.

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 17/37] hooks: allow callers to capture output
  2021-03-12  9:08   ` Ævar Arnfjörð Bjarmason
@ 2021-03-24 21:54     ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-24 21:54 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 10:08:04AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> > Some server-side hooks will require capturing output to send over
> > sideband instead of printing directly to stderr. Expose that capability.
> 
> So added here in 17/37 and not used until 30/37. As a point on
> readability (this isn't the first such patch) I think it would be better
> to just squash those together with some "since we now need access to
> consume_sideband in hooks, do that ...".

Yeah. When I was putting together the series I had two thoughts on how
best to organize it:

1. Adding functionality just-in-time for the hook that needs it (like
you describe)
or
2. Implementing the whole utility, then doing hook conversions in a
separate chunk or series (what I went with).

I chose 2 for a couple reasons: that it would be easier for people who
just care "did a hook I use start working differently?" to review only
the second chunk of the change, and that it would be easier if we wanted
to adopt the library part into the codebase without converting the hooks
to use it (this was listed as a step in the design doc, but I think we
ended up abandoning it). The differentiation was certainly easier when I
had the two "chunks" separated into a part I and part II series, but
Junio asked me to combine them starting with this revision so it would
be easier to merge to 'seen' (as I understood it).

At this point, I'd prefer not to rearrange the series, though.

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 05/37] hook: teach hook.runHookDir
  2021-03-24 18:46     ` Emily Shaffer
@ 2021-03-24 22:38       ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-24 22:38 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Wed, Mar 24 2021, Emily Shaffer wrote:

> On Fri, Mar 12, 2021 at 09:33:46AM +0100, �var Arnfj�r� Bjarmason wrote:
>> 
>> 
>> On Thu, Mar 11 2021, Emily Shaffer wrote:
>> 
>> > +	switch (should_run_hookdir) {
>> > +		case HOOKDIR_NO:
>> 
>> Style: case shouldn't be indented
>
> Done, thanks.
>
>> 
>> > +			strbuf_addstr(&hookdir_annotation, _(" (will not run)"));
>> > +			break;
>> > +		case HOOKDIR_ERROR:
>> > +			strbuf_addstr(&hookdir_annotation, _(" (will error and not run)"));
>> > +			break;
>> > +		case HOOKDIR_INTERACTIVE:
>> > +			strbuf_addstr(&hookdir_annotation, _(" (will prompt)"));
>> > +			break;
>> > +		case HOOKDIR_WARN:
>> > +			strbuf_addstr(&hookdir_annotation, _(" (will warn but run)"));
>> > +			break;
>> > +		case HOOKDIR_YES:
>> > +		/*
>> > +		 * The default behavior should agree with
>> > +		 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
>> > +		 * do the default behavior.
>> > +		 */
>> > +		case HOOKDIR_UNKNOWN:
>> > +		default:
>> > +			break;
>> 
>> We should avoid this sort of translation lego.
>> 
>> > +	}
>> > +
>> >  	list_for_each(pos, head) {
>> >  		struct hook *item = list_entry(pos, struct hook, list);
>> >  		item = list_entry(pos, struct hook, list);
>> >  		if (item) {
>> >  			/* Don't translate 'hookdir' - it matches the config */
>> > -			printf("%s: %s\n",
>> > +			printf("%s: %s%s\n",
>> 
>> native speakers in some languages to read the sentance backwards.
>> Because if you concatenate strings like this you force.
>> 
>> (We don't currently have a RTL language in po/, still, but let's not
>> create churn for if/when we do if we can help it)>
>
> Yeah, you are absolutely right. I'll take a look at your suggestion,
> thanks.
>
>> 
>> 
>> I have a patch on top to fix this, will send it as some general reply of
>> proposed fixup.s
>
> FWIW, I found this format of suggestions really hard to navigate. I had
> to go find your fixup (and I think you sent two different ones) and then
> had to scroll around and find what you're referring to from here. If I
> were to apply the fixup directly, I'd have to split it up and find the
> appropriate commits to associate each part of the diff with. I know
> generating scissors patches for each review is a pain, but I'd even
> prefer a prose "How about printing in each part of the case instead, so
> each string can be translated in full" or a non-formatted example inline
> to the catchall fixups patch you sent.

Sorry about that. FWIW I did try, but found myself jumping a bit too
much between different commits (as noted in some "maybe squash?"
comments elsewhere), and eventually just gave up and produced some
RFC-just-for-discussion patch on top of the whole thing and mostly
looked at the entire diff forthe series.

>> 
>> >  			       (item->from_hookdir
>> > +	git hook list pre-commit >actual &&
>> > +	# the hookdir annotation is translated
>> > +	test_i18ncmp expected actual
>> 
>> This (and the rest of test_i18ncmp in this series) can and should just
>> be "test_cmp" or "test_i18ncmp", the poison mode is dead. See my recent
>> patches to search/replace test_i18ncmp.
>> 
>> The reason the function isn't gone entirely was to help a series like
>> yours in "seen", but if we're re-rolling...
>
> Oh cool, thanks, will do.


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 03/37] hook: add list command
  2021-03-24 17:31     ` Emily Shaffer
@ 2021-03-25 12:36       ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-25 12:36 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git


On Wed, Mar 24 2021, Emily Shaffer wrote:

> On Fri, Mar 12, 2021 at 09:20:05AM +0100, �var Arnfj�r� Bjarmason wrote:
>> 
>> 
>> On Thu, Mar 11 2021, Emily Shaffer wrote:
>> 
>> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
>> > new file mode 100644
>> > index 0000000000..71449ecbc7
>> > --- /dev/null
>> > +++ b/Documentation/config/hook.txt
>> > @@ -0,0 +1,9 @@
>> > +hook.<command>.command::
>> > +	A command to execute during the <command> hook event. This can be an
>> > +	executable on your device, a oneliner for your shell, or the name of a
>> > +	hookcmd. See linkgit:git-hook[1].
>> > +
>> > +hookcmd.<name>.command::
>> > +	A command to execute during a hook for which <name> has been specified
>> > +	as a command. This can be an executable on your device or a oneliner for
>> > +	your shell. See linkgit:git-hook[1].
>> > diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
>> > index 9eeab0009d..f19875ed68 100644
>> > --- a/Documentation/git-hook.txt
>> > +++ b/Documentation/git-hook.txt
>> > @@ -8,12 +8,65 @@ git-hook - Manage configured hooks
>> >  SYNOPSIS
>> >  --------
>> >  [verse]
>> > -'git hook'
>> > +'git hook' list <hook-name>
>> 
>> Having just read this far (maybe this pattern is shared in the rest of
>> the series): Let's just squash this and the 2nd patch together.
>> 
>> Sometimes it's worth doing the scaffolding first, but adding a new
>> built-in is so trivial that I don't think it's worth it, and it just
>> results in back & forth churn like the above...
>
> Yeah, I think you are right here :)
>
>> > +void free_hook(struct hook *ptr)
>> > +{
>> > +	if (ptr) {
>> > +		strbuf_release(&ptr->command);
>> > +		free(ptr);
>> > +	}
>> > +}
>> 
>> Neither strbuf_release() nor free() need or should have a "if (ptr)" guard.
>
> I'll take free() out of the if guard, but I think
> 'strbuf_release(&<null>->command)' will go poorly - dereferencing the
> NULL to even invoke strbuf_release will not be a happy time, and
> strbuf_release internally is not NULL-resistant.

Sorry I meant something like:

    if (ptr) strbuf_release(&ptr->command);
    free(ptr);

But maybe even more idiomatic would be:

    if (!ptr)
	return;
    strbuf_release(&ptr->command);
    free(ptr);

Or some other variant of checking teh container struct early. Anyway,
this doesn't really matter, per a below comment I had more meaningful
feedback in [1]. Most of my other traffic on this topic (including this)
was some stream-of-consciousness notes as I went along.

>> > +struct list_head* hook_list(const struct strbuf* hookname)
>> > +{
>> > +	struct strbuf hook_key = STRBUF_INIT;
>> > +	struct list_head *hook_head = xmalloc(sizeof(struct list_head));
>> > +	struct hook_config_cb cb_data = { &hook_key, hook_head };
>> > +
>> > +	INIT_LIST_HEAD(hook_head);
>> > +
>> > +	if (!hookname)
>> > +		return NULL;
>> 
>> ...if a strbuf being passed in is NULL?
>
> Yeah, I think this is misplaced. But since it sounds like generally
> folks don't like having the strbuf at the input here, I will address the
> error checking then also.
>
>> 
>> > [...]
>> > +ROOT=
>> > +if test_have_prereq MINGW
>> > +then
>> > +	# In Git for Windows, Unix-like paths work only in shell scripts;
>> > +	# `git.exe`, however, will prefix them with the pseudo root directory
>> > +	# (of the Unix shell). Let's accommodate for that.
>> > +	ROOT="$(cd / && pwd)"
>> > +fi
>> 
>> I didn't read up on previous rounds, but if we're squashing this into 02
>> having a seperate commit summarizing this little hack would be most
>> welcome, or have it in this commit message.
>
> Sure. I squashed it in from a commit dscho sent, so I can preserve that
> commit in tree instead.
>
>> 
>> Isn't this sort of thing generally usable, maybe we can add it under a
>> longer variable name to test-lib.sh?
>
> I wonder. `git grep cd \/ &&` shows me that this hack also happens in
> t1509-root-work-tree.sh. I think most tests must use relative paths, so
> this must not be in broad use? But since it's not used elsewhere I feel
> ambivalent about adding a helper to test-lib.sh. I can if you feel
> strongly :)

After I sent this I saw that pretty much the same thing is happening in
t1300-config.sh for the --show-origin option.

    ! test_have_prereq MINGW ||
    HOME="$(pwd)" # convert to Windows path

I don't feel strongly about this at all, but per the outstanding
feedback I had in[1] I wondered whether this whole thing wouln't be
better as some variant of "git config --show-origin",

1. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/#t

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 00/37] config-based hooks
  2021-03-12 11:13 ` Ævar Arnfjörð Bjarmason
@ 2021-03-25 12:41   ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-03-25 12:41 UTC (permalink / raw)
  To: Emily Shaffer
  Cc: git, Jeff King, Junio C Hamano, James Ramsay, Jonathan Nieder,
	brian m. carlson, Phillip Wood, Josh Steadmon,
	Johannes Schindelin, Jonathan Tan


On Fri, Mar 12 2021, Ævar Arnfjörð Bjarmason wrote:

A small correction to one of my comments:

> On Thu, Mar 11 2021, Emily Shaffer wrote:

>  2. You're sticking full paths in the git config key, which is
>     case-insensitive, and a feature of this format is being able to
>     configure/override previously configured hooks.
>
>     So the behavior of this feature depends on git's interaction with
>     the case-sensitivity of filesystems, and not just one fs, any fs
>     we're walking in our various config sources, and where the hook
>     itself lives.
>
>     As recent CVEs have shown that's a big can of worms, particularly
>     for something whose goal is to address the security aspect of
>     running hooks from other config.
>
>     Arguably the case-sensitivity issue is just confusing since we
>     canonicalize it anyway. But once you add in FS path canonicalization
>     it becomes a real big can of worms. See the .gitmodules fsck code.
>
>     Even if it wasn't for that it's relatively nastier to edit/maintain
>     full paths and the appropriate escaping in the double-quoted key in
>     the config file v.s. having it as an optionally quoted value.

So the "case-insensitive" part of that *mostly* doesn't apply.

I'd forgotten that we don't consider the "LeVeL" part of
"ThReE.LeVeL.KeY" to be case-insensitive, but the other two components
are, as discussed in git-config(1)'s docs.

I say "mostly" because that's tolower()'s idea of case normalization,
which may or may not match the FS's, but anyway, I think that's probably
splitting hairs, but I worry more about the path normalization aspect
noted in the last two paragraphs there.

>  3. We're left with this "*.command = cmd", and "*.skip = true"
>     special-case syntax. I can't see any reason for why it's needed over
>     simply having "*.command = true" clobber earlier hooks as noted in
>     the proposed docs above.
>
>     And that doesn't require any magic to support, just like our
>     existing "core.pager=cat" case.
>
>     I mean, I suppose it's magical in that we might otherwise error on
>     non-consumed stdin (do we?), anyway, documenting it as a synonym for
>     "cat >/dev/null" would get around that :)
>
>  4. It makes the common case of having the same hooks for N commands
>     needlessly verbose, if you can just support "type" (or whatever we
>     should call it) you can add that N times...
>
>  5. At the end of this series we're left with the start of the docs
>     saying:
>
>       You can list and run configured hooks with this command. Later,
>       you will be able to add and modify hooks with this command.
>
>     But those patches have yet to land, and looking at the design
>     document I'm skeptical of that being a good addition v.s. just
>     adding the same thing to "git config".
>
>     As just one exmaple; surely "git config edit <name>" would need to
>     run around and find config files to edit, then open them in a loop
>     for you, no?
>
>     Which we'd eventually want for "git config" in general with an
>     --edit-regexp option or whatever, which brings us (well, at least
>     me) back to "then let's just add it to git-config?".
>
>  6. The whole 'git hook' config special-casing doesn't help other
>     commands or the security issue that seemed to have prompted (at
>     least some of) its existence
>
>     In the design doc we mention the "core.pager = rm -rf /" case for a
>     .git/config.
>
>     This series doesn't implement, but the design docs note a future
>     want for solving that issue for the hooks.
>
>     To me that's another case where we should just have general config
>     syntax, not something hook-specific, e.g. if I could do this in my
>     ~/.gitconfig:
>
>        ;; We consider 'config.ignore' in reverse order, so e.g setting
>        ;; it in. ~/.gitconfig will ignore any such keys for repo-level
>        ;; config
>        [config "ignore"]
>        key = core.pager
>        keyRegexp = "^hook\."
>
>     We'd address both any hook security concerns, as well as core.pager
>     etc. We could then just have e.g. some syntax sugar of:
>
>        [include]
>        path = built-in://gimme-safe-config
>
>     Which would just be a thin layer of magit to include
>     <path-to-git-prefix>/config-templates/gimme-safe-config or whatever.
>
>     We'd thus address the issue for all config types without
>     hook-specific magic.
>
> Anyway. I'm very willing to be convinced otherwise. I just think that
> for a first-draft implementation leaving aside 'hook.<command>.command'
> and the whole 'list' thing makes sense.
>
> We can consider the core code changes relatively separately from any
> future aspirations, particularly with a 40-some patch series, and the
> end-state of *this series* IMO not really justifying, that part of the
> implementation, and thus requiring reviewers to look ahead beyond the
> 40-some patches.

Emily: *Bump* on being interesed in what you think about the rest of
this though.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 19/37] am: convert applypatch hooks to use config
  2021-03-12 10:23   ` Junio C Hamano
@ 2021-03-29 23:39     ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-29 23:39 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

On Fri, Mar 12, 2021 at 02:23:39AM -0800, Junio C Hamano wrote:
> 
> Emily Shaffer <emilyshaffer@google.com> writes:
> 
> > @@ -1558,8 +1563,10 @@ static void do_commit(const struct am_state *state)
> >  	struct commit_list *parents = NULL;
> >  	const char *reflog_msg, *author, *committer = NULL;
> >  	struct strbuf sb = STRBUF_INIT;
> > +	struct run_hooks_opt hook_opt;
> > +	run_hooks_opt_init_async(&hook_opt);
> >  
> > -	if (run_hook_le(NULL, "pre-applypatch", NULL))
> > +	if (run_hooks("pre-applypatch", &hook_opt))
> >  		exit(1);
> >  
> >  	if (write_cache_as_tree(&tree, 0, NULL))
> > @@ -1611,8 +1618,9 @@ static void do_commit(const struct am_state *state)
> >  		fclose(fp);
> >  	}
> >  
> > -	run_hook_le(NULL, "post-applypatch", NULL);
> > +	run_hooks("post-applypatch", &hook_opt);
> >  
> > +	run_hooks_opt_clear(&hook_opt);
> >  	strbuf_release(&sb);
> >  }
> 
> This one does opt_init(), run_hooks(), and another run_hooks() and
> then opt_clear().  If run_hooks() is a read-only operation on the
> hook_opt, then that would be alright, but it just smells iffy that
> it is not done as two separate opt_init(), run_hooks(), opt_clear()
> sequences for two separate run_hooks() invocations.  The same worry
> about future safety I meantioned elsewhere in the series also
> applies.

Interesting observation. I think the only thing that could be mutated in
the run_hooks_opt struct today is the caller-provided callback data
(run_hooks_opt.feed_pipe_ctx) - which presumably is being manipulated by the
caller in a callback they wrote. But I don't think it hurts particularly
to clear/init again between the two invocations, to be safe - so I will
change the code here.

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 23/37] read-cache: convert post-index-change hook to use config
  2021-03-12 10:22   ` Junio C Hamano
@ 2021-03-29 23:56     ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-29 23:56 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

On Fri, Mar 12, 2021 at 02:22:08AM -0800, Junio C Hamano wrote:
> 
> Emily Shaffer <emilyshaffer@google.com> writes:
> 
> > @@ -3070,6 +3071,8 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
> >  				 unsigned flags)
> >  {
> >  	int ret;
> > +	struct run_hooks_opt hook_opt;
> > +	run_hooks_opt_init_async(&hook_opt);
> >  
> 
> Nit. blank line between the last of decls and the first stmt (many
> identical nits exist everywhere in this series).
> 
> >  	/*
> >  	 * TODO trace2: replace "the_repository" with the actual repo instance
> > @@ -3088,9 +3091,13 @@ static int do_write_locked_index(s
> >  	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("post-index-change", &hook_opt);
> > +	run_hooks_opt_clear(&hook_opt);
> 
> There is one early return before the precontext of this hunk that
> bypasses this opt_clear() call.  It is before any member of hook_opt
> structure that was opt_init()'ed gets touched, so with the current
> code, there is no leak, but it probably is laying a landmine for the
> future, where opt_init() may allocate some resource to its member,
> with the expectation that all users of the API would call
> opt_clear() to release.  Or the caller of the API (like this one) may
> start mucking with the opt structure before the existing early return,
> at which point the current assumption that it is safe to return from
> that point without opt_clear() would be broken.
> 
> I saw that there are other early returns in the series that are safe
> right now but may become unsafe when the API implementation gets
> extended that way.  If it does not involve too much code churning,
> we may want to restructure the code to make these early returns into
> "goto"s that jump to a single exit point, so that we can always
> match opt_init() with opt_clear(), like the structure of the
> existing code allowed cmd_rebase() to use the hooks API cleanly in
> [v8 22/37].

OK. I'll audit this second half of the series looking for this type of
thing and try to clean up/use gotos if appropriate/etc. Thanks for
pointing it out.

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h
  2021-03-12 10:24   ` Junio C Hamano
@ 2021-03-29 23:59     ` Emily Shaffer
  2021-03-30  0:10       ` Junio C Hamano
  0 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-03-29 23:59 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git

On Fri, Mar 12, 2021 at 02:24:41AM -0800, Junio C Hamano wrote:
> 
> Emily Shaffer <emilyshaffer@google.com> writes:
> 
> > @@ -1435,12 +1436,19 @@ 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_sync(&opt);
> > +
> >  	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(push_to_checkout_hook, &opt)) {
> > +		run_hooks_opt_clear(&opt);
> >  		return "push-to-checkout hook declined";
> > -	else
> > +	} else {
> > +		run_hooks_opt_clear(&opt);
> >  		return NULL;
> > +	}
> >  }
> 
> OK, we opt_init(), futz with opt, call run_hooks() and opt_clear()
> regardless of the outcome from run_hooks().  Narrow-sighted me
> wonders if it makes the use of the API easier if run_hooks() did the
> opt_clear() before it returns, but I haven't yet seen enough use at
> this point to judge.

Hrm, is that idiomatic? I guess it would be convenient, and as long as
it doesn't touch explicitly caller-managed context pointer it should be
safe, but wouldn't it be surprising?

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 32/37] post-update: use hook.h library
  2021-03-12  9:14   ` Ævar Arnfjörð Bjarmason
@ 2021-03-30  0:01     ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-30  0:01 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 10:14:31AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> > By using run_hooks() instead of run_hook_le(), 'post-update' hooks can
> > be specified in the config as well as the hookdir.
> 
> Looking ahead in the series no tests for this, seems like a good thing
> to have some at least trivial tests for each hook and their config
> invocation.

I'll look through the series and make sure the hooks being converted
have at least some test to make sure they worked; I think I checked that
for some of the early ones but got lazy :) I'll try and add some
config-specified tests too. Thanks, it's on my todo list.

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'
  2021-03-12  9:21   ` Ævar Arnfjörð Bjarmason
@ 2021-03-30  0:03     ` Emily Shaffer
  2021-03-31 21:47     ` Emily Shaffer
  1 sibling, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-30  0:03 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 10:21:08AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> > By using the new 'git hook run' subcommand to run 'sendemail-validate',
> > we can reduce the boilerplate needed to run this hook in perl. Using
> > config-based hooks also allows us to run 'sendemail-validate' hooks that
> > were configured globally when running 'git send-email' from outside of a
> > Git directory, alongside other benefits like multihooks and
> > parallelization.
> >
> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> > ---
> >  git-send-email.perl   | 21 ++++-----------------
> >  t/t9001-send-email.sh | 11 +----------
> >  2 files changed, 5 insertions(+), 27 deletions(-)
> >
> > diff --git a/git-send-email.perl b/git-send-email.perl
> > index 1f425c0809..73e1e0b51a 100755
> > --- a/git-send-email.perl
> > +++ b/git-send-email.perl
> > @@ -1941,23 +1941,10 @@ sub unique_email_list {
> >  sub validate_patch {
> >  	my ($fn, $xfer_encoding) = @_;
> >  
> > -	if ($repo) {
> > -		my $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),
> > -					    'sendemail-validate');
> > -		my $hook_error;
> > -		if (-x $validate_hook) {
> > -			my $target = abs_path($fn);
> > -			# The hook needs a correct cwd and GIT_DIR.
> > -			my $cwd_save = cwd();
> > -			chdir($repo->wc_path() or $repo->repo_path())
> > -				or die("chdir: $!");
> > -			local $ENV{"GIT_DIR"} = $repo->repo_path();
> > -			$hook_error = "rejected by sendemail-validate hook"
> > -				if system($validate_hook, $target);
> > -			chdir($cwd_save) or die("chdir: $!");
> > -		}
> > -		return $hook_error if $hook_error;
> > -	}
> > +	my $target = abs_path($fn);
> > +	return "rejected by sendemail-validate hook"
> > +		if system(("git", "hook", "run", "sendemail-validate", "-a",
> > +				$target));
> 
> I see it's just moving code around, but since we're touching this:
> 
> This conflates the hook exit code with a general failure to invoke it,
> Perl's system().
> 
> Not a big deal in this case, but there's two other existing system()
> invocations which use the right blurb for it:
> 
> 
> 	system('sh', '-c', $editor.' "$@"', $editor, $_);
> 	if (($? & 127) || ($? >> 8)) {
> 		die(__("the editor exited uncleanly, aborting everything"));
> 	}
> 
> Makes sense to do something similar here for consistency. See "perldoc
> -f system" for an example.

Oh cool, thanks. I'll do that.

> 
> >  
> >  	# Any long lines will be automatically fixed if we use a suitable transfer
> >  	# encoding.
> > diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
> > index 4eee9c3dcb..456b471c5c 100755
> > --- a/t/t9001-send-email.sh
> > +++ b/t/t9001-send-email.sh
> > @@ -2101,16 +2101,7 @@ test_expect_success $PREREQ 'invoke hook' '
> >  	mkdir -p .git/hooks &&
> >  
> >  	write_script .git/hooks/sendemail-validate <<-\EOF &&
> > -	# test that we have the correct environment variable, pwd, and
> > -	# argument
> > -	case "$GIT_DIR" in
> > -	*.git)
> > -		true
> > -		;;
> > -	*)
> > -		false
> > -		;;
> > -	esac &&
> > +	# test that we have the correct argument
> 
> This and getting rid of these Perl/Python/whatever special cases is very
> nice.

I thought so too :D :D

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 36/37] run-command: stop thinking about hooks
  2021-03-12  9:23   ` Ævar Arnfjörð Bjarmason
@ 2021-03-30  0:07     ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-30  0:07 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 10:23:55AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> > hook.h has replaced all run-command.h hook-related functionality.
> > run-command.h:run_hooks_le/ve and find_hook are no longer used anywhere
> > in the codebase. So, let's delete the dead code - or, in the one case
> > where it's still needed, move it to an internal function in hook.c.
> 
> Similar to other comments about squashing, I think just having this
> happen incrementally as we remove whatever is the last user of the
> function would be better.
> 
> E.g. find_hook() is last used in one commit, run_hook*() in another...

Hm. I could see it, coupled with a blurb like, "Nobody is using this
anymore so delete."

But it feels odd to move the find_hook() impl from here to hook.c
internal in a commit about, say, bugreport.

I'll consider this, thanks. Maybe it fits in one case (like run_hook_*)
better than in another (like find_hook). I'll play with it :)

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h
  2021-03-29 23:59     ` Emily Shaffer
@ 2021-03-30  0:10       ` Junio C Hamano
  0 siblings, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-03-30  0:10 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git

Emily Shaffer <emilyshaffer@google.com> writes:

>> OK, we opt_init(), futz with opt, call run_hooks() and opt_clear()
>> regardless of the outcome from run_hooks().  Narrow-sighted me
>> wonders if it makes the use of the API easier if run_hooks() did the
>> opt_clear() before it returns, but I haven't yet seen enough use at
>> this point to judge.
>
> Hrm, is that idiomatic? I guess it would be convenient, and as long as
> it doesn't touch explicitly caller-managed context pointer it should be
> safe, but wouldn't it be surprising?

The precedent (at this point, I will not judge if it is a good
pattern to emulate or an anti-pattern to stay away from) I had in
mind was the run_command() which clears child_process structure
as the side effect of internally calling finish_command().

Leaving them separate is of course more flexible, but depending on
how small we can keep down the number of call patterns of this new
API, always having to clear after run might become an unnecessary
source of leaks.  When I gave that comment, I didn't have enough
input to decide, and now it has been so long since I gave my
reviews, I do not quite remember what my impression after reading
all the patches through was X-<.



^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages
  2021-03-12  9:29   ` Ævar Arnfjörð Bjarmason
@ 2021-03-30  0:10     ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-30  0:10 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 10:29:52AM +0100, Ævar Arnfjörð Bjarmason wrote:
> 
> 
> On Thu, Mar 11 2021, Emily Shaffer wrote:
> 
> > By showing the list of all hooks in 'git help hook' for users to refer
> > to, 'git help hook' becomes a one-stop shop for hook authorship. Since
> > some may still have muscle memory for 'git help githooks', though,
> > reference the 'git hook' commands and otherwise don't remove content.
> 
> I think this should at least have something like what my b6a8d09f6d8 (gc
> docs: include the "gc.*" section from "config" in "gc", 2019-04-07) has
> on top, i.e.:

Yeah, this seems reasonable.

>     
>     diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
>     index 4ad31ac360a..5c9af30b43e 100644
>     --- a/Documentation/git-hook.txt
>     +++ b/Documentation/git-hook.txt
>     @@ -150,10 +150,18 @@ message body and cannot be parallelized.
>      
>      CONFIGURATION
>      -------------
>     +
>     +The below documentation is the same as what's found in
>     +linkgit:git-config[1]:
>     +
>      include::config/hook.txt[]
>      
>      HOOKS
>      -----
>     +
>     +The below documentation is the same as what's found in
>     +linkgit:githooks[5]:
>     +
>      include::native-hooks.txt[]
>      
>      GIT
> 
> But I also don't think we should demote githooks(5) as the canonical doc
> page for the hooks themselves.
> 
> If you run this in your terminal:
> 
>     man 5 git<TAB>
> 
> You'll get:
> 
>     gitattributes         gitignore             gitmailmap            gitrepository-layout  
>     githooks              git-lfs-config        gitmodules            gitweb.conf 
> 
> (Well, maybe not the lfs-part, but whatever...).
> 
> We should move more in the direction of splitting up our "file format"
> docs from implementation, like the git-hook runner.
> 
> I'm somewhat negative on including it at all in git-hook(1). For the
> config section it makes sense, and it's consistent with established doc
> convention.
> 
> But including githooks(5) is around 2/3 of the resulting manpage, I
> think just a link is better.

Maybe so. What I really would like would be if `git help githooks` //
`man githooks` opened `git-hook` manpage, but I had trouble getting it to do
that and still publish to the `githooks` manpage (because the command
doc format doesn't match the guide format). (Or, really, if `git help
githooks` didn't exist so we didn't need to split the docs up. But that
ship has sailed.)

Regardless, I won't complain that much about using a link instead. I'll
make this change for v9.

 - Emily


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'
  2021-03-12  9:21   ` Ævar Arnfjörð Bjarmason
  2021-03-30  0:03     ` Emily Shaffer
@ 2021-03-31 21:47     ` Emily Shaffer
  2021-03-31 22:06       ` Junio C Hamano
  2021-04-02 11:34       ` [PATCH 0/2] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
  1 sibling, 2 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-03-31 21:47 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git

On Fri, Mar 12, 2021 at 10:21:08AM +0100, Ævar Arnfjörð Bjarmason wrote:
> > +	my $target = abs_path($fn);
> > +	return "rejected by sendemail-validate hook"
> > +		if system(("git", "hook", "run", "sendemail-validate", "-a",
> > +				$target));
> 
> I see it's just moving code around, but since we're touching this:
> 
> This conflates the hook exit code with a general failure to invoke it,
> Perl's system().

Ah, at first I thought you meant "hook exit code vs. failure in 'git
hook run'" - but I think you are saying "system() can also exit
unhappily".

I had a look in 'perldoc -f system' like you suggested and saw that in
addition to $? & 127, it seems like I also should check $? == -1
("system() couldn't start the child process") and ($? >> 8) (the rc
from the child hangs out in the top byte). So then it seems like I want
something like so:

  system("git", "hook", "run", "sendemail-validate",
          "-j1", "-a", $target);

  return "git-send-email failed to launch hook process: $!"
          if ($? == -1) || ($? & 127))
  return "git-send-email invoked git-hook run incorrectly"
          if (($? >> 8) == 129);
  return "Rejected by 'sendemail-validate' hook"
          if ($? >> 8);

That seems really verbose, though. I guess ($? >> 8) includes -1 as well (since
0xFF... will meet that conditional), but do we care about the difference between
"system() couldn't run my thing" and "my thing returned upset"?

In this case, "my thing returned upset" - that is, $? >> 8 reflects an
error code from the hook exec - should already have some user output
associated with it, from the hook exec itself, but it's not guaranteed -
neither builtin/hook.c:run() nor hook.c:run_hooks() prints anything to
the user if rc != 0, because they're counting on either the hook execs
or the process that invoked the hook to do the tattling.

I think that means that it's a good idea to differentiate all these
things to the user:

 1. system() broke or your hook got a SIGINT (write a bug report or take
    out the infinite loop/memory violation from the hook you're
    developing)
 2. builtin/hook.c:run() wasn't invoked properly (fix the change you made
    to git-send-email.perl)
 3. your hook rejected your email (working as intended, fix the file you
    want to email)

I'd not expect users to encounter (1) or (2) so it seems fine to me to
include them; if (3) isn't present *and* the hook author did a bad job
communicating what failed, then I think the user experience would be
very confusing - even though they'd see some warning telling them their
patches didn't send, it wouldn't be clear whether it's because of an
issue in git-send-email or an issue with their patch.

Phew. I think I convinced myself that the wordy rc checking is OK. But I
am a perl noob so please correct me if I am wrong :)

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'
  2021-03-31 21:47     ` Emily Shaffer
@ 2021-03-31 22:06       ` Junio C Hamano
  2021-04-01 18:08         ` Emily Shaffer
  2021-04-02 11:34       ` [PATCH 0/2] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
  1 sibling, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-03-31 22:06 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: Ævar Arnfjörð Bjarmason, git

Emily Shaffer <emilyshaffer@google.com> writes:

> On Fri, Mar 12, 2021 at 10:21:08AM +0100, Ævar Arnfjörð Bjarmason wrote:
>> > +	my $target = abs_path($fn);
>> > +	return "rejected by sendemail-validate hook"
>> > +		if system(("git", "hook", "run", "sendemail-validate", "-a",
>> > +				$target));
>> 
>> I see it's just moving code around, but since we're touching this:
>> 
>> This conflates the hook exit code with a general failure to invoke it,
>> Perl's system().
>
> Ah, at first I thought you meant "hook exit code vs. failure in 'git
> hook run'" - but I think you are saying "system() can also exit
> unhappily".
>
> I had a look in 'perldoc -f system' like you suggested and saw that in
> addition to $? & 127, it seems like I also should check $? == -1
> ("system() couldn't start the child process") and ($? >> 8) (the rc
> from the child hangs out in the top byte). So then it seems like I want
> something like so:
>
>   system("git", "hook", "run", "sendemail-validate",
>           "-j1", "-a", $target);
>
>   return "git-send-email failed to launch hook process: $!"
>           if ($? == -1) || ($? & 127))
>   return "git-send-email invoked git-hook run incorrectly"
>           if (($? >> 8) == 129);
>   return "Rejected by 'sendemail-validate' hook"
>           if ($? >> 8);
>

The example in "perldoc -f system" distinguishes these two like so:

        if ($? == -1) {
                print "failed to execute: $!\n";
        }
        elsif ($? & 127) {
                printf "child died with signal %d, %s coredump\n",
                    ($? & 127), ($? & 128) ? 'with' : 'without';
        }
        else {
                printf "child exited with value %d\n", $? >> 8;
        }

> That seems really verbose, though. I guess ($? >> 8) includes -1 as well (since
> 0xFF... will meet that conditional), but do we care about the difference between
> "system() couldn't run my thing" and "my thing returned upset"?

If we classify the failure cases into three using the sample code in
the doc, I think the last one is the only case that we know the
logic in the hook is making a decision for us.  In the first case,
the hook did not even have a chance to decide for us, and in the
second case, the hook died with signal, most likely before it had a
chance to make a decision.  If we want to be conservative (sending
a message out is something you cannot easily undo), then it may make
sense to take the first two failure cases, even though the hook may
have said it is OK to send it out if it ran successfully, as a denial
to be safe, I would think.

Thanks.


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'
  2021-03-31 22:06       ` Junio C Hamano
@ 2021-04-01 18:08         ` Emily Shaffer
  2021-04-01 18:55           ` Junio C Hamano
  0 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-04-01 18:08 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Ævar Arnfjörð Bjarmason, git

On Wed, Mar 31, 2021 at 03:06:12PM -0700, Junio C Hamano wrote:
> 
> Emily Shaffer <emilyshaffer@google.com> writes:
> 
> > On Fri, Mar 12, 2021 at 10:21:08AM +0100, Ævar Arnfjörð Bjarmason wrote:
> >> > +	my $target = abs_path($fn);
> >> > +	return "rejected by sendemail-validate hook"
> >> > +		if system(("git", "hook", "run", "sendemail-validate", "-a",
> >> > +				$target));
> >> 
> >> I see it's just moving code around, but since we're touching this:
> >> 
> >> This conflates the hook exit code with a general failure to invoke it,
> >> Perl's system().
> >
> > Ah, at first I thought you meant "hook exit code vs. failure in 'git
> > hook run'" - but I think you are saying "system() can also exit
> > unhappily".
> >
> > I had a look in 'perldoc -f system' like you suggested and saw that in
> > addition to $? & 127, it seems like I also should check $? == -1
> > ("system() couldn't start the child process") and ($? >> 8) (the rc
> > from the child hangs out in the top byte). So then it seems like I want
> > something like so:
> >
> >   system("git", "hook", "run", "sendemail-validate",
> >           "-j1", "-a", $target);
> >
> >   return "git-send-email failed to launch hook process: $!"
> >           if ($? == -1) || ($? & 127))
> >   return "git-send-email invoked git-hook run incorrectly"
> >           if (($? >> 8) == 129);
> >   return "Rejected by 'sendemail-validate' hook"
> >           if ($? >> 8);
> >
> 
> The example in "perldoc -f system" distinguishes these two like so:
> 
>         if ($? == -1) {
>                 print "failed to execute: $!\n";
>         }
>         elsif ($? & 127) {
>                 printf "child died with signal %d, %s coredump\n",
>                     ($? & 127), ($? & 128) ? 'with' : 'without';
>         }
>         else {
>                 printf "child exited with value %d\n", $? >> 8;
>         }
> 
> > That seems really verbose, though. I guess ($? >> 8) includes -1 as well (since
> > 0xFF... will meet that conditional), but do we care about the difference between
> > "system() couldn't run my thing" and "my thing returned upset"?
> 
> If we classify the failure cases into three using the sample code in
> the doc, I think the last one is the only case that we know the
> logic in the hook is making a decision for us.  In the first case,
> the hook did not even have a chance to decide for us, and in the
> second case, the hook died with signal, most likely before it had a
> chance to make a decision.  If we want to be conservative (sending
> a message out is something you cannot easily undo), then it may make
> sense to take the first two failure cases, even though the hook may
> have said it is OK to send it out if it ran successfully, as a denial
> to be safe, I would think.

Yeah, I tend to agree. In that case I think you are saying: "Please
split the first case into two and differentiate launch failure from
signal, but otherwise continue to return all these cases as errors and
halt the email."

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'
  2021-04-01 18:08         ` Emily Shaffer
@ 2021-04-01 18:55           ` Junio C Hamano
  0 siblings, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-04-01 18:55 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: Ævar Arnfjörð Bjarmason, git

Emily Shaffer <emilyshaffer@google.com> writes:

>> If we classify the failure cases into three using the sample code in
>> the doc, I think the last one is the only case that we know the
>> logic in the hook is making a decision for us.  In the first case,
>> the hook did not even have a chance to decide for us, and in the
>> second case, the hook died with signal, most likely before it had a
>> chance to make a decision.  If we want to be conservative (sending
>> a message out is something you cannot easily undo), then it may make
>> sense to take the first two failure cases, even though the hook may
>> have said it is OK to send it out if it ran successfully, as a denial
>> to be safe, I would think.
>
> Yeah, I tend to agree. In that case I think you are saying: "Please
> split the first case into two and differentiate launch failure from
> signal, but otherwise continue to return all these cases as errors and
> halt the email."

Not exactly.  I do not have a strong opinion either way to split the
first two cases apart or lump them together.  If I were pressed, I
probably would vote for the latter.

The doc's example classfies into three and I think that
classification is logical:

 * Lumping the first two together would make sense with respect to
   deciding what to do when we see a failure. The first two are
   "hook failed to approve or disapprove" case, while the last one
   is "the hook actively disapproved".  The former is not under
   hook's control.

 * Further, treating a failure even from the first "hook failed to
   approve or disapprove" as a signal to stop sending would be more
   conservative.

 * Which leads us to say, with respect to deciding what to do, any
   failure just stops the program from sending.

It is a separate matter how to phrase the diagnoses and hints for
recovery.  It could be that sendmail-validate hook failed to run due
to a simple misconfiguration (e.g. because it lacked the executable
bit).  Giving an error message with strerr would be helpful for the
"hook failed to approve or disapprove" case.


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH 0/2] git-send-email: refactor duplicate $? checks into a function
  2021-03-31 21:47     ` Emily Shaffer
  2021-03-31 22:06       ` Junio C Hamano
@ 2021-04-02 11:34       ` Ævar Arnfjörð Bjarmason
  2021-04-02 11:34         ` [PATCH 1/2] git-send-email: replace "map" in void context with "for" Ævar Arnfjörð Bjarmason
                           ` (2 more replies)
  1 sibling, 3 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-02 11:34 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

A tiny series to help along the config-based hooks series[1]. Its
patch dealing with git-send-email.perl can now trivially be based on
top of this instead of adding another system() wrapper to
git-send-email.perl.

As alluded to in the TODO comment in 2/2 it's probably best to fix
things while we're at it to call validate_patch_error() instead of
just emitting the more brief "rejected by sendemail-validate hook".

But for now this series is just aiming for bug-for-bug compatibility
with the existing code, and to just reduce code duplication.

http://lore.kernel.org/git/20210311021037.3001235-1-emilyshaffer@google.com

Ævar Arnfjörð Bjarmason (2):
  git-send-email: replace "map" in void context with "for"
  git-send-email: refactor duplicate $? checks into a function

 git-send-email.perl | 49 +++++++++++++++++++++++++++++----------------
 1 file changed, 32 insertions(+), 17 deletions(-)

-- 
2.31.1.482.g6691c1be520


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH 1/2] git-send-email: replace "map" in void context with "for"
  2021-04-02 11:34       ` [PATCH 0/2] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
@ 2021-04-02 11:34         ` Ævar Arnfjörð Bjarmason
  2021-04-02 21:31           ` Junio C Hamano
  2021-04-02 11:34         ` [PATCH 2/2] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
  2021-04-04  9:19         ` [PATCH v2 0/4] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
  2 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-02 11:34 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

While using "map" instead of "for" or "map" instead of "grep" and
vice-versa makes for interesting trivia questions when interviewing
Perl programmers, it doesn't make for very readable code. Let's
refactor this loop initially added in 8fd5bb7f44b (git send-email: add
--annotate option, 2008-11-11) to be a for-loop instead.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git-send-email.perl b/git-send-email.perl
index f5bbf1647e3..6893c8e5808 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -217,12 +217,12 @@ sub do_edit {
 		$editor = Git::command_oneline('var', 'GIT_EDITOR');
 	}
 	if (defined($multiedit) && !$multiedit) {
-		map {
+		for (@_) {
 			system('sh', '-c', $editor.' "$@"', $editor, $_);
 			if (($? & 127) || ($? >> 8)) {
 				die(__("the editor exited uncleanly, aborting everything"));
 			}
-		} @_;
+		}
 	} else {
 		system('sh', '-c', $editor.' "$@"', $editor, @_);
 		if (($? & 127) || ($? >> 8)) {
-- 
2.31.1.482.g6691c1be520


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH 2/2] git-send-email: refactor duplicate $? checks into a function
  2021-04-02 11:34       ` [PATCH 0/2] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
  2021-04-02 11:34         ` [PATCH 1/2] git-send-email: replace "map" in void context with "for" Ævar Arnfjörð Bjarmason
@ 2021-04-02 11:34         ` Ævar Arnfjörð Bjarmason
  2021-04-02 21:36           ` Junio C Hamano
  2021-04-04  9:19         ` [PATCH v2 0/4] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
  2 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-02 11:34 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

Refactor the duplicate checking of $? into a function. There's an
outstanding series[1] wanting to add a third use of system() in this
file, let's not copy this boilerplate anymore when that happens.

1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl | 49 +++++++++++++++++++++++++++++----------------
 1 file changed, 32 insertions(+), 17 deletions(-)

diff --git a/git-send-email.perl b/git-send-email.perl
index 6893c8e5808..901c935455d 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -212,22 +212,30 @@ sub format_2822_time {
 my $multiedit;
 my $editor;
 
+sub system_or_msg {
+	my ($args, $msg) = @_;
+	system(@$args);
+	return unless (($? & 127) || ($? >> 8));
+
+	die $msg if $msg;
+	return sprintf(__("failed to run command %s, died with code %d"),
+		       "@$args", $? >> 8);
+}
+
+sub system_or_die {
+	my $msg = system_or_msg(@_);
+	die $msg if $msg;
+}
+
 sub do_edit {
 	if (!defined($editor)) {
 		$editor = Git::command_oneline('var', 'GIT_EDITOR');
 	}
+	my $die_msg = __("the editor exited uncleanly, aborting everything");
 	if (defined($multiedit) && !$multiedit) {
-		for (@_) {
-			system('sh', '-c', $editor.' "$@"', $editor, $_);
-			if (($? & 127) || ($? >> 8)) {
-				die(__("the editor exited uncleanly, aborting everything"));
-			}
-		}
+		system_or_die(['sh', '-c', $editor.' "$@"', $editor, $_], $die_msg) for @_;
 	} else {
-		system('sh', '-c', $editor.' "$@"', $editor, @_);
-		if (($? & 127) || ($? >> 8)) {
-			die(__("the editor exited uncleanly, aborting everything"));
-		}
+		system_or_die(['sh', '-c', $editor.' "$@"', $editor, @_], $die_msg);
 	}
 }
 
@@ -698,9 +706,7 @@ sub is_format_patch_arg {
 if ($validate) {
 	foreach my $f (@files) {
 		unless (-p $f) {
-			my $error = validate_patch($f, $target_xfer_encoding);
-			$error and die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
-						  $f, $error);
+			validate_patch($f, $target_xfer_encoding);
 		}
 	}
 }
@@ -1938,6 +1944,12 @@ sub unique_email_list {
 	return @emails;
 }
 
+sub validate_patch_error {
+	my ($fn, $error) = @_;
+	die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
+		    $fn, $error);
+}
+
 sub validate_patch {
 	my ($fn, $xfer_encoding) = @_;
 
@@ -1952,11 +1964,14 @@ sub validate_patch {
 			chdir($repo->wc_path() or $repo->repo_path())
 				or die("chdir: $!");
 			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			$hook_error = "rejected by sendemail-validate hook"
-				if system($validate_hook, $target);
+			if (my $msg = system_or_msg([$validate_hook, $target])) {
+				# TODO Use $msg and emit exit code on
+				# hook failures?
+				$hook_error = __("rejected by sendemail-validate hook");
+			}
 			chdir($cwd_save) or die("chdir: $!");
 		}
-		return $hook_error if $hook_error;
+		validate_patch_error($fn, $hook_error) if $hook_error;
 	}
 
 	# Any long lines will be automatically fixed if we use a suitable transfer
@@ -1966,7 +1981,7 @@ sub validate_patch {
 			or die sprintf(__("unable to open %s: %s\n"), $fn, $!);
 		while (my $line = <$fh>) {
 			if (length($line) > 998) {
-				return sprintf(__("%s: patch contains a line longer than 998 characters"), $.);
+				validate_patch_error($fn, sprintf(__("%s: patch contains a line longer than 998 characters"), $.));
 			}
 		}
 	}
-- 
2.31.1.482.g6691c1be520


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH 1/2] git-send-email: replace "map" in void context with "for"
  2021-04-02 11:34         ` [PATCH 1/2] git-send-email: replace "map" in void context with "for" Ævar Arnfjörð Bjarmason
@ 2021-04-02 21:31           ` Junio C Hamano
  2021-04-02 21:37             ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-04-02 21:31 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Emily Shaffer

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> While using "map" instead of "for" or "map" instead of "grep" and
> vice-versa makes for interesting trivia questions when interviewing
> Perl programmers, it doesn't make for very readable code. Let's
> refactor this loop initially added in 8fd5bb7f44b (git send-email: add
> --annotate option, 2008-11-11) to be a for-loop instead.

;-)

Will queue.  Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH 2/2] git-send-email: refactor duplicate $? checks into a function
  2021-04-02 11:34         ` [PATCH 2/2] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
@ 2021-04-02 21:36           ` Junio C Hamano
  0 siblings, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-04-02 21:36 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Emily Shaffer

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> Refactor the duplicate checking of $? into a function. There's an
> outstanding series[1] wanting to add a third use of system() in this
> file, let's not copy this boilerplate anymore when that happens.
>
> 1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com
>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  git-send-email.perl | 49 +++++++++++++++++++++++++++++----------------
>  1 file changed, 32 insertions(+), 17 deletions(-)
>
> diff --git a/git-send-email.perl b/git-send-email.perl
> index 6893c8e5808..901c935455d 100755
> --- a/git-send-email.perl
> +++ b/git-send-email.perl
> @@ -212,22 +212,30 @@ sub format_2822_time {
>  my $multiedit;
>  my $editor;
>  
> +sub system_or_msg {
> +	my ($args, $msg) = @_;
> +	system(@$args);
> +	return unless (($? & 127) || ($? >> 8));
> +
> +	die $msg if $msg;
> +	return sprintf(__("failed to run command %s, died with code %d"),
> +		       "@$args", $? >> 8);
> +}

That sounds more like system_and_die_or_msg to me.  More
importantly, the name of the helper makes it clear what difference
this has with ...

> +sub system_or_die {
> +	my $msg = system_or_msg(@_);
> +	die $msg if $msg;
> +}

... this one.  The former does nto die but returns message only when
X?  If that X were in its name, readers who look at the caller of
system_or_msg vs system_or_die would immediately know that why the
callsite is using one and not the other variant.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH 1/2] git-send-email: replace "map" in void context with "for"
  2021-04-02 21:31           ` Junio C Hamano
@ 2021-04-02 21:37             ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-04-02 21:37 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Ævar Arnfjörð Bjarmason, git

On Fri, Apr 02, 2021 at 02:31:38PM -0700, Junio C Hamano wrote:
> 
> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:
> 
> > While using "map" instead of "for" or "map" instead of "grep" and
> > vice-versa makes for interesting trivia questions when interviewing
> > Perl programmers, it doesn't make for very readable code. Let's
> > refactor this loop initially added in 8fd5bb7f44b (git send-email: add
> > --annotate option, 2008-11-11) to be a for-loop instead.
> 
> ;-)
> 
> Will queue.  Thanks.

Oh cool, thanks both :)

^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v2 0/4] refactor duplicate $? checks into a function + improve errors
  2021-04-02 11:34       ` [PATCH 0/2] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
  2021-04-02 11:34         ` [PATCH 1/2] git-send-email: replace "map" in void context with "for" Ævar Arnfjörð Bjarmason
  2021-04-02 11:34         ` [PATCH 2/2] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
@ 2021-04-04  9:19         ` Ævar Arnfjörð Bjarmason
  2021-04-04  9:19           ` [PATCH v2 1/4] git-send-email: replace "map" in void context with "for" Ævar Arnfjörð Bjarmason
                             ` (4 more replies)
  2 siblings, 5 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-04  9:19 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

There was a silly error in v1 noted by Junio. In practice we didn't
hit that "die $msg if $die" in system_or_msg(), but it didn't belong
there.

I see 1/2 of v1 of this series was merged to "next". I'm sending the
full thing anyway, but presumably just 3-4 will be picked up. They
apply cleanly on "next".

I added two patches at the end to improve the error output, the first
two patches in both v1 and v2 just reproduced the current output
bug-for-bug, but I've now made it more sensible.

Ævar Arnfjörð Bjarmason (4):
  git-send-email: replace "map" in void context with "for"
  git-send-email: refactor duplicate $? checks into a function
  git-send-email: test full --validate output
  git-send-email: improve --validate error output

 git-send-email.perl   | 45 +++++++++++++++++++++++++++----------------
 t/t9001-send-email.sh | 35 +++++++++++++++++++++++++--------
 2 files changed, 55 insertions(+), 25 deletions(-)

Range-diff:
1:  bea11504a67 = 1:  e37b861f239 git-send-email: replace "map" in void context with "for"
2:  f4bace5607c ! 2:  f236f083e36 git-send-email: refactor duplicate $? checks into a function
    @@ git-send-email.perl: sub format_2822_time {
     +sub system_or_msg {
     +	my ($args, $msg) = @_;
     +	system(@$args);
    -+	return unless (($? & 127) || ($? >> 8));
    ++	my $signalled = $? & 127;
    ++	my $exit_code = $? >> 8;
    ++	return unless $signalled or $exit_code;
     +
    -+	die $msg if $msg;
     +	return sprintf(__("failed to run command %s, died with code %d"),
    -+		       "@$args", $? >> 8);
    ++		       "@$args", $exit_code);
     +}
     +
     +sub system_or_die {
    @@ git-send-email.perl: sub validate_patch {
     -			$hook_error = "rejected by sendemail-validate hook"
     -				if system($validate_hook, $target);
     +			if (my $msg = system_or_msg([$validate_hook, $target])) {
    -+				# TODO Use $msg and emit exit code on
    -+				# hook failures?
     +				$hook_error = __("rejected by sendemail-validate hook");
     +			}
      			chdir($cwd_save) or die("chdir: $!");
-:  ----------- > 3:  15b59c226d4 git-send-email: test full --validate output
-:  ----------- > 4:  a1edceb4913 git-send-email: improve --validate error output
-- 
2.31.1.482.g6691c1be520


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v2 1/4] git-send-email: replace "map" in void context with "for"
  2021-04-04  9:19         ` [PATCH v2 0/4] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
@ 2021-04-04  9:19           ` Ævar Arnfjörð Bjarmason
  2021-04-04  9:19           ` [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
                             ` (3 subsequent siblings)
  4 siblings, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-04  9:19 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

While using "map" instead of "for" or "map" instead of "grep" and
vice-versa makes for interesting trivia questions when interviewing
Perl programmers, it doesn't make for very readable code. Let's
refactor this loop initially added in 8fd5bb7f44b (git send-email: add
--annotate option, 2008-11-11) to be a for-loop instead.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git-send-email.perl b/git-send-email.perl
index f5bbf1647e3..6893c8e5808 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -217,12 +217,12 @@ sub do_edit {
 		$editor = Git::command_oneline('var', 'GIT_EDITOR');
 	}
 	if (defined($multiedit) && !$multiedit) {
-		map {
+		for (@_) {
 			system('sh', '-c', $editor.' "$@"', $editor, $_);
 			if (($? & 127) || ($? >> 8)) {
 				die(__("the editor exited uncleanly, aborting everything"));
 			}
-		} @_;
+		}
 	} else {
 		system('sh', '-c', $editor.' "$@"', $editor, @_);
 		if (($? & 127) || ($? >> 8)) {
-- 
2.31.1.482.g6691c1be520


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function
  2021-04-04  9:19         ` [PATCH v2 0/4] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
  2021-04-04  9:19           ` [PATCH v2 1/4] git-send-email: replace "map" in void context with "for" Ævar Arnfjörð Bjarmason
@ 2021-04-04  9:19           ` Ævar Arnfjörð Bjarmason
  2021-04-05 19:11             ` Junio C Hamano
  2021-04-05 23:47             ` Junio C Hamano
  2021-04-04  9:19           ` [PATCH v2 3/4] git-send-email: test full --validate output Ævar Arnfjörð Bjarmason
                             ` (2 subsequent siblings)
  4 siblings, 2 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-04  9:19 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

Refactor the duplicate checking of $? into a function. There's an
outstanding series[1] wanting to add a third use of system() in this
file, let's not copy this boilerplate anymore when that happens.

1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl | 48 +++++++++++++++++++++++++++++----------------
 1 file changed, 31 insertions(+), 17 deletions(-)

diff --git a/git-send-email.perl b/git-send-email.perl
index 6893c8e5808..9724a9cae27 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -212,22 +212,31 @@ sub format_2822_time {
 my $multiedit;
 my $editor;
 
+sub system_or_msg {
+	my ($args, $msg) = @_;
+	system(@$args);
+	my $signalled = $? & 127;
+	my $exit_code = $? >> 8;
+	return unless $signalled or $exit_code;
+
+	return sprintf(__("failed to run command %s, died with code %d"),
+		       "@$args", $exit_code);
+}
+
+sub system_or_die {
+	my $msg = system_or_msg(@_);
+	die $msg if $msg;
+}
+
 sub do_edit {
 	if (!defined($editor)) {
 		$editor = Git::command_oneline('var', 'GIT_EDITOR');
 	}
+	my $die_msg = __("the editor exited uncleanly, aborting everything");
 	if (defined($multiedit) && !$multiedit) {
-		for (@_) {
-			system('sh', '-c', $editor.' "$@"', $editor, $_);
-			if (($? & 127) || ($? >> 8)) {
-				die(__("the editor exited uncleanly, aborting everything"));
-			}
-		}
+		system_or_die(['sh', '-c', $editor.' "$@"', $editor, $_], $die_msg) for @_;
 	} else {
-		system('sh', '-c', $editor.' "$@"', $editor, @_);
-		if (($? & 127) || ($? >> 8)) {
-			die(__("the editor exited uncleanly, aborting everything"));
-		}
+		system_or_die(['sh', '-c', $editor.' "$@"', $editor, @_], $die_msg);
 	}
 }
 
@@ -698,9 +707,7 @@ sub is_format_patch_arg {
 if ($validate) {
 	foreach my $f (@files) {
 		unless (-p $f) {
-			my $error = validate_patch($f, $target_xfer_encoding);
-			$error and die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
-						  $f, $error);
+			validate_patch($f, $target_xfer_encoding);
 		}
 	}
 }
@@ -1938,6 +1945,12 @@ sub unique_email_list {
 	return @emails;
 }
 
+sub validate_patch_error {
+	my ($fn, $error) = @_;
+	die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
+		    $fn, $error);
+}
+
 sub validate_patch {
 	my ($fn, $xfer_encoding) = @_;
 
@@ -1952,11 +1965,12 @@ sub validate_patch {
 			chdir($repo->wc_path() or $repo->repo_path())
 				or die("chdir: $!");
 			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			$hook_error = "rejected by sendemail-validate hook"
-				if system($validate_hook, $target);
+			if (my $msg = system_or_msg([$validate_hook, $target])) {
+				$hook_error = __("rejected by sendemail-validate hook");
+			}
 			chdir($cwd_save) or die("chdir: $!");
 		}
-		return $hook_error if $hook_error;
+		validate_patch_error($fn, $hook_error) if $hook_error;
 	}
 
 	# Any long lines will be automatically fixed if we use a suitable transfer
@@ -1966,7 +1980,7 @@ sub validate_patch {
 			or die sprintf(__("unable to open %s: %s\n"), $fn, $!);
 		while (my $line = <$fh>) {
 			if (length($line) > 998) {
-				return sprintf(__("%s: patch contains a line longer than 998 characters"), $.);
+				validate_patch_error($fn, sprintf(__("%s: patch contains a line longer than 998 characters"), $.));
 			}
 		}
 	}
-- 
2.31.1.482.g6691c1be520


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v2 3/4] git-send-email: test full --validate output
  2021-04-04  9:19         ` [PATCH v2 0/4] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
  2021-04-04  9:19           ` [PATCH v2 1/4] git-send-email: replace "map" in void context with "for" Ævar Arnfjörð Bjarmason
  2021-04-04  9:19           ` [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
@ 2021-04-04  9:19           ` Ævar Arnfjörð Bjarmason
  2021-04-04  9:19           ` [PATCH v2 4/4] git-send-email: improve --validate error output Ævar Arnfjörð Bjarmason
  2021-04-06 14:00           ` [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
  4 siblings, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-04  9:19 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

Change the tests that grep substrings out of the output to use a full
test_cmp, in preparation for improving the output.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t9001-send-email.sh | 24 ++++++++++++++++++------
 1 file changed, 18 insertions(+), 6 deletions(-)

diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index 1a1caf8f2ed..74225e3dc7a 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -422,8 +422,12 @@ test_expect_success $PREREQ 'reject long lines' '
 		--smtp-server="$(pwd)/fake.sendmail" \
 		--transfer-encoding=8bit \
 		$patches longline.patch \
-		2>errors &&
-	grep longline.patch errors
+		2>actual &&
+	cat >expect <<-\EOF &&
+	fatal: longline.patch: 35: patch contains a line longer than 998 characters
+	warning: no patches were sent
+	EOF
+	test_cmp expect actual
 '
 
 test_expect_success $PREREQ 'no patch was sent' '
@@ -527,9 +531,13 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		--validate \
-		longline.patch 2>err &&
+		longline.patch 2>actual &&
 	test_path_is_file my-hooks.ran &&
-	grep "rejected by sendemail-validate" err
+	cat >expect <<-\EOF &&
+	fatal: longline.patch: rejected by sendemail-validate hook
+	warning: no patches were sent
+	EOF
+	test_cmp expect actual
 '
 
 test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
@@ -540,9 +548,13 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		--validate \
-		longline.patch 2>err &&
+		longline.patch 2>actual &&
 	test_path_is_file my-hooks.ran &&
-	grep "rejected by sendemail-validate" err
+	cat >expect <<-\EOF &&
+	fatal: longline.patch: rejected by sendemail-validate hook
+	warning: no patches were sent
+	EOF
+	test_cmp expect actual
 '
 
 for enc in 7bit 8bit quoted-printable base64
-- 
2.31.1.482.g6691c1be520


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v2 4/4] git-send-email: improve --validate error output
  2021-04-04  9:19         ` [PATCH v2 0/4] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
                             ` (2 preceding siblings ...)
  2021-04-04  9:19           ` [PATCH v2 3/4] git-send-email: test full --validate output Ævar Arnfjörð Bjarmason
@ 2021-04-04  9:19           ` Ævar Arnfjörð Bjarmason
  2021-04-05 19:14             ` Junio C Hamano
  2021-04-06 14:00           ` [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
  4 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-04  9:19 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

Improve the output we emit on --validate error to:

 * Say "FILE:LINE" instead of "FILE: LINE".

 * Don't say "patch contains a" after just mentioning the filename,
   just leave it at "FILE:LINE: is longer than[...]. The "contains a"
   sounded like we were talking about the file in general, when we're
   actually checking it line-by-line.

 * Don't just say "rejected by sendemail-validate hook", but combine
   that with the system_or_msg() output to say what exit code the hook
   died with.

I had an aborted attempt to make the line length checker note all
lines that were longer than the limit. I didn't think that was worth
the effort, but I've left in the testing change to check that we die
as soon as we spot the first long line.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl   | 23 ++++++++++-------------
 t/t9001-send-email.sh | 17 ++++++++++++-----
 2 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/git-send-email.perl b/git-send-email.perl
index 9724a9cae27..175da07d946 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -219,8 +219,8 @@ sub system_or_msg {
 	my $exit_code = $? >> 8;
 	return unless $signalled or $exit_code;
 
-	return sprintf(__("failed to run command %s, died with code %d"),
-		       "@$args", $exit_code);
+	return sprintf(__("fatal: command '%s' died with exit code %d"),
+		       $args->[0], $exit_code);
 }
 
 sub system_or_die {
@@ -1945,12 +1945,6 @@ sub unique_email_list {
 	return @emails;
 }
 
-sub validate_patch_error {
-	my ($fn, $error) = @_;
-	die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
-		    $fn, $error);
-}
-
 sub validate_patch {
 	my ($fn, $xfer_encoding) = @_;
 
@@ -1965,12 +1959,14 @@ sub validate_patch {
 			chdir($repo->wc_path() or $repo->repo_path())
 				or die("chdir: $!");
 			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			if (my $msg = system_or_msg([$validate_hook, $target])) {
-				$hook_error = __("rejected by sendemail-validate hook");
-			}
+			$hook_error = system_or_msg([$validate_hook, $target]);
 			chdir($cwd_save) or die("chdir: $!");
 		}
-		validate_patch_error($fn, $hook_error) if $hook_error;
+		if ($hook_error) {
+			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
+				       "%s\n" .
+				       "warning: no patches were sent\n"), $fn, $hook_error);
+		}
 	}
 
 	# Any long lines will be automatically fixed if we use a suitable transfer
@@ -1980,7 +1976,8 @@ sub validate_patch {
 			or die sprintf(__("unable to open %s: %s\n"), $fn, $!);
 		while (my $line = <$fh>) {
 			if (length($line) > 998) {
-				validate_patch_error($fn, sprintf(__("%s: patch contains a line longer than 998 characters"), $.));
+				die sprintf(__("fatal: %s:%d is longer than 998 characters\n" .
+					       "warning: no patches were sent\n"), $fn, $.);
 			}
 		}
 	}
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index 74225e3dc7a..65b30353719 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -415,7 +415,11 @@ test_expect_success $PREREQ 'reject long lines' '
 	z512=$z64$z64$z64$z64$z64$z64$z64$z64 &&
 	clean_fake_sendmail &&
 	cp $patches longline.patch &&
-	echo $z512$z512 >>longline.patch &&
+	cat >>longline.patch <<-EOF &&
+	$z512$z512
+	not a long line
+	$z512$z512
+	EOF
 	test_must_fail git send-email \
 		--from="Example <nobody@example.com>" \
 		--to=nobody@example.com \
@@ -424,7 +428,7 @@ test_expect_success $PREREQ 'reject long lines' '
 		$patches longline.patch \
 		2>actual &&
 	cat >expect <<-\EOF &&
-	fatal: longline.patch: 35: patch contains a line longer than 998 characters
+	fatal: longline.patch:35 is longer than 998 characters
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
@@ -533,15 +537,17 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
 		--validate \
 		longline.patch 2>actual &&
 	test_path_is_file my-hooks.ran &&
-	cat >expect <<-\EOF &&
+	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
+	fatal: command '"'"'$(pwd)/my-hooks/sendemail-validate'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
 '
 
 test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
-	test_config core.hooksPath "$(pwd)/my-hooks" &&
+	hooks_path="$(pwd)/my-hooks" &&
+	test_config core.hooksPath "$hooks_path" &&
 	test_when_finished "rm my-hooks.ran" &&
 	test_must_fail git send-email \
 		--from="Example <nobody@example.com>" \
@@ -550,8 +556,9 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
 		--validate \
 		longline.patch 2>actual &&
 	test_path_is_file my-hooks.ran &&
-	cat >expect <<-\EOF &&
+	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
+	fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
-- 
2.31.1.482.g6691c1be520


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function
  2021-04-04  9:19           ` [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
@ 2021-04-05 19:11             ` Junio C Hamano
  2021-04-05 23:47             ` Junio C Hamano
  1 sibling, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-04-05 19:11 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Emily Shaffer

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> @@ -1938,6 +1945,12 @@ sub unique_email_list {
>  	return @emails;
>  }
>  
> +sub validate_patch_error {
> +	my ($fn, $error) = @_;
> +	die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
> +		    $fn, $error);
> +}
> +
>  sub validate_patch {
>  	my ($fn, $xfer_encoding) = @_;

I like the overall direction of this series, but this change will
soon be reverted back to have the die/sprintf in the two callsite
in 4/4 anyway, so this hunk looks more like "I thought this would
be a good way, but in the end I had to change my mind".

> @@ -1952,11 +1965,12 @@ sub validate_patch {
>  			chdir($repo->wc_path() or $repo->repo_path())
>  				or die("chdir: $!");
>  			local $ENV{"GIT_DIR"} = $repo->repo_path();
> -			$hook_error = "rejected by sendemail-validate hook"
> -				if system($validate_hook, $target);
> +			if (my $msg = system_or_msg([$validate_hook, $target])) {
> +				$hook_error = __("rejected by sendemail-validate hook");
> +			}
>  			chdir($cwd_save) or die("chdir: $!");
>  		}
> -		return $hook_error if $hook_error;
> +		validate_patch_error($fn, $hook_error) if $hook_error;
>  	}
>  
>  	# Any long lines will be automatically fixed if we use a suitable transfer
> @@ -1966,7 +1980,7 @@ sub validate_patch {
>  			or die sprintf(__("unable to open %s: %s\n"), $fn, $!);
>  		while (my $line = <$fh>) {
>  			if (length($line) > 998) {
> -				return sprintf(__("%s: patch contains a line longer than 998 characters"), $.);
> +				validate_patch_error($fn, sprintf(__("%s: patch contains a line longer than 998 characters"), $.));
>  			}
>  		}
>  	}

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v2 4/4] git-send-email: improve --validate error output
  2021-04-04  9:19           ` [PATCH v2 4/4] git-send-email: improve --validate error output Ævar Arnfjörð Bjarmason
@ 2021-04-05 19:14             ` Junio C Hamano
  0 siblings, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-04-05 19:14 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Emily Shaffer

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> Improve the output we emit on --validate error to:
>
>  * Say "FILE:LINE" instead of "FILE: LINE".

OK, that is an improvement because it matches "grep -n" hits,
compiler error messages, etc., to help the editors to jump to these
lines.

>  * Don't say "patch contains a" after just mentioning the filename,
>    just leave it at "FILE:LINE: is longer than[...]. The "contains a"
>    sounded like we were talking about the file in general, when we're
>    actually checking it line-by-line.

This, too.

>  * Don't just say "rejected by sendemail-validate hook", but combine
>    that with the system_or_msg() output to say what exit code the hook
>    died with.
>
> I had an aborted attempt to make the line length checker note all
> lines that were longer than the limit. I didn't think that was worth
> the effort, but I've left in the testing change to check that we die
> as soon as we spot the first long line.
>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  git-send-email.perl   | 23 ++++++++++-------------
>  t/t9001-send-email.sh | 17 ++++++++++++-----
>  2 files changed, 22 insertions(+), 18 deletions(-)

Will queue.  I like the end result, but left a comment about
flipping-and-flopping between 2/4 and this step on an extra
"validate_patch_error" helper sub.

Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function
  2021-04-04  9:19           ` [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
  2021-04-05 19:11             ` Junio C Hamano
@ 2021-04-05 23:47             ` Junio C Hamano
  2021-04-08 22:43               ` Junio C Hamano
  1 sibling, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-04-05 23:47 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, Emily Shaffer; +Cc: git

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> Refactor the duplicate checking of $? into a function. There's an
> outstanding series[1] wanting to add a third use of system() in this
> file, let's not copy this boilerplate anymore when that happens.
>
> 1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com
>
> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> ---
>  git-send-email.perl | 48 +++++++++++++++++++++++++++++----------------
>  1 file changed, 31 insertions(+), 17 deletions(-)

>  sub validate_patch {
>  	my ($fn, $xfer_encoding) = @_;
>  
> @@ -1952,11 +1965,12 @@ sub validate_patch {
>  			chdir($repo->wc_path() or $repo->repo_path())
>  				or die("chdir: $!");
>  			local $ENV{"GIT_DIR"} = $repo->repo_path();
> -			$hook_error = "rejected by sendemail-validate hook"
> -				if system($validate_hook, $target);
> +			if (my $msg = system_or_msg([$validate_hook, $target])) {
> +				$hook_error = __("rejected by sendemail-validate hook");
> +			}
>  			chdir($cwd_save) or die("chdir: $!");
>  		}
> -		return $hook_error if $hook_error;
> +		validate_patch_error($fn, $hook_error) if $hook_error;
>  	}

One big thing that is different between this version and the one in
Emily's "config hook" topic is that this is still limited to the
case where $repo exists.  In the new world order, it will not matter
in what directory the command runs, as long as "git hook" finds the
hook, and details of the invocation is hidden behind the command.

I presume that Emily's series is expected to be updated soonish?
Please figure out who to go first and other details to work well
together between you two.

I'd drop the "config hook" topic for now, and I think the endpoint
of these four-patch series (the first "map vs for" can move more or
less independently) are more-or-less in a good shape (even though as
I said already, I think 2/4 and 4/4 want to be updated not to
introduce the intermediate "validate_patch_error()" sub in 2/4 only
to get rid of it in 4/4) and would require only one update.

Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors
  2021-04-04  9:19         ` [PATCH v2 0/4] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
                             ` (3 preceding siblings ...)
  2021-04-04  9:19           ` [PATCH v2 4/4] git-send-email: improve --validate error output Ævar Arnfjörð Bjarmason
@ 2021-04-06 14:00           ` Ævar Arnfjörð Bjarmason
  2021-04-06 14:00             ` [PATCH v3 1/3] git-send-email: test full --validate output Ævar Arnfjörð Bjarmason
                               ` (3 more replies)
  4 siblings, 4 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-06 14:00 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

Version 3 yields the TREESAME end result as v2[1], but re-arranges the
way we get there to make the progression more understandable, along
with a minor commit message update.

I also peeled off the previous 1st patch, as Junio's picked it up
separately and marged it into "next" already.

1. http://lore.kernel.org/git/cover-0.5-00000000000-20210404T091649Z-avarab@gmail.com

Ævar Arnfjörð Bjarmason (3):
  git-send-email: test full --validate output
  git-send-email: refactor duplicate $? checks into a function
  git-send-email: improve --validate error output

 git-send-email.perl   | 45 +++++++++++++++++++++++++++----------------
 t/t9001-send-email.sh | 35 +++++++++++++++++++++++++--------
 2 files changed, 55 insertions(+), 25 deletions(-)

Range-diff:
2:  15b59c226d4 = 1:  6e1009e5bed git-send-email: test full --validate output
1:  f236f083e36 ! 2:  4ee582d8301 git-send-email: refactor duplicate $? checks into a function
    @@ git-send-email.perl: sub is_format_patch_arg {
      		}
      	}
      }
    -@@ git-send-email.perl: sub unique_email_list {
    - 	return @emails;
    - }
    - 
    -+sub validate_patch_error {
    -+	my ($fn, $error) = @_;
    -+	die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
    -+		    $fn, $error);
    -+}
    -+
    - sub validate_patch {
    - 	my ($fn, $xfer_encoding) = @_;
    - 
     @@ git-send-email.perl: sub validate_patch {
      			chdir($repo->wc_path() or $repo->repo_path())
      				or die("chdir: $!");
      			local $ENV{"GIT_DIR"} = $repo->repo_path();
     -			$hook_error = "rejected by sendemail-validate hook"
     -				if system($validate_hook, $target);
    -+			if (my $msg = system_or_msg([$validate_hook, $target])) {
    -+				$hook_error = __("rejected by sendemail-validate hook");
    -+			}
    ++			$hook_error = system_or_msg([$validate_hook, $target]);
      			chdir($cwd_save) or die("chdir: $!");
      		}
     -		return $hook_error if $hook_error;
    -+		validate_patch_error($fn, $hook_error) if $hook_error;
    ++		if ($hook_error) {
    ++			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
    ++				       "warning: no patches were sent\n"), $fn);
    ++		}
      	}
      
      	# Any long lines will be automatically fixed if we use a suitable transfer
    @@ git-send-email.perl: sub validate_patch {
      		while (my $line = <$fh>) {
      			if (length($line) > 998) {
     -				return sprintf(__("%s: patch contains a line longer than 998 characters"), $.);
    -+				validate_patch_error($fn, sprintf(__("%s: patch contains a line longer than 998 characters"), $.));
    ++				die sprintf(__("fatal: %s: %d: patch contains a line longer than 998 characters\n" .
    ++					       "warning: no patches were sent\n"),
    ++					    $fn, $.);
      			}
      		}
      	}
3:  a1edceb4913 ! 3:  8a67afd3404 git-send-email: improve --validate error output
    @@ Commit message
     
         Improve the output we emit on --validate error to:
     
    -     * Say "FILE:LINE" instead of "FILE: LINE".
    +     * Say "FILE:LINE" instead of "FILE: LINE", to match "grep -n",
    +       compiler error messages etc.
     
          * Don't say "patch contains a" after just mentioning the filename,
            just leave it at "FILE:LINE: is longer than[...]. The "contains a"
    @@ git-send-email.perl: sub system_or_msg {
      }
      
      sub system_or_die {
    -@@ git-send-email.perl: sub unique_email_list {
    - 	return @emails;
    - }
    - 
    --sub validate_patch_error {
    --	my ($fn, $error) = @_;
    --	die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
    --		    $fn, $error);
    --}
    --
    - sub validate_patch {
    - 	my ($fn, $xfer_encoding) = @_;
    - 
     @@ git-send-email.perl: sub validate_patch {
    - 			chdir($repo->wc_path() or $repo->repo_path())
    - 				or die("chdir: $!");
    - 			local $ENV{"GIT_DIR"} = $repo->repo_path();
    --			if (my $msg = system_or_msg([$validate_hook, $target])) {
    --				$hook_error = __("rejected by sendemail-validate hook");
    --			}
    -+			$hook_error = system_or_msg([$validate_hook, $target]);
    - 			chdir($cwd_save) or die("chdir: $!");
      		}
    --		validate_patch_error($fn, $hook_error) if $hook_error;
    -+		if ($hook_error) {
    -+			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
    + 		if ($hook_error) {
    + 			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
    +-				       "warning: no patches were sent\n"), $fn);
     +				       "%s\n" .
     +				       "warning: no patches were sent\n"), $fn, $hook_error);
    -+		}
    + 		}
      	}
      
    - 	# Any long lines will be automatically fixed if we use a suitable transfer
     @@ git-send-email.perl: sub validate_patch {
      			or die sprintf(__("unable to open %s: %s\n"), $fn, $!);
      		while (my $line = <$fh>) {
      			if (length($line) > 998) {
    --				validate_patch_error($fn, sprintf(__("%s: patch contains a line longer than 998 characters"), $.));
    +-				die sprintf(__("fatal: %s: %d: patch contains a line longer than 998 characters\n" .
    +-					       "warning: no patches were sent\n"),
    +-					    $fn, $.);
     +				die sprintf(__("fatal: %s:%d is longer than 998 characters\n" .
     +					       "warning: no patches were sent\n"), $fn, $.);
      			}
-- 
2.31.1.527.g9b8f7de2547


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v3 1/3] git-send-email: test full --validate output
  2021-04-06 14:00           ` [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
@ 2021-04-06 14:00             ` Ævar Arnfjörð Bjarmason
  2021-04-06 14:00             ` [PATCH v3 2/3] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
                               ` (2 subsequent siblings)
  3 siblings, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-06 14:00 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

Change the tests that grep substrings out of the output to use a full
test_cmp, in preparation for improving the output.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 t/t9001-send-email.sh | 24 ++++++++++++++++++------
 1 file changed, 18 insertions(+), 6 deletions(-)

diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index 1a1caf8f2ed..74225e3dc7a 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -422,8 +422,12 @@ test_expect_success $PREREQ 'reject long lines' '
 		--smtp-server="$(pwd)/fake.sendmail" \
 		--transfer-encoding=8bit \
 		$patches longline.patch \
-		2>errors &&
-	grep longline.patch errors
+		2>actual &&
+	cat >expect <<-\EOF &&
+	fatal: longline.patch: 35: patch contains a line longer than 998 characters
+	warning: no patches were sent
+	EOF
+	test_cmp expect actual
 '
 
 test_expect_success $PREREQ 'no patch was sent' '
@@ -527,9 +531,13 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		--validate \
-		longline.patch 2>err &&
+		longline.patch 2>actual &&
 	test_path_is_file my-hooks.ran &&
-	grep "rejected by sendemail-validate" err
+	cat >expect <<-\EOF &&
+	fatal: longline.patch: rejected by sendemail-validate hook
+	warning: no patches were sent
+	EOF
+	test_cmp expect actual
 '
 
 test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
@@ -540,9 +548,13 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
 		--to=nobody@example.com \
 		--smtp-server="$(pwd)/fake.sendmail" \
 		--validate \
-		longline.patch 2>err &&
+		longline.patch 2>actual &&
 	test_path_is_file my-hooks.ran &&
-	grep "rejected by sendemail-validate" err
+	cat >expect <<-\EOF &&
+	fatal: longline.patch: rejected by sendemail-validate hook
+	warning: no patches were sent
+	EOF
+	test_cmp expect actual
 '
 
 for enc in 7bit 8bit quoted-printable base64
-- 
2.31.1.527.g9b8f7de2547


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v3 2/3] git-send-email: refactor duplicate $? checks into a function
  2021-04-06 14:00           ` [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
  2021-04-06 14:00             ` [PATCH v3 1/3] git-send-email: test full --validate output Ævar Arnfjörð Bjarmason
@ 2021-04-06 14:00             ` Ævar Arnfjörð Bjarmason
  2021-04-06 14:00             ` [PATCH v3 3/3] git-send-email: improve --validate error output Ævar Arnfjörð Bjarmason
  2021-04-06 20:33             ` [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors Junio C Hamano
  3 siblings, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-06 14:00 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

Refactor the duplicate checking of $? into a function. There's an
outstanding series[1] wanting to add a third use of system() in this
file, let's not copy this boilerplate anymore when that happens.

1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl | 45 ++++++++++++++++++++++++++++-----------------
 1 file changed, 28 insertions(+), 17 deletions(-)

diff --git a/git-send-email.perl b/git-send-email.perl
index 6893c8e5808..2dd48621759 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -212,22 +212,31 @@ sub format_2822_time {
 my $multiedit;
 my $editor;
 
+sub system_or_msg {
+	my ($args, $msg) = @_;
+	system(@$args);
+	my $signalled = $? & 127;
+	my $exit_code = $? >> 8;
+	return unless $signalled or $exit_code;
+
+	return sprintf(__("failed to run command %s, died with code %d"),
+		       "@$args", $exit_code);
+}
+
+sub system_or_die {
+	my $msg = system_or_msg(@_);
+	die $msg if $msg;
+}
+
 sub do_edit {
 	if (!defined($editor)) {
 		$editor = Git::command_oneline('var', 'GIT_EDITOR');
 	}
+	my $die_msg = __("the editor exited uncleanly, aborting everything");
 	if (defined($multiedit) && !$multiedit) {
-		for (@_) {
-			system('sh', '-c', $editor.' "$@"', $editor, $_);
-			if (($? & 127) || ($? >> 8)) {
-				die(__("the editor exited uncleanly, aborting everything"));
-			}
-		}
+		system_or_die(['sh', '-c', $editor.' "$@"', $editor, $_], $die_msg) for @_;
 	} else {
-		system('sh', '-c', $editor.' "$@"', $editor, @_);
-		if (($? & 127) || ($? >> 8)) {
-			die(__("the editor exited uncleanly, aborting everything"));
-		}
+		system_or_die(['sh', '-c', $editor.' "$@"', $editor, @_], $die_msg);
 	}
 }
 
@@ -698,9 +707,7 @@ sub is_format_patch_arg {
 if ($validate) {
 	foreach my $f (@files) {
 		unless (-p $f) {
-			my $error = validate_patch($f, $target_xfer_encoding);
-			$error and die sprintf(__("fatal: %s: %s\nwarning: no patches were sent\n"),
-						  $f, $error);
+			validate_patch($f, $target_xfer_encoding);
 		}
 	}
 }
@@ -1952,11 +1959,13 @@ sub validate_patch {
 			chdir($repo->wc_path() or $repo->repo_path())
 				or die("chdir: $!");
 			local $ENV{"GIT_DIR"} = $repo->repo_path();
-			$hook_error = "rejected by sendemail-validate hook"
-				if system($validate_hook, $target);
+			$hook_error = system_or_msg([$validate_hook, $target]);
 			chdir($cwd_save) or die("chdir: $!");
 		}
-		return $hook_error if $hook_error;
+		if ($hook_error) {
+			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
+				       "warning: no patches were sent\n"), $fn);
+		}
 	}
 
 	# Any long lines will be automatically fixed if we use a suitable transfer
@@ -1966,7 +1975,9 @@ sub validate_patch {
 			or die sprintf(__("unable to open %s: %s\n"), $fn, $!);
 		while (my $line = <$fh>) {
 			if (length($line) > 998) {
-				return sprintf(__("%s: patch contains a line longer than 998 characters"), $.);
+				die sprintf(__("fatal: %s: %d: patch contains a line longer than 998 characters\n" .
+					       "warning: no patches were sent\n"),
+					    $fn, $.);
 			}
 		}
 	}
-- 
2.31.1.527.g9b8f7de2547


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v3 3/3] git-send-email: improve --validate error output
  2021-04-06 14:00           ` [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
  2021-04-06 14:00             ` [PATCH v3 1/3] git-send-email: test full --validate output Ævar Arnfjörð Bjarmason
  2021-04-06 14:00             ` [PATCH v3 2/3] git-send-email: refactor duplicate $? checks into a function Ævar Arnfjörð Bjarmason
@ 2021-04-06 14:00             ` Ævar Arnfjörð Bjarmason
  2021-04-06 20:33             ` [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors Junio C Hamano
  3 siblings, 0 replies; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-06 14:00 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Emily Shaffer, Ævar Arnfjörð Bjarmason

Improve the output we emit on --validate error to:

 * Say "FILE:LINE" instead of "FILE: LINE", to match "grep -n",
   compiler error messages etc.

 * Don't say "patch contains a" after just mentioning the filename,
   just leave it at "FILE:LINE: is longer than[...]. The "contains a"
   sounded like we were talking about the file in general, when we're
   actually checking it line-by-line.

 * Don't just say "rejected by sendemail-validate hook", but combine
   that with the system_or_msg() output to say what exit code the hook
   died with.

I had an aborted attempt to make the line length checker note all
lines that were longer than the limit. I didn't think that was worth
the effort, but I've left in the testing change to check that we die
as soon as we spot the first long line.

Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
---
 git-send-email.perl   | 12 ++++++------
 t/t9001-send-email.sh | 17 ++++++++++++-----
 2 files changed, 18 insertions(+), 11 deletions(-)

diff --git a/git-send-email.perl b/git-send-email.perl
index 2dd48621759..175da07d946 100755
--- a/git-send-email.perl
+++ b/git-send-email.perl
@@ -219,8 +219,8 @@ sub system_or_msg {
 	my $exit_code = $? >> 8;
 	return unless $signalled or $exit_code;
 
-	return sprintf(__("failed to run command %s, died with code %d"),
-		       "@$args", $exit_code);
+	return sprintf(__("fatal: command '%s' died with exit code %d"),
+		       $args->[0], $exit_code);
 }
 
 sub system_or_die {
@@ -1964,7 +1964,8 @@ sub validate_patch {
 		}
 		if ($hook_error) {
 			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
-				       "warning: no patches were sent\n"), $fn);
+				       "%s\n" .
+				       "warning: no patches were sent\n"), $fn, $hook_error);
 		}
 	}
 
@@ -1975,9 +1976,8 @@ sub validate_patch {
 			or die sprintf(__("unable to open %s: %s\n"), $fn, $!);
 		while (my $line = <$fh>) {
 			if (length($line) > 998) {
-				die sprintf(__("fatal: %s: %d: patch contains a line longer than 998 characters\n" .
-					       "warning: no patches were sent\n"),
-					    $fn, $.);
+				die sprintf(__("fatal: %s:%d is longer than 998 characters\n" .
+					       "warning: no patches were sent\n"), $fn, $.);
 			}
 		}
 	}
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index 74225e3dc7a..65b30353719 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -415,7 +415,11 @@ test_expect_success $PREREQ 'reject long lines' '
 	z512=$z64$z64$z64$z64$z64$z64$z64$z64 &&
 	clean_fake_sendmail &&
 	cp $patches longline.patch &&
-	echo $z512$z512 >>longline.patch &&
+	cat >>longline.patch <<-EOF &&
+	$z512$z512
+	not a long line
+	$z512$z512
+	EOF
 	test_must_fail git send-email \
 		--from="Example <nobody@example.com>" \
 		--to=nobody@example.com \
@@ -424,7 +428,7 @@ test_expect_success $PREREQ 'reject long lines' '
 		$patches longline.patch \
 		2>actual &&
 	cat >expect <<-\EOF &&
-	fatal: longline.patch: 35: patch contains a line longer than 998 characters
+	fatal: longline.patch:35 is longer than 998 characters
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
@@ -533,15 +537,17 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
 		--validate \
 		longline.patch 2>actual &&
 	test_path_is_file my-hooks.ran &&
-	cat >expect <<-\EOF &&
+	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
+	fatal: command '"'"'$(pwd)/my-hooks/sendemail-validate'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
 '
 
 test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
-	test_config core.hooksPath "$(pwd)/my-hooks" &&
+	hooks_path="$(pwd)/my-hooks" &&
+	test_config core.hooksPath "$hooks_path" &&
 	test_when_finished "rm my-hooks.ran" &&
 	test_must_fail git send-email \
 		--from="Example <nobody@example.com>" \
@@ -550,8 +556,9 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
 		--validate \
 		longline.patch 2>actual &&
 	test_path_is_file my-hooks.ran &&
-	cat >expect <<-\EOF &&
+	cat >expect <<-EOF &&
 	fatal: longline.patch: rejected by sendemail-validate hook
+	fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1
 	warning: no patches were sent
 	EOF
 	test_cmp expect actual
-- 
2.31.1.527.g9b8f7de2547


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors
  2021-04-06 14:00           ` [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors Ævar Arnfjörð Bjarmason
                               ` (2 preceding siblings ...)
  2021-04-06 14:00             ` [PATCH v3 3/3] git-send-email: improve --validate error output Ævar Arnfjörð Bjarmason
@ 2021-04-06 20:33             ` Junio C Hamano
  3 siblings, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-04-06 20:33 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: git, Emily Shaffer

Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:

> Version 3 yields the TREESAME end result as v2[1], but re-arranges the
> way we get there to make the progression more understandable, along
> with a minor commit message update.

Nice.  I see no more nits to pick ;-)

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages
  2021-03-11  2:10 ` [PATCH v8 37/37] docs: unify githooks and git-hook manpages Emily Shaffer
  2021-03-12  9:29   ` Ævar Arnfjörð Bjarmason
@ 2021-04-07  2:36   ` Junio C Hamano
  2021-04-08 20:20     ` Jeff Hostetler
  2021-04-08 23:46     ` Emily Shaffer
  1 sibling, 2 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-04-07  2:36 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git, Jeff Hostetler

Emily Shaffer <emilyshaffer@google.com> writes:

> By showing the list of all hooks in 'git help hook' for users to refer
> to, 'git help hook' becomes a one-stop shop for hook authorship. Since
> some may still have muscle memory for 'git help githooks', though,
> reference the 'git hook' commands and otherwise don't remove content.
>
> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> ---
>  Documentation/git-hook.txt     |  11 +
>  Documentation/githooks.txt     | 716 +--------------------------------
>  Documentation/native-hooks.txt | 708 ++++++++++++++++++++++++++++++++
>  3 files changed, 724 insertions(+), 711 deletions(-)
>  create mode 100644 Documentation/native-hooks.txt

While this would be a very good move when this were the only topic
juggling the hook related documentation, in the real world, it
creates rather nasty "ouch, the original hooks document was updated,
and we need to carry these changes over to the new native-hooks
file" conflicts with multiple commits on different topics.

$ git log --oneline --no-merges es/config-hooks..seen Documentation/githooks.txt
2d4e48b8ee fsmonitor--daemon: man page and documentation
23c781f173 githooks.txt: clarify documentation on reference-transaction hook
5f308a89d8 githooks.txt: replace mentions of SHA-1 specific properties
7efc378205 doc: fix some typos

$ git log --oneline --no-merges ^master es/config-hooks..seen Documentation/githooks.txt
2d4e48b8ee fsmonitor--daemon: man page and documentation

As three of the four changes are already in master, it probably is a
good idea to rebase this topic (and redo this step) to update the
native-hooks.txt

I am not sure offhand how ready fsmonitor--daemon stuff is, but if
it takes longer to stabilize than this topic, it might make sense to
hold off the changes to githooks.txt in that topic, until this topic
stabilizes enough to hit at least 'next', preferrably 'master', and
then base that topic (or at least the documentation part of it) on
the final shape of the native-hooks.txt.

Or better ideas?

Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages
  2021-04-07  2:36   ` Junio C Hamano
@ 2021-04-08 20:20     ` Jeff Hostetler
  2021-04-08 21:17       ` Junio C Hamano
  2021-04-08 23:46     ` Emily Shaffer
  1 sibling, 1 reply; 324+ messages in thread
From: Jeff Hostetler @ 2021-04-08 20:20 UTC (permalink / raw)
  To: Junio C Hamano, Emily Shaffer; +Cc: git, Jeff Hostetler



On 4/6/21 10:36 PM, Junio C Hamano wrote:
> Emily Shaffer <emilyshaffer@google.com> writes:
> 
>> By showing the list of all hooks in 'git help hook' for users to refer
>> to, 'git help hook' becomes a one-stop shop for hook authorship. Since
>> some may still have muscle memory for 'git help githooks', though,
>> reference the 'git hook' commands and otherwise don't remove content.
>>
>> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
>> ---
>>   Documentation/git-hook.txt     |  11 +
>>   Documentation/githooks.txt     | 716 +--------------------------------
>>   Documentation/native-hooks.txt | 708 ++++++++++++++++++++++++++++++++
>>   3 files changed, 724 insertions(+), 711 deletions(-)
>>   create mode 100644 Documentation/native-hooks.txt
> 
> While this would be a very good move when this were the only topic
> juggling the hook related documentation, in the real world, it
> creates rather nasty "ouch, the original hooks document was updated,
> and we need to carry these changes over to the new native-hooks
> file" conflicts with multiple commits on different topics.
> 
> $ git log --oneline --no-merges es/config-hooks..seen Documentation/githooks.txt
> 2d4e48b8ee fsmonitor--daemon: man page and documentation
> 23c781f173 githooks.txt: clarify documentation on reference-transaction hook
> 5f308a89d8 githooks.txt: replace mentions of SHA-1 specific properties
> 7efc378205 doc: fix some typos
> 
> $ git log --oneline --no-merges ^master es/config-hooks..seen Documentation/githooks.txt
> 2d4e48b8ee fsmonitor--daemon: man page and documentation
> 
> As three of the four changes are already in master, it probably is a
> good idea to rebase this topic (and redo this step) to update the
> native-hooks.txt
> 
> I am not sure offhand how ready fsmonitor--daemon stuff is, but if
> it takes longer to stabilize than this topic, it might make sense to
> hold off the changes to githooks.txt in that topic, until this topic
> stabilizes enough to hit at least 'next', preferrably 'master', and
> then base that topic (or at least the documentation part of it) on
> the final shape of the native-hooks.txt.
> 
> Or better ideas?
> 
> Thanks.
> 

I expect the fsmonitor stuff to take a while.  It is rather large
and complicated.  My changes in the Documentation are rather minor.
And I wouldn't want to be the sole reason to hold up Emily's changes.

If it would be helpful, you can add a "revert" commit on top of my
branch for my documentation commit -or- just drop it completely from
my series.  Then I can re-adjust/rebase my doc changes before
I send a V2.

Jeff

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages
  2021-04-08 20:20     ` Jeff Hostetler
@ 2021-04-08 21:17       ` Junio C Hamano
  0 siblings, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-04-08 21:17 UTC (permalink / raw)
  To: Jeff Hostetler; +Cc: Emily Shaffer, git, Jeff Hostetler

Jeff Hostetler <git@jeffhostetler.com> writes:

> I expect the fsmonitor stuff to take a while.  It is rather large
> and complicated.  My changes in the Documentation are rather minor.
> And I wouldn't want to be the sole reason to hold up Emily's changes.
>
> If it would be helpful, you can add a "revert" commit on top of my
> branch for my documentation commit -or- just drop it completely from
> my series.  Then I can re-adjust/rebase my doc changes before
> I send a V2.

Sounds like a plan.  I'll drop that step for now before the next
integration cycle.

There is another topic that interacts with es/config-hooks topic
badly (which I haven't resolved) in flight, though.

Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function
  2021-04-05 23:47             ` Junio C Hamano
@ 2021-04-08 22:43               ` Junio C Hamano
  2021-04-08 22:46                 ` Junio C Hamano
  0 siblings, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-04-08 22:43 UTC (permalink / raw)
  To: Emily Shaffer, Ævar Arnfjörð Bjarmason; +Cc: git

Junio C Hamano <gitster@pobox.com> writes:

> One big thing that is different between this version and the one in
> Emily's "config hook" topic is that this is still limited to the
> case where $repo exists.  In the new world order, it will not matter
> in what directory the command runs, as long as "git hook" finds the
> hook, and details of the invocation is hidden behind the command.
>
> I presume that Emily's series is expected to be updated soonish?
> Please figure out who to go first and other details to work well
> together between you two.

Since I didn't hear from either of you, I'll queue with this
possibly bogus conflict resolution for now.

Thanks.


diff --cc git-send-email.perl
index 175da07d94,73e1e0b51a..0000000000
--- i/git-send-email.perl
+++ w/git-send-email.perl
@@@ -1947,27 -1940,11 +1947,13 @@@ sub unique_email_list 
  
  sub validate_patch {
  	my ($fn, $xfer_encoding) = @_;
--
- 	if ($repo) {
- 		my $validate_hook = catfile($repo->hooks_path(),
- 					    'sendemail-validate');
- 		my $hook_error;
- 		if (-x $validate_hook) {
- 			my $target = abs_path($fn);
- 			# The hook needs a correct cwd and GIT_DIR.
- 			my $cwd_save = cwd();
- 			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]);
- 			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);
- 		}
+ 	my $target = abs_path($fn);
 -	return "rejected by sendemail-validate hook"
 -		if system(("git", "hook", "run", "sendemail-validate", "-a",
 -				$target));
++	$hook_error = system_or_msg([qw(git hook run sendemail-validate -a), $target]);
++	if ($hook_error) {
++		die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
++			       "%s\n" .
++			       "warning: no patches were sent\n"), $fn, $hook_error);
 +	}
  
  	# Any long lines will be automatically fixed if we use a suitable transfer
  	# encoding.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function
  2021-04-08 22:43               ` Junio C Hamano
@ 2021-04-08 22:46                 ` Junio C Hamano
  2021-04-08 23:54                   ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-04-08 22:46 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: Ævar Arnfjörð Bjarmason, git

Junio C Hamano <gitster@pobox.com> writes:

> Junio C Hamano <gitster@pobox.com> writes:
>
>> One big thing that is different between this version and the one in
>> Emily's "config hook" topic is that this is still limited to the
>> case where $repo exists.  In the new world order, it will not matter
>> in what directory the command runs, as long as "git hook" finds the
>> hook, and details of the invocation is hidden behind the command.
>>
>> I presume that Emily's series is expected to be updated soonish?
>> Please figure out who to go first and other details to work well
>> together between you two.
>
> Since I didn't hear from either of you, I'll queue with this
> possibly bogus conflict resolution for now.
>

Well, I retract it.  This makes many steps in send-email tests
fail.  For now, es/config-hooks topic is excluded from 'seen'.

What's the status of that topic, if there weren't other topics in
flight that interfere with it, by the way?  Is it otherwise a good
enough shape to be given priority and stable enough to get other
topics rebased on top of it?

Thanks.

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages
  2021-04-07  2:36   ` Junio C Hamano
  2021-04-08 20:20     ` Jeff Hostetler
@ 2021-04-08 23:46     ` Emily Shaffer
  2021-04-09  0:03       ` Junio C Hamano
  1 sibling, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-04-08 23:46 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: git, Jeff Hostetler

On Tue, Apr 06, 2021 at 07:36:15PM -0700, Junio C Hamano wrote:
> 
> Emily Shaffer <emilyshaffer@google.com> writes:
> 
> > By showing the list of all hooks in 'git help hook' for users to refer
> > to, 'git help hook' becomes a one-stop shop for hook authorship. Since
> > some may still have muscle memory for 'git help githooks', though,
> > reference the 'git hook' commands and otherwise don't remove content.
> >
> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
> > ---
> >  Documentation/git-hook.txt     |  11 +
> >  Documentation/githooks.txt     | 716 +--------------------------------
> >  Documentation/native-hooks.txt | 708 ++++++++++++++++++++++++++++++++
> >  3 files changed, 724 insertions(+), 711 deletions(-)
> >  create mode 100644 Documentation/native-hooks.txt
> 
> While this would be a very good move when this were the only topic
> juggling the hook related documentation, in the real world, it
> creates rather nasty "ouch, the original hooks document was updated,
> and we need to carry these changes over to the new native-hooks
> file" conflicts with multiple commits on different topics.
> 
> $ git log --oneline --no-merges es/config-hooks..seen Documentation/githooks.txt
> 2d4e48b8ee fsmonitor--daemon: man page and documentation
> 23c781f173 githooks.txt: clarify documentation on reference-transaction hook
> 5f308a89d8 githooks.txt: replace mentions of SHA-1 specific properties
> 7efc378205 doc: fix some typos
> 
> $ git log --oneline --no-merges ^master es/config-hooks..seen Documentation/githooks.txt
> 2d4e48b8ee fsmonitor--daemon: man page and documentation
> 
> As three of the four changes are already in master, it probably is a
> good idea to rebase this topic (and redo this step) to update the
> native-hooks.txt
> 
> I am not sure offhand how ready fsmonitor--daemon stuff is, but if
> it takes longer to stabilize than this topic, it might make sense to
> hold off the changes to githooks.txt in that topic, until this topic
> stabilizes enough to hit at least 'next', preferrably 'master', and
> then base that topic (or at least the documentation part of it) on
> the final shape of the native-hooks.txt.
> 
> Or better ideas?
> 
> Thanks.

I got bitten by this same issue with native-hooks.txt while addressing
comments, too. Another commenter suggested to not inline those hook
definitions into "git help hook" - so I plan to drop that part of this
patch. If it makes it easier for you, I think you could revert this last
commit; if we decide later that we want to have "git help hook" share
the hook definitions after all, I think we should do that separately and
as a quick change not stuck behind 36 other complicated patches.

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function
  2021-04-08 22:46                 ` Junio C Hamano
@ 2021-04-08 23:54                   ` Ævar Arnfjörð Bjarmason
  2021-04-09  0:08                     ` Junio C Hamano
  0 siblings, 1 reply; 324+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2021-04-08 23:54 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Emily Shaffer, git


On Fri, Apr 09 2021, Junio C Hamano wrote:

> Junio C Hamano <gitster@pobox.com> writes:
>
>> Junio C Hamano <gitster@pobox.com> writes:
>>
>>> One big thing that is different between this version and the one in
>>> Emily's "config hook" topic is that this is still limited to the
>>> case where $repo exists.  In the new world order, it will not matter
>>> in what directory the command runs, as long as "git hook" finds the
>>> hook, and details of the invocation is hidden behind the command.
>>>
>>> I presume that Emily's series is expected to be updated soonish?
>>> Please figure out who to go first and other details to work well
>>> together between you two.
>>
>> Since I didn't hear from either of you, I'll queue with this
>> possibly bogus conflict resolution for now.
>>
>
> Well, I retract it.  This makes many steps in send-email tests
> fail.  For now, es/config-hooks topic is excluded from 'seen'.

Sorry about not replying earlier upthread. FWIW I didn't look deeply
into how the chdir etc. might interact with Emily's topic. I figured
we'd want the $? etc. cleanup first, and that just deleting most of that
code once we had some hook runner to shell out to would be easy.

> What's the status of that topic, if there weren't other topics in
> flight that interfere with it, by the way?  Is it otherwise a good
> enough shape to be given priority and stable enough to get other
> topics rebased on top of it?

I see I've mentioned [1] in passing to you before, but in summary I have
some major qualms about parts of it, but very much like the overall
direction/goal of having hooks in config.

Elevator pitch summary of the lengthy [1]: hooks in config: good, but
having a "git hook" command introduce some nascent UI for managing a
subset of git-config: somewhere between "meh" / "bad idea" (see security
concerns in [1]) / "not needed". I.e. I demonstrated that we can replace
it with a trivial git-config wrapper, if the series doesn't go out of
its way to make it difficult (i.e. we can/should stick all config for a
given hook in the same <prefix>, and not re-invent the
"sendemail.identity" special-case).

I'd very much like the author to respond to that :) And/or for others to
chime in with what they think.

1. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages
  2021-04-08 23:46     ` Emily Shaffer
@ 2021-04-09  0:03       ` Junio C Hamano
  0 siblings, 0 replies; 324+ messages in thread
From: Junio C Hamano @ 2021-04-09  0:03 UTC (permalink / raw)
  To: Emily Shaffer; +Cc: git, Jeff Hostetler

Emily Shaffer <emilyshaffer@google.com> writes:

> I got bitten by this same issue with native-hooks.txt while addressing
> comments, too. Another commenter suggested to not inline those hook
> definitions into "git help hook" - so I plan to drop that part of this
> patch. If it makes it easier for you, I think you could revert this last
> commit; if we decide later that we want to have "git help hook" share
> the hook definitions after all, I think we should do that separately and
> as a quick change not stuck behind 36 other complicated patches.

I've already discarded the step, and then I had to eject the whole
topic from 'seen' for now (see my other message to you earlier
today).  The "other complicated patches" need to be whipped into
shape to be at least in 'next' first; I do not know how close the
last round is from that state.

Thanks.


^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function
  2021-04-08 23:54                   ` Ævar Arnfjörð Bjarmason
@ 2021-04-09  0:08                     ` Junio C Hamano
  2021-05-03 20:30                       ` Emily Shaffer
  0 siblings, 1 reply; 324+ messages in thread
From: Junio C Hamano @ 2021-04-09  0:08 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: Emily Shaffer, git

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> On Fri, Apr 09 2021, Junio C Hamano wrote:
> ...
>> What's the status of that topic, if there weren't other topics in
>> flight that interfere with it, by the way?  Is it otherwise a good
>> enough shape to be given priority and stable enough to get other
>> topics rebased on top of it?
>
> I see I've mentioned [1] in passing to you before, but in summary I have
> some major qualms about parts of it, but very much like the overall
> direction/goal of having hooks in config.
>
> Elevator pitch summary of the lengthy [1]: hooks in config: good, but
> having a "git hook" command introduce some nascent UI for managing a
> subset of git-config: somewhere between "meh" / "bad idea" (see security
> concerns in [1]) / "not needed". I.e. I demonstrated that we can replace
> it with a trivial git-config wrapper, if the series doesn't go out of
> its way to make it difficult (i.e. we can/should stick all config for a
> given hook in the same <prefix>, and not re-invent the
> "sendemail.identity" special-case).
>
> I'd very much like the author to respond to that :) And/or for others to
> chime in with what they think.
>
> 1. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/

OK, Emily, I guess the ball is in your court now?

^ permalink raw reply	[flat|nested] 324+ messages in thread

* Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function
  2021-04-09  0:08                     ` Junio C Hamano
@ 2021-05-03 20:30                       ` Emily Shaffer
  0 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-05-03 20:30 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Ævar Arnfjörð Bjarmason, git

On Thu, Apr 08, 2021 at 05:08:30PM -0700, Junio C Hamano wrote:
> 
> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
> 
> > On Fri, Apr 09 2021, Junio C Hamano wrote:
> > ...
> >> What's the status of that topic, if there weren't other topics in
> >> flight that interfere with it, by the way?  Is it otherwise a good
> >> enough shape to be given priority and stable enough to get other
> >> topics rebased on top of it?
> >
> > I see I've mentioned [1] in passing to you before, but in summary I have
> > some major qualms about parts of it, but very much like the overall
> > direction/goal of having hooks in config.
> >
> > Elevator pitch summary of the lengthy [1]: hooks in config: good, but
> > having a "git hook" command introduce some nascent UI for managing a
> > subset of git-config: somewhere between "meh" / "bad idea" (see security
> > concerns in [1]) / "not needed". I.e. I demonstrated that we can replace
> > it with a trivial git-config wrapper, if the series doesn't go out of
> > its way to make it difficult (i.e. we can/should stick all config for a
> > given hook in the same <prefix>, and not re-invent the
> > "sendemail.identity" special-case).
> >
> > I'd very much like the author to respond to that :) And/or for others to
> > chime in with what they think.
> >
> > 1. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/
> 
> OK, Emily, I guess the ball is in your court now?

The topic is not ready for submission besides interference. I have a
list of things to do and was sidetracked with other work (the submodule
RFC, etc.). This week I am working on getting this series polished and
ready to go.

 - Emily

^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v9 00/37] propose config-based hooks
  2021-03-11  2:10 [PATCH v8 00/37] config-based hooks Emily Shaffer
                   ` (39 preceding siblings ...)
  2021-03-12 11:13 ` Ævar Arnfjörð Bjarmason
@ 2021-05-27  0:08 ` Emily Shaffer
  2021-05-27  0:08   ` [PATCH v9 01/37] doc: propose hooks managed by the config Emily Shaffer
                     ` (38 more replies)
  40 siblings, 39 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-05-27  0:08 UTC (permalink / raw)
  To: git
  Cc: Emily Shaffer, Jeff King, Junio C Hamano, James Ramsay,
	Jonathan Nieder, brian m. carlson,
	Ævar Arnfjörð Bjarmason, Phillip Wood,
	Josh Steadmon, Johannes Schindelin, Jonathan Tan

After much delay and $DAYJOB, here is v9.

- Addressed nits in reviews on v8
- sendemail-validate hook becomes non-parallelized; updated to use
  Ævar's updated system_or_die() function
- changed strbuf to char* in hooks_list
  - Attempted to do so in run_command's stdout callback, but this made
    length protection difficult, so stuck with strbuf there.
- test_i18ncmp -> test_cmp
- Stop doing i18n lego in run_hooks()
- Checked that run_hooks_opt_init() is always separated by a space from
  variable decl blocks
- Checked for early returns which may skip run_hooks_opt_clear(); this
  resulted in minimizing the scope of run_hooks_opt in most places
- Got rid of native-hooks.txt. It was a nice idea, but not attached to a
  large and slow series like this one.
- In traces, log the name of the hook (e.g. "pre-commit") instead of the
  name of the executable (e.g. "/home/emily/check-for-debug-strings");
  the executable name is tracelogged as part of argv anyways, and we
  want to be able to tell which hook was responsible for invoking the
  executable in question.

Thanks.
 - Emily

Emily Shaffer (37):
  doc: propose hooks managed by the config
  hook: introduce git-hook subcommand
  hook: include hookdir hook in list
  hook: teach hook.runHookDir
  hook: implement hookcmd.<name>.skip
  parse-options: parse into strvec
  hook: add 'run' subcommand
  hook: introduce hook_exists()
  hook: support passing stdin to hooks
  run-command: allow stdin for run_processes_parallel
  hook: allow parallel hook execution
  hook: allow specifying working directory for hooks
  run-command: add stdin callback for parallelization
  hook: provide stdin by string_list or callback
  run-command: allow capturing of collated output
  hooks: allow callers to capture output
  commit: use config-based hooks
  am: convert applypatch hooks to use config
  merge: use config-based hooks for post-merge hook
  gc: use hook library for pre-auto-gc hook
  rebase: teach pre-rebase to use hook.h
  read-cache: convert post-index-change hook to use config
  receive-pack: convert push-to-checkout hook to hook.h
  git-p4: use 'git hook' to run hooks
  hooks: convert 'post-checkout' hook to hook library
  hook: convert 'post-rewrite' hook to config
  transport: convert pre-push hook to use config
  reference-transaction: look for hooks in config
  receive-pack: convert 'update' hook to hook.h
  proc-receive: acquire hook list from hook.h
  post-update: use hook.h library
  receive-pack: convert receive hooks to hook.h
  bugreport: use hook_exists instead of find_hook
  git-send-email: use 'git hook run' for 'sendemail-validate'
  run-command: stop thinking about hooks
  doc: clarify fsmonitor-watchman specification
  docs: link githooks and git-hook manpages

 .gitignore                                    |   1 +
 Documentation/Makefile                        |   1 +
 Documentation/config/hook.txt                 |  27 +
 Documentation/git-hook.txt                    | 162 ++++++
 Documentation/githooks.txt                    |  77 ++-
 Documentation/technical/api-parse-options.txt |   7 +
 .../technical/config-based-hooks.txt          | 369 +++++++++++++
 Makefile                                      |   2 +
 builtin.h                                     |   1 +
 builtin/am.c                                  |  39 +-
 builtin/bugreport.c                           |   4 +-
 builtin/checkout.c                            |  19 +-
 builtin/clone.c                               |   8 +-
 builtin/commit.c                              |  11 +-
 builtin/fetch.c                               |   1 +
 builtin/gc.c                                  |   9 +-
 builtin/hook.c                                | 190 +++++++
 builtin/merge.c                               |  15 +-
 builtin/rebase.c                              |  10 +-
 builtin/receive-pack.c                        | 326 ++++++------
 builtin/submodule--helper.c                   |   2 +-
 builtin/worktree.c                            |  32 +-
 command-list.txt                              |   1 +
 commit.c                                      |  22 +-
 commit.h                                      |   3 +-
 git-p4.py                                     |  67 +--
 git-send-email.perl                           |  26 +-
 git.c                                         |   1 +
 hook.c                                        | 483 ++++++++++++++++++
 hook.h                                        | 139 +++++
 parse-options-cb.c                            |  16 +
 parse-options.h                               |   4 +
 read-cache.c                                  |  13 +-
 refs.c                                        |  43 +-
 reset.c                                       |  17 +-
 run-command.c                                 | 156 +++---
 run-command.h                                 |  55 +-
 sequencer.c                                   |  92 ++--
 submodule.c                                   |   1 +
 t/helper/test-parse-options.c                 |   6 +
 t/helper/test-run-command.c                   |  46 +-
 t/t0040-parse-options.sh                      |  27 +
 t/t0061-run-command.sh                        |  37 ++
 t/t1360-config-based-hooks.sh                 | 329 ++++++++++++
 t/t1416-ref-transaction-hooks.sh              |  12 +-
 t/t5411/test-0015-too-many-hooks-error.sh     |  47 ++
 ...3-pre-commit-and-pre-merge-commit-hooks.sh |  17 +-
 t/t9001-send-email.sh                         |  13 +-
 transport.c                                   |  58 +--
 49 files changed, 2505 insertions(+), 539 deletions(-)
 create mode 100644 Documentation/config/hook.txt
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 Documentation/technical/config-based-hooks.txt
 create mode 100644 builtin/hook.c
 create mode 100644 hook.c
 create mode 100644 hook.h
 create mode 100755 t/t1360-config-based-hooks.sh
 create mode 100644 t/t5411/test-0015-too-many-hooks-error.sh



 1:  85b99369f1 !  1:  d2b7ee8317 doc: propose hooks managed by the config
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    +
    + ## Notes ##
    +    Since v6, checked for inconsistencies with implementation and added lots of
    +    caveats about whether 'git hook add' and 'git hook edit' will ever materialize.
    +
    +    Hopefully this reflects reality now; please review accordingly.
    +
    +    Since v6, checked for inconsistencies with implementation and added lots of
    +    caveats about whether 'git hook add' and 'git hook edit' will ever materialize.
    +
    +    Hopefully this reflects reality now; please review accordingly.
    +
    +    Since v4, addressed comments from Jonathan Tan about wording. However, I have
    +    not addressed AEvar's comments or done a full re-review of this document.
    +    I wanted to get the rest of the series out for initial review first.
    +
    +     - Emily
    +
    +    Since v4, addressed comments from Jonathan Tan about wording.
    +
      ## Documentation/Makefile ##
     @@ Documentation/Makefile: SP_ARTICLES += $(API_DOCS)
      TECH_DOCS += MyFirstContribution
 2:  1d19f1477c <  -:  ---------- hook: scaffolding for git-hook subcommand
 3:  c125c63880 !  2:  112a809f02 hook: add list command
    @@ Metadata
     Author: Emily Shaffer <emilyshaffer@google.com>
     
      ## Commit message ##
    -    hook: add list command
    +    hook: introduce git-hook subcommand
     
    -    Teach 'git hook list <hookname>', which checks the known configs in
    +    Add a new subcommand, git-hook, which will be used to ease config-based
    +    hook management. This command will handle parsing configs to compose a
    +    list of hooks to run for a given event, as well as adding or modifying
    +    hook configs in an interactive fashion.
    +
    +    Start with 'git hook list <hookname>', which checks the known configs in
         order to create an ordered list of hooks to run on a given hook event.
     
         Multiple commands can be specified for a given hook by providing
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    +
    + ## Notes ##
    +    Since v4, mainly changed to RUN_SETUP_GENTLY so that 'git hook list' can
    +    be executed outside of a repo.
    +
    + ## .gitignore ##
    +@@
    + /git-grep
    + /git-hash-object
    + /git-help
    ++/git-hook
    + /git-http-backend
    + /git-http-fetch
    + /git-http-push
    +
      ## Documentation/config/hook.txt (new) ##
     @@
     +hook.<command>.command::
    @@ Documentation/config/hook.txt (new)
     +	as a command. This can be an executable on your device or a oneliner for
     +	your shell. See linkgit:git-hook[1].
     
    - ## Documentation/git-hook.txt ##
    -@@ Documentation/git-hook.txt: git-hook - Manage configured hooks
    - SYNOPSIS
    - --------
    - [verse]
    --'git hook'
    + ## Documentation/git-hook.txt (new) ##
    +@@
    ++git-hook(1)
    ++===========
    ++
    ++NAME
    ++----
    ++git-hook - Manage configured hooks
    ++
    ++SYNOPSIS
    ++--------
    ++[verse]
     +'git hook' list <hook-name>
    - 
    - DESCRIPTION
    - -----------
    --A placeholder command. Later, you will be able to list, add, and modify hooks
    --with this command.
    ++
    ++DESCRIPTION
    ++-----------
     +You can list configured hooks with this command. Later, you will be able to run,
     +add, and modify hooks with this command.
     +
    @@ Documentation/git-hook.txt: git-hook - Manage configured hooks
     +CONFIGURATION
     +-------------
     +include::config/hook.txt[]
    - 
    - GIT
    - ---
    ++
    ++GIT
    ++---
    ++Part of the linkgit:git[1] suite
     
      ## Makefile ##
     @@ Makefile: LIB_OBJS += hash-lookup.o
    @@ Makefile: LIB_OBJS += hash-lookup.o
      LIB_OBJS += ident.o
      LIB_OBJS += json-writer.o
      LIB_OBJS += kwset.o
    +@@ Makefile: 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
     
    - ## builtin/hook.c ##
    + ## builtin.h ##
    +@@ builtin.h: 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);
    +
    + ## builtin/hook.c (new) ##
     @@
    - #include "cache.h"
    --
    - #include "builtin.h"
    ++#include "cache.h"
    ++#include "builtin.h"
     +#include "config.h"
     +#include "hook.h"
    - #include "parse-options.h"
    ++#include "parse-options.h"
     +#include "strbuf.h"
    - 
    - static const char * const builtin_hook_usage[] = {
    --	N_("git hook"),
    ++
    ++static const char * const builtin_hook_usage[] = {
     +	N_("git hook list <hookname>"),
    - 	NULL
    - };
    - 
    --int cmd_hook(int argc, const char **argv, const char *prefix)
    ++	NULL
    ++};
    ++
     +static int list(int argc, const char **argv, const char *prefix)
    - {
    --	struct option builtin_hook_options[] = {
    ++{
     +	struct list_head *head, *pos;
    -+	struct strbuf hookname = STRBUF_INIT;
    ++	const char *hookname = NULL;
     +
     +	struct option list_options[] = {
    - 		OPT_END(),
    - 	};
    - 
    --	argc = parse_options(argc, argv, prefix, builtin_hook_options,
    ++		OPT_END(),
    ++	};
    ++
     +	argc = parse_options(argc, argv, prefix, list_options,
    - 			     builtin_hook_usage, 0);
    - 
    ++			     builtin_hook_usage, 0);
    ++
     +	if (argc < 1) {
     +		usage_msg_opt(_("You must specify a hook event name to list."),
     +			      builtin_hook_usage, list_options);
     +	}
     +
    -+	strbuf_addstr(&hookname, argv[0]);
    ++	hookname = argv[0];
     +
    -+	head = hook_list(&hookname);
    ++	head = hook_list(hookname);
     +
     +	if (list_empty(head)) {
     +		printf(_("no commands configured for hook '%s'\n"),
    -+		       hookname.buf);
    -+		strbuf_release(&hookname);
    ++		       hookname);
     +		return 0;
     +	}
     +
    @@ builtin/hook.c
     +	}
     +
     +	clear_hook_list(head);
    -+	strbuf_release(&hookname);
     +
    - 	return 0;
    - }
    ++	return 0;
    ++}
     +
     +int cmd_hook(int argc, const char **argv, const char *prefix)
     +{
    @@ builtin/hook.c
     +	usage_with_options(builtin_hook_usage, builtin_hook_options);
     +}
     
    + ## command-list.txt ##
    +@@ command-list.txt: 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
    +
    + ## git.c ##
    +@@ git.c: static struct cmd_struct commands[] = {
    + 	{ "grep", cmd_grep, RUN_SETUP_GENTLY },
    + 	{ "hash-object", cmd_hash_object },
    + 	{ "help", cmd_help },
    ++	{ "hook", cmd_hook, RUN_SETUP_GENTLY },
    + 	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
    + 	{ "init", cmd_init_db },
    + 	{ "init-db", cmd_init_db },
    +
      ## hook.c (new) ##
     @@
     +#include "cache.h"
    @@ hook.c (new)
     +	return 0;
     +}
     +
    -+struct list_head* hook_list(const struct strbuf* hookname)
    ++struct list_head* hook_list(const char* hookname)
     +{
     +	struct strbuf hook_key = STRBUF_INIT;
     +	struct list_head *hook_head = xmalloc(sizeof(struct list_head));
    @@ hook.c (new)
     +	if (!hookname)
     +		return NULL;
     +
    -+	strbuf_addf(&hook_key, "hook.%s.command", hookname->buf);
    ++	strbuf_addf(&hook_key, "hook.%s.command", hookname);
     +
     +	git_config(hook_config_lookup, &cb_data);
     +
    @@ hook.h (new)
     + * Provides a linked list of 'struct hook' detailing commands which should run
     + * in response to the 'hookname' event, in execution order.
     + */
    -+struct list_head* hook_list(const struct strbuf *hookname);
    ++struct list_head* hook_list(const char *hookname);
     +
     +/* Free memory associated with a 'struct hook' */
     +void free_hook(struct hook *ptr);
     +/* Empties the list at 'head', calling 'free_hook()' on each entry */
     +void clear_hook_list(struct list_head *head);
     
    - ## t/t1360-config-based-hooks.sh ##
    -@@ t/t1360-config-based-hooks.sh: test_description='config-managed multihooks, including git-hook command'
    - 
    - . ./test-lib.sh
    - 
    --test_expect_success 'git hook command does not crash' '
    --	git hook
    + ## t/t1360-config-based-hooks.sh (new) ##
    +@@
    ++#!/bin/bash
    ++
    ++test_description='config-managed multihooks, including git-hook command'
    ++
    ++. ./test-lib.sh
    ++
     +ROOT=
     +if test_have_prereq MINGW
     +then
    @@ t/t1360-config-based-hooks.sh: test_description='config-managed multihooks, incl
     +
     +	git hook list pre-commit >actual &&
     +	test_cmp expected actual
    - '
    - 
    - test_done
    ++'
    ++
    ++test_done
 4:  0b8cd46ff9 !  3:  3114306368 hook: include hookdir hook in list
    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
      	list_for_each(pos, head) {
      		struct hook *item = list_entry(pos, struct hook, list);
     -		if (item)
    +-			printf("%s: %s\n",
    +-			       config_scope_name(item->origin),
     +		item = list_entry(pos, struct hook, list);
     +		if (item) {
    -+			/* Don't translate 'hookdir' - it matches the config */
    - 			printf("%s: %s\n",
    --			       config_scope_name(item->origin),
    ++			/*
    ++			 * TRANSLATORS: "<config scope>: <path>". Both fields
    ++			 * should be left untranslated; config scope matches the
    ++			 * output of 'git config --show-scope'. Marked for
    ++			 * translation to provide better RTL support later.
    ++			 */
    ++			printf(_("%s: %s\n"),
     +			       (item->from_hookdir
     +				? "hookdir"
     +				: config_scope_name(item->origin)),
    @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *comm
      	}
      
      	/* re-set the scope so we show where an override was specified */
    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
    +@@ hook.c: struct list_head* hook_list(const char* hookname)
      
      	git_config(hook_config_lookup, &cb_data);
      
     +	if (have_git_dir()) {
    -+		const char *legacy_hook_path = find_hook(hookname->buf);
    ++		const char *legacy_hook_path = find_hook(hookname);
     +
     +		/* Unconditionally add legacy hook, but annotate it. */
     +		if (legacy_hook_path) {
 5:  05c503fbe1 !  4:  681013c32a hook: teach hook.runHookDir
    @@ Commit message
         list'. Later on, though, we will pay attention to this enum when running
         the hooks.
     
    +
    + ## Notes ##
    +    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to
    +    enforce it - now it matches the design doc much better.
    +
    +    Also, thanks Jonathan Tan for pointing out that the commit message made no sense
    +    and was targeted for a different change. Rewrote the commit message now.
    +
    +    Plus, added HOOK_ERROR flag per Junio and Jonathan Nieder.
    +
    +    Newly split into its own commit since v4, and taking place much sooner.
    +
    +    An unfortunate side effect of adding this support *before* the
    +    hook.runHookDir support is that the labels on the list are not clear -
    +    because we aren't yet flagging which hooks are from the hookdir versus
    +    the config. I suppose we could move the addition of that field to the
    +    struct hook up to this patch, but it didn't make a lot of sense to me to
    +    do it just for cosmetic purposes.
    +
    +    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to
    +    enforce it - now it matches the design doc much better.
    +
    +    Also, thanks Jonathan Tan for pointing out that the commit message made no sense
    +    and was targeted for a different change. Rewrote the commit message now.
    +
    +    Newly split into its own commit since v4, and taking place much sooner.
    +
    +    An unfortunate side effect of adding this support *before* the
    +    hook.runHookDir support is that the labels on the list are not clear -
    +    because we aren't yet flagging which hooks are from the hookdir versus
    +    the config. I suppose we could move the addition of that field to the
    +    struct hook up to this patch, but it didn't make a lot of sense to me to
    +    do it just for cosmetic purposes.
    +
      ## Documentation/config/hook.txt ##
     @@ Documentation/config/hook.txt: hookcmd.<name>.command::
      	A command to execute during a hook for which <name> has been specified
    @@ builtin/hook.c: static const char * const builtin_hook_usage[] = {
      static int list(int argc, const char **argv, const char *prefix)
      {
      	struct list_head *head, *pos;
    - 	struct strbuf hookname = STRBUF_INIT;
    + 	const char *hookname = NULL;
     +	struct strbuf hookdir_annotation = STRBUF_INIT;
      
      	struct option list_options[] = {
      		OPT_END(),
     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
    - 		return 0;
    - 	}
    - 
    -+	switch (should_run_hookdir) {
    -+		case HOOKDIR_NO:
    -+			strbuf_addstr(&hookdir_annotation, _(" (will not run)"));
    -+			break;
    -+		case HOOKDIR_ERROR:
    -+			strbuf_addstr(&hookdir_annotation, _(" (will error and not run)"));
    -+			break;
    -+		case HOOKDIR_INTERACTIVE:
    -+			strbuf_addstr(&hookdir_annotation, _(" (will prompt)"));
    -+			break;
    -+		case HOOKDIR_WARN:
    -+			strbuf_addstr(&hookdir_annotation, _(" (will warn but run)"));
    -+			break;
    -+		case HOOKDIR_YES:
    -+		/*
    -+		 * The default behavior should agree with
    -+		 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
    -+		 * do the default behavior.
    -+		 */
    -+		case HOOKDIR_UNKNOWN:
    -+		default:
    -+			break;
    -+	}
    -+
    - 	list_for_each(pos, head) {
      		struct hook *item = list_entry(pos, struct hook, list);
      		item = list_entry(pos, struct hook, list);
      		if (item) {
    - 			/* Don't translate 'hookdir' - it matches the config */
    --			printf("%s: %s\n",
    -+			printf("%s: %s%s\n",
    - 			       (item->from_hookdir
    - 				? "hookdir"
    - 				: config_scope_name(item->origin)),
    +-			/*
    +-			 * TRANSLATORS: "<config scope>: <path>". Both fields
    +-			 * should be left untranslated; config scope matches the
    +-			 * output of 'git config --show-scope'. Marked for
    +-			 * translation to provide better RTL support later.
    +-			 */
    +-			printf(_("%s: %s\n"),
    +-			       (item->from_hookdir
    +-				? "hookdir"
    +-				: config_scope_name(item->origin)),
     -			       item->command.buf);
    -+			       item->command.buf,
    -+			       (item->from_hookdir
    -+				? hookdir_annotation.buf
    -+				: ""));
    ++			if (item->from_hookdir) {
    ++				/*
    ++				 * TRANSLATORS: do not translate 'hookdir' as
    ++				 * it matches the config setting.
    ++				 */
    ++				switch (should_run_hookdir) {
    ++				case HOOKDIR_NO:
    ++					printf(_("hookdir: %s (will not run)\n"),
    ++					       item->command.buf);
    ++					break;
    ++				case HOOKDIR_ERROR:
    ++					printf(_("hookdir: %s (will error and not run)\n"),
    ++					       item->command.buf);
    ++					break;
    ++				case HOOKDIR_INTERACTIVE:
    ++					printf(_("hookdir: %s (will prompt)\n"),
    ++					       item->command.buf);
    ++					break;
    ++				case HOOKDIR_WARN:
    ++					printf(_("hookdir: %s (will warn but run)\n"),
    ++					       item->command.buf);
    ++					break;
    ++				case HOOKDIR_YES:
    ++				/*
    ++				 * The default behavior should agree with
    ++				 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
    ++				 * do the default behavior.
    ++				 */
    ++				case HOOKDIR_UNKNOWN:
    ++				default:
    ++					printf(_("hookdir: %s\n"),
    ++						 item->command.buf);
    ++					break;
    ++				}
    ++			} else {
    ++				/*
    ++				 * TRANSLATORS: "<config scope>: <path>". Both fields
    ++				 * should be left untranslated; config scope matches the
    ++				 * output of 'git config --show-scope'. Marked for
    ++				 * translation to provide better RTL support later.
    ++				 */
    ++				printf(_("%s: %s\n"),
    ++					config_scope_name(item->origin),
    ++					item->command.buf);
    ++			}
      		}
      	}
      
      	clear_hook_list(head);
     +	strbuf_release(&hookdir_annotation);
    - 	strbuf_release(&hookname);
      
      	return 0;
    -@@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
    + }
      
      int cmd_hook(int argc, const char **argv, const char *prefix)
      {
    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)
     +		else if (!strcmp(run_hookdir, "interactive"))
     +			should_run_hookdir = HOOKDIR_INTERACTIVE;
     +		else
    ++			/*
    ++			 * TRANSLATORS: leave "yes/warn/interactive/no"
    ++			 * untranslated; the strings are compared literally.
    ++			 */
     +			die(_("'%s' is not a valid option for --run-hookdir "
     +			      "(yes, warn, interactive, no)"), run_hookdir);
     +	else
    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *
     +	return HOOKDIR_UNKNOWN;
     +}
     +
    - struct list_head* hook_list(const struct strbuf* hookname)
    + struct list_head* hook_list(const char* hookname)
      {
      	struct strbuf hook_key = STRBUF_INIT;
     
      ## hook.h ##
     @@ hook.h: struct hook {
       */
    - struct list_head* hook_list(const struct strbuf *hookname);
    + struct list_head* hook_list(const char *hookname);
      
     +enum hookdir_opt
     +{
    @@ hook.h: struct hook {
     + * command line arguments.
     + */
     +enum hookdir_opt configured_hookdir_opt(void);
    ++
    ++/*
    ++ * Provides the hookdir_opt specified in the config without consulting any
    ++ * command line arguments.
    ++ */
    ++enum hookdir_opt configured_hookdir_opt(void);
     +
      /* Free memory associated with a 'struct hook' */
      void free_hook(struct hook *ptr);
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr
     +
     +	git hook list pre-commit >actual &&
     +	# the hookdir annotation is translated
    -+	test_i18ncmp expected actual
    ++	test_cmp expected actual
     +'
     +
     +test_expect_success 'hook.runHookDir = error is respected by list' '
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr
     +
     +	git hook list pre-commit >actual &&
     +	# the hookdir annotation is translated
    -+	test_i18ncmp expected actual
    ++	test_cmp expected actual
     +'
     +
     +test_expect_success 'hook.runHookDir = warn is respected by list' '
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr
     +
     +	git hook list pre-commit >actual &&
     +	# the hookdir annotation is translated
    -+	test_i18ncmp expected actual
    ++	test_cmp expected actual
     +'
     +
     +
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr
     +
     +	git hook list pre-commit >actual &&
     +	# the hookdir annotation is translated
    -+	test_i18ncmp expected actual
    ++	test_cmp expected actual
     +'
     +
     +test_expect_success 'hook.runHookDir is tolerant to unknown values' '
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr
     +
     +	git hook list pre-commit >actual &&
     +	# the hookdir annotation is translated
    -+	test_i18ncmp expected actual
    ++	test_cmp expected actual
     +'
     +
      test_done
 6:  e86025853a !  5:  0a4b9f27b3 hook: implement hookcmd.<name>.skip
    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *
     
      ## t/t1360-config-based-hooks.sh ##
     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is respected by list' '
    - 	test_i18ncmp expected actual
    + 	test_cmp expected actual
      '
      
     +test_expect_success 'git hook list removes skipped hookcmd' '
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is re
     +	EOF
     +
     +	git hook list pre-commit >actual &&
    -+	test_i18ncmp expected actual
    ++	test_cmp expected actual
     +'
     +
     +test_expect_success 'git hook list ignores skip referring to unused hookcmd' '
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is re
     +	EOF
     +
     +	git hook list pre-commit >actual &&
    -+	test_i18ncmp expected actual
    ++	test_cmp expected actual
     +'
     +
     +test_expect_success 'git hook list removes skipped inlined hook' '
 7:  6e10593d75 !  6:  2ad4f44d08 parse-options: parse into strvec
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    +
    + ## Notes ##
    +    Since v7, updated the reference doc to make the intended usage for OPT_STRVEC
    +    more clear.
    +
    +    Since v4, fixed one or two more places where I missed the argv_array->strvec
    +    rename.
    +
      ## Documentation/technical/api-parse-options.txt ##
     @@ Documentation/technical/api-parse-options.txt: There are some macros to easily define options:
      	The string argument is stored as an element in `string_list`.
    @@ parse-options.h: int parse_opt_commits(const struct option *, const char *, int)
      int parse_opt_noop_cb(const struct option *, const char *, int);
      enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx,
      					   const struct option *,
    +
    + ## t/helper/test-parse-options.c ##
    +@@
    + #include "cache.h"
    + #include "parse-options.h"
    + #include "string-list.h"
    ++#include "strvec.h"
    + #include "trace2.h"
    + 
    + static int boolean = 0;
    +@@ t/helper/test-parse-options.c: static char *string = NULL;
    + static char *file = NULL;
    + static int ambiguous;
    + static struct string_list list = STRING_LIST_INIT_NODUP;
    ++static struct strvec vector = STRVEC_INIT;
    + 
    + static struct {
    + 	int called;
    +@@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **argv)
    + 		OPT_STRING('o', NULL, &string, "str", "get another string"),
    + 		OPT_NOOP_NOARG(0, "obsolete"),
    + 		OPT_STRING_LIST(0, "list", &list, "str", "add str to list"),
    ++		OPT_STRVEC(0, "vector", &vector, "str", "add str to strvec"),
    + 		OPT_GROUP("Magic arguments"),
    + 		OPT_ARGUMENT("quux", NULL, "means --quux"),
    + 		OPT_NUMBER_CALLBACK(&integer, "set integer to NUM",
    +@@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **argv)
    + 	for (i = 0; i < list.nr; i++)
    + 		show(&expect, &ret, "list: %s", list.items[i].string);
    + 
    ++	for (i = 0; i < vector.nr; i++)
    ++		show(&expect, &ret, "vector: %s", vector.v[i]);
    ++
    + 	for (i = 0; i < argc; i++)
    + 		show(&expect, &ret, "arg %02d: %s", i, argv[i]);
    + 
    +
    + ## t/t0040-parse-options.sh ##
    +@@ t/t0040-parse-options.sh: String options
    +     --st <st>             get another string (pervert ordering)
    +     -o <str>              get another string
    +     --list <str>          add str to list
    ++    --vector <str>        add str to strvec
    + 
    + Magic arguments
    +     --quux                means --quux
    +@@ t/t0040-parse-options.sh: test_expect_success '--no-list resets list' '
    + 	test_cmp expect output
    + '
    + 
    ++cat >expect <<\EOF
    ++boolean: 0
    ++integer: 0
    ++magnitude: 0
    ++timestamp: 0
    ++string: (not set)
    ++abbrev: 7
    ++verbose: -1
    ++quiet: 0
    ++dry run: no
    ++file: (not set)
    ++vector: foo
    ++vector: bar
    ++vector: baz
    ++EOF
    ++test_expect_success '--vector keeps list of strings' '
    ++	test-tool parse-options --vector foo --vector=bar --vector=baz >output &&
    ++	test_cmp expect output
    ++'
    ++
    ++test_expect_success '--no-vector resets list' '
    ++	test-tool parse-options --vector=other --vector=irrelevant --vector=options \
    ++		--no-vector --vector=foo --vector=bar --vector=baz >output &&
    ++	test_cmp expect output
    ++'
    ++
    + test_expect_success 'multiple quiet levels' '
    + 	test-tool parse-options --expect="quiet: 3" -q -q -q
    + '
 8:  0dc9284057 !  7:  27dd8e3edf hook: add 'run' subcommand
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    +
    + ## Notes ##
    +    Since v7, added support for "error" hook.runHookDir setting.
    +
    +    Since v4, updated the docs, and did less local application of single
    +    quotes. In order for hookdir hooks to run successfully with a space in
    +    the path, though, they must not be run with 'sh -c'. So we can treat the
    +    hookdir hooks specially, and warn users via doc about special
    +    considerations for configured hooks with spaces in their path.
    +
      ## Documentation/git-hook.txt ##
     @@ Documentation/git-hook.txt: SYNOPSIS
      --------
    @@ hook.c
      
      void free_hook(struct hook *ptr)
      {
    +-	if (ptr) {
    ++	if (ptr)
    + 		strbuf_release(&ptr->command);
    +-		free(ptr);
    +-	}
    ++	free(ptr);
    + }
    + 
    + static struct hook * find_hook_by_command(struct list_head *head, const char *command)
     @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)
      	return HOOKDIR_UNKNOWN;
      }
    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)
     +
     +	switch (cfg)
     +	{
    -+		case HOOKDIR_ERROR:
    -+			fprintf(stderr, _("Skipping legacy hook at '%s'\n"),
    -+				path);
    -+			/* FALLTHROUGH */
    -+		case HOOKDIR_NO:
    -+			return 0;
    -+		case HOOKDIR_WARN:
    -+			fprintf(stderr, _("Running legacy hook at '%s'\n"),
    -+				path);
    -+			return 1;
    -+		case HOOKDIR_INTERACTIVE:
    -+			do {
    -+				/*
    -+				 * TRANSLATORS: Make sure to include [Y] and [n]
    -+				 * in your translation. Only English input is
    -+				 * accepted. Default option is "yes".
    -+				 */
    -+				fprintf(stderr, _("Run '%s'? [Yn] "), path);
    -+				git_read_line_interactively(&prompt);
    -+				strbuf_tolower(&prompt);
    -+				if (starts_with(prompt.buf, "n")) {
    -+					strbuf_release(&prompt);
    -+					return 0;
    -+				} else if (starts_with(prompt.buf, "y")) {
    -+					strbuf_release(&prompt);
    -+					return 1;
    -+				}
    -+				/* otherwise, we didn't understand the input */
    -+			} while (prompt.len); /* an empty reply means "Yes" */
    -+			strbuf_release(&prompt);
    -+			return 1;
    -+		/*
    -+		 * HOOKDIR_UNKNOWN should match the default behavior, but let's
    -+		 * give a heads up to the user.
    -+		 */
    -+		case HOOKDIR_UNKNOWN:
    -+			fprintf(stderr,
    -+				_("Unrecognized value for 'hook.runHookDir'. "
    -+				  "Is there a typo? "));
    -+			/* FALLTHROUGH */
    -+		case HOOKDIR_YES:
    -+		default:
    -+			return 1;
    ++	case HOOKDIR_ERROR:
    ++		fprintf(stderr, _("Skipping legacy hook at '%s'\n"),
    ++			path);
    ++		/* FALLTHROUGH */
    ++	case HOOKDIR_NO:
    ++		return 0;
    ++	case HOOKDIR_WARN:
    ++		fprintf(stderr, _("Running legacy hook at '%s'\n"),
    ++			path);
    ++		return 1;
    ++	case HOOKDIR_INTERACTIVE:
    ++		do {
    ++			/*
    ++			 * TRANSLATORS: Make sure to include [Y] and [n]
    ++			 * in your translation. Only English input is
    ++			 * accepted. Default option is "yes".
    ++			 */
    ++			fprintf(stderr, _("Run '%s'? [Y/n] "), path);
    ++			git_read_line_interactively(&prompt);
    ++			/*
    ++			 * In case of prompt = '' - that is, user hit enter,
    ++			 * saying "yes I want the default" - strncasecmp will
    ++			 * return 0 regardless. So list the default first.
    ++			 *
    ++			 * Case insensitively, accept "y", "ye", or "yes" as
    ++			 * "yes"; accept "n" or "no" as "no".
    ++			 */
    ++			if (!strncasecmp(prompt.buf, "yes", prompt.len)) {
    ++				strbuf_release(&prompt);
    ++				return 1;
    ++			} else if (!strncasecmp(prompt.buf, "no", prompt.len)) {
    ++				strbuf_release(&prompt);
    ++				return 0;
    ++			}
    ++			/* otherwise, we didn't understand the input */
    ++		} while (prompt.len); /* an empty reply means default (yes) */
    ++		return 1;
    ++	/*
    ++	 * HOOKDIR_UNKNOWN should match the default behavior, but let's
    ++	 * give a heads up to the user.
    ++	 */
    ++	case HOOKDIR_UNKNOWN:
    ++		fprintf(stderr,
    ++			_("Unrecognized value for 'hook.runHookDir'. "
    ++			  "Is there a typo? "));
    ++		/* FALLTHROUGH */
    ++	case HOOKDIR_YES:
    ++	default:
    ++		return 1;
     +	}
     +}
     +
    - struct list_head* hook_list(const struct strbuf* hookname)
    + struct list_head* hook_list(const char* hookname)
      {
      	struct strbuf hook_key = STRBUF_INIT;
    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
    +@@ hook.c: struct list_head* hook_list(const char* hookname)
      	strbuf_release(&hook_key);
      	return hook_head;
      }
    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
     +	strvec_clear(&o->args);
     +}
     +
    -+static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
    ++static void prepare_hook_cp(const char *hookname, struct hook *hook,
    ++			    struct run_hooks_opt *options,
     +			    struct child_process *cp)
     +{
     +	if (!hook)
    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
     +	cp->no_stdin = 1;
     +	cp->env = options->env.v;
     +	cp->stdout_to_stderr = 1;
    -+	cp->trace2_hook_name = hook->command.buf;
    ++	cp->trace2_hook_name = hookname;
     +
     +	/*
     +	 * Commands from the config could be oneliners, but we know
    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
     +
     +int run_hooks(const char *hookname, struct run_hooks_opt *options)
     +{
    -+	struct strbuf hookname_str = STRBUF_INIT;
     +	struct list_head *to_run, *pos = NULL, *tmp = NULL;
     +	int rc = 0;
     +
     +	if (!options)
     +		BUG("a struct run_hooks_opt must be provided to run_hooks");
     +
    -+	strbuf_addstr(&hookname_str, hookname);
    -+
    -+	to_run = hook_list(&hookname_str);
    ++	to_run = hook_list(hookname);
     +
     +	list_for_each_safe(pos, tmp, to_run) {
     +		struct child_process hook_proc = CHILD_PROCESS_INIT;
    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
     +		    !should_include_hookdir(hook->command.buf, options->run_hookdir))
     +			continue;
     +
    -+		prepare_hook_cp(hook, options, &hook_proc);
    ++		prepare_hook_cp(hookname, hook, options, &hook_proc);
     +
     +		rc |= run_command(&hook_proc);
     +	}
    @@ hook.h: enum hookdir_opt
     +void run_hooks_opt_init(struct run_hooks_opt *o);
     +void run_hooks_opt_clear(struct run_hooks_opt *o);
     +
    -+/*
    + /*
    +- * Provides the hookdir_opt specified in the config without consulting any
    +- * command line arguments.
     + * Runs all hooks associated to the 'hookname' event in order. Each hook will be
     + * passed 'env' and 'args'.
    -+ */
    +  */
    +-enum hookdir_opt configured_hookdir_opt(void);
     +int run_hooks(const char *hookname, struct run_hooks_opt *options);
    -+
    + 
      /* Free memory associated with a 'struct hook' */
      void free_hook(struct hook *ptr);
    - /* Empties the list at 'head', calling 'free_hook()' on each entry */
     
      ## t/t1360-config-based-hooks.sh ##
     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = no is respected by list' '
      
      	git hook list pre-commit >actual &&
      	# the hookdir annotation is translated
    --	test_i18ncmp expected actual
    -+	test_i18ncmp expected actual &&
    +-	test_cmp expected actual
    ++	test_cmp expected actual &&
     +
     +	git hook run pre-commit 2>actual &&
     +	test_must_be_empty actual
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = error is r
      
      	git hook list pre-commit >actual &&
      	# the hookdir annotation is translated
    -+	test_i18ncmp expected actual &&
    ++	test_cmp expected actual &&
     +
     +	cat >expected <<-EOF &&
     +	Skipping legacy hook at '\''$(pwd)/.git/hooks/pre-commit'\''
     +	EOF
     +
     +	git hook run pre-commit 2>actual &&
    - 	test_i18ncmp expected actual
    + 	test_cmp expected actual
      '
      
     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is respected by list' '
      
      	git hook list pre-commit >actual &&
      	# the hookdir annotation is translated
    -+	test_i18ncmp expected actual &&
    ++	test_cmp expected actual &&
     +
     +	cat >expected <<-EOF &&
     +	Running legacy hook at '\''$(pwd)/.git/hooks/pre-commit'\''
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is re
     +	EOF
     +
     +	git hook run pre-commit 2>actual &&
    - 	test_i18ncmp expected actual
    + 	test_cmp expected actual
      '
      
     @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list removes skipped inlined hook' '
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = interactiv
      
      	git hook list pre-commit >actual &&
      	# the hookdir annotation is translated
    --	test_i18ncmp expected actual
    -+	test_i18ncmp expected actual &&
    ++	test_cmp expected actual &&
     +
     +	test_write_lines n | git hook run pre-commit 2>actual &&
     +	! grep "Legacy Hook" actual &&
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = interactiv
     +	test_cmp expected actual
     +'
     +
    ++test_expect_success 'git hook run can pass args and env vars' '
    ++	write_script sample-hook.sh <<-\EOF &&
    ++	echo $1
    ++	echo $2
    ++	echo $TEST_ENV_1
    ++	echo $TEST_ENV_2
    ++	EOF
    ++
    ++	test_config hook.pre-commit.command "\"$(pwd)/sample-hook.sh\"" &&
    ++
    ++	cat >expected <<-EOF &&
    ++	arg1
    ++	arg2
    ++	env1
    ++	env2
    ++	EOF
    ++
    ++	git hook run --arg arg1 \
    ++		--env TEST_ENV_1=env1 \
    ++		-a arg2 \
    ++		-e TEST_ENV_2=env2 \
    ++		pre-commit 2>actual &&
    ++
    + 	test_cmp expected actual
    + '
    + 
     +test_expect_success 'hookdir hook included in git hook run' '
     +	setup_hookdir &&
     +
    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = interactiv
     +	setup_hooks &&
     +
     +	nongit test_must_fail git hook run pre-commit
    - '
    - 
    ++'
    ++
      test_expect_success 'hook.runHookDir is tolerant to unknown values' '
    + 	setup_hookdir &&
    + 
 9:  92c67ed9da !  8:  46975c11c8 hook: introduce hook_exists()
    @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)
      	strvec_clear(&o->env);
     
      ## hook.h ##
    -@@ hook.h: struct list_head* hook_list(const struct strbuf *hookname);
    +@@ hook.h: struct list_head* hook_list(const char *hookname);
      
      enum hookdir_opt
      {
10:  9b3bb0b655 !  9:  e11f9e28a3 hook: support passing stdin to hooks
    @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)
      	o->run_hookdir = configured_hookdir_opt();
      }
      
    -@@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
    +@@ hook.c: static void prepare_hook_cp(const char *hookname, struct hook *hook,
      	if (!hook)
      		return;
      
    @@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *opt
     +
      	cp->env = options->env.v;
      	cp->stdout_to_stderr = 1;
    - 	cp->trace2_hook_name = hook->command.buf;
    + 	cp->trace2_hook_name = hookname;
     
      ## hook.h ##
     @@ hook.h: struct run_hooks_opt
    @@ hook.h: int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdi
     
      ## t/t1360-config-based-hooks.sh ##
     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir is tolerant to unknown values' '
    - 	test_i18ncmp expected actual
    + 	test_cmp expected actual
      '
      
     +test_expect_success 'stdin to multiple hooks' '
11:  9933985e78 = 10:  3ceb4156fd run-command: allow stdin for run_processes_parallel
12:  43caafe656 ! 11:  93a47f5242 hook: allow parallel hook execution
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    +
    + ## Notes ##
    +    Per AEvar's request - parallel hook execution on day zero.
    +
    +    In most ways run_processes_parallel() worked great for me - but it didn't
    +    have great support for hooks where we pipe to and from. I had to add this
    +    support later in the series.
    +
    +    Since I modified an existing and in-use library I'd appreciate a keen look on
    +    these patches.
    +
    +     - Emily
    +
      ## Documentation/config/hook.txt ##
     @@ Documentation/config/hook.txt: hook.runHookDir::
      	Controls how hooks contained in your hookdir are executed. Can be any of
    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)
      static int should_include_hookdir(const char *path, enum hookdir_opt cfg)
      {
      	struct strbuf prompt = STRBUF_INIT;
    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
    +@@ hook.c: struct list_head* hook_list(const char* hookname)
      	return hook_head;
      }
      
    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)
      	strvec_clear(&o->args);
      }
      
    --static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
    +-static void prepare_hook_cp(const char *hookname, struct hook *hook,
    +-			    struct run_hooks_opt *options,
     -			    struct child_process *cp)
     +static int pick_next_hook(struct child_process *cp,
     +			  struct strbuf *out,
    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)
     -	cp->env = options->env.v;
     +	cp->env = hook_cb->options->env.v;
      	cp->stdout_to_stderr = 1;
    - 	cp->trace2_hook_name = hook->command.buf;
    +-	cp->trace2_hook_name = hookname;
    ++	cp->trace2_hook_name = hook_cb->hookname;
      
    -@@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,
    + 	/*
    + 	 * Commands from the config could be oneliners, but we know
    +@@ hook.c: static void prepare_hook_cp(const char *hookname, struct hook *hook,
      	 * add passed-in argv, without expanding - let the user get back
      	 * exactly what they put in
      	 */
    @@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *opt
      
      int run_hooks(const char *hookname, struct run_hooks_opt *options)
      {
    - 	struct strbuf hookname_str = STRBUF_INIT;
      	struct list_head *to_run, *pos = NULL, *tmp = NULL;
     -	int rc = 0;
    -+	struct hook_cb_data cb_data = { 0, NULL, NULL, options };
    ++	struct hook_cb_data cb_data = { 0, hookname, NULL, NULL, options };
      
      	if (!options)
      		BUG("a struct run_hooks_opt must be provided to run_hooks");
     @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)
    - 	to_run = hook_list(&hookname_str);
    + 	to_run = hook_list(hookname);
      
      	list_for_each_safe(pos, tmp, to_run) {
     -		struct child_process hook_proc = CHILD_PROCESS_INIT;
    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)
     +	if (list_empty(to_run))
     +		return 0;
      
    --		prepare_hook_cp(hook, options, &hook_proc);
    +-		prepare_hook_cp(hookname, hook, options, &hook_proc);
     +	cb_data.head = to_run;
     +	cb_data.run_me = list_entry(to_run->next, struct hook, list);
      
    @@ hook.h: struct run_hooks_opt
     + */
     +struct hook_cb_data {
     +	int rc;
    ++	const char *hookname;
     +	struct list_head *head;
     +	struct hook *run_me;
     +	struct run_hooks_opt *options;
13:  2e189a7566 ! 12:  7f8c886d3f hook: allow specifying working directory for hooks
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    +
    + ## Notes ##
    +    Needed later for "post-checkout" conversion.
    +
      ## hook.c ##
     @@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)
      	o->path_to_stdin = NULL;
    @@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)
     @@ hook.c: 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->command.buf;
    + 	cp->trace2_hook_name = hook_cb->hookname;
     +	cp->dir = hook_cb->options->dir;
      
      	/*
14:  07899ad346 = 13:  8930baa9db run-command: add stdin callback for parallelization
15:  d3f18e433f ! 14:  d0f362591a hook: provide stdin by string_list or callback
    @@ Commit message
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
      ## hook.c ##
    -@@ hook.c: void free_hook(struct hook *ptr)
    +@@
    + 
    + void free_hook(struct hook *ptr)
      {
    - 	if (ptr) {
    +-	if (ptr)
    ++	if (ptr) {
      		strbuf_release(&ptr->command);
     +		free(ptr->feed_pipe_cb_data);
    - 		free(ptr);
    - 	}
    ++	}
    + 	free(ptr);
      }
    + 
     @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *command)
      		strbuf_init(&to_add->command, 0);
      		strbuf_addstr(&to_add->command, command);
    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)
     +	if (options->path_to_stdin && options->feed_pipe)
     +		BUG("choose only one method to populate stdin");
     +
    - 	strbuf_addstr(&hookname_str, hookname);
    + 	to_run = hook_list(hookname);
      
    - 	to_run = hook_list(&hookname_str);
    + 	list_for_each_safe(pos, tmp, to_run) {
     @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)
      	run_processes_parallel_tr2(options->jobs,
      				   pick_next_hook,
16:  417c3f054e ! 15:  83bbb405a5 run-command: allow capturing of collated output
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    +
    + ## Notes ##
    +    Originally when writing this patch I attempted to use a pipe in memory -
    +    but managing its lifetime was actually pretty tricky, and I found I could
    +    achieve the same thing with less code by doing it this way. Critique welcome,
    +    including "no, you really need to do it with a pipe".
    +
      ## builtin/fetch.c ##
     @@ builtin/fetch.c: static int fetch_multiple(struct string_list *list, int max_children)
      		result = run_processes_parallel_tr2(max_children,
17:  c0f3471bd1 ! 16:  73ed5de54c hooks: allow callers to capture output
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    +
    + ## Notes ##
    +    You can see this in practice in the conversions for some of the push hooks,
    +    like 'receive-pack'.
    +
      ## hook.c ##
     @@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)
      	o->dir = NULL;
18:  13446e4273 = 17:  900c4d8963 commit: use config-based hooks
19:  9380c43180 ! 18:  a562120e22 am: convert applypatch hooks to use config
    @@ builtin/am.c: static void am_destroy(const struct am_state *state)
      {
      	int ret;
     +	struct run_hooks_opt opt;
    ++
     +	run_hooks_opt_init_sync(&opt);
      
      	assert(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;
     +	struct run_hooks_opt hook_opt;
    ++
     +	run_hooks_opt_init_async(&hook_opt);
      
     -	if (run_hook_le(NULL, "pre-applypatch", NULL))
    -+	if (run_hooks("pre-applypatch", &hook_opt))
    ++	if (run_hooks("pre-applypatch", &hook_opt)) {
    ++		run_hooks_opt_clear(&hook_opt);
      		exit(1);
    ++	}
    ++
    ++	run_hooks_opt_clear(&hook_opt);
      
      	if (write_cache_as_tree(&tree, 0, NULL))
    + 		die(_("git write-tree failed to write a tree"));
     @@ builtin/am.c: static void do_commit(const struct am_state *state)
      		fclose(fp);
      	}
      
     -	run_hook_le(NULL, "post-applypatch", NULL);
    ++	run_hooks_opt_init_async(&hook_opt);
     +	run_hooks("post-applypatch", &hook_opt);
      
     +	run_hooks_opt_clear(&hook_opt);
20:  316a605606 ! 19:  e841ed4384 merge: use config-based hooks for post-merge hook
    @@ builtin/merge.c: static void finish(struct commit *head_commit,
      	struct strbuf reflog_message = STRBUF_INIT;
     +	struct run_hooks_opt opt;
      	const struct object_id *head = &head_commit->object.oid;
    -+	run_hooks_opt_init_async(&opt);
      
      	if (!msg)
    - 		strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION"));
     @@ 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);
    ++	run_hooks_opt_init_async(&opt);
     +	strvec_push(&opt.args, squash ? "1" : "0");
     +	run_hooks("post-merge", &opt);
     +	run_hooks_opt_clear(&opt);
21:  a5132f14b3 ! 20:  7e99398f7d gc: use hook library for pre-auto-gc hook
    @@ builtin/gc.c: static void add_repack_incremental_option(void)
      static int need_to_gc(void)
      {
     +	struct run_hooks_opt hook_opt;
    -+	run_hooks_opt_init_async(&hook_opt);
    ++
      	/*
      	 * Setting gc.auto to 0 or negative can disable the
      	 * automatic gc.
    @@ builtin/gc.c: static int need_to_gc(void)
      		return 0;
      
     -	if (run_hook_le(NULL, "pre-auto-gc", NULL))
    -+	if (run_hooks("pre-auto-gc", &hook_opt))
    ++	run_hooks_opt_init_async(&hook_opt);
    ++	if (run_hooks("pre-auto-gc", &hook_opt)) {
    ++		run_hooks_opt_clear(&hook_opt);
      		return 0;
    ++	}
    ++	run_hooks_opt_clear(&hook_opt);
      	return 1;
      }
    + 
22:  01f1331cc9 ! 21:  5423217ef2 rebase: teach pre-rebase to use hook.h
    @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
      	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)
    - 	};
    - 	int i;
    - 
    -+	run_hooks_opt_init_async(&hook_opt);
    -+
    - 	if (argc == 2 && !strcmp(argv[1], "-h"))
    - 		usage_with_options(builtin_rebase_usage,
    - 				   builtin_rebase_options);
     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
      	}
      
      	/* If a hook exists, give it a chance to interrupt*/
    ++	run_hooks_opt_init_async(&hook_opt);
     +	strvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);
      	if (!ok_to_skip_pre_rebase &&
     -	    run_hook_le(NULL, "pre-rebase", options.upstream_arg,
     -			argc ? argv[0] : NULL, NULL))
    -+	    run_hooks("pre-rebase", &hook_opt))
    ++	    run_hooks("pre-rebase", &hook_opt)) {
    ++		run_hooks_opt_clear(&hook_opt);
      		die(_("The pre-rebase hook refused to rebase."));
    ++	}
    ++	run_hooks_opt_clear(&hook_opt);
      
      	if (options.flags & REBASE_DIFFSTAT) {
    -@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
    - 	ret = !!run_specific_rebase(&options, action);
    - 
    - cleanup:
    -+	run_hooks_opt_clear(&hook_opt);
    - 	strbuf_release(&buf);
    - 	strbuf_release(&revisions);
    - 	free(options.head_name);
    + 		struct diff_options opts;
23:  85a7721adc ! 22:  1c0c5ad129 read-cache: convert post-index-change hook to use config
    @@ Documentation/githooks.txt: and "0" meaning they were not.
     
      ## read-cache.c ##
     @@
    - #include "fsmonitor.h"
      #include "thread-utils.h"
      #include "progress.h"
    + #include "sparse-index.h"
     +#include "hook.h"
    ++>>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)
      
      /* 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
    - 				 unsigned flags)
      {
      	int ret;
    + 	int was_full = !istate->sparse_index;
     +	struct run_hooks_opt hook_opt;
    -+	run_hooks_opt_init_async(&hook_opt);
      
    - 	/*
    - 	 * TODO trace2: replace "the_repository" with the actual repo instance
    + 	ret = convert_to_sparse(istate);
    + 
     @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struct lock_file *l
      	else
      		ret = close_lock_file_gently(lock);
    @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struc
     -	run_hook_le(NULL, "post-index-change",
     -			istate->updated_workdir ? "1" : "0",
     -			istate->updated_skipworktree ? "1" : "0", NULL);
    ++	run_hooks_opt_init_async(&hook_opt);
     +	strvec_pushl(&hook_opt.args,
     +		     istate->updated_workdir ? "1" : "0",
     +		     istate->updated_skipworktree ? "1" : "0",
24:  21ec3e1a9d ! 23:  1193e856e6 receive-pack: convert push-to-checkout hook to hook.h
    @@ builtin/receive-pack.c: static const char *push_to_checkout(unsigned char *hash,
      				    const char *work_tree)
      {
     +	struct run_hooks_opt opt;
    ++
     +	run_hooks_opt_init_sync(&opt);
     +
      	strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
25:  e0405e96ad ! 24:  1817b6851b git-p4: use 'git hook' to run hooks
    @@ Commit message
     
         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
     
    +
    + ## Notes ##
    +    Maybe there is a better way to do this - I had a hard time getting this to run
    +    locally, and Python is not my forte, so if anybody has a better approach I'd
    +    love to just take that patch instead :)
    +
    +    Since v6, removed the developer debug print statements.... :X
    +
    +    Maybe there is a better way to do this - I had a hard time getting this to run
    +    locally, and Python is not my forte, so if anybody has a better approach I'd
    +    love to just take that patch instead :)
    +
      ## git-p4.py ##
     @@ git-p4.py: def decode_path(path):
      
26:  c52578e078 ! 25:  b3a354e4a8 hooks: convert 'post-checkout' hook to hook library
    @@ builtin/checkout.c: struct branch_info {
      			      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),
    +-			   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;
     +	int rc;
    @@ builtin/checkout.c: struct branch_info {
      	   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),
    ++		     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);
     +	rc = run_hooks("post-checkout", &opt);
    @@ builtin/clone.c: static int checkout(int submodule_progress)
      	struct tree_desc t;
      	int err = 0;
     +	struct run_hooks_opt hook_opt;
    -+	run_hooks_opt_init_sync(&hook_opt);
      
      	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),
    +-	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);
    ++	run_hooks_opt_init_sync(&hook_opt);
    ++	strvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), "1", NULL);
     +	err |= run_hooks("post-checkout", &hook_opt);
     +	run_hooks_opt_clear(&hook_opt);
      
    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam
     -			cp.argv = NULL;
     -			cp.trace2_hook_name = "post-checkout";
     -			strvec_pushl(&cp.args, absolute_path(hook),
    --				     oid_to_hex(&null_oid),
    +-				     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_sync(&opt);
     +
     +		strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
     +		strvec_pushl(&opt.args,
    -+			     oid_to_hex(&null_oid),
    ++			     oid_to_hex(null_oid()),
     +			     oid_to_hex(&commit->object.oid),
     +			     "1",
     +			     NULL);
    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam
      
      	strvec_clear(&child_env);
     
    + ## read-cache.c ##
    +@@
    + #include "progress.h"
    + #include "sparse-index.h"
    + #include "hook.h"
    +->>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)
    + 
    + /* Mask for the name length in ce_flags in the on-disk index */
    + 
    +
      ## reset.c ##
     @@
      #include "tree-walk.h"
    @@ reset.c: int reset_head(struct repository *r, struct object_id *oid, const char
      	}
     -	if (run_hook)
     -		run_hook_le(NULL, "post-checkout",
    --			    oid_to_hex(orig ? orig : &null_oid),
    +-			    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_sync(&opt);
     +		strvec_pushl(&opt.args,
    -+			     oid_to_hex(orig ? orig : &null_oid),
    ++			     oid_to_hex(orig ? orig : null_oid()),
     +			     oid_to_hex(oid),
     +			     "1",
     +			     NULL);
27:  316cb6f584 ! 26:  692352f9aa hook: convert 'post-rewrite' hook to config
    @@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)
     -	const char *hook = find_hook("post-rewrite");
     +	struct run_hooks_opt opt;
      	int ret;
    -+	run_hooks_opt_init_async(&opt);
      
     -	if (!hook)
     -		return 0;
    --
    ++	run_hooks_opt_init_async(&opt);
    + 
     -	strvec_push(&cp.args, hook);
     -	strvec_push(&cp.args, "rebase");
    --
    --	cp.in = xopen(am_path(state, "rewritten"), O_RDONLY);
    --	cp.stdout_to_stderr = 1;
    --	cp.trace2_hook_name = "post-rewrite";
     +	strvec_push(&opt.args, "rebase");
     +	opt.path_to_stdin = am_path(state, "rewritten");
      
    --	ret = run_command(&cp);
    +-	cp.in = xopen(am_path(state, "rewritten"), O_RDONLY);
    +-	cp.stdout_to_stderr = 1;
    +-	cp.trace2_hook_name = "post-rewrite";
     +	ret = run_hooks("post-rewrite", &opt);
      
    +-	ret = run_command(&cp);
    +-
     -	close(cp.in);
     +	run_hooks_opt_clear(&opt);
      	return ret;
    @@ sequencer.c: int update_head_with_reflog(const struct commit *old_head,
     +	struct string_list to_stdin = STRING_LIST_INIT_DUP;
      	int code;
     -	struct strbuf sb = STRBUF_INIT;
    -+	run_hooks_opt_init_async(&opt);
      
     -	argv[0] = find_hook("post-rewrite");
     -	if (!argv[0])
     -		return 0;
    -+	strvec_push(&opt.args, "amend");
    ++	run_hooks_opt_init_async(&opt);
      
     -	argv[1] = "amend";
     -	argv[2] = NULL;
    @@ sequencer.c: int update_head_with_reflog(const struct commit *old_head,
     -	strbuf_release(&sb);
     -	sigchain_pop(SIGPIPE);
     -	return finish_command(&proc);
    ++	strvec_push(&opt.args, "amend");
    ++
     +	strbuf_addf(&tmp,
     +		    "%s %s",
     +		    oid_to_hex(oldoid),
    @@ sequencer.c: static int pick_commits(struct repository *r,
     -			strvec_push(&child.args, "--for-rewrite=rebase");
     +			struct child_process notes_cp = CHILD_PROCESS_INIT;
     +			struct run_hooks_opt hook_opt;
    ++
     +			run_hooks_opt_init_async(&hook_opt);
     +
     +			notes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);
28:  0ab776068d = 27:  964011dfdd transport: convert pre-push hook to use config
29:  601dada804 ! 28:  c04822add9 reference-transaction: look for hooks in config
    @@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'interleaving hook calls s
      	EOF
      
      	git push ./target-repo.git PRE POST &&
    +
    + ## transport.c ##
    +@@ transport.c: static int run_pre_push_hook(struct transport *transport,
    + 	struct strbuf tmp = STRBUF_INIT;
    + 	struct ref *r;
    + 	struct string_list to_stdin = STRING_LIST_INIT_DUP;
    ++
    + 	run_hooks_opt_init_async(&opt);
    + 
    + 	strvec_push(&opt.args, transport->remote->name);
30:  d60f2b146e ! 29:  ddc6f56bec receive-pack: convert 'update' hook to hook.h
    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,
      	return status;
      }
      
    --static int run_update_hook(struct command *cmd)
     +static void hook_output_to_sideband(struct strbuf *output, void *cb_data)
    - {
    --	const char *argv[5];
    --	struct child_process proc = CHILD_PROCESS_INIT;
    --	int code;
    ++{
     +	int keepalive_active = 0;
    - 
    --	argv[0] = find_hook("update");
    --	if (!argv[0])
    --		return 0;
    ++
     +	if (keepalive_in_sec <= 0)
     +		use_keepalive = KEEPALIVE_NEVER;
     +	if (use_keepalive == KEEPALIVE_ALWAYS)
     +		keepalive_active = 1;
    - 
    --	argv[1] = cmd->ref_name;
    --	argv[2] = oid_to_hex(&cmd->old_oid);
    --	argv[3] = oid_to_hex(&cmd->new_oid);
    --	argv[4] = NULL;
    ++
     +	/* send a keepalive if there is no data to write */
     +	if (keepalive_active && !output->len) {
     +		static const char buf[] = "0005\1";
     +		write_or_die(1, buf, sizeof(buf) - 1);
     +		return;
     +	}
    - 
    --	proc.no_stdin = 1;
    --	proc.stdout_to_stderr = 1;
    --	proc.err = use_sideband ? -1 : 0;
    --	proc.argv = argv;
    --	proc.trace2_hook_name = "update";
    ++
     +	if (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {
     +		const char *first_null = memchr(output->buf, '\0', output->len);
     +		if (first_null) {
    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,
     +	send_sideband(1, 2, output->buf, output->len, use_sideband);
     +}
     +
    -+static int run_update_hook(struct command *cmd)
    -+{
    + static int run_update_hook(struct command *cmd)
    + {
    +-	const char *argv[5];
    +-	struct child_process proc = CHILD_PROCESS_INIT;
     +	struct run_hooks_opt opt;
    -+	int code;
    + 	int code;
    + 
    +-	argv[0] = find_hook("update");
    +-	if (!argv[0])
    +-		return 0;
     +	run_hooks_opt_init_async(&opt);
    -+
    + 
    +-	argv[1] = cmd->ref_name;
    +-	argv[2] = oid_to_hex(&cmd->old_oid);
    +-	argv[3] = oid_to_hex(&cmd->new_oid);
    +-	argv[4] = NULL;
     +	strvec_pushl(&opt.args,
     +		     cmd->ref_name,
     +		     oid_to_hex(&cmd->old_oid),
     +		     oid_to_hex(&cmd->new_oid),
     +		     NULL);
      
    +-	proc.no_stdin = 1;
    +-	proc.stdout_to_stderr = 1;
    +-	proc.err = use_sideband ? -1 : 0;
    +-	proc.argv = argv;
    +-	proc.trace2_hook_name = "update";
    +-
     -	code = start_command(&proc);
     -	if (code)
     -		return code;
31:  1e6898670b ! 30:  e1e810869f proc-receive: acquire hook list from hook.h
    @@ builtin/receive-pack.c: static int run_proc_receive_hook(struct command *command
      
     -	argv[0] = find_hook("proc-receive");
     -	if (!argv[0]) {
    -+	struct strbuf hookname = STRBUF_INIT;
     +	struct hook *proc_receive = NULL;
     +	struct list_head *pos, *hooks;
     +
    -+	strbuf_addstr(&hookname, "proc-receive");
    -+	hooks = hook_list(&hookname);
    ++	hooks = hook_list("proc-receive");
     +
     +	list_for_each(pos, hooks) {
     +		if (proc_receive) {
32:  012e3a7a79 ! 31:  b8be5a2288 post-update: use hook.h library
    @@ builtin/receive-pack.c: static const char *update(struct command *cmd, struct sh
      	struct command *cmd;
     -	struct child_process proc = CHILD_PROCESS_INIT;
     -	const char *hook;
    --
    ++	struct run_hooks_opt opt;
    + 
     -	hook = find_hook("post-update");
     -	if (!hook)
     -		return;
    -+	struct run_hooks_opt opt;
     +	run_hooks_opt_init_async(&opt);
      
      	for (cmd = commands; cmd; cmd = cmd->next) {
33:  2740bcda6d = 32:  1cc1384eae receive-pack: convert receive hooks to hook.h
34:  f201f3af5f = 33:  1bb9a3810c bugreport: use hook_exists instead of find_hook
35:  0956a94cc7 ! 34:  3db7bf3b0d git-send-email: use 'git hook run' for 'sendemail-validate'
    @@ git-send-email.perl: sub unique_email_list {
      	my ($fn, $xfer_encoding) = @_;
      
     -	if ($repo) {
    --		my $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),
    +-		my $validate_hook = catfile($repo->hooks_path(),
     -					    'sendemail-validate');
     -		my $hook_error;
     -		if (-x $validate_hook) {
    @@ git-send-email.perl: sub unique_email_list {
     -			chdir($repo->wc_path() or $repo->repo_path())
     -				or die("chdir: $!");
     -			local $ENV{"GIT_DIR"} = $repo->repo_path();
    --			$hook_error = "rejected by sendemail-validate hook"
    --				if system($validate_hook, $target);
    +-			$hook_error = system_or_msg([$validate_hook, $target]);
     -			chdir($cwd_save) or die("chdir: $!");
     -		}
    --		return $hook_error if $hook_error;
    +-		if ($hook_error) {
    +-			die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
    +-				       "%s\n" .
    +-				       "warning: no patches were sent\n"), $fn, $hook_error);
    +-		}
     -	}
     +	my $target = abs_path($fn);
    -+	return "rejected by sendemail-validate hook"
    -+		if system(("git", "hook", "run", "sendemail-validate", "-a",
    -+				$target));
    ++
    ++	system_or_die(["git", "hook", "run", "sendemail-validate", "-j1", "-a", $target],
    ++		sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
    ++			   "warning: no patches were sent\n"),
    ++		        $fn));
      
      	# Any long lines will be automatically fixed if we use a suitable transfer
      	# encoding.
     
      ## t/t9001-send-email.sh ##
    +@@ t/t9001-send-email.sh: 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 '"'"'$PWD/my-hooks/sendemail-validate'"'"' died with exit code 1
    + 	warning: no patches were sent
    + 	EOF
    + 	test_cmp expect actual
    +@@ t/t9001-send-email.sh: 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 '"'"'$PWD/my-hooks/sendemail-validate'"'"' died with exit code 1
    + 	warning: no patches were sent
    + 	EOF
    + 	test_cmp expect actual
     @@ t/t9001-send-email.sh: test_expect_success $PREREQ 'invoke hook' '
      	mkdir -p .git/hooks &&
      
36:  7f05b25392 ! 35:  d2a477d9e3 run-command: stop thinking about hooks
    @@ hook.c: static int should_include_hookdir(const char *path, enum hookdir_opt cfg
     +}
     +
     +
    - struct list_head* hook_list(const struct strbuf* hookname)
    + struct list_head* hook_list(const char* hookname)
      {
      	struct strbuf hook_key = STRBUF_INIT;
    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)
    +@@ hook.c: struct list_head* hook_list(const char* hookname)
      	git_config(hook_config_lookup, &cb_data);
      
      	if (have_git_dir()) {
    --		const char *legacy_hook_path = find_hook(hookname->buf);
    -+		const char *legacy_hook_path = find_legacy_hook(hookname->buf);
    +-		const char *legacy_hook_path = find_hook(hookname);
    ++		const char *legacy_hook_path = find_legacy_hook(hookname);
      
      		/* Unconditionally add legacy hook, but annotate it. */
      		if (legacy_hook_path) {
37:  e9b1f847f2 <  -:  ---------- docs: unify githooks and git-hook manpages
 -:  ---------- > 36:  62a3e3b419 doc: clarify fsmonitor-watchman specification
 -:  ---------- > 37:  5c864de1aa docs: link githooks and git-hook manpages

-- 
2.31.1.818.g46aad6cb9e-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v9 01/37] doc: propose hooks managed by the config
  2021-05-27  0:08 ` [PATCH v9 00/37] propose " Emily Shaffer
@ 2021-05-27  0:08   ` Emily Shaffer
  2021-05-27  0:08   ` [PATCH v9 02/37] hook: introduce git-hook subcommand Emily Shaffer
                     ` (37 subsequent siblings)
  38 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-05-27  0:08 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Begin a design document for config-based hooks, managed via git-hook.
Focus on an overview of the implementation and motivation for design
decisions. Briefly discuss the alternatives considered before this
point. Also, attempt to redefine terms to fit into a multihook world.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Since v6, checked for inconsistencies with implementation and added lots of
    caveats about whether 'git hook add' and 'git hook edit' will ever materialize.
    
    Hopefully this reflects reality now; please review accordingly.
    
    Since v6, checked for inconsistencies with implementation and added lots of
    caveats about whether 'git hook add' and 'git hook edit' will ever materialize.
    
    Hopefully this reflects reality now; please review accordingly.
    
    Since v4, addressed comments from Jonathan Tan about wording. However, I have
    not addressed AEvar's comments or done a full re-review of this document.
    I wanted to get the rest of the series out for initial review first.
    
     - Emily
    
    Since v4, addressed comments from Jonathan Tan about wording.

 Documentation/Makefile                        |   1 +
 .../technical/config-based-hooks.txt          | 369 ++++++++++++++++++
 2 files changed, 370 insertions(+)
 create mode 100644 Documentation/technical/config-based-hooks.txt

diff --git a/Documentation/Makefile b/Documentation/Makefile
index 2aae4c9cbb..5d19eddb0e 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -90,6 +90,7 @@ SP_ARTICLES += $(API_DOCS)
 TECH_DOCS += MyFirstContribution
 TECH_DOCS += MyFirstObjectWalk
 TECH_DOCS += SubmittingPatches
+TECH_DOCS += technical/config-based-hooks
 TECH_DOCS += technical/hash-function-transition
 TECH_DOCS += technical/http-protocol
 TECH_DOCS += technical/index-format
diff --git a/Documentation/technical/config-based-hooks.txt b/Documentation/technical/config-based-hooks.txt
new file mode 100644
index 0000000000..1f973117e4
--- /dev/null
+++ b/Documentation/technical/config-based-hooks.txt
@@ -0,0 +1,369 @@
+Configuration-based hook management
+===================================
+:sectanchors:
+
+[[motivation]]
+== Motivation
+
+Replace the `.git/hook/hookname` path as the only source of hooks to execute;
+allow users to define hooks using config files, in a way which is friendly to
+users with multiple repos which have similar needs - hooks can be easily shared
+between multiple Git repos.
+
+Redefine "hook" as an event rather than a single script, allowing users to
+perform multiple unrelated actions on a single event.
+
+Make it easier for users to discover Git's hook feature and automate their
+workflows.
+
+[[user-interfaces]]
+== User interfaces
+
+[[config-schema]]
+=== Config schema
+
+Hooks can be introduced by editing the configuration manually. There are two new
+sections added, `hook` and `hookcmd`.
+
+[[config-schema-hook]]
+==== `hook`
+
+Primarily contains subsections for each hook event. The order of variables in
+these subsections defines the hook command execution order; hook commands can be
+specified by setting the value directly to the command if no additional
+configuration is needed, or by setting the value as the name of a `hookcmd`. If
+Git does not find a `hookcmd` whose subsection matches the value of the given
+command string, Git will try to execute the string directly. Hooks are executed
+by passing the resolved command string to the shell. In the future, hook event
+subsections could also contain per-hook-event settings; see
+<<per-hook-event-settings,the section in Future Work>> for more details.
+
+Also contains top-level hook execution settings, for example, `hook.runHookDir`.
+(These settings are described more in <<library,Library>>.)
+
+----
+[hook "pre-commit"]
+  command = perl-linter
+  command = /usr/bin/git-secrets --pre-commit
+
+[hook "pre-applypatch"]
+  command = perl-linter
+  # for illustration purposes; error behavior isn't planned yet
+  error = ignore
+
+[hook]
+  runHookDir = interactive
+----
+
+[[config-schema-hookcmd]]
+==== `hookcmd`
+
+Defines a hook command and its attributes, which will be used when a hook event
+occurs. Unqualified attributes are assumed to apply to this hook during all hook
+events, but event-specific attributes can also be supplied. The example runs
+`/usr/bin/lint-it --language=perl <args passed by Git>`, but for repos which
+include this config, the hook command will be skipped for all events.
+Theoretically, the last line could be used to "un-skip" the hook command for
+`pre-commit` hooks, but this hasn't been scoped or implemented yet.
+
+----
+[hookcmd "perl-linter"]
+  command = /usr/bin/lint-it --language=perl
+  skip = true
+  # for illustration purposes; below hasn't been defined yet
+  pre-commit-skip = false
+----
+
+[[command-line-api]]
+=== Command-line API
+
+Users should be able to view, run, reorder, and create hook commands via the
+command line. External tools should be able to view a list of hooks in the
+correct order to run. Modifier commands (`edit` and `add`) have not been
+implemented yet and may not be if manually editing the config proves usable
+enough.
+
+*`git hook list <hook-event>`*
+
+*`git hook run <hook-event> [-a <arg>]... [-e <env-var>]...`*
+
+*`git hook edit <hook-event>`*
+
+*`git hook add <hook-command> <hook-event> <options...>`*
+
+[[hook-editor]]
+=== Hook editor
+
+The tool which is presented by `git hook edit <hook-command>`. Ideally, this
+tool should be easier to use than manually editing the config, and then produce
+a concise config afterwards. It may take a form similar to `git rebase
+--interactive`. This has not been designed or implemented yet and may not be if
+the config proves usable enough.
+
+[[implementation]]
+== Implementation
+
+[[library]]
+=== Library
+
+`hook.c` and `hook.h` are responsible for interacting with the config files. The
+hook library provides a basic API to call all hooks in config order with more
+complex options passed via `struct run_hooks_opt`:
+
+*`int run_hooks(const char *hookname, struct run_hooks_opt *options)`*
+
+`struct run_hooks_opt` allows callers to set:
+
+- environment variables
+- command-line arguments
+- behavior for the hook command provided by `run-command.h:find_hook()` (see
+  below)
+- a method to provide stdin to each hook, either via a file containing stdin, a
+  `struct string_list` containing a list of lines to print, or a callback
+  function to allow the caller to populate stdin manually
+- a method to process stdout from each hook, e.g. for printing to sideband
+  during a network operation
+- parallelism
+- a custom working directory for hooks to execute in
+
+And this struct can be extended with more options as necessary in the future.
+
+The "legacy" hook provided by `run-command.h:find_hook()` - that is, the hook
+present in `.git/hooks/<hookname>` or
+`$(git config --get core.hooksPath)/<hookname>` - can be handled in a number of
+ways, providing an avenue to deprecate these "legacy" hooks if desired. The
+handling is based on a config `hook.runHookDir`, which is checked against a
+number of cases:
+
+- "no": the legacy hook will not be run
+- "error": Git will print a warning to stderr before ignoring the legacy hook
+- "interactive": Git will prompt the user before running the legacy hook
+- "warn": Git will print a warning to stderr before running the legacy hook
+- "yes" (default): Git will silently run the legacy hook
+
+In case this list is expanded in the future, if a value for `hook.runHookDir` is
+given which Git does not recognize, Git should discard that config entry. For
+example, if "warn" was specified at system level and "junk" was specified at
+global level, Git would resolve the value to "warn"; if the only time the config
+was set was to "junk", Git would use the default value of "yes" (but print a
+warning to the user first to let them know their value is wrong).
+
+`struct hookcmd` is expected to grow in size over time as more functionality is
+added to hooks; so that other parts of the code don't need to understand the
+config schema, `struct hookcmd` should contain logical values instead of string
+pairs.
+
+By default, hook parallelism is chosen based on the semantics of each hook;
+callsites initialize their `struct run_hooks_opt` via one of two macros,
+`RUN_HOOKS_OPT_INIT_SYNC` or `RUN_HOOKS_OPT_INIT_ASYNC`. The default number of
+jobs can be configured in `hook.jobs`; this config applies across all hook
+events. If unset, the value of `online_cpus()` (equivalent to `nproc`) is used.
+
+[[builtin]]
+=== Builtin
+
+`builtin/hook.c` is responsible for providing the frontend. It's responsible for
+formatting user-provided data and then calling the library API to set the
+configs as appropriate. The builtin frontend is not responsible for calling the
+config directly, so that other areas of Git can rely on the hook library to
+understand the most recent config schema for hooks.
+
+[[migration]]
+=== Migration path
+
+[[stage-0]]
+==== Stage 0
+
+Hooks are called by running `run-command.h:find_hook()` with the hookname and
+executing the result. The hook library and builtin do not exist. Hooks only
+exist as specially named scripts within `.git/hooks/`.
+
+[[stage-1]]
+==== Stage 1
+
+`git hook list --porcelain <hook-event>` is implemented. `hook.h:run_hooks()` is
+taught to include `run-command.h:find_hook()` at the end; calls to `find_hook()`
+are replaced with calls to `run_hooks()`. Users can opt-in to config-based hooks
+simply by creating some in their config; otherwise users should remain
+unaffected by the change.
+
+[[stage-2]]
+==== Stage 2
+
+The call to `find_hook()` inside of `run_hooks()` learns to check for a config,
+`hook.runHookDir`. Users can opt into managing their hooks completely via the
+config this way.
+
+[[stage-3]]
+==== Stage 3
+
+`.git/hooks` is removed from the template and the hook directory is considered
+deprecated. To avoid breaking older repos, the default of `hook.runHookDir` is
+not changed, and `find_hook()` is not removed.
+
+[[caveats]]
+== Caveats
+
+[[security]]
+=== Security and repo config
+
+Part of the motivation behind this refactor is to mitigate hooks as an attack
+vector.footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/]
+However, as the design stands, users can still provide hooks in the repo-level
+config, which is included when a repo is zipped and sent elsewhere. The
+security of the repo-level config is still under discussion; this design
+generally assumes the repo-level config is secure, which is not true yet. This
+assumption was made to avoid overcomplicating the design. So, this series
+doesn't particularly improve security or resistance to zip attacks.
+
+[[ease-of-use]]
+=== Ease of use
+
+The config schema is nontrivial; that's why it's important for the `git hook`
+modifier commands to be usable. Contributors with UX expertise are encouraged to
+share their suggestions.
+
+[[alternatives]]
+== Alternative approaches
+
+A previous summary of alternatives exists in the
+archives.footnote:[https://lore.kernel.org/git/20191116011125.GG22855@google.com]
+
+The table below shows a number of goals and how they might be achieved with
+config-based hooks, by implementing directory support (i.e.
+'.git/hooks/pre-commit.d'), or as hooks are run today.
+
+.Comparison of alternatives
+|===
+|Feature |Config-based hooks |Hook directories |Status quo
+
+|Supports multiple hooks
+|Natively
+|Natively
+|With user effort
+
+|Supports parallelization
+|Natively
+|Natively
+|No (user's multihook trampoline script would need to handle parallelism)
+
+|Safer for zipped repos
+|A little
+|No
+|No
+
+|Previous hooks just work
+|If configured
+|Yes
+|Yes
+
+|Can install one hook to many repos
+|Yes
+|With symlinks or core.hooksPath
+|With symlinks or core.hooksPath
+
+|Discoverability
+|Findable with 'git help git' or tab-completion via 'git hook' subcommand
+|Findable via improved documentation
+|Same as before
+
+|Hard to run unexpected hook
+|If configured
+|Could be made to warn or look for a config
+|No
+|===
+
+[[status-quo]]
+=== Status quo
+
+Today users can implement multihooks themselves by using a "trampoline script"
+as their hook, and pointing that script to a directory or list of other scripts
+they wish to run.
+
+[[hook-directories]]
+=== Hook directories
+
+Other contributors have suggested Git learn about the existence of a directory
+such as `.git/hooks/<hookname>.d` and execute those hooks in alphabetical order.
+
+[[future-work]]
+== Future work
+
+[[execution-ordering]]
+=== Execution ordering
+
+We may find that config order is insufficient for some users; for example,
+config order makes it difficult to add a new hook to the system or global config
+which runs at the end of the hook list. A new ordering schema should be:
+
+1) Specified by a `hook.order` config, so that users will not unexpectedly see
+their order change;
+
+2) Either dependency or numerically based.
+
+Dependency-based ordering is prone to classic linked-list problems, like a
+cycles and handling of missing dependencies. But, it paves the way for enabling
+parallelization if some tasks truly depend on others.
+
+Numerical ordering makes it tricky for Git to generate suggested ordering
+numbers for each command, but is easy to determine a definitive order.
+
+[[parallelization]]
+=== Parallelization with dependencies
+
+Currently hooks use a naive parallelization scheme or are run in series.  But if
+one hook depends on another's output, then users will want to specify those
+dependencies. If we decide to solve this problem, we may want to look to modern
+build systems for inspiration on how to manage dependencies and parallel tasks.
+
+[[nontrivial-hooks]]
+=== Multihooks and nontrivial output
+
+Some hooks - like 'proc-receive' - don't lend themselves well to multihooks at
+all. In the case of 'proc-receive', for now, multiple hook definitions are
+disallowed. In the future we might be able to conceive a better approach, for
+example, running the hooks in series and using the output from one hook as the
+input to the next.
+
+[[securing-hookdir-hooks]]
+=== Securing hookdir hooks
+
+With the design as written in this doc, it's still possible for a malicious user
+to modify `.git/config` to include `hook.pre-receive.command = rm -rf /`, then
+zip their repo and send it to another user. It may be necessary to teach Git to
+only allow inlined hooks like this if they were configured outside of the local
+scope (in other words, only run hookcmds, and only allow hookcmds to be
+configured in global or system scope); or another approach, like a list of safe
+projects, might be useful. It may also be sufficient (or at least useful) to
+teach a `hook.disableAll` config or similar flag to the Git executable.
+
+[[submodule-inheritance]]
+=== Submodule inheritance
+
+It's possible some submodules may want to run the identical set of hooks that
+their superrepo runs. While a globally-configured hook set is helpful, it's not
+a great solution for users who have multiple repos-with-submodules under the
+same user. It would be useful for submodules to learn how to run hooks from
+their superrepo's config, or inherit that hook setting.
+
+[[per-hook-event-settings]]
+=== Per-hook-event settings
+
+It might be desirable to keep settings specifically for some hook events, but
+not for others - for example, a user may wish to disable hookdir hooks for all
+events but pre-commit, which they haven't had time to convert yet; or, a user
+may wish for execution order settings to differ based on hook event. In that
+case, it would be useful to set something like `hook.pre-commit.executionOrder`
+which would not apply to the 'prepare-commit-msg' hook, for example.
+
+[[glossary]]
+== Glossary
+
+*hook event*
+
+A point during Git's execution where user scripts may be run, for example,
+_prepare-commit-msg_ or _pre-push_.
+
+*hook command*
+
+A user script or executable which will be run on one or more hook events.
-- 
2.31.1.818.g46aad6cb9e-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v9 02/37] hook: introduce git-hook subcommand
  2021-05-27  0:08 ` [PATCH v9 00/37] propose " Emily Shaffer
  2021-05-27  0:08   ` [PATCH v9 01/37] doc: propose hooks managed by the config Emily Shaffer
@ 2021-05-27  0:08   ` Emily Shaffer
  2021-05-27  2:18     ` Junio C Hamano
  2021-05-27  0:08   ` [PATCH v9 03/37] hook: include hookdir hook in list Emily Shaffer
                     ` (36 subsequent siblings)
  38 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-05-27  0:08 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Add a new subcommand, git-hook, which will be used to ease config-based
hook management. This command will handle parsing configs to compose a
list of hooks to run for a given event, as well as adding or modifying
hook configs in an interactive fashion.

Start with 'git hook list <hookname>', which checks the known configs in
order to create an ordered list of hooks to run on a given hook event.

Multiple commands can be specified for a given hook by providing
multiple "hook.<hookname>.command = <path-to-hook>" lines. Hooks will be
run in config order. If more properties need to be set on a given hook
in the future, commands can also be specified by providing
"hook.<hookname>.command = <hookcmd-name>", as well as a "[hookcmd
<hookcmd-name>]" subsection; this subsection should contain a
"hookcmd.<hookcmd-name>.command = <path-to-hook>" line.

For example:

  $ git config --list | grep ^hook
  hook.pre-commit.command=baz
  hook.pre-commit.command=~/bar.sh
  hookcmd.baz.command=~/baz/from/hookcmd.sh

  $ git hook list pre-commit
  global: ~/baz/from/hookcmd.sh
  local: ~/bar.sh

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Since v4, mainly changed to RUN_SETUP_GENTLY so that 'git hook list' can
    be executed outside of a repo.

 .gitignore                    |   1 +
 Documentation/config/hook.txt |   9 +++
 Documentation/git-hook.txt    |  73 +++++++++++++++++++++
 Makefile                      |   2 +
 builtin.h                     |   1 +
 builtin/hook.c                |  65 ++++++++++++++++++
 command-list.txt              |   1 +
 git.c                         |   1 +
 hook.c                        | 120 ++++++++++++++++++++++++++++++++++
 hook.h                        |  25 +++++++
 t/t1360-config-based-hooks.sh |  88 +++++++++++++++++++++++++
 11 files changed, 386 insertions(+)
 create mode 100644 Documentation/config/hook.txt
 create mode 100644 Documentation/git-hook.txt
 create mode 100644 builtin/hook.c
 create mode 100644 hook.c
 create mode 100644 hook.h
 create mode 100755 t/t1360-config-based-hooks.sh

diff --git a/.gitignore b/.gitignore
index 311841f9be..de39dc9961 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/config/hook.txt b/Documentation/config/hook.txt
new file mode 100644
index 0000000000..71449ecbc7
--- /dev/null
+++ b/Documentation/config/hook.txt
@@ -0,0 +1,9 @@
+hook.<command>.command::
+	A command to execute during the <command> hook event. This can be an
+	executable on your device, a oneliner for your shell, or the name of a
+	hookcmd. See linkgit:git-hook[1].
+
+hookcmd.<name>.command::
+	A command to execute during a hook for which <name> has been specified
+	as a command. This can be an executable on your device or a oneliner for
+	your shell. See linkgit:git-hook[1].
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
new file mode 100644
index 0000000000..f19875ed68
--- /dev/null
+++ b/Documentation/git-hook.txt
@@ -0,0 +1,73 @@
+git-hook(1)
+===========
+
+NAME
+----
+git-hook - Manage configured hooks
+
+SYNOPSIS
+--------
+[verse]
+'git hook' list <hook-name>
+
+DESCRIPTION
+-----------
+You can list configured hooks with this command. Later, you will be able to run,
+add, and modify hooks with this command.
+
+This command parses the default configuration files for sections `hook` and
+`hookcmd`. `hook` is used to describe the commands which will be run during a
+particular hook event; commands are run in the order Git encounters them during
+the configuration parse (see linkgit:git-config[1]). `hookcmd` is used to
+describe attributes of a specific command. If additional attributes don't need
+to be specified, a command to run can be specified directly in the `hook`
+section; if a `hookcmd` by that name isn't found, Git will attempt to run the
+provided value directly. For example:
+
+Global config
+----
+  [hook "post-commit"]
+    command = "linter"
+    command = "~/typocheck.sh"
+
+  [hookcmd "linter"]
+    command = "/bin/linter --c"
+----
+
+Local config
+----
+  [hook "prepare-commit-msg"]
+    command = "linter"
+  [hook "post-commit"]
+    command = "python ~/run-test-suite.py"
+----
+
+With these configs, you'd then see:
+
+----
+$ git hook list "post-commit"
+global: /bin/linter --c
+global: ~/typocheck.sh
+local: python ~/run-test-suite.py
+
+$ git hook list "prepare-commit-msg"
+local: /bin/linter --c
+----
+
+COMMANDS
+--------
+
+list `<hook-name>`::
+
+List the hooks which have been configured for `<hook-name>`. Hooks appear
+in the order they should be run, and print the config scope where the relevant
+`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).
+This output is human-readable and the format is subject to change over time.
+
+CONFIGURATION
+-------------
+include::config/hook.txt[]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Makefile b/Makefile
index c3565fc0f8..a6b71a0fbe 100644
--- a/Makefile
+++ b/Makefile
@@ -901,6 +901,7 @@ LIB_OBJS += hash-lookup.o
 LIB_OBJS += hashmap.o
 LIB_OBJS += help.o
 LIB_OBJS += hex.o
+LIB_OBJS += hook.o
 LIB_OBJS += ident.o
 LIB_OBJS += json-writer.o
 LIB_OBJS += kwset.o
@@ -1101,6 +1102,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 16ecd5586f..91740c1514 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 0000000000..79e150437e
--- /dev/null
+++ b/builtin/hook.c
@@ -0,0 +1,65 @@
+#include "cache.h"
+#include "builtin.h"
+#include "config.h"
+#include "hook.h"
+#include "parse-options.h"
+#include "strbuf.h"
+
+static const char * const builtin_hook_usage[] = {
+	N_("git hook list <hookname>"),
+	NULL
+};
+
+static int list(int argc, const char **argv, const char *prefix)
+{
+	struct list_head *head, *pos;
+	const char *hookname = NULL;
+
+	struct option list_options[] = {
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, list_options,
+			     builtin_hook_usage, 0);
+
+	if (argc < 1) {
+		usage_msg_opt(_("You must specify a hook event name to list."),
+			      builtin_hook_usage, list_options);
+	}
+
+	hookname = argv[0];
+
+	head = hook_list(hookname);
+
+	if (list_empty(head)) {
+		printf(_("no commands configured for hook '%s'\n"),
+		       hookname);
+		return 0;
+	}
+
+	list_for_each(pos, head) {
+		struct hook *item = list_entry(pos, struct hook, list);
+		if (item)
+			printf("%s: %s\n",
+			       config_scope_name(item->origin),
+			       item->command.buf);
+	}
+
+	clear_hook_list(head);
+
+	return 0;
+}
+
+int cmd_hook(int argc, const char **argv, const char *prefix)
+{
+	struct option builtin_hook_options[] = {
+		OPT_END(),
+	};
+	if (argc < 2)
+		usage_with_options(builtin_hook_usage, builtin_hook_options);
+
+	if (!strcmp(argv[1], "list"))
+		return list(argc - 1, argv + 1, prefix);
+
+	usage_with_options(builtin_hook_usage, builtin_hook_options);
+}
diff --git a/command-list.txt b/command-list.txt
index a289f09ed6..9ccd8e5aeb 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 18bed9a996..39988ee3b0 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_GENTLY },
 	{ "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
new file mode 100644
index 0000000000..d3e28aa73a
--- /dev/null
+++ b/hook.c
@@ -0,0 +1,120 @@
+#include "cache.h"
+
+#include "hook.h"
+#include "config.h"
+
+void free_hook(struct hook *ptr)
+{
+	if (ptr) {
+		strbuf_release(&ptr->command);
+		free(ptr);
+	}
+}
+
+static void append_or_move_hook(struct list_head *head, const char *command)
+{
+	struct list_head *pos = NULL, *tmp = NULL;
+	struct hook *to_add = NULL;
+
+	/*
+	 * remove the prior entry with this command; we'll replace it at the
+	 * end.
+	 */
+	list_for_each_safe(pos, tmp, head) {
+		struct hook *it = list_entry(pos, struct hook, list);
+		if (!strcmp(it->command.buf, command)) {
+		    list_del(pos);
+		    /* we'll simply move the hook to the end */
+		    to_add = it;
+		    break;
+		}
+	}
+
+	if (!to_add) {
+		/* adding a new hook, not moving an old one */
+		to_add = xmalloc(sizeof(*to_add));
+		strbuf_init(&to_add->command, 0);
+		strbuf_addstr(&to_add->command, command);
+	}
+
+	/* re-set the scope so we show where an override was specified */
+	to_add->origin = current_config_scope();
+
+	list_add_tail(&to_add->list, head);
+}
+
+static void remove_hook(struct list_head *to_remove)
+{
+	struct hook *hook_to_remove = list_entry(to_remove, struct hook, list);
+	list_del(to_remove);
+	free_hook(hook_to_remove);
+}
+
+void clear_hook_list(struct list_head *head)
+{
+	struct list_head *pos, *tmp;
+	list_for_each_safe(pos, tmp, head)
+		remove_hook(pos);
+}
+
+struct hook_config_cb
+{
+	struct strbuf *hookname;
+	struct list_head *list;
+};
+
+static int hook_config_lookup(const char *key, const char *value, void *cb_data)
+{
+	struct hook_config_cb *data = cb_data;
+	const char *hook_key = data->hookname->buf;
+	struct list_head *head = data->list;
+
+	if (!strcmp(key, hook_key)) {
+		const char *command = value;
+		struct strbuf hookcmd_name = STRBUF_INIT;
+
+		/*
+		 * Check if a hookcmd with that name exists. If it doesn't,
+		 * 'git_config_get_value()' is documented not to touch &command,
+		 * so we don't need to do anything.
+		 */
+		strbuf_addf(&hookcmd_name, "hookcmd.%s.command", command);
+		git_config_get_value(hookcmd_name.buf, &command);
+
+		if (!command) {
+			strbuf_release(&hookcmd_name);
+			BUG("git_config_get_value overwrote a string it shouldn't have");
+		}
+
+		/*
+		 * TODO: implement an option-getting callback, e.g.
+		 *   get configs by pattern hookcmd.$value.*
+		 *   for each key+value, do_callback(key, value, cb_data)
+		 */
+
+		append_or_move_hook(head, command);
+
+		strbuf_release(&hookcmd_name);
+	}
+
+	return 0;
+}
+
+struct list_head* hook_list(const char* hookname)
+{
+	struct strbuf hook_key = STRBUF_INIT;
+	struct list_head *hook_head = xmalloc(sizeof(struct list_head));
+	struct hook_config_cb cb_data = { &hook_key, hook_head };
+
+	INIT_LIST_HEAD(hook_head);
+
+	if (!hookname)
+		return NULL;
+
+	strbuf_addf(&hook_key, "hook.%s.command", hookname);
+
+	git_config(hook_config_lookup, &cb_data);
+
+	strbuf_release(&hook_key);
+	return hook_head;
+}
diff --git a/hook.h b/hook.h
new file mode 100644
index 0000000000..042cab8446
--- /dev/null
+++ b/hook.h
@@ -0,0 +1,25 @@
+#include "config.h"
+#include "list.h"
+#include "strbuf.h"
+
+struct hook {
+	struct list_head list;
+	/*
+	 * Config file which holds the hook.*.command definition.
+	 * (This has nothing to do with the hookcmd.<name>.* configs.)
+	 */
+	enum config_scope origin;
+	/* The literal command to run. */
+	struct strbuf command;
+};
+
+/*
+ * Provides a linked list of 'struct hook' detailing commands which should run
+ * in response to the 'hookname' event, in execution order.
+ */
+struct list_head* hook_list(const char *hookname);
+
+/* Free memory associated with a 'struct hook' */
+void free_hook(struct hook *ptr);
+/* Empties the list at 'head', calling 'free_hook()' on each entry */
+void clear_hook_list(struct list_head *head);
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
new file mode 100755
index 0000000000..6e4a3e763f
--- /dev/null
+++ b/t/t1360-config-based-hooks.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+
+test_description='config-managed multihooks, including git-hook command'
+
+. ./test-lib.sh
+
+ROOT=
+if test_have_prereq MINGW
+then
+	# In Git for Windows, Unix-like paths work only in shell scripts;
+	# `git.exe`, however, will prefix them with the pseudo root directory
+	# (of the Unix shell). Let's accommodate for that.
+	ROOT="$(cd / && pwd)"
+fi
+
+setup_hooks () {
+	test_config hook.pre-commit.command "/path/ghi" --add
+	test_config_global hook.pre-commit.command "/path/def" --add
+}
+
+setup_hookcmd () {
+	test_config hook.pre-commit.command "abc" --add
+	test_config_global hookcmd.abc.command "/path/abc" --add
+}
+
+test_expect_success 'git hook rejects commands without a mode' '
+	test_must_fail git hook pre-commit
+'
+
+
+test_expect_success 'git hook rejects commands without a hookname' '
+	test_must_fail git hook list
+'
+
+test_expect_success 'git hook runs outside of a repo' '
+	setup_hooks &&
+
+	cat >expected <<-EOF &&
+	global: $ROOT/path/def
+	EOF
+
+	nongit git config --list --global &&
+
+	nongit git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git hook list orders by config order' '
+	setup_hooks &&
+
+	cat >expected <<-EOF &&
+	global: $ROOT/path/def
+	local: $ROOT/path/ghi
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git hook list dereferences a hookcmd' '
+	setup_hooks &&
+	setup_hookcmd &&
+
+	cat >expected <<-EOF &&
+	global: $ROOT/path/def
+	local: $ROOT/path/ghi
+	local: $ROOT/path/abc
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git hook list reorders on duplicate commands' '
+	setup_hooks &&
+
+	test_config hook.pre-commit.command "/path/def" --add &&
+
+	cat >expected <<-EOF &&
+	local: $ROOT/path/ghi
+	local: $ROOT/path/def
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
+test_done
-- 
2.31.1.818.g46aad6cb9e-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v9 03/37] hook: include hookdir hook in list
  2021-05-27  0:08 ` [PATCH v9 00/37] propose " Emily Shaffer
  2021-05-27  0:08   ` [PATCH v9 01/37] doc: propose hooks managed by the config Emily Shaffer
  2021-05-27  0:08   ` [PATCH v9 02/37] hook: introduce git-hook subcommand Emily Shaffer
@ 2021-05-27  0:08   ` Emily Shaffer
  2021-05-27  0:08   ` [PATCH v9 04/37] hook: teach hook.runHookDir Emily Shaffer
                     ` (35 subsequent siblings)
  38 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-05-27  0:08 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

Historically, hooks are declared by placing an executable into
$GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken
from the config are more featureful than hooks placed in the $HOOKDIR,
those hooks should not stop working for users who already have them.
Let's list them to the user, but instead of displaying a config scope
(e.g. "global: blah") we can prefix them with "hookdir:".

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 builtin/hook.c                | 18 +++++++++++++++---
 hook.c                        | 17 +++++++++++++++++
 hook.h                        |  1 +
 t/t1360-config-based-hooks.sh | 19 +++++++++++++++++++
 4 files changed, 52 insertions(+), 3 deletions(-)

diff --git a/builtin/hook.c b/builtin/hook.c
index 79e150437e..e82725f0a6 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -39,10 +39,20 @@ static int list(int argc, const char **argv, const char *prefix)
 
 	list_for_each(pos, head) {
 		struct hook *item = list_entry(pos, struct hook, list);
-		if (item)
-			printf("%s: %s\n",
-			       config_scope_name(item->origin),
+		item = list_entry(pos, struct hook, list);
+		if (item) {
+			/*
+			 * TRANSLATORS: "<config scope>: <path>". Both fields
+			 * should be left untranslated; config scope matches the
+			 * output of 'git config --show-scope'. Marked for
+			 * translation to provide better RTL support later.
+			 */
+			printf(_("%s: %s\n"),
+			       (item->from_hookdir
+				? "hookdir"
+				: config_scope_name(item->origin)),
 			       item->command.buf);
+		}
 	}
 
 	clear_hook_list(head);
@@ -58,6 +68,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)
 	if (argc < 2)
 		usage_with_options(builtin_hook_usage, builtin_hook_options);
 
+	git_config(git_default_config, NULL);
+
 	if (!strcmp(argv[1], "list"))
 		return list(argc - 1, argv + 1, prefix);
 
diff --git a/hook.c b/hook.c
index d3e28aa73a..b4994fc108 100644
--- a/hook.c
+++ b/hook.c
@@ -2,6 +2,7 @@
 
 #include "hook.h"
 #include "config.h"
+#include "run-command.h"
 
 void free_hook(struct hook *ptr)
 {
@@ -35,6 +36,7 @@ static void append_or_move_hook(struct list_head *head, const char *command)
 		to_add = xmalloc(sizeof(*to_add));
 		strbuf_init(&to_add->command, 0);
 		strbuf_addstr(&to_add->command, command);
+		to_add->from_hookdir = 0;
 	}
 
 	/* re-set the scope so we show where an override was specified */
@@ -115,6 +117,21 @@ struct list_head* hook_list(const char* hookname)
 
 	git_config(hook_config_lookup, &cb_data);
 
+	if (have_git_dir()) {
+		const char *legacy_hook_path = find_hook(hookname);
+
+		/* Unconditionally add legacy hook, but annotate it. */
+		if (legacy_hook_path) {
+			struct hook *legacy_hook;
+
+			append_or_move_hook(hook_head,
+					    absolute_path(legacy_hook_path));
+			legacy_hook = list_entry(hook_head->prev, struct hook,
+						 list);
+			legacy_hook->from_hookdir = 1;
+		}
+	}
+
 	strbuf_release(&hook_key);
 	return hook_head;
 }
diff --git a/hook.h b/hook.h
index 042cab8446..b6c5480325 100644
--- a/hook.h
+++ b/hook.h
@@ -11,6 +11,7 @@ struct hook {
 	enum config_scope origin;
 	/* The literal command to run. */
 	struct strbuf command;
+	unsigned from_hookdir : 1;
 };
 
 /*
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
index 6e4a3e763f..0f12af4659 100755
--- a/t/t1360-config-based-hooks.sh
+++ b/t/t1360-config-based-hooks.sh
@@ -23,6 +23,14 @@ setup_hookcmd () {
 	test_config_global hookcmd.abc.command "/path/abc" --add
 }
 
+setup_hookdir () {
+	mkdir .git/hooks
+	write_script .git/hooks/pre-commit <<-EOF
+	echo \"Legacy Hook\"
+	EOF
+	test_when_finished rm -rf .git/hooks
+}
+
 test_expect_success 'git hook rejects commands without a mode' '
 	test_must_fail git hook pre-commit
 '
@@ -85,4 +93,15 @@ test_expect_success 'git hook list reorders on duplicate commands' '
 	test_cmp expected actual
 '
 
+test_expect_success 'git hook list shows hooks from the hookdir' '
+	setup_hookdir &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
 test_done
-- 
2.31.1.818.g46aad6cb9e-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v9 04/37] hook: teach hook.runHookDir
  2021-05-27  0:08 ` [PATCH v9 00/37] propose " Emily Shaffer
                     ` (2 preceding siblings ...)
  2021-05-27  0:08   ` [PATCH v9 03/37] hook: include hookdir hook in list Emily Shaffer
@ 2021-05-27  0:08   ` Emily Shaffer
  2021-05-27  0:08   ` [PATCH v9 05/37] hook: implement hookcmd.<name>.skip Emily Shaffer
                     ` (34 subsequent siblings)
  38 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-05-27  0:08 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

For now, just give a hint about how these hooks will be run in 'git hook
list'. Later on, though, we will pay attention to this enum when running
the hooks.
---

Notes:
    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to
    enforce it - now it matches the design doc much better.
    
    Also, thanks Jonathan Tan for pointing out that the commit message made no sense
    and was targeted for a different change. Rewrote the commit message now.
    
    Plus, added HOOK_ERROR flag per Junio and Jonathan Nieder.
    
    Newly split into its own commit since v4, and taking place much sooner.
    
    An unfortunate side effect of adding this support *before* the
    hook.runHookDir support is that the labels on the list are not clear -
    because we aren't yet flagging which hooks are from the hookdir versus
    the config. I suppose we could move the addition of that field to the
    struct hook up to this patch, but it didn't make a lot of sense to me to
    do it just for cosmetic purposes.
    
    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to
    enforce it - now it matches the design doc much better.
    
    Also, thanks Jonathan Tan for pointing out that the commit message made no sense
    and was targeted for a different change. Rewrote the commit message now.
    
    Newly split into its own commit since v4, and taking place much sooner.
    
    An unfortunate side effect of adding this support *before* the
    hook.runHookDir support is that the labels on the list are not clear -
    because we aren't yet flagging which hooks are from the hookdir versus
    the config. I suppose we could move the addition of that field to the
    struct hook up to this patch, but it didn't make a lot of sense to me to
    do it just for cosmetic purposes.

 Documentation/config/hook.txt |  5 ++
 builtin/hook.c                | 98 ++++++++++++++++++++++++++++++-----
 hook.c                        | 24 +++++++++
 hook.h                        | 22 ++++++++
 t/t1360-config-based-hooks.sh | 71 +++++++++++++++++++++++++
 5 files changed, 206 insertions(+), 14 deletions(-)

diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
index 71449ecbc7..75312754ae 100644
--- a/Documentation/config/hook.txt
+++ b/Documentation/config/hook.txt
@@ -7,3 +7,8 @@ hookcmd.<name>.command::
 	A command to execute during a hook for which <name> has been specified
 	as a command. This can be an executable on your device or a oneliner for
 	your shell. See linkgit:git-hook[1].
+
+hook.runHookDir::
+	Controls how hooks contained in your hookdir are executed. Can be any of
+	"yes", "warn", "interactive", or "no". Defaults to "yes". See
+	linkgit:git-hook[1] and linkgit:git-config[1] "core.hooksPath").
diff --git a/builtin/hook.c b/builtin/hook.c
index e82725f0a6..b1e63a9576 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -10,10 +10,13 @@ static const char * const builtin_hook_usage[] = {
 	NULL
 };
 
+static enum hookdir_opt should_run_hookdir;
+
 static int list(int argc, const char **argv, const char *prefix)
 {
 	struct list_head *head, *pos;
 	const char *hookname = NULL;
+	struct strbuf hookdir_annotation = STRBUF_INIT;
 
 	struct option list_options[] = {
 		OPT_END(),
@@ -41,37 +44,104 @@ static int list(int argc, const char **argv, const char *prefix)
 		struct hook *item = list_entry(pos, struct hook, list);
 		item = list_entry(pos, struct hook, list);
 		if (item) {
-			/*
-			 * TRANSLATORS: "<config scope>: <path>". Both fields
-			 * should be left untranslated; config scope matches the
-			 * output of 'git config --show-scope'. Marked for
-			 * translation to provide better RTL support later.
-			 */
-			printf(_("%s: %s\n"),
-			       (item->from_hookdir
-				? "hookdir"
-				: config_scope_name(item->origin)),
-			       item->command.buf);
+			if (item->from_hookdir) {
+				/*
+				 * TRANSLATORS: do not translate 'hookdir' as
+				 * it matches the config setting.
+				 */
+				switch (should_run_hookdir) {
+				case HOOKDIR_NO:
+					printf(_("hookdir: %s (will not run)\n"),
+					       item->command.buf);
+					break;
+				case HOOKDIR_ERROR:
+					printf(_("hookdir: %s (will error and not run)\n"),
+					       item->command.buf);
+					break;
+				case HOOKDIR_INTERACTIVE:
+					printf(_("hookdir: %s (will prompt)\n"),
+					       item->command.buf);
+					break;
+				case HOOKDIR_WARN:
+					printf(_("hookdir: %s (will warn but run)\n"),
+					       item->command.buf);
+					break;
+				case HOOKDIR_YES:
+				/*
+				 * The default behavior should agree with
+				 * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just
+				 * do the default behavior.
+				 */
+				case HOOKDIR_UNKNOWN:
+				default:
+					printf(_("hookdir: %s\n"),
+						 item->command.buf);
+					break;
+				}
+			} else {
+				/*
+				 * TRANSLATORS: "<config scope>: <path>". Both fields
+				 * should be left untranslated; config scope matches the
+				 * output of 'git config --show-scope'. Marked for
+				 * translation to provide better RTL support later.
+				 */
+				printf(_("%s: %s\n"),
+					config_scope_name(item->origin),
+					item->command.buf);
+			}
 		}
 	}
 
 	clear_hook_list(head);
+	strbuf_release(&hookdir_annotation);
 
 	return 0;
 }
 
 int cmd_hook(int argc, const char **argv, const char *prefix)
 {
+	const char *run_hookdir = NULL;
+
 	struct option builtin_hook_options[] = {
+		OPT_STRING(0, "run-hookdir", &run_hookdir, N_("option"),
+			   N_("what to do with hooks found in the hookdir")),
 		OPT_END(),
 	};
-	if (argc < 2)
+
+	argc = parse_options(argc, argv, prefix, builtin_hook_options,
+			     builtin_hook_usage, 0);
+
+	/* after the parse, we should have "<command> <hookname> <args...>" */
+	if (argc < 1)
 		usage_with_options(builtin_hook_usage, builtin_hook_options);
 
 	git_config(git_default_config, NULL);
 
-	if (!strcmp(argv[1], "list"))
-		return list(argc - 1, argv + 1, prefix);
+
+	/* argument > config */
+	if (run_hookdir)
+		if (!strcmp(run_hookdir, "no"))
+			should_run_hookdir = HOOKDIR_NO;
+		else if (!strcmp(run_hookdir, "error"))
+			should_run_hookdir = HOOKDIR_ERROR;
+		else if (!strcmp(run_hookdir, "yes"))
+			should_run_hookdir = HOOKDIR_YES;
+		else if (!strcmp(run_hookdir, "warn"))
+			should_run_hookdir = HOOKDIR_WARN;
+		else if (!strcmp(run_hookdir, "interactive"))
+			should_run_hookdir = HOOKDIR_INTERACTIVE;
+		else
+			/*
+			 * TRANSLATORS: leave "yes/warn/interactive/no"
+			 * untranslated; the strings are compared literally.
+			 */
+			die(_("'%s' is not a valid option for --run-hookdir "
+			      "(yes, warn, interactive, no)"), run_hookdir);
+	else
+		should_run_hookdir = configured_hookdir_opt();
+
+	if (!strcmp(argv[0], "list"))
+		return list(argc, argv, prefix);
 
 	usage_with_options(builtin_hook_usage, builtin_hook_options);
 }
diff --git a/hook.c b/hook.c
index b4994fc108..030051cab2 100644
--- a/hook.c
+++ b/hook.c
@@ -102,6 +102,30 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)
 	return 0;
 }
 
+enum hookdir_opt configured_hookdir_opt(void)
+{
+	const char *key;
+	if (git_config_get_value("hook.runhookdir", &key))
+		return HOOKDIR_YES; /* by default, just run it. */
+
+	if (!strcmp(key, "no"))
+		return HOOKDIR_NO;
+
+	if (!strcmp(key, "error"))
+		return HOOKDIR_ERROR;
+
+	if (!strcmp(key, "yes"))
+		return HOOKDIR_YES;
+
+	if (!strcmp(key, "warn"))
+		return HOOKDIR_WARN;
+
+	if (!strcmp(key, "interactive"))
+		return HOOKDIR_INTERACTIVE;
+
+	return HOOKDIR_UNKNOWN;
+}
+
 struct list_head* hook_list(const char* hookname)
 {
 	struct strbuf hook_key = STRBUF_INIT;
diff --git a/hook.h b/hook.h
index b6c5480325..7f2b2ee8f2 100644
--- a/hook.h
+++ b/hook.h
@@ -20,6 +20,28 @@ struct hook {
  */
 struct list_head* hook_list(const char *hookname);
 
+enum hookdir_opt
+{
+	HOOKDIR_NO,
+	HOOKDIR_ERROR,
+	HOOKDIR_WARN,
+	HOOKDIR_INTERACTIVE,
+	HOOKDIR_YES,
+	HOOKDIR_UNKNOWN,
+};
+
+/*
+ * Provides the hookdir_opt specified in the config without consulting any
+ * command line arguments.
+ */
+enum hookdir_opt configured_hookdir_opt(void);
+
+/*
+ * Provides the hookdir_opt specified in the config without consulting any
+ * command line arguments.
+ */
+enum hookdir_opt configured_hookdir_opt(void);
+
 /* Free memory associated with a 'struct hook' */
 void free_hook(struct hook *ptr);
 /* Empties the list at 'head', calling 'free_hook()' on each entry */
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
index 0f12af4659..141e6f7590 100755
--- a/t/t1360-config-based-hooks.sh
+++ b/t/t1360-config-based-hooks.sh
@@ -104,4 +104,75 @@ test_expect_success 'git hook list shows hooks from the hookdir' '
 	test_cmp expected actual
 '
 
+test_expect_success 'hook.runHookDir = no is respected by list' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "no" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit (will not run)
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_cmp expected actual
+'
+
+test_expect_success 'hook.runHookDir = error is respected by list' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "error" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit (will error and not run)
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_cmp expected actual
+'
+
+test_expect_success 'hook.runHookDir = warn is respected by list' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "warn" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit (will warn but run)
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_cmp expected actual
+'
+
+
+test_expect_success 'hook.runHookDir = interactive is respected by list' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "interactive" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit (will prompt)
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_cmp expected actual
+'
+
+test_expect_success 'hook.runHookDir is tolerant to unknown values' '
+	setup_hookdir &&
+
+	test_config hook.runHookDir "junk" &&
+
+	cat >expected <<-EOF &&
+	hookdir: $(pwd)/.git/hooks/pre-commit
+	EOF
+
+	git hook list pre-commit >actual &&
+	# the hookdir annotation is translated
+	test_cmp expected actual
+'
+
 test_done
-- 
2.31.1.818.g46aad6cb9e-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v9 05/37] hook: implement hookcmd.<name>.skip
  2021-05-27  0:08 ` [PATCH v9 00/37] propose " Emily Shaffer
                     ` (3 preceding siblings ...)
  2021-05-27  0:08   ` [PATCH v9 04/37] hook: teach hook.runHookDir Emily Shaffer
@ 2021-05-27  0:08   ` Emily Shaffer
  2021-05-27  0:08   ` [PATCH v9 06/37] parse-options: parse into strvec Emily Shaffer
                     ` (33 subsequent siblings)
  38 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-05-27  0:08 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

If a user wants a specific repo to skip execution of a hook which is set
at a global or system level, they will be able to do so by specifying
'skip' in their repo config:

~/.gitconfig
  [hook.pre-commit]
    command = skippable-oneliner
    command = skippable-hookcmd

  [hookcmd.skippable-hookcmd]
    command = foo.sh

$GIT_DIR/.git/config
  [hookcmd.skippable-oneliner]
    skip = true
  [hookcmd.skippable-hookcmd]
    skip = true

Later it may make sense to add an option like
"hookcmd.<name>.<hook-event>-skip" - but for simplicity, let's start
with a universal skip setting like this.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---
 Documentation/config/hook.txt |  8 ++++++++
 Documentation/git-hook.txt    | 33 +++++++++++++++++++++++++++++++++
 hook.c                        | 35 ++++++++++++++++++++++++++---------
 t/t1360-config-based-hooks.sh | 35 +++++++++++++++++++++++++++++++++++
 4 files changed, 102 insertions(+), 9 deletions(-)

diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt
index 75312754ae..8b12512e33 100644
--- a/Documentation/config/hook.txt
+++ b/Documentation/config/hook.txt
@@ -8,6 +8,14 @@ hookcmd.<name>.command::
 	as a command. This can be an executable on your device or a oneliner for
 	your shell. See linkgit:git-hook[1].
 
+hookcmd.<name>.skip::
+	Specify this boolean to remove a command from earlier in the execution
+	order. Useful if you want to make a single repo an exception to hook
+	configured at the system or global scope. If there is no hookcmd
+	specified for the command you want to skip, you can use the value of
+	`hook.<command>.command` as <name> as a shortcut. The "skip" setting
+	must be specified after the "hook.<command>.command" to have an effect.
+
 hook.runHookDir::
 	Controls how hooks contained in your hookdir are executed. Can be any of
 	"yes", "warn", "interactive", or "no". Defaults to "yes". See
diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index f19875ed68..c84520cb38 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -54,6 +54,39 @@ $ git hook list "prepare-commit-msg"
 local: /bin/linter --c
 ----
 
+If there is a command you wish to run in most cases but have one or two
+exceptional repos where it should be skipped, you can use specify
+`hookcmd.<name>.skip`, for example:
+
+System config
+----
+  [hook "pre-commit"]
+    command = check-for-secrets
+
+  [hookcmd "check-for-secrets"]
+    command = /bin/secret-checker --aggressive
+----
+
+Local config
+----
+  [hookcmd "check-for-secrets"]
+    skip = true
+  # This works for inlined hook commands, too:
+  [hookcmd "~/typocheck.sh"]
+    skip = true
+----
+
+After these configs are added, the hook list becomes:
+
+----
+$ git hook list "post-commit"
+global: /bin/linter --c
+local: python ~/run-test-suite.py
+
+$ git hook list "pre-commit"
+no commands configured for hook 'pre-commit'
+----
+
 COMMANDS
 --------
 
diff --git a/hook.c b/hook.c
index 030051cab2..65cbad8dba 100644
--- a/hook.c
+++ b/hook.c
@@ -12,24 +12,25 @@ void free_hook(struct hook *ptr)
 	}
 }
 
-static void append_or_move_hook(struct list_head *head, const char *command)
+static struct hook * find_hook_by_command(struct list_head *head, const char *command)
 {
 	struct list_head *pos = NULL, *tmp = NULL;
-	struct hook *to_add = NULL;
+	struct hook *found = NULL;
 
-	/*
-	 * remove the prior entry with this command; we'll replace it at the
-	 * end.
-	 */
 	list_for_each_safe(pos, tmp, head) {
 		struct hook *it = list_entry(pos, struct hook, list);
 		if (!strcmp(it->command.buf, command)) {
 		    list_del(pos);
-		    /* we'll simply move the hook to the end */
-		    to_add = it;
+		    found = it;
 		    break;
 		}
 	}
+	return found;
+}
+
+static void append_or_move_hook(struct list_head *head, const char *command)
+{
+	struct hook *to_add = find_hook_by_command(head, command);
 
 	if (!to_add) {
 		/* adding a new hook, not moving an old one */
@@ -74,12 +75,22 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)
 	if (!strcmp(key, hook_key)) {
 		const char *command = value;
 		struct strbuf hookcmd_name = STRBUF_INIT;
+		int skip = 0;
+
+		/*
+		 * Check if we're removing that hook instead. Hookcmds are
+		 * removed by name, and inlined hooks are removed by command
+		 * content.
+		 */
+		strbuf_addf(&hookcmd_name, "hookcmd.%s.skip", command);
+		git_config_get_bool(hookcmd_name.buf, &skip);
 
 		/*
 		 * Check if a hookcmd with that name exists. If it doesn't,
 		 * 'git_config_get_value()' is documented not to touch &command,
 		 * so we don't need to do anything.
 		 */
+		strbuf_reset(&hookcmd_name);
 		strbuf_addf(&hookcmd_name, "hookcmd.%s.command", command);
 		git_config_get_value(hookcmd_name.buf, &command);
 
@@ -94,7 +105,13 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)
 		 *   for each key+value, do_callback(key, value, cb_data)
 		 */
 
-		append_or_move_hook(head, command);
+		if (skip) {
+			struct hook *to_remove = find_hook_by_command(head, command);
+			if (to_remove)
+				remove_hook(&(to_remove->list));
+		} else {
+			append_or_move_hook(head, command);
+		}
 
 		strbuf_release(&hookcmd_name);
 	}
diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh
index 141e6f7590..33ac27aa97 100755
--- a/t/t1360-config-based-hooks.sh
+++ b/t/t1360-config-based-hooks.sh
@@ -146,6 +146,41 @@ test_expect_success 'hook.runHookDir = warn is respected by list' '
 	test_cmp expected actual
 '
 
+test_expect_success 'git hook list removes skipped hookcmd' '
+	setup_hookcmd &&
+	test_config hookcmd.abc.skip "true" --add &&
+
+	cat >expected <<-EOF &&
+	no commands configured for hook '\''pre-commit'\''
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git hook list ignores skip referring to unused hookcmd' '
+	test_config hookcmd.abc.command "/path/abc" --add &&
+	test_config hookcmd.abc.skip "true" --add &&
+
+	cat >expected <<-EOF &&
+	no commands configured for hook '\''pre-commit'\''
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git hook list removes skipped inlined hook' '
+	setup_hooks &&
+	test_config hookcmd."$ROOT/path/ghi".skip "true" --add &&
+
+	cat >expected <<-EOF &&
+	global: $ROOT/path/def
+	EOF
+
+	git hook list pre-commit >actual &&
+	test_cmp expected actual
+'
 
 test_expect_success 'hook.runHookDir = interactive is respected by list' '
 	setup_hookdir &&
-- 
2.31.1.818.g46aad6cb9e-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v9 06/37] parse-options: parse into strvec
  2021-05-27  0:08 ` [PATCH v9 00/37] propose " Emily Shaffer
                     ` (4 preceding siblings ...)
  2021-05-27  0:08   ` [PATCH v9 05/37] hook: implement hookcmd.<name>.skip Emily Shaffer
@ 2021-05-27  0:08   ` Emily Shaffer
  2021-05-27  0:08   ` [PATCH v9 07/37] hook: add 'run' subcommand Emily Shaffer
                     ` (32 subsequent siblings)
  38 siblings, 0 replies; 324+ messages in thread
From: Emily Shaffer @ 2021-05-27  0:08 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

parse-options already knows how to read into a string_list, and it knows
how to read into an strvec as a passthrough (that is, including the
argument as well as its value). string_list and strvec serve similar
purposes but are somewhat painful to convert between; so, let's teach
parse-options to read values of string arguments directly into an
strvec without preserving the argument name.

This is useful if collecting generic arguments to pass through to
another command, for example, 'git hook run --arg "--quiet" --arg
"--format=pretty" some-hook'. The resulting strvec would contain
{ "--quiet", "--format=pretty" }.

The implementation is based on that of OPT_STRING_LIST.

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Since v7, updated the reference doc to make the intended usage for OPT_STRVEC
    more clear.
    
    Since v4, fixed one or two more places where I missed the argv_array->strvec
    rename.

 Documentation/technical/api-parse-options.txt |  7 +++++
 parse-options-cb.c                            | 16 +++++++++++
 parse-options.h                               |  4 +++
 t/helper/test-parse-options.c                 |  6 +++++
 t/t0040-parse-options.sh                      | 27 +++++++++++++++++++
 5 files changed, 60 insertions(+)

diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt
index 5a60bbfa7f..f79b17e7fc 100644
--- a/Documentation/technical/api-parse-options.txt
+++ b/Documentation/technical/api-parse-options.txt
@@ -173,6 +173,13 @@ There are some macros to easily define options:
 	The string argument is stored as an element in `string_list`.
 	Use of `--no-option` will clear the list of preceding values.
 
+`OPT_STRVEC(short, long, &struct strvec, arg_str, description)`::
+	Introduce an option with a string argument, meant to be specified
+	multiple times.
+	The string argument is stored as an element in `strvec`, and later
+	arguments are added to the same `strvec`.
+	Use of `--no-option` will clear the list of preceding values.
+
 `OPT_INTEGER(short, long, &int_var, description)`::
 	Introduce an option with integer argument.
 	The integer is put into `int_var`.
diff --git a/parse-options-cb.c b/parse-options-cb.c
index 3c811e1e4a..8227499eb6 100644
--- a/parse-options-cb.c
+++ b/parse-options-cb.c
@@ -207,6 +207,22 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset)
 	return 0;
 }
 
+int parse_opt_strvec(const struct option *opt, const char *arg, int unset)
+{
+	struct strvec *v = opt->value;
+
+	if (unset) {
+		strvec_clear(v);
+		return 0;
+	}
+
+	if (!arg)
+		return -1;
+
+	strvec_push(v, arg);
+	return 0;
+}
+
 int parse_opt_noop_cb(const struct option *opt, const char *arg, int unset)
 {
 	return 0;
diff --git a/parse-options.h b/parse-options.h
index a845a9d952..fcb0f1f31e 100644
--- a/parse-options.h
+++ b/parse-options.h
@@ -178,6 +178,9 @@ struct option {
 #define OPT_STRING_LIST(s, l, v, a, h) \
 				    { OPTION_CALLBACK, (s), (l), (v), (a), \
 				      (h), 0, &parse_opt_string_list }
+#define OPT_STRVEC(s, l, v, a, h) \
+				    { OPTION_CALLBACK, (s), (l), (v), (a), \
+				      (h), 0, &parse_opt_strvec }
 #define OPT_UYN(s, l, v, h)         { OPTION_CALLBACK, (s), (l), (v), NULL, \
 				      (h), PARSE_OPT_NOARG, &parse_opt_tertiary }
 #define OPT_EXPIRY_DATE(s, l, v, h) \
@@ -297,6 +300,7 @@ int parse_opt_commits(const struct option *, const char *, int);
 int parse_opt_commit(const struct option *, const char *, int);
 int parse_opt_tertiary(const struct option *, const char *, int);
 int parse_opt_string_list(const struct option *, const char *, int);
+int parse_opt_strvec(const struct option *, const char *, int);
 int parse_opt_noop_cb(const struct option *, const char *, int);
 enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx,
 					   const struct option *,
diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c
index 2051ce57db..922af56156 100644
--- a/t/helper/test-parse-options.c
+++ b/t/helper/test-parse-options.c
@@ -2,6 +2,7 @@
 #include "cache.h"
 #include "parse-options.h"
 #include "string-list.h"
+#include "strvec.h"
 #include "trace2.h"
 
 static int boolean = 0;
@@ -15,6 +16,7 @@ static char *string = NULL;
 static char *file = NULL;
 static int ambiguous;
 static struct string_list list = STRING_LIST_INIT_NODUP;
+static struct strvec vector = STRVEC_INIT;
 
 static struct {
 	int called;
@@ -133,6 +135,7 @@ int cmd__parse_options(int argc, const char **argv)
 		OPT_STRING('o', NULL, &string, "str", "get another string"),
 		OPT_NOOP_NOARG(0, "obsolete"),
 		OPT_STRING_LIST(0, "list", &list, "str", "add str to list"),
+		OPT_STRVEC(0, "vector", &vector, "str", "add str to strvec"),
 		OPT_GROUP("Magic arguments"),
 		OPT_ARGUMENT("quux", NULL, "means --quux"),
 		OPT_NUMBER_CALLBACK(&integer, "set integer to NUM",
@@ -183,6 +186,9 @@ int cmd__parse_options(int argc, const char **argv)
 	for (i = 0; i < list.nr; i++)
 		show(&expect, &ret, "list: %s", list.items[i].string);
 
+	for (i = 0; i < vector.nr; i++)
+		show(&expect, &ret, "vector: %s", vector.v[i]);
+
 	for (i = 0; i < argc; i++)
 		show(&expect, &ret, "arg %02d: %s", i, argv[i]);
 
diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh
index ad4746d899..485e0170bf 100755
--- a/t/t0040-parse-options.sh
+++ b/t/t0040-parse-options.sh
@@ -35,6 +35,7 @@ String options
     --st <st>             get another string (pervert ordering)
     -o <str>              get another string
     --list <str>          add str to list
+    --vector <str>        add str to strvec
 
 Magic arguments
     --quux                means --quux
@@ -386,6 +387,32 @@ test_expect_success '--no-list resets list' '
 	test_cmp expect output
 '
 
+cat >expect <<\EOF
+boolean: 0
+integer: 0
+magnitude: 0
+timestamp: 0
+string: (not set)
+abbrev: 7
+verbose: -1
+quiet: 0
+dry run: no
+file: (not set)
+vector: foo
+vector: bar
+vector: baz
+EOF
+test_expect_success '--vector keeps list of strings' '
+	test-tool parse-options --vector foo --vector=bar --vector=baz >output &&
+	test_cmp expect output
+'
+
+test_expect_success '--no-vector resets list' '
+	test-tool parse-options --vector=other --vector=irrelevant --vector=options \
+		--no-vector --vector=foo --vector=bar --vector=baz >output &&
+	test_cmp expect output
+'
+
 test_expect_success 'multiple quiet levels' '
 	test-tool parse-options --expect="quiet: 3" -q -q -q
 '
-- 
2.31.1.818.g46aad6cb9e-goog


^ permalink raw reply	[flat|nested] 324+ messages in thread

* [PATCH v9 07/37] hook: add 'run' subcommand
  2021-05-27  0:08 ` [PATCH v9 00/37] propose " Emily Shaffer
                     ` (5 preceding siblings ...)
  2021-05-27  0:08   ` [PATCH v9 06/37] parse-options: parse into strvec Emily Shaffer
@ 2021-05-27  0:08   ` Emily Shaffer
  2021-06-03  9:07     ` Ævar Arnfjörð Bjarmason
  2021-05-27  0:08   ` [PATCH v9 08/37] hook: introduce hook_exists() Emily Shaffer
                     ` (31 subsequent siblings)
  38 siblings, 1 reply; 324+ messages in thread
From: Emily Shaffer @ 2021-05-27  0:08 UTC (permalink / raw)
  To: git; +Cc: Emily Shaffer

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.

For now, the hook commands will run in config order, in series. As
alternate ordering or parallelism is supported in the future, we should
add knobs to use those to the command line as well.

As with the legacy hook implementation, all stdout generated by hook
commands is redirected to stderr. Piping from stdin is not yet
supported.

Legacy hooks (those present in $GITDIR/hooks) are run at the end of the
execution list. They can be disabled, or made to print warnings, or to
prompt before running, with the 'hook.runHookDir' config.

Users may wish to provide hook commands like 'git config
hook.pre-commit.command "~/linter.sh --pre-commit"'. To enable this,
config-defined hooks are run in a shell. (Since hooks in $GITDIR/hooks
can't be specified with included arguments or paths which need expansion
like this, they are run without a shell instead.)

Signed-off-by: Emily Shaffer <emilyshaffer@google.com>
---

Notes:
    Since v7, added support for "error" hook.runHookDir setting.
    
    Since v4, updated the docs, and did less local application of single
    quotes. In order for hookdir hooks to run successfully with a space in
    the path, though, they must not be run with 'sh -c'. So we can treat the
    hookdir hooks specially, and warn users via doc about special
    considerations for configured hooks with spaces in their path.

 Documentation/git-hook.txt    |  31 +++++++-
 builtin/hook.c                |  42 ++++++++++-
 hook.c                        | 137 +++++++++++++++++++++++++++++++++-
 hook.h                        |  26 ++++++-
 t/t1360-config-based-hooks.sh |  96 +++++++++++++++++++++++-
 5 files changed, 320 insertions(+), 12 deletions(-)

diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt
index c84520cb38..8f96c347ea 100644
--- a/Documentation/git-hook.txt
+++ b/Documentation/git-hook.txt
@@ -9,11 +9,12 @@ SYNOPSIS
 --------
 [verse]
 'git hook' list <hook-name>
+'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>
 
 DESCRIPTION
 -----------
-You can list configured hooks with this command. Later, you will be able to run,
-add, and modify hooks with this command.
+You can list and run configured hooks with this command. Later, you will be able
+to add and modify hooks with this command.
 
 This command parses the default configuration files for sections `hook` and
 `hookcmd`. `hook` is used to describe the commands which will be run during a
@@ -97,6 +98,32 @@ in the order they should be run, and print the config scope where the relevant
 `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).
 This output is human-readable and the format is subject to change over time.
 
+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::
+
+Runs hooks configured for `<hook-name>`, in the same order displayed by `git
+hook list`. Hooks configured this way may be run prepended with `sh -c`, so
+paths containing special characters or spaces should be wrapped in single
+quotes: `command = '/my/path with spaces/script.sh' some args`.
+
+OPTIONS
+-------
+--run-hookdir::
+	Overrides the hook.runHookDir config. Must be 'yes', 'warn',
+	'interactive', or 'no'. Specifies how to handle hooks located in the Git
+	hook directory (core.hooksPath).
+
+-a::
+--arg::
+	Only valid for `run`.
++
+Specify arguments to pass to every hook that is run.
+
+-e::
+--env::
+	Only valid for `run`.
++
+Specify environment variables to set for every hook that is run.
+
 CONFIGURATION
 -------------
 include::config/hook.txt[]
diff --git a/builtin/hook.c b/builtin/hook.c
index b1e63a9576..4673c9091c 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -4,9 +4,11 @@
 #include "hook.h"
 #include "parse-options.h"
 #include "strbuf.h"
+#include "strvec.h"
 
 static const char * const builtin_hook_usage[] = {
 	N_("git hook list <hookname>"),
+	N_("git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hookname>"),
 	NULL
 };
 
@@ -98,6 +100,40 @@ static int list(int argc, const char **argv, const char *prefix)
 	return 0;
 }
 
+static int run(int argc, const char **argv, const char *prefix)
+{
+	struct strbuf hookname = STRBUF_INIT;
+	struct run_hooks_opt opt;
+	int rc = 0;
+
+	struct option run_options[] = {
+		OPT_STRVEC('e', "env", &opt.env, N_("var"),
+			   N_("environment variables for hook to use")),
+		OPT_STRVEC('a', "arg", &opt.args, N_("args"),
+			   N_("argument to pass to hook")),
+		OPT_END(),
+	};
+
+	run_hooks_opt_init(&opt);
+
+	argc = parse_options(argc, argv, prefix, run_options,
+			     builtin_hook_usage, 0);
+
+	if (argc < 1)
+		usage_msg_opt(_("You must specify a hook event to run."),
+			      builtin_hook_usage, run_options);
+
+	strbuf_addstr(&hookname, argv[0]);
+	opt.run_hookdir = should_run_hookdir;
+
+	rc = run_hooks(hookname.buf, &opt);
+
+	strbuf_release(&hookname);
+	run_hooks_opt_clear(&opt);
+
+	return rc;
+}
+
 int cmd_hook(int argc, const char **argv, const char *prefix)
 {
 	const char *run_hookdir = NULL;
@@ -109,10 +145,10 @@ int cmd_hook(int argc, const char **argv, const char *prefix)
 	};
 
 	argc = parse_options(argc, argv, prefix, builtin_hook_options,
-			     builtin_hook_usage, 0);
+			     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN);
 
 	/* after the parse, we should have "<command> <hookname> <args...>" */
-	if (argc < 1)
+	if (argc < 2)
 		usage_with_options(builtin_hook_usage, builtin_hook_options);
 
 	git_config(git_default_config, NULL);
@@ -142,6 +178,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)
 
 	if (!strcmp(argv[0], "list"))
 		return list(argc, argv, prefix);
+	if (!strcmp(argv[0], "run"))
+		return run(argc, argv, prefix);
 
 	usage_with_options(builtin_hook_usage, builtin_hook_options);
 }
diff --git a/hook.c b/hook.c
index 65cbad8dba..b631da659b 100644
--- a/hook.c
+++ b/hook.c
@@ -3,13 +3,13 @@
 #include "hook.h"
 #include "config.h"
 #include "run-command.h"
+#include "prompt.h"
 
 void free_hook(struct hook *ptr)
 {
-	if (ptr) {
+	if (ptr)
 		strbuf_release(&ptr->command);
-		free(ptr);
-	}
+	free(ptr);
 }
 
 static struct hook * find_hook_by_command(struct list_head *head, const char *command)
@@ -143,6 +143,70 @@ enum hookdir_opt configured_hookdir_opt(void)
 	return HOOKDIR_UNKNOWN;
 }
 
+static int should_include_hookdir(const char *path, enum hookdir_opt cfg)
+{
+	struct strbuf prompt = STRBUF_INIT;
+	/*
+	 * If the path doesn't exist, don't bother adding the empty hook and
+	 * don't bother checking the config or prompting the user.
+	 */
+	if (!path)
+		return 0;
+
+	switch (cfg)
+	{
+	case HOOKDIR_ERROR:
+		fprintf(stderr, _("Skipping legacy hook at '%s'\n"),
+			path);
+		/* FALLTHROUGH */
+	case HOOKDIR_NO:
+		return 0;
+	case HOOKDIR_WARN:
+		fprintf(stderr, _("Running legacy hook at '%s'\n"),
+			path);
+		return 1;
+	case HOOKDIR_INTERACTIVE:
+		do {
+			/*
+			 * TRANSLATORS: Make sure to include [Y] and [n]
+			 * in your translation. Only English input is
+			 * accepted. Default option is "yes".
+			 */
+			fprintf(stderr, _("Run '%s'? [Y/n] "), path);
+			git_read_line_interactively(&prompt);
+			/*
+			 * In case of prompt = '' - that is, user hit enter,
+			 * saying "yes I want the default" - strncasecmp will
+			 * return 0 regardless. So list the default first.
+			 *
+			 * Case insensitively, accept "y", "ye", or "yes" as
+			 * "yes"; accept "n" or "no" as "no".
+			 */
+			if (!strncasecmp(prompt.buf, "yes", prompt.len)) {
+				strbuf_release(&prompt);
+				return 1;
+			} else if (!strncasecmp(prompt.buf, "no", prompt.len)) {
+				strbuf_release(&prompt);
+				return 0;
+			}
+			/* otherwise, we didn't understand the input */
+		} while (prompt.len); /* an empty reply means default (yes) */
+		return 1;
+	/*
+	 * HOOKDIR_UNKNOWN should match the default behavior, but let's
+	 * give a heads up to the user.
+	 */
+	case HOOKDIR_UNKNOWN:
+		fprintf(stderr,
+			_("Unrecognized value for 'hook.runHookDir'. "
+			  "Is there a typo? "));
+		/* FALLTHROUGH */
+	case HOOKDIR_YES:
+	default:
+		return 1;
+	}
+}
+
 struct list_head* hook_list(const char* hookname)
 {
 	struct strbuf hook_key = STRBUF_INIT;
@@ -176,3 +240,70 @@ struct list_head* hook_list(const char* hookname)
 	strbuf_release(&hook_key);
 	return hook_head;
 }
+
+void run_hooks_opt_init(struct run_hooks_opt *o)
+{
+	strvec_init(&o->env);
+	strvec_init(&o->args);
+	o->run_hookdir = configured_hookdir_opt();
+}
+
+void run_hooks_opt_clear(struct run_hooks_opt *o)
+{
+	strvec_clear(&o->env);
+	strvec_clear(&o->args);
+}
+
+static void prepare_hook_cp(const char *hookname, struct hook *hook,
+			    struct run_hooks_opt *options,
+			    struct child_process *cp)
+{
+	if (!hook)
+		return;
+
+	cp->no_stdin = 1;
+	cp->env = options->env.v;
+	cp->stdout_to_stderr = 1;
+	cp->trace2_hook_name = hookname;
+
+	/*
+	 * Commands from the config could be oneliners, but we know
+	 * for certain that hookdir commands are not.
+	 */
+	cp->use_shell = !hook->from_hookdir;
+
+	/* add command */
+	strvec_push(&cp->args, hook->command.buf);
+
+	/*
+	 * add passed-in argv, without expanding - let the user get back
+	 * exactly what they put in
+	 */
+	strvec_pushv(&cp->args, options->args.v);
+}
+
+int run_hooks(const char *hookname, struct run_hooks_opt *options)
+{
+	struct list_head *to_run, *pos = NULL, *tmp = NULL;
+	int rc = 0;
+
+	if (!options)
+		BUG("a struct run_hooks_opt must be provided to run_hooks");
+
+	to_run = hook_list(hookname);
+
+	list_for_each_safe(pos, tmp, to_run) {
+		struct child_process hook_proc = CHILD_PROCESS_INIT;
+		struct hook *hook = list_entry(pos, struct hook, list);
+
+		if (hook->from_hookdir &&
+		    !should_include_hookdir(hook->command.buf, options->run_hookdir))
+			continue;
+
+		prepare_hook_cp(hookname, hook, options, &hook_proc);
+
+		rc |= run_command(&hook_proc);
+	}
+
+	return rc;
+}
diff --git a/hook.h b/hook.h
index 7f2b2ee8f2..fb5132305f 100644
--- a/hook.h
+++ b/hook.h
@@