{"thread":{"id":"55286","subject":"[PATCH v8 04/37] hook: include hookdir hook in list","startedAt":"2021-03-11T02:11:34Z","lastAt":"2021-10-12T13:00:25Z","messageCount":479,"participants":["Emily Shaffer","Junio C Hamano","Ævar Arnfjörð Bjarmason","Eric Sunshine","Jeff Hostetler","Felipe Contreras","Taylor Blau","Bagas Sanjaya","Derrick Stolee","Jonathan Tan","Randall S. Becker"],"isPatch":true,"patchVersion":8,"patchTotal":37},"messages":[{"id":"418780","messageId":"20210311021037.3001235-5-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 04/37] hook: include hookdir hook in list","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:04Z","receivedAt":"2021-03-11T02:11:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Historically, hooks are declared by placing an executable into\n$GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken\nfrom the config are more featureful than hooks placed in the $HOOKDIR,\nthose hooks should not stop working for users who already have them.\nLet's list them to the user, but instead of displaying a config scope\n(e.g. \"global: blah\") we can prefix them with \"hookdir:\".\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Since v7, fix some nits from Jonathan Tan. The largest is to move reference to\n    \"hookdir annotation\" from this commit to the next one which introduces the\n    hook.runHookDir option.\n\n builtin/hook.c                | 11 +++++++++--\n hook.c                        | 17 +++++++++++++++++\n hook.h                        |  1 +\n t/t1360-config-based-hooks.sh | 19 +++++++++++++++++++\n 4 files changed, 46 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex bb64cd77ca..c8fbfbb39d 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -40,10 +40,15 @@ static int list(int argc, const char **argv, const char *prefix)\n \n \tlist_for_each(pos, head) {\n \t\tstruct hook *item = list_entry(pos, struct hook, list);\n-\t\tif (item)\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item) {\n+\t\t\t/* Don't translate 'hookdir' - it matches the config */\n \t\t\tprintf(\"%s: %s\\n\",\n-\t\t\t       config_scope_name(item->origin),\n+\t\t\t       (item->from_hookdir\n+\t\t\t\t? \"hookdir\"\n+\t\t\t\t: config_scope_name(item->origin)),\n \t\t\t       item->command.buf);\n+\t\t}\n \t}\n \n \tclear_hook_list(head);\n@@ -60,6 +65,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \tif (argc < 2)\n \t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n \n+\tgit_config(git_default_config, NULL);\n+\n \tif (!strcmp(argv[1], \"list\"))\n \t\treturn list(argc - 1, argv + 1, prefix);\n \ndiff --git a/hook.c b/hook.c\nindex fede40e925..080e25696b 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -2,6 +2,7 @@\n \n #include \"hook.h\"\n #include \"config.h\"\n+#include \"run-command.h\"\n \n void free_hook(struct hook *ptr)\n {\n@@ -35,6 +36,7 @@ static void append_or_move_hook(struct list_head *head, const char *command)\n \t\tto_add = xmalloc(sizeof(*to_add));\n \t\tstrbuf_init(&to_add->command, 0);\n \t\tstrbuf_addstr(&to_add->command, command);\n+\t\tto_add->from_hookdir = 0;\n \t}\n \n \t/* re-set the scope so we show where an override was specified */\n@@ -115,6 +117,21 @@ struct list_head* hook_list(const struct strbuf* hookname)\n \n \tgit_config(hook_config_lookup, &cb_data);\n \n+\tif (have_git_dir()) {\n+\t\tconst char *legacy_hook_path = find_hook(hookname->buf);\n+\n+\t\t/* Unconditionally add legacy hook, but annotate it. */\n+\t\tif (legacy_hook_path) {\n+\t\t\tstruct hook *legacy_hook;\n+\n+\t\t\tappend_or_move_hook(hook_head,\n+\t\t\t\t\t    absolute_path(legacy_hook_path));\n+\t\t\tlegacy_hook = list_entry(hook_head->prev, struct hook,\n+\t\t\t\t\t\t list);\n+\t\t\tlegacy_hook->from_hookdir = 1;\n+\t\t}\n+\t}\n+\n \tstrbuf_release(&hook_key);\n \treturn hook_head;\n }\ndiff --git a/hook.h b/hook.h\nindex e48dfc6d27..a97d43670d 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -11,6 +11,7 @@ struct hook {\n \tenum config_scope origin;\n \t/* The literal command to run. */\n \tstruct strbuf command;\n+\tunsigned from_hookdir : 1;\n };\n \n /*\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 6e4a3e763f..0f12af4659 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -23,6 +23,14 @@ setup_hookcmd () {\n \ttest_config_global hookcmd.abc.command \"/path/abc\" --add\n }\n \n+setup_hookdir () {\n+\tmkdir .git/hooks\n+\twrite_script .git/hooks/pre-commit <<-EOF\n+\techo \\\"Legacy Hook\\\"\n+\tEOF\n+\ttest_when_finished rm -rf .git/hooks\n+}\n+\n test_expect_success 'git hook rejects commands without a mode' '\n \ttest_must_fail git hook pre-commit\n '\n@@ -85,4 +93,15 @@ test_expect_success 'git hook list reorders on duplicate commands' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list shows hooks from the hookdir' '\n+\tsetup_hookdir &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418781","messageId":"20210311021037.3001235-2-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 01/37] doc: propose hooks managed by the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:01Z","receivedAt":"2021-03-11T02:11:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Begin a design document for config-based hooks, managed via git-hook.\nFocus on an overview of the implementation and motivation for design\ndecisions. Briefly discuss the alternatives considered before this\npoint. Also, attempt to redefine terms to fit into a multihook world.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Since v7, made some wording changes based on reviewer comments\n    (mostly Junio's, I think).\n\n - Emily\n\n Documentation/Makefile                        |   1 +\n .../technical/config-based-hooks.txt          | 369 ++++++++++++++++++\n 2 files changed, 370 insertions(+)\n create mode 100644 Documentation/technical/config-based-hooks.txt\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex 81d1bf7a04..2743de8995 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -82,6 +82,7 @@ SP_ARTICLES += $(API_DOCS)\n TECH_DOCS += MyFirstContribution\n TECH_DOCS += MyFirstObjectWalk\n TECH_DOCS += SubmittingPatches\n+TECH_DOCS += technical/config-based-hooks\n TECH_DOCS += technical/hash-function-transition\n TECH_DOCS += technical/http-protocol\n TECH_DOCS += technical/index-format\ndiff --git a/Documentation/technical/config-based-hooks.txt b/Documentation/technical/config-based-hooks.txt\nnew file mode 100644\nindex 0000000000..1f973117e4\n--- /dev/null\n+++ b/Documentation/technical/config-based-hooks.txt\n@@ -0,0 +1,369 @@\n+Configuration-based hook management\n+===================================\n+:sectanchors:\n+\n+[[motivation]]\n+== Motivation\n+\n+Replace the `.git/hook/hookname` path as the only source of hooks to execute;\n+allow users to define hooks using config files, in a way which is friendly to\n+users with multiple repos which have similar needs - hooks can be easily shared\n+between multiple Git repos.\n+\n+Redefine \"hook\" as an event rather than a single script, allowing users to\n+perform multiple unrelated actions on a single event.\n+\n+Make it easier for users to discover Git's hook feature and automate their\n+workflows.\n+\n+[[user-interfaces]]\n+== User interfaces\n+\n+[[config-schema]]\n+=== Config schema\n+\n+Hooks can be introduced by editing the configuration manually. There are two new\n+sections added, `hook` and `hookcmd`.\n+\n+[[config-schema-hook]]\n+==== `hook`\n+\n+Primarily contains subsections for each hook event. The order of variables in\n+these subsections defines the hook command execution order; hook commands can be\n+specified by setting the value directly to the command if no additional\n+configuration is needed, or by setting the value as the name of a `hookcmd`. If\n+Git does not find a `hookcmd` whose subsection matches the value of the given\n+command string, Git will try to execute the string directly. Hooks are executed\n+by passing the resolved command string to the shell. In the future, hook event\n+subsections could also contain per-hook-event settings; see\n+<<per-hook-event-settings,the section in Future Work>> for more details.\n+\n+Also contains top-level hook execution settings, for example, `hook.runHookDir`.\n+(These settings are described more in <<library,Library>>.)\n+\n+----\n+[hook \"pre-commit\"]\n+  command = perl-linter\n+  command = /usr/bin/git-secrets --pre-commit\n+\n+[hook \"pre-applypatch\"]\n+  command = perl-linter\n+  # for illustration purposes; error behavior isn't planned yet\n+  error = ignore\n+\n+[hook]\n+  runHookDir = interactive\n+----\n+\n+[[config-schema-hookcmd]]\n+==== `hookcmd`\n+\n+Defines a hook command and its attributes, which will be used when a hook event\n+occurs. Unqualified attributes are assumed to apply to this hook during all hook\n+events, but event-specific attributes can also be supplied. The example runs\n+`/usr/bin/lint-it --language=perl <args passed by Git>`, but for repos which\n+include this config, the hook command will be skipped for all events.\n+Theoretically, the last line could be used to \"un-skip\" the hook command for\n+`pre-commit` hooks, but this hasn't been scoped or implemented yet.\n+\n+----\n+[hookcmd \"perl-linter\"]\n+  command = /usr/bin/lint-it --language=perl\n+  skip = true\n+  # for illustration purposes; below hasn't been defined yet\n+  pre-commit-skip = false\n+----\n+\n+[[command-line-api]]\n+=== Command-line API\n+\n+Users should be able to view, run, reorder, and create hook commands via the\n+command line. External tools should be able to view a list of hooks in the\n+correct order to run. Modifier commands (`edit` and `add`) have not been\n+implemented yet and may not be if manually editing the config proves usable\n+enough.\n+\n+*`git hook list <hook-event>`*\n+\n+*`git hook run <hook-event> [-a <arg>]... [-e <env-var>]...`*\n+\n+*`git hook edit <hook-event>`*\n+\n+*`git hook add <hook-command> <hook-event> <options...>`*\n+\n+[[hook-editor]]\n+=== Hook editor\n+\n+The tool which is presented by `git hook edit <hook-command>`. Ideally, this\n+tool should be easier to use than manually editing the config, and then produce\n+a concise config afterwards. It may take a form similar to `git rebase\n+--interactive`. This has not been designed or implemented yet and may not be if\n+the config proves usable enough.\n+\n+[[implementation]]\n+== Implementation\n+\n+[[library]]\n+=== Library\n+\n+`hook.c` and `hook.h` are responsible for interacting with the config files. The\n+hook library provides a basic API to call all hooks in config order with more\n+complex options passed via `struct run_hooks_opt`:\n+\n+*`int run_hooks(const char *hookname, struct run_hooks_opt *options)`*\n+\n+`struct run_hooks_opt` allows callers to set:\n+\n+- environment variables\n+- command-line arguments\n+- behavior for the hook command provided by `run-command.h:find_hook()` (see\n+  below)\n+- a method to provide stdin to each hook, either via a file containing stdin, a\n+  `struct string_list` containing a list of lines to print, or a callback\n+  function to allow the caller to populate stdin manually\n+- a method to process stdout from each hook, e.g. for printing to sideband\n+  during a network operation\n+- parallelism\n+- a custom working directory for hooks to execute in\n+\n+And this struct can be extended with more options as necessary in the future.\n+\n+The \"legacy\" hook provided by `run-command.h:find_hook()` - that is, the hook\n+present in `.git/hooks/<hookname>` or\n+`$(git config --get core.hooksPath)/<hookname>` - can be handled in a number of\n+ways, providing an avenue to deprecate these \"legacy\" hooks if desired. The\n+handling is based on a config `hook.runHookDir`, which is checked against a\n+number of cases:\n+\n+- \"no\": the legacy hook will not be run\n+- \"error\": Git will print a warning to stderr before ignoring the legacy hook\n+- \"interactive\": Git will prompt the user before running the legacy hook\n+- \"warn\": Git will print a warning to stderr before running the legacy hook\n+- \"yes\" (default): Git will silently run the legacy hook\n+\n+In case this list is expanded in the future, if a value for `hook.runHookDir` is\n+given which Git does not recognize, Git should discard that config entry. For\n+example, if \"warn\" was specified at system level and \"junk\" was specified at\n+global level, Git would resolve the value to \"warn\"; if the only time the config\n+was set was to \"junk\", Git would use the default value of \"yes\" (but print a\n+warning to the user first to let them know their value is wrong).\n+\n+`struct hookcmd` is expected to grow in size over time as more functionality is\n+added to hooks; so that other parts of the code don't need to understand the\n+config schema, `struct hookcmd` should contain logical values instead of string\n+pairs.\n+\n+By default, hook parallelism is chosen based on the semantics of each hook;\n+callsites initialize their `struct run_hooks_opt` via one of two macros,\n+`RUN_HOOKS_OPT_INIT_SYNC` or `RUN_HOOKS_OPT_INIT_ASYNC`. The default number of\n+jobs can be configured in `hook.jobs`; this config applies across all hook\n+events. If unset, the value of `online_cpus()` (equivalent to `nproc`) is used.\n+\n+[[builtin]]\n+=== Builtin\n+\n+`builtin/hook.c` is responsible for providing the frontend. It's responsible for\n+formatting user-provided data and then calling the library API to set the\n+configs as appropriate. The builtin frontend is not responsible for calling the\n+config directly, so that other areas of Git can rely on the hook library to\n+understand the most recent config schema for hooks.\n+\n+[[migration]]\n+=== Migration path\n+\n+[[stage-0]]\n+==== Stage 0\n+\n+Hooks are called by running `run-command.h:find_hook()` with the hookname and\n+executing the result. The hook library and builtin do not exist. Hooks only\n+exist as specially named scripts within `.git/hooks/`.\n+\n+[[stage-1]]\n+==== Stage 1\n+\n+`git hook list --porcelain <hook-event>` is implemented. `hook.h:run_hooks()` is\n+taught to include `run-command.h:find_hook()` at the end; calls to `find_hook()`\n+are replaced with calls to `run_hooks()`. Users can opt-in to config-based hooks\n+simply by creating some in their config; otherwise users should remain\n+unaffected by the change.\n+\n+[[stage-2]]\n+==== Stage 2\n+\n+The call to `find_hook()` inside of `run_hooks()` learns to check for a config,\n+`hook.runHookDir`. Users can opt into managing their hooks completely via the\n+config this way.\n+\n+[[stage-3]]\n+==== Stage 3\n+\n+`.git/hooks` is removed from the template and the hook directory is considered\n+deprecated. To avoid breaking older repos, the default of `hook.runHookDir` is\n+not changed, and `find_hook()` is not removed.\n+\n+[[caveats]]\n+== Caveats\n+\n+[[security]]\n+=== Security and repo config\n+\n+Part of the motivation behind this refactor is to mitigate hooks as an attack\n+vector.footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/]\n+However, as the design stands, users can still provide hooks in the repo-level\n+config, which is included when a repo is zipped and sent elsewhere. The\n+security of the repo-level config is still under discussion; this design\n+generally assumes the repo-level config is secure, which is not true yet. This\n+assumption was made to avoid overcomplicating the design. So, this series\n+doesn't particularly improve security or resistance to zip attacks.\n+\n+[[ease-of-use]]\n+=== Ease of use\n+\n+The config schema is nontrivial; that's why it's important for the `git hook`\n+modifier commands to be usable. Contributors with UX expertise are encouraged to\n+share their suggestions.\n+\n+[[alternatives]]\n+== Alternative approaches\n+\n+A previous summary of alternatives exists in the\n+archives.footnote:[https://lore.kernel.org/git/20191116011125.GG22855@google.com]\n+\n+The table below shows a number of goals and how they might be achieved with\n+config-based hooks, by implementing directory support (i.e.\n+'.git/hooks/pre-commit.d'), or as hooks are run today.\n+\n+.Comparison of alternatives\n+|===\n+|Feature |Config-based hooks |Hook directories |Status quo\n+\n+|Supports multiple hooks\n+|Natively\n+|Natively\n+|With user effort\n+\n+|Supports parallelization\n+|Natively\n+|Natively\n+|No (user's multihook trampoline script would need to handle parallelism)\n+\n+|Safer for zipped repos\n+|A little\n+|No\n+|No\n+\n+|Previous hooks just work\n+|If configured\n+|Yes\n+|Yes\n+\n+|Can install one hook to many repos\n+|Yes\n+|With symlinks or core.hooksPath\n+|With symlinks or core.hooksPath\n+\n+|Discoverability\n+|Findable with 'git help git' or tab-completion via 'git hook' subcommand\n+|Findable via improved documentation\n+|Same as before\n+\n+|Hard to run unexpected hook\n+|If configured\n+|Could be made to warn or look for a config\n+|No\n+|===\n+\n+[[status-quo]]\n+=== Status quo\n+\n+Today users can implement multihooks themselves by using a \"trampoline script\"\n+as their hook, and pointing that script to a directory or list of other scripts\n+they wish to run.\n+\n+[[hook-directories]]\n+=== Hook directories\n+\n+Other contributors have suggested Git learn about the existence of a directory\n+such as `.git/hooks/<hookname>.d` and execute those hooks in alphabetical order.\n+\n+[[future-work]]\n+== Future work\n+\n+[[execution-ordering]]\n+=== Execution ordering\n+\n+We may find that config order is insufficient for some users; for example,\n+config order makes it difficult to add a new hook to the system or global config\n+which runs at the end of the hook list. A new ordering schema should be:\n+\n+1) Specified by a `hook.order` config, so that users will not unexpectedly see\n+their order change;\n+\n+2) Either dependency or numerically based.\n+\n+Dependency-based ordering is prone to classic linked-list problems, like a\n+cycles and handling of missing dependencies. But, it paves the way for enabling\n+parallelization if some tasks truly depend on others.\n+\n+Numerical ordering makes it tricky for Git to generate suggested ordering\n+numbers for each command, but is easy to determine a definitive order.\n+\n+[[parallelization]]\n+=== Parallelization with dependencies\n+\n+Currently hooks use a naive parallelization scheme or are run in series.  But if\n+one hook depends on another's output, then users will want to specify those\n+dependencies. If we decide to solve this problem, we may want to look to modern\n+build systems for inspiration on how to manage dependencies and parallel tasks.\n+\n+[[nontrivial-hooks]]\n+=== Multihooks and nontrivial output\n+\n+Some hooks - like 'proc-receive' - don't lend themselves well to multihooks at\n+all. In the case of 'proc-receive', for now, multiple hook definitions are\n+disallowed. In the future we might be able to conceive a better approach, for\n+example, running the hooks in series and using the output from one hook as the\n+input to the next.\n+\n+[[securing-hookdir-hooks]]\n+=== Securing hookdir hooks\n+\n+With the design as written in this doc, it's still possible for a malicious user\n+to modify `.git/config` to include `hook.pre-receive.command = rm -rf /`, then\n+zip their repo and send it to another user. It may be necessary to teach Git to\n+only allow inlined hooks like this if they were configured outside of the local\n+scope (in other words, only run hookcmds, and only allow hookcmds to be\n+configured in global or system scope); or another approach, like a list of safe\n+projects, might be useful. It may also be sufficient (or at least useful) to\n+teach a `hook.disableAll` config or similar flag to the Git executable.\n+\n+[[submodule-inheritance]]\n+=== Submodule inheritance\n+\n+It's possible some submodules may want to run the identical set of hooks that\n+their superrepo runs. While a globally-configured hook set is helpful, it's not\n+a great solution for users who have multiple repos-with-submodules under the\n+same user. It would be useful for submodules to learn how to run hooks from\n+their superrepo's config, or inherit that hook setting.\n+\n+[[per-hook-event-settings]]\n+=== Per-hook-event settings\n+\n+It might be desirable to keep settings specifically for some hook events, but\n+not for others - for example, a user may wish to disable hookdir hooks for all\n+events but pre-commit, which they haven't had time to convert yet; or, a user\n+may wish for execution order settings to differ based on hook event. In that\n+case, it would be useful to set something like `hook.pre-commit.executionOrder`\n+which would not apply to the 'prepare-commit-msg' hook, for example.\n+\n+[[glossary]]\n+== Glossary\n+\n+*hook event*\n+\n+A point during Git's execution where user scripts may be run, for example,\n+_prepare-commit-msg_ or _pre-push_.\n+\n+*hook command*\n+\n+A user script or executable which will be run on one or more hook events.\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418782","messageId":"20210311021037.3001235-1-emilyshaffer@google.com","threadId":"55286","inReplyTo":null,"subject":"[PATCH v8 00/37] config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:00Z","receivedAt":"2021-03-11T02:11:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Since v7:\n- Addressed Jonathan Tan's review of part I\n- Addressed Junio's review of part I and II\n- Combined parts I and II\n\nI think the updates to patch 1 between the rest of the work I've been\ndoing probably have covered Ævar's comments.\n\nMore details about per-patch changes found in the notes on each mail (I\nhope).\n\nI know that Junio was talking about merging v7 after Josh Steadmon's\nreview and I asked him not to - this reroll has those changes from\nJonathan Tan's review that I was wanting to wait for.\n\nThanks!\n - Emily\n\nEmily Shaffer (37):\n  doc: propose hooks managed by the config\n  hook: scaffolding for git-hook subcommand\n  hook: add list command\n  hook: include hookdir hook in list\n  hook: teach hook.runHookDir\n  hook: implement hookcmd.<name>.skip\n  parse-options: parse into strvec\n  hook: add 'run' subcommand\n  hook: introduce hook_exists()\n  hook: support passing stdin to hooks\n  run-command: allow stdin for run_processes_parallel\n  hook: allow parallel hook execution\n  hook: allow specifying working directory for hooks\n  run-command: add stdin callback for parallelization\n  hook: provide stdin by string_list or callback\n  run-command: allow capturing of collated output\n  hooks: allow callers to capture output\n  commit: use config-based hooks\n  am: convert applypatch hooks to use config\n  merge: use config-based hooks for post-merge hook\n  gc: use hook library for pre-auto-gc hook\n  rebase: teach pre-rebase to use hook.h\n  read-cache: convert post-index-change hook to use config\n  receive-pack: convert push-to-checkout hook to hook.h\n  git-p4: use 'git hook' to run hooks\n  hooks: convert 'post-checkout' hook to hook library\n  hook: convert 'post-rewrite' hook to config\n  transport: convert pre-push hook to use config\n  reference-transaction: look for hooks in config\n  receive-pack: convert 'update' hook to hook.h\n  proc-receive: acquire hook list from hook.h\n  post-update: use hook.h library\n  receive-pack: convert receive hooks to hook.h\n  bugreport: use hook_exists instead of find_hook\n  git-send-email: use 'git hook run' for 'sendemail-validate'\n  run-command: stop thinking about hooks\n  docs: unify githooks and git-hook manpages\n\n .gitignore                                    |   1 +\n Documentation/Makefile                        |   1 +\n Documentation/config/hook.txt                 |  27 +\n Documentation/git-hook.txt                    | 161 ++++\n Documentation/githooks.txt                    | 655 +---------------\n Documentation/native-hooks.txt                | 708 ++++++++++++++++++\n Documentation/technical/api-parse-options.txt |   7 +\n .../technical/config-based-hooks.txt          | 369 +++++++++\n Makefile                                      |   2 +\n builtin.h                                     |   1 +\n builtin/am.c                                  |  33 +-\n builtin/bugreport.c                           |   4 +-\n builtin/checkout.c                            |  19 +-\n builtin/clone.c                               |   8 +-\n builtin/commit.c                              |  11 +-\n builtin/fetch.c                               |   1 +\n builtin/gc.c                                  |   5 +-\n builtin/hook.c                                | 176 +++++\n builtin/merge.c                               |  15 +-\n builtin/rebase.c                              |   9 +-\n builtin/receive-pack.c                        | 329 ++++----\n builtin/submodule--helper.c                   |   2 +-\n builtin/worktree.c                            |  31 +-\n command-list.txt                              |   1 +\n commit.c                                      |  22 +-\n commit.h                                      |   3 +-\n git-p4.py                                     |  67 +-\n git-send-email.perl                           |  21 +-\n git.c                                         |   1 +\n hook.c                                        | 480 ++++++++++++\n hook.h                                        | 138 ++++\n parse-options-cb.c                            |  16 +\n parse-options.h                               |   4 +\n read-cache.c                                  |  13 +-\n refs.c                                        |  43 +-\n reset.c                                       |  16 +-\n run-command.c                                 | 156 ++--\n run-command.h                                 |  55 +-\n sequencer.c                                   |  90 +--\n submodule.c                                   |   1 +\n t/helper/test-run-command.c                   |  46 +-\n t/t0061-run-command.sh                        |  37 +\n t/t1360-config-based-hooks.sh                 | 303 ++++++++\n t/t1416-ref-transaction-hooks.sh              |  12 +-\n t/t5411/test-0015-too-many-hooks-error.sh     |  47 ++\n ...3-pre-commit-and-pre-merge-commit-hooks.sh |  17 +-\n t/t9001-send-email.sh                         |  11 +-\n transport.c                                   |  59 +-\n 48 files changed, 3052 insertions(+), 1182 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 Documentation/native-hooks.txt\n create mode 100644 Documentation/technical/config-based-hooks.txt\n create mode 100644 builtin/hook.c\n create mode 100644 hook.c\n create mode 100644 hook.h\n create mode 100755 t/t1360-config-based-hooks.sh\n create mode 100644 t/t5411/test-0015-too-many-hooks-error.sh\n\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418783","messageId":"20210311021037.3001235-6-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 05/37] hook: teach hook.runHookDir","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:05Z","receivedAt":"2021-03-11T02:11:35Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"For now, just give a hint about how these hooks will be run in 'git hook\nlist'. Later on, though, we will pay attention to this enum when running\nthe hooks.\n---\n\nNotes:\n    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to\n    enforce it - now it matches the design doc much better.\n    \n    Also, thanks Jonathan Tan for pointing out that the commit message made no sense\n    and was targeted for a different change. Rewrote the commit message now.\n    \n    Plus, added HOOK_ERROR flag per Junio and Jonathan Nieder.\n\n - Emily\n\n Documentation/config/hook.txt |  5 +++\n builtin/hook.c                | 69 +++++++++++++++++++++++++++++++---\n hook.c                        | 24 ++++++++++++\n hook.h                        | 16 ++++++++\n t/t1360-config-based-hooks.sh | 71 +++++++++++++++++++++++++++++++++++\n 5 files changed, 180 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 71449ecbc7..75312754ae 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -7,3 +7,8 @@ hookcmd.<name>.command::\n \tA command to execute during a hook for which <name> has been specified\n \tas a command. This can be an executable on your device or a oneliner for\n \tyour shell. See linkgit:git-hook[1].\n+\n+hook.runHookDir::\n+\tControls how hooks contained in your hookdir are executed. Can be any of\n+\t\"yes\", \"warn\", \"interactive\", or \"no\". Defaults to \"yes\". See\n+\tlinkgit:git-hook[1] and linkgit:git-config[1] \"core.hooksPath\").\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex c8fbfbb39d..310f696ebf 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -10,10 +10,13 @@ static const char * const builtin_hook_usage[] = {\n \tNULL\n };\n \n+static enum hookdir_opt should_run_hookdir;\n+\n static int list(int argc, const char **argv, const char *prefix)\n {\n \tstruct list_head *head, *pos;\n \tstruct strbuf hookname = STRBUF_INIT;\n+\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n \n \tstruct option list_options[] = {\n \t\tOPT_END(),\n@@ -38,20 +41,48 @@ static int list(int argc, const char **argv, const char *prefix)\n \t\treturn 0;\n \t}\n \n+\tswitch (should_run_hookdir) {\n+\t\tcase HOOKDIR_NO:\n+\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will not run)\"));\n+\t\t\tbreak;\n+\t\tcase HOOKDIR_ERROR:\n+\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will error and not run)\"));\n+\t\t\tbreak;\n+\t\tcase HOOKDIR_INTERACTIVE:\n+\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will prompt)\"));\n+\t\t\tbreak;\n+\t\tcase HOOKDIR_WARN:\n+\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will warn but run)\"));\n+\t\t\tbreak;\n+\t\tcase HOOKDIR_YES:\n+\t\t/*\n+\t\t * The default behavior should agree with\n+\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n+\t\t * do the default behavior.\n+\t\t */\n+\t\tcase HOOKDIR_UNKNOWN:\n+\t\tdefault:\n+\t\t\tbreak;\n+\t}\n+\n \tlist_for_each(pos, head) {\n \t\tstruct hook *item = list_entry(pos, struct hook, list);\n \t\titem = list_entry(pos, struct hook, list);\n \t\tif (item) {\n \t\t\t/* Don't translate 'hookdir' - it matches the config */\n-\t\t\tprintf(\"%s: %s\\n\",\n+\t\t\tprintf(\"%s: %s%s\\n\",\n \t\t\t       (item->from_hookdir\n \t\t\t\t? \"hookdir\"\n \t\t\t\t: config_scope_name(item->origin)),\n-\t\t\t       item->command.buf);\n+\t\t\t       item->command.buf,\n+\t\t\t       (item->from_hookdir\n+\t\t\t\t? hookdir_annotation.buf\n+\t\t\t\t: \"\"));\n \t\t}\n \t}\n \n \tclear_hook_list(head);\n+\tstrbuf_release(&hookdir_annotation);\n \tstrbuf_release(&hookname);\n \n \treturn 0;\n@@ -59,16 +90,44 @@ static int list(int argc, const char **argv, const char *prefix)\n \n int cmd_hook(int argc, const char **argv, const char *prefix)\n {\n+\tconst char *run_hookdir = NULL;\n+\n \tstruct option builtin_hook_options[] = {\n+\t\tOPT_STRING(0, \"run-hookdir\", &run_hookdir, N_(\"option\"),\n+\t\t\t   N_(\"what to do with hooks found in the hookdir\")),\n \t\tOPT_END(),\n \t};\n-\tif (argc < 2)\n+\n+\targc = parse_options(argc, argv, prefix, builtin_hook_options,\n+\t\t\t     builtin_hook_usage, 0);\n+\n+\t/* after the parse, we should have \"<command> <hookname> <args...>\" */\n+\tif (argc < 1)\n \t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n \n \tgit_config(git_default_config, NULL);\n \n-\tif (!strcmp(argv[1], \"list\"))\n-\t\treturn list(argc - 1, argv + 1, prefix);\n+\n+\t/* argument > config */\n+\tif (run_hookdir)\n+\t\tif (!strcmp(run_hookdir, \"no\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_NO;\n+\t\telse if (!strcmp(run_hookdir, \"error\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_ERROR;\n+\t\telse if (!strcmp(run_hookdir, \"yes\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_YES;\n+\t\telse if (!strcmp(run_hookdir, \"warn\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_WARN;\n+\t\telse if (!strcmp(run_hookdir, \"interactive\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_INTERACTIVE;\n+\t\telse\n+\t\t\tdie(_(\"'%s' is not a valid option for --run-hookdir \"\n+\t\t\t      \"(yes, warn, interactive, no)\"), run_hookdir);\n+\telse\n+\t\tshould_run_hookdir = configured_hookdir_opt();\n+\n+\tif (!strcmp(argv[0], \"list\"))\n+\t\treturn list(argc, argv, prefix);\n \n \tusage_with_options(builtin_hook_usage, builtin_hook_options);\n }\ndiff --git a/hook.c b/hook.c\nindex 080e25696b..039ff0a378 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -102,6 +102,30 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n \treturn 0;\n }\n \n+enum hookdir_opt configured_hookdir_opt(void)\n+{\n+\tconst char *key;\n+\tif (git_config_get_value(\"hook.runhookdir\", &key))\n+\t\treturn HOOKDIR_YES; /* by default, just run it. */\n+\n+\tif (!strcmp(key, \"no\"))\n+\t\treturn HOOKDIR_NO;\n+\n+\tif (!strcmp(key, \"error\"))\n+\t\treturn HOOKDIR_ERROR;\n+\n+\tif (!strcmp(key, \"yes\"))\n+\t\treturn HOOKDIR_YES;\n+\n+\tif (!strcmp(key, \"warn\"))\n+\t\treturn HOOKDIR_WARN;\n+\n+\tif (!strcmp(key, \"interactive\"))\n+\t\treturn HOOKDIR_INTERACTIVE;\n+\n+\treturn HOOKDIR_UNKNOWN;\n+}\n+\n struct list_head* hook_list(const struct strbuf* hookname)\n {\n \tstruct strbuf hook_key = STRBUF_INIT;\ndiff --git a/hook.h b/hook.h\nindex a97d43670d..1c4b953aec 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -20,6 +20,22 @@ struct hook {\n  */\n struct list_head* hook_list(const struct strbuf *hookname);\n \n+enum hookdir_opt\n+{\n+\tHOOKDIR_NO,\n+\tHOOKDIR_ERROR,\n+\tHOOKDIR_WARN,\n+\tHOOKDIR_INTERACTIVE,\n+\tHOOKDIR_YES,\n+\tHOOKDIR_UNKNOWN,\n+};\n+\n+/*\n+ * Provides the hookdir_opt specified in the config without consulting any\n+ * command line arguments.\n+ */\n+enum hookdir_opt configured_hookdir_opt(void);\n+\n /* Free memory associated with a 'struct hook' */\n void free_hook(struct hook *ptr);\n /* Empties the list at 'head', calling 'free_hook()' on each entry */\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 0f12af4659..66b0b6b7ad 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -104,4 +104,75 @@ test_expect_success 'git hook list shows hooks from the hookdir' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'hook.runHookDir = no is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"no\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will not run)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_i18ncmp expected actual\n+'\n+\n+test_expect_success 'hook.runHookDir = error is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"error\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will error and not run)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_i18ncmp expected actual\n+'\n+\n+test_expect_success 'hook.runHookDir = warn is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"warn\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will warn but run)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_i18ncmp expected actual\n+'\n+\n+\n+test_expect_success 'hook.runHookDir = interactive is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"interactive\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will prompt)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_i18ncmp expected actual\n+'\n+\n+test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"junk\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_i18ncmp expected actual\n+'\n+\n test_done\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418784","messageId":"20210311021037.3001235-8-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 07/37] parse-options: parse into strvec","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:07Z","receivedAt":"2021-03-11T02:12:06Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"parse-options already knows how to read into a string_list, and it knows\nhow to read into an strvec as a passthrough (that is, including the\nargument as well as its value). string_list and strvec serve similar\npurposes but are somewhat painful to convert between; so, let's teach\nparse-options to read values of string arguments directly into an\nstrvec without preserving the argument name.\n\nThis is useful if collecting generic arguments to pass through to\nanother command, for example, 'git hook run --arg \"--quiet\" --arg\n\"--format=pretty\" some-hook'. The resulting strvec would contain\n{ \"--quiet\", \"--format=pretty\" }.\n\nThe implementation is based on that of OPT_STRING_LIST.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Since v7, updated the reference doc to make the intended usage for OPT_STRVEC\n    more clear.\n    \n    Since v4, fixed one or two more places where I missed the argv_array->strvec\n    rename.\n\n Documentation/technical/api-parse-options.txt |  7 +++++++\n parse-options-cb.c                            | 16 ++++++++++++++++\n parse-options.h                               |  4 ++++\n 3 files changed, 27 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex 5a60bbfa7f..f79b17e7fc 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -173,6 +173,13 @@ There are some macros to easily define options:\n \tThe string argument is stored as an element in `string_list`.\n \tUse of `--no-option` will clear the list of preceding values.\n \n+`OPT_STRVEC(short, long, &struct strvec, arg_str, description)`::\n+\tIntroduce an option with a string argument, meant to be specified\n+\tmultiple times.\n+\tThe string argument is stored as an element in `strvec`, and later\n+\targuments are added to the same `strvec`.\n+\tUse of `--no-option` will clear the list of preceding values.\n+\n `OPT_INTEGER(short, long, &int_var, description)`::\n \tIntroduce an option with integer argument.\n \tThe integer is put into `int_var`.\ndiff --git a/parse-options-cb.c b/parse-options-cb.c\nindex 4542d4d3f9..c2451dfb1b 100644\n--- a/parse-options-cb.c\n+++ b/parse-options-cb.c\n@@ -207,6 +207,22 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset)\n \treturn 0;\n }\n \n+int parse_opt_strvec(const struct option *opt, const char *arg, int unset)\n+{\n+\tstruct strvec *v = opt->value;\n+\n+\tif (unset) {\n+\t\tstrvec_clear(v);\n+\t\treturn 0;\n+\t}\n+\n+\tif (!arg)\n+\t\treturn -1;\n+\n+\tstrvec_push(v, arg);\n+\treturn 0;\n+}\n+\n int parse_opt_noop_cb(const struct option *opt, const char *arg, int unset)\n {\n \treturn 0;\ndiff --git a/parse-options.h b/parse-options.h\nindex ff6506a504..44c4ac08e9 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -177,6 +177,9 @@ struct option {\n #define OPT_STRING_LIST(s, l, v, a, h) \\\n \t\t\t\t    { OPTION_CALLBACK, (s), (l), (v), (a), \\\n \t\t\t\t      (h), 0, &parse_opt_string_list }\n+#define OPT_STRVEC(s, l, v, a, h) \\\n+\t\t\t\t    { OPTION_CALLBACK, (s), (l), (v), (a), \\\n+\t\t\t\t      (h), 0, &parse_opt_strvec }\n #define OPT_UYN(s, l, v, h)         { OPTION_CALLBACK, (s), (l), (v), NULL, \\\n \t\t\t\t      (h), PARSE_OPT_NOARG, &parse_opt_tertiary }\n #define OPT_EXPIRY_DATE(s, l, v, h) \\\n@@ -296,6 +299,7 @@ int parse_opt_commits(const struct option *, const char *, int);\n int parse_opt_commit(const struct option *, const char *, int);\n int parse_opt_tertiary(const struct option *, const char *, int);\n int parse_opt_string_list(const struct option *, const char *, int);\n+int parse_opt_strvec(const struct option *, const char *, int);\n int parse_opt_noop_cb(const struct option *, const char *, int);\n enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t   const struct option *,\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418785","messageId":"20210311021037.3001235-10-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 09/37] hook: introduce hook_exists()","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:09Z","receivedAt":"2021-03-11T02:12:07Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Add a helper to easily determine whether any hooks exist for a given\nhook event.\n\nMany callers want to check whether some state could be modified by a\nhook; that check should include the config-based hooks as well. Optimize\nby checking the config directly. Since commands which execute hooks\nmight want to take args to replace 'hook.runHookDir', let\n'hook_exists()' take a hookdir_opt to override that config.\n\nIn some cases, external callers today use find_hook() to discover the\nlocation of a hook and then run it manually with run-command.h (that is,\nnot with run_hook_le()). Later, those cases will call hook.h:run_hook()\ndirectly instead.\n\nOnce the entire codebase is using hook_exists() instead of find_hook(),\nfind_hook() can be safely rolled into hook_exists() and removed from\nrun-command.h.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n hook.c | 19 +++++++++++++++++++\n hook.h | 10 ++++++++++\n 2 files changed, 29 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex d166d17fb0..118931f273 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -243,6 +243,25 @@ void run_hooks_opt_init(struct run_hooks_opt *o)\n \to->run_hookdir = configured_hookdir_opt();\n }\n \n+int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n+{\n+\tconst char *value = NULL; /* throwaway */\n+\tstruct strbuf hook_key = STRBUF_INIT;\n+\tint could_run_hookdir;\n+\n+\tif (should_run_hookdir == HOOKDIR_USE_CONFIG)\n+\t\tshould_run_hookdir = configured_hookdir_opt();\n+\n+\tcould_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||\n+\t\t\t\tshould_run_hookdir == HOOKDIR_WARN ||\n+\t\t\t\tshould_run_hookdir == HOOKDIR_YES)\n+\t\t\t\t&& !!find_hook(hookname);\n+\n+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n+\n+\treturn (!git_config_get_value(hook_key.buf, &value)) || could_run_hookdir;\n+}\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o)\n {\n \tstrvec_clear(&o->env);\ndiff --git a/hook.h b/hook.h\nindex c24b2c9ecd..0df785add5 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -23,6 +23,7 @@ struct list_head* hook_list(const struct strbuf *hookname);\n \n enum hookdir_opt\n {\n+\tHOOKDIR_USE_CONFIG,\n \tHOOKDIR_NO,\n \tHOOKDIR_ERROR,\n \tHOOKDIR_WARN,\n@@ -56,6 +57,15 @@ struct run_hooks_opt\n void run_hooks_opt_init(struct run_hooks_opt *o);\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n+/*\n+ * Returns 1 if any hooks are specified in the config or if a hook exists in the\n+ * hookdir. Typically, invoke hook_exsts() like:\n+ *   hook_exists(hookname, configured_hookdir_opt());\n+ * Like with run_hooks, if you take a --run-hookdir flag, reflect that\n+ * user-specified behavior here instead.\n+ */\n+int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);\n+\n /*\n  * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n  * passed 'env' and 'args'.\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418786","messageId":"20210311021037.3001235-12-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 11/37] run-command: allow stdin for run_processes_parallel","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:11Z","receivedAt":"2021-03-11T02:12:07Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"While it makes sense not to inherit stdin from the parent process to\navoid deadlocking, it's not necessary to completely ban stdin to\nchildren. An informed user should be able to configure stdin safely. By\nsetting `some_child.process.no_stdin=1` before calling `get_next_task()`\nwe provide a reasonable default behavior but enable users to set up\nstdin streaming for themselves during the callback.\n\n`some_child.process.stdout_to_stderr`, however, remains unmodifiable by\n`get_next_task()` - the rest of the run_processes_parallel() API depends\non child output in stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n run-command.c | 9 ++++++++-\n 1 file changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex 4e34623e2e..e6d7541b84 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1693,6 +1693,14 @@ static int pp_start_one(struct parallel_processes *pp)\n \tif (i == pp->max_processes)\n \t\tBUG(\"bookkeeping is hard\");\n \n+\t/*\n+\t * By default, do not inherit stdin from the parent process - otherwise,\n+\t * all children would share stdin! Users may overwrite this to provide\n+\t * something to the child's stdin by having their 'get_next_task'\n+\t * callback assign 0 to .no_stdin and an appropriate integer to .in.\n+\t */\n+\tpp->children[i].process.no_stdin = 1;\n+\n \tcode = pp->get_next_task(&pp->children[i].process,\n \t\t\t\t &pp->children[i].err,\n \t\t\t\t pp->data,\n@@ -1704,7 +1712,6 @@ static int pp_start_one(struct parallel_processes *pp)\n \t}\n \tpp->children[i].process.err = -1;\n \tpp->children[i].process.stdout_to_stderr = 1;\n-\tpp->children[i].process.no_stdin = 1;\n \n \tif (start_command(&pp->children[i].process)) {\n \t\tcode = pp->start_failure(&pp->children[i].err,\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418787","messageId":"20210311021037.3001235-3-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 02/37] hook: scaffolding for git-hook subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:02Z","receivedAt":"2021-03-11T02:12:07Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Introduce infrastructure for a new subcommand, git-hook, which will be\nused to ease config-based hook management. This command will handle\nparsing configs to compose a list of hooks to run for a given event, as\nwell as adding or modifying hook configs in an interactive fashion.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\nNo change since v7.\n\n .gitignore                    |  1 +\n Documentation/git-hook.txt    | 20 ++++++++++++++++++++\n Makefile                      |  1 +\n builtin.h                     |  1 +\n builtin/hook.c                | 21 +++++++++++++++++++++\n command-list.txt              |  1 +\n git.c                         |  1 +\n t/t1360-config-based-hooks.sh | 11 +++++++++++\n 8 files changed, 57 insertions(+)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100755 t/t1360-config-based-hooks.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 3dcdb6bb5a..3608c35b73 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -76,6 +76,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-hook\n /git-http-backend\n /git-http-fetch\n /git-http-push\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nnew file mode 100644\nindex 0000000000..9eeab0009d\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,20 @@\n+git-hook(1)\n+===========\n+\n+NAME\n+----\n+git-hook - Manage configured hooks\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git hook'\n+\n+DESCRIPTION\n+-----------\n+A placeholder command. Later, you will be able to list, add, and modify hooks\n+with this command.\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex dfb0f1000f..8e904a1ab5 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1087,6 +1087,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\ndiff --git a/builtin.h b/builtin.h\nindex b6ce981b73..8df1d36a7a 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -163,6 +163,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);\n int cmd_grep(int argc, const char **argv, const char *prefix);\n int cmd_hash_object(int argc, const char **argv, const char *prefix);\n int cmd_help(int argc, const char **argv, const char *prefix);\n+int cmd_hook(int argc, const char **argv, const char *prefix);\n int cmd_index_pack(int argc, const char **argv, const char *prefix);\n int cmd_init_db(int argc, const char **argv, const char *prefix);\n int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/hook.c b/builtin/hook.c\nnew file mode 100644\nindex 0000000000..b2bbc84d4d\n--- /dev/null\n+++ b/builtin/hook.c\n@@ -0,0 +1,21 @@\n+#include \"cache.h\"\n+\n+#include \"builtin.h\"\n+#include \"parse-options.h\"\n+\n+static const char * const builtin_hook_usage[] = {\n+\tN_(\"git hook\"),\n+\tNULL\n+};\n+\n+int cmd_hook(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option builtin_hook_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, builtin_hook_options,\n+\t\t\t     builtin_hook_usage, 0);\n+\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex a289f09ed6..9ccd8e5aeb 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-hook                                mainporcelain\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\n git-http-push                           synchelpers\ndiff --git a/git.c b/git.c\nindex 9bc077a025..14adac716f 100644\n--- a/git.c\n+++ b/git.c\n@@ -528,6 +528,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nnew file mode 100755\nindex 0000000000..34b0df5216\n--- /dev/null\n+++ b/t/t1360-config-based-hooks.sh\n@@ -0,0 +1,11 @@\n+#!/bin/bash\n+\n+test_description='config-managed multihooks, including git-hook command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'git hook command does not crash' '\n+\tgit hook\n+'\n+\n+test_done\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418788","messageId":"20210311021037.3001235-9-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 08/37] hook: add 'run' subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:08Z","receivedAt":"2021-03-11T02:12:07Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"In order to enable hooks to be run as an external process, by a\nstandalone Git command, or by tools which wrap Git, provide an external\nmeans to run all configured hook commands for a given hook event.\n\nFor now, the hook commands will run in config order, in series. As\nalternate ordering or parallelism is supported in the future, we should\nadd knobs to use those to the command line as well.\n\nAs with the legacy hook implementation, all stdout generated by hook\ncommands is redirected to stderr. Piping from stdin is not yet\nsupported.\n\nLegacy hooks (those present in $GITDIR/hooks) are run at the end of the\nexecution list. They can be disabled, or made to print warnings, or to\nprompt before running, with the 'hook.runHookDir' config.\n\nUsers may wish to provide hook commands like 'git config\nhook.pre-commit.command \"~/linter.sh --pre-commit\"'. To enable this,\nconfig-defined hooks are run in a shell. (Since hooks in $GITDIR/hooks\ncan't be specified with included arguments or paths which need expansion\nlike this, they are run without a shell instead.)\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Since v7, added support for \"error\" hook.runHookDir setting.\n    \n    Since v4, updated the docs, and did less local application of single\n    quotes. In order for hookdir hooks to run successfully with a space in\n    the path, though, they must not be run with 'sh -c'. So we can treat the\n    hookdir hooks specially, and warn users via doc about special\n    considerations for configured hooks with spaces in their path.\n\n Documentation/git-hook.txt    |  31 +++++++-\n builtin/hook.c                |  42 ++++++++++-\n hook.c                        | 128 ++++++++++++++++++++++++++++++++++\n hook.h                        |  26 +++++++\n t/t1360-config-based-hooks.sh |  72 ++++++++++++++++++-\n 5 files changed, 292 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex c84520cb38..8f96c347ea 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -9,11 +9,12 @@ SYNOPSIS\n --------\n [verse]\n 'git hook' list <hook-name>\n+'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>\n \n DESCRIPTION\n -----------\n-You can list configured hooks with this command. Later, you will be able to run,\n-add, and modify hooks with this command.\n+You can list and run configured hooks with this command. Later, you will be able\n+to add and modify hooks with this command.\n \n This command parses the default configuration files for sections `hook` and\n `hookcmd`. `hook` is used to describe the commands which will be run during a\n@@ -97,6 +98,32 @@ in the order they should be run, and print the config scope where the relevant\n `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n This output is human-readable and the format is subject to change over time.\n \n+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::\n+\n+Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n+hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n+paths containing special characters or spaces should be wrapped in single\n+quotes: `command = '/my/path with spaces/script.sh' some args`.\n+\n+OPTIONS\n+-------\n+--run-hookdir::\n+\tOverrides the hook.runHookDir config. Must be 'yes', 'warn',\n+\t'interactive', or 'no'. Specifies how to handle hooks located in the Git\n+\thook directory (core.hooksPath).\n+\n+-a::\n+--arg::\n+\tOnly valid for `run`.\n++\n+Specify arguments to pass to every hook that is run.\n+\n+-e::\n+--env::\n+\tOnly valid for `run`.\n++\n+Specify environment variables to set for every hook that is run.\n+\n CONFIGURATION\n -------------\n include::config/hook.txt[]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 310f696ebf..e823a96238 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -4,9 +4,11 @@\n #include \"hook.h\"\n #include \"parse-options.h\"\n #include \"strbuf.h\"\n+#include \"strvec.h\"\n \n static const char * const builtin_hook_usage[] = {\n \tN_(\"git hook list <hookname>\"),\n+\tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hookname>\"),\n \tNULL\n };\n \n@@ -88,6 +90,40 @@ static int list(int argc, const char **argv, const char *prefix)\n \treturn 0;\n }\n \n+static int run(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct strbuf hookname = STRBUF_INIT;\n+\tstruct run_hooks_opt opt;\n+\tint rc = 0;\n+\n+\tstruct option run_options[] = {\n+\t\tOPT_STRVEC('e', \"env\", &opt.env, N_(\"var\"),\n+\t\t\t   N_(\"environment variables for hook to use\")),\n+\t\tOPT_STRVEC('a', \"arg\", &opt.args, N_(\"args\"),\n+\t\t\t   N_(\"argument to pass to hook\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\trun_hooks_opt_init(&opt);\n+\n+\targc = parse_options(argc, argv, prefix, run_options,\n+\t\t\t     builtin_hook_usage, 0);\n+\n+\tif (argc < 1)\n+\t\tusage_msg_opt(_(\"You must specify a hook event to run.\"),\n+\t\t\t      builtin_hook_usage, run_options);\n+\n+\tstrbuf_addstr(&hookname, argv[0]);\n+\topt.run_hookdir = should_run_hookdir;\n+\n+\trc = run_hooks(hookname.buf, &opt);\n+\n+\tstrbuf_release(&hookname);\n+\trun_hooks_opt_clear(&opt);\n+\n+\treturn rc;\n+}\n+\n int cmd_hook(int argc, const char **argv, const char *prefix)\n {\n \tconst char *run_hookdir = NULL;\n@@ -99,10 +135,10 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \t};\n \n \targc = parse_options(argc, argv, prefix, builtin_hook_options,\n-\t\t\t     builtin_hook_usage, 0);\n+\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN);\n \n \t/* after the parse, we should have \"<command> <hookname> <args...>\" */\n-\tif (argc < 1)\n+\tif (argc < 2)\n \t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n \n \tgit_config(git_default_config, NULL);\n@@ -128,6 +164,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \n \tif (!strcmp(argv[0], \"list\"))\n \t\treturn list(argc, argv, prefix);\n+\tif (!strcmp(argv[0], \"run\"))\n+\t\treturn run(argc, argv, prefix);\n \n \tusage_with_options(builtin_hook_usage, builtin_hook_options);\n }\ndiff --git a/hook.c b/hook.c\nindex 37b740d58d..d166d17fb0 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -3,6 +3,7 @@\n #include \"hook.h\"\n #include \"config.h\"\n #include \"run-command.h\"\n+#include \"prompt.h\"\n \n void free_hook(struct hook *ptr)\n {\n@@ -143,6 +144,64 @@ enum hookdir_opt configured_hookdir_opt(void)\n \treturn HOOKDIR_UNKNOWN;\n }\n \n+static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n+{\n+\tstruct strbuf prompt = STRBUF_INIT;\n+\t/*\n+\t * If the path doesn't exist, don't bother adding the empty hook and\n+\t * don't bother checking the config or prompting the user.\n+\t */\n+\tif (!path)\n+\t\treturn 0;\n+\n+\tswitch (cfg)\n+\t{\n+\t\tcase HOOKDIR_ERROR:\n+\t\t\tfprintf(stderr, _(\"Skipping legacy hook at '%s'\\n\"),\n+\t\t\t\tpath);\n+\t\t\t/* FALLTHROUGH */\n+\t\tcase HOOKDIR_NO:\n+\t\t\treturn 0;\n+\t\tcase HOOKDIR_WARN:\n+\t\t\tfprintf(stderr, _(\"Running legacy hook at '%s'\\n\"),\n+\t\t\t\tpath);\n+\t\t\treturn 1;\n+\t\tcase HOOKDIR_INTERACTIVE:\n+\t\t\tdo {\n+\t\t\t\t/*\n+\t\t\t\t * TRANSLATORS: Make sure to include [Y] and [n]\n+\t\t\t\t * in your translation. Only English input is\n+\t\t\t\t * accepted. Default option is \"yes\".\n+\t\t\t\t */\n+\t\t\t\tfprintf(stderr, _(\"Run '%s'? [Yn] \"), path);\n+\t\t\t\tgit_read_line_interactively(&prompt);\n+\t\t\t\tstrbuf_tolower(&prompt);\n+\t\t\t\tif (starts_with(prompt.buf, \"n\")) {\n+\t\t\t\t\tstrbuf_release(&prompt);\n+\t\t\t\t\treturn 0;\n+\t\t\t\t} else if (starts_with(prompt.buf, \"y\")) {\n+\t\t\t\t\tstrbuf_release(&prompt);\n+\t\t\t\t\treturn 1;\n+\t\t\t\t}\n+\t\t\t\t/* otherwise, we didn't understand the input */\n+\t\t\t} while (prompt.len); /* an empty reply means \"Yes\" */\n+\t\t\tstrbuf_release(&prompt);\n+\t\t\treturn 1;\n+\t\t/*\n+\t\t * HOOKDIR_UNKNOWN should match the default behavior, but let's\n+\t\t * give a heads up to the user.\n+\t\t */\n+\t\tcase HOOKDIR_UNKNOWN:\n+\t\t\tfprintf(stderr,\n+\t\t\t\t_(\"Unrecognized value for 'hook.runHookDir'. \"\n+\t\t\t\t  \"Is there a typo? \"));\n+\t\t\t/* FALLTHROUGH */\n+\t\tcase HOOKDIR_YES:\n+\t\tdefault:\n+\t\t\treturn 1;\n+\t}\n+}\n+\n struct list_head* hook_list(const struct strbuf* hookname)\n {\n \tstruct strbuf hook_key = STRBUF_INIT;\n@@ -176,3 +235,72 @@ struct list_head* hook_list(const struct strbuf* hookname)\n \tstrbuf_release(&hook_key);\n \treturn hook_head;\n }\n+\n+void run_hooks_opt_init(struct run_hooks_opt *o)\n+{\n+\tstrvec_init(&o->env);\n+\tstrvec_init(&o->args);\n+\to->run_hookdir = configured_hookdir_opt();\n+}\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o)\n+{\n+\tstrvec_clear(&o->env);\n+\tstrvec_clear(&o->args);\n+}\n+\n+static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n+\t\t\t    struct child_process *cp)\n+{\n+\tif (!hook)\n+\t\treturn;\n+\n+\tcp->no_stdin = 1;\n+\tcp->env = options->env.v;\n+\tcp->stdout_to_stderr = 1;\n+\tcp->trace2_hook_name = hook->command.buf;\n+\n+\t/*\n+\t * Commands from the config could be oneliners, but we know\n+\t * for certain that hookdir commands are not.\n+\t */\n+\tcp->use_shell = !hook->from_hookdir;\n+\n+\t/* add command */\n+\tstrvec_push(&cp->args, hook->command.buf);\n+\n+\t/*\n+\t * add passed-in argv, without expanding - let the user get back\n+\t * exactly what they put in\n+\t */\n+\tstrvec_pushv(&cp->args, options->args.v);\n+}\n+\n+int run_hooks(const char *hookname, struct run_hooks_opt *options)\n+{\n+\tstruct strbuf hookname_str = STRBUF_INIT;\n+\tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n+\tint rc = 0;\n+\n+\tif (!options)\n+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n+\n+\tstrbuf_addstr(&hookname_str, hookname);\n+\n+\tto_run = hook_list(&hookname_str);\n+\n+\tlist_for_each_safe(pos, tmp, to_run) {\n+\t\tstruct child_process hook_proc = CHILD_PROCESS_INIT;\n+\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n+\n+\t\tif (hook->from_hookdir &&\n+\t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n+\t\t\tcontinue;\n+\n+\t\tprepare_hook_cp(hook, options, &hook_proc);\n+\n+\t\trc |= run_command(&hook_proc);\n+\t}\n+\n+\treturn rc;\n+}\ndiff --git a/hook.h b/hook.h\nindex 1c4b953aec..c24b2c9ecd 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,6 +1,7 @@\n #include \"config.h\"\n #include \"list.h\"\n #include \"strbuf.h\"\n+#include \"strvec.h\"\n \n struct hook {\n \tstruct list_head list;\n@@ -36,6 +37,31 @@ enum hookdir_opt\n  */\n enum hookdir_opt configured_hookdir_opt(void);\n \n+struct run_hooks_opt\n+{\n+\t/* Environment vars to be set for each hook */\n+\tstruct strvec env;\n+\n+\t/* Args to be passed to each hook */\n+\tstruct strvec args;\n+\n+\t/*\n+\t * How should the hookdir be handled?\n+\t * Leave the RUN_HOOKS_OPT_INIT default in most cases; this only needs\n+\t * to be overridden if the user can override it at the command line.\n+\t */\n+\tenum hookdir_opt run_hookdir;\n+};\n+\n+void run_hooks_opt_init(struct run_hooks_opt *o);\n+void run_hooks_opt_clear(struct run_hooks_opt *o);\n+\n+/*\n+ * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n+ * passed 'env' and 'args'.\n+ */\n+int run_hooks(const char *hookname, struct run_hooks_opt *options);\n+\n /* Free memory associated with a 'struct hook' */\n void free_hook(struct hook *ptr);\n /* Empties the list at 'head', calling 'free_hook()' on each entry */\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex a9b1b046c1..1fca83d536 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -115,7 +115,10 @@ test_expect_success 'hook.runHookDir = no is respected by list' '\n \n \tgit hook list pre-commit >actual &&\n \t# the hookdir annotation is translated\n-\ttest_i18ncmp expected actual\n+\ttest_i18ncmp expected actual &&\n+\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_must_be_empty actual\n '\n \n test_expect_success 'hook.runHookDir = error is respected by list' '\n@@ -129,6 +132,13 @@ test_expect_success 'hook.runHookDir = error is respected by list' '\n \n \tgit hook list pre-commit >actual &&\n \t# the hookdir annotation is translated\n+\ttest_i18ncmp expected actual &&\n+\n+\tcat >expected <<-EOF &&\n+\tSkipping legacy hook at '\\''$(pwd)/.git/hooks/pre-commit'\\''\n+\tEOF\n+\n+\tgit hook run pre-commit 2>actual &&\n \ttest_i18ncmp expected actual\n '\n \n@@ -143,6 +153,14 @@ test_expect_success 'hook.runHookDir = warn is respected by list' '\n \n \tgit hook list pre-commit >actual &&\n \t# the hookdir annotation is translated\n+\ttest_i18ncmp expected actual &&\n+\n+\tcat >expected <<-EOF &&\n+\tRunning legacy hook at '\\''$(pwd)/.git/hooks/pre-commit'\\''\n+\t\"Legacy Hook\"\n+\tEOF\n+\n+\tgit hook run pre-commit 2>actual &&\n \ttest_i18ncmp expected actual\n '\n \n@@ -182,7 +200,7 @@ test_expect_success 'git hook list removes skipped inlined hook' '\n \ttest_cmp expected actual\n '\n \n-test_expect_success 'hook.runHookDir = interactive is respected by list' '\n+test_expect_success 'hook.runHookDir = interactive is respected by list and run' '\n \tsetup_hookdir &&\n \n \ttest_config hook.runHookDir \"interactive\" &&\n@@ -193,7 +211,55 @@ test_expect_success 'hook.runHookDir = interactive is respected by list' '\n \n \tgit hook list pre-commit >actual &&\n \t# the hookdir annotation is translated\n-\ttest_i18ncmp expected actual\n+\ttest_i18ncmp expected actual &&\n+\n+\ttest_write_lines n | git hook run pre-commit 2>actual &&\n+\t! grep \"Legacy Hook\" actual &&\n+\n+\ttest_write_lines y | git hook run pre-commit 2>actual &&\n+\tgrep \"Legacy Hook\" actual\n+'\n+\n+test_expect_success 'inline hook definitions execute oneliners' '\n+\ttest_config hook.pre-commit.command \"echo \\\"Hello World\\\"\" &&\n+\n+\techo \"Hello World\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions resolve paths' '\n+\twrite_script sample-hook.sh <<-EOF &&\n+\techo \\\"Sample Hook\\\"\n+\tEOF\n+\n+\ttest_when_finished \"rm sample-hook.sh\" &&\n+\n+\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\techo \\\"Sample Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hookdir hook included in git hook run' '\n+\tsetup_hookdir &&\n+\n+\techo \\\"Legacy Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'out-of-repo runs excluded' '\n+\tsetup_hooks &&\n+\n+\tnongit test_must_fail git hook run pre-commit\n '\n \n test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418789","messageId":"20210311021037.3001235-11-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 10/37] hook: support passing stdin to hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:10Z","receivedAt":"2021-03-11T02:12:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Some hooks (such as post-rewrite) need to take input via stdin.\nPreviously, callers provided stdin to hooks by setting\nrun-command.h:child_process.in, which takes a FD. Callers would open the\nfile in question themselves before calling run-command(). However, since\nwe will now need to seek to the front of the file and read it again for\nevery hook which runs, hook.h:run_command() takes a path and handles FD\nmanagement itself. Since this file is opened for read only, it should\nnot prevent later parallel execution support.\n\nOn the frontend, this is supported by asking for a file path, rather\nthan by reading stdin. Reading directly from stdin would involve caching\nthe entire stdin (to memory or to disk) and reading it back from the\nbeginning to each hook. We'd want to support cases like insufficient\nmemory or storage for the file. While this may prove useful later, for\nnow the path of least resistance is to just ask the user to make this\ninterim file themselves.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt    | 11 +++++++++--\n builtin/hook.c                |  5 ++++-\n hook.c                        |  8 +++++++-\n hook.h                        |  6 +++++-\n t/t1360-config-based-hooks.sh | 24 ++++++++++++++++++++++++\n 5 files changed, 49 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 8f96c347ea..96a857c682 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -9,7 +9,8 @@ SYNOPSIS\n --------\n [verse]\n 'git hook' list <hook-name>\n-'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>\n+'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>]\n+\t<hook-name>\n \n DESCRIPTION\n -----------\n@@ -98,7 +99,7 @@ in the order they should be run, and print the config scope where the relevant\n `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n This output is human-readable and the format is subject to change over time.\n \n-run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::\n+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] `<hook-name>`::\n \n Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n@@ -124,6 +125,12 @@ Specify arguments to pass to every hook that is run.\n +\n Specify environment variables to set for every hook that is run.\n \n+--to-stdin::\n+\tOnly valid for `run`.\n++\n+Specify a file which will be streamed into stdin for every hook that is run.\n+Each hook will receive the entire file from beginning to EOF.\n+\n CONFIGURATION\n -------------\n include::config/hook.txt[]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex e823a96238..38a4555e05 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -8,7 +8,8 @@\n \n static const char * const builtin_hook_usage[] = {\n \tN_(\"git hook list <hookname>\"),\n-\tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hookname>\"),\n+\tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...]\"\n+\t   \"[--to-stdin=<path>] <hookname>\"),\n \tNULL\n };\n \n@@ -101,6 +102,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \t\t\t   N_(\"environment variables for hook to use\")),\n \t\tOPT_STRVEC('a', \"arg\", &opt.args, N_(\"args\"),\n \t\t\t   N_(\"argument to pass to hook\")),\n+\t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n+\t\t\t   N_(\"file to read into hooks' stdin\")),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/hook.c b/hook.c\nindex 118931f273..f906e8c61c 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -240,6 +240,7 @@ void run_hooks_opt_init(struct run_hooks_opt *o)\n {\n \tstrvec_init(&o->env);\n \tstrvec_init(&o->args);\n+\to->path_to_stdin = NULL;\n \to->run_hookdir = configured_hookdir_opt();\n }\n \n@@ -274,7 +275,12 @@ static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n \tif (!hook)\n \t\treturn;\n \n-\tcp->no_stdin = 1;\n+\t/* reopen the file for stdin; run_command closes it. */\n+\tif (options->path_to_stdin)\n+\t\tcp->in = xopen(options->path_to_stdin, O_RDONLY);\n+\telse\n+\t\tcp->no_stdin = 1;\n+\n \tcp->env = options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook->command.buf;\ndiff --git a/hook.h b/hook.h\nindex 0df785add5..2314ec5962 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -52,6 +52,9 @@ struct run_hooks_opt\n \t * to be overridden if the user can override it at the command line.\n \t */\n \tenum hookdir_opt run_hookdir;\n+\n+\t/* Path to file which should be piped to stdin for each hook */\n+\tconst char *path_to_stdin;\n };\n \n void run_hooks_opt_init(struct run_hooks_opt *o);\n@@ -68,7 +71,8 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);\n \n /*\n  * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n- * passed 'env' and 'args'.\n+ * passed 'env' and 'args'. The file at 'stdin_path' will be closed and reopened\n+ * for each hook that runs.\n  */\n int run_hooks(const char *hookname, struct run_hooks_opt *options);\n \ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 1fca83d536..cace5a23c1 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -276,4 +276,28 @@ test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n \ttest_i18ncmp expected actual\n '\n \n+test_expect_success 'stdin to multiple hooks' '\n+\tgit config --add hook.test.command \"xargs -P1 -I% echo a%\" &&\n+\tgit config --add hook.test.command \"xargs -P1 -I% echo b%\" &&\n+\ttest_when_finished \"test_unconfig hook.test.command\" &&\n+\n+\tcat >input <<-EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tcat >expected <<-EOF &&\n+\ta1\n+\ta2\n+\ta3\n+\tb1\n+\tb2\n+\tb3\n+\tEOF\n+\n+\tgit hook run --to-stdin=input test 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418790","messageId":"20210311021037.3001235-4-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 03/37] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:03Z","receivedAt":"2021-03-11T02:12:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach 'git hook list <hookname>', which checks the known configs in\norder to create an ordered list of hooks to run on a given hook event.\n\nMultiple commands can be specified for a given hook by providing\nmultiple \"hook.<hookname>.command = <path-to-hook>\" lines. Hooks will be\nrun in config order. If more properties need to be set on a given hook\nin the future, commands can also be specified by providing\n\"hook.<hookname>.command = <hookcmd-name>\", as well as a \"[hookcmd\n<hookcmd-name>]\" subsection; this subsection should contain a\n\"hookcmd.<hookcmd-name>.command = <path-to-hook>\" line.\n\nFor example:\n\n  $ git config --list | grep ^hook\n  hook.pre-commit.command=baz\n  hook.pre-commit.command=~/bar.sh\n  hookcmd.baz.command=~/baz/from/hookcmd.sh\n\n  $ git hook list pre-commit\n  global: ~/baz/from/hookcmd.sh\n  local: ~/bar.sh\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Since v7, fixed some nits from Jonathan Tan, one of which revealed a bug in\n    how I was adding hooks to the list.\n\n Documentation/config/hook.txt |   9 +++\n Documentation/git-hook.txt    |  59 ++++++++++++++++-\n Makefile                      |   1 +\n builtin/hook.c                |  56 ++++++++++++++--\n hook.c                        | 120 ++++++++++++++++++++++++++++++++++\n hook.h                        |  25 +++++++\n t/t1360-config-based-hooks.sh |  81 ++++++++++++++++++++++-\n 7 files changed, 341 insertions(+), 10 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n create mode 100644 hook.c\n create mode 100644 hook.h\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nnew file mode 100644\nindex 0000000000..71449ecbc7\n--- /dev/null\n+++ b/Documentation/config/hook.txt\n@@ -0,0 +1,9 @@\n+hook.<command>.command::\n+\tA command to execute during the <command> hook event. This can be an\n+\texecutable on your device, a oneliner for your shell, or the name of a\n+\thookcmd. See linkgit:git-hook[1].\n+\n+hookcmd.<name>.command::\n+\tA command to execute during a hook for which <name> has been specified\n+\tas a command. This can be an executable on your device or a oneliner for\n+\tyour shell. See linkgit:git-hook[1].\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 9eeab0009d..f19875ed68 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,12 +8,65 @@ git-hook - Manage configured hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook'\n+'git hook' list <hook-name>\n \n DESCRIPTION\n -----------\n-A placeholder command. Later, you will be able to list, add, and modify hooks\n-with this command.\n+You can list configured hooks with this command. Later, you will be able to run,\n+add, and modify hooks with this command.\n+\n+This command parses the default configuration files for sections `hook` and\n+`hookcmd`. `hook` is used to describe the commands which will be run during a\n+particular hook event; commands are run in the order Git encounters them during\n+the configuration parse (see linkgit:git-config[1]). `hookcmd` is used to\n+describe attributes of a specific command. If additional attributes don't need\n+to be specified, a command to run can be specified directly in the `hook`\n+section; if a `hookcmd` by that name isn't found, Git will attempt to run the\n+provided value directly. For example:\n+\n+Global config\n+----\n+  [hook \"post-commit\"]\n+    command = \"linter\"\n+    command = \"~/typocheck.sh\"\n+\n+  [hookcmd \"linter\"]\n+    command = \"/bin/linter --c\"\n+----\n+\n+Local config\n+----\n+  [hook \"prepare-commit-msg\"]\n+    command = \"linter\"\n+  [hook \"post-commit\"]\n+    command = \"python ~/run-test-suite.py\"\n+----\n+\n+With these configs, you'd then see:\n+\n+----\n+$ git hook list \"post-commit\"\n+global: /bin/linter --c\n+global: ~/typocheck.sh\n+local: python ~/run-test-suite.py\n+\n+$ git hook list \"prepare-commit-msg\"\n+local: /bin/linter --c\n+----\n+\n+COMMANDS\n+--------\n+\n+list `<hook-name>`::\n+\n+List the hooks which have been configured for `<hook-name>`. Hooks appear\n+in the order they should be run, and print the config scope where the relevant\n+`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n+This output is human-readable and the format is subject to change over time.\n+\n+CONFIGURATION\n+-------------\n+include::config/hook.txt[]\n \n GIT\n ---\ndiff --git a/Makefile b/Makefile\nindex 8e904a1ab5..3fa51597d8 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -891,6 +891,7 @@ LIB_OBJS += hash-lookup.o\n LIB_OBJS += hashmap.o\n LIB_OBJS += help.o\n LIB_OBJS += hex.o\n+LIB_OBJS += hook.o\n LIB_OBJS += ident.o\n LIB_OBJS += json-writer.o\n LIB_OBJS += kwset.o\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex b2bbc84d4d..bb64cd77ca 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -1,21 +1,67 @@\n #include \"cache.h\"\n-\n #include \"builtin.h\"\n+#include \"config.h\"\n+#include \"hook.h\"\n #include \"parse-options.h\"\n+#include \"strbuf.h\"\n \n static const char * const builtin_hook_usage[] = {\n-\tN_(\"git hook\"),\n+\tN_(\"git hook list <hookname>\"),\n \tNULL\n };\n \n-int cmd_hook(int argc, const char **argv, const char *prefix)\n+static int list(int argc, const char **argv, const char *prefix)\n {\n-\tstruct option builtin_hook_options[] = {\n+\tstruct list_head *head, *pos;\n+\tstruct strbuf hookname = STRBUF_INIT;\n+\n+\tstruct option list_options[] = {\n \t\tOPT_END(),\n \t};\n \n-\targc = parse_options(argc, argv, prefix, builtin_hook_options,\n+\targc = parse_options(argc, argv, prefix, list_options,\n \t\t\t     builtin_hook_usage, 0);\n \n+\tif (argc < 1) {\n+\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\t\t      builtin_hook_usage, list_options);\n+\t}\n+\n+\tstrbuf_addstr(&hookname, argv[0]);\n+\n+\thead = hook_list(&hookname);\n+\n+\tif (list_empty(head)) {\n+\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n+\t\t       hookname.buf);\n+\t\tstrbuf_release(&hookname);\n+\t\treturn 0;\n+\t}\n+\n+\tlist_for_each(pos, head) {\n+\t\tstruct hook *item = list_entry(pos, struct hook, list);\n+\t\tif (item)\n+\t\t\tprintf(\"%s: %s\\n\",\n+\t\t\t       config_scope_name(item->origin),\n+\t\t\t       item->command.buf);\n+\t}\n+\n+\tclear_hook_list(head);\n+\tstrbuf_release(&hookname);\n+\n \treturn 0;\n }\n+\n+int cmd_hook(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option builtin_hook_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tif (argc < 2)\n+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+\n+\tif (!strcmp(argv[1], \"list\"))\n+\t\treturn list(argc - 1, argv + 1, prefix);\n+\n+\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+}\ndiff --git a/hook.c b/hook.c\nnew file mode 100644\nindex 0000000000..fede40e925\n--- /dev/null\n+++ b/hook.c\n@@ -0,0 +1,120 @@\n+#include \"cache.h\"\n+\n+#include \"hook.h\"\n+#include \"config.h\"\n+\n+void free_hook(struct hook *ptr)\n+{\n+\tif (ptr) {\n+\t\tstrbuf_release(&ptr->command);\n+\t\tfree(ptr);\n+\t}\n+}\n+\n+static void append_or_move_hook(struct list_head *head, const char *command)\n+{\n+\tstruct list_head *pos = NULL, *tmp = NULL;\n+\tstruct hook *to_add = NULL;\n+\n+\t/*\n+\t * remove the prior entry with this command; we'll replace it at the\n+\t * end.\n+\t */\n+\tlist_for_each_safe(pos, tmp, head) {\n+\t\tstruct hook *it = list_entry(pos, struct hook, list);\n+\t\tif (!strcmp(it->command.buf, command)) {\n+\t\t    list_del(pos);\n+\t\t    /* we'll simply move the hook to the end */\n+\t\t    to_add = it;\n+\t\t    break;\n+\t\t}\n+\t}\n+\n+\tif (!to_add) {\n+\t\t/* adding a new hook, not moving an old one */\n+\t\tto_add = xmalloc(sizeof(*to_add));\n+\t\tstrbuf_init(&to_add->command, 0);\n+\t\tstrbuf_addstr(&to_add->command, command);\n+\t}\n+\n+\t/* re-set the scope so we show where an override was specified */\n+\tto_add->origin = current_config_scope();\n+\n+\tlist_add_tail(&to_add->list, head);\n+}\n+\n+static void remove_hook(struct list_head *to_remove)\n+{\n+\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n+\tlist_del(to_remove);\n+\tfree_hook(hook_to_remove);\n+}\n+\n+void clear_hook_list(struct list_head *head)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, head)\n+\t\tremove_hook(pos);\n+}\n+\n+struct hook_config_cb\n+{\n+\tstruct strbuf *hookname;\n+\tstruct list_head *list;\n+};\n+\n+static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n+{\n+\tstruct hook_config_cb *data = cb_data;\n+\tconst char *hook_key = data->hookname->buf;\n+\tstruct list_head *head = data->list;\n+\n+\tif (!strcmp(key, hook_key)) {\n+\t\tconst char *command = value;\n+\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\n+\t\t/*\n+\t\t * Check if a hookcmd with that name exists. If it doesn't,\n+\t\t * 'git_config_get_value()' is documented not to touch &command,\n+\t\t * so we don't need to do anything.\n+\t\t */\n+\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n+\t\tgit_config_get_value(hookcmd_name.buf, &command);\n+\n+\t\tif (!command) {\n+\t\t\tstrbuf_release(&hookcmd_name);\n+\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n+\t\t}\n+\n+\t\t/*\n+\t\t * TODO: implement an option-getting callback, e.g.\n+\t\t *   get configs by pattern hookcmd.$value.*\n+\t\t *   for each key+value, do_callback(key, value, cb_data)\n+\t\t */\n+\n+\t\tappend_or_move_hook(head, command);\n+\n+\t\tstrbuf_release(&hookcmd_name);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+struct list_head* hook_list(const struct strbuf* hookname)\n+{\n+\tstruct strbuf hook_key = STRBUF_INIT;\n+\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n+\n+\tINIT_LIST_HEAD(hook_head);\n+\n+\tif (!hookname)\n+\t\treturn NULL;\n+\n+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname->buf);\n+\n+\tgit_config(hook_config_lookup, &cb_data);\n+\n+\tstrbuf_release(&hook_key);\n+\treturn hook_head;\n+}\ndiff --git a/hook.h b/hook.h\nnew file mode 100644\nindex 0000000000..e48dfc6d27\n--- /dev/null\n+++ b/hook.h\n@@ -0,0 +1,25 @@\n+#include \"config.h\"\n+#include \"list.h\"\n+#include \"strbuf.h\"\n+\n+struct hook {\n+\tstruct list_head list;\n+\t/*\n+\t * Config file which holds the hook.*.command definition.\n+\t * (This has nothing to do with the hookcmd.<name>.* configs.)\n+\t */\n+\tenum config_scope origin;\n+\t/* The literal command to run. */\n+\tstruct strbuf command;\n+};\n+\n+/*\n+ * Provides a linked list of 'struct hook' detailing commands which should run\n+ * in response to the 'hookname' event, in execution order.\n+ */\n+struct list_head* hook_list(const struct strbuf *hookname);\n+\n+/* Free memory associated with a 'struct hook' */\n+void free_hook(struct hook *ptr);\n+/* Empties the list at 'head', calling 'free_hook()' on each entry */\n+void clear_hook_list(struct list_head *head);\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 34b0df5216..6e4a3e763f 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -4,8 +4,85 @@ test_description='config-managed multihooks, including git-hook command'\n \n . ./test-lib.sh\n \n-test_expect_success 'git hook command does not crash' '\n-\tgit hook\n+ROOT=\n+if test_have_prereq MINGW\n+then\n+\t# In Git for Windows, Unix-like paths work only in shell scripts;\n+\t# `git.exe`, however, will prefix them with the pseudo root directory\n+\t# (of the Unix shell). Let's accommodate for that.\n+\tROOT=\"$(cd / && pwd)\"\n+fi\n+\n+setup_hooks () {\n+\ttest_config hook.pre-commit.command \"/path/ghi\" --add\n+\ttest_config_global hook.pre-commit.command \"/path/def\" --add\n+}\n+\n+setup_hookcmd () {\n+\ttest_config hook.pre-commit.command \"abc\" --add\n+\ttest_config_global hookcmd.abc.command \"/path/abc\" --add\n+}\n+\n+test_expect_success 'git hook rejects commands without a mode' '\n+\ttest_must_fail git hook pre-commit\n+'\n+\n+\n+test_expect_success 'git hook rejects commands without a hookname' '\n+\ttest_must_fail git hook list\n+'\n+\n+test_expect_success 'git hook runs outside of a repo' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tEOF\n+\n+\tnongit git config --list --global &&\n+\n+\tnongit git hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list orders by config order' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tlocal: $ROOT/path/ghi\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list dereferences a hookcmd' '\n+\tsetup_hooks &&\n+\tsetup_hookcmd &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tlocal: $ROOT/path/ghi\n+\tlocal: $ROOT/path/abc\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list reorders on duplicate commands' '\n+\tsetup_hooks &&\n+\n+\ttest_config hook.pre-commit.command \"/path/def\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tlocal: $ROOT/path/ghi\n+\tlocal: $ROOT/path/def\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n '\n \n test_done\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418791","messageId":"20210311021037.3001235-14-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 13/37] hook: allow specifying working directory for hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:13Z","receivedAt":"2021-03-11T02:12:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Hooks like \"post-checkout\" require that hooks have a different working\ndirectory than the initial process. Pipe that directly through to struct\nchild_process.\n\nBecause we can just run 'git -C <some-dir> hook run ...' it shouldn't be\nnecessary to pipe this option through the frontend. In fact, this\nreduces the possibility of users running hooks which affect some part of\nthe filesystem outside of the repo in question.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Needed later for \"post-checkout\" conversion.\n\n hook.c | 2 ++\n hook.h | 3 +++\n 2 files changed, 5 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex fe8860860b..67ad3aa747 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -251,6 +251,7 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n \to->path_to_stdin = NULL;\n \to->run_hookdir = configured_hookdir_opt();\n \to->jobs = 1;\n+\to->dir = NULL;\n }\n \n void run_hooks_opt_init_async(struct run_hooks_opt *o)\n@@ -306,6 +307,7 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook->command.buf;\n+\tcp->dir = hook_cb->options->dir;\n \n \t/*\n \t * Commands from the config could be oneliners, but we know\ndiff --git a/hook.h b/hook.h\nindex 2593f932c0..fcd8e99e39 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -61,6 +61,9 @@ struct run_hooks_opt\n \n \t/* Number of threads to parallelize across */\n \tint jobs;\n+\n+\t/* Path to initial working directory for subprocess */\n+\tconst char *dir;\n };\n \n /*\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418792","messageId":"20210311021037.3001235-7-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 06/37] hook: implement hookcmd.<name>.skip","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:06Z","receivedAt":"2021-03-11T02:12:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"If a user wants a specific repo to skip execution of a hook which is set\nat a global or system level, they will be able to do so by specifying\n'skip' in their repo config:\n\n~/.gitconfig\n  [hook.pre-commit]\n    command = skippable-oneliner\n    command = skippable-hookcmd\n\n  [hookcmd.skippable-hookcmd]\n    command = foo.sh\n\n$GIT_DIR/.git/config\n  [hookcmd.skippable-oneliner]\n    skip = true\n  [hookcmd.skippable-hookcmd]\n    skip = true\n\nLater it may make sense to add an option like\n\"hookcmd.<name>.<hook-event>-skip\" - but for simplicity, let's start\nwith a universal skip setting like this.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\n Documentation/config/hook.txt |  8 ++++++++\n Documentation/git-hook.txt    | 33 +++++++++++++++++++++++++++++++++\n hook.c                        | 35 ++++++++++++++++++++++++++---------\n t/t1360-config-based-hooks.sh | 35 +++++++++++++++++++++++++++++++++++\n 4 files changed, 102 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 75312754ae..8b12512e33 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -8,6 +8,14 @@ hookcmd.<name>.command::\n \tas a command. This can be an executable on your device or a oneliner for\n \tyour shell. See linkgit:git-hook[1].\n \n+hookcmd.<name>.skip::\n+\tSpecify this boolean to remove a command from earlier in the execution\n+\torder. Useful if you want to make a single repo an exception to hook\n+\tconfigured at the system or global scope. If there is no hookcmd\n+\tspecified for the command you want to skip, you can use the value of\n+\t`hook.<command>.command` as <name> as a shortcut. The \"skip\" setting\n+\tmust be specified after the \"hook.<command>.command\" to have an effect.\n+\n hook.runHookDir::\n \tControls how hooks contained in your hookdir are executed. Can be any of\n \t\"yes\", \"warn\", \"interactive\", or \"no\". Defaults to \"yes\". See\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex f19875ed68..c84520cb38 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -54,6 +54,39 @@ $ git hook list \"prepare-commit-msg\"\n local: /bin/linter --c\n ----\n \n+If there is a command you wish to run in most cases but have one or two\n+exceptional repos where it should be skipped, you can use specify\n+`hookcmd.<name>.skip`, for example:\n+\n+System config\n+----\n+  [hook \"pre-commit\"]\n+    command = check-for-secrets\n+\n+  [hookcmd \"check-for-secrets\"]\n+    command = /bin/secret-checker --aggressive\n+----\n+\n+Local config\n+----\n+  [hookcmd \"check-for-secrets\"]\n+    skip = true\n+  # This works for inlined hook commands, too:\n+  [hookcmd \"~/typocheck.sh\"]\n+    skip = true\n+----\n+\n+After these configs are added, the hook list becomes:\n+\n+----\n+$ git hook list \"post-commit\"\n+global: /bin/linter --c\n+local: python ~/run-test-suite.py\n+\n+$ git hook list \"pre-commit\"\n+no commands configured for hook 'pre-commit'\n+----\n+\n COMMANDS\n --------\n \ndiff --git a/hook.c b/hook.c\nindex 039ff0a378..37b740d58d 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -12,24 +12,25 @@ void free_hook(struct hook *ptr)\n \t}\n }\n \n-static void append_or_move_hook(struct list_head *head, const char *command)\n+static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n {\n \tstruct list_head *pos = NULL, *tmp = NULL;\n-\tstruct hook *to_add = NULL;\n+\tstruct hook *found = NULL;\n \n-\t/*\n-\t * remove the prior entry with this command; we'll replace it at the\n-\t * end.\n-\t */\n \tlist_for_each_safe(pos, tmp, head) {\n \t\tstruct hook *it = list_entry(pos, struct hook, list);\n \t\tif (!strcmp(it->command.buf, command)) {\n \t\t    list_del(pos);\n-\t\t    /* we'll simply move the hook to the end */\n-\t\t    to_add = it;\n+\t\t    found = it;\n \t\t    break;\n \t\t}\n \t}\n+\treturn found;\n+}\n+\n+static void append_or_move_hook(struct list_head *head, const char *command)\n+{\n+\tstruct hook *to_add = find_hook_by_command(head, command);\n \n \tif (!to_add) {\n \t\t/* adding a new hook, not moving an old one */\n@@ -74,12 +75,22 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n \tif (!strcmp(key, hook_key)) {\n \t\tconst char *command = value;\n \t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\t\tint skip = 0;\n+\n+\t\t/*\n+\t\t * Check if we're removing that hook instead. Hookcmds are\n+\t\t * removed by name, and inlined hooks are removed by command\n+\t\t * content.\n+\t\t */\n+\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.skip\", command);\n+\t\tgit_config_get_bool(hookcmd_name.buf, &skip);\n \n \t\t/*\n \t\t * Check if a hookcmd with that name exists. If it doesn't,\n \t\t * 'git_config_get_value()' is documented not to touch &command,\n \t\t * so we don't need to do anything.\n \t\t */\n+\t\tstrbuf_reset(&hookcmd_name);\n \t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n \t\tgit_config_get_value(hookcmd_name.buf, &command);\n \n@@ -94,7 +105,13 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n \t\t *   for each key+value, do_callback(key, value, cb_data)\n \t\t */\n \n-\t\tappend_or_move_hook(head, command);\n+\t\tif (skip) {\n+\t\t\tstruct hook *to_remove = find_hook_by_command(head, command);\n+\t\t\tif (to_remove)\n+\t\t\t\tremove_hook(&(to_remove->list));\n+\t\t} else {\n+\t\t\tappend_or_move_hook(head, command);\n+\t\t}\n \n \t\tstrbuf_release(&hookcmd_name);\n \t}\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 66b0b6b7ad..a9b1b046c1 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -146,6 +146,41 @@ test_expect_success 'hook.runHookDir = warn is respected by list' '\n \ttest_i18ncmp expected actual\n '\n \n+test_expect_success 'git hook list removes skipped hookcmd' '\n+\tsetup_hookcmd &&\n+\ttest_config hookcmd.abc.skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tno commands configured for hook '\\''pre-commit'\\''\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_i18ncmp expected actual\n+'\n+\n+test_expect_success 'git hook list ignores skip referring to unused hookcmd' '\n+\ttest_config hookcmd.abc.command \"/path/abc\" --add &&\n+\ttest_config hookcmd.abc.skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tno commands configured for hook '\\''pre-commit'\\''\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_i18ncmp expected actual\n+'\n+\n+test_expect_success 'git hook list removes skipped inlined hook' '\n+\tsetup_hooks &&\n+\ttest_config hookcmd.\"$ROOT/path/ghi\".skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n \n test_expect_success 'hook.runHookDir = interactive is respected by list' '\n \tsetup_hookdir &&\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418793","messageId":"20210311021037.3001235-15-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 14/37] run-command: add stdin callback for parallelization","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:14Z","receivedAt":"2021-03-11T02:12:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"If a user of the run_processes_parallel() API wants to pipe a large\namount of information to stdin of each parallel command, that\ninformation could exceed the buffer of the pipe allocated for that\nprocess's stdin.  Generally this is solved by repeatedly writing to\nchild_process.in between calls to start_command() and finish_command();\nrun_processes_parallel() did not provide users an opportunity to access\nchild_process at that time.\n\nBecause the data might be extremely large (for example, a list of all\nrefs received during a push from a client) simply taking a string_list\nor strbuf is not as scalable as using a callback; the rest of the\nrun_processes_parallel() API also uses callbacks, so making this feature\nmatch the rest of the API reduces mental load on the user.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/fetch.c             |  1 +\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 54 +++++++++++++++++++++++++++++++++++--\n run-command.h               | 17 +++++++++++-\n submodule.c                 |  1 +\n t/helper/test-run-command.c | 31 ++++++++++++++++++---\n t/t0061-run-command.sh      | 30 +++++++++++++++++++++\n 8 files changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 0b90de87c7..d8e798dc69 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1757,6 +1757,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n+\t\t\t\t\t\t    NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 9d505a6329..14f6e4ee8c 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2294,7 +2294,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure,\n+\t\t\t\t   update_clone_start_failure, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex 67ad3aa747..9088b520f3 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -400,6 +400,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex e6d7541b84..e7eeb6c49b 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1558,6 +1558,7 @@ struct parallel_processes {\n \n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n+\tfeed_pipe_fn feed_pipe;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1585,6 +1586,13 @@ static int default_start_failure(struct strbuf *out,\n \treturn 0;\n }\n \n+static int default_feed_pipe(struct strbuf *pipe,\n+\t\t\t     void *pp_cb,\n+\t\t\t     void *pp_task_cb)\n+{\n+\treturn 1;\n+}\n+\n static int default_task_finished(int result,\n \t\t\t\t struct strbuf *out,\n \t\t\t\t void *pp_cb,\n@@ -1615,6 +1623,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    int n,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n+\t\t    feed_pipe_fn feed_pipe,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1633,6 +1642,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->get_next_task = get_next_task;\n \n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n+\tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n \n \tpp->nr_processes = 0;\n@@ -1730,6 +1740,37 @@ static int pp_start_one(struct parallel_processes *pp)\n \treturn 0;\n }\n \n+static void pp_buffer_stdin(struct parallel_processes *pp)\n+{\n+\tint i;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t/* Buffer stdin for each pipe. */\n+\tfor (i = 0; i < pp->max_processes; i++) {\n+\t\tif (pp->children[i].state == GIT_CP_WORKING &&\n+\t\t    pp->children[i].process.in > 0) {\n+\t\t\tint done;\n+\t\t\tstrbuf_reset(&sb);\n+\t\t\tdone = pp->feed_pipe(&sb, pp->data,\n+\t\t\t\t\t      pp->children[i].data);\n+\t\t\tif (sb.len) {\n+\t\t\t\tif (write_in_full(pp->children[i].process.in,\n+\t\t\t\t\t      sb.buf, sb.len) < 0) {\n+\t\t\t\t\tif (errno != EPIPE)\n+\t\t\t\t\t\tdie_errno(\"write\");\n+\t\t\t\t\tdone = 1;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif (done) {\n+\t\t\t\tclose(pp->children[i].process.in);\n+\t\t\t\tpp->children[i].process.in = 0;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&sb);\n+}\n+\n static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n {\n \tint i;\n@@ -1794,6 +1835,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\tpp->nr_processes--;\n \t\tpp->children[i].state = GIT_CP_FREE;\n \t\tpp->pfd[i].fd = -1;\n+\t\tpp->children[i].process.in = 0;\n \t\tchild_process_init(&pp->children[i].process);\n \n \t\tif (i != pp->output_owner) {\n@@ -1827,6 +1869,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n+\t\t\t   feed_pipe_fn feed_pipe,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1835,7 +1878,9 @@ int run_processes_parallel(int n,\n \tint spawn_cap = 4;\n \tstruct parallel_processes pp;\n \n-\tpp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb);\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1852,6 +1897,7 @@ int run_processes_parallel(int n,\n \t\t}\n \t\tif (!pp.nr_processes)\n \t\t\tbreak;\n+\t\tpp_buffer_stdin(&pp);\n \t\tpp_buffer_stderr(&pp, output_timeout);\n \t\tpp_output(&pp);\n \t\tcode = pp_collect_finished(&pp);\n@@ -1863,11 +1909,15 @@ int run_processes_parallel(int n,\n \t}\n \n \tpp_cleanup(&pp);\n+\n+\tsigchain_pop(SIGPIPE);\n+\n \treturn 0;\n }\n \n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n+\t\t\t       feed_pipe_fn feed_pipe,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1877,7 +1927,7 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\ttask_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex d08414a92e..1e3cf0999f 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -443,6 +443,20 @@ typedef int (*start_failure_fn)(struct strbuf *out,\n \t\t\t\tvoid *pp_cb,\n \t\t\t\tvoid *pp_task_cb);\n \n+/**\n+ * This callback is called repeatedly on every child process who requests\n+ * start_command() to create a pipe by setting child_process.in < 0.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel, and\n+ * pp_task_cb is the callback cookie as passed into get_next_task_fn.\n+ * The contents of 'send' will be read into the pipe and passed to the pipe.\n+ *\n+ * Return nonzero to close the pipe.\n+ */\n+typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n+\t\t\t    void *pp_cb,\n+\t\t\t    void *pp_task_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -477,10 +491,11 @@ typedef int (*task_finished_fn)(int result,\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n+\t\t\t   feed_pipe_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex 9767ba9893..dc4a6a60f4 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1644,6 +1644,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 7ae03dc712..9348184d30 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,\n \t\treturn 0;\n \n \tstrvec_pushv(&cp->args, d->argv);\n+\tcp->in = d->in;\n+\tcp->no_stdin = d->no_stdin;\n \tstrbuf_addstr(err, \"preloaded output of a child\\n\");\n \tnumber_callbacks++;\n+\n+\t*task_cb = xmalloc(sizeof(int));\n+\t*(int*)(*task_cb) = 2;\n \treturn 1;\n }\n \n@@ -55,6 +60,17 @@ static int task_finished(int result,\n \treturn 1;\n }\n \n+static int test_stdin(struct strbuf *pipe, void *cb, void *task_cb)\n+{\n+\tint *lines_remaining = task_cb;\n+\n+\tif (*lines_remaining)\n+\t\tstrbuf_addf(pipe, \"sample stdin %d\\n\", --(*lines_remaining));\n+\n+\treturn !(*lines_remaining);\n+}\n+\n+\n struct testsuite {\n \tstruct string_list tests, failed;\n \tint next;\n@@ -185,7 +201,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_finished, &suite);\n+\t\t\t\t     test_stdin, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -413,15 +429,22 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\n+\tif (!strcmp(argv[1], \"run-command-stdin\")) {\n+\t\tproc.in = -1;\n+\t\tproc.no_stdin = 0;\n+\t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n+\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t}\n \n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 7d599675e3..87759482ad 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,36 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+cat >expect <<-EOF\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+EOF\n+\n+test_expect_success 'run_command listens to stdin' '\n+\twrite_script stdin-script <<-\\EOF &&\n+\techo \"listening for stdin:\"\n+\twhile read line; do\n+\t\techo \"$line\"\n+\tdone\n+\tEOF\n+\ttest-tool run-command run-command-stdin 2 ./stdin-script 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n asking for a quick stop\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418795","messageId":"20210311021037.3001235-19-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 18/37] commit: use config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:18Z","receivedAt":"2021-03-11T02:12:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"As part of the adoption of config-based hooks, teach run_commit_hook()\nto call hook.h instead of run-command.h. This covers 'pre-commit',\n'commit-msg', and 'prepare-commit-msg'. Additionally, ask the hook\nlibrary - not run-command - whether any hooks will be run, as it's\npossible hooks may exist in the config but not the hookdir.\n\nBecause all but 'post-commit' hooks are expected to make some state\nchange, force all but 'post-commit' hook to run in series. 'post-commit'\n\"is meant primarily for notification, and cannot affect the outcome of\n`git commit`,\" so it is fine to run in parallel.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt                    | 13 +++++++++++\n builtin/commit.c                              | 11 +++++-----\n builtin/merge.c                               |  9 ++++----\n commit.c                                      | 22 ++++++++++++++-----\n commit.h                                      |  3 ++-\n sequencer.c                                   |  7 +++---\n ...3-pre-commit-and-pre-merge-commit-hooks.sh | 17 ++++++++++++--\n 7 files changed, 61 insertions(+), 21 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 1f3b57d04d..984fb998b2 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -103,6 +103,8 @@ The default 'pre-commit' hook, when enabled--and with the\n `hooks.allownonascii` config option unset or set to false--prevents\n the use of non-ASCII filenames.\n \n+Hooks executed during 'pre-commit' will not be parallelized.\n+\n pre-merge-commit\n ~~~~~~~~~~~~~~~~\n \n@@ -125,6 +127,8 @@ need to be resolved and the result committed separately (see\n linkgit:git-merge[1]). At that point, this hook will not be executed,\n but the 'pre-commit' hook will, if it is enabled.\n \n+Hooks executed during 'pre-merge-commit' will not be parallelized.\n+\n prepare-commit-msg\n ~~~~~~~~~~~~~~~~~~\n \n@@ -150,6 +154,9 @@ be used as replacement for pre-commit hook.\n The sample `prepare-commit-msg` hook that comes with Git removes the\n help message found in the commented portion of the commit template.\n \n+Hooks executed during 'prepare-commit-msg' will not be parallelized, because\n+hooks are expected to edit the file containing the commit log message.\n+\n commit-msg\n ~~~~~~~~~~\n \n@@ -166,6 +173,9 @@ file.\n The default 'commit-msg' hook, when enabled, detects duplicate\n `Signed-off-by` trailers, and aborts the commit if one is found.\n \n+Hooks executed during 'commit-msg' will not be parallelized, because hooks are\n+expected to edit the file containing the proposed commit log message.\n+\n post-commit\n ~~~~~~~~~~~\n \n@@ -175,6 +185,9 @@ invoked after a commit is made.\n This hook is meant primarily for notification, and cannot affect\n the outcome of `git commit`.\n \n+Hooks executed during 'post-commit' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n pre-rebase\n ~~~~~~~~~~\n \ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 739110c5a7..39f387e8f7 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -36,6 +36,7 @@\n #include \"help.h\"\n #include \"commit-reach.h\"\n #include \"commit-graph.h\"\n+#include \"hook.h\"\n \n static const char * const builtin_commit_usage[] = {\n \tN_(\"git commit [<options>] [--] <pathspec>...\"),\n@@ -699,7 +700,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t/* This checks and barfs if author is badly specified */\n \tdetermine_author_info(author_ident);\n \n-\tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(use_editor, 0, index_file, \"pre-commit\", NULL))\n \t\treturn 0;\n \n \tif (squash_message) {\n@@ -983,7 +984,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && find_hook(\"pre-commit\")) {\n+\tif (!no_verify && hook_exists(\"pre-commit\", HOOKDIR_USE_CONFIG)) {\n \t\t/*\n \t\t * Re-read the index as pre-commit hook could have updated it,\n \t\t * and write it out as a tree.  We must do this before we invoke\n@@ -998,7 +999,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n+\tif (run_commit_hook(use_editor, 0, index_file, \"prepare-commit-msg\",\n \t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n \t\treturn 0;\n \n@@ -1015,7 +1016,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t}\n \n \tif (!no_verify &&\n-\t    run_commit_hook(use_editor, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n+\t    run_commit_hook(use_editor, 0, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n \t\treturn 0;\n \t}\n \n@@ -1701,7 +1702,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \n \trepo_rerere(the_repository, 0);\n \trun_auto_maintenance(quiet);\n-\trun_commit_hook(use_editor, get_index_file(), \"post-commit\", NULL);\n+\trun_commit_hook(use_editor, 1, get_index_file(), \"post-commit\", NULL);\n \tif (amend && !no_post_rewrite) {\n \t\tcommit_post_rewrite(the_repository, current_head, &oid);\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex eb00b273e6..33df744ab0 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -43,6 +43,7 @@\n #include \"commit-reach.h\"\n #include \"wt-status.h\"\n #include \"commit-graph.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_TWOHEAD (1<<0)\n #define DEFAULT_OCTOPUS (1<<1)\n@@ -837,14 +838,14 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *index_file = get_index_file();\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, index_file, \"pre-merge-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(0 < option_edit, 0, index_file, \"pre-merge-commit\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \t/*\n \t * Re-read the index as pre-merge-commit hook could have updated it,\n \t * and write it out as a tree.  We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (find_hook(\"pre-merge-commit\"))\n+\tif (hook_exists(\"pre-merge-commit\", HOOKDIR_USE_CONFIG))\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n@@ -865,7 +866,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tappend_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);\n \twrite_merge_heads(remoteheads);\n \twrite_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);\n-\tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n+\tif (run_commit_hook(0 < option_edit, 0, get_index_file(), \"prepare-commit-msg\",\n \t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \tif (0 < option_edit) {\n@@ -873,7 +874,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\t\tabort_commit(remoteheads, NULL);\n \t}\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n+\tif (!no_verify && run_commit_hook(0 < option_edit, 0, get_index_file(),\n \t\t\t\t\t  \"commit-msg\",\n \t\t\t\t\t  git_path_merge_msg(the_repository), NULL))\n \t\tabort_commit(remoteheads, NULL);\ndiff --git a/commit.c b/commit.c\nindex 6ccd774841..b72158cb34 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -21,6 +21,7 @@\n #include \"commit-reach.h\"\n #include \"run-command.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);\n \n@@ -1681,25 +1682,34 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n \treturn boc ? len - boc : len - cutoff;\n }\n \n-int run_commit_hook(int editor_is_used, const char *index_file,\n+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,\n \t\t    const char *name, ...)\n {\n-\tstruct strvec hook_env = STRVEC_INIT;\n+\tstruct run_hooks_opt opt;\n \tva_list args;\n+\tconst char *arg;\n \tint ret;\n \n-\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n+\trun_hooks_opt_init_sync(&opt);\n+\n+\tif (parallelize)\n+\t\topt.jobs = configured_hook_jobs();\n+\n+\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\n \t * Let the hook know that no editor will be launched.\n \t */\n \tif (!editor_is_used)\n-\t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n+\t\tstrvec_push(&opt.env, \"GIT_EDITOR=:\");\n \n \tva_start(args, name);\n-\tret = run_hook_ve(hook_env.v, name, args);\n+\twhile ((arg = va_arg(args, const char *)))\n+\t\tstrvec_push(&opt.args, arg);\n \tva_end(args);\n-\tstrvec_clear(&hook_env);\n+\n+\tret = run_hooks(name, &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \treturn ret;\n }\ndiff --git a/commit.h b/commit.h\nindex 49c0f50396..abea90a3f9 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -360,7 +360,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\n \n LAST_ARG_MUST_BE_NULL\n-int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);\n+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,\n+\t\t    const char *name, ...);\n \n /* Sign a commit or tag buffer, storing the result in a header. */\n int sign_with_header(struct strbuf *buf, const char *keyid);\ndiff --git a/sequencer.c b/sequencer.c\nindex d2332d3e17..e3a951fbeb 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -34,6 +34,7 @@\n #include \"commit-reach.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -1206,7 +1207,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n \t} else {\n \t\targ1 = \"message\";\n \t}\n-\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n+\tif (run_commit_hook(0, 0, r->index_file, \"prepare-commit-msg\", name,\n \t\t\t    arg1, arg2, NULL))\n \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n \n@@ -1444,7 +1445,7 @@ static int try_to_commit(struct repository *r,\n \t\t}\n \t}\n \n-\tif (find_hook(\"prepare-commit-msg\")) {\n+\tif (hook_exists(\"prepare-commit-msg\", HOOKDIR_USE_CONFIG)) {\n \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n \t\tif (res)\n \t\t\tgoto out;\n@@ -1536,7 +1537,7 @@ static int try_to_commit(struct repository *r,\n \t\tgoto out;\n \t}\n \n-\trun_commit_hook(0, r->index_file, \"post-commit\", NULL);\n+\trun_commit_hook(0, 1, r->index_file, \"post-commit\", NULL);\n \tif (flags & AMEND_MSG)\n \t\tcommit_post_rewrite(r, current_head, oid);\n \ndiff --git a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\nindex 606d8d0f08..e9e3713033 100755\n--- a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\n+++ b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\n@@ -8,8 +8,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n \n HOOKDIR=\"$(git rev-parse --git-dir)/hooks\"\n-PRECOMMIT=\"$HOOKDIR/pre-commit\"\n-PREMERGE=\"$HOOKDIR/pre-merge-commit\"\n+PRECOMMIT=\"$(pwd)/$HOOKDIR/pre-commit\"\n+PREMERGE=\"$(pwd)/$HOOKDIR/pre-merge-commit\"\n \n # Prepare sample scripts that write their $0 to actual_hooks\n test_expect_success 'sample script setup' '\n@@ -106,6 +106,19 @@ test_expect_success 'with succeeding hook' '\n \ttest_cmp expected_hooks actual_hooks\n '\n \n+# NEEDSWORK: when 'git hook add' and 'git hook remove' have been added, use that\n+# instead\n+test_expect_success 'with succeeding hook (config-based)' '\n+\ttest_when_finished \"git config --unset hook.pre-commit.command success.sample\" &&\n+\ttest_when_finished \"rm -f expected_hooks actual_hooks\" &&\n+\tgit config hook.pre-commit.command \"$HOOKDIR/success.sample\" &&\n+\techo \"$HOOKDIR/success.sample\" >expected_hooks &&\n+\techo \"more\" >>file &&\n+\tgit add file &&\n+\tgit commit -m \"more\" &&\n+\ttest_cmp expected_hooks actual_hooks\n+'\n+\n test_expect_success 'with succeeding hook (merge)' '\n \ttest_when_finished \"rm -f \\\"$PREMERGE\\\" expected_hooks actual_hooks\" &&\n \tcp \"$HOOKDIR/success.sample\" \"$PREMERGE\" &&\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418800","messageId":"20210311021037.3001235-13-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 12/37] hook: allow parallel hook execution","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:12Z","receivedAt":"2021-03-11T02:12:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"In many cases, there's no reason not to allow hooks to execute in\nparallel. run_processes_parallel() is well-suited - it's a task queue\nthat runs its housekeeping in series, which means users don't\nneed to worry about thread safety on their callback data. True\nmultithreaded execution with the async_* functions isn't necessary here.\nSynchronous hook execution can be achieved by only allowing 1 job to run\nat a time.\n\nTeach run_hooks() to use that function for simple hooks which don't\nrequire stdin or capture of stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Per AEvar's request - parallel hook execution on day zero.\n    \n    In most ways run_processes_parallel() worked great for me - but it didn't\n    have great support for hooks where we pipe to and from. I had to add this\n    support later in the series.\n    \n    Since I modified an existing and in-use library I'd appreciate a keen look on\n    these patches.\n    \n     - Emily\n\n Documentation/config/hook.txt |   5 ++\n Documentation/git-hook.txt    |  14 ++++-\n builtin/hook.c                |   6 +-\n hook.c                        | 108 +++++++++++++++++++++++++++++-----\n hook.h                        |  21 ++++++-\n 5 files changed, 132 insertions(+), 22 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 8b12512e33..4f66bb35cf 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -20,3 +20,8 @@ hook.runHookDir::\n \tControls how hooks contained in your hookdir are executed. Can be any of\n \t\"yes\", \"warn\", \"interactive\", or \"no\". Defaults to \"yes\". See\n \tlinkgit:git-hook[1] and linkgit:git-config[1] \"core.hooksPath\").\n+\n+hook.jobs::\n+\tSpecifies how many hooks can be run simultaneously during parallelized\n+\thook execution. If unspecified, defaults to the number of processors on\n+\tthe current system.\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 96a857c682..81b8e94994 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -10,7 +10,7 @@ SYNOPSIS\n [verse]\n 'git hook' list <hook-name>\n 'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>]\n-\t<hook-name>\n+\t[(-j|--jobs) <n>] <hook-name>\n \n DESCRIPTION\n -----------\n@@ -99,7 +99,7 @@ in the order they should be run, and print the config scope where the relevant\n `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n This output is human-readable and the format is subject to change over time.\n \n-run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] `<hook-name>`::\n+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] [(-j|--jobs)<n>] `<hook-name>`::\n \n Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n@@ -131,6 +131,16 @@ Specify environment variables to set for every hook that is run.\n Specify a file which will be streamed into stdin for every hook that is run.\n Each hook will receive the entire file from beginning to EOF.\n \n+-j::\n+--jobs::\n+\tOnly valid for `run`.\n++\n+Specify how many hooks to run simultaneously. If this flag is not specified, use\n+the value of the `hook.jobs` config. If the config is not specified, use the\n+number of CPUs on the current system. Some hooks may be ineligible for\n+parallelization: for example, 'commit-msg' intends hooks modify the commit\n+message body and cannot be parallelized.\n+\n CONFIGURATION\n -------------\n include::config/hook.txt[]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 38a4555e05..b4f4adb1de 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -9,7 +9,7 @@\n static const char * const builtin_hook_usage[] = {\n \tN_(\"git hook list <hookname>\"),\n \tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...]\"\n-\t   \"[--to-stdin=<path>] <hookname>\"),\n+\t   \"[--to-stdin=<path>] [(-j|--jobs) <count>] <hookname>\"),\n \tNULL\n };\n \n@@ -104,10 +104,12 @@ static int run(int argc, const char **argv, const char *prefix)\n \t\t\t   N_(\"argument to pass to hook\")),\n \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n \t\t\t   N_(\"file to read into hooks' stdin\")),\n+\t\tOPT_INTEGER('j', \"jobs\", &opt.jobs,\n+\t\t\t    N_(\"run up to <n> hooks simultaneously\")),\n \t\tOPT_END(),\n \t};\n \n-\trun_hooks_opt_init(&opt);\n+\trun_hooks_opt_init_async(&opt);\n \n \targc = parse_options(argc, argv, prefix, run_options,\n \t\t\t     builtin_hook_usage, 0);\ndiff --git a/hook.c b/hook.c\nindex f906e8c61c..fe8860860b 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -144,6 +144,14 @@ enum hookdir_opt configured_hookdir_opt(void)\n \treturn HOOKDIR_UNKNOWN;\n }\n \n+int configured_hook_jobs(void)\n+{\n+\tint n = online_cpus();\n+\tgit_config_get_int(\"hook.jobs\", &n);\n+\n+\treturn n;\n+}\n+\n static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n {\n \tstruct strbuf prompt = STRBUF_INIT;\n@@ -236,12 +244,19 @@ struct list_head* hook_list(const struct strbuf* hookname)\n \treturn hook_head;\n }\n \n-void run_hooks_opt_init(struct run_hooks_opt *o)\n+void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n {\n \tstrvec_init(&o->env);\n \tstrvec_init(&o->args);\n \to->path_to_stdin = NULL;\n \to->run_hookdir = configured_hookdir_opt();\n+\to->jobs = 1;\n+}\n+\n+void run_hooks_opt_init_async(struct run_hooks_opt *o)\n+{\n+\trun_hooks_opt_init_sync(o);\n+\to->jobs = configured_hook_jobs();\n }\n \n int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n@@ -269,19 +284,26 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)\n \tstrvec_clear(&o->args);\n }\n \n-static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n-\t\t\t    struct child_process *cp)\n+static int pick_next_hook(struct child_process *cp,\n+\t\t\t  struct strbuf *out,\n+\t\t\t  void *pp_cb,\n+\t\t\t  void **pp_task_cb)\n {\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *hook = hook_cb->run_me;\n+\n \tif (!hook)\n-\t\treturn;\n+\t\treturn 0;\n \n \t/* reopen the file for stdin; run_command closes it. */\n-\tif (options->path_to_stdin)\n-\t\tcp->in = xopen(options->path_to_stdin, O_RDONLY);\n-\telse\n+\tif (hook_cb->options->path_to_stdin) {\n+\t\tcp->no_stdin = 0;\n+\t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else {\n \t\tcp->no_stdin = 1;\n+\t}\n \n-\tcp->env = options->env.v;\n+\tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook->command.buf;\n \n@@ -298,14 +320,59 @@ static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n \t * add passed-in argv, without expanding - let the user get back\n \t * exactly what they put in\n \t */\n-\tstrvec_pushv(&cp->args, options->args.v);\n+\tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n+\n+\t/* Provide context for errors if necessary */\n+\t*pp_task_cb = hook;\n+\n+\t/* Get the next entry ready */\n+\tif (hook_cb->run_me->list.next == hook_cb->head)\n+\t\thook_cb->run_me = NULL;\n+\telse\n+\t\thook_cb->run_me = list_entry(hook_cb->run_me->list.next,\n+\t\t\t\t\t     struct hook, list);\n+\n+\treturn 1;\n+}\n+\n+static int notify_start_failure(struct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cp)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *attempted = pp_task_cp;\n+\n+\t/* |= rc in cb */\n+\thook_cb->rc |= 1;\n+\n+\tstrbuf_addf(out, _(\"Couldn't start '%s', configured in '%s'\\n\"),\n+\t\t    attempted->command.buf,\n+\t\t    attempted->from_hookdir ? \"hookdir\"\n+\t\t\t: config_scope_name(attempted->origin));\n+\n+\t/* NEEDSWORK: if halt_on_error is desired, do it here. */\n+\treturn 0;\n+}\n+\n+static int notify_hook_finished(int result,\n+\t\t\t\tstruct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\n+\t/* |= rc in cb */\n+\thook_cb->rc |= result;\n+\n+\t/* NEEDSWORK: if halt_on_error is desired, do it here. */\n+\treturn 0;\n }\n \n int run_hooks(const char *hookname, struct run_hooks_opt *options)\n {\n \tstruct strbuf hookname_str = STRBUF_INIT;\n \tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n-\tint rc = 0;\n+\tstruct hook_cb_data cb_data = { 0, NULL, NULL, options };\n \n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n@@ -315,17 +382,26 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \tto_run = hook_list(&hookname_str);\n \n \tlist_for_each_safe(pos, tmp, to_run) {\n-\t\tstruct child_process hook_proc = CHILD_PROCESS_INIT;\n \t\tstruct hook *hook = list_entry(pos, struct hook, list);\n \n \t\tif (hook->from_hookdir &&\n \t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n-\t\t\tcontinue;\n+\t\t\t    list_del(pos);\n+\t}\n+\n+\tif (list_empty(to_run))\n+\t\treturn 0;\n \n-\t\tprepare_hook_cp(hook, options, &hook_proc);\n+\tcb_data.head = to_run;\n+\tcb_data.run_me = list_entry(to_run->next, struct hook, list);\n \n-\t\trc |= run_command(&hook_proc);\n-\t}\n+\trun_processes_parallel_tr2(options->jobs,\n+\t\t\t\t   pick_next_hook,\n+\t\t\t\t   notify_start_failure,\n+\t\t\t\t   notify_hook_finished,\n+\t\t\t\t   &cb_data,\n+\t\t\t\t   \"hook\",\n+\t\t\t\t   hookname);\n \n-\treturn rc;\n+\treturn cb_data.rc;\n }\ndiff --git a/hook.h b/hook.h\nindex 2314ec5962..2593f932c0 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -38,6 +38,9 @@ enum hookdir_opt\n  */\n enum hookdir_opt configured_hookdir_opt(void);\n \n+/* Provides the number of threads to use for parallel hook execution. */\n+int configured_hook_jobs(void);\n+\n struct run_hooks_opt\n {\n \t/* Environment vars to be set for each hook */\n@@ -48,16 +51,30 @@ struct run_hooks_opt\n \n \t/*\n \t * How should the hookdir be handled?\n-\t * Leave the RUN_HOOKS_OPT_INIT default in most cases; this only needs\n+\t * Leave the run_hooks_opt_init_*() default in most cases; this only needs\n \t * to be overridden if the user can override it at the command line.\n \t */\n \tenum hookdir_opt run_hookdir;\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n+\n+\t/* Number of threads to parallelize across */\n+\tint jobs;\n+};\n+\n+/*\n+ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n+ */\n+struct hook_cb_data {\n+\tint rc;\n+\tstruct list_head *head;\n+\tstruct hook *run_me;\n+\tstruct run_hooks_opt *options;\n };\n \n-void run_hooks_opt_init(struct run_hooks_opt *o);\n+void run_hooks_opt_init_sync(struct run_hooks_opt *o);\n+void run_hooks_opt_init_async(struct run_hooks_opt *o);\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /*\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418794","messageId":"20210311021037.3001235-16-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 15/37] hook: provide stdin by string_list or callback","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:15Z","receivedAt":"2021-03-11T02:12:09Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"In cases where a hook requires only a small amount of information via\nstdin, it should be simple for users to provide a string_list alone. But\nin more complicated cases where the stdin is too large to hold in\nmemory, let's provide a callback the users can populate line after line\nwith instead.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n hook.c | 35 ++++++++++++++++++++++++++++++++++-\n hook.h | 28 ++++++++++++++++++++++++++++\n 2 files changed, 62 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 9088b520f3..a509d2d80e 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -9,6 +9,7 @@ void free_hook(struct hook *ptr)\n {\n \tif (ptr) {\n \t\tstrbuf_release(&ptr->command);\n+\t\tfree(ptr->feed_pipe_cb_data);\n \t\tfree(ptr);\n \t}\n }\n@@ -39,6 +40,7 @@ static void append_or_move_hook(struct list_head *head, const char *command)\n \t\tstrbuf_init(&to_add->command, 0);\n \t\tstrbuf_addstr(&to_add->command, command);\n \t\tto_add->from_hookdir = 0;\n+\t\tto_add->feed_pipe_cb_data = NULL;\n \t}\n \n \t/* re-set the scope so we show where an override was specified */\n@@ -252,6 +254,8 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n \to->run_hookdir = configured_hookdir_opt();\n \to->jobs = 1;\n \to->dir = NULL;\n+\to->feed_pipe = NULL;\n+\to->feed_pipe_ctx = NULL;\n }\n \n void run_hooks_opt_init_async(struct run_hooks_opt *o)\n@@ -285,6 +289,28 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)\n \tstrvec_clear(&o->args);\n }\n \n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n+{\n+\tint *item_idx;\n+\tstruct hook *ctx = pp_task_cb;\n+\tstruct string_list *to_pipe = ((struct hook_cb_data*)pp_cb)->options->feed_pipe_ctx;\n+\n+\t/* Bootstrap the state manager if necessary. */\n+\tif (!ctx->feed_pipe_cb_data) {\n+\t\tctx->feed_pipe_cb_data = xmalloc(sizeof(unsigned int));\n+\t\t*(int*)ctx->feed_pipe_cb_data = 0;\n+\t}\n+\n+\titem_idx = ctx->feed_pipe_cb_data;\n+\n+\tif (*item_idx < to_pipe->nr) {\n+\t\tstrbuf_addf(pipe, \"%s\\n\", to_pipe->items[*item_idx].string);\n+\t\t(*item_idx)++;\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n static int pick_next_hook(struct child_process *cp,\n \t\t\t  struct strbuf *out,\n \t\t\t  void *pp_cb,\n@@ -300,6 +326,10 @@ static int pick_next_hook(struct child_process *cp,\n \tif (hook_cb->options->path_to_stdin) {\n \t\tcp->no_stdin = 0;\n \t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else if (hook_cb->options->feed_pipe) {\n+\t\t/* ask for start_command() to make a pipe for us */\n+\t\tcp->in = -1;\n+\t\tcp->no_stdin = 0;\n \t} else {\n \t\tcp->no_stdin = 1;\n \t}\n@@ -379,6 +409,9 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n+\tif (options->path_to_stdin && options->feed_pipe)\n+\t\tBUG(\"choose only one method to populate stdin\");\n+\n \tstrbuf_addstr(&hookname_str, hookname);\n \n \tto_run = hook_list(&hookname_str);\n@@ -400,7 +433,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->feed_pipe,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/hook.h b/hook.h\nindex fcd8e99e39..ecf0228a46 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -2,6 +2,7 @@\n #include \"list.h\"\n #include \"strbuf.h\"\n #include \"strvec.h\"\n+#include \"run-command.h\"\n \n struct hook {\n \tstruct list_head list;\n@@ -13,6 +14,12 @@ struct hook {\n \t/* The literal command to run. */\n \tstruct strbuf command;\n \tunsigned from_hookdir : 1;\n+\n+\t/*\n+\t * Use this to keep state for your feed_pipe_fn if you are using\n+\t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n+\t */\n+\tvoid *feed_pipe_cb_data;\n };\n \n /*\n@@ -58,14 +65,35 @@ struct run_hooks_opt\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n+\t/*\n+\t * Callback and state pointer to ask for more content to pipe to stdin.\n+\t * Will be called repeatedly, for each hook. See\n+\t * hook.c:pipe_from_stdin() for an example. Keep per-hook state in\n+\t * hook.feed_pipe_cb_data (per process). Keep initialization context in\n+\t * feed_pipe_ctx (shared by all processes).\n+\t *\n+\t * See 'pipe_from_string_list()' for info about how to specify a\n+\t * string_list as the stdin input instead of writing your own handler.\n+\t */\n+\tfeed_pipe_fn feed_pipe;\n+\tvoid *feed_pipe_ctx;\n \n \t/* Number of threads to parallelize across */\n \tint jobs;\n \n \t/* Path to initial working directory for subprocess */\n \tconst char *dir;\n+\n };\n \n+/*\n+ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n+ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n+ * This will pipe each string in the list to stdin, separated by newlines.  (Do\n+ * not inject your own newlines.)\n+ */\n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n+\n /*\n  * Callback provided to feed_pipe_fn and consume_sideband_fn.\n  */\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418796","messageId":"20210311021037.3001235-18-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 17/37] hooks: allow callers to capture output","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:17Z","receivedAt":"2021-03-11T02:12:09Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Some server-side hooks will require capturing output to send over\nsideband instead of printing directly to stderr. Expose that capability.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    You can see this in practice in the conversions for some of the push hooks,\n    like 'receive-pack'.\n\n hook.c | 3 ++-\n hook.h | 8 ++++++++\n 2 files changed, 10 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex e16b082cbd..2322720ffe 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -256,6 +256,7 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n \to->dir = NULL;\n \to->feed_pipe = NULL;\n \to->feed_pipe_ctx = NULL;\n+\to->consume_sideband = NULL;\n }\n \n void run_hooks_opt_init_async(struct run_hooks_opt *o)\n@@ -434,7 +435,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->consume_sideband,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/hook.h b/hook.h\nindex ecf0228a46..4ff9999b04 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -78,6 +78,14 @@ struct run_hooks_opt\n \tfeed_pipe_fn feed_pipe;\n \tvoid *feed_pipe_ctx;\n \n+\t/*\n+\t * Populate this to capture output and prevent it from being printed to\n+\t * stderr. This will be passed directly through to\n+\t * run_command:run_parallel_processes(). See t/helper/test-run-command.c\n+\t * for an example.\n+\t */\n+\tconsume_sideband_fn consume_sideband;\n+\n \t/* Number of threads to parallelize across */\n \tint jobs;\n \n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418797","messageId":"20210311021037.3001235-22-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 21/37] gc: use hook library for pre-auto-gc hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:21Z","receivedAt":"2021-03-11T02:12:09Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Using the hook.h library instead of the run-command.h library to run\npre-auto-gc means that those hooks can be set up in config files, as\nwell as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt | 3 +++\n builtin/gc.c               | 5 ++++-\n 2 files changed, 7 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 664ad4803e..00f88912cd 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -560,6 +560,9 @@ This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It\n takes no parameter, and exiting with non-zero status from this script\n causes the `git gc --auto` to abort.\n \n+Hooks run during 'pre-auto-gc' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n post-rewrite\n ~~~~~~~~~~~~\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex ef7226d7bc..e62cb510ee 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -32,6 +32,7 @@\n #include \"remote.h\"\n #include \"object-store.h\"\n #include \"exec-cmd.h\"\n+#include \"hook.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -348,6 +349,8 @@ static void add_repack_incremental_option(void)\n \n static int need_to_gc(void)\n {\n+\tstruct run_hooks_opt hook_opt;\n+\trun_hooks_opt_init_async(&hook_opt);\n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n \t * automatic gc.\n@@ -394,7 +397,7 @@ static int need_to_gc(void)\n \telse\n \t\treturn 0;\n \n-\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n+\tif (run_hooks(\"pre-auto-gc\", &hook_opt))\n \t\treturn 0;\n \treturn 1;\n }\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418798","messageId":"20210311021037.3001235-20-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 19/37] am: convert applypatch hooks to use config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:19Z","receivedAt":"2021-03-11T02:12:09Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach pre-applypatch, post-applypatch, and applypatch-msg to use the\nhook.h library instead of the run-command.h library. This enables use of\nhooks specified in the config, in addition to those in the hookdir.\nThese three hooks are called only by builtin/am.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  9 +++++++++\n builtin/am.c               | 14 +++++++++++---\n 2 files changed, 20 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 984fb998b2..0e7eb972ab 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -58,6 +58,9 @@ the message file.\n The default 'applypatch-msg' hook, when enabled, runs the\n 'commit-msg' hook, if the latter is enabled.\n \n+Hooks run during 'applypatch-msg' will not be parallelized, because hooks are\n+expected to edit the file holding the commit log message.\n+\n pre-applypatch\n ~~~~~~~~~~~~~~\n \n@@ -73,6 +76,9 @@ make a commit if it does not pass certain test.\n The default 'pre-applypatch' hook, when enabled, runs the\n 'pre-commit' hook, if the latter is enabled.\n \n+Hooks run during 'pre-applypatch' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n post-applypatch\n ~~~~~~~~~~~~~~~\n \n@@ -82,6 +88,9 @@ and is invoked after the patch is applied and a commit is made.\n This hook is meant primarily for notification, and cannot affect\n the outcome of `git am`.\n \n+Hooks run during 'post-applypatch' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n pre-commit\n ~~~~~~~~~~\n \ndiff --git a/builtin/am.c b/builtin/am.c\nindex 8355e3566f..4467fd9e63 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -33,6 +33,7 @@\n #include \"string-list.h\"\n #include \"packfile.h\"\n #include \"repository.h\"\n+#include \"hook.h\"\n \n /**\n  * Returns the length of the first line of msg.\n@@ -426,9 +427,13 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n+\tstruct run_hooks_opt opt;\n+\trun_hooks_opt_init_sync(&opt);\n \n \tassert(state->msg);\n-\tret = run_hook_le(NULL, \"applypatch-msg\", am_path(state, \"final-commit\"), NULL);\n+\tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n+\tret = run_hooks(\"applypatch-msg\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tif (!ret) {\n \t\tFREE_AND_NULL(state->msg);\n@@ -1558,8 +1563,10 @@ static void do_commit(const struct am_state *state)\n \tstruct commit_list *parents = NULL;\n \tconst char *reflog_msg, *author, *committer = NULL;\n \tstruct strbuf sb = STRBUF_INIT;\n+\tstruct run_hooks_opt hook_opt;\n+\trun_hooks_opt_init_async(&hook_opt);\n \n-\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n+\tif (run_hooks(\"pre-applypatch\", &hook_opt))\n \t\texit(1);\n \n \tif (write_cache_as_tree(&tree, 0, NULL))\n@@ -1611,8 +1618,9 @@ static void do_commit(const struct am_state *state)\n \t\tfclose(fp);\n \t}\n \n-\trun_hook_le(NULL, \"post-applypatch\", NULL);\n+\trun_hooks(\"post-applypatch\", &hook_opt);\n \n+\trun_hooks_opt_clear(&hook_opt);\n \tstrbuf_release(&sb);\n }\n \n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418799","messageId":"20210311021037.3001235-21-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 20/37] merge: use config-based hooks for post-merge hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:20Z","receivedAt":"2021-03-11T02:12:09Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach post-merge to use the hook.h library instead of the run-command.h\nlibrary to run hooks. This means that post-merge hooks can come from the\nconfig as well as from the hookdir. post-merge is invoked only from\nbuiltin/merge.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt | 3 +++\n builtin/merge.c            | 6 +++++-\n 2 files changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 0e7eb972ab..664ad4803e 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -242,6 +242,9 @@ save and restore any form of metadata associated with the working tree\n (e.g.: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl\n for an example of how to do this.\n \n+Hooks executed during 'post-merge' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n pre-push\n ~~~~~~~~\n \ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 33df744ab0..b473c8c5d3 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -444,7 +444,9 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n+\tstruct run_hooks_opt opt;\n \tconst struct object_id *head = &head_commit->object.oid;\n+\trun_hooks_opt_init_async(&opt);\n \n \tif (!msg)\n \t\tstrbuf_addstr(&reflog_message, getenv(\"GIT_REFLOG_ACTION\"));\n@@ -485,7 +487,9 @@ static void finish(struct commit *head_commit,\n \t}\n \n \t/* Run a post-merge hook */\n-\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n+\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n+\trun_hooks(\"post-merge\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tapply_autostash(git_path_merge_autostash(the_repository));\n \tstrbuf_release(&reflog_message);\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418801","messageId":"20210311021037.3001235-17-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 16/37] run-command: allow capturing of collated output","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:16Z","receivedAt":"2021-03-11T02:12:09Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Some callers, for example server-side hooks which wish to relay hook\noutput to clients across a transport, want to capture what would\nnormally print to stderr and do something else with it. Allow that via a\ncallback.\n\nBy calling the callback regardless of whether there's output available,\nwe allow clients to send e.g. a keepalive if necessary.\n\nBecause we expose a strbuf, not a fd or FILE*, there's no need to create\na temporary pipe or similar - we can just skip the print to stderr and\ninstead hand it to the caller.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 33 +++++++++++++++++++++++++--------\n run-command.h               | 18 +++++++++++++++++-\n submodule.c                 |  2 +-\n t/helper/test-run-command.c | 25 ++++++++++++++++++++-----\n t/t0061-run-command.sh      |  7 +++++++\n 8 files changed, 73 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex d8e798dc69..b6d45f8359 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1757,7 +1757,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n-\t\t\t\t\t\t    NULL,\n+\t\t\t\t\t\t    NULL, NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 14f6e4ee8c..136e09a016 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2294,7 +2294,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure, NULL,\n+\t\t\t\t   update_clone_start_failure, NULL, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex a509d2d80e..e16b082cbd 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -434,6 +434,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex e7eeb6c49b..36a4edbacf 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1559,6 +1559,7 @@ struct parallel_processes {\n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n \tfeed_pipe_fn feed_pipe;\n+\tconsume_sideband_fn consume_sideband;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1624,6 +1625,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n \t\t    feed_pipe_fn feed_pipe,\n+\t\t    consume_sideband_fn consume_sideband,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1644,6 +1646,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n \tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n+\tpp->consume_sideband = consume_sideband;\n \n \tpp->nr_processes = 0;\n \tpp->output_owner = 0;\n@@ -1680,7 +1683,10 @@ static void pp_cleanup(struct parallel_processes *pp)\n \t * When get_next_task added messages to the buffer in its last\n \t * iteration, the buffered output is non empty.\n \t */\n-\tstrbuf_write(&pp->buffered_output, stderr);\n+\tif (pp->consume_sideband)\n+\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\telse\n+\t\tstrbuf_write(&pp->buffered_output, stderr);\n \tstrbuf_release(&pp->buffered_output);\n \n \tsigchain_pop_common();\n@@ -1801,9 +1807,13 @@ static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n static void pp_output(struct parallel_processes *pp)\n {\n \tint i = pp->output_owner;\n+\n \tif (pp->children[i].state == GIT_CP_WORKING &&\n \t    pp->children[i].err.len) {\n-\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\tif (pp->consume_sideband)\n+\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\telse\n+\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n \t\tstrbuf_reset(&pp->children[i].err);\n \t}\n }\n@@ -1842,11 +1852,15 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\t\tstrbuf_addbuf(&pp->buffered_output, &pp->children[i].err);\n \t\t\tstrbuf_reset(&pp->children[i].err);\n \t\t} else {\n-\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t/* Output errors, then all other finished child processes */\n+\t\t\tif (pp->consume_sideband) {\n+\t\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\t\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\t\t\t} else {\n+\t\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n+\t\t\t}\n \t\t\tstrbuf_reset(&pp->children[i].err);\n-\n-\t\t\t/* Output all other finished child processes */\n-\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n \t\t\tstrbuf_reset(&pp->buffered_output);\n \n \t\t\t/*\n@@ -1870,6 +1884,7 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n \t\t\t   feed_pipe_fn feed_pipe,\n+\t\t\t   consume_sideband_fn consume_sideband,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1880,7 +1895,7 @@ int run_processes_parallel(int n,\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n \n-\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, consume_sideband, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1918,6 +1933,7 @@ int run_processes_parallel(int n,\n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n \t\t\t       feed_pipe_fn feed_pipe,\n+\t\t\t       consume_sideband_fn consume_sideband,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1927,7 +1943,8 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, consume_sideband,\n+\t\t\t\t\ttask_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 1e3cf0999f..ebc4a95a94 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -457,6 +457,20 @@ typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n \t\t\t    void *pp_cb,\n \t\t\t    void *pp_task_cb);\n \n+/**\n+ * If this callback is provided, instead of collating process output to stderr,\n+ * they will be collated into a new pipe. consume_sideband_fn will be called\n+ * repeatedly. When output is available on that pipe, it will be contained in\n+ * 'output'. But it will be called with an empty 'output' too, to allow for\n+ * keepalives or similar operations if necessary.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel.\n+ *\n+ * Since this callback is provided with the collated output, no task cookie is\n+ * provided.\n+ */\n+typedef void (*consume_sideband_fn)(struct strbuf *output, void *pp_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -492,10 +506,12 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n \t\t\t   feed_pipe_fn,\n+\t\t\t   consume_sideband_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, consume_sideband_fn,\n+\t\t\t       task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex dc4a6a60f4..4926642451 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1644,7 +1644,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   NULL, NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 9348184d30..d53db6d11c 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -51,6 +51,16 @@ static int no_job(struct child_process *cp,\n \treturn 0;\n }\n \n+static void test_consume_sideband(struct strbuf *output, void *cb)\n+{\n+\tFILE *sideband;\n+\n+\tsideband = fopen(\"./sideband\", \"a\");\n+\n+\tstrbuf_write(output, sideband);\n+\tfclose(sideband);\n+}\n+\n static int task_finished(int result,\n \t\t\t struct strbuf *err,\n \t\t\t void *pp_cb,\n@@ -201,7 +211,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_stdin, test_finished, &suite);\n+\t\t\t\t     test_stdin, NULL, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -429,23 +439,28 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-stdin\")) {\n \t\tproc.in = -1;\n \t\tproc.no_stdin = 0;\n \t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n-\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t\t\t\t\t     test_stdin, NULL, NULL, &proc));\n \t}\n \n+\tif (!strcmp(argv[1], \"run-command-sideband\"))\n+\t\texit(run_processes_parallel(jobs, parallel_next, NULL, NULL,\n+\t\t\t\t\t    test_consume_sideband, NULL,\n+\t\t\t\t\t    &proc));\n+\n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\n }\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 87759482ad..e99f6c7f44 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,13 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'run_command can divert output' '\n+\ttest_when_finished rm sideband &&\n+\ttest-tool run-command run-command-sideband 3 sh -c \"printf \\\"%s\\n%s\\n\\\" Hello World\" 2>actual &&\n+\ttest_must_be_empty actual &&\n+\ttest_cmp expect sideband\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n listening for stdin:\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418802","messageId":"20210311021037.3001235-23-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 22/37] rebase: teach pre-rebase to use hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:22Z","receivedAt":"2021-03-11T02:12:09Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using hook.h instead of run-command.h to run hooks, pre-rebase hooks\ncan now be specified in the config as well as in the hookdir. pre-rebase\nis not called anywhere besides builtin/rebase.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt | 3 +++\n builtin/rebase.c           | 9 +++++++--\n 2 files changed, 10 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 00f88912cd..e3a0375827 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -206,6 +206,9 @@ two parameters.  The first parameter is the upstream from which\n the series was forked.  The second parameter is the branch being\n rebased, and is not set when rebasing the current branch.\n \n+Hooks executed during 'pre-rebase' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n post-checkout\n ~~~~~~~~~~~~~\n \ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex de400f9a19..c35b5ba452 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -28,6 +28,7 @@\n #include \"sequencer.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_REFLOG_ACTION \"rebase\"\n \n@@ -1318,6 +1319,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n+\tstruct run_hooks_opt hook_opt;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\n@@ -1431,6 +1433,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t};\n \tint i;\n \n+\trun_hooks_opt_init_async(&hook_opt);\n+\n \tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n \t\tusage_with_options(builtin_rebase_usage,\n \t\t\t\t   builtin_rebase_options);\n@@ -2032,9 +2036,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t}\n \n \t/* If a hook exists, give it a chance to interrupt*/\n+\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n \tif (!ok_to_skip_pre_rebase &&\n-\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n-\t\t\targc ? argv[0] : NULL, NULL))\n+\t    run_hooks(\"pre-rebase\", &hook_opt))\n \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n \n \tif (options.flags & REBASE_DIFFSTAT) {\n@@ -2114,6 +2118,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tret = !!run_specific_rebase(&options, action);\n \n cleanup:\n+\trun_hooks_opt_clear(&hook_opt);\n \tstrbuf_release(&buf);\n \tstrbuf_release(&revisions);\n \tfree(options.head_name);\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418803","messageId":"20210311021037.3001235-27-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 26/37] hooks: convert 'post-checkout' hook to hook library","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:26Z","receivedAt":"2021-03-11T02:12:38Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the 'hook.h' library, 'post-checkout' hooks can now be\nspecified in the config as well as in the hook directory.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  2 ++\n builtin/checkout.c         | 19 ++++++++++++++-----\n builtin/clone.c            |  8 ++++++--\n builtin/worktree.c         | 31 +++++++++++++++----------------\n reset.c                    | 16 ++++++++++++----\n 5 files changed, 49 insertions(+), 27 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex f2178dbc83..362224a03b 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -231,6 +231,8 @@ This hook can be used to perform repository validity checks, auto-display\n differences from the previous HEAD if different, or set working dir metadata\n properties.\n \n+Hooks executed during 'post-checkout' will not be parallelized.\n+\n post-merge\n ~~~~~~~~~~\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 2d6550bc3c..f287b5e643 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"diff.h\"\n #include \"dir.h\"\n+#include \"hook.h\"\n #include \"ll-merge.h\"\n #include \"lockfile.h\"\n #include \"merge-recursive.h\"\n@@ -104,13 +105,21 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\treturn run_hook_le(NULL, \"post-checkout\",\n-\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : &null_oid),\n-\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : &null_oid),\n-\t\t\t   changed ? \"1\" : \"0\", NULL);\n+\tstruct run_hooks_opt opt;\n+\tint rc;\n+\n+\trun_hooks_opt_init_sync(&opt);\n+\n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\n-\n+\tstrvec_pushl(&opt.args,\n+\t\t     oid_to_hex(old_commit ? &old_commit->object.oid : &null_oid),\n+\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : &null_oid),\n+\t\t     changed ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trc = run_hooks(\"post-checkout\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n }\n \n static int update_some(const struct object_id *oid, struct strbuf *base,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 51e844a2de..52f2a5ecb4 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -32,6 +32,7 @@\n #include \"connected.h\"\n #include \"packfile.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"hook.h\"\n \n /*\n  * Overall FIXMEs:\n@@ -771,6 +772,8 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n+\tstruct run_hooks_opt hook_opt;\n+\trun_hooks_opt_init_sync(&hook_opt);\n \n \tif (option_no_checkout)\n \t\treturn 0;\n@@ -816,8 +819,9 @@ static int checkout(int submodule_progress)\n \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n \t\tdie(_(\"unable to write new index file\"));\n \n-\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(&null_oid),\n-\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n+\tstrvec_pushl(&hook_opt.args, oid_to_hex(&null_oid), oid_to_hex(&oid), \"1\", NULL);\n+\terr |= run_hooks(\"post-checkout\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (!err && (option_recurse_submodules.nr > 0)) {\n \t\tstruct strvec args = STRVEC_INIT;\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 1cd5c2016e..8b06d121e5 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -13,6 +13,7 @@\n #include \"utf8.h\"\n #include \"worktree.h\"\n #include \"quote.h\"\n+#include \"hook.h\"\n \n static const char * const worktree_usage[] = {\n \tN_(\"git worktree add [<options>] <path> [<commit-ish>]\"),\n@@ -383,22 +384,20 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tconst char *hook = find_hook(\"post-checkout\");\n-\t\tif (hook) {\n-\t\t\tconst char *env[] = { \"GIT_DIR\", \"GIT_WORK_TREE\", NULL };\n-\t\t\tcp.git_cmd = 0;\n-\t\t\tcp.no_stdin = 1;\n-\t\t\tcp.stdout_to_stderr = 1;\n-\t\t\tcp.dir = path;\n-\t\t\tcp.env = env;\n-\t\t\tcp.argv = NULL;\n-\t\t\tcp.trace2_hook_name = \"post-checkout\";\n-\t\t\tstrvec_pushl(&cp.args, absolute_path(hook),\n-\t\t\t\t     oid_to_hex(&null_oid),\n-\t\t\t\t     oid_to_hex(&commit->object.oid),\n-\t\t\t\t     \"1\", NULL);\n-\t\t\tret = run_command(&cp);\n-\t\t}\n+\t\tstruct run_hooks_opt opt;\n+\t\trun_hooks_opt_init_sync(&opt);\n+\n+\t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(&null_oid),\n+\t\t\t     oid_to_hex(&commit->object.oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\topt.dir = path;\n+\n+\t\tret = run_hooks(\"post-checkout\", &opt);\n+\n+\t\trun_hooks_opt_clear(&opt);\n \t}\n \n \tstrvec_clear(&child_env);\ndiff --git a/reset.c b/reset.c\nindex 2f4fbd07c5..85ee75f7fd 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -7,6 +7,7 @@\n #include \"tree-walk.h\"\n #include \"tree.h\"\n #include \"unpack-trees.h\"\n+#include \"hook.h\"\n \n int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t       const char *switch_to_branch, unsigned flags,\n@@ -126,10 +127,17 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\tret = create_symref(\"HEAD\", switch_to_branch,\n \t\t\t\t\t    reflog_head);\n \t}\n-\tif (run_hook)\n-\t\trun_hook_le(NULL, \"post-checkout\",\n-\t\t\t    oid_to_hex(orig ? orig : &null_oid),\n-\t\t\t    oid_to_hex(oid), \"1\", NULL);\n+\tif (run_hook) {\n+\t\tstruct run_hooks_opt opt;\n+\t\trun_hooks_opt_init_sync(&opt);\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(orig ? orig : &null_oid),\n+\t\t\t     oid_to_hex(oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\trun_hooks(\"post-checkout\", &opt);\n+\t\trun_hooks_opt_clear(&opt);\n+\t}\n \n leave_reset_head:\n \tstrbuf_release(&msg);\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418804","messageId":"20210311021037.3001235-24-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 23/37] read-cache: convert post-index-change hook to use config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:23Z","receivedAt":"2021-03-11T02:12:39Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using hook.h instead of run-command.h to run, post-index-change hooks\ncan now be specified in the config in addition to the hookdir.\npost-index-change is not run anywhere besides in read-cache.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  3 +++\n read-cache.c               | 13 ++++++++++---\n 2 files changed, 13 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex e3a0375827..e5c2cef271 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -720,6 +720,9 @@ and \"0\" meaning they were not.\n Only one parameter should be set to \"1\" when the hook runs.  The hook\n running passing \"1\", \"1\" should not be possible.\n \n+Hooks run during 'post-index-change' will be run in parallel, unless hook.jobs\n+is configured to 1.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/read-cache.c b/read-cache.c\nindex 1e9a50c6c7..fd6c111372 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -25,6 +25,7 @@\n #include \"fsmonitor.h\"\n #include \"thread-utils.h\"\n #include \"progress.h\"\n+#include \"hook.h\"\n \n /* Mask for the name length in ce_flags in the on-disk index */\n \n@@ -3070,6 +3071,8 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \t\t\t\t unsigned flags)\n {\n \tint ret;\n+\tstruct run_hooks_opt hook_opt;\n+\trun_hooks_opt_init_async(&hook_opt);\n \n \t/*\n \t * TODO trace2: replace \"the_repository\" with the actual repo instance\n@@ -3088,9 +3091,13 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \telse\n \t\tret = close_lock_file_gently(lock);\n \n-\trun_hook_le(NULL, \"post-index-change\",\n-\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n-\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n+\tstrvec_pushl(&hook_opt.args,\n+\t\t     istate->updated_workdir ? \"1\" : \"0\",\n+\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trun_hooks(\"post-index-change\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n+\n \tistate->updated_workdir = 0;\n \tistate->updated_skipworktree = 0;\n \n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418805","messageId":"20210311021037.3001235-30-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 29/37] reference-transaction: look for hooks in config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:29Z","receivedAt":"2021-03-11T02:12:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the hook.h library, reference-transaction hooks can be\nspecified in the config instead.\n\nThe expected output of the test is not fully updated to reflect the\nabsolute path of the hook called because the 'update' hook has not yet\nbeen converted to use hook.h.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt       |  3 +++\n refs.c                           | 43 +++++++++++++-------------------\n t/t1416-ref-transaction-hooks.sh |  8 +++---\n 3 files changed, 24 insertions(+), 30 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 489c93a7cb..dc8b7111d5 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -530,6 +530,9 @@ The exit status of the hook is ignored for any state except for the\n cause the transaction to be aborted. The hook will not be called with\n \"aborted\" state in that case.\n \n+Hooks run during 'reference-transaction' will be run in parallel, unless\n+hook.jobs is configured to 1.\n+\n push-to-checkout\n ~~~~~~~~~~~~~~~~\n \ndiff --git a/refs.c b/refs.c\nindex a665ed5e10..4fccbac3e6 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -18,6 +18,7 @@\n #include \"strvec.h\"\n #include \"repository.h\"\n #include \"sigchain.h\"\n+#include \"hook.h\"\n \n /*\n  * List of all available backends\n@@ -2061,47 +2062,37 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n \tstruct strbuf buf = STRBUF_INIT;\n-\tconst char *hook;\n+\tstruct run_hooks_opt opt;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint ret = 0, i;\n+\tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n \n-\thook = find_hook(\"reference-transaction\");\n-\tif (!hook)\n-\t\treturn ret;\n-\n-\tstrvec_pushl(&proc.args, hook, state, NULL);\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"reference-transaction\";\n+\trun_hooks_opt_init_async(&opt);\n \n-\tret = start_command(&proc);\n-\tif (ret)\n+\tif (!hook_exists(\"reference-transaction\", HOOKDIR_USE_CONFIG))\n \t\treturn ret;\n \n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstrvec_push(&opt.args, state);\n \n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n+\t\toid_to_hex_r(o, &update->old_oid);\n+\t\toid_to_hex_r(n, &update->new_oid);\n \n \t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n-\t\t\t    oid_to_hex(&update->old_oid),\n-\t\t\t    oid_to_hex(&update->new_oid),\n-\t\t\t    update->refname);\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstrbuf_addf(&buf, \"%s %s %s\", o, n, update->refname);\n+\t\tstring_list_append(&to_stdin, buf.buf);\n \t}\n \n-\tclose(proc.in);\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tret = run_hooks(\"reference-transaction\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \tstrbuf_release(&buf);\n+\tstring_list_clear(&to_stdin, 0);\n \n-\tret |= finish_command(&proc);\n \treturn ret;\n }\n \ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 6c941027a8..3a90a59143 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -125,11 +125,11 @@ test_expect_success 'interleaving hook calls succeed' '\n \n \tcat >expect <<-EOF &&\n \t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n-\t\thooks/reference-transaction prepared\n-\t\thooks/reference-transaction committed\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n \t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n-\t\thooks/reference-transaction prepared\n-\t\thooks/reference-transaction committed\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n \tEOF\n \n \tgit push ./target-repo.git PRE POST &&\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418806","messageId":"20210311021037.3001235-29-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 28/37] transport: convert pre-push hook to use config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:28Z","receivedAt":"2021-03-11T02:12:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the hook.h:run_hooks API, pre-push hooks can be specified in\nthe config as well as in the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  3 ++\n transport.c                | 59 +++++++++++---------------------------\n 2 files changed, 20 insertions(+), 42 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 544238b381..489c93a7cb 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -279,6 +279,9 @@ If this hook exits with a non-zero status, `git push` will abort without\n pushing anything.  Information about why the push is rejected may be sent\n to the user by writing to standard error.\n \n+Hooks executed during 'pre-push' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n [[pre-receive]]\n pre-receive\n ~~~~~~~~~~~\ndiff --git a/transport.c b/transport.c\nindex b13fab5dc3..286b73881b 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -22,6 +22,7 @@\n #include \"protocol.h\"\n #include \"object-store.h\"\n #include \"color.h\"\n+#include \"hook.h\"\n \n static int transport_use_color = -1;\n static char transport_colors[][COLOR_MAXLEN] = {\n@@ -1172,31 +1173,15 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)\n static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n-\tint ret = 0, x;\n+\tint ret = 0;\n+\tstruct run_hooks_opt opt;\n+\tstruct strbuf tmp = STRBUF_INIT;\n \tstruct ref *r;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct strbuf buf;\n-\tconst char *argv[4];\n-\n-\tif (!(argv[0] = find_hook(\"pre-push\")))\n-\t\treturn 0;\n-\n-\targv[1] = transport->remote->name;\n-\targv[2] = transport->url;\n-\targv[3] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.trace2_hook_name = \"pre-push\";\n-\n-\tif (start_command(&proc)) {\n-\t\tfinish_command(&proc);\n-\t\treturn -1;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n+\trun_hooks_opt_init_async(&opt);\n \n-\tstrbuf_init(&buf, 256);\n+\tstrvec_push(&opt.args, transport->remote->name);\n+\tstrvec_push(&opt.args, transport->url);\n \n \tfor (r = remote_refs; r; r = r->next) {\n \t\tif (!r->peer_ref) continue;\n@@ -1205,30 +1190,20 @@ static int run_pre_push_hook(struct transport *transport,\n \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n \n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n+\t\tstrbuf_reset(&tmp);\n+\t\tstrbuf_addf(&tmp, \"%s %s %s %s\",\n \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n \t\t\t r->name, oid_to_hex(&r->old_oid));\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\t/* We do not mind if a hook does not read all refs. */\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstring_list_append(&to_stdin, tmp.buf);\n \t}\n \n-\tstrbuf_release(&buf);\n-\n-\tx = close(proc.in);\n-\tif (!ret)\n-\t\tret = x;\n-\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n \n-\tx = finish_command(&proc);\n-\tif (!ret)\n-\t\tret = x;\n+\tret = run_hooks(\"pre-push\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n \n \treturn ret;\n }\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418807","messageId":"20210311021037.3001235-28-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 27/37] hook: convert 'post-rewrite' hook to config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:27Z","receivedAt":"2021-03-11T02:12:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using 'hook.h' for 'post-rewrite', we simplify hook invocations by\nnot needing to put together our own 'struct child_process' and we also\nlearn to run hooks specified in the config as well as the hook dir.\n\nThe signal handling that's being removed by this commit now takes place\nin run-command.h:run_processes_parallel(), so it is OK to remove them\nhere.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  3 ++\n builtin/am.c               | 19 +++------\n sequencer.c                | 83 +++++++++++++++++---------------------\n 3 files changed, 45 insertions(+), 60 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 362224a03b..544238b381 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -594,6 +594,9 @@ The hook always runs after the automatic note copying (see\n \"notes.rewrite.<command>\" in linkgit:git-config[1]) has happened, and\n thus has access to these notes.\n \n+Hooks run during 'post-rewrite' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n The following command-specific comments apply:\n \n rebase::\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 4467fd9e63..45425105e8 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -450,23 +450,16 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tconst char *hook = find_hook(\"post-rewrite\");\n+\tstruct run_hooks_opt opt;\n \tint ret;\n+\trun_hooks_opt_init_async(&opt);\n \n-\tif (!hook)\n-\t\treturn 0;\n-\n-\tstrvec_push(&cp.args, hook);\n-\tstrvec_push(&cp.args, \"rebase\");\n-\n-\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n-\tcp.stdout_to_stderr = 1;\n-\tcp.trace2_hook_name = \"post-rewrite\";\n+\tstrvec_push(&opt.args, \"rebase\");\n+\topt.path_to_stdin = am_path(state, \"rewritten\");\n \n-\tret = run_command(&cp);\n+\tret = run_hooks(\"post-rewrite\", &opt);\n \n-\tclose(cp.in);\n+\trun_hooks_opt_clear(&opt);\n \treturn ret;\n }\n \ndiff --git a/sequencer.c b/sequencer.c\nindex e3a951fbeb..8280ba828b 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -35,6 +35,7 @@\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n #include \"hook.h\"\n+#include \"string-list.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -1146,33 +1147,29 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *argv[3];\n+\tstruct run_hooks_opt opt;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n-\tstruct strbuf sb = STRBUF_INIT;\n+\trun_hooks_opt_init_async(&opt);\n \n-\targv[0] = find_hook(\"post-rewrite\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tstrvec_push(&opt.args, \"amend\");\n \n-\targv[1] = \"amend\";\n-\targv[2] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"post-rewrite\";\n-\n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n-\tstrbuf_addf(&sb, \"%s %s\\n\", oid_to_hex(oldoid), oid_to_hex(newoid));\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\twrite_in_full(proc.in, sb.buf, sb.len);\n-\tclose(proc.in);\n-\tstrbuf_release(&sb);\n-\tsigchain_pop(SIGPIPE);\n-\treturn finish_command(&proc);\n+\tstrbuf_addf(&tmp,\n+\t\t    \"%s %s\",\n+\t\t    oid_to_hex(oldoid),\n+\t\t    oid_to_hex(newoid));\n+\tstring_list_append(&to_stdin, tmp.buf);\n+\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tcode = run_hooks(\"post-rewrite\", &opt);\n+\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n+\treturn code;\n }\n \n void commit_post_rewrite(struct repository *r,\n@@ -4325,30 +4322,22 @@ static int pick_commits(struct repository *r,\n \t\tflush_rewritten_pending();\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n-\t\t\tstruct child_process child = CHILD_PROCESS_INIT;\n-\t\t\tconst char *post_rewrite_hook =\n-\t\t\t\tfind_hook(\"post-rewrite\");\n-\n-\t\t\tchild.in = open(rebase_path_rewritten_list(), O_RDONLY);\n-\t\t\tchild.git_cmd = 1;\n-\t\t\tstrvec_push(&child.args, \"notes\");\n-\t\t\tstrvec_push(&child.args, \"copy\");\n-\t\t\tstrvec_push(&child.args, \"--for-rewrite=rebase\");\n+\t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt;\n+\t\t\trun_hooks_opt_init_async(&hook_opt);\n+\n+\t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n+\t\t\tnotes_cp.git_cmd = 1;\n+\t\t\tstrvec_push(&notes_cp.args, \"notes\");\n+\t\t\tstrvec_push(&notes_cp.args, \"copy\");\n+\t\t\tstrvec_push(&notes_cp.args, \"--for-rewrite=rebase\");\n \t\t\t/* we don't care if this copying failed */\n-\t\t\trun_command(&child);\n-\n-\t\t\tif (post_rewrite_hook) {\n-\t\t\t\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\n-\t\t\t\thook.in = open(rebase_path_rewritten_list(),\n-\t\t\t\t\tO_RDONLY);\n-\t\t\t\thook.stdout_to_stderr = 1;\n-\t\t\t\thook.trace2_hook_name = \"post-rewrite\";\n-\t\t\t\tstrvec_push(&hook.args, post_rewrite_hook);\n-\t\t\t\tstrvec_push(&hook.args, \"rebase\");\n-\t\t\t\t/* we don't care if this hook failed */\n-\t\t\t\trun_command(&hook);\n-\t\t\t}\n+\t\t\trun_command(&notes_cp);\n+\n+\t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n+\t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n+\t\t\trun_hooks(\"post-rewrite\", &hook_opt);\n+\t\t\trun_hooks_opt_clear(&hook_opt);\n \t\t}\n \t\tapply_autostash(rebase_path_autostash());\n \n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418808","messageId":"20210311021037.3001235-31-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 30/37] receive-pack: convert 'update' hook to hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:30Z","receivedAt":"2021-03-11T02:12:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using hook.h to invoke the 'update' hook, now hooks can be specified\nin the config in addition to the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt       |  3 ++\n builtin/receive-pack.c           | 66 ++++++++++++++++++++++----------\n t/t1416-ref-transaction-hooks.sh |  4 +-\n 3 files changed, 50 insertions(+), 23 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex dc8b7111d5..60fd43d1da 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -368,6 +368,9 @@ The default 'update' hook, when enabled--and with\n `hooks.allowunannotated` config option unset or set to false--prevents\n unannotated tags to be pushed.\n \n+Hooks executed during 'update' are run in parallel, unless hook.jobs is\n+configured to 1.\n+\n [[proc-receive]]\n proc-receive\n ~~~~~~~~~~~~\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 234b70f0d1..b34a27a303 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -938,33 +938,57 @@ static int run_receive_hook(struct command *commands,\n \treturn status;\n }\n \n-static int run_update_hook(struct command *cmd)\n+static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n {\n-\tconst char *argv[5];\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tint code;\n+\tint keepalive_active = 0;\n \n-\targv[0] = find_hook(\"update\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tif (keepalive_in_sec <= 0)\n+\t\tuse_keepalive = KEEPALIVE_NEVER;\n+\tif (use_keepalive == KEEPALIVE_ALWAYS)\n+\t\tkeepalive_active = 1;\n \n-\targv[1] = cmd->ref_name;\n-\targv[2] = oid_to_hex(&cmd->old_oid);\n-\targv[3] = oid_to_hex(&cmd->new_oid);\n-\targv[4] = NULL;\n+\t/* send a keepalive if there is no data to write */\n+\tif (keepalive_active && !output->len) {\n+\t\tstatic const char buf[] = \"0005\\1\";\n+\t\twrite_or_die(1, buf, sizeof(buf) - 1);\n+\t\treturn;\n+\t}\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.argv = argv;\n-\tproc.trace2_hook_name = \"update\";\n+\tif (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {\n+\t\tconst char *first_null = memchr(output->buf, '\\0', output->len);\n+\t\tif (first_null) {\n+\t\t\t/* The null bit is excluded. */\n+\t\t\tsize_t before_null = first_null - output->buf;\n+\t\t\tsize_t after_null = output->len - (before_null + 1);\n+\t\t\tkeepalive_active = 1;\n+\t\t\tsend_sideband(1, 2, output->buf, before_null, use_sideband);\n+\t\t\tsend_sideband(1, 2, first_null + 1, after_null, use_sideband);\n+\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n+}\n+\n+static int run_update_hook(struct command *cmd)\n+{\n+\tstruct run_hooks_opt opt;\n+\tint code;\n+\trun_hooks_opt_init_async(&opt);\n+\n+\tstrvec_pushl(&opt.args,\n+\t\t     cmd->ref_name,\n+\t\t     oid_to_hex(&cmd->old_oid),\n+\t\t     oid_to_hex(&cmd->new_oid),\n+\t\t     NULL);\n \n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n \tif (use_sideband)\n-\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\treturn finish_command(&proc);\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\tcode = run_hooks(\"update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn code;\n }\n \n static struct command *find_command_by_refname(struct command *list,\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 3a90a59143..0a3c3e4a86 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -124,10 +124,10 @@ test_expect_success 'interleaving hook calls succeed' '\n \tEOF\n \n \tcat >expect <<-EOF &&\n-\t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n+\t\t$(pwd)/target-repo.git/hooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n \t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n \t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n-\t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n+\t\t$(pwd)/target-repo.git/hooks/update refs/tags/POST $ZERO_OID $POST_OID\n \t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n \t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n \tEOF\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418809","messageId":"20210311021037.3001235-25-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:24Z","receivedAt":"2021-03-11T02:12:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using hook.h instead of run-command.h to invoke push-to-checkout,\nhooks can now be specified in the config as well as in the hookdir.\npush-to-checkout is not called anywhere but in builtin/receive-pack.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  1 +\n builtin/receive-pack.c     | 16 ++++++++++++----\n 2 files changed, 13 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex e5c2cef271..f2178dbc83 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -555,6 +555,7 @@ that switches branches while\n keeping the local changes in the working tree that do not interfere\n with the difference between the branches.\n \n+Hooks executed during 'push-to-checkout' will not be parallelized.\n \n pre-auto-gc\n ~~~~~~~~~~~\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex d26040c477..234b70f0d1 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -29,6 +29,7 @@\n #include \"commit-reach.h\"\n #include \"worktree.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static const char * const receive_pack_usage[] = {\n \tN_(\"git receive-pack <git-dir>\"),\n@@ -1435,12 +1436,19 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n+\tstruct run_hooks_opt opt;\n+\trun_hooks_opt_init_sync(&opt);\n+\n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n-\tif (run_hook_le(env->v, push_to_checkout_hook,\n-\t\t\thash_to_hex(hash), NULL))\n+\tstrvec_pushv(&opt.env, env->v);\n+\tstrvec_push(&opt.args, hash_to_hex(hash));\n+\tif (run_hooks(push_to_checkout_hook, &opt)) {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn \"push-to-checkout hook declined\";\n-\telse\n+\t} else {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn NULL;\n+\t}\n }\n \n static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree)\n@@ -1464,7 +1472,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!find_hook(push_to_checkout_hook))\n+\tif (!hook_exists(push_to_checkout_hook, HOOKDIR_USE_CONFIG))\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n \telse\n \t\tretval = push_to_checkout(sha1, &env, work_tree);\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418810","messageId":"20210311021037.3001235-26-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 25/37] git-p4: use 'git hook' to run hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:25Z","receivedAt":"2021-03-11T02:12:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Instead of duplicating the behavior of run-command.h:run_hook_le() in\nPython, we can directly call 'git hook run'. As a bonus, this means\ngit-p4 learns how to find hook specifications from the Git config as\nwell as from the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Maybe there is a better way to do this - I had a hard time getting this to run\n    locally, and Python is not my forte, so if anybody has a better approach I'd\n    love to just take that patch instead :)\n    \n    Since v6, removed the developer debug print statements.... :X\n    \n    Maybe there is a better way to do this - I had a hard time getting this to run\n    locally, and Python is not my forte, so if anybody has a better approach I'd\n    love to just take that patch instead :)\n\n git-p4.py | 67 +++++--------------------------------------------------\n 1 file changed, 6 insertions(+), 61 deletions(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex 09c9e93ac4..4b1c69822c 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -208,70 +208,15 @@ def decode_path(path):\n \n def run_git_hook(cmd, param=[]):\n     \"\"\"Execute a hook if the hook exists.\"\"\"\n-    if verbose:\n-        sys.stderr.write(\"Looking for hook: %s\\n\" % cmd)\n-        sys.stderr.flush()\n-\n-    hooks_path = gitConfig(\"core.hooksPath\")\n-    if len(hooks_path) <= 0:\n-        hooks_path = os.path.join(os.environ[\"GIT_DIR\"], \"hooks\")\n-\n-    if not isinstance(param, list):\n-        param=[param]\n-\n-    # resolve hook file name, OS depdenent\n-    hook_file = os.path.join(hooks_path, cmd)\n-    if platform.system() == 'Windows':\n-        if not os.path.isfile(hook_file):\n-            # look for the file with an extension\n-            files = glob.glob(hook_file + \".*\")\n-            if not files:\n-                return True\n-            files.sort()\n-            hook_file = files.pop()\n-            while hook_file.upper().endswith(\".SAMPLE\"):\n-                # The file is a sample hook. We don't want it\n-                if len(files) > 0:\n-                    hook_file = files.pop()\n-                else:\n-                    return True\n-\n-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):\n+    if not cmd:\n         return True\n \n-    return run_hook_command(hook_file, param) == 0\n-\n-def run_hook_command(cmd, param):\n-    \"\"\"Executes a git hook command\n-       cmd = the command line file to be executed. This can be\n-       a file that is run by OS association.\n-\n-       param = a list of parameters to pass to the cmd command\n-\n-       On windows, the extension is checked to see if it should\n-       be run with the Git for Windows Bash shell.  If there\n-       is no file extension, the file is deemed a bash shell\n-       and will be handed off to sh.exe. Otherwise, Windows\n-       will be called with the shell to handle the file assocation.\n-\n-       For non Windows operating systems, the file is called\n-       as an executable.\n-    \"\"\"\n-    cli = [cmd] + param\n-    use_shell = False\n-    if platform.system() == 'Windows':\n-        (root,ext) = os.path.splitext(cmd)\n-        if ext == \"\":\n-            exe_path = os.environ.get(\"EXEPATH\")\n-            if exe_path is None:\n-                exe_path = \"\"\n-            else:\n-                exe_path = os.path.join(exe_path, \"bin\")\n-            cli = [os.path.join(exe_path, \"SH.EXE\")] + cli\n-        else:\n-            use_shell = True\n-    return subprocess.call(cli, shell=use_shell)\n+    \"\"\"args are specified with -a <arg> -a <arg> -a <arg>\"\"\"\n+    args = (['git', 'hook', 'run'] +\n+\t    [\"-a\" + arg for arg in param] +\n+\t    [cmd])\n \n+    return subprocess.call(args) == 0\n \n def write_pipe(c, stdin):\n     if verbose:\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418811","messageId":"20210311021037.3001235-33-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 32/37] post-update: use hook.h library","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:32Z","receivedAt":"2021-03-11T02:12:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using run_hooks() instead of run_hook_le(), 'post-update' hooks can\nbe specified in the config as well as the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  3 +++\n builtin/receive-pack.c     | 27 ++++++++-------------------\n 2 files changed, 11 insertions(+), 19 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex c16353be2d..fe5381b95b 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -508,6 +508,9 @@ Both standard output and standard error output are forwarded to\n `git send-pack` on the other end, so you can simply `echo` messages\n for the user.\n \n+Hooks run during 'post-update' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n reference-transaction\n ~~~~~~~~~~~~~~~~~~~~~\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex e448956a32..955efbdf6d 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1688,33 +1688,22 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *hook;\n-\n-\thook = find_hook(\"post-update\");\n-\tif (!hook)\n-\t\treturn;\n+\tstruct run_hooks_opt opt;\n+\trun_hooks_opt_init_async(&opt);\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\n \t\t\tcontinue;\n-\t\tif (!proc.args.nr)\n-\t\t\tstrvec_push(&proc.args, hook);\n-\t\tstrvec_push(&proc.args, cmd->ref_name);\n+\t\tstrvec_push(&opt.args, cmd->ref_name);\n \t}\n-\tif (!proc.args.nr)\n+\tif (!opt.args.nr)\n \t\treturn;\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.trace2_hook_name = \"post-update\";\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n \n-\tif (!start_command(&proc)) {\n-\t\tif (use_sideband)\n-\t\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\t\tfinish_command(&proc);\n-\t}\n+\trun_hooks(\"post-update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n }\n \n static void check_aliased_update_internal(struct command *cmd,\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418812","messageId":"20210311021037.3001235-32-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 31/37] proc-receive: acquire hook list from hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:31Z","receivedAt":"2021-03-11T02:12:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"The proc-receive hook differs from most other hooks Git invokes because\nthe hook and the parent Git process engage in bidirectional\ncommunication via stdin/stdout. This bidirectional communication is\nunsuitable for multiple hooks, whether they are in series or in\nparallel, and is incompatible with run-command.h:run_processes_parallel:\n\n- The proc-receive hook is intended to modify the state of the Git repo.\n  From 'git help githooks':\n    This [proc-receive] hook is responsible for updating the relevant\n    references and reporting the results back to 'receive-pack'.\n  This prevents parallelization and implies, at least, specific ordering\n  of hook execution.\n- The proc-receive hook can reject a push by aborting early with an\n  error code. If a former hook ran through the entire push contents\n  successfully but a later hook rejects some of the push, the repo may\n  be left in a partially-updated (and corrupt) state.\n- The callback model of the run_processes_parallel() API is unsuited to\n  the current implementation of proc-receive, which loops through\n  \"send-receive-consider\" with the child process. proc-receive today\n  relies on stateful communication with the child process, which would be\n  unwieldy to implement with callbacks and saved state.\n- Additionally, run_processes_parallel() is designed to collate the\n  output of many child processes into a single output (stderr or callback),\n  and would require significant work to tell the caller which process sent\n  the output, and indeed to collect any output before the child process\n  has exited.\n\nSo, rather than using hook.h:run_hooks() to invoke the proc-receive\nhook, receive-pack.c can learn to ask hook.h:hook_list() for the\nlocation of a hook to run. This allows users to configure their\nproc-receive in a global config for all repos if they want, or a local\nconfig if they just don't want to use the hookdir. Because running more\nthan one proc-receive hook doesn't make sense from a repo state\nperspective, we can explicitly ban configuring more than one\nproc-receive hook at a time.\n\nIf a user wants to globally configure one proc-receive hook for most of\ntheir repos, but override that hook in a single repo, they should use\n'skip' to manually remove the global hook in their special repo:\n\n~/.gitconfig:\n[hook.proc-receive]\n  command = /usr/bin/usual-proc-receive\n\n~/special-repo/.git/config:\n[hookcmd./usr/bin/usual-proc-receive]\n  skip = true\n[hook.proc-receive]\n  command = /usr/bin/special-proc-receive\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt                |  4 ++\n builtin/receive-pack.c                    | 33 +++++++++++++++-\n t/t5411/test-0015-too-many-hooks-error.sh | 47 +++++++++++++++++++++++\n 3 files changed, 82 insertions(+), 2 deletions(-)\n create mode 100644 t/t5411/test-0015-too-many-hooks-error.sh\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 60fd43d1da..c16353be2d 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -433,6 +433,10 @@ the input.  The exit status of the 'proc-receive' hook only determines\n the success or failure of the group of commands sent to it, unless\n atomic push is in use.\n \n+It is forbidden to specify more than one hook for 'proc-receive'. If a\n+globally-configured 'proc-receive' must be overridden, use\n+'hookcmd.<global-hook>.skip = true' to ignore it.\n+\n [[post-receive]]\n post-receive\n ~~~~~~~~~~~~\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex b34a27a303..e448956a32 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1146,11 +1146,40 @@ static int run_proc_receive_hook(struct command *commands,\n \tint version = 0;\n \tint code;\n \n-\targv[0] = find_hook(\"proc-receive\");\n-\tif (!argv[0]) {\n+\tstruct strbuf hookname = STRBUF_INIT;\n+\tstruct hook *proc_receive = NULL;\n+\tstruct list_head *pos, *hooks;\n+\n+\tstrbuf_addstr(&hookname, \"proc-receive\");\n+\thooks = hook_list(&hookname);\n+\n+\tlist_for_each(pos, hooks) {\n+\t\tif (proc_receive) {\n+\t\t\trp_error(\"only one 'proc-receive' hook can be specified\");\n+\t\t\treturn -1;\n+\t\t}\n+\t\tproc_receive = list_entry(pos, struct hook, list);\n+\t\t/* check if the hookdir hook should be ignored */\n+\t\tif (proc_receive->from_hookdir) {\n+\t\t\tswitch (configured_hookdir_opt()) {\n+\t\t\tcase HOOKDIR_INTERACTIVE:\n+\t\t\tcase HOOKDIR_NO:\n+\t\t\t\tproc_receive = NULL;\n+\t\t\t\tbreak;\n+\t\t\tdefault:\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\n+\t}\n+\n+\tif (!proc_receive) {\n \t\trp_error(\"cannot find hook 'proc-receive'\");\n \t\treturn -1;\n \t}\n+\n+\n+\targv[0] = proc_receive->command.buf;\n \targv[1] = NULL;\n \n \tproc.argv = argv;\ndiff --git a/t/t5411/test-0015-too-many-hooks-error.sh b/t/t5411/test-0015-too-many-hooks-error.sh\nnew file mode 100644\nindex 0000000000..2d64534510\n--- /dev/null\n+++ b/t/t5411/test-0015-too-many-hooks-error.sh\n@@ -0,0 +1,47 @@\n+test_expect_success \"setup too  many proc-receive hooks (ok, $PROTOCOL)\" '\n+\twrite_script \"proc-receive\" <<-EOF &&\n+\tprintf >&2 \"# proc-receive hook\\n\"\n+\ttest-tool proc-receive -v \\\n+\t\t-r \"ok refs/for/main/topic\"\n+\tEOF\n+\n+\tgit -C \"$upstream\" config --add \"hook.proc-receive.command\" proc-receive &&\n+\tcp proc-receive \"$upstream/hooks/proc-receive\"\n+'\n+\n+# Refs of upstream : main(A)\n+# Refs of workbench: main(A)  tags/v123\n+# git push         :                       next(A)  refs/for/main/topic(A)\n+test_expect_success \"proc-receive: reject more than one configured hook\" '\n+\ttest_must_fail git -C workbench push origin \\\n+\t\tHEAD:next \\\n+\t\tHEAD:refs/for/main/topic \\\n+\t\t>out 2>&1 &&\n+\tmake_user_friendly_and_stable_output <out >actual &&\n+\tcat >expect <<-EOF &&\n+\tremote: # pre-receive hook\n+\tremote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next\n+\tremote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic\n+\tremote: error: only one \"proc-receive\" hook can be specified\n+\tremote: # post-receive hook\n+\tremote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next\n+\tTo <URL/of/upstream.git>\n+\t * [new branch] HEAD -> next\n+\t ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)\n+\tEOF\n+\ttest_cmp expect actual &&\n+\tgit -C \"$upstream\" show-ref >out &&\n+\tmake_user_friendly_and_stable_output <out >actual &&\n+\tcat >expect <<-EOF &&\n+\t<COMMIT-A> refs/heads/main\n+\t<COMMIT-A> refs/heads/next\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+# Refs of upstream : main(A)             next(A)\n+# Refs of workbench: main(A)  tags/v123\n+test_expect_success \"cleanup ($PROTOCOL)\" '\n+\tgit -C \"$upstream\" config --unset \"hook.proc-receive.command\" \"proc-receive\" &&\n+\tgit -C \"$upstream\" update-ref -d refs/heads/next\n+'\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418813","messageId":"20210311021037.3001235-35-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 34/37] bugreport: use hook_exists instead of find_hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:34Z","receivedAt":"2021-03-11T02:12:41Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the helper in hook.h instead of the one in run-command.h, we\ncan also check whether a hook exists in the config - not just whether it\nexists in the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/bugreport.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex ad3cc9c02f..eac3726527 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -3,7 +3,7 @@\n #include \"strbuf.h\"\n #include \"help.h\"\n #include \"compat/compiler.h\"\n-#include \"run-command.h\"\n+#include \"hook.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\n@@ -82,7 +82,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t}\n \n \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (find_hook(hook[i]))\n+\t\tif (hook_exists(hook[i], HOOKDIR_USE_CONFIG))\n \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n }\n \n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418814","messageId":"20210311021037.3001235-34-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 33/37] receive-pack: convert receive hooks to hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:33Z","receivedAt":"2021-03-11T02:12:41Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the hook.h library to run receive hooks, they can be specified\nin the config as well as in the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |   5 +\n builtin/receive-pack.c     | 199 +++++++++++++++++--------------------\n 2 files changed, 97 insertions(+), 107 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex fe5381b95b..b63054b947 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -323,6 +323,8 @@ will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n See the section on \"Quarantine Environment\" in\n linkgit:git-receive-pack[1] for some caveats.\n \n+Hooks executed during 'pre-receive' will not be parallelized.\n+\n [[update]]\n update\n ~~~~~~\n@@ -476,6 +478,9 @@ environment variables will not be set. If the client selects\n to use push options, but doesn't transmit any, the count variable\n will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n \n+Hooks executed during 'post-receive' are run in parallel, unless hook.jobs is\n+configured to 1.\n+\n [[post-update]]\n post-update\n ~~~~~~~~~~~\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 955efbdf6d..d124718d0b 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -748,7 +748,7 @@ static int check_cert_push_options(const struct string_list *push_options)\n \treturn retval;\n }\n \n-static void prepare_push_cert_sha1(struct child_process *proc)\n+static void prepare_push_cert_sha1(struct run_hooks_opt *opt)\n {\n \tstatic int already_done;\n \n@@ -772,110 +772,42 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n \t}\n \tif (!is_null_oid(&push_cert_oid)) {\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT=%s\",\n \t\t\t     oid_to_hex(&push_cert_oid));\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_SIGNER=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_SIGNER=%s\",\n \t\t\t     sigcheck.signer ? sigcheck.signer : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_KEY=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_KEY=%s\",\n \t\t\t     sigcheck.key ? sigcheck.key : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_STATUS=%c\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_STATUS=%c\",\n \t\t\t     sigcheck.result);\n \t\tif (push_cert_nonce) {\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE=%s\",\n \t\t\t\t     push_cert_nonce);\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE_STATUS=%s\",\n \t\t\t\t     nonce_status);\n \t\t\tif (nonce_status == NONCE_SLOP)\n-\t\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t\t     \"GIT_PUSH_CERT_NONCE_SLOP=%ld\",\n \t\t\t\t\t     nonce_stamp_slop);\n \t\t}\n \t}\n }\n \n+struct receive_hook_feed_context {\n+\tstruct command *cmd;\n+\tint skip_broken;\n+};\n+\n struct receive_hook_feed_state {\n \tstruct command *cmd;\n \tstruct ref_push_report *report;\n \tint skip_broken;\n \tstruct strbuf buf;\n-\tconst struct string_list *push_options;\n };\n \n-typedef int (*feed_fn)(void *, const char **, size_t *);\n-static int run_and_feed_hook(const char *hook_name, feed_fn feed,\n-\t\t\t     struct receive_hook_feed_state *feed_state)\n-{\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct async muxer;\n-\tconst char *argv[2];\n-\tint code;\n-\n-\targv[0] = find_hook(hook_name);\n-\tif (!argv[0])\n-\t\treturn 0;\n-\n-\targv[1] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = hook_name;\n-\n-\tif (feed_state->push_options) {\n-\t\tint i;\n-\t\tfor (i = 0; i < feed_state->push_options->nr; i++)\n-\t\t\tstrvec_pushf(&proc.env_array,\n-\t\t\t\t     \"GIT_PUSH_OPTION_%d=%s\", i,\n-\t\t\t\t     feed_state->push_options->items[i].string);\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT=%d\",\n-\t\t\t     feed_state->push_options->nr);\n-\t} else\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT\");\n-\n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&proc.env_array, tmp_objdir_env(tmp_objdir));\n-\n-\tif (use_sideband) {\n-\t\tmemset(&muxer, 0, sizeof(muxer));\n-\t\tmuxer.proc = copy_to_sideband;\n-\t\tmuxer.in = -1;\n-\t\tcode = start_async(&muxer);\n-\t\tif (code)\n-\t\t\treturn code;\n-\t\tproc.err = muxer.in;\n-\t}\n-\n-\tprepare_push_cert_sha1(&proc);\n-\n-\tcode = start_command(&proc);\n-\tif (code) {\n-\t\tif (use_sideband)\n-\t\t\tfinish_async(&muxer);\n-\t\treturn code;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\n-\twhile (1) {\n-\t\tconst char *buf;\n-\t\tsize_t n;\n-\t\tif (feed(feed_state, &buf, &n))\n-\t\t\tbreak;\n-\t\tif (write_in_full(proc.in, buf, n) < 0)\n-\t\t\tbreak;\n-\t}\n-\tclose(proc.in);\n-\tif (use_sideband)\n-\t\tfinish_async(&muxer);\n-\n-\tsigchain_pop(SIGPIPE);\n-\n-\treturn finish_command(&proc);\n-}\n-\n-static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n+static int feed_receive_hook(void *state_)\n {\n \tstruct receive_hook_feed_state *state = state_;\n \tstruct command *cmd = state->cmd;\n@@ -884,9 +816,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t       state->skip_broken && (cmd->error_string || cmd->did_not_exist))\n \t\tcmd = cmd->next;\n \tif (!cmd)\n-\t\treturn -1; /* EOF */\n-\tif (!bufp)\n-\t\treturn 0; /* OK, can feed something. */\n+\t\treturn 1; /* EOF - close the pipe*/\n \tstrbuf_reset(&state->buf);\n \tif (!state->report)\n \t\tstate->report = cmd->report;\n@@ -910,32 +840,36 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t\t\t    cmd->ref_name);\n \t\tstate->cmd = cmd->next;\n \t}\n-\tif (bufp) {\n-\t\t*bufp = state->buf.buf;\n-\t\t*sizep = state->buf.len;\n-\t}\n \treturn 0;\n }\n \n-static int run_receive_hook(struct command *commands,\n-\t\t\t    const char *hook_name,\n-\t\t\t    int skip_broken,\n-\t\t\t    const struct string_list *push_options)\n+static int feed_receive_hook_cb(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n {\n-\tstruct receive_hook_feed_state state;\n-\tint status;\n-\n-\tstrbuf_init(&state.buf, 0);\n-\tstate.cmd = commands;\n-\tstate.skip_broken = skip_broken;\n-\tstate.report = NULL;\n-\tif (feed_receive_hook(&state, NULL, NULL))\n-\t\treturn 0;\n-\tstate.cmd = commands;\n-\tstate.push_options = push_options;\n-\tstatus = run_and_feed_hook(hook_name, feed_receive_hook, &state);\n-\tstrbuf_release(&state.buf);\n-\treturn status;\n+\tstruct hook *hook = pp_task_cb;\n+\tstruct receive_hook_feed_state *feed_state = hook->feed_pipe_cb_data;\n+\tint rc;\n+\n+\t/* first-time setup */\n+\tif (!feed_state) {\n+\t\tstruct hook_cb_data *hook_cb = pp_cb;\n+\t\tstruct run_hooks_opt *opt = hook_cb->options;\n+\t\tstruct receive_hook_feed_context *ctx = opt->feed_pipe_ctx;\n+\t\tif (!ctx)\n+\t\t\tBUG(\"run_hooks_opt.feed_pipe_ctx required for receive hook\");\n+\n+\t\tfeed_state = xmalloc(sizeof(struct receive_hook_feed_state));\n+\t\tstrbuf_init(&feed_state->buf, 0);\n+\t\tfeed_state->cmd = ctx->cmd;\n+\t\tfeed_state->skip_broken = ctx->skip_broken;\n+\t\tfeed_state->report = NULL;\n+\n+\t\thook->feed_pipe_cb_data = feed_state;\n+\t}\n+\n+\trc = feed_receive_hook(feed_state);\n+\tif (!rc)\n+\t\tstrbuf_addbuf(pipe, &feed_state->buf);\n+\treturn rc;\n }\n \n static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n@@ -971,6 +905,57 @@ static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n \tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n }\n \n+static int run_receive_hook(struct command *commands,\n+\t\t\t    const char *hook_name,\n+\t\t\t    int skip_broken,\n+\t\t\t    const struct string_list *push_options)\n+{\n+\tstruct run_hooks_opt opt;\n+\tstruct receive_hook_feed_context ctx;\n+\tint rc;\n+\tstruct command *iter = commands;\n+\n+\trun_hooks_opt_init_async(&opt);\n+\n+\t/* if there are no valid commands, don't invoke the hook at all. */\n+\twhile (iter && skip_broken && (iter->error_string || iter->did_not_exist))\n+\t\titer = iter->next;\n+\tif (!iter)\n+\t\treturn 0;\n+\n+\t/* pre-receive hooks should run in series as the hook updates refs */\n+\tif (!strcmp(hook_name, \"pre-receive\"))\n+\t\topt.jobs = 1;\n+\n+\tif (push_options) {\n+\t\tint i;\n+\t\tfor (i = 0; i < push_options->nr; i++)\n+\t\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_%d=%s\", i,\n+\t\t\t\t     push_options->items[i].string);\n+\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_COUNT=%d\", push_options->nr);\n+\t} else\n+\t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n+\n+\tif (tmp_objdir)\n+\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\n+\tprepare_push_cert_sha1(&opt);\n+\n+\t/* set up sideband printer */\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\t/* set up stdin callback */\n+\tctx.cmd = commands;\n+\tctx.skip_broken = skip_broken;\n+\topt.feed_pipe = feed_receive_hook_cb;\n+\topt.feed_pipe_ctx = &ctx;\n+\n+\trc = run_hooks(hook_name, &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n+}\n+\n static int run_update_hook(struct command *cmd)\n {\n \tstruct run_hooks_opt opt;\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418815","messageId":"20210311021037.3001235-36-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:35Z","receivedAt":"2021-03-11T02:12:41Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the new 'git hook run' subcommand to run 'sendemail-validate',\nwe can reduce the boilerplate needed to run this hook in perl. Using\nconfig-based hooks also allows us to run 'sendemail-validate' hooks that\nwere configured globally when running 'git send-email' from outside of a\nGit directory, alongside other benefits like multihooks and\nparallelization.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n git-send-email.perl   | 21 ++++-----------------\n t/t9001-send-email.sh | 11 +----------\n 2 files changed, 5 insertions(+), 27 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 1f425c0809..73e1e0b51a 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -1941,23 +1941,10 @@ sub unique_email_list {\n sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n-\tif ($repo) {\n-\t\tmy $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),\n-\t\t\t\t\t    'sendemail-validate');\n-\t\tmy $hook_error;\n-\t\tif (-x $validate_hook) {\n-\t\t\tmy $target = abs_path($fn);\n-\t\t\t# The hook needs a correct cwd and GIT_DIR.\n-\t\t\tmy $cwd_save = cwd();\n-\t\t\tchdir($repo->wc_path() or $repo->repo_path())\n-\t\t\t\tor die(\"chdir: $!\");\n-\t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n-\t\t\t\tif system($validate_hook, $target);\n-\t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n-\t\t}\n-\t\treturn $hook_error if $hook_error;\n-\t}\n+\tmy $target = abs_path($fn);\n+\treturn \"rejected by sendemail-validate hook\"\n+\t\tif system((\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-a\",\n+\t\t\t\t$target));\n \n \t# Any long lines will be automatically fixed if we use a suitable transfer\n \t# encoding.\ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 4eee9c3dcb..456b471c5c 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -2101,16 +2101,7 @@ test_expect_success $PREREQ 'invoke hook' '\n \tmkdir -p .git/hooks &&\n \n \twrite_script .git/hooks/sendemail-validate <<-\\EOF &&\n-\t# test that we have the correct environment variable, pwd, and\n-\t# argument\n-\tcase \"$GIT_DIR\" in\n-\t*.git)\n-\t\ttrue\n-\t\t;;\n-\t*)\n-\t\tfalse\n-\t\t;;\n-\tesac &&\n+\t# test that we have the correct argument\n \ttest -f 0001-add-main.patch &&\n \tgrep \"add main\" \"$1\"\n \tEOF\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418816","messageId":"20210311021037.3001235-37-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 36/37] run-command: stop thinking about hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:36Z","receivedAt":"2021-03-11T02:12:41Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"hook.h has replaced all run-command.h hook-related functionality.\nrun-command.h:run_hooks_le/ve and find_hook are no longer used anywhere\nin the codebase. So, let's delete the dead code - or, in the one case\nwhere it's still needed, move it to an internal function in hook.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n hook.c        | 39 ++++++++++++++++++++++++++++--\n run-command.c | 66 ---------------------------------------------------\n run-command.h | 24 -------------------\n 3 files changed, 37 insertions(+), 92 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 2322720ffe..7f6f3b9a61 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -212,6 +212,41 @@ static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n \t}\n }\n \n+static const char *find_legacy_hook(const char *name)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\n+\tstrbuf_reset(&path);\n+\tstrbuf_git_path(&path, \"hooks/%s\", name);\n+\tif (access(path.buf, X_OK) < 0) {\n+\t\tint err = errno;\n+\n+#ifdef STRIP_EXTENSION\n+\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n+\t\tif (access(path.buf, X_OK) >= 0)\n+\t\t\treturn path.buf;\n+\t\tif (errno == EACCES)\n+\t\t\terr = errno;\n+#endif\n+\n+\t\tif (err == EACCES && advice_ignored_hook) {\n+\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n+\n+\t\t\tif (!string_list_lookup(&advise_given, name)) {\n+\t\t\t\tstring_list_insert(&advise_given, name);\n+\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n+\t\t\t\t\t \"it's not set as executable.\\n\"\n+\t\t\t\t\t \"You can disable this warning with \"\n+\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n+\t\t\t\t       path.buf);\n+\t\t\t}\n+\t\t}\n+\t\treturn NULL;\n+\t}\n+\treturn path.buf;\n+}\n+\n+\n struct list_head* hook_list(const struct strbuf* hookname)\n {\n \tstruct strbuf hook_key = STRBUF_INIT;\n@@ -228,7 +263,7 @@ struct list_head* hook_list(const struct strbuf* hookname)\n \tgit_config(hook_config_lookup, &cb_data);\n \n \tif (have_git_dir()) {\n-\t\tconst char *legacy_hook_path = find_hook(hookname->buf);\n+\t\tconst char *legacy_hook_path = find_legacy_hook(hookname->buf);\n \n \t\t/* Unconditionally add legacy hook, but annotate it. */\n \t\tif (legacy_hook_path) {\n@@ -277,7 +312,7 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n \tcould_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||\n \t\t\t\tshould_run_hookdir == HOOKDIR_WARN ||\n \t\t\t\tshould_run_hookdir == HOOKDIR_YES)\n-\t\t\t\t&& !!find_hook(hookname);\n+\t\t\t\t&& !!find_legacy_hook(hookname);\n \n \tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n \ndiff --git a/run-command.c b/run-command.c\nindex 36a4edbacf..837415131d 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1320,72 +1320,6 @@ int async_with_fork(void)\n #endif\n }\n \n-const char *find_hook(const char *name)\n-{\n-\tstatic struct strbuf path = STRBUF_INIT;\n-\n-\tstrbuf_reset(&path);\n-\tstrbuf_git_path(&path, \"hooks/%s\", name);\n-\tif (access(path.buf, X_OK) < 0) {\n-\t\tint err = errno;\n-\n-#ifdef STRIP_EXTENSION\n-\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n-\t\tif (access(path.buf, X_OK) >= 0)\n-\t\t\treturn path.buf;\n-\t\tif (errno == EACCES)\n-\t\t\terr = errno;\n-#endif\n-\n-\t\tif (err == EACCES && advice_ignored_hook) {\n-\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n-\n-\t\t\tif (!string_list_lookup(&advise_given, name)) {\n-\t\t\t\tstring_list_insert(&advise_given, name);\n-\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n-\t\t\t\t\t \"it's not set as executable.\\n\"\n-\t\t\t\t\t \"You can disable this warning with \"\n-\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n-\t\t\t\t       path.buf);\n-\t\t\t}\n-\t\t}\n-\t\treturn NULL;\n-\t}\n-\treturn path.buf;\n-}\n-\n-int run_hook_ve(const char *const *env, const char *name, va_list args)\n-{\n-\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\tconst char *p;\n-\n-\tp = find_hook(name);\n-\tif (!p)\n-\t\treturn 0;\n-\n-\tstrvec_push(&hook.args, p);\n-\twhile ((p = va_arg(args, const char *)))\n-\t\tstrvec_push(&hook.args, p);\n-\thook.env = env;\n-\thook.no_stdin = 1;\n-\thook.stdout_to_stderr = 1;\n-\thook.trace2_hook_name = name;\n-\n-\treturn run_command(&hook);\n-}\n-\n-int run_hook_le(const char *const *env, const char *name, ...)\n-{\n-\tva_list args;\n-\tint ret;\n-\n-\tva_start(args, name);\n-\tret = run_hook_ve(env, name, args);\n-\tva_end(args);\n-\n-\treturn ret;\n-}\n-\n struct io_pump {\n \t/* initialized by caller */\n \tint fd;\ndiff --git a/run-command.h b/run-command.h\nindex ebc4a95a94..7150da851a 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -201,30 +201,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/*\n- * Returns the path to the hook file, or NULL if the hook is missing\n- * or disabled. Note that this points to static storage that will be\n- * overwritten by further calls to find_hook and run_hook_*.\n- */\n-const char *find_hook(const char *name);\n-\n-/**\n- * Run a hook.\n- * The first argument is a pathname to an index file, or NULL\n- * if the hook uses the default index file or no index is needed.\n- * The second argument is the name of the hook.\n- * The further arguments correspond to the hook arguments.\n- * The last argument has to be NULL to terminate the arguments list.\n- * If the hook does not exist or is not executable, the return\n- * value will be zero.\n- * If it is executable, the hook will be executed and the exit\n- * status of the hook is returned.\n- * On execution, .stdout_to_stderr and .no_stdin will be set.\n- */\n-LAST_ARG_MUST_BE_NULL\n-int run_hook_le(const char *const *env, const char *name, ...);\n-int run_hook_ve(const char *const *env, const char *name, va_list args);\n-\n /*\n  * Trigger an auto-gc\n  */\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418817","messageId":"20210311021037.3001235-38-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v8 37/37] docs: unify githooks and git-hook manpages","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-11T02:10:37Z","receivedAt":"2021-03-11T02:12:41Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By showing the list of all hooks in 'git help hook' for users to refer\nto, 'git help hook' becomes a one-stop shop for hook authorship. Since\nsome may still have muscle memory for 'git help githooks', though,\nreference the 'git hook' commands and otherwise don't remove content.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt     |  11 +\n Documentation/githooks.txt     | 716 +--------------------------------\n Documentation/native-hooks.txt | 708 ++++++++++++++++++++++++++++++++\n 3 files changed, 724 insertions(+), 711 deletions(-)\n create mode 100644 Documentation/native-hooks.txt\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 81b8e94994..4ad31ac360 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -17,6 +17,13 @@ DESCRIPTION\n You can list and run configured hooks with this command. Later, you will be able\n to add and modify hooks with this command.\n \n+In general, when instructions suggest adding a script to\n+`.git/hooks/<something>`, you can specify it in the config instead by running\n+`git config --add hook.<something>.command <path-to-script>` - this way you can\n+share the script between multiple repos. That is, `cp ~/my-script.sh\n+~/project/.git/hooks/pre-commit` would become `git config --add\n+hook.pre-commit.command ~/my-script.sh`.\n+\n This command parses the default configuration files for sections `hook` and\n `hookcmd`. `hook` is used to describe the commands which will be run during a\n particular hook event; commands are run in the order Git encounters them during\n@@ -145,6 +152,10 @@ CONFIGURATION\n -------------\n include::config/hook.txt[]\n \n+HOOKS\n+-----\n+include::native-hooks.txt[]\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex b63054b947..9a25dfdc3f 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -7,15 +7,16 @@ githooks - Hooks used by Git\n \n SYNOPSIS\n --------\n+'git hook'\n $GIT_DIR/hooks/* (or \\`git config core.hooksPath`/*)\n \n \n DESCRIPTION\n -----------\n \n-Hooks are programs you can place in a hooks directory to trigger\n-actions at certain points in git's execution. Hooks that don't have\n-the executable bit set are ignored.\n+Hooks are programs you can specify in your config (see linkgit:git-hook[1]) or\n+place in a hooks directory to trigger actions at certain points in git's\n+execution. Hooks that don't have the executable bit set are ignored.\n \n By default the hooks directory is `$GIT_DIR/hooks`, but that can be\n changed via the `core.hooksPath` configuration variable (see\n@@ -41,714 +42,7 @@ The currently supported hooks are described below.\n \n HOOKS\n -----\n-\n-applypatch-msg\n-~~~~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-am[1].  It takes a single\n-parameter, the name of the file that holds the proposed commit\n-log message.  Exiting with a non-zero status causes `git am` to abort\n-before applying the patch.\n-\n-The hook is allowed to edit the message file in place, and can\n-be used to normalize the message into some project standard\n-format. It can also be used to refuse the commit after inspecting\n-the message file.\n-\n-The default 'applypatch-msg' hook, when enabled, runs the\n-'commit-msg' hook, if the latter is enabled.\n-\n-Hooks run during 'applypatch-msg' will not be parallelized, because hooks are\n-expected to edit the file holding the commit log message.\n-\n-pre-applypatch\n-~~~~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-am[1].  It takes no parameter, and is\n-invoked after the patch is applied, but before a commit is made.\n-\n-If it exits with non-zero status, then the working tree will not be\n-committed after applying the patch.\n-\n-It can be used to inspect the current working tree and refuse to\n-make a commit if it does not pass certain test.\n-\n-The default 'pre-applypatch' hook, when enabled, runs the\n-'pre-commit' hook, if the latter is enabled.\n-\n-Hooks run during 'pre-applypatch' will be run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-post-applypatch\n-~~~~~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-am[1].  It takes no parameter,\n-and is invoked after the patch is applied and a commit is made.\n-\n-This hook is meant primarily for notification, and cannot affect\n-the outcome of `git am`.\n-\n-Hooks run during 'post-applypatch' will be run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-pre-commit\n-~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-commit[1], and can be bypassed\n-with the `--no-verify` option.  It takes no parameters, and is\n-invoked before obtaining the proposed commit log message and\n-making a commit.  Exiting with a non-zero status from this script\n-causes the `git commit` command to abort before creating a commit.\n-\n-The default 'pre-commit' hook, when enabled, catches introduction\n-of lines with trailing whitespaces and aborts the commit when\n-such a line is found.\n-\n-All the `git commit` hooks are invoked with the environment\n-variable `GIT_EDITOR=:` if the command will not bring up an editor\n-to modify the commit message.\n-\n-The default 'pre-commit' hook, when enabled--and with the\n-`hooks.allownonascii` config option unset or set to false--prevents\n-the use of non-ASCII filenames.\n-\n-Hooks executed during 'pre-commit' will not be parallelized.\n-\n-pre-merge-commit\n-~~~~~~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-merge[1], and can be bypassed\n-with the `--no-verify` option.  It takes no parameters, and is\n-invoked after the merge has been carried out successfully and before\n-obtaining the proposed commit log message to\n-make a commit.  Exiting with a non-zero status from this script\n-causes the `git merge` command to abort before creating a commit.\n-\n-The default 'pre-merge-commit' hook, when enabled, runs the\n-'pre-commit' hook, if the latter is enabled.\n-\n-This hook is invoked with the environment variable\n-`GIT_EDITOR=:` if the command will not bring up an editor\n-to modify the commit message.\n-\n-If the merge cannot be carried out automatically, the conflicts\n-need to be resolved and the result committed separately (see\n-linkgit:git-merge[1]). At that point, this hook will not be executed,\n-but the 'pre-commit' hook will, if it is enabled.\n-\n-Hooks executed during 'pre-merge-commit' will not be parallelized.\n-\n-prepare-commit-msg\n-~~~~~~~~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-commit[1] right after preparing the\n-default log message, and before the editor is started.\n-\n-It takes one to three parameters.  The first is the name of the file\n-that contains the commit log message.  The second is the source of the commit\n-message, and can be: `message` (if a `-m` or `-F` option was\n-given); `template` (if a `-t` option was given or the\n-configuration option `commit.template` is set); `merge` (if the\n-commit is a merge or a `.git/MERGE_MSG` file exists); `squash`\n-(if a `.git/SQUASH_MSG` file exists); or `commit`, followed by\n-a commit SHA-1 (if a `-c`, `-C` or `--amend` option was given).\n-\n-If the exit status is non-zero, `git commit` will abort.\n-\n-The purpose of the hook is to edit the message file in place, and\n-it is not suppressed by the `--no-verify` option.  A non-zero exit\n-means a failure of the hook and aborts the commit.  It should not\n-be used as replacement for pre-commit hook.\n-\n-The sample `prepare-commit-msg` hook that comes with Git removes the\n-help message found in the commented portion of the commit template.\n-\n-Hooks executed during 'prepare-commit-msg' will not be parallelized, because\n-hooks are expected to edit the file containing the commit log message.\n-\n-commit-msg\n-~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-commit[1] and linkgit:git-merge[1], and can be\n-bypassed with the `--no-verify` option.  It takes a single parameter,\n-the name of the file that holds the proposed commit log message.\n-Exiting with a non-zero status causes the command to abort.\n-\n-The hook is allowed to edit the message file in place, and can be used\n-to normalize the message into some project standard format. It\n-can also be used to refuse the commit after inspecting the message\n-file.\n-\n-The default 'commit-msg' hook, when enabled, detects duplicate\n-`Signed-off-by` trailers, and aborts the commit if one is found.\n-\n-Hooks executed during 'commit-msg' will not be parallelized, because hooks are\n-expected to edit the file containing the proposed commit log message.\n-\n-post-commit\n-~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-commit[1]. It takes no parameters, and is\n-invoked after a commit is made.\n-\n-This hook is meant primarily for notification, and cannot affect\n-the outcome of `git commit`.\n-\n-Hooks executed during 'post-commit' will run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-pre-rebase\n-~~~~~~~~~~\n-\n-This hook is called by linkgit:git-rebase[1] and can be used to prevent a\n-branch from getting rebased.  The hook may be called with one or\n-two parameters.  The first parameter is the upstream from which\n-the series was forked.  The second parameter is the branch being\n-rebased, and is not set when rebasing the current branch.\n-\n-Hooks executed during 'pre-rebase' will run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-post-checkout\n-~~~~~~~~~~~~~\n-\n-This hook is invoked when a linkgit:git-checkout[1] or\n-linkgit:git-switch[1] is run after having updated the\n-worktree.  The hook is given three parameters: the ref of the previous HEAD,\n-the ref of the new HEAD (which may or may not have changed), and a flag\n-indicating whether the checkout was a branch checkout (changing branches,\n-flag=1) or a file checkout (retrieving a file from the index, flag=0).\n-This hook cannot affect the outcome of `git switch` or `git checkout`,\n-other than that the hook's exit status becomes the exit status of\n-these two commands.\n-\n-It is also run after linkgit:git-clone[1], unless the `--no-checkout` (`-n`) option is\n-used. The first parameter given to the hook is the null-ref, the second the\n-ref of the new HEAD and the flag is always 1. Likewise for `git worktree add`\n-unless `--no-checkout` is used.\n-\n-This hook can be used to perform repository validity checks, auto-display\n-differences from the previous HEAD if different, or set working dir metadata\n-properties.\n-\n-Hooks executed during 'post-checkout' will not be parallelized.\n-\n-post-merge\n-~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-merge[1], which happens when a `git pull`\n-is done on a local repository.  The hook takes a single parameter, a status\n-flag specifying whether or not the merge being done was a squash merge.\n-This hook cannot affect the outcome of `git merge` and is not executed,\n-if the merge failed due to conflicts.\n-\n-This hook can be used in conjunction with a corresponding pre-commit hook to\n-save and restore any form of metadata associated with the working tree\n-(e.g.: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl\n-for an example of how to do this.\n-\n-Hooks executed during 'post-merge' will run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-pre-push\n-~~~~~~~~\n-\n-This hook is called by linkgit:git-push[1] and can be used to prevent\n-a push from taking place.  The hook is called with two parameters\n-which provide the name and location of the destination remote, if a\n-named remote is not being used both values will be the same.\n-\n-Information about what is to be pushed is provided on the hook's standard\n-input with lines of the form:\n-\n-  <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF\n-\n-For instance, if the command +git push origin master:foreign+ were run the\n-hook would receive a line like the following:\n-\n-  refs/heads/master 67890 refs/heads/foreign 12345\n-\n-although the full, 40-character SHA-1s would be supplied.  If the foreign ref\n-does not yet exist the `<remote SHA-1>` will be 40 `0`.  If a ref is to be\n-deleted, the `<local ref>` will be supplied as `(delete)` and the `<local\n-SHA-1>` will be 40 `0`.  If the local commit was specified by something other\n-than a name which could be expanded (such as `HEAD~`, or a SHA-1) it will be\n-supplied as it was originally given.\n-\n-If this hook exits with a non-zero status, `git push` will abort without\n-pushing anything.  Information about why the push is rejected may be sent\n-to the user by writing to standard error.\n-\n-Hooks executed during 'pre-push' will run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-[[pre-receive]]\n-pre-receive\n-~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n-`git push` and updates reference(s) in its repository.\n-Just before starting to update refs on the remote repository, the\n-pre-receive hook is invoked.  Its exit status determines the success\n-or failure of the update.\n-\n-This hook executes once for the receive operation. It takes no\n-arguments, but for each ref to be updated it receives on standard\n-input a line of the format:\n-\n-  <old-value> SP <new-value> SP <ref-name> LF\n-\n-where `<old-value>` is the old object name stored in the ref,\n-`<new-value>` is the new object name to be stored in the ref and\n-`<ref-name>` is the full name of the ref.\n-When creating a new ref, `<old-value>` is 40 `0`.\n-\n-If the hook exits with non-zero status, none of the refs will be\n-updated. If the hook exits with zero, updating of individual refs can\n-still be prevented by the <<update,'update'>> hook.\n-\n-Both standard output and standard error output are forwarded to\n-`git send-pack` on the other end, so you can simply `echo` messages\n-for the user.\n-\n-The number of push options given on the command line of\n-`git push --push-option=...` can be read from the environment\n-variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are\n-found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...\n-If it is negotiated to not use the push options phase, the\n-environment variables will not be set. If the client selects\n-to use push options, but doesn't transmit any, the count variable\n-will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n-\n-See the section on \"Quarantine Environment\" in\n-linkgit:git-receive-pack[1] for some caveats.\n-\n-Hooks executed during 'pre-receive' will not be parallelized.\n-\n-[[update]]\n-update\n-~~~~~~\n-\n-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n-`git push` and updates reference(s) in its repository.\n-Just before updating the ref on the remote repository, the update hook\n-is invoked.  Its exit status determines the success or failure of\n-the ref update.\n-\n-The hook executes once for each ref to be updated, and takes\n-three parameters:\n-\n- - the name of the ref being updated,\n- - the old object name stored in the ref,\n- - and the new object name to be stored in the ref.\n-\n-A zero exit from the update hook allows the ref to be updated.\n-Exiting with a non-zero status prevents `git receive-pack`\n-from updating that ref.\n-\n-This hook can be used to prevent 'forced' update on certain refs by\n-making sure that the object name is a commit object that is a\n-descendant of the commit object named by the old object name.\n-That is, to enforce a \"fast-forward only\" policy.\n-\n-It could also be used to log the old..new status.  However, it\n-does not know the entire set of branches, so it would end up\n-firing one e-mail per ref when used naively, though.  The\n-<<post-receive,'post-receive'>> hook is more suited to that.\n-\n-In an environment that restricts the users' access only to git\n-commands over the wire, this hook can be used to implement access\n-control without relying on filesystem ownership and group\n-membership. See linkgit:git-shell[1] for how you might use the login\n-shell to restrict the user's access to only git commands.\n-\n-Both standard output and standard error output are forwarded to\n-`git send-pack` on the other end, so you can simply `echo` messages\n-for the user.\n-\n-The default 'update' hook, when enabled--and with\n-`hooks.allowunannotated` config option unset or set to false--prevents\n-unannotated tags to be pushed.\n-\n-Hooks executed during 'update' are run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-[[proc-receive]]\n-proc-receive\n-~~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-receive-pack[1].  If the server has\n-set the multi-valued config variable `receive.procReceiveRefs`, and the\n-commands sent to 'receive-pack' have matching reference names, these\n-commands will be executed by this hook, instead of by the internal\n-`execute_commands()` function.  This hook is responsible for updating\n-the relevant references and reporting the results back to 'receive-pack'.\n-\n-This hook executes once for the receive operation.  It takes no\n-arguments, but uses a pkt-line format protocol to communicate with\n-'receive-pack' to read commands, push-options and send results.  In the\n-following example for the protocol, the letter 'S' stands for\n-'receive-pack' and the letter 'H' stands for this hook.\n-\n-    # Version and features negotiation.\n-    S: PKT-LINE(version=1\\0push-options atomic...)\n-    S: flush-pkt\n-    H: PKT-LINE(version=1\\0push-options...)\n-    H: flush-pkt\n-\n-    # Send commands from server to the hook.\n-    S: PKT-LINE(<old-oid> <new-oid> <ref>)\n-    S: ... ...\n-    S: flush-pkt\n-    # Send push-options only if the 'push-options' feature is enabled.\n-    S: PKT-LINE(push-option)\n-    S: ... ...\n-    S: flush-pkt\n-\n-    # Receive result from the hook.\n-    # OK, run this command successfully.\n-    H: PKT-LINE(ok <ref>)\n-    # NO, I reject it.\n-    H: PKT-LINE(ng <ref> <reason>)\n-    # Fall through, let 'receive-pack' to execute it.\n-    H: PKT-LINE(ok <ref>)\n-    H: PKT-LINE(option fall-through)\n-    # OK, but has an alternate reference.  The alternate reference name\n-    # and other status can be given in option directives.\n-    H: PKT-LINE(ok <ref>)\n-    H: PKT-LINE(option refname <refname>)\n-    H: PKT-LINE(option old-oid <old-oid>)\n-    H: PKT-LINE(option new-oid <new-oid>)\n-    H: PKT-LINE(option forced-update)\n-    H: ... ...\n-    H: flush-pkt\n-\n-Each command for the 'proc-receive' hook may point to a pseudo-reference\n-and always has a zero-old as its old-oid, while the 'proc-receive' hook\n-may update an alternate reference and the alternate reference may exist\n-already with a non-zero old-oid.  For this case, this hook will use\n-\"option\" directives to report extended attributes for the reference given\n-by the leading \"ok\" directive.\n-\n-The report of the commands of this hook should have the same order as\n-the input.  The exit status of the 'proc-receive' hook only determines\n-the success or failure of the group of commands sent to it, unless\n-atomic push is in use.\n-\n-It is forbidden to specify more than one hook for 'proc-receive'. If a\n-globally-configured 'proc-receive' must be overridden, use\n-'hookcmd.<global-hook>.skip = true' to ignore it.\n-\n-[[post-receive]]\n-post-receive\n-~~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n-`git push` and updates reference(s) in its repository.\n-It executes on the remote repository once after all the refs have\n-been updated.\n-\n-This hook executes once for the receive operation.  It takes no\n-arguments, but gets the same information as the\n-<<pre-receive,'pre-receive'>>\n-hook does on its standard input.\n-\n-This hook does not affect the outcome of `git receive-pack`, as it\n-is called after the real work is done.\n-\n-This supersedes the <<post-update,'post-update'>> hook in that it gets\n-both old and new values of all the refs in addition to their\n-names.\n-\n-Both standard output and standard error output are forwarded to\n-`git send-pack` on the other end, so you can simply `echo` messages\n-for the user.\n-\n-The default 'post-receive' hook is empty, but there is\n-a sample script `post-receive-email` provided in the `contrib/hooks`\n-directory in Git distribution, which implements sending commit\n-emails.\n-\n-The number of push options given on the command line of\n-`git push --push-option=...` can be read from the environment\n-variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are\n-found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...\n-If it is negotiated to not use the push options phase, the\n-environment variables will not be set. If the client selects\n-to use push options, but doesn't transmit any, the count variable\n-will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n-\n-Hooks executed during 'post-receive' are run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-[[post-update]]\n-post-update\n-~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n-`git push` and updates reference(s) in its repository.\n-It executes on the remote repository once after all the refs have\n-been updated.\n-\n-It takes a variable number of parameters, each of which is the\n-name of ref that was actually updated.\n-\n-This hook is meant primarily for notification, and cannot affect\n-the outcome of `git receive-pack`.\n-\n-The 'post-update' hook can tell what are the heads that were pushed,\n-but it does not know what their original and updated values are,\n-so it is a poor place to do log old..new. The\n-<<post-receive,'post-receive'>> hook does get both original and\n-updated values of the refs. You might consider it instead if you need\n-them.\n-\n-When enabled, the default 'post-update' hook runs\n-`git update-server-info` to keep the information used by dumb\n-transports (e.g., HTTP) up to date.  If you are publishing\n-a Git repository that is accessible via HTTP, you should\n-probably enable this hook.\n-\n-Both standard output and standard error output are forwarded to\n-`git send-pack` on the other end, so you can simply `echo` messages\n-for the user.\n-\n-Hooks run during 'post-update' will be run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-reference-transaction\n-~~~~~~~~~~~~~~~~~~~~~\n-\n-This hook is invoked by any Git command that performs reference\n-updates. It executes whenever a reference transaction is prepared,\n-committed or aborted and may thus get called multiple times.\n-\n-The hook takes exactly one argument, which is the current state the\n-given reference transaction is in:\n-\n-    - \"prepared\": All reference updates have been queued to the\n-      transaction and references were locked on disk.\n-\n-    - \"committed\": The reference transaction was committed and all\n-      references now have their respective new value.\n-\n-    - \"aborted\": The reference transaction was aborted, no changes\n-      were performed and the locks have been released.\n-\n-For each reference update that was added to the transaction, the hook\n-receives on standard input a line of the format:\n-\n-  <old-value> SP <new-value> SP <ref-name> LF\n-\n-The exit status of the hook is ignored for any state except for the\n-\"prepared\" state. In the \"prepared\" state, a non-zero exit status will\n-cause the transaction to be aborted. The hook will not be called with\n-\"aborted\" state in that case.\n-\n-Hooks run during 'reference-transaction' will be run in parallel, unless\n-hook.jobs is configured to 1.\n-\n-push-to-checkout\n-~~~~~~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n-`git push` and updates reference(s) in its repository, and when\n-the push tries to update the branch that is currently checked out\n-and the `receive.denyCurrentBranch` configuration variable is set to\n-`updateInstead`.  Such a push by default is refused if the working\n-tree and the index of the remote repository has any difference from\n-the currently checked out commit; when both the working tree and the\n-index match the current commit, they are updated to match the newly\n-pushed tip of the branch.  This hook is to be used to override the\n-default behaviour.\n-\n-The hook receives the commit with which the tip of the current\n-branch is going to be updated.  It can exit with a non-zero status\n-to refuse the push (when it does so, it must not modify the index or\n-the working tree).  Or it can make any necessary changes to the\n-working tree and to the index to bring them to the desired state\n-when the tip of the current branch is updated to the new commit, and\n-exit with a zero status.\n-\n-For example, the hook can simply run `git read-tree -u -m HEAD \"$1\"`\n-in order to emulate `git fetch` that is run in the reverse direction\n-with `git push`, as the two-tree form of `git read-tree -u -m` is\n-essentially the same as `git switch` or `git checkout`\n-that switches branches while\n-keeping the local changes in the working tree that do not interfere\n-with the difference between the branches.\n-\n-Hooks executed during 'push-to-checkout' will not be parallelized.\n-\n-pre-auto-gc\n-~~~~~~~~~~~\n-\n-This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It\n-takes no parameter, and exiting with non-zero status from this script\n-causes the `git gc --auto` to abort.\n-\n-Hooks run during 'pre-auto-gc' will be run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-post-rewrite\n-~~~~~~~~~~~~\n-\n-This hook is invoked by commands that rewrite commits\n-(linkgit:git-commit[1] when called with `--amend` and\n-linkgit:git-rebase[1]; however, full-history (re)writing tools like\n-linkgit:git-fast-import[1] or\n-https://github.com/newren/git-filter-repo[git-filter-repo] typically\n-do not call it!).  Its first argument denotes the command it was\n-invoked by: currently one of `amend` or `rebase`.  Further\n-command-dependent arguments may be passed in the future.\n-\n-The hook receives a list of the rewritten commits on stdin, in the\n-format\n-\n-  <old-sha1> SP <new-sha1> [ SP <extra-info> ] LF\n-\n-The 'extra-info' is again command-dependent.  If it is empty, the\n-preceding SP is also omitted.  Currently, no commands pass any\n-'extra-info'.\n-\n-The hook always runs after the automatic note copying (see\n-\"notes.rewrite.<command>\" in linkgit:git-config[1]) has happened, and\n-thus has access to these notes.\n-\n-Hooks run during 'post-rewrite' will be run in parallel, unless hook.jobs is\n-configured to 1.\n-\n-The following command-specific comments apply:\n-\n-rebase::\n-\tFor the 'squash' and 'fixup' operation, all commits that were\n-\tsquashed are listed as being rewritten to the squashed commit.\n-\tThis means that there will be several lines sharing the same\n-\t'new-sha1'.\n-+\n-The commits are guaranteed to be listed in the order that they were\n-processed by rebase.\n-\n-sendemail-validate\n-~~~~~~~~~~~~~~~~~~\n-\n-This hook is invoked by linkgit:git-send-email[1].  It takes a single parameter,\n-the name of the file that holds the e-mail to be sent.  Exiting with a\n-non-zero status causes `git send-email` to abort before sending any\n-e-mails.\n-\n-fsmonitor-watchman\n-~~~~~~~~~~~~~~~~~~\n-\n-This hook is invoked when the configuration option `core.fsmonitor` is\n-set to `.git/hooks/fsmonitor-watchman` or `.git/hooks/fsmonitor-watchmanv2`\n-depending on the version of the hook to use.\n-\n-Version 1 takes two arguments, a version (1) and the time in elapsed\n-nanoseconds since midnight, January 1, 1970.\n-\n-Version 2 takes two arguments, a version (2) and a token that is used\n-for identifying changes since the token. For watchman this would be\n-a clock id. This version must output to stdout the new token followed\n-by a NUL before the list of files.\n-\n-The hook should output to stdout the list of all files in the working\n-directory that may have changed since the requested time.  The logic\n-should be inclusive so that it does not miss any potential changes.\n-The paths should be relative to the root of the working directory\n-and be separated by a single NUL.\n-\n-It is OK to include files which have not actually changed.  All changes\n-including newly-created and deleted files should be included. When\n-files are renamed, both the old and the new name should be included.\n-\n-Git will limit what files it checks for changes as well as which\n-directories are checked for untracked files based on the path names\n-given.\n-\n-An optimized way to tell git \"all files have changed\" is to return\n-the filename `/`.\n-\n-The exit status determines whether git will use the data from the\n-hook to limit its search.  On error, it will fall back to verifying\n-all files and folders.\n-\n-p4-changelist\n-~~~~~~~~~~~~~\n-\n-This hook is invoked by `git-p4 submit`.\n-\n-The `p4-changelist` hook is executed after the changelist\n-message has been edited by the user. It can be bypassed with the\n-`--no-verify` option. It takes a single parameter, the name\n-of the file that holds the proposed changelist text. Exiting\n-with a non-zero status causes the command to abort.\n-\n-The hook is allowed to edit the changelist file and can be used\n-to normalize the text into some project standard format. It can\n-also be used to refuse the Submit after inspect the message file.\n-\n-Run `git-p4 submit --help` for details.\n-\n-p4-prepare-changelist\n-~~~~~~~~~~~~~~~~~~~~~\n-\n-This hook is invoked by `git-p4 submit`.\n-\n-The `p4-prepare-changelist` hook is executed right after preparing\n-the default changelist message and before the editor is started.\n-It takes one parameter, the name of the file that contains the\n-changelist text. Exiting with a non-zero status from the script\n-will abort the process.\n-\n-The purpose of the hook is to edit the message file in place,\n-and it is not suppressed by the `--no-verify` option. This hook\n-is called even if `--prepare-p4-only` is set.\n-\n-Run `git-p4 submit --help` for details.\n-\n-p4-post-changelist\n-~~~~~~~~~~~~~~~~~~\n-\n-This hook is invoked by `git-p4 submit`.\n-\n-The `p4-post-changelist` hook is invoked after the submit has\n-successfully occurred in P4. It takes no parameters and is meant\n-primarily for notification and cannot affect the outcome of the\n-git p4 submit action.\n-\n-Run `git-p4 submit --help` for details.\n-\n-p4-pre-submit\n-~~~~~~~~~~~~~\n-\n-This hook is invoked by `git-p4 submit`. It takes no parameters and nothing\n-from standard input. Exiting with non-zero status from this script prevent\n-`git-p4 submit` from launching. It can be bypassed with the `--no-verify`\n-command line option. Run `git-p4 submit --help` for details.\n-\n-\n-\n-post-index-change\n-~~~~~~~~~~~~~~~~~\n-\n-This hook is invoked when the index is written in read-cache.c\n-do_write_locked_index.\n-\n-The first parameter passed to the hook is the indicator for the\n-working directory being updated.  \"1\" meaning working directory\n-was updated or \"0\" when the working directory was not updated.\n-\n-The second parameter passed to the hook is the indicator for whether\n-or not the index was updated and the skip-worktree bit could have\n-changed.  \"1\" meaning skip-worktree bits could have been updated\n-and \"0\" meaning they were not.\n-\n-Only one parameter should be set to \"1\" when the hook runs.  The hook\n-running passing \"1\", \"1\" should not be possible.\n-\n-Hooks run during 'post-index-change' will be run in parallel, unless hook.jobs\n-is configured to 1.\n+include::native-hooks.txt[]\n \n GIT\n ---\ndiff --git a/Documentation/native-hooks.txt b/Documentation/native-hooks.txt\nnew file mode 100644\nindex 0000000000..6c4aad83e1\n--- /dev/null\n+++ b/Documentation/native-hooks.txt\n@@ -0,0 +1,708 @@\n+applypatch-msg\n+~~~~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-am[1].  It takes a single\n+parameter, the name of the file that holds the proposed commit\n+log message.  Exiting with a non-zero status causes `git am` to abort\n+before applying the patch.\n+\n+The hook is allowed to edit the message file in place, and can\n+be used to normalize the message into some project standard\n+format. It can also be used to refuse the commit after inspecting\n+the message file.\n+\n+The default 'applypatch-msg' hook, when enabled, runs the\n+'commit-msg' hook, if the latter is enabled.\n+\n+Hooks run during 'applypatch-msg' will not be parallelized, because hooks are\n+expected to edit the file holding the commit log message.\n+\n+pre-applypatch\n+~~~~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-am[1].  It takes no parameter, and is\n+invoked after the patch is applied, but before a commit is made.\n+\n+If it exits with non-zero status, then the working tree will not be\n+committed after applying the patch.\n+\n+It can be used to inspect the current working tree and refuse to\n+make a commit if it does not pass certain test.\n+\n+The default 'pre-applypatch' hook, when enabled, runs the\n+'pre-commit' hook, if the latter is enabled.\n+\n+Hooks run during 'pre-applypatch' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+post-applypatch\n+~~~~~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-am[1].  It takes no parameter,\n+and is invoked after the patch is applied and a commit is made.\n+\n+This hook is meant primarily for notification, and cannot affect\n+the outcome of `git am`.\n+\n+Hooks run during 'post-applypatch' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+pre-commit\n+~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-commit[1], and can be bypassed\n+with the `--no-verify` option.  It takes no parameters, and is\n+invoked before obtaining the proposed commit log message and\n+making a commit.  Exiting with a non-zero status from this script\n+causes the `git commit` command to abort before creating a commit.\n+\n+The default 'pre-commit' hook, when enabled, catches introduction\n+of lines with trailing whitespaces and aborts the commit when\n+such a line is found.\n+\n+All the `git commit` hooks are invoked with the environment\n+variable `GIT_EDITOR=:` if the command will not bring up an editor\n+to modify the commit message.\n+\n+The default 'pre-commit' hook, when enabled--and with the\n+`hooks.allownonascii` config option unset or set to false--prevents\n+the use of non-ASCII filenames.\n+\n+Hooks executed during 'pre-commit' will not be parallelized.\n+\n+pre-merge-commit\n+~~~~~~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-merge[1], and can be bypassed\n+with the `--no-verify` option.  It takes no parameters, and is\n+invoked after the merge has been carried out successfully and before\n+obtaining the proposed commit log message to\n+make a commit.  Exiting with a non-zero status from this script\n+causes the `git merge` command to abort before creating a commit.\n+\n+The default 'pre-merge-commit' hook, when enabled, runs the\n+'pre-commit' hook, if the latter is enabled.\n+\n+This hook is invoked with the environment variable\n+`GIT_EDITOR=:` if the command will not bring up an editor\n+to modify the commit message.\n+\n+If the merge cannot be carried out automatically, the conflicts\n+need to be resolved and the result committed separately (see\n+linkgit:git-merge[1]). At that point, this hook will not be executed,\n+but the 'pre-commit' hook will, if it is enabled.\n+\n+Hooks executed during 'pre-merge-commit' will not be parallelized.\n+\n+prepare-commit-msg\n+~~~~~~~~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-commit[1] right after preparing the\n+default log message, and before the editor is started.\n+\n+It takes one to three parameters.  The first is the name of the file\n+that contains the commit log message.  The second is the source of the commit\n+message, and can be: `message` (if a `-m` or `-F` option was\n+given); `template` (if a `-t` option was given or the\n+configuration option `commit.template` is set); `merge` (if the\n+commit is a merge or a `.git/MERGE_MSG` file exists); `squash`\n+(if a `.git/SQUASH_MSG` file exists); or `commit`, followed by\n+a commit SHA-1 (if a `-c`, `-C` or `--amend` option was given).\n+\n+If the exit status is non-zero, `git commit` will abort.\n+\n+The purpose of the hook is to edit the message file in place, and\n+it is not suppressed by the `--no-verify` option.  A non-zero exit\n+means a failure of the hook and aborts the commit.  It should not\n+be used as replacement for pre-commit hook.\n+\n+The sample `prepare-commit-msg` hook that comes with Git removes the\n+help message found in the commented portion of the commit template.\n+\n+Hooks executed during 'prepare-commit-msg' will not be parallelized, because\n+hooks are expected to edit the file containing the commit log message.\n+\n+commit-msg\n+~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-commit[1] and linkgit:git-merge[1], and can be\n+bypassed with the `--no-verify` option.  It takes a single parameter,\n+the name of the file that holds the proposed commit log message.\n+Exiting with a non-zero status causes the command to abort.\n+\n+The hook is allowed to edit the message file in place, and can be used\n+to normalize the message into some project standard format. It\n+can also be used to refuse the commit after inspecting the message\n+file.\n+\n+The default 'commit-msg' hook, when enabled, detects duplicate\n+`Signed-off-by` trailers, and aborts the commit if one is found.\n+\n+Hooks executed during 'commit-msg' will not be parallelized, because hooks are\n+expected to edit the file containing the proposed commit log message.\n+\n+post-commit\n+~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-commit[1]. It takes no parameters, and is\n+invoked after a commit is made.\n+\n+This hook is meant primarily for notification, and cannot affect\n+the outcome of `git commit`.\n+\n+Hooks executed during 'post-commit' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+pre-rebase\n+~~~~~~~~~~\n+\n+This hook is called by linkgit:git-rebase[1] and can be used to prevent a\n+branch from getting rebased.  The hook may be called with one or\n+two parameters.  The first parameter is the upstream from which\n+the series was forked.  The second parameter is the branch being\n+rebased, and is not set when rebasing the current branch.\n+\n+Hooks executed during 'pre-rebase' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+post-checkout\n+~~~~~~~~~~~~~\n+\n+This hook is invoked when a linkgit:git-checkout[1] or\n+linkgit:git-switch[1] is run after having updated the\n+worktree.  The hook is given three parameters: the ref of the previous HEAD,\n+the ref of the new HEAD (which may or may not have changed), and a flag\n+indicating whether the checkout was a branch checkout (changing branches,\n+flag=1) or a file checkout (retrieving a file from the index, flag=0).\n+This hook cannot affect the outcome of `git switch` or `git checkout`,\n+other than that the hook's exit status becomes the exit status of\n+these two commands.\n+\n+It is also run after linkgit:git-clone[1], unless the `--no-checkout` (`-n`) option is\n+used. The first parameter given to the hook is the null-ref, the second the\n+ref of the new HEAD and the flag is always 1. Likewise for `git worktree add`\n+unless `--no-checkout` is used.\n+\n+This hook can be used to perform repository validity checks, auto-display\n+differences from the previous HEAD if different, or set working dir metadata\n+properties.\n+\n+Hooks executed during 'post-checkout' will not be parallelized.\n+\n+post-merge\n+~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-merge[1], which happens when a `git pull`\n+is done on a local repository.  The hook takes a single parameter, a status\n+flag specifying whether or not the merge being done was a squash merge.\n+This hook cannot affect the outcome of `git merge` and is not executed,\n+if the merge failed due to conflicts.\n+\n+This hook can be used in conjunction with a corresponding pre-commit hook to\n+save and restore any form of metadata associated with the working tree\n+(e.g.: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl\n+for an example of how to do this.\n+\n+Hooks executed during 'post-merge' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+pre-push\n+~~~~~~~~\n+\n+This hook is called by linkgit:git-push[1] and can be used to prevent\n+a push from taking place.  The hook is called with two parameters\n+which provide the name and location of the destination remote, if a\n+named remote is not being used both values will be the same.\n+\n+Information about what is to be pushed is provided on the hook's standard\n+input with lines of the form:\n+\n+  <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF\n+\n+For instance, if the command +git push origin master:foreign+ were run the\n+hook would receive a line like the following:\n+\n+  refs/heads/master 67890 refs/heads/foreign 12345\n+\n+although the full, 40-character SHA-1s would be supplied.  If the foreign ref\n+does not yet exist the `<remote SHA-1>` will be 40 `0`.  If a ref is to be\n+deleted, the `<local ref>` will be supplied as `(delete)` and the `<local\n+SHA-1>` will be 40 `0`.  If the local commit was specified by something other\n+than a name which could be expanded (such as `HEAD~`, or a SHA-1) it will be\n+supplied as it was originally given.\n+\n+If this hook exits with a non-zero status, `git push` will abort without\n+pushing anything.  Information about why the push is rejected may be sent\n+to the user by writing to standard error.\n+\n+Hooks executed during 'pre-push' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+[[pre-receive]]\n+pre-receive\n+~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n+`git push` and updates reference(s) in its repository.\n+Just before starting to update refs on the remote repository, the\n+pre-receive hook is invoked.  Its exit status determines the success\n+or failure of the update.\n+\n+This hook executes once for the receive operation. It takes no\n+arguments, but for each ref to be updated it receives on standard\n+input a line of the format:\n+\n+  <old-value> SP <new-value> SP <ref-name> LF\n+\n+where `<old-value>` is the old object name stored in the ref,\n+`<new-value>` is the new object name to be stored in the ref and\n+`<ref-name>` is the full name of the ref.\n+When creating a new ref, `<old-value>` is 40 `0`.\n+\n+If the hook exits with non-zero status, none of the refs will be\n+updated. If the hook exits with zero, updating of individual refs can\n+still be prevented by the <<update,'update'>> hook.\n+\n+Both standard output and standard error output are forwarded to\n+`git send-pack` on the other end, so you can simply `echo` messages\n+for the user.\n+\n+The number of push options given on the command line of\n+`git push --push-option=...` can be read from the environment\n+variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are\n+found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...\n+If it is negotiated to not use the push options phase, the\n+environment variables will not be set. If the client selects\n+to use push options, but doesn't transmit any, the count variable\n+will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n+\n+See the section on \"Quarantine Environment\" in\n+linkgit:git-receive-pack[1] for some caveats.\n+\n+Hooks executed during 'pre-receive' will not be parallelized.\n+\n+[[update]]\n+update\n+~~~~~~\n+\n+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n+`git push` and updates reference(s) in its repository.\n+Just before updating the ref on the remote repository, the update hook\n+is invoked.  Its exit status determines the success or failure of\n+the ref update.\n+\n+The hook executes once for each ref to be updated, and takes\n+three parameters:\n+\n+ - the name of the ref being updated,\n+ - the old object name stored in the ref,\n+ - and the new object name to be stored in the ref.\n+\n+A zero exit from the update hook allows the ref to be updated.\n+Exiting with a non-zero status prevents `git receive-pack`\n+from updating that ref.\n+\n+This hook can be used to prevent 'forced' update on certain refs by\n+making sure that the object name is a commit object that is a\n+descendant of the commit object named by the old object name.\n+That is, to enforce a \"fast-forward only\" policy.\n+\n+It could also be used to log the old..new status.  However, it\n+does not know the entire set of branches, so it would end up\n+firing one e-mail per ref when used naively, though.  The\n+<<post-receive,'post-receive'>> hook is more suited to that.\n+\n+In an environment that restricts the users' access only to git\n+commands over the wire, this hook can be used to implement access\n+control without relying on filesystem ownership and group\n+membership. See linkgit:git-shell[1] for how you might use the login\n+shell to restrict the user's access to only git commands.\n+\n+Both standard output and standard error output are forwarded to\n+`git send-pack` on the other end, so you can simply `echo` messages\n+for the user.\n+\n+The default 'update' hook, when enabled--and with\n+`hooks.allowunannotated` config option unset or set to false--prevents\n+unannotated tags to be pushed.\n+\n+Hooks executed during 'update' are run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+[[proc-receive]]\n+proc-receive\n+~~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-receive-pack[1].  If the server has\n+set the multi-valued config variable `receive.procReceiveRefs`, and the\n+commands sent to 'receive-pack' have matching reference names, these\n+commands will be executed by this hook, instead of by the internal\n+`execute_commands()` function.  This hook is responsible for updating\n+the relevant references and reporting the results back to 'receive-pack'.\n+\n+This hook executes once for the receive operation.  It takes no\n+arguments, but uses a pkt-line format protocol to communicate with\n+'receive-pack' to read commands, push-options and send results.  In the\n+following example for the protocol, the letter 'S' stands for\n+'receive-pack' and the letter 'H' stands for this hook.\n+\n+    # Version and features negotiation.\n+    S: PKT-LINE(version=1\\0push-options atomic...)\n+    S: flush-pkt\n+    H: PKT-LINE(version=1\\0push-options...)\n+    H: flush-pkt\n+\n+    # Send commands from server to the hook.\n+    S: PKT-LINE(<old-oid> <new-oid> <ref>)\n+    S: ... ...\n+    S: flush-pkt\n+    # Send push-options only if the 'push-options' feature is enabled.\n+    S: PKT-LINE(push-option)\n+    S: ... ...\n+    S: flush-pkt\n+\n+    # Receive result from the hook.\n+    # OK, run this command successfully.\n+    H: PKT-LINE(ok <ref>)\n+    # NO, I reject it.\n+    H: PKT-LINE(ng <ref> <reason>)\n+    # Fall through, let 'receive-pack' to execute it.\n+    H: PKT-LINE(ok <ref>)\n+    H: PKT-LINE(option fall-through)\n+    # OK, but has an alternate reference.  The alternate reference name\n+    # and other status can be given in option directives.\n+    H: PKT-LINE(ok <ref>)\n+    H: PKT-LINE(option refname <refname>)\n+    H: PKT-LINE(option old-oid <old-oid>)\n+    H: PKT-LINE(option new-oid <new-oid>)\n+    H: PKT-LINE(option forced-update)\n+    H: ... ...\n+    H: flush-pkt\n+\n+Each command for the 'proc-receive' hook may point to a pseudo-reference\n+and always has a zero-old as its old-oid, while the 'proc-receive' hook\n+may update an alternate reference and the alternate reference may exist\n+already with a non-zero old-oid.  For this case, this hook will use\n+\"option\" directives to report extended attributes for the reference given\n+by the leading \"ok\" directive.\n+\n+The report of the commands of this hook should have the same order as\n+the input.  The exit status of the 'proc-receive' hook only determines\n+the success or failure of the group of commands sent to it, unless\n+atomic push is in use.\n+\n+It is forbidden to specify more than one hook for 'proc-receive'. If a\n+globally-configured 'proc-receive' must be overridden, use\n+'hookcmd.<global-hook>.skip = true' to ignore it.\n+\n+[[post-receive]]\n+post-receive\n+~~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n+`git push` and updates reference(s) in its repository.\n+It executes on the remote repository once after all the refs have\n+been updated.\n+\n+This hook executes once for the receive operation.  It takes no\n+arguments, but gets the same information as the\n+<<pre-receive,'pre-receive'>>\n+hook does on its standard input.\n+\n+This hook does not affect the outcome of `git receive-pack`, as it\n+is called after the real work is done.\n+\n+This supersedes the <<post-update,'post-update'>> hook in that it gets\n+both old and new values of all the refs in addition to their\n+names.\n+\n+Both standard output and standard error output are forwarded to\n+`git send-pack` on the other end, so you can simply `echo` messages\n+for the user.\n+\n+The default 'post-receive' hook is empty, but there is\n+a sample script `post-receive-email` provided in the `contrib/hooks`\n+directory in Git distribution, which implements sending commit\n+emails.\n+\n+The number of push options given on the command line of\n+`git push --push-option=...` can be read from the environment\n+variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are\n+found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...\n+If it is negotiated to not use the push options phase, the\n+environment variables will not be set. If the client selects\n+to use push options, but doesn't transmit any, the count variable\n+will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n+\n+Hooks executed during 'post-receive' are run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+[[post-update]]\n+post-update\n+~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n+`git push` and updates reference(s) in its repository.\n+It executes on the remote repository once after all the refs have\n+been updated.\n+\n+It takes a variable number of parameters, each of which is the\n+name of ref that was actually updated.\n+\n+This hook is meant primarily for notification, and cannot affect\n+the outcome of `git receive-pack`.\n+\n+The 'post-update' hook can tell what are the heads that were pushed,\n+but it does not know what their original and updated values are,\n+so it is a poor place to do log old..new. The\n+<<post-receive,'post-receive'>> hook does get both original and\n+updated values of the refs. You might consider it instead if you need\n+them.\n+\n+When enabled, the default 'post-update' hook runs\n+`git update-server-info` to keep the information used by dumb\n+transports (e.g., HTTP) up to date.  If you are publishing\n+a Git repository that is accessible via HTTP, you should\n+probably enable this hook.\n+\n+Both standard output and standard error output are forwarded to\n+`git send-pack` on the other end, so you can simply `echo` messages\n+for the user.\n+\n+Hooks run during 'post-update' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+reference-transaction\n+~~~~~~~~~~~~~~~~~~~~~\n+\n+This hook is invoked by any Git command that performs reference\n+updates. It executes whenever a reference transaction is prepared,\n+committed or aborted and may thus get called multiple times.\n+\n+The hook takes exactly one argument, which is the current state the\n+given reference transaction is in:\n+\n+    - \"prepared\": All reference updates have been queued to the\n+      transaction and references were locked on disk.\n+\n+    - \"committed\": The reference transaction was committed and all\n+      references now have their respective new value.\n+\n+    - \"aborted\": The reference transaction was aborted, no changes\n+      were performed and the locks have been released.\n+\n+For each reference update that was added to the transaction, the hook\n+receives on standard input a line of the format:\n+\n+  <old-value> SP <new-value> SP <ref-name> LF\n+\n+The exit status of the hook is ignored for any state except for the\n+\"prepared\" state. In the \"prepared\" state, a non-zero exit status will\n+cause the transaction to be aborted. The hook will not be called with\n+\"aborted\" state in that case.\n+\n+Hooks run during 'reference-transaction' will be run in parallel, unless\n+hook.jobs is configured to 1.\n+\n+push-to-checkout\n+~~~~~~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to\n+`git push` and updates reference(s) in its repository, and when\n+the push tries to update the branch that is currently checked out\n+and the `receive.denyCurrentBranch` configuration variable is set to\n+`updateInstead`.  Such a push by default is refused if the working\n+tree and the index of the remote repository has any difference from\n+the currently checked out commit; when both the working tree and the\n+index match the current commit, they are updated to match the newly\n+pushed tip of the branch.  This hook is to be used to override the\n+default behaviour.\n+\n+The hook receives the commit with which the tip of the current\n+branch is going to be updated.  It can exit with a non-zero status\n+to refuse the push (when it does so, it must not modify the index or\n+the working tree).  Or it can make any necessary changes to the\n+working tree and to the index to bring them to the desired state\n+when the tip of the current branch is updated to the new commit, and\n+exit with a zero status.\n+\n+For example, the hook can simply run `git read-tree -u -m HEAD \"$1\"`\n+in order to emulate `git fetch` that is run in the reverse direction\n+with `git push`, as the two-tree form of `git read-tree -u -m` is\n+essentially the same as `git switch` or `git checkout`\n+that switches branches while\n+keeping the local changes in the working tree that do not interfere\n+with the difference between the branches.\n+\n+Hooks executed during 'push-to-checkout' will not be parallelized.\n+\n+pre-auto-gc\n+~~~~~~~~~~~\n+\n+This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It\n+takes no parameter, and exiting with non-zero status from this script\n+causes the `git gc --auto` to abort.\n+\n+Hooks run during 'pre-auto-gc' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+post-rewrite\n+~~~~~~~~~~~~\n+\n+This hook is invoked by commands that rewrite commits\n+(linkgit:git-commit[1] when called with `--amend` and\n+linkgit:git-rebase[1]; however, full-history (re)writing tools like\n+linkgit:git-fast-import[1] or\n+https://github.com/newren/git-filter-repo[git-filter-repo] typically\n+do not call it!).  Its first argument denotes the command it was\n+invoked by: currently one of `amend` or `rebase`.  Further\n+command-dependent arguments may be passed in the future.\n+\n+The hook receives a list of the rewritten commits on stdin, in the\n+format\n+\n+  <old-sha1> SP <new-sha1> [ SP <extra-info> ] LF\n+\n+The 'extra-info' is again command-dependent.  If it is empty, the\n+preceding SP is also omitted.  Currently, no commands pass any\n+'extra-info'.\n+\n+The hook always runs after the automatic note copying (see\n+\"notes.rewrite.<command>\" in linkgit:git-config[1]) has happened, and\n+thus has access to these notes.\n+\n+Hooks run during 'post-rewrite' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n+The following command-specific comments apply:\n+\n+rebase::\n+\tFor the 'squash' and 'fixup' operation, all commits that were\n+\tsquashed are listed as being rewritten to the squashed commit.\n+\tThis means that there will be several lines sharing the same\n+\t'new-sha1'.\n++\n+The commits are guaranteed to be listed in the order that they were\n+processed by rebase.\n+\n+sendemail-validate\n+~~~~~~~~~~~~~~~~~~\n+\n+This hook is invoked by linkgit:git-send-email[1].  It takes a single parameter,\n+the name of the file that holds the e-mail to be sent.  Exiting with a\n+non-zero status causes `git send-email` to abort before sending any\n+e-mails.\n+\n+fsmonitor-watchman\n+~~~~~~~~~~~~~~~~~~\n+\n+This hook is invoked when the configuration option `core.fsmonitor` is\n+set to `.git/hooks/fsmonitor-watchman` or `.git/hooks/fsmonitor-watchmanv2`\n+depending on the version of the hook to use.\n+\n+Version 1 takes two arguments, a version (1) and the time in elapsed\n+nanoseconds since midnight, January 1, 1970.\n+\n+Version 2 takes two arguments, a version (2) and a token that is used\n+for identifying changes since the token. For watchman this would be\n+a clock id. This version must output to stdout the new token followed\n+by a NUL before the list of files.\n+\n+The hook should output to stdout the list of all files in the working\n+directory that may have changed since the requested time.  The logic\n+should be inclusive so that it does not miss any potential changes.\n+The paths should be relative to the root of the working directory\n+and be separated by a single NUL.\n+\n+It is OK to include files which have not actually changed.  All changes\n+including newly-created and deleted files should be included. When\n+files are renamed, both the old and the new name should be included.\n+\n+Git will limit what files it checks for changes as well as which\n+directories are checked for untracked files based on the path names\n+given.\n+\n+An optimized way to tell git \"all files have changed\" is to return\n+the filename `/`.\n+\n+The exit status determines whether git will use the data from the\n+hook to limit its search.  On error, it will fall back to verifying\n+all files and folders.\n+\n+p4-changelist\n+~~~~~~~~~~~~~\n+\n+This hook is invoked by `git-p4 submit`.\n+\n+The `p4-changelist` hook is executed after the changelist\n+message has been edited by the user. It can be bypassed with the\n+`--no-verify` option. It takes a single parameter, the name\n+of the file that holds the proposed changelist text. Exiting\n+with a non-zero status causes the command to abort.\n+\n+The hook is allowed to edit the changelist file and can be used\n+to normalize the text into some project standard format. It can\n+also be used to refuse the Submit after inspect the message file.\n+\n+Run `git-p4 submit --help` for details.\n+\n+p4-prepare-changelist\n+~~~~~~~~~~~~~~~~~~~~~\n+\n+This hook is invoked by `git-p4 submit`.\n+\n+The `p4-prepare-changelist` hook is executed right after preparing\n+the default changelist message and before the editor is started.\n+It takes one parameter, the name of the file that contains the\n+changelist text. Exiting with a non-zero status from the script\n+will abort the process.\n+\n+The purpose of the hook is to edit the message file in place,\n+and it is not suppressed by the `--no-verify` option. This hook\n+is called even if `--prepare-p4-only` is set.\n+\n+Run `git-p4 submit --help` for details.\n+\n+p4-post-changelist\n+~~~~~~~~~~~~~~~~~~\n+\n+This hook is invoked by `git-p4 submit`.\n+\n+The `p4-post-changelist` hook is invoked after the submit has\n+successfully occurred in P4. It takes no parameters and is meant\n+primarily for notification and cannot affect the outcome of the\n+git p4 submit action.\n+\n+Run `git-p4 submit --help` for details.\n+\n+p4-pre-submit\n+~~~~~~~~~~~~~\n+\n+This hook is invoked by `git-p4 submit`. It takes no parameters and nothing\n+from standard input. Exiting with non-zero status from this script prevent\n+`git-p4 submit` from launching. It can be bypassed with the `--no-verify`\n+command line option. Run `git-p4 submit --help` for details.\n+\n+\n+\n+post-index-change\n+~~~~~~~~~~~~~~~~~\n+\n+This hook is invoked when the index is written in read-cache.c\n+do_write_locked_index.\n+\n+The first parameter passed to the hook is the indicator for the\n+working directory being updated.  \"1\" meaning working directory\n+was updated or \"0\" when the working directory was not updated.\n+\n+The second parameter passed to the hook is the indicator for whether\n+or not the index was updated and the skip-worktree bit could have\n+changed.  \"1\" meaning skip-worktree bits could have been updated\n+and \"0\" meaning they were not.\n+\n+Only one parameter should be set to \"1\" when the hook runs.  The hook\n+running passing \"1\", \"1\" should not be possible.\n+\n+Hooks run during 'post-index-change' will be run in parallel, unless hook.jobs\n+is configured to 1.\n+\n-- \n2.31.0.rc2.261.g7f71774620-goog\n\n"},{"id":"418908","messageId":"xmqqim5xba4d.fsf@gitster.g","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"Re: [PATCH v8 00/37] config-based hooks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-11T22:26:10Z","receivedAt":"2021-03-11T22:26:57Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> Since v7:\n> - Addressed Jonathan Tan's review of part I\n> - Addressed Junio's review of part I and II\n> - Combined parts I and II\n>\n> I think the updates to patch 1 between the rest of the work I've been\n> doing probably have covered Ævar's comments.\n>\n> More details about per-patch changes found in the notes on each mail (I\n> hope).\n>\n> I know that Junio was talking about merging v7 after Josh Steadmon's\n> review and I asked him not to - this reroll has those changes from\n> Jonathan Tan's review that I was wanting to wait for.\n\nI picked it up and replaced, not necessarily because it is an urgent\nthing to do during the pre-release period, but primarily because I\nwanted to be prepared for any nasty surprises by unmanageable\nconflicts I may have to face once the current cycle is over.\n\nIt turns out that it was a bit painful to merge to 'seen' as there\nare in-flight topics that touch the hooks documentation, and the\nchanges they make must be carried forward to the new file.\n\nBut it was not too bad.  \n\nThe merge into 'seen' is 3cdeaeab (Merge branch 'es/config-hooks'\ninto seen, 2021-03-11) as of this writing, and the output of\n\n    $ git diff 3cdeaeab3a^:Documentation/githooks.txt \\\n               3cdeaeab3a:Documentation/native-hooks.txt\n\n    (i.e. the version of the file before the merge, where your topic\n    being merged took material to edit to produce the new \"native-hooks\"\n    document, is compared with the result)\n\nlooks reasonable to me, but please double check.\n\nThanks.\n"},{"id":"418940","messageId":"87o8fohjgq.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-4-emilyshaffer@google.com","subject":"Re: [PATCH v8 03/37] hook: add list command","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T08:20:05Z","receivedAt":"2021-03-12T08:21:15Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> new file mode 100644\n> index 0000000000..71449ecbc7\n> --- /dev/null\n> +++ b/Documentation/config/hook.txt\n> @@ -0,0 +1,9 @@\n> +hook.<command>.command::\n> +\tA command to execute during the <command> hook event. This can be an\n> +\texecutable on your device, a oneliner for your shell, or the name of a\n> +\thookcmd. See linkgit:git-hook[1].\n> +\n> +hookcmd.<name>.command::\n> +\tA command to execute during a hook for which <name> has been specified\n> +\tas a command. This can be an executable on your device or a oneliner for\n> +\tyour shell. See linkgit:git-hook[1].\n> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> index 9eeab0009d..f19875ed68 100644\n> --- a/Documentation/git-hook.txt\n> +++ b/Documentation/git-hook.txt\n> @@ -8,12 +8,65 @@ git-hook - Manage configured hooks\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -'git hook'\n> +'git hook' list <hook-name>\n\nHaving just read this far (maybe this pattern is shared in the rest of\nthe series): Let's just squash this and the 2nd patch together.\n\nSometimes it's worth doing the scaffolding first, but adding a new\nbuilt-in is so trivial that I don't think it's worth it, and it just\nresults in back & forth churn like the above...\n\n>  DESCRIPTION\n>  -----------\n> -A placeholder command. Later, you will be able to list, add, and modify hooks\n> -with this command.\n\n...and this...\n\n> +You can list configured hooks with this command. Later, you will be able to run,\n> +add, and modify hooks with this command.\n> +\n> +This command parses the default configuration files for sections `hook` and\n> +`hookcmd`. `hook` is used to describe the commands which will be run during a\n> +particular hook event; commands are run in the order Git encounters them during\n> +the configuration parse (see linkgit:git-config[1]). `hookcmd` is used to\n> +describe attributes of a specific command. If additional attributes don't need\n> +to be specified, a command to run can be specified directly in the `hook`\n> +section; if a `hookcmd` by that name isn't found, Git will attempt to run the\n> +provided value directly. For example:\n> +\n> +Global config\n> +----\n> +  [hook \"post-commit\"]\n> +    command = \"linter\"\n> +    command = \"~/typocheck.sh\"\n> +\n> +  [hookcmd \"linter\"]\n> +    command = \"/bin/linter --c\"\n> +----\n> +\n> +Local config\n> +----\n> +  [hook \"prepare-commit-msg\"]\n> +    command = \"linter\"\n> +  [hook \"post-commit\"]\n> +    command = \"python ~/run-test-suite.py\"\n> +----\n> +\n> +With these configs, you'd then see:\n> +\n> +----\n> +$ git hook list \"post-commit\"\n> +global: /bin/linter --c\n> +global: ~/typocheck.sh\n> +local: python ~/run-test-suite.py\n> +\n> +$ git hook list \"prepare-commit-msg\"\n> +local: /bin/linter --c\n> +----\n> +\n> +COMMANDS\n> +--------\n> +\n> +list `<hook-name>`::\n> +\n> +List the hooks which have been configured for `<hook-name>`. Hooks appear\n> +in the order they should be run, and print the config scope where the relevant\n> +`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n> +This output is human-readable and the format is subject to change over time.\n> +\n> +CONFIGURATION\n> +-------------\n> +include::config/hook.txt[]\n>  \n>  GIT\n>  ---\n> diff --git a/Makefile b/Makefile\n> index 8e904a1ab5..3fa51597d8 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -891,6 +891,7 @@ LIB_OBJS += hash-lookup.o\n>  LIB_OBJS += hashmap.o\n>  LIB_OBJS += help.o\n>  LIB_OBJS += hex.o\n> +LIB_OBJS += hook.o\n>  LIB_OBJS += ident.o\n>  LIB_OBJS += json-writer.o\n>  LIB_OBJS += kwset.o\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index b2bbc84d4d..bb64cd77ca 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -1,21 +1,67 @@\n>  #include \"cache.h\"\n> -\n\nStray back & forth whitespace churn?\n\n>  #include \"builtin.h\"\n> +#include \"config.h\"\n> +#include \"hook.h\"\n>  #include \"parse-options.h\"\n> +#include \"strbuf.h\"\n>  \n>  static const char * const builtin_hook_usage[] = {\n> -\tN_(\"git hook\"),\n> +\tN_(\"git hook list <hookname>\"),\n>  \tNULL\n>  };\n>  \n> -int cmd_hook(int argc, const char **argv, const char *prefix)\n> +static int list(int argc, const char **argv, const char *prefix)\n\n...and here the cmd_hook() function being replaced (not really, just\nmoved below, but you get my drift...)\n\n>  {\n> -\tstruct option builtin_hook_options[] = {\n> +\tstruct list_head *head, *pos;\n> +\tstruct strbuf hookname = STRBUF_INIT;\n> +\n> +\tstruct option list_options[] = {\n>  \t\tOPT_END(),\n>  \t};\n>  \n> -\targc = parse_options(argc, argv, prefix, builtin_hook_options,\n> +\targc = parse_options(argc, argv, prefix, list_options,\n>  \t\t\t     builtin_hook_usage, 0);\n>  \n> +\tif (argc < 1) {\n> +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n> +\t\t\t      builtin_hook_usage, list_options);\n> +\t}\n> +\n> +\tstrbuf_addstr(&hookname, argv[0]);\n> +\n> +\thead = hook_list(&hookname);\n> +\n\nMore on strbuf usage later in another soon-to-be-sent E-Mail.\n\n> +\tif (list_empty(head)) {\n> +\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> +\t\t       hookname.buf);\n> +\t\tstrbuf_release(&hookname);\n> +\t\treturn 0;\n> +\t}\n> +\n> +\tlist_for_each(pos, head) {\n> +\t\tstruct hook *item = list_entry(pos, struct hook, list);\n> +\t\tif (item)\n> +\t\t\tprintf(\"%s: %s\\n\",\n> +\t\t\t       config_scope_name(item->origin),\n> +\t\t\t       item->command.buf);\n> +\t}\n> +\n> +\tclear_hook_list(head);\n> +\tstrbuf_release(&hookname);\n> +\n>  \treturn 0;\n>  }\n> +\n> +int cmd_hook(int argc, const char **argv, const char *prefix)\n> +{\n> +\tstruct option builtin_hook_options[] = {\n> +\t\tOPT_END(),\n> +\t};\n> +\tif (argc < 2)\n> +\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n> +\n> +\tif (!strcmp(argv[1], \"list\"))\n> +\t\treturn list(argc - 1, argv + 1, prefix);\n> +\n> +\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n> +}\n> diff --git a/hook.c b/hook.c\n> new file mode 100644\n> index 0000000000..fede40e925\n> --- /dev/null\n> +++ b/hook.c\n> @@ -0,0 +1,120 @@\n> +#include \"cache.h\"\n> +\n> +#include \"hook.h\"\n> +#include \"config.h\"\n> +\n> +void free_hook(struct hook *ptr)\n> +{\n> +\tif (ptr) {\n> +\t\tstrbuf_release(&ptr->command);\n> +\t\tfree(ptr);\n> +\t}\n> +}\n\nNeither strbuf_release() nor free() need or should have a \"if (ptr)\" guard.\n\n> +\n> +static void append_or_move_hook(struct list_head *head, const char *command)\n> +{\n> +\tstruct list_head *pos = NULL, *tmp = NULL;\n> +\tstruct hook *to_add = NULL;\n> +\n> +\t/*\n> +\t * remove the prior entry with this command; we'll replace it at the\n> +\t * end.\n> +\t */\n> +\tlist_for_each_safe(pos, tmp, head) {\n> +\t\tstruct hook *it = list_entry(pos, struct hook, list);\n> +\t\tif (!strcmp(it->command.buf, command)) {\n> +\t\t    list_del(pos);\n> +\t\t    /* we'll simply move the hook to the end */\n> +\t\t    to_add = it;\n> +\t\t    break;\n> +\t\t}\n> +\t}\n> +\n> +\tif (!to_add) {\n> +\t\t/* adding a new hook, not moving an old one */\n> +\t\tto_add = xmalloc(sizeof(*to_add));\n> +\t\tstrbuf_init(&to_add->command, 0);\n> +\t\tstrbuf_addstr(&to_add->command, command);\n> +\t}\n> +\n> +\t/* re-set the scope so we show where an override was specified */\n> +\tto_add->origin = current_config_scope();\n> +\n> +\tlist_add_tail(&to_add->list, head);\n> +}\n> +\n> +static void remove_hook(struct list_head *to_remove)\n> +{\n> +\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n> +\tlist_del(to_remove);\n> +\tfree_hook(hook_to_remove);\n> +}\n> +\n> +void clear_hook_list(struct list_head *head)\n> +{\n> +\tstruct list_head *pos, *tmp;\n> +\tlist_for_each_safe(pos, tmp, head)\n> +\t\tremove_hook(pos);\n> +}\n> +\n> +struct hook_config_cb\n> +{\n> +\tstruct strbuf *hookname;\n> +\tstruct list_head *list;\n> +};\n> +\n> +static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n> +{\n> +\tstruct hook_config_cb *data = cb_data;\n> +\tconst char *hook_key = data->hookname->buf;\n> +\tstruct list_head *head = data->list;\n> +\n> +\tif (!strcmp(key, hook_key)) {\n> +\t\tconst char *command = value;\n> +\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n> +\n> +\t\t/*\n> +\t\t * Check if a hookcmd with that name exists. If it doesn't,\n> +\t\t * 'git_config_get_value()' is documented not to touch &command,\n> +\t\t * so we don't need to do anything.\n> +\t\t */\n> +\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n> +\t\tgit_config_get_value(hookcmd_name.buf, &command);\n> +\n> +\t\tif (!command) {\n> +\t\t\tstrbuf_release(&hookcmd_name);\n> +\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n> +\t\t}\n> +\n> +\t\t/*\n> +\t\t * TODO: implement an option-getting callback, e.g.\n> +\t\t *   get configs by pattern hookcmd.$value.*\n> +\t\t *   for each key+value, do_callback(key, value, cb_data)\n> +\t\t */\n> +\n> +\t\tappend_or_move_hook(head, command);\n> +\n> +\t\tstrbuf_release(&hookcmd_name);\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +struct list_head* hook_list(const struct strbuf* hookname)\n> +{\n> +\tstruct strbuf hook_key = STRBUF_INIT;\n> +\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> +\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n> +\n> +\tINIT_LIST_HEAD(hook_head);\n> +\n> +\tif (!hookname)\n> +\t\treturn NULL;\n\n...if a strbuf being passed in is NULL?\n\n> [...]\n> +ROOT=\n> +if test_have_prereq MINGW\n> +then\n> +\t# In Git for Windows, Unix-like paths work only in shell scripts;\n> +\t# `git.exe`, however, will prefix them with the pseudo root directory\n> +\t# (of the Unix shell). Let's accommodate for that.\n> +\tROOT=\"$(cd / && pwd)\"\n> +fi\n\nI didn't read up on previous rounds, but if we're squashing this into 02\nhaving a seperate commit summarizing this little hack would be most\nwelcome, or have it in this commit message.\n\nIsn't this sort of thing generally usable, maybe we can add it under a\nlonger variable name to test-lib.sh?\n"},{"id":"418941","messageId":"87lfashj03.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-5-emilyshaffer@google.com","subject":"Re: [PATCH v8 04/37] hook: include hookdir hook in list","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T08:30:04Z","receivedAt":"2021-03-12T08:30:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> Historically, hooks are declared by placing an executable into\n> $GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken\n> from the config are more featureful than hooks placed in the $HOOKDIR,\n> those hooks should not stop working for users who already have them.\n> Let's list them to the user, but instead of displaying a config scope\n> (e.g. \"global: blah\") we can prefix them with \"hookdir:\".\n>\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>\n> Notes:\n>     Since v7, fix some nits from Jonathan Tan. The largest is to move reference to\n>     \"hookdir annotation\" from this commit to the next one which introduces the\n>     hook.runHookDir option.\n>\n>  builtin/hook.c                | 11 +++++++++--\n>  hook.c                        | 17 +++++++++++++++++\n>  hook.h                        |  1 +\n>  t/t1360-config-based-hooks.sh | 19 +++++++++++++++++++\n>  4 files changed, 46 insertions(+), 2 deletions(-)\n>\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index bb64cd77ca..c8fbfbb39d 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -40,10 +40,15 @@ static int list(int argc, const char **argv, const char *prefix)\n>  \n>  \tlist_for_each(pos, head) {\n>  \t\tstruct hook *item = list_entry(pos, struct hook, list);\n> -\t\tif (item)\n> +\t\titem = list_entry(pos, struct hook, list);\n> +\t\tif (item) {\n> +\t\t\t/* Don't translate 'hookdir' - it matches the config */\n\nLet's prefix comments for translators with /* TRANSLATORS: .., see the\ncoding style doc. That's what they'll see, and this is useful to them.\n\nBetter yet have a note here about the first argument being 'system',\n'local' etc., which I had to source spelunge for, and translators won't\nhave any idea about unless the magic parameter is documented.\n\n> +setup_hookdir () {\n> +\tmkdir .git/hooks\n> +\twrite_script .git/hooks/pre-commit <<-EOF\n> +\techo \\\"Legacy Hook\\\"\n\nNit, \"'s not needed, but it also seems nothing uses this, so if it's\njust a pass-through script either \"exit 0\", or actually check if it's\nrun or something?\n\n> [...]\n> +test_expect_success 'git hook list shows hooks from the hookdir' '\n> +\tsetup_hookdir &&\n> +\n> +\tcat >expected <<-EOF &&\n> +\thookdir: $(pwd)/.git/hooks/pre-commit\n> +\tEOF\n> +\n> +\tgit hook list pre-commit >actual &&\n> +\ttest_cmp expected actual\n> +'\n\nAh, so it's just checking if it exists...\n"},{"id":"418942","messageId":"87im5whitx.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-6-emilyshaffer@google.com","subject":"Re: [PATCH v8 05/37] hook: teach hook.runHookDir","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T08:33:46Z","receivedAt":"2021-03-12T08:34:45Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> +\tswitch (should_run_hookdir) {\n> +\t\tcase HOOKDIR_NO:\n\nStyle: case shouldn't be indented\n\n> +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will not run)\"));\n> +\t\t\tbreak;\n> +\t\tcase HOOKDIR_ERROR:\n> +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will error and not run)\"));\n> +\t\t\tbreak;\n> +\t\tcase HOOKDIR_INTERACTIVE:\n> +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will prompt)\"));\n> +\t\t\tbreak;\n> +\t\tcase HOOKDIR_WARN:\n> +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will warn but run)\"));\n> +\t\t\tbreak;\n> +\t\tcase HOOKDIR_YES:\n> +\t\t/*\n> +\t\t * The default behavior should agree with\n> +\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n> +\t\t * do the default behavior.\n> +\t\t */\n> +\t\tcase HOOKDIR_UNKNOWN:\n> +\t\tdefault:\n> +\t\t\tbreak;\n\nWe should avoid this sort of translation lego.\n\n> +\t}\n> +\n>  \tlist_for_each(pos, head) {\n>  \t\tstruct hook *item = list_entry(pos, struct hook, list);\n>  \t\titem = list_entry(pos, struct hook, list);\n>  \t\tif (item) {\n>  \t\t\t/* Don't translate 'hookdir' - it matches the config */\n> -\t\t\tprintf(\"%s: %s\\n\",\n> +\t\t\tprintf(\"%s: %s%s\\n\",\n\nnative speakers in some languages to read the sentance backwards.\nBecause if you concatenate strings like this you force.\n\n(We don't currently have a RTL language in po/, still, but let's not\ncreate churn for if/when we do if we can help it)>\n\n\nI have a patch on top to fix this, will send it as some general reply of\nproposed fixup.s\n\n>  \t\t\t       (item->from_hookdir\n> +\tgit hook list pre-commit >actual &&\n> +\t# the hookdir annotation is translated\n> +\ttest_i18ncmp expected actual\n\nThis (and the rest of test_i18ncmp in this series) can and should just\nbe \"test_cmp\" or \"test_i18ncmp\", the poison mode is dead. See my recent\npatches to search/replace test_i18ncmp.\n\nThe reason the function isn't gone entirely was to help a series like\nyours in \"seen\", but if we're re-rolling...\n"},{"id":"418943","messageId":"87ft10hi30.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-7-emilyshaffer@google.com","subject":"Re: [PATCH v8 06/37] hook: implement hookcmd.<name>.skip","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T08:49:55Z","receivedAt":"2021-03-12T08:50:59Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> +\tcat >expected <<-EOF &&\n> +\tno commands configured for hook '\\''pre-commit'\\''\n> +\tEOF\n> +\n> +\tgit hook list pre-commit >actual &&\n> +\ttest_i18ncmp expected actual\n> +'\n> +\n> +test_expect_success 'git hook list ignores skip referring to unused hookcmd' '\n> +\ttest_config hookcmd.abc.command \"/path/abc\" --add &&\n> +\ttest_config hookcmd.abc.skip \"true\" --add &&\n> +\n> +\tcat >expected <<-EOF &&\n> +\tno commands configured for hook '\\''pre-commit'\\''\n\nditto on the \"echo\" comment in a previous mail, looks like we can avoid\nboth of these entirely.\n"},{"id":"418944","messageId":"87czw4hi19.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-8-emilyshaffer@google.com","subject":"Re: [PATCH v8 07/37] parse-options: parse into strvec","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T08:50:58Z","receivedAt":"2021-03-12T08:52:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\n> index 5a60bbfa7f..f79b17e7fc 100644\n> --- a/Documentation/technical/api-parse-options.txt\n> +++ b/Documentation/technical/api-parse-options.txt\n> @@ -173,6 +173,13 @@ There are some macros to easily define options:\n>  \tThe string argument is stored as an element in `string_list`.\n>  \tUse of `--no-option` will clear the list of preceding values.\n>  \n> +`OPT_STRVEC(short, long, &struct strvec, arg_str, description)`::\n> +\tIntroduce an option with a string argument, meant to be specified\n> +\tmultiple times.\n> +\tThe string argument is stored as an element in `strvec`, and later\n> +\targuments are added to the same `strvec`.\n> +\tUse of `--no-option` will clear the list of preceding values.\n> +\n>  `OPT_INTEGER(short, long, &int_var, description)`::\n>  \tIntroduce an option with integer argument.\n>  \tThe integer is put into `int_var`.\n> diff --git a/parse-options-cb.c b/parse-options-cb.c\n> index 4542d4d3f9..c2451dfb1b 100644\n> --- a/parse-options-cb.c\n> +++ b/parse-options-cb.c\n> @@ -207,6 +207,22 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset)\n>  \treturn 0;\n>  }\n>  \n> +int parse_opt_strvec(const struct option *opt, const char *arg, int unset)\n> +{\n> +\tstruct strvec *v = opt->value;\n> +\n> +\tif (unset) {\n> +\t\tstrvec_clear(v);\n> +\t\treturn 0;\n> +\t}\n> +\n> +\tif (!arg)\n> +\t\treturn -1;\n> +\n> +\tstrvec_push(v, arg);\n> +\treturn 0;\n> +}\n> +\n>  int parse_opt_noop_cb(const struct option *opt, const char *arg, int unset)\n>  {\n>  \treturn 0;\n> diff --git a/parse-options.h b/parse-options.h\n> index ff6506a504..44c4ac08e9 100644\n> --- a/parse-options.h\n> +++ b/parse-options.h\n> @@ -177,6 +177,9 @@ struct option {\n>  #define OPT_STRING_LIST(s, l, v, a, h) \\\n>  \t\t\t\t    { OPTION_CALLBACK, (s), (l), (v), (a), \\\n>  \t\t\t\t      (h), 0, &parse_opt_string_list }\n> +#define OPT_STRVEC(s, l, v, a, h) \\\n> +\t\t\t\t    { OPTION_CALLBACK, (s), (l), (v), (a), \\\n> +\t\t\t\t      (h), 0, &parse_opt_strvec }\n>  #define OPT_UYN(s, l, v, h)         { OPTION_CALLBACK, (s), (l), (v), NULL, \\\n>  \t\t\t\t      (h), PARSE_OPT_NOARG, &parse_opt_tertiary }\n>  #define OPT_EXPIRY_DATE(s, l, v, h) \\\n> @@ -296,6 +299,7 @@ int parse_opt_commits(const struct option *, const char *, int);\n>  int parse_opt_commit(const struct option *, const char *, int);\n>  int parse_opt_tertiary(const struct option *, const char *, int);\n>  int parse_opt_string_list(const struct option *, const char *, int);\n> +int parse_opt_strvec(const struct option *, const char *, int);\n>  int parse_opt_noop_cb(const struct option *, const char *, int);\n>  enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx,\n>  \t\t\t\t\t   const struct option *,\n\nNice, seems very useful.\n\nBut let's add a test in test-parse-options.c like we have for\nstring_list?\n"},{"id":"418945","messageId":"87a6r8hhvf.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-9-emilyshaffer@google.com","subject":"Re: [PATCH v8 08/37] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T08:54:28Z","receivedAt":"2021-03-12T08:57:09Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n>  'git hook' list <hook-name>\n> +'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>\n\n[...]\n\n> +\tswitch (cfg)\n> +\t{\n> +\t\tcase HOOKDIR_ERROR:\n\nOverly indented case statements again.\n\n> +\t\t\tfprintf(stderr, _(\"Skipping legacy hook at '%s'\\n\"),\n> +\t\t\t\tpath);\n> +\t\t\t/* FALLTHROUGH */\n> +\t\tcase HOOKDIR_NO:\n> +\t\t\treturn 0;\n> +\t\tcase HOOKDIR_WARN:\n> +\t\t\tfprintf(stderr, _(\"Running legacy hook at '%s'\\n\"),\n> +\t\t\t\tpath);\n> +\t\t\treturn 1;\n> +\t\tcase HOOKDIR_INTERACTIVE:\n> +\t\t\tdo {\n> +\t\t\t\t/*\n> +\t\t\t\t * TRANSLATORS: Make sure to include [Y] and [n]\n> +\t\t\t\t * in your translation. Only English input is\n> +\t\t\t\t * accepted. Default option is \"yes\".\n> +\t\t\t\t */\n> +\t\t\t\tfprintf(stderr, _(\"Run '%s'? [Yn] \"), path);\n\nNit: [Y/n]\n\n> +\t\t\t\t} else if (starts_with(prompt.buf, \"y\")) {\n\nSo also \"Y\", \"yes\" and \"yellow\"...\n\n> [...]\n>  \tgit hook list pre-commit >actual &&\n>  \t# the hookdir annotation is translated\n> -\ttest_i18ncmp expected actual\n> +\ttest_i18ncmp expected actual &&\n> +\n> +\ttest_write_lines n | git hook run pre-commit 2>actual &&\n> +\t! grep \"Legacy Hook\" actual &&\n> +\n> +\ttest_write_lines y | git hook run pre-commit 2>actual &&\n> +\tgrep \"Legacy Hook\" actual\n> +'\n> +\n> +test_expect_success 'inline hook definitions execute oneliners' '\n> +\ttest_config hook.pre-commit.command \"echo \\\"Hello World\\\"\" &&\n> +\n> +\techo \"Hello World\" >expected &&\n> +\n> +\t# hooks are run with stdout_to_stderr = 1\n> +\tgit hook run pre-commit 2>actual &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'inline hook definitions resolve paths' '\n> +\twrite_script sample-hook.sh <<-EOF &&\n> +\techo \\\"Sample Hook\\\"\n> +\tEOF\n> +\n> +\ttest_when_finished \"rm sample-hook.sh\" &&\n> +\n> +\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n> +\n> +\techo \\\"Sample Hook\\\" >expected &&\n> +\n> +\t# hooks are run with stdout_to_stderr = 1\n> +\tgit hook run pre-commit 2>actual &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'hookdir hook included in git hook run' '\n> +\tsetup_hookdir &&\n> +\n> +\techo \\\"Legacy Hook\\\" >expected &&\n> +\n> +\t# hooks are run with stdout_to_stderr = 1\n> +\tgit hook run pre-commit 2>actual &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'out-of-repo runs excluded' '\n> +\tsetup_hooks &&\n> +\n> +\tnongit test_must_fail git hook run pre-commit\n>  '\n>  \n>  test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n\nNo tests for --env or --arg?\n"},{"id":"418946","messageId":"877dmchhlo.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-11-emilyshaffer@google.com","subject":"Re: [PATCH v8 10/37] hook: support passing stdin to hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T09:00:19Z","receivedAt":"2021-03-12T09:01:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> On the frontend, this is supported by asking for a file path, rather\n> than by reading stdin. Reading directly from stdin would involve caching\n> the entire stdin (to memory or to disk) and reading it back from the\n> beginning to each hook. We'd want to support cases like insufficient\n> memory or storage for the file. While this may prove useful later, for\n> now the path of least resistance is to just ask the user to make this\n> interim file themselves.\n\nWe need to worry about cases where we wouldn't have enough memory to\nbuffer the stdin, but still need to then do repo operations on the input\nfrom such a file, presumably some giant pre-receive update or something.\n\nSeems unlikely, and the convenience of having stdin just work by just\nallocating that seems appealing, but let's read on to the rest of the\nseries...\n"},{"id":"418947","messageId":"874khghh8r.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-18-emilyshaffer@google.com","subject":"Re: [PATCH v8 17/37] hooks: allow callers to capture output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T09:08:04Z","receivedAt":"2021-03-12T09:08:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> Some server-side hooks will require capturing output to send over\n> sideband instead of printing directly to stderr. Expose that capability.\n\nSo added here in 17/37 and not used until 30/37. As a point on\nreadability (this isn't the first such patch) I think it would be better\nto just squash those together with some \"since we now need access to\nconsume_sideband in hooks, do that ...\".\n\nIf there's a much larger API it makes sense to do it as another step...\n\n>  hook.c | 3 ++-\n>  hook.h | 8 ++++++++\n>  2 files changed, 10 insertions(+), 1 deletion(-)\n>\n> diff --git a/hook.c b/hook.c\n> index e16b082cbd..2322720ffe 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -256,6 +256,7 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n>  \to->dir = NULL;\n>  \to->feed_pipe = NULL;\n>  \to->feed_pipe_ctx = NULL;\n> +\to->consume_sideband = NULL;\n>  }\n>  \n>  void run_hooks_opt_init_async(struct run_hooks_opt *o)\n> @@ -434,7 +435,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n>  \t\t\t\t   pick_next_hook,\n>  \t\t\t\t   notify_start_failure,\n>  \t\t\t\t   options->feed_pipe,\n> -\t\t\t\t   NULL,\n> +\t\t\t\t   options->consume_sideband,\n>  \t\t\t\t   notify_hook_finished,\n>  \t\t\t\t   &cb_data,\n>  \t\t\t\t   \"hook\",\n> diff --git a/hook.h b/hook.h\n> index ecf0228a46..4ff9999b04 100644\n> --- a/hook.h\n> +++ b/hook.h\n> @@ -78,6 +78,14 @@ struct run_hooks_opt\n>  \tfeed_pipe_fn feed_pipe;\n>  \tvoid *feed_pipe_ctx;\n>  \n> +\t/*\n> +\t * Populate this to capture output and prevent it from being printed to\n> +\t * stderr. This will be passed directly through to\n> +\t * run_command:run_parallel_processes(). See t/helper/test-run-command.c\n> +\t * for an example.\n> +\t */\n> +\tconsume_sideband_fn consume_sideband;\n> +\n>  \t/* Number of threads to parallelize across */\n>  \tint jobs;\n\n...but this scaffolding is rather trivial.\n"},{"id":"418948","messageId":"871rckhgy0.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-33-emilyshaffer@google.com","subject":"Re: [PATCH v8 32/37] post-update: use hook.h library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T09:14:31Z","receivedAt":"2021-03-12T09:16:59Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> By using run_hooks() instead of run_hook_le(), 'post-update' hooks can\n> be specified in the config as well as the hookdir.\n\nLooking ahead in the series no tests for this, seems like a good thing\nto have some at least trivial tests for each hook and their config\ninvocation.\n"},{"id":"418949","messageId":"87y2esg22j.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-36-emilyshaffer@google.com","subject":"Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T09:21:08Z","receivedAt":"2021-03-12T09:21:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> By using the new 'git hook run' subcommand to run 'sendemail-validate',\n> we can reduce the boilerplate needed to run this hook in perl. Using\n> config-based hooks also allows us to run 'sendemail-validate' hooks that\n> were configured globally when running 'git send-email' from outside of a\n> Git directory, alongside other benefits like multihooks and\n> parallelization.\n>\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  git-send-email.perl   | 21 ++++-----------------\n>  t/t9001-send-email.sh | 11 +----------\n>  2 files changed, 5 insertions(+), 27 deletions(-)\n>\n> diff --git a/git-send-email.perl b/git-send-email.perl\n> index 1f425c0809..73e1e0b51a 100755\n> --- a/git-send-email.perl\n> +++ b/git-send-email.perl\n> @@ -1941,23 +1941,10 @@ sub unique_email_list {\n>  sub validate_patch {\n>  \tmy ($fn, $xfer_encoding) = @_;\n>  \n> -\tif ($repo) {\n> -\t\tmy $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),\n> -\t\t\t\t\t    'sendemail-validate');\n> -\t\tmy $hook_error;\n> -\t\tif (-x $validate_hook) {\n> -\t\t\tmy $target = abs_path($fn);\n> -\t\t\t# The hook needs a correct cwd and GIT_DIR.\n> -\t\t\tmy $cwd_save = cwd();\n> -\t\t\tchdir($repo->wc_path() or $repo->repo_path())\n> -\t\t\t\tor die(\"chdir: $!\");\n> -\t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n> -\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n> -\t\t\t\tif system($validate_hook, $target);\n> -\t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n> -\t\t}\n> -\t\treturn $hook_error if $hook_error;\n> -\t}\n> +\tmy $target = abs_path($fn);\n> +\treturn \"rejected by sendemail-validate hook\"\n> +\t\tif system((\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-a\",\n> +\t\t\t\t$target));\n\nI see it's just moving code around, but since we're touching this:\n\nThis conflates the hook exit code with a general failure to invoke it,\nPerl's system().\n\nNot a big deal in this case, but there's two other existing system()\ninvocations which use the right blurb for it:\n\n\n\tsystem('sh', '-c', $editor.' \"$@\"', $editor, $_);\n\tif (($? & 127) || ($? >> 8)) {\n\t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n\t}\n\nMakes sense to do something similar here for consistency. See \"perldoc\n-f system\" for an example.\n\n>  \n>  \t# Any long lines will be automatically fixed if we use a suitable transfer\n>  \t# encoding.\n> diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\n> index 4eee9c3dcb..456b471c5c 100755\n> --- a/t/t9001-send-email.sh\n> +++ b/t/t9001-send-email.sh\n> @@ -2101,16 +2101,7 @@ test_expect_success $PREREQ 'invoke hook' '\n>  \tmkdir -p .git/hooks &&\n>  \n>  \twrite_script .git/hooks/sendemail-validate <<-\\EOF &&\n> -\t# test that we have the correct environment variable, pwd, and\n> -\t# argument\n> -\tcase \"$GIT_DIR\" in\n> -\t*.git)\n> -\t\ttrue\n> -\t\t;;\n> -\t*)\n> -\t\tfalse\n> -\t\t;;\n> -\tesac &&\n> +\t# test that we have the correct argument\n\nThis and getting rid of these Perl/Python/whatever special cases is very\nnice.\n"},{"id":"418950","messageId":"87v99wg1xw.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-37-emilyshaffer@google.com","subject":"Re: [PATCH v8 36/37] run-command: stop thinking about hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T09:23:55Z","receivedAt":"2021-03-12T09:24:39Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> hook.h has replaced all run-command.h hook-related functionality.\n> run-command.h:run_hooks_le/ve and find_hook are no longer used anywhere\n> in the codebase. So, let's delete the dead code - or, in the one case\n> where it's still needed, move it to an internal function in hook.c.\n\nSimilar to other comments about squashing, I think just having this\nhappen incrementally as we remove whatever is the last user of the\nfunction would be better.\n\nE.g. find_hook() is last used in one commit, run_hook*() in another...\n"},{"id":"418951","messageId":"87sg50g1nz.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-38-emilyshaffer@google.com","subject":"Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T09:29:52Z","receivedAt":"2021-03-12T09:30:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> By showing the list of all hooks in 'git help hook' for users to refer\n> to, 'git help hook' becomes a one-stop shop for hook authorship. Since\n> some may still have muscle memory for 'git help githooks', though,\n> reference the 'git hook' commands and otherwise don't remove content.\n\nI think this should at least have something like what my b6a8d09f6d8 (gc\ndocs: include the \"gc.*\" section from \"config\" in \"gc\", 2019-04-07) has\non top, i.e.:\n    \n    diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n    index 4ad31ac360a..5c9af30b43e 100644\n    --- a/Documentation/git-hook.txt\n    +++ b/Documentation/git-hook.txt\n    @@ -150,10 +150,18 @@ message body and cannot be parallelized.\n     \n     CONFIGURATION\n     -------------\n    +\n    +The below documentation is the same as what's found in\n    +linkgit:git-config[1]:\n    +\n     include::config/hook.txt[]\n     \n     HOOKS\n     -----\n    +\n    +The below documentation is the same as what's found in\n    +linkgit:githooks[5]:\n    +\n     include::native-hooks.txt[]\n     \n     GIT\n\nBut I also don't think we should demote githooks(5) as the canonical doc\npage for the hooks themselves.\n\nIf you run this in your terminal:\n\n    man 5 git<TAB>\n\nYou'll get:\n\n    gitattributes         gitignore             gitmailmap            gitrepository-layout  \n    githooks              git-lfs-config        gitmodules            gitweb.conf \n\n(Well, maybe not the lfs-part, but whatever...).\n\nWe should move more in the direction of splitting up our \"file format\"\ndocs from implementation, like the git-hook runner.\n\nI'm somewhat negative on including it at all in git-hook(1). For the\nconfig section it makes sense, and it's consistent with established doc\nconvention.\n\nBut including githooks(5) is around 2/3 of the resulting manpage, I\nthink just a link is better.\n"},{"id":"418952","messageId":"87pn04g0r1.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"Re: [PATCH v8 00/37] config-based hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T09:49:38Z","receivedAt":"2021-03-12T09:50:39Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> Since v7:\n> - Addressed Jonathan Tan's review of part I\n> - Addressed Junio's review of part I and II\n> - Combined parts I and II\n>\n> I think the updates to patch 1 between the rest of the work I've been\n> doing probably have covered Ævar's comments.\n\nA range-diff between iterations of such a large series would be most\nuseful. Do you have a public repo with tags or whatever the different\nversions, for those who'd like an easier way to follow along the\ndiffering versions than scraping the ML archive?\n\nWhile reading this I came up with the following fixup patches on top,\nfor discussion, maybe not something you want as-is:\n\t\n\t Documentation/git-hook.txt |  8 +++++\n\t builtin/bugreport.c        |  8 +++--\n\t builtin/commit.c           |  3 +-\n\t builtin/hook.c             | 79 ++++++++++++++++++++--------------------------\n\t builtin/merge.c            |  3 +-\n\t builtin/receive-pack.c     | 11 +++----\n\t hook.c                     | 21 +++++-------\n\t hook.h                     |  5 +--\n\t refs.c                     |  4 ++-\n\t sequencer.c                |  4 ++-\n\t 10 files changed, 73 insertions(+), 73 deletions(-)\n\t\n\tdiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n\tindex 4ad31ac360a..5c9af30b43e 100644\n\t--- a/Documentation/git-hook.txt\n\t+++ b/Documentation/git-hook.txt\n\t@@ -150,10 +150,18 @@ message body and cannot be parallelized.\n\t \n\t CONFIGURATION\n\t -------------\n\t+\n\t+The below documentation is the same as what's found in\n\t+linkgit:git-config[1]:\n\t+\n\t include::config/hook.txt[]\n\t \n\t HOOKS\n\t -----\n\t+\n\t+The below documentation is the same as what's found in\n\t+linkgit:githooks[5]:\n\t+\n\t include::native-hooks.txt[]\n\t \nNoted in another reply, including it here for completeness.\n\n\t GIT\n\tdiff --git a/builtin/bugreport.c b/builtin/bugreport.c\n\tindex 04467cd1d3a..b64e53fd625 100644\n\t--- a/builtin/bugreport.c\n\t+++ b/builtin/bugreport.c\n\t@@ -81,9 +81,13 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n\t \t\treturn;\n\t \t}\n\t \n\t-\tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n\t-\t\tif (hook_exists(hook[i], HOOKDIR_USE_CONFIG))\n\t+\tfor (i = 0; i < ARRAY_SIZE(hook); i++) {\n\t+\t\tstruct strbuf config;\n\t+\t\tstrbuf_addf(&config, \"hook.%s.config\", hook[i]);\n\t+\t\tif (hook_exists(hook[i], config.buf, HOOKDIR_USE_CONFIG))\n\t \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n\t+\t\tstrbuf_release(&config);\n\t+\t}\n\t }\n\nLess strbuf, see below.\n\t \n\t static const char * const bugreport_usage[] = {\n\tdiff --git a/builtin/commit.c b/builtin/commit.c\n\tindex 31df571f123..fc9f1f5ee58 100644\n\t--- a/builtin/commit.c\n\t+++ b/builtin/commit.c\n\t@@ -984,7 +984,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n\t \t\treturn 0;\n\t \t}\n\t \n\t-\tif (!no_verify && hook_exists(\"pre-commit\", HOOKDIR_USE_CONFIG)) {\n\t+\tif (!no_verify && hook_exists(\"pre-commit\", \"hook.pre-commit.command\",\n\t+\t\t\t\t      HOOKDIR_USE_CONFIG)) {\n\t \t\t/*\n\t \t\t * Re-read the index as pre-commit hook could have updated it,\n\t \t\t * and write it out as a tree.  We must do this before we invoke\n\n\n..ditto.\n\n\tdiff --git a/builtin/hook.c b/builtin/hook.c\n\tindex b4f4adb1dea..d0b56ee47f8 100644\n\t--- a/builtin/hook.c\n\t+++ b/builtin/hook.c\n\t@@ -18,8 +18,6 @@ static enum hookdir_opt should_run_hookdir;\n\t static int list(int argc, const char **argv, const char *prefix)\n\t {\n\t \tstruct list_head *head, *pos;\n\t-\tstruct strbuf hookname = STRBUF_INIT;\n\t-\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n\t \n\t \tstruct option list_options[] = {\n\t \t\tOPT_END(),\n\t@@ -33,67 +31,60 @@ static int list(int argc, const char **argv, const char *prefix)\n\t \t\t\t      builtin_hook_usage, list_options);\n\t \t}\n\t \n\t-\tstrbuf_addstr(&hookname, argv[0]);\n\t-\n\t-\thead = hook_list(&hookname);\n\t+\thead = hook_list(argv[0]);\n\t \n\t \tif (list_empty(head)) {\n\t \t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n\t-\t\t       hookname.buf);\n\t-\t\tstrbuf_release(&hookname);\n\t+\t\t       argv[0]);\n\t \t\treturn 0;\n\t \t}\n\t \n\t-\tswitch (should_run_hookdir) {\n\t-\t\tcase HOOKDIR_NO:\n\t-\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will not run)\"));\n\t-\t\t\tbreak;\n\t-\t\tcase HOOKDIR_ERROR:\n\t-\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will error and not run)\"));\n\t-\t\t\tbreak;\n\t-\t\tcase HOOKDIR_INTERACTIVE:\n\t-\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will prompt)\"));\n\t-\t\t\tbreak;\n\t-\t\tcase HOOKDIR_WARN:\n\t-\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will warn but run)\"));\n\t-\t\t\tbreak;\n\t-\t\tcase HOOKDIR_YES:\n\t-\t\t/*\n\t-\t\t * The default behavior should agree with\n\t-\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n\t-\t\t * do the default behavior.\n\t-\t\t */\n\t-\t\tcase HOOKDIR_UNKNOWN:\n\t-\t\tdefault:\n\t-\t\t\tbreak;\n\t-\t}\n\t-\n\t \tlist_for_each(pos, head) {\n\t \t\tstruct hook *item = list_entry(pos, struct hook, list);\n\t \t\titem = list_entry(pos, struct hook, list);\n\t \t\tif (item) {\n\t-\t\t\t/* Don't translate 'hookdir' - it matches the config */\n\t-\t\t\tprintf(\"%s: %s%s\\n\",\n\t-\t\t\t       (item->from_hookdir\n\t+\t\t\tconst char *scope = item->from_hookdir\n\t \t\t\t\t? \"hookdir\"\n\t-\t\t\t\t: config_scope_name(item->origin)),\n\t-\t\t\t       item->command.buf,\n\t-\t\t\t       (item->from_hookdir\n\t-\t\t\t\t? hookdir_annotation.buf\n\t-\t\t\t\t: \"\"));\n\t+\t\t\t\t: config_scope_name(item->origin);\n\t+\t\t\tswitch (should_run_hookdir) {\n\t+\t\t\tcase HOOKDIR_NO:\n\t+\t\t\t\tprintf(_(\"%s: %s (will not run)\\n\"),\n\t+\t\t\t\t       scope, item->command.buf);\n\t+\t\t\t\tbreak;\n\t+\t\t\tcase HOOKDIR_ERROR:\n\t+\t\t\t\tprintf(_(\"%s: %s (will error and not run)\\n\"),\n\t+\t\t\t\t       scope, item->command.buf);\n\t+\t\t\t\tbreak;\n\t+\t\t\tcase HOOKDIR_INTERACTIVE:\n\t+\t\t\t\tprintf(_(\"%s: %s (will prompt)\\n\"),\n\t+\t\t\t\t       scope, item->command.buf);\n\t+\t\t\t\tbreak;\n\t+\t\t\tcase HOOKDIR_WARN:\n\t+\t\t\t\tprintf(_(\"%s: %s (will warn but run)\\n\"),\n\t+\t\t\t\t       scope, item->command.buf);\n\t+\t\t\t\tbreak;\n\t+\t\t\tcase HOOKDIR_YES:\n\t+\t\t\t\t/*\n\t+\t\t\t\t * The default behavior should agree with\n\t+\t\t\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n\t+\t\t\t\t * do the default behavior.\n\t+\t\t\t\t */\n\t+\t\t\tcase HOOKDIR_UNKNOWN:\n\t+\t\t\tdefault:\n\t+\t\t\t\tprintf(_(\"%s: %s\\n\"),\n\t+\t\t\t\t       scope, item->command.buf);\n\t+\t\t\t\tbreak;\n\t+\t\t\t}\n\t \t\t}\n\t \t}\n\t \n\t \tclear_hook_list(head);\n\t-\tstrbuf_release(&hookdir_annotation);\n\t-\tstrbuf_release(&hookname);\n\t \n\t \treturn 0;\n\t }\n\nI think this is better to avoid i18n lego, as noted in another reply\n(but I didn't include the patch).\n\nMore on strbuf below:\n\t \n\t static int run(int argc, const char **argv, const char *prefix)\n\t {\n\t-\tstruct strbuf hookname = STRBUF_INIT;\n\t \tstruct run_hooks_opt opt;\n\t \tint rc = 0;\n\t \n\t@@ -118,12 +109,10 @@ static int run(int argc, const char **argv, const char *prefix)\n\t \t\tusage_msg_opt(_(\"You must specify a hook event to run.\"),\n\t \t\t\t      builtin_hook_usage, run_options);\n\t \n\t-\tstrbuf_addstr(&hookname, argv[0]);\n\t \topt.run_hookdir = should_run_hookdir;\n\t \n\t-\trc = run_hooks(hookname.buf, &opt);\n\t+\trc = run_hooks(argv[0], &opt);\n\t \n\t-\tstrbuf_release(&hookname);\n\t \trun_hooks_opt_clear(&opt);\n\t \n\t \treturn rc;\n\tdiff --git a/builtin/merge.c b/builtin/merge.c\n\tindex 3a2af257a6b..df4ff72fbc7 100644\n\t--- a/builtin/merge.c\n\t+++ b/builtin/merge.c\n\t@@ -848,7 +848,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n\t \t * and write it out as a tree.  We must do this before we invoke\n\t \t * the editor and after we invoke run_status above.\n\t \t */\n\t-\tif (hook_exists(\"pre-merge-commit\", HOOKDIR_USE_CONFIG))\n\t+\tif (hook_exists(\"pre-merge-commit\", \"hook.pre-merge-commit.command\",\n\t+\t\t\tHOOKDIR_USE_CONFIG))\n\t \t\tdiscard_cache();\n\t \tread_cache_from(index_file);\n\t \tstrbuf_addbuf(&msg, &merge_msg);\n\tdiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n\tindex eaedeeb1e8b..a76069ea592 100644\n\t--- a/builtin/receive-pack.c\n\t+++ b/builtin/receive-pack.c\n\t@@ -1123,12 +1123,10 @@ static int run_proc_receive_hook(struct command *commands,\n\t \tint version = 0;\n\t \tint code;\n\t \n\t-\tstruct strbuf hookname = STRBUF_INIT;\n\t \tstruct hook *proc_receive = NULL;\n\t \tstruct list_head *pos, *hooks;\n\t \n\t-\tstrbuf_addstr(&hookname, \"proc-receive\");\n\t-\thooks = hook_list(&hookname);\n\t+\thooks = hook_list(\"proc-receive\");\n\t \n\t \tlist_for_each(pos, hooks) {\n\t \t\tif (proc_receive) {\n\t@@ -1460,8 +1458,6 @@ static const char *push_to_deploy(unsigned char *sha1,\n\t \treturn NULL;\n\t }\n\t \n\t-static const char *push_to_checkout_hook = \"push-to-checkout\";\n\t-\n\t static const char *push_to_checkout(unsigned char *hash,\n\t \t\t\t\t    struct strvec *env,\n\t \t\t\t\t    const char *work_tree)\n\t@@ -1472,7 +1468,7 @@ static const char *push_to_checkout(unsigned char *hash,\n\t \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n\t \tstrvec_pushv(&opt.env, env->v);\n\t \tstrvec_push(&opt.args, hash_to_hex(hash));\n\t-\tif (run_hooks(push_to_checkout_hook, &opt)) {\n\t+\tif (run_hooks(\"push-to-checkout\", &opt)) {\n\t \t\trun_hooks_opt_clear(&opt);\n\t \t\treturn \"push-to-checkout hook declined\";\n\t \t} else {\n\t@@ -1502,7 +1498,8 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n\t \n\t \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n\t \n\t-\tif (!hook_exists(push_to_checkout_hook, HOOKDIR_USE_CONFIG))\n\t+\tif (!hook_exists(\"push-to-checkout\", \"hook.push-to-checkout.command\",\n\t+\t\t\t HOOKDIR_USE_CONFIG))\n\t \t\tretval = push_to_deploy(sha1, &env, work_tree);\n\t \telse\n\t \t\tretval = push_to_checkout(sha1, &env, work_tree);\n\tdiff --git a/hook.c b/hook.c\n\tindex 7f6f3b9a616..49c3861ce00 100644\n\t--- a/hook.c\n\t+++ b/hook.c\n\t@@ -247,7 +247,7 @@ static const char *find_legacy_hook(const char *name)\n\t }\n\t \n\t \n\t-struct list_head* hook_list(const struct strbuf* hookname)\n\t+struct list_head* hook_list(const char *hookname)\n\t {\n\t \tstruct strbuf hook_key = STRBUF_INIT;\n\t \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n\t@@ -256,14 +256,14 @@ struct list_head* hook_list(const struct strbuf* hookname)\n\t \tINIT_LIST_HEAD(hook_head);\n\t \n\t \tif (!hookname)\n\t-\t\treturn NULL;\n\t+\t\tBUG(\"???\");;\n\t \n\t-\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname->buf);\n\t+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n\t \n\t \tgit_config(hook_config_lookup, &cb_data);\n\t \n\t \tif (have_git_dir()) {\n\t-\t\tconst char *legacy_hook_path = find_legacy_hook(hookname->buf);\n\t+\t\tconst char *legacy_hook_path = find_legacy_hook(hookname);\n\t \n\t \t\t/* Unconditionally add legacy hook, but annotate it. */\n\t \t\tif (legacy_hook_path) {\n\t@@ -300,10 +300,10 @@ void run_hooks_opt_init_async(struct run_hooks_opt *o)\n\t \to->jobs = configured_hook_jobs();\n\t }\n\t \n\t-int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n\t+int hook_exists(const char *hookname, const char *hook_config,\n\t+\t\tenum hookdir_opt should_run_hookdir)\n\t {\n\t \tconst char *value = NULL; /* throwaway */\n\t-\tstruct strbuf hook_key = STRBUF_INIT;\n\t \tint could_run_hookdir;\n\t \n\t \tif (should_run_hookdir == HOOKDIR_USE_CONFIG)\n\t@@ -314,9 +314,7 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n\t \t\t\t\tshould_run_hookdir == HOOKDIR_YES)\n\t \t\t\t\t&& !!find_legacy_hook(hookname);\n\t \n\t-\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n\t-\n\t-\treturn (!git_config_get_value(hook_key.buf, &value)) || could_run_hookdir;\n\t+\treturn (!git_config_get_value(hook_config, &value)) || could_run_hookdir;\n\t }\n\t \n\t void run_hooks_opt_clear(struct run_hooks_opt *o)\n\t@@ -438,7 +436,6 @@ static int notify_hook_finished(int result,\n\t \n\t int run_hooks(const char *hookname, struct run_hooks_opt *options)\n\t {\n\t-\tstruct strbuf hookname_str = STRBUF_INIT;\n\t \tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n\t \tstruct hook_cb_data cb_data = { 0, NULL, NULL, options };\n\t \n\t@@ -448,9 +445,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n\t \tif (options->path_to_stdin && options->feed_pipe)\n\t \t\tBUG(\"choose only one method to populate stdin\");\n\t \n\t-\tstrbuf_addstr(&hookname_str, hookname);\n\t-\n\t-\tto_run = hook_list(&hookname_str);\n\t+\tto_run = hook_list(hookname);\n\t \n\t \tlist_for_each_safe(pos, tmp, to_run) {\n\t \t\tstruct hook *hook = list_entry(pos, struct hook, list);\n\tdiff --git a/hook.h b/hook.h\n\tindex 4ff9999b049..bfbbf36882d 100644\n\t--- a/hook.h\n\t+++ b/hook.h\n\t@@ -26,7 +26,7 @@ struct hook {\n\t  * Provides a linked list of 'struct hook' detailing commands which should run\n\t  * in response to the 'hookname' event, in execution order.\n\t  */\n\t-struct list_head* hook_list(const struct strbuf *hookname);\n\t+struct list_head* hook_list(const char *hookname);\n\t \n\t enum hookdir_opt\n\t {\n\t@@ -123,7 +123,8 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);\n\t  * Like with run_hooks, if you take a --run-hookdir flag, reflect that\n\t  * user-specified behavior here instead.\n\t  */\n\t-int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);\n\t+int hook_exists(const char *hookname, const char *hook_config,\n\t+\t\tenum hookdir_opt should_run_hookdir);\n\t \n\t /*\n\t  * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n\tdiff --git a/refs.c b/refs.c\n\tindex 334fdd9103c..f01995fe64f 100644\n\t--- a/refs.c\n\t+++ b/refs.c\n\t@@ -1966,7 +1966,9 @@ static int run_transaction_hook(struct ref_transaction *transaction,\n\t \n\t \trun_hooks_opt_init_async(&opt);\n\t \n\t-\tif (!hook_exists(\"reference-transaction\", HOOKDIR_USE_CONFIG))\n\t+\tif (!hook_exists(\"reference-transaction\",\n\t+\t\t\t \"hook.reference-transaction.command\",\n\t+\t\t\t HOOKDIR_USE_CONFIG))\n\t \t\treturn ret;\n\t \n\t \tstrvec_push(&opt.args, state);\n\tdiff --git a/sequencer.c b/sequencer.c\n\tindex 34ff275f0d1..52c067c1688 100644\n\t--- a/sequencer.c\n\t+++ b/sequencer.c\n\t@@ -1436,7 +1436,9 @@ static int try_to_commit(struct repository *r,\n\t \t\t}\n\t \t}\n\t \n\t-\tif (hook_exists(\"prepare-commit-msg\", HOOKDIR_USE_CONFIG)) {\n\t+\tif (hook_exists(\"prepare-commit-msg\",\n\t+\t\t\t\"hook.prepare-commit-msg.command\",\n\t+\t\t\tHOOKDIR_USE_CONFIG)) {\n\t \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n\t \t\tif (res)\n\t \t\t\tgoto out;\n\nThere was another reply (from JT I believe, but didn't go back and look\nit up) about the over use of strbuf.\n\nI tend to agree, as much as I love the API it's really not better to\nwrite C with it if all you need is a const char* that's never modified,\nparticularly if you get it from elsewhere.\n\nSo it's really not meant for or good for \"everything we need a const\nchar*\", but to avoid verbose realloc() dances all over the place, and\nfor things like getline() loops without a hardcoded buffer size.\n\nE.g. in the first hunk here we're creating a strbuf just to copy argv[0]\nto it, and then throwing it away, let's just pass down argv[0].\n\nFor hook_exists I think just having the code more grep-able and having\nthe config value inline is better, but I admit that's a matter of taste.\n\nI didn't try to find all such strbuf() occurrences, anyway, in the\noverall scheme of things it's a relatively small nit.\n\nI'm hoping to do some deeper diving into this series, in particular the\nparallelism, but just sending the shallow-ish comments I have for now.\n\nThanks for working on this!\n"},{"id":"418954","messageId":"xmqqr1kk8yen.fsf@gitster.g","threadId":"55286","inReplyTo":"20210311021037.3001235-24-emilyshaffer@google.com","subject":"Re: [PATCH v8 23/37] read-cache: convert post-index-change hook to use config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-12T10:22:08Z","receivedAt":"2021-03-12T10:27:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> @@ -3070,6 +3071,8 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n>  \t\t\t\t unsigned flags)\n>  {\n>  \tint ret;\n> +\tstruct run_hooks_opt hook_opt;\n> +\trun_hooks_opt_init_async(&hook_opt);\n>  \n\nNit. blank line between the last of decls and the first stmt (many\nidentical nits exist everywhere in this series).\n\n>  \t/*\n>  \t * TODO trace2: replace \"the_repository\" with the actual repo instance\n> @@ -3088,9 +3091,13 @@ static int do_write_locked_index(s\n>  \telse\n>  \t\tret = close_lock_file_gently(lock);\n>  \n> -\trun_hook_le(NULL, \"post-index-change\",\n> -\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n> -\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n> +\tstrvec_pushl(&hook_opt.args,\n> +\t\t     istate->updated_workdir ? \"1\" : \"0\",\n> +\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n> +\t\t     NULL);\n> +\trun_hooks(\"post-index-change\", &hook_opt);\n> +\trun_hooks_opt_clear(&hook_opt);\n\nThere is one early return before the precontext of this hunk that\nbypasses this opt_clear() call.  It is before any member of hook_opt\nstructure that was opt_init()'ed gets touched, so with the current\ncode, there is no leak, but it probably is laying a landmine for the\nfuture, where opt_init() may allocate some resource to its member,\nwith the expectation that all users of the API would call\nopt_clear() to release.  Or the caller of the API (like this one) may\nstart mucking with the opt structure before the existing early return,\nat which point the current assumption that it is safe to return from\nthat point without opt_clear() would be broken.\n\nI saw that there are other early returns in the series that are safe\nright now but may become unsafe when the API implementation gets\nextended that way.  If it does not involve too much code churning,\nwe may want to restructure the code to make these early returns into\n\"goto\"s that jump to a single exit point, so that we can always\nmatch opt_init() with opt_clear(), like the structure of the\nexisting code allowed cmd_rebase() to use the hooks API cleanly in\n[v8 22/37].\n\nThanks.\n"},{"id":"418953","messageId":"xmqqk0qc8yec.fsf@gitster.g","threadId":"55286","inReplyTo":"20210311021037.3001235-9-emilyshaffer@google.com","subject":"Re: [PATCH v8 08/37] hook: add 'run' subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-12T10:22:19Z","receivedAt":"2021-03-12T10:27:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> +static int run(int argc, const char **argv, const char *prefix)\n> +{\n> +\tstruct strbuf hookname = STRBUF_INIT;\n> +\tstruct run_hooks_opt opt;\n> +\tint rc = 0;\n> +\n> +\tstruct option run_options[] = {\n> +\t\tOPT_STRVEC('e', \"env\", &opt.env, N_(\"var\"),\n> +\t\t\t   N_(\"environment variables for hook to use\")),\n> +\t\tOPT_STRVEC('a', \"arg\", &opt.args, N_(\"args\"),\n> +\t\t\t   N_(\"argument to pass to hook\")),\n> +\t\tOPT_END(),\n> +\t};\n> +\n> +\trun_hooks_opt_init(&opt);\n> +\n> +\targc = parse_options(argc, argv, prefix, run_options,\n> +\t\t\t     builtin_hook_usage, 0);\n> +\n> +\tif (argc < 1)\n> +\t\tusage_msg_opt(_(\"You must specify a hook event to run.\"),\n> +\t\t\t      builtin_hook_usage, run_options);\n> +\n> +\tstrbuf_addstr(&hookname, argv[0]);\n> +\topt.run_hookdir = should_run_hookdir;\n> +\n> +\trc = run_hooks(hookname.buf, &opt);\n> +\n> +\tstrbuf_release(&hookname);\n> +\trun_hooks_opt_clear(&opt);\n> +\n> +\treturn rc;\n> +}\n\nThis looks like a small and clean example that is good for people to\nemulate when using the new run-hooks API.  You opt_init(), futz with\nthe its fields, call run_hooks(), and finally opt_clear() to release\nthe resources.\n"},{"id":"418955","messageId":"xmqqczw48ye2.fsf@gitster.g","threadId":"55286","inReplyTo":"20210311021037.3001235-11-emilyshaffer@google.com","subject":"Re: [PATCH v8 10/37] hook: support passing stdin to hooks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-12T10:22:29Z","receivedAt":"2021-03-12T10:27:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> diff --git a/hook.c b/hook.c\n> index 118931f273..f906e8c61c 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -240,6 +240,7 @@ void run_hooks_opt_init(struct run_hooks_opt *o)\n>  {\n>  \tstrvec_init(&o->env);\n>  \tstrvec_init(&o->args);\n> +\to->path_to_stdin = NULL;\n>  \to->run_hookdir = configured_hookdir_opt();\n>  }\n\nThe new member is initialized to NULL, and presumably the user of\nthe API would point an existing string with it.  Since there is no\nfree() in opt_clear() introduced by this patch, the member is\nobviously a pointer to a borrowed piece of memory.\n\n> diff --git a/hook.h b/hook.h\n> index 0df785add5..2314ec5962 100644\n> --- a/hook.h\n> +++ b/hook.h\n> @@ -52,6 +52,9 @@ struct run_hooks_opt\n>  \t * to be overridden if the user can override it at the command line.\n>  \t */\n>  \tenum hookdir_opt run_hookdir;\n> +\n> +\t/* Path to file which should be piped to stdin for each hook */\n> +\tconst char *path_to_stdin;\n>  };\n\nAnd we mark the fact that hook subsystem does not own it by making\nit \"const char *\".  Looks quite consistent.  Good.\n"},{"id":"418956","messageId":"xmqq5z1w8ydq.fsf@gitster.g","threadId":"55286","inReplyTo":"20210311021037.3001235-19-emilyshaffer@google.com","subject":"Re: [PATCH v8 18/37] commit: use config-based hooks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-12T10:22:41Z","receivedAt":"2021-03-12T10:27:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> -int run_commit_hook(int editor_is_used, const char *index_file,\n> +int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,\n>  \t\t    const char *name, ...)\n>  {\n> -\tstruct strvec hook_env = STRVEC_INIT;\n> +\tstruct run_hooks_opt opt;\n>  \tva_list args;\n> +\tconst char *arg;\n>  \tint ret;\n>  \n> -\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n> +\trun_hooks_opt_init_sync(&opt);\n> +\n> +\tif (parallelize)\n> +\t\topt.jobs = configured_hook_jobs();\n> +\n> +\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n>  \n>  \t/*\n>  \t * Let the hook know that no editor will be launched.\n>  \t */\n>  \tif (!editor_is_used)\n> -\t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n> +\t\tstrvec_push(&opt.env, \"GIT_EDITOR=:\");\n>  \n>  \tva_start(args, name);\n> -\tret = run_hook_ve(hook_env.v, name, args);\n> +\twhile ((arg = va_arg(args, const char *)))\n> +\t\tstrvec_push(&opt.args, arg);\n>  \tva_end(args);\n> -\tstrvec_clear(&hook_env);\n> +\n> +\tret = run_hooks(name, &opt);\n> +\trun_hooks_opt_clear(&opt);\n>  \n>  \treturn ret;\n>  }\n\nThis follows the textbook pattern established earlier and\ndemonstrated in [v8 08/37].  opt_init() to initialize, populate its\nmembers, call run_hooks() and finally opt_clear().\n\nQuite nicely demonstrated.\n\n"},{"id":"418957","messageId":"xmqqy2es7jro.fsf@gitster.g","threadId":"55286","inReplyTo":"20210311021037.3001235-20-emilyshaffer@google.com","subject":"Re: [PATCH v8 19/37] am: convert applypatch hooks to use config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-12T10:23:39Z","receivedAt":"2021-03-12T10:27:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> @@ -1558,8 +1563,10 @@ static void do_commit(const struct am_state *state)\n>  \tstruct commit_list *parents = NULL;\n>  \tconst char *reflog_msg, *author, *committer = NULL;\n>  \tstruct strbuf sb = STRBUF_INIT;\n> +\tstruct run_hooks_opt hook_opt;\n> +\trun_hooks_opt_init_async(&hook_opt);\n>  \n> -\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n> +\tif (run_hooks(\"pre-applypatch\", &hook_opt))\n>  \t\texit(1);\n>  \n>  \tif (write_cache_as_tree(&tree, 0, NULL))\n> @@ -1611,8 +1618,9 @@ static void do_commit(const struct am_state *state)\n>  \t\tfclose(fp);\n>  \t}\n>  \n> -\trun_hook_le(NULL, \"post-applypatch\", NULL);\n> +\trun_hooks(\"post-applypatch\", &hook_opt);\n>  \n> +\trun_hooks_opt_clear(&hook_opt);\n>  \tstrbuf_release(&sb);\n>  }\n\nThis one does opt_init(), run_hooks(), and another run_hooks() and\nthen opt_clear().  If run_hooks() is a read-only operation on the\nhook_opt, then that would be alright, but it just smells iffy that\nit is not done as two separate opt_init(), run_hooks(), opt_clear()\nsequences for two separate run_hooks() invocations.  The same worry\nabout future safety I meantioned elsewhere in the series also\napplies.\n\nThanks.\n\n\n"},{"id":"418958","messageId":"xmqqr1kk7jq7.fsf@gitster.g","threadId":"55286","inReplyTo":"20210311021037.3001235-23-emilyshaffer@google.com","subject":"Re: [PATCH v8 22/37] rebase: teach pre-rebase to use hook.h","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-12T10:24:32Z","receivedAt":"2021-03-12T10:27:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> @@ -1318,6 +1319,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n>  \tchar *squash_onto_name = NULL;\n>  \tint reschedule_failed_exec = -1;\n>  \tint allow_preemptive_ff = 1;\n> +\tstruct run_hooks_opt hook_opt;\n>  \tstruct option builtin_rebase_options[] = {\n>  \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n>  \t\t\t   N_(\"revision\"),\n> @@ -1431,6 +1433,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n>  \t};\n>  \tint i;\n>  \n> +\trun_hooks_opt_init_async(&hook_opt);\n> +\n>  \tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n>  \t\tusage_with_options(builtin_rebase_usage,\n>  \t\t\t\t   builtin_rebase_options);\n> @@ -2032,9 +2036,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n>  \t}\n>  \n>  \t/* If a hook exists, give it a chance to interrupt*/\n> +\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n>  \tif (!ok_to_skip_pre_rebase &&\n> -\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n> -\t\t\targc ? argv[0] : NULL, NULL))\n> +\t    run_hooks(\"pre-rebase\", &hook_opt))\n>  \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n\nThis may needlessly populate hook_opt.args even when run_hooks() is\nnot triggered, but that probably is OK.  Except for a place or two\nwhere we call die(), the exit path from this function after this\npoint all eventually passes ...\n\n>  \tif (options.flags & REBASE_DIFFSTAT) {\n> @@ -2114,6 +2118,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n>  \tret = !!run_specific_rebase(&options, action);\n>  \n>  cleanup:\n\n... this label, so everybody calls opt_clear() at the end, which is\ngood.\n\n> +\trun_hooks_opt_clear(&hook_opt);\n>  \tstrbuf_release(&buf);\n>  \tstrbuf_release(&revisions);\n>  \tfree(options.head_name);\n"},{"id":"418959","messageId":"xmqqk0qc7jpy.fsf@gitster.g","threadId":"55286","inReplyTo":"20210311021037.3001235-25-emilyshaffer@google.com","subject":"Re: [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-12T10:24:41Z","receivedAt":"2021-03-12T10:27:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> @@ -1435,12 +1436,19 @@ static const char *push_to_checkout(unsigned char *hash,\n>  \t\t\t\t    struct strvec *env,\n>  \t\t\t\t    const char *work_tree)\n>  {\n> +\tstruct run_hooks_opt opt;\n> +\trun_hooks_opt_init_sync(&opt);\n> +\n>  \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n> -\tif (run_hook_le(env->v, push_to_checkout_hook,\n> -\t\t\thash_to_hex(hash), NULL))\n> +\tstrvec_pushv(&opt.env, env->v);\n> +\tstrvec_push(&opt.args, hash_to_hex(hash));\n> +\tif (run_hooks(push_to_checkout_hook, &opt)) {\n> +\t\trun_hooks_opt_clear(&opt);\n>  \t\treturn \"push-to-checkout hook declined\";\n> -\telse\n> +\t} else {\n> +\t\trun_hooks_opt_clear(&opt);\n>  \t\treturn NULL;\n> +\t}\n>  }\n\nOK, we opt_init(), futz with opt, call run_hooks() and opt_clear()\nregardless of the outcome from run_hooks().  Narrow-sighted me\nwonders if it makes the use of the API easier if run_hooks() did the\nopt_clear() before it returns, but I haven't yet seen enough use at\nthis point to judge.\n\nThanks.\n"},{"id":"418962","messageId":"87mtv8fww3.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"Re: [PATCH v8 00/37] config-based hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-12T11:13:00Z","receivedAt":"2021-03-12T11:14:07Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Mar 11 2021, Emily Shaffer wrote:\n\n> Since v7:\n> - Addressed Jonathan Tan's review of part I\n> - Addressed Junio's review of part I and II\n> - Combined parts I and II\n>\n\nComments on the overall design / goals (I don't just have strbuf nits):\n\nI think I mentioned this in earlier rounds, but I'm still very skeptical\nof the need for a \"git hook\" command for anything except the \"run\" case\n(which is very useful).\n\nSo I tried patching it with this on top:\n\t\n\tdiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n\tindex 4f66bb35cf8..eb48da1dcf0 100644\n\t--- a/Documentation/config/hook.txt\n\t+++ b/Documentation/config/hook.txt\n\t@@ -1,20 +1,17 @@\n\t-hook.<command>.command::\n\t-\tA command to execute during the <command> hook event. This can be an\n\t-\texecutable on your device, a oneliner for your shell, or the name of a\n\t-\thookcmd. See linkgit:git-hook[1].\n\t-\n\t-hookcmd.<name>.command::\n\t-\tA command to execute during a hook for which <name> has been specified\n\t-\tas a command. This can be an executable on your device or a oneliner for\n\t-\tyour shell. See linkgit:git-hook[1].\n\t-\n\t-hookcmd.<name>.skip::\n\t-\tSpecify this boolean to remove a command from earlier in the execution\n\t-\torder. Useful if you want to make a single repo an exception to hook\n\t-\tconfigured at the system or global scope. If there is no hookcmd\n\t-\tspecified for the command you want to skip, you can use the value of\n\t-\t`hook.<command>.command` as <name> as a shortcut. The \"skip\" setting\n\t-\tmust be specified after the \"hook.<command>.command\" to have an effect.\n\t+hook.<name>.event::\n\t+hook.<name>.command::\n\t+\tA command to execute during a given hook event for which\n\t+\t<name> has been specified This can be an executable on your\n\t+\tdevice or a oneliner for your shell. See linkgit:git-hook[1].\n\t++\n\t+As a convention setting this to the string `true` will clobber and\n\t+omit a command from earlier in the execution order. Similarly to the\n\t+\"cat\" special-case for `pager.<cmd>` we won't execute the hook at all\n\t+in that case.\n\t++\n\t+To have a single hook handle multiple types of events (such as\n\t+`pre-receive` and `post-receive`) specify `hook.<name>.event` multiple\n\t+times.\n\t \n\t hook.runHookDir::\n\t \tControls how hooks contained in your hookdir are executed. Can be any of\n\nI didn't finish that WIP patch, but I have yet to see any reason for why\nit wouldn't work.\n\nIn experimenting with it further I tried just adding a \"git config\n--show-hook\" as a convenience alias for \"git config --show-origin\n--show-scope --get-regexp '^hook\\.<name>\\.'\", something like:\n\t\n\tdiff --git a/builtin/config.c b/builtin/config.c\n\tindex 963d65fd3fc..f62356b923a 100644\n\t--- a/builtin/config.c\n\t+++ b/builtin/config.c\n\t@@ -33,6 +33,7 @@ static int end_nul;\n\t static int respect_includes_opt = -1;\n\t static struct config_options config_options;\n\t static int show_origin;\n\t+static int show_hook;\n\t static int show_scope;\n\t \n\t #define ACTION_GET (1<<0)\n\t@@ -159,6 +160,7 @@ static struct option builtin_config_options[] = {\n\t \tOPT_BOOL('z', \"null\", &end_nul, N_(\"terminate values with NUL byte\")),\n\t \tOPT_BOOL(0, \"name-only\", &omit_values, N_(\"show variable names only\")),\n\t \tOPT_BOOL(0, \"includes\", &respect_includes_opt, N_(\"respect include directives on lookup\")),\n\t+\tOPT_BOOL(0, \"show-hook\", &show_hook, N_(\"show configuration for a given hook (convenience alias for --show-origin --show-scope --get-regexp '^hook\\\\.<name>\\\\.')\")),\n\t \tOPT_BOOL(0, \"show-origin\", &show_origin, N_(\"show origin of config (file, standard input, blob, command line)\")),\n\t \tOPT_BOOL(0, \"show-scope\", &show_scope, N_(\"show scope of config (worktree, local, global, system, command)\")),\n\t \tOPT_STRING(0, \"default\", &default_value, N_(\"value\"), N_(\"with --get, use default value when missing entry\")),\n\t@@ -631,6 +633,7 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n\t {\n\t \tint nongit = !startup_info->have_repository;\n\t \tchar *value;\n\t+\tstruct strbuf show_hook_arg = STRBUF_INIT;\n\t \n\t \tgiven_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n\t \n\t@@ -645,6 +648,14 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n\t \t\tusage_builtin_config();\n\t \t}\n\t \n\t+\tif (show_hook) {\n\t+\t\tstrbuf_addf(&show_hook_arg, \"^hook\\\\.%s\\\\.\", argv[0]);\n\t+\t\tactions = ACTION_GET_REGEXP;\n\t+\t\tshow_scope = 1;\n\t+\t\targv[0] = show_hook_arg.buf;\n\t+\t}\n\t+\t\t\n\t+\n\t \tif (nongit) {\n\t \t\tif (use_local_config)\n\t \t\t\tdie(_(\"--local can only be used inside a git repository\"));\n\t@@ -915,5 +926,8 @@ int cmd_config(int argc, const char **argv, const char *prefix)\n\t \t\treturn get_colorbool(argv[0], argc == 2);\n\t \t}\n\t \n\t+\t/* TODO: Memory leak on non-zero return, do we care? */\n\t+\tstrbuf_release(&show_hook_arg);\n\t+\n\t \treturn 0;\n\t }\n\nSo the reason that naïve approach doesn't work is that the current\ndesign has both a hook.<command>.command, *or* a\nhookcmd.<command>.<cfg>. So it can't be just a single --get-regexp, you\nneed to statefully parse it, as indeed your implementation does.\n\nBut this seems like a bad idea to me for at least these reasons I've\nthought of so far:\n\n 1. If we just change the design a bit we can make this a much simpler\n    git-config wrapper, or point to that directly.\n\n 2. You're sticking full paths in the git config key, which is\n    case-insensitive, and a feature of this format is being able to\n    configure/override previously configured hooks.\n\n    So the behavior of this feature depends on git's interaction with\n    the case-sensitivity of filesystems, and not just one fs, any fs\n    we're walking in our various config sources, and where the hook\n    itself lives.\n\n    As recent CVEs have shown that's a big can of worms, particularly\n    for something whose goal is to address the security aspect of\n    running hooks from other config.\n\n    Arguably the case-sensitivity issue is just confusing since we\n    canonicalize it anyway. But once you add in FS path canonicalization\n    it becomes a real big can of worms. See the .gitmodules fsck code.\n\n    Even if it wasn't for that it's relatively nastier to edit/maintain\n    full paths and the appropriate escaping in the double-quoted key in\n    the config file v.s. having it as an optionally quoted value.\n\n 3. We're left with this \"*.command = cmd\", and \"*.skip = true\"\n    special-case syntax. I can't see any reason for why it's needed over\n    simply having \"*.command = true\" clobber earlier hooks as noted in\n    the proposed docs above.\n\n    And that doesn't require any magic to support, just like our\n    existing \"core.pager=cat\" case.\n\n    I mean, I suppose it's magical in that we might otherwise error on\n    non-consumed stdin (do we?), anyway, documenting it as a synonym for\n    \"cat >/dev/null\" would get around that :)\n\n 4. It makes the common case of having the same hooks for N commands\n    needlessly verbose, if you can just support \"type\" (or whatever we\n    should call it) you can add that N times...\n\n 5. At the end of this series we're left with the start of the docs\n    saying:\n\n      You can list and run configured hooks with this command. Later,\n      you will be able to add and modify hooks with this command.\n\n    But those patches have yet to land, and looking at the design\n    document I'm skeptical of that being a good addition v.s. just\n    adding the same thing to \"git config\".\n\n    As just one exmaple; surely \"git config edit <name>\" would need to\n    run around and find config files to edit, then open them in a loop\n    for you, no?\n\n    Which we'd eventually want for \"git config\" in general with an\n    --edit-regexp option or whatever, which brings us (well, at least\n    me) back to \"then let's just add it to git-config?\".\n\n 6. The whole 'git hook' config special-casing doesn't help other\n    commands or the security issue that seemed to have prompted (at\n    least some of) its existence\n\n    In the design doc we mention the \"core.pager = rm -rf /\" case for a\n    .git/config.\n\n    This series doesn't implement, but the design docs note a future\n    want for solving that issue for the hooks.\n\n    To me that's another case where we should just have general config\n    syntax, not something hook-specific, e.g. if I could do this in my\n    ~/.gitconfig:\n\n       ;; We consider 'config.ignore' in reverse order, so e.g setting\n       ;; it in. ~/.gitconfig will ignore any such keys for repo-level\n       ;; config\n       [config \"ignore\"]\n       key = core.pager\n       keyRegexp = \"^hook\\.\"\n\n    We'd address both any hook security concerns, as well as core.pager\n    etc. We could then just have e.g. some syntax sugar of:\n\n       [include]\n       path = built-in://gimme-safe-config\n\n    Which would just be a thin layer of magit to include\n    <path-to-git-prefix>/config-templates/gimme-safe-config or whatever.\n\n    We'd thus address the issue for all config types without\n    hook-specific magic.\n\nAnyway. I'm very willing to be convinced otherwise. I just think that\nfor a first-draft implementation leaving aside 'hook.<command>.command'\nand the whole 'list' thing makes sense.\n\nWe can consider the core code changes relatively separately from any\nfuture aspirations, particularly with a 40-some patch series, and the\nend-state of *this series* IMO not really justifying, that part of the\nimplementation, and thus requiring reviewers to look ahead beyond the\n40-some patches.\n"},{"id":"419015","messageId":"YEv45hrEytdM00iI@google.com","threadId":"55286","inReplyTo":"xmqqim5xba4d.fsf@gitster.g","subject":"Re: [PATCH v8 00/37] config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-12T23:27:34Z","receivedAt":"2021-03-12T23:28:39Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Mar 11, 2021 at 02:26:10PM -0800, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > Since v7:\n> > - Addressed Jonathan Tan's review of part I\n> > - Addressed Junio's review of part I and II\n> > - Combined parts I and II\n> >\n> > I think the updates to patch 1 between the rest of the work I've been\n> > doing probably have covered Ævar's comments.\n> >\n> > More details about per-patch changes found in the notes on each mail (I\n> > hope).\n> >\n> > I know that Junio was talking about merging v7 after Josh Steadmon's\n> > review and I asked him not to - this reroll has those changes from\n> > Jonathan Tan's review that I was wanting to wait for.\n> \n> I picked it up and replaced, not necessarily because it is an urgent\n> thing to do during the pre-release period, but primarily because I\n> wanted to be prepared for any nasty surprises by unmanageable\n> conflicts I may have to face once the current cycle is over.\n> \n> It turns out that it was a bit painful to merge to 'seen' as there\n> are in-flight topics that touch the hooks documentation, and the\n> changes they make must be carried forward to the new file.\n> \n> But it was not too bad.  \n> \n> The merge into 'seen' is 3cdeaeab (Merge branch 'es/config-hooks'\n> into seen, 2021-03-11) as of this writing, and the output of\n> \n>     $ git diff 3cdeaeab3a^:Documentation/githooks.txt \\\n>                3cdeaeab3a:Documentation/native-hooks.txt\n> \n>     (i.e. the version of the file before the merge, where your topic\n>     being merged took material to edit to produce the new \"native-hooks\"\n>     document, is compared with the result)\n> \n> looks reasonable to me, but please double check.\n\nI had a look at that diff (but targeting 6da6893c, which is what I see\nfor \"Merge branch 'es/config-hooks' into seen\" when I fetch from\ngitster/git today) and it looks fine to me, very reasonable. Thanks for\ndoing that.\n\n> \n> Thanks.\n"},{"id":"419016","messageId":"YEv5TJFjtbnP1gG7@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-36-emilyshaffer@google.com","subject":"Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-12T23:29:16Z","receivedAt":"2021-03-12T23:30:14Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Mar 10, 2021 at 06:10:35PM -0800, Emily Shaffer wrote:\n> \n> By using the new 'git hook run' subcommand to run 'sendemail-validate',\n> we can reduce the boilerplate needed to run this hook in perl. Using\n> config-based hooks also allows us to run 'sendemail-validate' hooks that\n> were configured globally when running 'git send-email' from outside of a\n> Git directory, alongside other benefits like multihooks and\n> parallelization.\n> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n\nWithout having had time to look at reviews to this (or the rest of the\nseries) yet - it occurred to me that this hook should be run in series\ninstead. That is, I should invoke 'git hook run' with '-j1'.\n\n>  git-send-email.perl   | 21 ++++-----------------\n>  t/t9001-send-email.sh | 11 +----------\n>  2 files changed, 5 insertions(+), 27 deletions(-)\n> \n> diff --git a/git-send-email.perl b/git-send-email.perl\n> index 1f425c0809..73e1e0b51a 100755\n> --- a/git-send-email.perl\n> +++ b/git-send-email.perl\n> @@ -1941,23 +1941,10 @@ sub unique_email_list {\n>  sub validate_patch {\n>  \tmy ($fn, $xfer_encoding) = @_;\n>  \n> -\tif ($repo) {\n> -\t\tmy $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),\n> -\t\t\t\t\t    'sendemail-validate');\n> -\t\tmy $hook_error;\n> -\t\tif (-x $validate_hook) {\n> -\t\t\tmy $target = abs_path($fn);\n> -\t\t\t# The hook needs a correct cwd and GIT_DIR.\n> -\t\t\tmy $cwd_save = cwd();\n> -\t\t\tchdir($repo->wc_path() or $repo->repo_path())\n> -\t\t\t\tor die(\"chdir: $!\");\n> -\t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n> -\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n> -\t\t\t\tif system($validate_hook, $target);\n> -\t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n> -\t\t}\n> -\t\treturn $hook_error if $hook_error;\n> -\t}\n> +\tmy $target = abs_path($fn);\n> +\treturn \"rejected by sendemail-validate hook\"\n> +\t\tif system((\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-a\",\n> +\t\t\t\t$target));\n>  \n>  \t# Any long lines will be automatically fixed if we use a suitable transfer\n>  \t# encoding.\n> diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\n> index 4eee9c3dcb..456b471c5c 100755\n> --- a/t/t9001-send-email.sh\n> +++ b/t/t9001-send-email.sh\n> @@ -2101,16 +2101,7 @@ test_expect_success $PREREQ 'invoke hook' '\n>  \tmkdir -p .git/hooks &&\n>  \n>  \twrite_script .git/hooks/sendemail-validate <<-\\EOF &&\n> -\t# test that we have the correct environment variable, pwd, and\n> -\t# argument\n> -\tcase \"$GIT_DIR\" in\n> -\t*.git)\n> -\t\ttrue\n> -\t\t;;\n> -\t*)\n> -\t\tfalse\n> -\t\t;;\n> -\tesac &&\n> +\t# test that we have the correct argument\n>  \ttest -f 0001-add-main.patch &&\n>  \tgrep \"add main\" \"$1\"\n>  \tEOF\n> -- \n> 2.31.0.rc2.261.g7f71774620-goog\n> \n"},{"id":"419527","messageId":"YFJNd6an0eGRC4/p@google.com","threadId":"55286","inReplyTo":"87pn04g0r1.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 00/37] config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-17T18:41:59Z","receivedAt":"2021-03-17T18:42:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 10:49:38AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> > Since v7:\n> > - Addressed Jonathan Tan's review of part I\n> > - Addressed Junio's review of part I and II\n> > - Combined parts I and II\n> >\n> > I think the updates to patch 1 between the rest of the work I've been\n> > doing probably have covered Ævar's comments.\n> \n> A range-diff between iterations of such a large series would be most\n> useful. Do you have a public repo with tags or whatever the different\n> versions, for those who'd like an easier way to follow along the\n> differing versions than scraping the ML archive?\n\nI am really embarrassed to say that I don't have the\nbranches/tags/whatever up. I have not succeeded in building that habit\nyet. I'll generate one from my local patches today and send it here.\n\n> \n> While reading this I came up with the following fixup patches on top,\n> for discussion, maybe not something you want as-is:\n\nI was a little bit confused reading this fixup without seeing the rest of your\nreview, so I'll revisit this once I get through what else you wrote.\n\n> \t\n> \t Documentation/git-hook.txt |  8 +++++\n> \t builtin/bugreport.c        |  8 +++--\n> \t builtin/commit.c           |  3 +-\n> \t builtin/hook.c             | 79 ++++++++++++++++++++--------------------------\n> \t builtin/merge.c            |  3 +-\n> \t builtin/receive-pack.c     | 11 +++----\n> \t hook.c                     | 21 +++++-------\n> \t hook.h                     |  5 +--\n> \t refs.c                     |  4 ++-\n> \t sequencer.c                |  4 ++-\n> \t 10 files changed, 73 insertions(+), 73 deletions(-)\n> \t\n> \tdiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> \tindex 4ad31ac360a..5c9af30b43e 100644\n> \t--- a/Documentation/git-hook.txt\n> \t+++ b/Documentation/git-hook.txt\n> \t@@ -150,10 +150,18 @@ message body and cannot be parallelized.\n> \t \n> \t CONFIGURATION\n> \t -------------\n> \t+\n> \t+The below documentation is the same as what's found in\n> \t+linkgit:git-config[1]:\n> \t+\n> \t include::config/hook.txt[]\n> \t \n> \t HOOKS\n> \t -----\n> \t+\n> \t+The below documentation is the same as what's found in\n> \t+linkgit:githooks[5]:\n> \t+\n> \t include::native-hooks.txt[]\n> \t \n> Noted in another reply, including it here for completeness.\n> \n> \t GIT\n> \tdiff --git a/builtin/bugreport.c b/builtin/bugreport.c\n> \tindex 04467cd1d3a..b64e53fd625 100644\n> \t--- a/builtin/bugreport.c\n> \t+++ b/builtin/bugreport.c\n> \t@@ -81,9 +81,13 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n> \t \t\treturn;\n> \t \t}\n> \t \n> \t-\tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n> \t-\t\tif (hook_exists(hook[i], HOOKDIR_USE_CONFIG))\n> \t+\tfor (i = 0; i < ARRAY_SIZE(hook); i++) {\n> \t+\t\tstruct strbuf config;\n> \t+\t\tstrbuf_addf(&config, \"hook.%s.config\", hook[i]);\n> \t+\t\tif (hook_exists(hook[i], config.buf, HOOKDIR_USE_CONFIG))\n> \t \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n> \t+\t\tstrbuf_release(&config);\n> \t+\t}\n> \t }\n> \n> Less strbuf, see below.\n> \t \n> \t static const char * const bugreport_usage[] = {\n> \tdiff --git a/builtin/commit.c b/builtin/commit.c\n> \tindex 31df571f123..fc9f1f5ee58 100644\n> \t--- a/builtin/commit.c\n> \t+++ b/builtin/commit.c\n> \t@@ -984,7 +984,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n> \t \t\treturn 0;\n> \t \t}\n> \t \n> \t-\tif (!no_verify && hook_exists(\"pre-commit\", HOOKDIR_USE_CONFIG)) {\n> \t+\tif (!no_verify && hook_exists(\"pre-commit\", \"hook.pre-commit.command\",\n> \t+\t\t\t\t      HOOKDIR_USE_CONFIG)) {\n> \t \t\t/*\n> \t \t\t * Re-read the index as pre-commit hook could have updated it,\n> \t \t\t * and write it out as a tree.  We must do this before we invoke\n> \n> \n> ..ditto.\n> \n> \tdiff --git a/builtin/hook.c b/builtin/hook.c\n> \tindex b4f4adb1dea..d0b56ee47f8 100644\n> \t--- a/builtin/hook.c\n> \t+++ b/builtin/hook.c\n> \t@@ -18,8 +18,6 @@ static enum hookdir_opt should_run_hookdir;\n> \t static int list(int argc, const char **argv, const char *prefix)\n> \t {\n> \t \tstruct list_head *head, *pos;\n> \t-\tstruct strbuf hookname = STRBUF_INIT;\n> \t-\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n> \t \n> \t \tstruct option list_options[] = {\n> \t \t\tOPT_END(),\n> \t@@ -33,67 +31,60 @@ static int list(int argc, const char **argv, const char *prefix)\n> \t \t\t\t      builtin_hook_usage, list_options);\n> \t \t}\n> \t \n> \t-\tstrbuf_addstr(&hookname, argv[0]);\n> \t-\n> \t-\thead = hook_list(&hookname);\n> \t+\thead = hook_list(argv[0]);\n> \t \n> \t \tif (list_empty(head)) {\n> \t \t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> \t-\t\t       hookname.buf);\n> \t-\t\tstrbuf_release(&hookname);\n> \t+\t\t       argv[0]);\n> \t \t\treturn 0;\n> \t \t}\n> \t \n> \t-\tswitch (should_run_hookdir) {\n> \t-\t\tcase HOOKDIR_NO:\n> \t-\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will not run)\"));\n> \t-\t\t\tbreak;\n> \t-\t\tcase HOOKDIR_ERROR:\n> \t-\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will error and not run)\"));\n> \t-\t\t\tbreak;\n> \t-\t\tcase HOOKDIR_INTERACTIVE:\n> \t-\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will prompt)\"));\n> \t-\t\t\tbreak;\n> \t-\t\tcase HOOKDIR_WARN:\n> \t-\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will warn but run)\"));\n> \t-\t\t\tbreak;\n> \t-\t\tcase HOOKDIR_YES:\n> \t-\t\t/*\n> \t-\t\t * The default behavior should agree with\n> \t-\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n> \t-\t\t * do the default behavior.\n> \t-\t\t */\n> \t-\t\tcase HOOKDIR_UNKNOWN:\n> \t-\t\tdefault:\n> \t-\t\t\tbreak;\n> \t-\t}\n> \t-\n> \t \tlist_for_each(pos, head) {\n> \t \t\tstruct hook *item = list_entry(pos, struct hook, list);\n> \t \t\titem = list_entry(pos, struct hook, list);\n> \t \t\tif (item) {\n> \t-\t\t\t/* Don't translate 'hookdir' - it matches the config */\n> \t-\t\t\tprintf(\"%s: %s%s\\n\",\n> \t-\t\t\t       (item->from_hookdir\n> \t+\t\t\tconst char *scope = item->from_hookdir\n> \t \t\t\t\t? \"hookdir\"\n> \t-\t\t\t\t: config_scope_name(item->origin)),\n> \t-\t\t\t       item->command.buf,\n> \t-\t\t\t       (item->from_hookdir\n> \t-\t\t\t\t? hookdir_annotation.buf\n> \t-\t\t\t\t: \"\"));\n> \t+\t\t\t\t: config_scope_name(item->origin);\n> \t+\t\t\tswitch (should_run_hookdir) {\n> \t+\t\t\tcase HOOKDIR_NO:\n> \t+\t\t\t\tprintf(_(\"%s: %s (will not run)\\n\"),\n> \t+\t\t\t\t       scope, item->command.buf);\n> \t+\t\t\t\tbreak;\n> \t+\t\t\tcase HOOKDIR_ERROR:\n> \t+\t\t\t\tprintf(_(\"%s: %s (will error and not run)\\n\"),\n> \t+\t\t\t\t       scope, item->command.buf);\n> \t+\t\t\t\tbreak;\n> \t+\t\t\tcase HOOKDIR_INTERACTIVE:\n> \t+\t\t\t\tprintf(_(\"%s: %s (will prompt)\\n\"),\n> \t+\t\t\t\t       scope, item->command.buf);\n> \t+\t\t\t\tbreak;\n> \t+\t\t\tcase HOOKDIR_WARN:\n> \t+\t\t\t\tprintf(_(\"%s: %s (will warn but run)\\n\"),\n> \t+\t\t\t\t       scope, item->command.buf);\n> \t+\t\t\t\tbreak;\n> \t+\t\t\tcase HOOKDIR_YES:\n> \t+\t\t\t\t/*\n> \t+\t\t\t\t * The default behavior should agree with\n> \t+\t\t\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n> \t+\t\t\t\t * do the default behavior.\n> \t+\t\t\t\t */\n> \t+\t\t\tcase HOOKDIR_UNKNOWN:\n> \t+\t\t\tdefault:\n> \t+\t\t\t\tprintf(_(\"%s: %s\\n\"),\n> \t+\t\t\t\t       scope, item->command.buf);\n> \t+\t\t\t\tbreak;\n> \t+\t\t\t}\n> \t \t\t}\n> \t \t}\n> \t \n> \t \tclear_hook_list(head);\n> \t-\tstrbuf_release(&hookdir_annotation);\n> \t-\tstrbuf_release(&hookname);\n> \t \n> \t \treturn 0;\n> \t }\n> \n> I think this is better to avoid i18n lego, as noted in another reply\n> (but I didn't include the patch).\n> \n> More on strbuf below:\n> \t \n> \t static int run(int argc, const char **argv, const char *prefix)\n> \t {\n> \t-\tstruct strbuf hookname = STRBUF_INIT;\n> \t \tstruct run_hooks_opt opt;\n> \t \tint rc = 0;\n> \t \n> \t@@ -118,12 +109,10 @@ static int run(int argc, const char **argv, const char *prefix)\n> \t \t\tusage_msg_opt(_(\"You must specify a hook event to run.\"),\n> \t \t\t\t      builtin_hook_usage, run_options);\n> \t \n> \t-\tstrbuf_addstr(&hookname, argv[0]);\n> \t \topt.run_hookdir = should_run_hookdir;\n> \t \n> \t-\trc = run_hooks(hookname.buf, &opt);\n> \t+\trc = run_hooks(argv[0], &opt);\n> \t \n> \t-\tstrbuf_release(&hookname);\n> \t \trun_hooks_opt_clear(&opt);\n> \t \n> \t \treturn rc;\n> \tdiff --git a/builtin/merge.c b/builtin/merge.c\n> \tindex 3a2af257a6b..df4ff72fbc7 100644\n> \t--- a/builtin/merge.c\n> \t+++ b/builtin/merge.c\n> \t@@ -848,7 +848,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n> \t \t * and write it out as a tree.  We must do this before we invoke\n> \t \t * the editor and after we invoke run_status above.\n> \t \t */\n> \t-\tif (hook_exists(\"pre-merge-commit\", HOOKDIR_USE_CONFIG))\n> \t+\tif (hook_exists(\"pre-merge-commit\", \"hook.pre-merge-commit.command\",\n> \t+\t\t\tHOOKDIR_USE_CONFIG))\n> \t \t\tdiscard_cache();\n> \t \tread_cache_from(index_file);\n> \t \tstrbuf_addbuf(&msg, &merge_msg);\n> \tdiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> \tindex eaedeeb1e8b..a76069ea592 100644\n> \t--- a/builtin/receive-pack.c\n> \t+++ b/builtin/receive-pack.c\n> \t@@ -1123,12 +1123,10 @@ static int run_proc_receive_hook(struct command *commands,\n> \t \tint version = 0;\n> \t \tint code;\n> \t \n> \t-\tstruct strbuf hookname = STRBUF_INIT;\n> \t \tstruct hook *proc_receive = NULL;\n> \t \tstruct list_head *pos, *hooks;\n> \t \n> \t-\tstrbuf_addstr(&hookname, \"proc-receive\");\n> \t-\thooks = hook_list(&hookname);\n> \t+\thooks = hook_list(\"proc-receive\");\n> \t \n> \t \tlist_for_each(pos, hooks) {\n> \t \t\tif (proc_receive) {\n> \t@@ -1460,8 +1458,6 @@ static const char *push_to_deploy(unsigned char *sha1,\n> \t \treturn NULL;\n> \t }\n> \t \n> \t-static const char *push_to_checkout_hook = \"push-to-checkout\";\n> \t-\n> \t static const char *push_to_checkout(unsigned char *hash,\n> \t \t\t\t\t    struct strvec *env,\n> \t \t\t\t\t    const char *work_tree)\n> \t@@ -1472,7 +1468,7 @@ static const char *push_to_checkout(unsigned char *hash,\n> \t \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n> \t \tstrvec_pushv(&opt.env, env->v);\n> \t \tstrvec_push(&opt.args, hash_to_hex(hash));\n> \t-\tif (run_hooks(push_to_checkout_hook, &opt)) {\n> \t+\tif (run_hooks(\"push-to-checkout\", &opt)) {\n> \t \t\trun_hooks_opt_clear(&opt);\n> \t \t\treturn \"push-to-checkout hook declined\";\n> \t \t} else {\n> \t@@ -1502,7 +1498,8 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n> \t \n> \t \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n> \t \n> \t-\tif (!hook_exists(push_to_checkout_hook, HOOKDIR_USE_CONFIG))\n> \t+\tif (!hook_exists(\"push-to-checkout\", \"hook.push-to-checkout.command\",\n> \t+\t\t\t HOOKDIR_USE_CONFIG))\n> \t \t\tretval = push_to_deploy(sha1, &env, work_tree);\n> \t \telse\n> \t \t\tretval = push_to_checkout(sha1, &env, work_tree);\n> \tdiff --git a/hook.c b/hook.c\n> \tindex 7f6f3b9a616..49c3861ce00 100644\n> \t--- a/hook.c\n> \t+++ b/hook.c\n> \t@@ -247,7 +247,7 @@ static const char *find_legacy_hook(const char *name)\n> \t }\n> \t \n> \t \n> \t-struct list_head* hook_list(const struct strbuf* hookname)\n> \t+struct list_head* hook_list(const char *hookname)\n> \t {\n> \t \tstruct strbuf hook_key = STRBUF_INIT;\n> \t \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> \t@@ -256,14 +256,14 @@ struct list_head* hook_list(const struct strbuf* hookname)\n> \t \tINIT_LIST_HEAD(hook_head);\n> \t \n> \t \tif (!hookname)\n> \t-\t\treturn NULL;\n> \t+\t\tBUG(\"???\");;\n> \t \n> \t-\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname->buf);\n> \t+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n> \t \n> \t \tgit_config(hook_config_lookup, &cb_data);\n> \t \n> \t \tif (have_git_dir()) {\n> \t-\t\tconst char *legacy_hook_path = find_legacy_hook(hookname->buf);\n> \t+\t\tconst char *legacy_hook_path = find_legacy_hook(hookname);\n> \t \n> \t \t\t/* Unconditionally add legacy hook, but annotate it. */\n> \t \t\tif (legacy_hook_path) {\n> \t@@ -300,10 +300,10 @@ void run_hooks_opt_init_async(struct run_hooks_opt *o)\n> \t \to->jobs = configured_hook_jobs();\n> \t }\n> \t \n> \t-int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n> \t+int hook_exists(const char *hookname, const char *hook_config,\n> \t+\t\tenum hookdir_opt should_run_hookdir)\n> \t {\n> \t \tconst char *value = NULL; /* throwaway */\n> \t-\tstruct strbuf hook_key = STRBUF_INIT;\n> \t \tint could_run_hookdir;\n> \t \n> \t \tif (should_run_hookdir == HOOKDIR_USE_CONFIG)\n> \t@@ -314,9 +314,7 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n> \t \t\t\t\tshould_run_hookdir == HOOKDIR_YES)\n> \t \t\t\t\t&& !!find_legacy_hook(hookname);\n> \t \n> \t-\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n> \t-\n> \t-\treturn (!git_config_get_value(hook_key.buf, &value)) || could_run_hookdir;\n> \t+\treturn (!git_config_get_value(hook_config, &value)) || could_run_hookdir;\n> \t }\n> \t \n> \t void run_hooks_opt_clear(struct run_hooks_opt *o)\n> \t@@ -438,7 +436,6 @@ static int notify_hook_finished(int result,\n> \t \n> \t int run_hooks(const char *hookname, struct run_hooks_opt *options)\n> \t {\n> \t-\tstruct strbuf hookname_str = STRBUF_INIT;\n> \t \tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n> \t \tstruct hook_cb_data cb_data = { 0, NULL, NULL, options };\n> \t \n> \t@@ -448,9 +445,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n> \t \tif (options->path_to_stdin && options->feed_pipe)\n> \t \t\tBUG(\"choose only one method to populate stdin\");\n> \t \n> \t-\tstrbuf_addstr(&hookname_str, hookname);\n> \t-\n> \t-\tto_run = hook_list(&hookname_str);\n> \t+\tto_run = hook_list(hookname);\n> \t \n> \t \tlist_for_each_safe(pos, tmp, to_run) {\n> \t \t\tstruct hook *hook = list_entry(pos, struct hook, list);\n> \tdiff --git a/hook.h b/hook.h\n> \tindex 4ff9999b049..bfbbf36882d 100644\n> \t--- a/hook.h\n> \t+++ b/hook.h\n> \t@@ -26,7 +26,7 @@ struct hook {\n> \t  * Provides a linked list of 'struct hook' detailing commands which should run\n> \t  * in response to the 'hookname' event, in execution order.\n> \t  */\n> \t-struct list_head* hook_list(const struct strbuf *hookname);\n> \t+struct list_head* hook_list(const char *hookname);\n> \t \n> \t enum hookdir_opt\n> \t {\n> \t@@ -123,7 +123,8 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);\n> \t  * Like with run_hooks, if you take a --run-hookdir flag, reflect that\n> \t  * user-specified behavior here instead.\n> \t  */\n> \t-int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);\n> \t+int hook_exists(const char *hookname, const char *hook_config,\n> \t+\t\tenum hookdir_opt should_run_hookdir);\n> \t \n> \t /*\n> \t  * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n> \tdiff --git a/refs.c b/refs.c\n> \tindex 334fdd9103c..f01995fe64f 100644\n> \t--- a/refs.c\n> \t+++ b/refs.c\n> \t@@ -1966,7 +1966,9 @@ static int run_transaction_hook(struct ref_transaction *transaction,\n> \t \n> \t \trun_hooks_opt_init_async(&opt);\n> \t \n> \t-\tif (!hook_exists(\"reference-transaction\", HOOKDIR_USE_CONFIG))\n> \t+\tif (!hook_exists(\"reference-transaction\",\n> \t+\t\t\t \"hook.reference-transaction.command\",\n> \t+\t\t\t HOOKDIR_USE_CONFIG))\n> \t \t\treturn ret;\n> \t \n> \t \tstrvec_push(&opt.args, state);\n> \tdiff --git a/sequencer.c b/sequencer.c\n> \tindex 34ff275f0d1..52c067c1688 100644\n> \t--- a/sequencer.c\n> \t+++ b/sequencer.c\n> \t@@ -1436,7 +1436,9 @@ static int try_to_commit(struct repository *r,\n> \t \t\t}\n> \t \t}\n> \t \n> \t-\tif (hook_exists(\"prepare-commit-msg\", HOOKDIR_USE_CONFIG)) {\n> \t+\tif (hook_exists(\"prepare-commit-msg\",\n> \t+\t\t\t\"hook.prepare-commit-msg.command\",\n> \t+\t\t\tHOOKDIR_USE_CONFIG)) {\n> \t \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n> \t \t\tif (res)\n> \t \t\t\tgoto out;\n> \n> There was another reply (from JT I believe, but didn't go back and look\n> it up) about the over use of strbuf.\n> \n> I tend to agree, as much as I love the API it's really not better to\n> write C with it if all you need is a const char* that's never modified,\n> particularly if you get it from elsewhere.\n> \n> So it's really not meant for or good for \"everything we need a const\n> char*\", but to avoid verbose realloc() dances all over the place, and\n> for things like getline() loops without a hardcoded buffer size.\n> \n> E.g. in the first hunk here we're creating a strbuf just to copy argv[0]\n> to it, and then throwing it away, let's just pass down argv[0].\n> \n> For hook_exists I think just having the code more grep-able and having\n> the config value inline is better, but I admit that's a matter of taste.\n> \n> I didn't try to find all such strbuf() occurrences, anyway, in the\n> overall scheme of things it's a relatively small nit.\n> \n> I'm hoping to do some deeper diving into this series, in particular the\n> parallelism, but just sending the shallow-ish comments I have for now.\n> \n> Thanks for working on this!\n"},{"id":"419536","messageId":"YFJVp+5IVJux9Vbe@google.com","threadId":"55286","inReplyTo":"YFJNd6an0eGRC4/p@google.com","subject":"Re: [PATCH v8 00/37] config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-17T19:16:55Z","receivedAt":"2021-03-17T19:17:55Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Mar 17, 2021 at 11:41:59AM -0700, Emily Shaffer wrote:\n> \n> On Fri, Mar 12, 2021 at 10:49:38AM +0100, ï¿½var Arnfjï¿½rï¿½ Bjarmason wrote:\n> > \n> > \n> > On Thu, Mar 11 2021, Emily Shaffer wrote:\n> > \n> > > Since v7:\n> > > - Addressed Jonathan Tan's review of part I\n> > > - Addressed Junio's review of part I and II\n> > > - Combined parts I and II\n> > >\n> > > I think the updates to patch 1 between the rest of the work I've been\n> > > doing probably have covered ï¿½var's comments.\n> > \n> > A range-diff between iterations of such a large series would be most\n> > useful. Do you have a public repo with tags or whatever the different\n> > versions, for those who'd like an easier way to follow along the\n> > differing versions than scraping the ML archive?\n> \n> I am really embarrassed to say that I don't have the\n> branches/tags/whatever up. I have not succeeded in building that habit\n> yet. I'll generate one from my local patches today and send it here.\n\n 1:  be907f68b9 !  1:  a5e8c233c3 doc: propose hooks managed by the config\n    @@ Documentation/technical/config-based-hooks.txt (new)\n     +[[motivation]]\n     +== Motivation\n     +\n    -+Replace the .git/hook/hookname path as the only source of hooks to execute;\n    ++Replace the `.git/hook/hookname` path as the only source of hooks to execute;\n     +allow users to define hooks using config files, in a way which is friendly to\n    -+users with multiple repos which have similar needs.\n    ++users with multiple repos which have similar needs - hooks can be easily shared\n    ++between multiple Git repos.\n     +\n     +Redefine \"hook\" as an event rather than a single script, allowing users to\n     +perform multiple unrelated actions on a single event.\n     +\n    -+Take a step closer to safety when copying zipped Git repositories from untrusted\n    -+users by making it more apparent to users which scripts will be run during\n    -+normal Git operations.\n    -+\n     +Make it easier for users to discover Git's hook feature and automate their\n     +workflows.\n     +\n    @@ Documentation/technical/config-based-hooks.txt (new)\n     +number of cases:\n     +\n     +- \"no\": the legacy hook will not be run\n    ++- \"error\": Git will print a warning to stderr before ignoring the legacy hook\n     +- \"interactive\": Git will prompt the user before running the legacy hook\n     +- \"warn\": Git will print a warning to stderr before running the legacy hook\n     +- \"yes\" (default): Git will silently run the legacy hook\n    @@ Documentation/technical/config-based-hooks.txt (new)\n     +given which Git does not recognize, Git should discard that config entry. For\n     +example, if \"warn\" was specified at system level and \"junk\" was specified at\n     +global level, Git would resolve the value to \"warn\"; if the only time the config\n    -+was set was to \"junk\", Git would use the default value of \"yes\".\n    ++was set was to \"junk\", Git would use the default value of \"yes\" (but print a\n    ++warning to the user first to let them know their value is wrong).\n     +\n     +`struct hookcmd` is expected to grow in size over time as more functionality is\n     +added to hooks; so that other parts of the code don't need to understand the\n    @@ Documentation/technical/config-based-hooks.txt (new)\n     +=== Security and repo config\n     +\n     +Part of the motivation behind this refactor is to mitigate hooks as an attack\n    -+vector;footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/]\n    -+however, as the design stands, users can still provide hooks in the repo-level\n    -+config, which is included when a repo is zipped and sent elsewhere.  The\n    ++vector.footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/]\n    ++However, as the design stands, users can still provide hooks in the repo-level\n    ++config, which is included when a repo is zipped and sent elsewhere. The\n     +security of the repo-level config is still under discussion; this design\n    -+generally assumes the repo-level config is secure, which is not true yet. The\n    -+goal is to avoid an overcomplicated design to work around a problem which has\n    -+ceased to exist.\n    ++generally assumes the repo-level config is secure, which is not true yet. This\n    ++assumption was made to avoid overcomplicating the design. So, this series\n    ++doesn't particularly improve security or resistance to zip attacks.\n     +\n     +[[ease-of-use]]\n     +=== Ease of use\n    @@ Documentation/technical/config-based-hooks.txt (new)\n     +A previous summary of alternatives exists in the\n     +archives.footnote:[https://lore.kernel.org/git/20191116011125.GG22855@google.com]\n     +\n    -+[[status-quo]]\n    -+=== Status quo\n    -+\n    -+Today users can implement multihooks themselves by using a \"trampoline script\"\n    -+as their hook, and pointing that script to a directory or list of other scripts\n    -+they wish to run.\n    -+\n    -+[[hook-directories]]\n    -+=== Hook directories\n    -+\n    -+Other contributors have suggested Git learn about the existence of a directory\n    -+such as `.git/hooks/<hookname>.d` and execute those hooks in alphabetical order.\n    -+\n    -+[[comparison]]\n    -+=== Comparison table\n    ++The table below shows a number of goals and how they might be achieved with\n    ++config-based hooks, by implementing directory support (i.e.\n    ++'.git/hooks/pre-commit.d'), or as hooks are run today.\n     +\n     +.Comparison of alternatives\n     +|===\n    @@ Documentation/technical/config-based-hooks.txt (new)\n     +|Natively\n     +|With user effort\n     +\n    ++|Supports parallelization\n    ++|Natively\n    ++|Natively\n    ++|No (user's multihook trampoline script would need to handle parallelism)\n    ++\n     +|Safer for zipped repos\n     +|A little\n     +|No\n    @@ Documentation/technical/config-based-hooks.txt (new)\n     +\n     +|Can install one hook to many repos\n     +|Yes\n    -+|No\n    -+|No\n    ++|With symlinks or core.hooksPath\n    ++|With symlinks or core.hooksPath\n     +\n     +|Discoverability\n    -+|Better (in `git help git`)\n    -+|Same as before\n    ++|Findable with 'git help git' or tab-completion via 'git hook' subcommand\n    ++|Findable via improved documentation\n     +|Same as before\n     +\n     +|Hard to run unexpected hook\n     +|If configured\n    -+|No\n    ++|Could be made to warn or look for a config\n     +|No\n     +|===\n     +\n    ++[[status-quo]]\n    ++=== Status quo\n    ++\n    ++Today users can implement multihooks themselves by using a \"trampoline script\"\n    ++as their hook, and pointing that script to a directory or list of other scripts\n    ++they wish to run.\n    ++\n    ++[[hook-directories]]\n    ++=== Hook directories\n    ++\n    ++Other contributors have suggested Git learn about the existence of a directory\n    ++such as `.git/hooks/<hookname>.d` and execute those hooks in alphabetical order.\n    ++\n     +[[future-work]]\n     +== Future work\n     +\n    @@ Documentation/technical/config-based-hooks.txt (new)\n     +dependencies. If we decide to solve this problem, we may want to look to modern\n     +build systems for inspiration on how to manage dependencies and parallel tasks.\n     +\n    ++[[nontrivial-hooks]]\n    ++=== Multihooks and nontrivial output\n    ++\n    ++Some hooks - like 'proc-receive' - don't lend themselves well to multihooks at\n    ++all. In the case of 'proc-receive', for now, multiple hook definitions are\n    ++disallowed. In the future we might be able to conceive a better approach, for\n    ++example, running the hooks in series and using the output from one hook as the\n    ++input to the next.\n    ++\n     +[[securing-hookdir-hooks]]\n     +=== Securing hookdir hooks\n     +\n 2:  b1d37c3911 =  2:  a3e858d056 hook: scaffolding for git-hook subcommand\n 3:  fea411c598 !  3:  60b28a652b hook: add list command\n    @@ Commit message\n         run in config order. If more properties need to be set on a given hook\n         in the future, commands can also be specified by providing\n         \"hook.<hookname>.command = <hookcmd-name>\", as well as a \"[hookcmd\n    -    <hookcmd-name>]\" subsection; at minimum, this subsection must contain a\n    +    <hookcmd-name>]\" subsection; this subsection should contain a\n         \"hookcmd.<hookcmd-name>.command = <path-to-hook>\" line.\n     \n         For example:\n    @@ Makefile: LIB_OBJS += hash-lookup.o\n      ## builtin/hook.c ##\n     @@\n      #include \"cache.h\"\n    - \n    +-\n      #include \"builtin.h\"\n     +#include \"config.h\"\n     +#include \"hook.h\"\n    @@ builtin/hook.c\n      {\n     -\tstruct option builtin_hook_options[] = {\n     +\tstruct list_head *head, *pos;\n    -+\tstruct hook *item;\n     +\tstruct strbuf hookname = STRBUF_INIT;\n     +\n     +\tstruct option list_options[] = {\n    @@ builtin/hook.c\n     +\t}\n     +\n     +\tlist_for_each(pos, head) {\n    -+\t\titem = list_entry(pos, struct hook, list);\n    ++\t\tstruct hook *item = list_entry(pos, struct hook, list);\n     +\t\tif (item)\n     +\t\t\tprintf(\"%s: %s\\n\",\n     +\t\t\t       config_scope_name(item->origin),\n    @@ hook.c (new)\n     +\t\t    list_del(pos);\n     +\t\t    /* we'll simply move the hook to the end */\n     +\t\t    to_add = it;\n    ++\t\t    break;\n     +\t\t}\n     +\t}\n     +\n     +\tif (!to_add) {\n     +\t\t/* adding a new hook, not moving an old one */\n    -+\t\tto_add = xmalloc(sizeof(struct hook));\n    ++\t\tto_add = xmalloc(sizeof(*to_add));\n     +\t\tstrbuf_init(&to_add->command, 0);\n     +\t\tstrbuf_addstr(&to_add->command, command);\n     +\t}\n    @@ hook.c (new)\n     +\t/* re-set the scope so we show where an override was specified */\n     +\tto_add->origin = current_config_scope();\n     +\n    -+\tlist_add_tail(&to_add->list, pos);\n    ++\tlist_add_tail(&to_add->list, head);\n     +}\n     +\n     +static void remove_hook(struct list_head *to_remove)\n    @@ hook.c (new)\n     +\t\tconst char *command = value;\n     +\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n     +\n    -+\t\t/* Check if a hookcmd with that name exists. */\n    ++\t\t/*\n    ++\t\t * Check if a hookcmd with that name exists. If it doesn't,\n    ++\t\t * 'git_config_get_value()' is documented not to touch &command,\n    ++\t\t * so we don't need to do anything.\n    ++\t\t */\n     +\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n     +\t\tgit_config_get_value(hookcmd_name.buf, &command);\n     +\n    @@ hook.c (new)\n     +\n     +\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname->buf);\n     +\n    -+\tgit_config(hook_config_lookup, (void*)&cb_data);\n    ++\tgit_config(hook_config_lookup, &cb_data);\n     +\n     +\tstrbuf_release(&hook_key);\n     +\treturn hook_head;\n    @@ hook.h (new)\n     +#include \"list.h\"\n     +#include \"strbuf.h\"\n     +\n    -+struct hook\n    -+{\n    ++struct hook {\n     +\tstruct list_head list;\n     +\t/*\n     +\t * Config file which holds the hook.*.command definition.\n 4:  89f1adf34d !  4:  d8232a8254 hook: include hookdir hook in list\n    @@ Commit message\n         $GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken\n         from the config are more featureful than hooks placed in the $HOOKDIR,\n         those hooks should not stop working for users who already have them.\n    -\n    -    Legacy hooks should be run directly, not in shell. We know that they are\n    -    a path to an executable, not a oneliner script - and running them\n    -    directly takes care of path quoting concerns for us for free.\n    +    Let's list them to the user, but instead of displaying a config scope\n    +    (e.g. \"global: blah\") we can prefix them with \"hookdir:\".\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## builtin/hook.c ##\n     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n    - \tstruct list_head *head, *pos;\n    - \tstruct hook *item;\n    - \tstruct strbuf hookname = STRBUF_INIT;\n    -+\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n    - \n    - \tstruct option list_options[] = {\n    - \t\tOPT_END(),\n    -@@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n      \n      \tlist_for_each(pos, head) {\n    - \t\titem = list_entry(pos, struct hook, list);\n    + \t\tstruct hook *item = list_entry(pos, struct hook, list);\n     -\t\tif (item)\n    --\t\t\tprintf(\"%s: %s\\n\",\n    --\t\t\t       config_scope_name(item->origin),\n    --\t\t\t       item->command.buf);\n    ++\t\titem = list_entry(pos, struct hook, list);\n     +\t\tif (item) {\n     +\t\t\t/* Don't translate 'hookdir' - it matches the config */\n    -+\t\t\tprintf(\"%s: %s%s\\n\",\n    + \t\t\tprintf(\"%s: %s\\n\",\n    +-\t\t\t       config_scope_name(item->origin),\n     +\t\t\t       (item->from_hookdir\n     +\t\t\t\t? \"hookdir\"\n     +\t\t\t\t: config_scope_name(item->origin)),\n    -+\t\t\t       item->command.buf,\n    -+\t\t\t       (item->from_hookdir\n    -+\t\t\t\t? hookdir_annotation.buf\n    -+\t\t\t\t: \"\"));\n    + \t\t\t       item->command.buf);\n     +\t\t}\n      \t}\n      \n    @@ hook.c\n      void free_hook(struct hook *ptr)\n      {\n     @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *command)\n    - \t\tto_add = xmalloc(sizeof(struct hook));\n    + \t\tto_add = xmalloc(sizeof(*to_add));\n      \t\tstrbuf_init(&to_add->command, 0);\n      \t\tstrbuf_addstr(&to_add->command, command);\n     +\t\tto_add->from_hookdir = 0;\n    @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *comm\n      \n      \t/* re-set the scope so we show where an override was specified */\n     @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n    - \tstruct strbuf hook_key = STRBUF_INIT;\n    - \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n    - \tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n    -+\tconst char *legacy_hook_path = NULL;\n    - \n    - \tINIT_LIST_HEAD(hook_head);\n      \n    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n    + \tgit_config(hook_config_lookup, &cb_data);\n      \n    - \tgit_config(hook_config_lookup, (void*)&cb_data);\n    - \n    -+\tif (have_git_dir())\n    -+\t\tlegacy_hook_path = find_hook(hookname->buf);\n    ++\tif (have_git_dir()) {\n    ++\t\tconst char *legacy_hook_path = find_hook(hookname->buf);\n     +\n    -+\t/* Unconditionally add legacy hook, but annotate it. */\n    -+\tif (legacy_hook_path) {\n    -+\t\tstruct hook *legacy_hook;\n    ++\t\t/* Unconditionally add legacy hook, but annotate it. */\n    ++\t\tif (legacy_hook_path) {\n    ++\t\t\tstruct hook *legacy_hook;\n     +\n    -+\t\tappend_or_move_hook(hook_head, absolute_path(legacy_hook_path));\n    -+\t\tlegacy_hook = list_entry(hook_head->prev, struct hook, list);\n    -+\t\tlegacy_hook->from_hookdir = 1;\n    ++\t\t\tappend_or_move_hook(hook_head,\n    ++\t\t\t\t\t    absolute_path(legacy_hook_path));\n    ++\t\t\tlegacy_hook = list_entry(hook_head->prev, struct hook,\n    ++\t\t\t\t\t\t list);\n    ++\t\t\tlegacy_hook->from_hookdir = 1;\n    ++\t\t}\n     +\t}\n     +\n      \tstrbuf_release(&hook_key);\n    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n      }\n     \n      ## hook.h ##\n    -@@ hook.h: struct hook\n    +@@ hook.h: struct hook {\n      \tenum config_scope origin;\n      \t/* The literal command to run. */\n      \tstruct strbuf command;\n    -+\tint from_hookdir;\n    ++\tunsigned from_hookdir : 1;\n      };\n      \n      /*\n 5:  723edcd785 !  5:  96c0a4838f hook: respect hook.runHookDir\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    hook: respect hook.runHookDir\n    +    hook: teach hook.runHookDir\n     \n    -    Include hooks specified in the hook directory in the list of hooks to\n    -    run. These hooks do need to be treated differently from config-specified\n    -    ones - they do not need to run in a shell, and later on may be disabled\n    -    or warned about based on a config setting.\n    -\n    -    Because they are at least as local as the local config, we'll run them\n    -    last - to keep the hook execution order from global to local.\n    -\n    -    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    +    For now, just give a hint about how these hooks will be run in 'git hook\n    +    list'. Later on, though, we will pay attention to this enum when running\n    +    the hooks.\n     \n      ## Documentation/config/hook.txt ##\n     @@ Documentation/config/hook.txt: hookcmd.<name>.command::\n    @@ builtin/hook.c: static const char * const builtin_hook_usage[] = {\n      static int list(int argc, const char **argv, const char *prefix)\n      {\n      \tstruct list_head *head, *pos;\n    + \tstruct strbuf hookname = STRBUF_INIT;\n    ++\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n    + \n    + \tstruct option list_options[] = {\n    + \t\tOPT_END(),\n     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n      \t\treturn 0;\n      \t}\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n     +\t\tcase HOOKDIR_NO:\n     +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will not run)\"));\n     +\t\t\tbreak;\n    ++\t\tcase HOOKDIR_ERROR:\n    ++\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will error and not run)\"));\n    ++\t\t\tbreak;\n     +\t\tcase HOOKDIR_INTERACTIVE:\n     +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will prompt)\"));\n     +\t\t\tbreak;\n     +\t\tcase HOOKDIR_WARN:\n    -+\t\tcase HOOKDIR_UNKNOWN:\n    -+\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will warn)\"));\n    ++\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will warn but run)\"));\n     +\t\t\tbreak;\n     +\t\tcase HOOKDIR_YES:\n     +\t\t/*\n     +\t\t * The default behavior should agree with\n    -+\t\t * hook.c:configured_hookdir_opt().\n    ++\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n    ++\t\t * do the default behavior.\n     +\t\t */\n    ++\t\tcase HOOKDIR_UNKNOWN:\n     +\t\tdefault:\n     +\t\t\tbreak;\n     +\t}\n     +\n      \tlist_for_each(pos, head) {\n    + \t\tstruct hook *item = list_entry(pos, struct hook, list);\n      \t\titem = list_entry(pos, struct hook, list);\n      \t\tif (item) {\n    + \t\t\t/* Don't translate 'hookdir' - it matches the config */\n    +-\t\t\tprintf(\"%s: %s\\n\",\n    ++\t\t\tprintf(\"%s: %s%s\\n\",\n    + \t\t\t       (item->from_hookdir\n    + \t\t\t\t? \"hookdir\"\n    + \t\t\t\t: config_scope_name(item->origin)),\n    +-\t\t\t       item->command.buf);\n    ++\t\t\t       item->command.buf,\n    ++\t\t\t       (item->from_hookdir\n    ++\t\t\t\t? hookdir_annotation.buf\n    ++\t\t\t\t: \"\"));\n    + \t\t}\n    + \t}\n    + \n    + \tclear_hook_list(head);\n    ++\tstrbuf_release(&hookdir_annotation);\n    + \tstrbuf_release(&hookname);\n    + \n    + \treturn 0;\n     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n      \n      int cmd_hook(int argc, const char **argv, const char *prefix)\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n     +\tif (run_hookdir)\n     +\t\tif (!strcmp(run_hookdir, \"no\"))\n     +\t\t\tshould_run_hookdir = HOOKDIR_NO;\n    ++\t\telse if (!strcmp(run_hookdir, \"error\"))\n    ++\t\t\tshould_run_hookdir = HOOKDIR_ERROR;\n     +\t\telse if (!strcmp(run_hookdir, \"yes\"))\n     +\t\t\tshould_run_hookdir = HOOKDIR_YES;\n     +\t\telse if (!strcmp(run_hookdir, \"warn\"))\n    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *\n     +\tif (!strcmp(key, \"no\"))\n     +\t\treturn HOOKDIR_NO;\n     +\n    ++\tif (!strcmp(key, \"error\"))\n    ++\t\treturn HOOKDIR_ERROR;\n    ++\n     +\tif (!strcmp(key, \"yes\"))\n     +\t\treturn HOOKDIR_YES;\n     +\n    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *\n      \tstruct strbuf hook_key = STRBUF_INIT;\n     \n      ## hook.h ##\n    -@@ hook.h: struct hook\n    +@@ hook.h: struct hook {\n       */\n      struct list_head* hook_list(const struct strbuf *hookname);\n      \n     +enum hookdir_opt\n     +{\n     +\tHOOKDIR_NO,\n    ++\tHOOKDIR_ERROR,\n     +\tHOOKDIR_WARN,\n     +\tHOOKDIR_INTERACTIVE,\n     +\tHOOKDIR_YES,\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr\n     +\ttest_i18ncmp expected actual\n     +'\n     +\n    ++test_expect_success 'hook.runHookDir = error is respected by list' '\n    ++\tsetup_hookdir &&\n    ++\n    ++\ttest_config hook.runHookDir \"error\" &&\n    ++\n    ++\tcat >expected <<-EOF &&\n    ++\thookdir: $(pwd)/.git/hooks/pre-commit (will error and not run)\n    ++\tEOF\n    ++\n    ++\tgit hook list pre-commit >actual &&\n    ++\t# the hookdir annotation is translated\n    ++\ttest_i18ncmp expected actual\n    ++'\n    ++\n     +test_expect_success 'hook.runHookDir = warn is respected by list' '\n     +\tsetup_hookdir &&\n     +\n     +\ttest_config hook.runHookDir \"warn\" &&\n     +\n     +\tcat >expected <<-EOF &&\n    -+\thookdir: $(pwd)/.git/hooks/pre-commit (will warn)\n    ++\thookdir: $(pwd)/.git/hooks/pre-commit (will warn but run)\n     +\tEOF\n     +\n     +\tgit hook list pre-commit >actual &&\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr\n     +\t# the hookdir annotation is translated\n     +\ttest_i18ncmp expected actual\n     +'\n    ++\n    ++test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n    ++\tsetup_hookdir &&\n    ++\n    ++\ttest_config hook.runHookDir \"junk\" &&\n    ++\n    ++\tcat >expected <<-EOF &&\n    ++\thookdir: $(pwd)/.git/hooks/pre-commit\n    ++\tEOF\n    ++\n    ++\tgit hook list pre-commit >actual &&\n    ++\t# the hookdir annotation is translated\n    ++\ttest_i18ncmp expected actual\n    ++'\n     +\n      test_done\n 6:  567f6d9d00 !  6:  9068e11679 hook: implement hookcmd.<name>.skip\n    @@ Commit message\n         hook: implement hookcmd.<name>.skip\n     \n         If a user wants a specific repo to skip execution of a hook which is set\n    -    at a global or system level, they can now do so by specifying 'skip' in\n    -    their repo config:\n    +    at a global or system level, they will be able to do so by specifying\n    +    'skip' in their repo config:\n     \n         ~/.gitconfig\n           [hook.pre-commit]\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    + ## Documentation/config/hook.txt ##\n    +@@ Documentation/config/hook.txt: hookcmd.<name>.command::\n    + \tas a command. This can be an executable on your device or a oneliner for\n    + \tyour shell. See linkgit:git-hook[1].\n    + \n    ++hookcmd.<name>.skip::\n    ++\tSpecify this boolean to remove a command from earlier in the execution\n    ++\torder. Useful if you want to make a single repo an exception to hook\n    ++\tconfigured at the system or global scope. If there is no hookcmd\n    ++\tspecified for the command you want to skip, you can use the value of\n    ++\t`hook.<command>.command` as <name> as a shortcut. The \"skip\" setting\n    ++\tmust be specified after the \"hook.<command>.command\" to have an effect.\n    ++\n    + hook.runHookDir::\n    + \tControls how hooks contained in your hookdir are executed. Can be any of\n    + \t\"yes\", \"warn\", \"interactive\", or \"no\". Defaults to \"yes\". See\n    +\n    + ## Documentation/git-hook.txt ##\n    +@@ Documentation/git-hook.txt: $ git hook list \"prepare-commit-msg\"\n    + local: /bin/linter --c\n    + ----\n    + \n    ++If there is a command you wish to run in most cases but have one or two\n    ++exceptional repos where it should be skipped, you can use specify\n    ++`hookcmd.<name>.skip`, for example:\n    ++\n    ++System config\n    ++----\n    ++  [hook \"pre-commit\"]\n    ++    command = check-for-secrets\n    ++\n    ++  [hookcmd \"check-for-secrets\"]\n    ++    command = /bin/secret-checker --aggressive\n    ++----\n    ++\n    ++Local config\n    ++----\n    ++  [hookcmd \"check-for-secrets\"]\n    ++    skip = true\n    ++  # This works for inlined hook commands, too:\n    ++  [hookcmd \"~/typocheck.sh\"]\n    ++    skip = true\n    ++----\n    ++\n    ++After these configs are added, the hook list becomes:\n    ++\n    ++----\n    ++$ git hook list \"post-commit\"\n    ++global: /bin/linter --c\n    ++local: python ~/run-test-suite.py\n    ++\n    ++$ git hook list \"pre-commit\"\n    ++no commands configured for hook 'pre-commit'\n    ++----\n    ++\n    + COMMANDS\n    + --------\n    + \n    +\n      ## hook.c ##\n     @@ hook.c: void free_hook(struct hook *ptr)\n      \t}\n      }\n      \n     -static void append_or_move_hook(struct list_head *head, const char *command)\n    -+static struct hook* find_hook_by_command(struct list_head *head, const char *command)\n    ++static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n      {\n      \tstruct list_head *pos = NULL, *tmp = NULL;\n     -\tstruct hook *to_add = NULL;\n    @@ hook.c: void free_hook(struct hook *ptr)\n     -\t\t    /* we'll simply move the hook to the end */\n     -\t\t    to_add = it;\n     +\t\t    found = it;\n    + \t\t    break;\n      \t\t}\n      \t}\n     +\treturn found;\n    @@ hook.c: void free_hook(struct hook *ptr)\n      \n      \tif (!to_add) {\n      \t\t/* adding a new hook, not moving an old one */\n    -@@ hook.c: static void append_or_move_hook(struct list_head *head, const char *command)\n    - \t/* re-set the scope so we show where an override was specified */\n    - \tto_add->origin = current_config_scope();\n    - \n    --\tlist_add_tail(&to_add->list, pos);\n    -+\tlist_add_tail(&to_add->list, head);\n    - }\n    - \n    - static void remove_hook(struct list_head *to_remove)\n     @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n      \tif (!strcmp(key, hook_key)) {\n      \t\tconst char *command = value;\n    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *\n     +\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.skip\", command);\n     +\t\tgit_config_get_bool(hookcmd_name.buf, &skip);\n      \n    - \t\t/* Check if a hookcmd with that name exists. */\n    + \t\t/*\n    + \t\t * Check if a hookcmd with that name exists. If it doesn't,\n    + \t\t * 'git_config_get_value()' is documented not to touch &command,\n    + \t\t * so we don't need to do anything.\n    + \t\t */\n     +\t\tstrbuf_reset(&hookcmd_name);\n      \t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n      \t\tgit_config_get_value(hookcmd_name.buf, &command);\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is re\n     +\ttest_i18ncmp expected actual\n     +'\n     +\n    ++test_expect_success 'git hook list ignores skip referring to unused hookcmd' '\n    ++\ttest_config hookcmd.abc.command \"/path/abc\" --add &&\n    ++\ttest_config hookcmd.abc.skip \"true\" --add &&\n    ++\n    ++\tcat >expected <<-EOF &&\n    ++\tno commands configured for hook '\\''pre-commit'\\''\n    ++\tEOF\n    ++\n    ++\tgit hook list pre-commit >actual &&\n    ++\ttest_i18ncmp expected actual\n    ++'\n    ++\n     +test_expect_success 'git hook list removes skipped inlined hook' '\n     +\tsetup_hooks &&\n     +\ttest_config hookcmd.\"$ROOT/path/ghi\".skip \"true\" --add &&\n 7:  a1c02b6758 !  7:  a2867ab8c0 parse-options: parse into strvec\n    @@ Documentation/technical/api-parse-options.txt: There are some macros to easily d\n      \tUse of `--no-option` will clear the list of preceding values.\n      \n     +`OPT_STRVEC(short, long, &struct strvec, arg_str, description)`::\n    -+\tIntroduce an option with a string argument.\n    -+\tThe string argument is stored as an element in `strvec`.\n    ++\tIntroduce an option with a string argument, meant to be specified\n    ++\tmultiple times.\n    ++\tThe string argument is stored as an element in `strvec`, and later\n    ++\targuments are added to the same `strvec`.\n     +\tUse of `--no-option` will clear the list of preceding values.\n     +\n      `OPT_INTEGER(short, long, &int_var, description)`::\n 8:  d865772ebc !  8:  8848eeddf2 hook: add 'run' subcommand\n    @@ Commit message\n         supported.\n     \n         Legacy hooks (those present in $GITDIR/hooks) are run at the end of the\n    -    execution list. For now, there is no way to disable them.\n    +    execution list. They can be disabled, or made to print warnings, or to\n    +    prompt before running, with the 'hook.runHookDir' config.\n     \n         Users may wish to provide hook commands like 'git config\n         hook.pre-commit.command \"~/linter.sh --pre-commit\"'. To enable this,\n    @@ Documentation/git-hook.txt: in the order they should be run, and print the confi\n     +run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::\n     +\n     +Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n    -+hook list`. Hooks configured this way are run prepended with `sh -c`, so paths\n    -+containing special characters or spaces should be wrapped in single quotes:\n    -+`command = '/my/path with spaces/script.sh' some args`.\n    ++hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n    ++paths containing special characters or spaces should be wrapped in single\n    ++quotes: `command = '/my/path with spaces/script.sh' some args`.\n     +\n     +OPTIONS\n     +-------\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n     +static int run(int argc, const char **argv, const char *prefix)\n     +{\n     +\tstruct strbuf hookname = STRBUF_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    ++\tstruct run_hooks_opt opt;\n     +\tint rc = 0;\n     +\n     +\tstruct option run_options[] = {\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n     +\t\tOPT_END(),\n     +\t};\n     +\n    -+\t/*\n    -+\t * While it makes sense to list hooks out-of-repo, it doesn't make sense\n    -+\t * to execute them. Hooks usually want to look at repository artifacts.\n    -+\t */\n    -+\tif (!have_git_dir())\n    -+\t\tusage_msg_opt(_(\"You must be in a Git repo to execute hooks.\"),\n    -+\t\t\t      builtin_hook_usage, run_options);\n    ++\trun_hooks_opt_init(&opt);\n     +\n     +\targc = parse_options(argc, argv, prefix, run_options,\n     +\t\t\t     builtin_hook_usage, 0);\n    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)\n     +\n     +\tswitch (cfg)\n     +\t{\n    ++\t\tcase HOOKDIR_ERROR:\n    ++\t\t\tfprintf(stderr, _(\"Skipping legacy hook at '%s'\\n\"),\n    ++\t\t\t\tpath);\n    ++\t\t\t/* FALLTHROUGH */\n     +\t\tcase HOOKDIR_NO:\n     +\t\t\treturn 0;\n    -+\t\tcase HOOKDIR_UNKNOWN:\n    -+\t\t\tfprintf(stderr,\n    -+\t\t\t\t_(\"Unrecognized value for 'hook.runHookDir'. \"\n    -+\t\t\t\t  \"Is there a typo? \"));\n    -+\t\t\t/* FALLTHROUGH */\n     +\t\tcase HOOKDIR_WARN:\n     +\t\t\tfprintf(stderr, _(\"Running legacy hook at '%s'\\n\"),\n     +\t\t\t\tpath);\n    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)\n     +\t\t\t} while (prompt.len); /* an empty reply means \"Yes\" */\n     +\t\t\tstrbuf_release(&prompt);\n     +\t\t\treturn 1;\n    ++\t\t/*\n    ++\t\t * HOOKDIR_UNKNOWN should match the default behavior, but let's\n    ++\t\t * give a heads up to the user.\n    ++\t\t */\n    ++\t\tcase HOOKDIR_UNKNOWN:\n    ++\t\t\tfprintf(stderr,\n    ++\t\t\t\t_(\"Unrecognized value for 'hook.runHookDir'. \"\n    ++\t\t\t\t  \"Is there a typo? \"));\n    ++\t\t\t/* FALLTHROUGH */\n     +\t\tcase HOOKDIR_YES:\n     +\t\tdefault:\n     +\t\t\treturn 1;\n    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n     +\tstrvec_clear(&o->args);\n     +}\n     +\n    ++static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n    ++\t\t\t    struct child_process *cp)\n    ++{\n    ++\tif (!hook)\n    ++\t\treturn;\n    ++\n    ++\tcp->no_stdin = 1;\n    ++\tcp->env = options->env.v;\n    ++\tcp->stdout_to_stderr = 1;\n    ++\tcp->trace2_hook_name = hook->command.buf;\n    ++\n    ++\t/*\n    ++\t * Commands from the config could be oneliners, but we know\n    ++\t * for certain that hookdir commands are not.\n    ++\t */\n    ++\tcp->use_shell = !hook->from_hookdir;\n    ++\n    ++\t/* add command */\n    ++\tstrvec_push(&cp->args, hook->command.buf);\n    ++\n    ++\t/*\n    ++\t * add passed-in argv, without expanding - let the user get back\n    ++\t * exactly what they put in\n    ++\t */\n    ++\tstrvec_pushv(&cp->args, options->args.v);\n    ++}\n    ++\n     +int run_hooks(const char *hookname, struct run_hooks_opt *options)\n     +{\n     +\tstruct strbuf hookname_str = STRBUF_INIT;\n    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n     +\t\tstruct child_process hook_proc = CHILD_PROCESS_INIT;\n     +\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n     +\n    -+\t\thook_proc.env = options->env.v;\n    -+\t\thook_proc.no_stdin = 1;\n    -+\t\thook_proc.stdout_to_stderr = 1;\n    -+\t\thook_proc.trace2_hook_name = hook->command.buf;\n    -+\t\thook_proc.use_shell = 1;\n    -+\n    -+\t\tif (hook->from_hookdir) {\n    -+\t\t    if (!should_include_hookdir(hook->command.buf, options->run_hookdir))\n    ++\t\tif (hook->from_hookdir &&\n    ++\t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n     +\t\t\tcontinue;\n    -+\t\t    /*\n    -+\t\t     * Commands from the config could be oneliners, but we know\n    -+\t\t     * for certain that hookdir commands are not.\n    -+\t\t     */\n    -+\t\t    hook_proc.use_shell = 0;\n    -+\t\t}\n    -+\n    -+\t\t/* add command */\n    -+\t\tstrvec_push(&hook_proc.args, hook->command.buf);\n     +\n    -+\t\t/*\n    -+\t\t * add passed-in argv, without expanding - let the user get back\n    -+\t\t * exactly what they put in\n    -+\t\t */\n    -+\t\tstrvec_pushv(&hook_proc.args, options->args.v);\n    ++\t\tprepare_hook_cp(hook, options, &hook_proc);\n     +\n     +\t\trc |= run_command(&hook_proc);\n     +\t}\n    @@ hook.h\n      #include \"strbuf.h\"\n     +#include \"strvec.h\"\n      \n    - struct hook\n    - {\n    + struct hook {\n    + \tstruct list_head list;\n     @@ hook.h: enum hookdir_opt\n       */\n      enum hookdir_opt configured_hookdir_opt(void);\n    @@ hook.h: enum hookdir_opt\n     +\tenum hookdir_opt run_hookdir;\n     +};\n     +\n    -+#define RUN_HOOKS_OPT_INIT  {   \t\t\\\n    -+\t.env = STRVEC_INIT, \t\t\t\t\\\n    -+\t.args = STRVEC_INIT, \t\t\t\\\n    -+\t.run_hookdir = configured_hookdir_opt()\t\\\n    -+}\n    -+\n     +void run_hooks_opt_init(struct run_hooks_opt *o);\n     +void run_hooks_opt_clear(struct run_hooks_opt *o);\n     +\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = no is resp\n     +\ttest_must_be_empty actual\n      '\n      \n    - test_expect_success 'hook.runHookDir = warn is respected by list' '\n    + test_expect_success 'hook.runHookDir = error is respected by list' '\n    +@@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = error is respected by list' '\n    + \n    + \tgit hook list pre-commit >actual &&\n    + \t# the hookdir annotation is translated\n    ++\ttest_i18ncmp expected actual &&\n    ++\n    ++\tcat >expected <<-EOF &&\n    ++\tSkipping legacy hook at '\\''$(pwd)/.git/hooks/pre-commit'\\''\n    ++\tEOF\n    ++\n    ++\tgit hook run pre-commit 2>actual &&\n    + \ttest_i18ncmp expected actual\n    + '\n    + \n     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is respected by list' '\n      \n      \tgit hook list pre-commit >actual &&\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = interactiv\n     +\tnongit test_must_fail git hook run pre-commit\n      '\n      \n    - test_done\n    + test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n 9:  53a655ed2c !  9:  452f7eea89 hook: replace find_hook() with hook_exists()\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    hook: replace find_hook() with hook_exists()\n    +    hook: introduce hook_exists()\n     \n         Add a helper to easily determine whether any hooks exist for a given\n         hook event.\n    @@ Commit message\n         hook; that check should include the config-based hooks as well. Optimize\n         by checking the config directly. Since commands which execute hooks\n         might want to take args to replace 'hook.runHookDir', let\n    -    'hook_exists()' mirror the behavior of 'hook.runHookDir'.\n    +    'hook_exists()' take a hookdir_opt to override that config.\n     \n    -    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    +    In some cases, external callers today use find_hook() to discover the\n    +    location of a hook and then run it manually with run-command.h (that is,\n    +    not with run_hook_le()). Later, those cases will call hook.h:run_hook()\n    +    directly instead.\n     \n    - ## builtin/bugreport.c ##\n    -@@\n    - #include \"strbuf.h\"\n    - #include \"help.h\"\n    - #include \"compat/compiler.h\"\n    --#include \"run-command.h\"\n    -+#include \"hook.h\"\n    - \n    - \n    - static void get_system_info(struct strbuf *sys_info)\n    -@@ builtin/bugreport.c: static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n    - \t}\n    - \n    - \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n    --\t\tif (find_hook(hook[i]))\n    -+\t\tif (hook_exists(hook[i], configured_hookdir_opt()))\n    - \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n    - }\n    - \n    +    Once the entire codebase is using hook_exists() instead of find_hook(),\n    +    find_hook() can be safely rolled into hook_exists() and removed from\n    +    run-command.h.\n    +\n    +    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## hook.c ##\n     @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)\n    @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)\n     +{\n     +\tconst char *value = NULL; /* throwaway */\n     +\tstruct strbuf hook_key = STRBUF_INIT;\n    ++\tint could_run_hookdir;\n    ++\n    ++\tif (should_run_hookdir == HOOKDIR_USE_CONFIG)\n    ++\t\tshould_run_hookdir = configured_hookdir_opt();\n     +\n    -+\tint could_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||\n    ++\tcould_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||\n     +\t\t\t\tshould_run_hookdir == HOOKDIR_WARN ||\n     +\t\t\t\tshould_run_hookdir == HOOKDIR_YES)\n     +\t\t\t\t&& !!find_hook(hookname);\n    @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)\n      \tstrvec_clear(&o->env);\n     \n      ## hook.h ##\n    +@@ hook.h: struct list_head* hook_list(const struct strbuf *hookname);\n    + \n    + enum hookdir_opt\n    + {\n    ++\tHOOKDIR_USE_CONFIG,\n    + \tHOOKDIR_NO,\n    + \tHOOKDIR_ERROR,\n    + \tHOOKDIR_WARN,\n     @@ hook.h: struct run_hooks_opt\n      void run_hooks_opt_init(struct run_hooks_opt *o);\n      void run_hooks_opt_clear(struct run_hooks_opt *o);\n10:  13abc6ce24 ! 10:  e76507b290 hook: support passing stdin to hooks\n    @@ Documentation/git-hook.txt: in the order they should be run, and print the confi\n     +run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] `<hook-name>`::\n      \n      Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n    - hook list`. Hooks configured this way are run prepended with `sh -c`, so paths\n    + hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n     @@ Documentation/git-hook.txt: Specify arguments to pass to every hook that is run.\n      +\n      Specify environment variables to set for every hook that is run.\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \n     \n      ## hook.c ##\n    -@@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n    - \t\tstruct child_process hook_proc = CHILD_PROCESS_INIT;\n    - \t\tstruct hook *hook = list_entry(pos, struct hook, list);\n    +@@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)\n    + {\n    + \tstrvec_init(&o->env);\n    + \tstrvec_init(&o->args);\n    ++\to->path_to_stdin = NULL;\n    + \to->run_hookdir = configured_hookdir_opt();\n    + }\n    + \n    +@@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n    + \tif (!hook)\n    + \t\treturn;\n      \n    -+\t\t/* reopen the file for stdin; run_command closes it. */\n    -+\t\tif (options->path_to_stdin)\n    -+\t\t\thook_proc.in = xopen(options->path_to_stdin, O_RDONLY);\n    -+\t\telse\n    -+\t\t\thook_proc.no_stdin = 1;\n    +-\tcp->no_stdin = 1;\n    ++\t/* reopen the file for stdin; run_command closes it. */\n    ++\tif (options->path_to_stdin)\n    ++\t\tcp->in = xopen(options->path_to_stdin, O_RDONLY);\n    ++\telse\n    ++\t\tcp->no_stdin = 1;\n     +\n    - \t\thook_proc.env = options->env.v;\n    --\t\thook_proc.no_stdin = 1;\n    - \t\thook_proc.stdout_to_stderr = 1;\n    - \t\thook_proc.trace2_hook_name = hook->command.buf;\n    - \t\thook_proc.use_shell = 1;\n    + \tcp->env = options->env.v;\n    + \tcp->stdout_to_stderr = 1;\n    + \tcp->trace2_hook_name = hook->command.buf;\n     \n      ## hook.h ##\n     @@ hook.h: struct run_hooks_opt\n    @@ hook.h: struct run_hooks_opt\n     +\tconst char *path_to_stdin;\n      };\n      \n    - #define RUN_HOOKS_OPT_INIT  {   \t\t\\\n    --\t.env = STRVEC_INIT, \t\t\t\t\\\n    -+\t.env = STRVEC_INIT, \t\t\t\\\n    - \t.args = STRVEC_INIT, \t\t\t\\\n    -+\t.path_to_stdin = NULL,\t\t\t\\\n    - \t.run_hookdir = configured_hookdir_opt()\t\\\n    - }\n    - \n    + void run_hooks_opt_init(struct run_hooks_opt *o);\n     @@ hook.h: int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);\n      \n      /*\n    @@ hook.h: int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdi\n      \n     \n      ## t/t1360-config-based-hooks.sh ##\n    -@@ t/t1360-config-based-hooks.sh: test_expect_success 'out-of-repo runs excluded' '\n    - \tnongit test_must_fail git hook run pre-commit\n    +@@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n    + \ttest_i18ncmp expected actual\n      '\n      \n     +test_expect_success 'stdin to multiple hooks' '\n11:  0465a9ec94 ! 11:  5f41555e49 run-command: allow stdin for run_processes_parallel\n    @@ run-command.c: static int pp_start_one(struct parallel_processes *pp)\n      \tif (i == pp->max_processes)\n      \t\tBUG(\"bookkeeping is hard\");\n      \n    -+\t/* disallow by default, but allow users to set up stdin if they wish */\n    ++\t/*\n    ++\t * By default, do not inherit stdin from the parent process - otherwise,\n    ++\t * all children would share stdin! Users may overwrite this to provide\n    ++\t * something to the child's stdin by having their 'get_next_task'\n    ++\t * callback assign 0 to .no_stdin and an appropriate integer to .in.\n    ++\t */\n     +\tpp->children[i].process.no_stdin = 1;\n     +\n      \tcode = pp->get_next_task(&pp->children[i].process,\n12:  83eb7805a4 ! 12:  a3bf826304 hook: allow parallel hook execution\n    @@ Documentation/git-hook.txt: in the order they should be run, and print the confi\n     +run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] [(-j|--jobs)<n>] `<hook-name>`::\n      \n      Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n    - hook list`. Hooks configured this way are run prepended with `sh -c`, so paths\n    + hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n     @@ Documentation/git-hook.txt: Specify environment variables to set for every hook that is run.\n      Specify a file which will be streamed into stdin for every hook that is run.\n      Each hook will receive the entire file from beginning to EOF.\n    @@ builtin/hook.c\n      \tNULL\n      };\n      \n    -@@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n    - static int run(int argc, const char **argv, const char *prefix)\n    - {\n    - \tstruct strbuf hookname = STRBUF_INIT;\n    --\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    - \tint rc = 0;\n    - \n    - \tstruct option run_options[] = {\n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \t\t\t   N_(\"argument to pass to hook\")),\n      \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \t\tOPT_END(),\n      \t};\n      \n    +-\trun_hooks_opt_init(&opt);\n    ++\trun_hooks_opt_init_async(&opt);\n    + \n    + \targc = parse_options(argc, argv, prefix, run_options,\n    + \t\t\t     builtin_hook_usage, 0);\n     \n      ## hook.c ##\n     @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)\n    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)\n      static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n      {\n      \tstruct strbuf prompt = STRBUF_INIT;\n    -@@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)\n    +@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n    + \treturn hook_head;\n    + }\n    + \n    +-void run_hooks_opt_init(struct run_hooks_opt *o)\n    ++void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n    + {\n      \tstrvec_init(&o->env);\n      \tstrvec_init(&o->args);\n    + \to->path_to_stdin = NULL;\n      \to->run_hookdir = configured_hookdir_opt();\n    ++\to->jobs = 1;\n    ++}\n    ++\n    ++void run_hooks_opt_init_async(struct run_hooks_opt *o)\n    ++{\n    ++\trun_hooks_opt_init_sync(o);\n     +\to->jobs = configured_hook_jobs();\n      }\n      \n    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)\n      \tstrvec_clear(&o->args);\n      }\n      \n    -+\n    +-static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n    +-\t\t\t    struct child_process *cp)\n     +static int pick_next_hook(struct child_process *cp,\n     +\t\t\t  struct strbuf *out,\n     +\t\t\t  void *pp_cb,\n     +\t\t\t  void **pp_task_cb)\n    -+{\n    + {\n     +\tstruct hook_cb_data *hook_cb = pp_cb;\n    ++\tstruct hook *hook = hook_cb->run_me;\n     +\n    -+\tstruct hook *hook = list_entry(hook_cb->run_me, struct hook, list);\n    -+\n    -+\tif (hook_cb->head == hook_cb->run_me)\n    + \tif (!hook)\n    +-\t\treturn;\n     +\t\treturn 0;\n    -+\n    -+\tcp->env = hook_cb->options->env.v;\n    -+\tcp->stdout_to_stderr = 1;\n    -+\tcp->trace2_hook_name = hook->command.buf;\n    -+\n    -+\t/* reopen the file for stdin; run_command closes it. */\n    + \n    + \t/* reopen the file for stdin; run_command closes it. */\n    +-\tif (options->path_to_stdin)\n    +-\t\tcp->in = xopen(options->path_to_stdin, O_RDONLY);\n    +-\telse\n     +\tif (hook_cb->options->path_to_stdin) {\n     +\t\tcp->no_stdin = 0;\n     +\t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n     +\t} else {\n    -+\t\tcp->no_stdin = 1;\n    + \t\tcp->no_stdin = 1;\n     +\t}\n    -+\n    -+\t/*\n    -+\t * Commands from the config could be oneliners, but we know\n    -+\t * for certain that hookdir commands are not.\n    -+\t */\n    -+\tif (hook->from_hookdir)\n    -+\t\tcp->use_shell = 0;\n    -+\telse\n    -+\t\tcp->use_shell = 1;\n    -+\n    -+\t/* add command */\n    -+\tstrvec_push(&cp->args, hook->command.buf);\n    -+\n    -+\t/*\n    -+\t * add passed-in argv, without expanding - let the user get back\n    -+\t * exactly what they put in\n    -+\t */\n    + \n    +-\tcp->env = options->env.v;\n    ++\tcp->env = hook_cb->options->env.v;\n    + \tcp->stdout_to_stderr = 1;\n    + \tcp->trace2_hook_name = hook->command.buf;\n    + \n    +@@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n    + \t * add passed-in argv, without expanding - let the user get back\n    + \t * exactly what they put in\n    + \t */\n    +-\tstrvec_pushv(&cp->args, options->args.v);\n     +\tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n     +\n     +\t/* Provide context for errors if necessary */\n     +\t*pp_task_cb = hook;\n     +\n     +\t/* Get the next entry ready */\n    -+\thook_cb->run_me = hook_cb->run_me->next;\n    ++\tif (hook_cb->run_me->list.next == hook_cb->head)\n    ++\t\thook_cb->run_me = NULL;\n    ++\telse\n    ++\t\thook_cb->run_me = list_entry(hook_cb->run_me->list.next,\n    ++\t\t\t\t\t     struct hook, list);\n     +\n     +\treturn 1;\n     +}\n    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)\n     +\tstrbuf_addf(out, _(\"Couldn't start '%s', configured in '%s'\\n\"),\n     +\t\t    attempted->command.buf,\n     +\t\t    attempted->from_hookdir ? \"hookdir\"\n    -+\t\t    \t: config_scope_name(attempted->origin));\n    ++\t\t\t: config_scope_name(attempted->origin));\n     +\n     +\t/* NEEDSWORK: if halt_on_error is desired, do it here. */\n     +\treturn 0;\n    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)\n     +\n     +\t/* NEEDSWORK: if halt_on_error is desired, do it here. */\n     +\treturn 0;\n    -+}\n    -+\n    + }\n    + \n      int run_hooks(const char *hookname, struct run_hooks_opt *options)\n      {\n      \tstruct strbuf hookname_str = STRBUF_INIT;\n    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n     -\t\tstruct child_process hook_proc = CHILD_PROCESS_INIT;\n      \t\tstruct hook *hook = list_entry(pos, struct hook, list);\n      \n    --\t\t/* reopen the file for stdin; run_command closes it. */\n    --\t\tif (options->path_to_stdin)\n    --\t\t\thook_proc.in = xopen(options->path_to_stdin, O_RDONLY);\n    --\t\telse\n    --\t\t\thook_proc.no_stdin = 1;\n    --\n    --\t\thook_proc.env = options->env.v;\n    --\t\thook_proc.stdout_to_stderr = 1;\n    --\t\thook_proc.trace2_hook_name = hook->command.buf;\n    --\t\thook_proc.use_shell = 1;\n    --\n    --\t\tif (hook->from_hookdir) {\n    --\t\t    if (!should_include_hookdir(hook->command.buf, options->run_hookdir))\n    + \t\tif (hook->from_hookdir &&\n    + \t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n     -\t\t\tcontinue;\n    --\t\t    /*\n    --\t\t     * Commands from the config could be oneliners, but we know\n    --\t\t     * for certain that hookdir commands are not.\n    --\t\t     */\n    --\t\t    hook_proc.use_shell = 0;\n    --\t\t}\n    --\n    --\t\t/* add command */\n    --\t\tstrvec_push(&hook_proc.args, hook->command.buf);\n    -+\t\tif (hook->from_hookdir &&\n    -+\t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n     +\t\t\t    list_del(pos);\n     +\t}\n    ++\n    ++\tif (list_empty(to_run))\n    ++\t\treturn 0;\n      \n    --\t\t/*\n    --\t\t * add passed-in argv, without expanding - let the user get back\n    --\t\t * exactly what they put in\n    --\t\t */\n    --\t\tstrvec_pushv(&hook_proc.args, options->args.v);\n    +-\t\tprepare_hook_cp(hook, options, &hook_proc);\n     +\tcb_data.head = to_run;\n    -+\tcb_data.run_me = to_run->next;\n    ++\tcb_data.run_me = list_entry(to_run->next, struct hook, list);\n      \n     -\t\trc |= run_command(&hook_proc);\n     -\t}\n    @@ hook.h: enum hookdir_opt\n      \t/* Environment vars to be set for each hook */\n     @@ hook.h: struct run_hooks_opt\n      \n    + \t/*\n    + \t * How should the hookdir be handled?\n    +-\t * Leave the RUN_HOOKS_OPT_INIT default in most cases; this only needs\n    ++\t * Leave the run_hooks_opt_init_*() default in most cases; this only needs\n    + \t * to be overridden if the user can override it at the command line.\n    + \t */\n    + \tenum hookdir_opt run_hookdir;\n    + \n      \t/* Path to file which should be piped to stdin for each hook */\n      \tconst char *path_to_stdin;\n     +\n     +\t/* Number of threads to parallelize across */\n     +\tint jobs;\n    - };\n    - \n    --#define RUN_HOOKS_OPT_INIT  {   \t\t\\\n    ++};\n    ++\n     +/*\n     + * Callback provided to feed_pipe_fn and consume_sideband_fn.\n     + */\n     +struct hook_cb_data {\n     +\tint rc;\n     +\tstruct list_head *head;\n    -+\tstruct list_head *run_me;\n    ++\tstruct hook *run_me;\n     +\tstruct run_hooks_opt *options;\n    -+};\n    -+\n    -+#define RUN_HOOKS_OPT_INIT_SYNC  {   \t\t\\\n    - \t.env = STRVEC_INIT, \t\t\t\\\n    - \t.args = STRVEC_INIT, \t\t\t\\\n    - \t.path_to_stdin = NULL,\t\t\t\\\n    -+\t.jobs = 1,\t\t\t\t\\\n    - \t.run_hookdir = configured_hookdir_opt()\t\\\n    - }\n    + };\n      \n    -+#define RUN_HOOKS_OPT_INIT_ASYNC {\t\t\\\n    -+\t.env = STRVEC_INIT, \t\t\t\\\n    -+\t.args = STRVEC_INIT, \t\t\t\\\n    -+\t.path_to_stdin = NULL,\t\t\t\\\n    -+\t.jobs = configured_hook_jobs(),\t\t\\\n    -+\t.run_hookdir = configured_hookdir_opt()\t\\\n    -+}\n    -+\n    -+\n    - void run_hooks_opt_init(struct run_hooks_opt *o);\n    +-void run_hooks_opt_init(struct run_hooks_opt *o);\n    ++void run_hooks_opt_init_sync(struct run_hooks_opt *o);\n    ++void run_hooks_opt_init_async(struct run_hooks_opt *o);\n      void run_hooks_opt_clear(struct run_hooks_opt *o);\n      \n    + /*\n13:  f84c879d5a <  -:  ---------- hook: allow specifying working directory for hooks\n -:  ---------- > 13:  0c4add98a4 hook: allow specifying working directory for hooks\n14:  ac9cec6587 = 14:  1847c4c675 run-command: add stdin callback for parallelization\n15:  71fca28ccf ! 15:  ab781c94d7 hook: provide stdin by string_list or callback\n    @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *comm\n      \t}\n      \n      \t/* re-set the scope so we show where an override was specified */\n    +@@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n    + \to->run_hookdir = configured_hookdir_opt();\n    + \to->jobs = 1;\n    + \to->dir = NULL;\n    ++\to->feed_pipe = NULL;\n    ++\to->feed_pipe_ctx = NULL;\n    + }\n    + \n    + void run_hooks_opt_init_async(struct run_hooks_opt *o)\n     @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)\n    - {\n    - \tstrvec_clear(&o->env);\n      \tstrvec_clear(&o->args);\n    -+\tstring_list_clear(&o->str_stdin, 0);\n      }\n      \n    - \n    -+static int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n    ++int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n     +{\n     +\tint *item_idx;\n     +\tstruct hook *ctx = pp_task_cb;\n    -+\tstruct string_list *to_pipe = &((struct hook_cb_data*)pp_cb)->options->str_stdin;\n    ++\tstruct string_list *to_pipe = ((struct hook_cb_data*)pp_cb)->options->feed_pipe_ctx;\n     +\n     +\t/* Bootstrap the state manager if necessary. */\n     +\tif (!ctx->feed_pipe_cb_data) {\n    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n      \tif (!options)\n      \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n      \n    -+\tif ((options->path_to_stdin && options->str_stdin.nr) ||\n    -+\t    (options->path_to_stdin && options->feed_pipe) ||\n    -+\t    (options->str_stdin.nr && options->feed_pipe))\n    ++\tif (options->path_to_stdin && options->feed_pipe)\n     +\t\tBUG(\"choose only one method to populate stdin\");\n    -+\n    -+\tif (options->str_stdin.nr)\n    -+\t\toptions->feed_pipe = &pipe_from_string_list;\n     +\n      \tstrbuf_addstr(&hookname_str, hookname);\n      \n    @@ hook.h\n      #include \"strvec.h\"\n     +#include \"run-command.h\"\n      \n    - struct hook\n    - {\n    -@@ hook.h: struct hook\n    + struct hook {\n    + \tstruct list_head list;\n    +@@ hook.h: struct hook {\n      \t/* The literal command to run. */\n      \tstruct strbuf command;\n    - \tint from_hookdir;\n    + \tunsigned from_hookdir : 1;\n     +\n     +\t/*\n     +\t * Use this to keep state for your feed_pipe_fn if you are using\n    @@ hook.h: struct run_hooks_opt\n      \n      \t/* Path to file which should be piped to stdin for each hook */\n      \tconst char *path_to_stdin;\n    -+\t/* Pipe each string to stdin, separated by newlines */\n    -+\tstruct string_list str_stdin;\n     +\t/*\n     +\t * Callback and state pointer to ask for more content to pipe to stdin.\n     +\t * Will be called repeatedly, for each hook. See\n     +\t * hook.c:pipe_from_stdin() for an example. Keep per-hook state in\n     +\t * hook.feed_pipe_cb_data (per process). Keep initialization context in\n     +\t * feed_pipe_ctx (shared by all processes).\n    ++\t *\n    ++\t * See 'pipe_from_string_list()' for info about how to specify a\n    ++\t * string_list as the stdin input instead of writing your own handler.\n     +\t */\n     +\tfeed_pipe_fn feed_pipe;\n     +\tvoid *feed_pipe_ctx;\n    @@ hook.h: struct run_hooks_opt\n     +\n      };\n      \n    ++/*\n    ++ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n    ++ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n    ++ * This will pipe each string in the list to stdin, separated by newlines.  (Do\n    ++ * not inject your own newlines.)\n    ++ */\n    ++int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n    ++\n      /*\n    -@@ hook.h: struct hook_cb_data {\n    - \t.path_to_stdin = NULL,\t\t\t\\\n    - \t.jobs = 1,\t\t\t\t\\\n    - \t.dir = NULL,\t\t\t\t\\\n    -+\t.str_stdin = STRING_LIST_INIT_DUP,\t\\\n    -+\t.feed_pipe = NULL,\t\t\t\\\n    -+\t.feed_pipe_ctx = NULL,\t\t\t\\\n    - \t.run_hookdir = configured_hookdir_opt()\t\\\n    - }\n    - \n    -@@ hook.h: struct hook_cb_data {\n    - \t.path_to_stdin = NULL,\t\t\t\\\n    - \t.jobs = configured_hook_jobs(),\t\t\\\n    - \t.dir = NULL,\t\t\t\t\\\n    -+\t.str_stdin = STRING_LIST_INIT_DUP,\t\\\n    -+\t.feed_pipe = NULL,\t\t\t\\\n    -+\t.feed_pipe_ctx = NULL,\t\t\t\\\n    - \t.run_hookdir = configured_hookdir_opt()\t\\\n    - }\n    - \n    +  * Callback provided to feed_pipe_fn and consume_sideband_fn.\n    +  */\n16:  98253fa8fd = 16:  c51bf46e8d run-command: allow capturing of collated output\n17:  9505812b74 ! 17:  b90a4ee79b hooks: allow callers to capture output\n    @@ Commit message\n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## hook.c ##\n    +@@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n    + \to->dir = NULL;\n    + \to->feed_pipe = NULL;\n    + \to->feed_pipe_ctx = NULL;\n    ++\to->consume_sideband = NULL;\n    + }\n    + \n    + void run_hooks_opt_init_async(struct run_hooks_opt *o)\n     @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n      \t\t\t\t   pick_next_hook,\n      \t\t\t\t   notify_start_failure,\n    @@ hook.h: struct run_hooks_opt\n      \t/* Number of threads to parallelize across */\n      \tint jobs;\n      \n    -@@ hook.h: struct hook_cb_data {\n    - \t.str_stdin = STRING_LIST_INIT_DUP,\t\\\n    - \t.feed_pipe = NULL,\t\t\t\\\n    - \t.feed_pipe_ctx = NULL,\t\t\t\\\n    -+\t.consume_sideband = NULL,\t\t\\\n    - \t.run_hookdir = configured_hookdir_opt()\t\\\n    - }\n    - \n    -@@ hook.h: struct hook_cb_data {\n    - \t.str_stdin = STRING_LIST_INIT_DUP,\t\\\n    - \t.feed_pipe = NULL,\t\t\t\\\n    - \t.feed_pipe_ctx = NULL,\t\t\t\\\n    -+\t.consume_sideband = NULL,\t\t\\\n    - \t.run_hookdir = configured_hookdir_opt()\t\\\n    - }\n    - \n"},{"id":"420120","messageId":"YFt3WOAGVKohAF6X@google.com","threadId":"55286","inReplyTo":"87o8fohjgq.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 03/37] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-24T17:31:04Z","receivedAt":"2021-03-24T17:31:54Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 09:20:05AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> > new file mode 100644\n> > index 0000000000..71449ecbc7\n> > --- /dev/null\n> > +++ b/Documentation/config/hook.txt\n> > @@ -0,0 +1,9 @@\n> > +hook.<command>.command::\n> > +\tA command to execute during the <command> hook event. This can be an\n> > +\texecutable on your device, a oneliner for your shell, or the name of a\n> > +\thookcmd. See linkgit:git-hook[1].\n> > +\n> > +hookcmd.<name>.command::\n> > +\tA command to execute during a hook for which <name> has been specified\n> > +\tas a command. This can be an executable on your device or a oneliner for\n> > +\tyour shell. See linkgit:git-hook[1].\n> > diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> > index 9eeab0009d..f19875ed68 100644\n> > --- a/Documentation/git-hook.txt\n> > +++ b/Documentation/git-hook.txt\n> > @@ -8,12 +8,65 @@ git-hook - Manage configured hooks\n> >  SYNOPSIS\n> >  --------\n> >  [verse]\n> > -'git hook'\n> > +'git hook' list <hook-name>\n> \n> Having just read this far (maybe this pattern is shared in the rest of\n> the series): Let's just squash this and the 2nd patch together.\n> \n> Sometimes it's worth doing the scaffolding first, but adding a new\n> built-in is so trivial that I don't think it's worth it, and it just\n> results in back & forth churn like the above...\n\nYeah, I think you are right here :)\n\n> > +void free_hook(struct hook *ptr)\n> > +{\n> > +\tif (ptr) {\n> > +\t\tstrbuf_release(&ptr->command);\n> > +\t\tfree(ptr);\n> > +\t}\n> > +}\n> \n> Neither strbuf_release() nor free() need or should have a \"if (ptr)\" guard.\n\nI'll take free() out of the if guard, but I think\n'strbuf_release(&<null>->command)' will go poorly - dereferencing the\nNULL to even invoke strbuf_release will not be a happy time, and\nstrbuf_release internally is not NULL-resistant.\n\n> > +struct list_head* hook_list(const struct strbuf* hookname)\n> > +{\n> > +\tstruct strbuf hook_key = STRBUF_INIT;\n> > +\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> > +\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n> > +\n> > +\tINIT_LIST_HEAD(hook_head);\n> > +\n> > +\tif (!hookname)\n> > +\t\treturn NULL;\n> \n> ...if a strbuf being passed in is NULL?\n\nYeah, I think this is misplaced. But since it sounds like generally\nfolks don't like having the strbuf at the input here, I will address the\nerror checking then also.\n\n> \n> > [...]\n> > +ROOT=\n> > +if test_have_prereq MINGW\n> > +then\n> > +\t# In Git for Windows, Unix-like paths work only in shell scripts;\n> > +\t# `git.exe`, however, will prefix them with the pseudo root directory\n> > +\t# (of the Unix shell). Let's accommodate for that.\n> > +\tROOT=\"$(cd / && pwd)\"\n> > +fi\n> \n> I didn't read up on previous rounds, but if we're squashing this into 02\n> having a seperate commit summarizing this little hack would be most\n> welcome, or have it in this commit message.\n\nSure. I squashed it in from a commit dscho sent, so I can preserve that\ncommit in tree instead.\n\n> \n> Isn't this sort of thing generally usable, maybe we can add it under a\n> longer variable name to test-lib.sh?\n\nI wonder. `git grep cd \\/ &&` shows me that this hack also happens in\nt1509-root-work-tree.sh. I think most tests must use relative paths, so\nthis must not be in broad use? But since it's not used elsewhere I feel\nambivalent about adding a helper to test-lib.sh. I can if you feel\nstrongly :)\n\n - Emily\n"},{"id":"420124","messageId":"YFt9MQsJAJsjWqo2@google.com","threadId":"55286","inReplyTo":"87lfashj03.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 04/37] hook: include hookdir hook in list","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-24T17:56:01Z","receivedAt":"2021-03-24T17:57:13Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 09:30:04AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> > Historically, hooks are declared by placing an executable into\n> > $GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken\n> > from the config are more featureful than hooks placed in the $HOOKDIR,\n> > those hooks should not stop working for users who already have them.\n> > Let's list them to the user, but instead of displaying a config scope\n> > (e.g. \"global: blah\") we can prefix them with \"hookdir:\".\n> >\n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >\n> > Notes:\n> >     Since v7, fix some nits from Jonathan Tan. The largest is to move reference to\n> >     \"hookdir annotation\" from this commit to the next one which introduces the\n> >     hook.runHookDir option.\n> >\n> >  builtin/hook.c                | 11 +++++++++--\n> >  hook.c                        | 17 +++++++++++++++++\n> >  hook.h                        |  1 +\n> >  t/t1360-config-based-hooks.sh | 19 +++++++++++++++++++\n> >  4 files changed, 46 insertions(+), 2 deletions(-)\n> >\n> > diff --git a/builtin/hook.c b/builtin/hook.c\n> > index bb64cd77ca..c8fbfbb39d 100644\n> > --- a/builtin/hook.c\n> > +++ b/builtin/hook.c\n> > @@ -40,10 +40,15 @@ static int list(int argc, const char **argv, const char *prefix)\n> >  \n> >  \tlist_for_each(pos, head) {\n> >  \t\tstruct hook *item = list_entry(pos, struct hook, list);\n> > -\t\tif (item)\n> > +\t\titem = list_entry(pos, struct hook, list);\n> > +\t\tif (item) {\n> > +\t\t\t/* Don't translate 'hookdir' - it matches the config */\n> \n> Let's prefix comments for translators with /* TRANSLATORS: .., see the\n> coding style doc. That's what they'll see, and this is useful to them.\n> \n> Better yet have a note here about the first argument being 'system',\n> 'local' etc., which I had to source spelunge for, and translators won't\n> have any idea about unless the magic parameter is documented.\n\nIt's not a comment for translators. It's a comment for someone helpful\nwho comes later and says \"oh, none of this is marked for translation,\nI'd better fix that.\"\n\n> \n> > +setup_hookdir () {\n> > +\tmkdir .git/hooks\n> > +\twrite_script .git/hooks/pre-commit <<-EOF\n> > +\techo \\\"Legacy Hook\\\"\n> \n> Nit, \"'s not needed, but it also seems nothing uses this, so if it's\n> just a pass-through script either \"exit 0\", or actually check if it's\n> run or something?\n\nThe output is checked in the run tests later on. I can remove it for\nthis commit if you want.\n\n> \n> > [...]\n> > +test_expect_success 'git hook list shows hooks from the hookdir' '\n> > +\tsetup_hookdir &&\n> > +\n> > +\tcat >expected <<-EOF &&\n> > +\thookdir: $(pwd)/.git/hooks/pre-commit\n> > +\tEOF\n> > +\n> > +\tgit hook list pre-commit >actual &&\n> > +\ttest_cmp expected actual\n> > +'\n> \n> Ah, so it's just checking if it exists...\n"},{"id":"420128","messageId":"YFuJGG1MUokL4o4i@google.com","threadId":"55286","inReplyTo":"87im5whitx.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 05/37] hook: teach hook.runHookDir","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-24T18:46:48Z","receivedAt":"2021-03-24T18:47:33Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 09:33:46AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> > +\tswitch (should_run_hookdir) {\n> > +\t\tcase HOOKDIR_NO:\n> \n> Style: case shouldn't be indented\n\nDone, thanks.\n\n> \n> > +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will not run)\"));\n> > +\t\t\tbreak;\n> > +\t\tcase HOOKDIR_ERROR:\n> > +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will error and not run)\"));\n> > +\t\t\tbreak;\n> > +\t\tcase HOOKDIR_INTERACTIVE:\n> > +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will prompt)\"));\n> > +\t\t\tbreak;\n> > +\t\tcase HOOKDIR_WARN:\n> > +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will warn but run)\"));\n> > +\t\t\tbreak;\n> > +\t\tcase HOOKDIR_YES:\n> > +\t\t/*\n> > +\t\t * The default behavior should agree with\n> > +\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n> > +\t\t * do the default behavior.\n> > +\t\t */\n> > +\t\tcase HOOKDIR_UNKNOWN:\n> > +\t\tdefault:\n> > +\t\t\tbreak;\n> \n> We should avoid this sort of translation lego.\n> \n> > +\t}\n> > +\n> >  \tlist_for_each(pos, head) {\n> >  \t\tstruct hook *item = list_entry(pos, struct hook, list);\n> >  \t\titem = list_entry(pos, struct hook, list);\n> >  \t\tif (item) {\n> >  \t\t\t/* Don't translate 'hookdir' - it matches the config */\n> > -\t\t\tprintf(\"%s: %s\\n\",\n> > +\t\t\tprintf(\"%s: %s%s\\n\",\n> \n> native speakers in some languages to read the sentance backwards.\n> Because if you concatenate strings like this you force.\n> \n> (We don't currently have a RTL language in po/, still, but let's not\n> create churn for if/when we do if we can help it)>\n\nYeah, you are absolutely right. I'll take a look at your suggestion,\nthanks.\n\n> \n> \n> I have a patch on top to fix this, will send it as some general reply of\n> proposed fixup.s\n\nFWIW, I found this format of suggestions really hard to navigate. I had\nto go find your fixup (and I think you sent two different ones) and then\nhad to scroll around and find what you're referring to from here. If I\nwere to apply the fixup directly, I'd have to split it up and find the\nappropriate commits to associate each part of the diff with. I know\ngenerating scissors patches for each review is a pain, but I'd even\nprefer a prose \"How about printing in each part of the case instead, so\neach string can be translated in full\" or a non-formatted example inline\nto the catchall fixups patch you sent.\n\n> \n> >  \t\t\t       (item->from_hookdir\n> > +\tgit hook list pre-commit >actual &&\n> > +\t# the hookdir annotation is translated\n> > +\ttest_i18ncmp expected actual\n> \n> This (and the rest of test_i18ncmp in this series) can and should just\n> be \"test_cmp\" or \"test_i18ncmp\", the poison mode is dead. See my recent\n> patches to search/replace test_i18ncmp.\n> \n> The reason the function isn't gone entirely was to help a series like\n> yours in \"seen\", but if we're re-rolling...\n\nOh cool, thanks, will do.\n"},{"id":"420132","messageId":"xmqq1rc4z7ry.fsf@gitster.g","threadId":"55286","inReplyTo":"YFt9MQsJAJsjWqo2@google.com","subject":"Re: [PATCH v8 04/37] hook: include hookdir hook in list","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-24T19:11:29Z","receivedAt":"2021-03-24T19:12:27Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n>> > @@ -40,10 +40,15 @@ static int list(int argc, const char **argv, const char *prefix)\n>> >  \n>> >  \tlist_for_each(pos, head) {\n>> >  \t\tstruct hook *item = list_entry(pos, struct hook, list);\n>> > -\t\tif (item)\n>> > +\t\titem = list_entry(pos, struct hook, list);\n>> > +\t\tif (item) {\n>> > +\t\t\t/* Don't translate 'hookdir' - it matches the config */\n>> \n>> Let's prefix comments for translators with /* TRANSLATORS: .., see the\n>> coding style doc. That's what they'll see, and this is useful to them.\n>> \n>> Better yet have a note here about the first argument being 'system',\n>> 'local' etc., which I had to source spelunge for, and translators won't\n>> have any idea about unless the magic parameter is documented.\n>\n> It's not a comment for translators. It's a comment for someone helpful\n> who comes later and says \"oh, none of this is marked for translation,\n> I'd better fix that.\"\n\nThen, it is not limited to \"hookdir\", is it?  Resurrecting the\nelided part back here:\n\nNot just we do not want \"hookdir\" placed inside _(),\n\n \t\t\tprintf(\"%s: %s\\n\",\n+\t\t\t       (item->from_hookdir\n+\t\t\t\t? \"hookdir\"\n+\t\t\t\t: config_scope_name(item->origin)),\n \t\t\t       item->command.buf);\n\nwe do not want the \"%s: %s\\n\" to be placed inside _() and get munged\ninto \"%2$s: %1$s\\n\" for languages that want the order swapped, for\nexample.\n\nSo perhaps the comment should be about the entire output, i.e.\n\"don't translate the output from this helper, as it is meant to be\nmachine parseable\", or something?\n\n"},{"id":"420136","messageId":"CAPig+cRcWUgM=c9k-P1hGZGJEZpSgHXK=_ribJeLVsiQWV3LnQ@mail.gmail.com","threadId":"55286","inReplyTo":"xmqq1rc4z7ry.fsf@gitster.g","subject":"Re: [PATCH v8 04/37] hook: include hookdir hook in list","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-03-24T19:23:30Z","receivedAt":"2021-03-24T19:24:27Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Wed, Mar 24, 2021 at 3:12 PM Junio C Hamano <gitster@pobox.com> wrote:\n> Not just we do not want \"hookdir\" placed inside _(),\n>\n>                         printf(\"%s: %s\\n\",\n> +                              (item->from_hookdir\n> +                               ? \"hookdir\"\n> +                               : config_scope_name(item->origin)),\n>                                item->command.buf);\n>\n> we do not want the \"%s: %s\\n\" to be placed inside _() and get munged\n> into \"%2$s: %1$s\\n\" for languages that want the order swapped, for\n> example.\n>\n> So perhaps the comment should be about the entire output, i.e.\n> \"don't translate the output from this helper, as it is meant to be\n> machine parseable\", or something?\n\nHaving the word \"translate\" in the comment automatically implies\nlocalization, which confuses the issue. It would be clearer to avoid\nthat word altogether. Perhaps something along the lines of:\n\n    /* machine-parseable output; do not apply _() localization */\n"},{"id":"420142","messageId":"YFucFoVsJXi1UBX+@google.com","threadId":"55286","inReplyTo":"CAPig+cRcWUgM=c9k-P1hGZGJEZpSgHXK=_ribJeLVsiQWV3LnQ@mail.gmail.com","subject":"Re: [PATCH v8 04/37] hook: include hookdir hook in list","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-24T20:07:50Z","receivedAt":"2021-03-24T20:08:39Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Mar 24, 2021 at 03:23:30PM -0400, Eric Sunshine wrote:\n> \n> On Wed, Mar 24, 2021 at 3:12 PM Junio C Hamano <gitster@pobox.com> wrote:\n> > Not just we do not want \"hookdir\" placed inside _(),\n> >\n> >                         printf(\"%s: %s\\n\",\n> > +                              (item->from_hookdir\n> > +                               ? \"hookdir\"\n> > +                               : config_scope_name(item->origin)),\n> >                                item->command.buf);\n> >\n> > we do not want the \"%s: %s\\n\" to be placed inside _() and get munged\n> > into \"%2$s: %1$s\\n\" for languages that want the order swapped, for\n> > example.\n> >\n> > So perhaps the comment should be about the entire output, i.e.\n> > \"don't translate the output from this helper, as it is meant to be\n> > machine parseable\", or something?\n> \n> Having the word \"translate\" in the comment automatically implies\n> localization, which confuses the issue. It would be clearer to avoid\n> that word altogether. Perhaps something along the lines of:\n> \n>     /* machine-parseable output; do not apply _() localization */\n\nAfter I read Ævar's comments on the next patch in this series, I decided\nto rework the comments and translation markers for this whole section.\n\n  if (item) {\n          if (item->from_hookdir) {\n                  /*\n                   * TRANSLATORS: do not translate 'hookdir' as\n                   * it matches the config setting.\n                   */\n                  switch (should_run_hookdir) {\n                  case HOOKDIR_NO:\n                          printf(_(\"hookdir: %s (will not run)\\n\"),\n                                 item->command.buf);\n                          break;\n                  case HOOKDIR_ERROR:\n                          printf(_(\"hookdir: %s (will error and not run)\\n\"),\n                                 item->command.buf);\n                          break;\n                  case HOOKDIR_INTERACTIVE:\n                          printf(_(\"hookdir: %s (will prompt)\\n\"),\n                                 item->command.buf);\n                          break;\n                  case HOOKDIR_WARN:\n                          printf(_(\"hookdir: %s (will warn but run)\\n\"),\n                                 item->command.buf);\n                          break;\n                  case HOOKDIR_YES:\n                  /*\n                   * The default behavior should agree with\n                   * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n                   * do the default behavior.\n                   */\n                  case HOOKDIR_UNKNOWN:\n                  default:\n                          printf(_(\"hookdir: %s\\n\"),\n                                   item->command.buf);\n                          break;\n                  }\n          } else {\n                  /*\n                   * TRANSLATORS: \"<config scope>: <path>\". Both fields\n                   * should be left untranslated; config scope matches the\n                   * output of 'git config --show-scope'. Marked for\n                   * translation to provide better RTL support later.\n                   */\n                  printf(_(\"%s: %s\\n\"),\n                          config_scope_name(item->origin),\n                          item->command.buf);\n          }\n  }\n"},{"id":"420147","messageId":"YFuiTQx1sHPvntC8@google.com","threadId":"55286","inReplyTo":"87czw4hi19.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 07/37] parse-options: parse into strvec","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-24T20:34:21Z","receivedAt":"2021-03-24T20:35:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 09:50:58AM +0100, Ævar Arnfjörð Bjarmason wrote:\n[snip]\n> Nice, seems very useful.\n> \n> But let's add a test in test-parse-options.c like we have for\n> string_list?\n\nSure, done. Thanks.\n"},{"id":"420150","messageId":"YFuvUlMOaJ0qAxzk@google.com","threadId":"55286","inReplyTo":"87a6r8hhvf.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 08/37] hook: add 'run' subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-24T21:29:54Z","receivedAt":"2021-03-24T21:31:06Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 09:54:28AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> >  'git hook' list <hook-name>\n> > +'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>\n> \n> [...]\n> \n> > +\tswitch (cfg)\n> > +\t{\n> > +\t\tcase HOOKDIR_ERROR:\n> \n> Overly indented case statements again.\nThanks. Probably need to fix my autoformatter or something.\n> \n> > +\t\t\tfprintf(stderr, _(\"Skipping legacy hook at '%s'\\n\"),\n> > +\t\t\t\tpath);\n> > +\t\t\t/* FALLTHROUGH */\n> > +\t\tcase HOOKDIR_NO:\n> > +\t\t\treturn 0;\n> > +\t\tcase HOOKDIR_WARN:\n> > +\t\t\tfprintf(stderr, _(\"Running legacy hook at '%s'\\n\"),\n> > +\t\t\t\tpath);\n> > +\t\t\treturn 1;\n> > +\t\tcase HOOKDIR_INTERACTIVE:\n> > +\t\t\tdo {\n> > +\t\t\t\t/*\n> > +\t\t\t\t * TRANSLATORS: Make sure to include [Y] and [n]\n> > +\t\t\t\t * in your translation. Only English input is\n> > +\t\t\t\t * accepted. Default option is \"yes\".\n> > +\t\t\t\t */\n> > +\t\t\t\tfprintf(stderr, _(\"Run '%s'? [Yn] \"), path);\n> \n> Nit: [Y/n]\nACK\n\n> \n> > +\t\t\t\t} else if (starts_with(prompt.buf, \"y\")) {\n> \n> So also \"Y\", \"yes\" and \"yellow\"...\nThat's also how add-patch.c:prompt_yesno(),\nbuiltin/bisect--helper.c:decide_next(), and\ngit-add--interactive.perl:prompt_yesno (assuming I'm grokking the perl\ncorrectly) work. builtin/clean.c:ask_each_cmd checks that the user's\nreply matches a substring anchored to the beginning (e.g. \"y\", \"ye\",\n\"yes\").  git-diftool--helper.sh:launch_merge_tool just checks for \"not\n'n'\". And git-send-email.perl:ask just checks for \"'y' or not\".\n\nSo I think there's a little flexibility :) But I like builtin/clean.c's\napproach most out of these, so I'll switch.\n\n> \n> > [...]\n> >  \tgit hook list pre-commit >actual &&\n> >  \t# the hookdir annotation is translated\n> > -\ttest_i18ncmp expected actual\n> > +\ttest_i18ncmp expected actual &&\n> > +\n> > +\ttest_write_lines n | git hook run pre-commit 2>actual &&\n> > +\t! grep \"Legacy Hook\" actual &&\n> > +\n> > +\ttest_write_lines y | git hook run pre-commit 2>actual &&\n> > +\tgrep \"Legacy Hook\" actual\n> > +'\n> > +\n> > +test_expect_success 'inline hook definitions execute oneliners' '\n> > +\ttest_config hook.pre-commit.command \"echo \\\"Hello World\\\"\" &&\n> > +\n> > +\techo \"Hello World\" >expected &&\n> > +\n> > +\t# hooks are run with stdout_to_stderr = 1\n> > +\tgit hook run pre-commit 2>actual &&\n> > +\ttest_cmp expected actual\n> > +'\n> > +\n> > +test_expect_success 'inline hook definitions resolve paths' '\n> > +\twrite_script sample-hook.sh <<-EOF &&\n> > +\techo \\\"Sample Hook\\\"\n> > +\tEOF\n> > +\n> > +\ttest_when_finished \"rm sample-hook.sh\" &&\n> > +\n> > +\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n> > +\n> > +\techo \\\"Sample Hook\\\" >expected &&\n> > +\n> > +\t# hooks are run with stdout_to_stderr = 1\n> > +\tgit hook run pre-commit 2>actual &&\n> > +\ttest_cmp expected actual\n> > +'\n> > +\n> > +test_expect_success 'hookdir hook included in git hook run' '\n> > +\tsetup_hookdir &&\n> > +\n> > +\techo \\\"Legacy Hook\\\" >expected &&\n> > +\n> > +\t# hooks are run with stdout_to_stderr = 1\n> > +\tgit hook run pre-commit 2>actual &&\n> > +\ttest_cmp expected actual\n> > +'\n> > +\n> > +test_expect_success 'out-of-repo runs excluded' '\n> > +\tsetup_hooks &&\n> > +\n> > +\tnongit test_must_fail git hook run pre-commit\n> >  '\n> >  \n> >  test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n> \n> No tests for --env or --arg?\n\nYikes, I guess not. Thanks, will add.\n\n - Emily\n"},{"id":"420159","messageId":"YFu1DeF5PSiY8XR1@google.com","threadId":"55286","inReplyTo":"874khghh8r.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 17/37] hooks: allow callers to capture output","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-24T21:54:21Z","receivedAt":"2021-03-24T21:55:18Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 10:08:04AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> > Some server-side hooks will require capturing output to send over\n> > sideband instead of printing directly to stderr. Expose that capability.\n> \n> So added here in 17/37 and not used until 30/37. As a point on\n> readability (this isn't the first such patch) I think it would be better\n> to just squash those together with some \"since we now need access to\n> consume_sideband in hooks, do that ...\".\n\nYeah. When I was putting together the series I had two thoughts on how\nbest to organize it:\n\n1. Adding functionality just-in-time for the hook that needs it (like\nyou describe)\nor\n2. Implementing the whole utility, then doing hook conversions in a\nseparate chunk or series (what I went with).\n\nI chose 2 for a couple reasons: that it would be easier for people who\njust care \"did a hook I use start working differently?\" to review only\nthe second chunk of the change, and that it would be easier if we wanted\nto adopt the library part into the codebase without converting the hooks\nto use it (this was listed as a step in the design doc, but I think we\nended up abandoning it). The differentiation was certainly easier when I\nhad the two \"chunks\" separated into a part I and part II series, but\nJunio asked me to combine them starting with this revision so it would\nbe easier to merge to 'seen' (as I understood it).\n\nAt this point, I'd prefer not to rearrange the series, though.\n\n - Emily\n"},{"id":"420161","messageId":"87o8f8p48b.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YFuJGG1MUokL4o4i@google.com","subject":"Re: [PATCH v8 05/37] hook: teach hook.runHookDir","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-24T22:38:12Z","receivedAt":"2021-03-24T22:39:13Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Mar 24 2021, Emily Shaffer wrote:\n\n> On Fri, Mar 12, 2021 at 09:33:46AM +0100, ï¿½var Arnfjï¿½rï¿½ Bjarmason wrote:\n>> \n>> \n>> On Thu, Mar 11 2021, Emily Shaffer wrote:\n>> \n>> > +\tswitch (should_run_hookdir) {\n>> > +\t\tcase HOOKDIR_NO:\n>> \n>> Style: case shouldn't be indented\n>\n> Done, thanks.\n>\n>> \n>> > +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will not run)\"));\n>> > +\t\t\tbreak;\n>> > +\t\tcase HOOKDIR_ERROR:\n>> > +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will error and not run)\"));\n>> > +\t\t\tbreak;\n>> > +\t\tcase HOOKDIR_INTERACTIVE:\n>> > +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will prompt)\"));\n>> > +\t\t\tbreak;\n>> > +\t\tcase HOOKDIR_WARN:\n>> > +\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will warn but run)\"));\n>> > +\t\t\tbreak;\n>> > +\t\tcase HOOKDIR_YES:\n>> > +\t\t/*\n>> > +\t\t * The default behavior should agree with\n>> > +\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n>> > +\t\t * do the default behavior.\n>> > +\t\t */\n>> > +\t\tcase HOOKDIR_UNKNOWN:\n>> > +\t\tdefault:\n>> > +\t\t\tbreak;\n>> \n>> We should avoid this sort of translation lego.\n>> \n>> > +\t}\n>> > +\n>> >  \tlist_for_each(pos, head) {\n>> >  \t\tstruct hook *item = list_entry(pos, struct hook, list);\n>> >  \t\titem = list_entry(pos, struct hook, list);\n>> >  \t\tif (item) {\n>> >  \t\t\t/* Don't translate 'hookdir' - it matches the config */\n>> > -\t\t\tprintf(\"%s: %s\\n\",\n>> > +\t\t\tprintf(\"%s: %s%s\\n\",\n>> \n>> native speakers in some languages to read the sentance backwards.\n>> Because if you concatenate strings like this you force.\n>> \n>> (We don't currently have a RTL language in po/, still, but let's not\n>> create churn for if/when we do if we can help it)>\n>\n> Yeah, you are absolutely right. I'll take a look at your suggestion,\n> thanks.\n>\n>> \n>> \n>> I have a patch on top to fix this, will send it as some general reply of\n>> proposed fixup.s\n>\n> FWIW, I found this format of suggestions really hard to navigate. I had\n> to go find your fixup (and I think you sent two different ones) and then\n> had to scroll around and find what you're referring to from here. If I\n> were to apply the fixup directly, I'd have to split it up and find the\n> appropriate commits to associate each part of the diff with. I know\n> generating scissors patches for each review is a pain, but I'd even\n> prefer a prose \"How about printing in each part of the case instead, so\n> each string can be translated in full\" or a non-formatted example inline\n> to the catchall fixups patch you sent.\n\nSorry about that. FWIW I did try, but found myself jumping a bit too\nmuch between different commits (as noted in some \"maybe squash?\"\ncomments elsewhere), and eventually just gave up and produced some\nRFC-just-for-discussion patch on top of the whole thing and mostly\nlooked at the entire diff forthe series.\n\n>> \n>> >  \t\t\t       (item->from_hookdir\n>> > +\tgit hook list pre-commit >actual &&\n>> > +\t# the hookdir annotation is translated\n>> > +\ttest_i18ncmp expected actual\n>> \n>> This (and the rest of test_i18ncmp in this series) can and should just\n>> be \"test_cmp\" or \"test_i18ncmp\", the poison mode is dead. See my recent\n>> patches to search/replace test_i18ncmp.\n>> \n>> The reason the function isn't gone entirely was to help a series like\n>> yours in \"seen\", but if we're re-rolling...\n>\n> Oh cool, thanks, will do.\n\n"},{"id":"420190","messageId":"871rc3pg04.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YFt3WOAGVKohAF6X@google.com","subject":"Re: [PATCH v8 03/37] hook: add list command","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-25T12:36:11Z","receivedAt":"2021-03-25T12:37:07Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Mar 24 2021, Emily Shaffer wrote:\n\n> On Fri, Mar 12, 2021 at 09:20:05AM +0100, ï¿½var Arnfjï¿½rï¿½ Bjarmason wrote:\n>> \n>> \n>> On Thu, Mar 11 2021, Emily Shaffer wrote:\n>> \n>> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n>> > new file mode 100644\n>> > index 0000000000..71449ecbc7\n>> > --- /dev/null\n>> > +++ b/Documentation/config/hook.txt\n>> > @@ -0,0 +1,9 @@\n>> > +hook.<command>.command::\n>> > +\tA command to execute during the <command> hook event. This can be an\n>> > +\texecutable on your device, a oneliner for your shell, or the name of a\n>> > +\thookcmd. See linkgit:git-hook[1].\n>> > +\n>> > +hookcmd.<name>.command::\n>> > +\tA command to execute during a hook for which <name> has been specified\n>> > +\tas a command. This can be an executable on your device or a oneliner for\n>> > +\tyour shell. See linkgit:git-hook[1].\n>> > diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n>> > index 9eeab0009d..f19875ed68 100644\n>> > --- a/Documentation/git-hook.txt\n>> > +++ b/Documentation/git-hook.txt\n>> > @@ -8,12 +8,65 @@ git-hook - Manage configured hooks\n>> >  SYNOPSIS\n>> >  --------\n>> >  [verse]\n>> > -'git hook'\n>> > +'git hook' list <hook-name>\n>> \n>> Having just read this far (maybe this pattern is shared in the rest of\n>> the series): Let's just squash this and the 2nd patch together.\n>> \n>> Sometimes it's worth doing the scaffolding first, but adding a new\n>> built-in is so trivial that I don't think it's worth it, and it just\n>> results in back & forth churn like the above...\n>\n> Yeah, I think you are right here :)\n>\n>> > +void free_hook(struct hook *ptr)\n>> > +{\n>> > +\tif (ptr) {\n>> > +\t\tstrbuf_release(&ptr->command);\n>> > +\t\tfree(ptr);\n>> > +\t}\n>> > +}\n>> \n>> Neither strbuf_release() nor free() need or should have a \"if (ptr)\" guard.\n>\n> I'll take free() out of the if guard, but I think\n> 'strbuf_release(&<null>->command)' will go poorly - dereferencing the\n> NULL to even invoke strbuf_release will not be a happy time, and\n> strbuf_release internally is not NULL-resistant.\n\nSorry I meant something like:\n\n    if (ptr) strbuf_release(&ptr->command);\n    free(ptr);\n\nBut maybe even more idiomatic would be:\n\n    if (!ptr)\n\treturn;\n    strbuf_release(&ptr->command);\n    free(ptr);\n\nOr some other variant of checking teh container struct early. Anyway,\nthis doesn't really matter, per a below comment I had more meaningful\nfeedback in [1]. Most of my other traffic on this topic (including this)\nwas some stream-of-consciousness notes as I went along.\n\n>> > +struct list_head* hook_list(const struct strbuf* hookname)\n>> > +{\n>> > +\tstruct strbuf hook_key = STRBUF_INIT;\n>> > +\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n>> > +\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n>> > +\n>> > +\tINIT_LIST_HEAD(hook_head);\n>> > +\n>> > +\tif (!hookname)\n>> > +\t\treturn NULL;\n>> \n>> ...if a strbuf being passed in is NULL?\n>\n> Yeah, I think this is misplaced. But since it sounds like generally\n> folks don't like having the strbuf at the input here, I will address the\n> error checking then also.\n>\n>> \n>> > [...]\n>> > +ROOT=\n>> > +if test_have_prereq MINGW\n>> > +then\n>> > +\t# In Git for Windows, Unix-like paths work only in shell scripts;\n>> > +\t# `git.exe`, however, will prefix them with the pseudo root directory\n>> > +\t# (of the Unix shell). Let's accommodate for that.\n>> > +\tROOT=\"$(cd / && pwd)\"\n>> > +fi\n>> \n>> I didn't read up on previous rounds, but if we're squashing this into 02\n>> having a seperate commit summarizing this little hack would be most\n>> welcome, or have it in this commit message.\n>\n> Sure. I squashed it in from a commit dscho sent, so I can preserve that\n> commit in tree instead.\n>\n>> \n>> Isn't this sort of thing generally usable, maybe we can add it under a\n>> longer variable name to test-lib.sh?\n>\n> I wonder. `git grep cd \\/ &&` shows me that this hack also happens in\n> t1509-root-work-tree.sh. I think most tests must use relative paths, so\n> this must not be in broad use? But since it's not used elsewhere I feel\n> ambivalent about adding a helper to test-lib.sh. I can if you feel\n> strongly :)\n\nAfter I sent this I saw that pretty much the same thing is happening in\nt1300-config.sh for the --show-origin option.\n\n    ! test_have_prereq MINGW ||\n    HOME=\"$(pwd)\" # convert to Windows path\n\nI don't feel strongly about this at all, but per the outstanding\nfeedback I had in[1] I wondered whether this whole thing wouln't be\nbetter as some variant of \"git config --show-origin\",\n\n1. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/#t\n"},{"id":"420191","messageId":"87y2ebo16v.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"87mtv8fww3.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 00/37] config-based hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-03-25T12:41:28Z","receivedAt":"2021-03-25T12:42:32Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Mar 12 2021, Ævar Arnfjörð Bjarmason wrote:\n\nA small correction to one of my comments:\n\n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n\n>  2. You're sticking full paths in the git config key, which is\n>     case-insensitive, and a feature of this format is being able to\n>     configure/override previously configured hooks.\n>\n>     So the behavior of this feature depends on git's interaction with\n>     the case-sensitivity of filesystems, and not just one fs, any fs\n>     we're walking in our various config sources, and where the hook\n>     itself lives.\n>\n>     As recent CVEs have shown that's a big can of worms, particularly\n>     for something whose goal is to address the security aspect of\n>     running hooks from other config.\n>\n>     Arguably the case-sensitivity issue is just confusing since we\n>     canonicalize it anyway. But once you add in FS path canonicalization\n>     it becomes a real big can of worms. See the .gitmodules fsck code.\n>\n>     Even if it wasn't for that it's relatively nastier to edit/maintain\n>     full paths and the appropriate escaping in the double-quoted key in\n>     the config file v.s. having it as an optionally quoted value.\n\nSo the \"case-insensitive\" part of that *mostly* doesn't apply.\n\nI'd forgotten that we don't consider the \"LeVeL\" part of\n\"ThReE.LeVeL.KeY\" to be case-insensitive, but the other two components\nare, as discussed in git-config(1)'s docs.\n\nI say \"mostly\" because that's tolower()'s idea of case normalization,\nwhich may or may not match the FS's, but anyway, I think that's probably\nsplitting hairs, but I worry more about the path normalization aspect\nnoted in the last two paragraphs there.\n\n>  3. We're left with this \"*.command = cmd\", and \"*.skip = true\"\n>     special-case syntax. I can't see any reason for why it's needed over\n>     simply having \"*.command = true\" clobber earlier hooks as noted in\n>     the proposed docs above.\n>\n>     And that doesn't require any magic to support, just like our\n>     existing \"core.pager=cat\" case.\n>\n>     I mean, I suppose it's magical in that we might otherwise error on\n>     non-consumed stdin (do we?), anyway, documenting it as a synonym for\n>     \"cat >/dev/null\" would get around that :)\n>\n>  4. It makes the common case of having the same hooks for N commands\n>     needlessly verbose, if you can just support \"type\" (or whatever we\n>     should call it) you can add that N times...\n>\n>  5. At the end of this series we're left with the start of the docs\n>     saying:\n>\n>       You can list and run configured hooks with this command. Later,\n>       you will be able to add and modify hooks with this command.\n>\n>     But those patches have yet to land, and looking at the design\n>     document I'm skeptical of that being a good addition v.s. just\n>     adding the same thing to \"git config\".\n>\n>     As just one exmaple; surely \"git config edit <name>\" would need to\n>     run around and find config files to edit, then open them in a loop\n>     for you, no?\n>\n>     Which we'd eventually want for \"git config\" in general with an\n>     --edit-regexp option or whatever, which brings us (well, at least\n>     me) back to \"then let's just add it to git-config?\".\n>\n>  6. The whole 'git hook' config special-casing doesn't help other\n>     commands or the security issue that seemed to have prompted (at\n>     least some of) its existence\n>\n>     In the design doc we mention the \"core.pager = rm -rf /\" case for a\n>     .git/config.\n>\n>     This series doesn't implement, but the design docs note a future\n>     want for solving that issue for the hooks.\n>\n>     To me that's another case where we should just have general config\n>     syntax, not something hook-specific, e.g. if I could do this in my\n>     ~/.gitconfig:\n>\n>        ;; We consider 'config.ignore' in reverse order, so e.g setting\n>        ;; it in. ~/.gitconfig will ignore any such keys for repo-level\n>        ;; config\n>        [config \"ignore\"]\n>        key = core.pager\n>        keyRegexp = \"^hook\\.\"\n>\n>     We'd address both any hook security concerns, as well as core.pager\n>     etc. We could then just have e.g. some syntax sugar of:\n>\n>        [include]\n>        path = built-in://gimme-safe-config\n>\n>     Which would just be a thin layer of magit to include\n>     <path-to-git-prefix>/config-templates/gimme-safe-config or whatever.\n>\n>     We'd thus address the issue for all config types without\n>     hook-specific magic.\n>\n> Anyway. I'm very willing to be convinced otherwise. I just think that\n> for a first-draft implementation leaving aside 'hook.<command>.command'\n> and the whole 'list' thing makes sense.\n>\n> We can consider the core code changes relatively separately from any\n> future aspirations, particularly with a 40-some patch series, and the\n> end-state of *this series* IMO not really justifying, that part of the\n> implementation, and thus requiring reviewers to look ahead beyond the\n> 40-some patches.\n\nEmily: *Bump* on being interesed in what you think about the rest of\nthis though.\n"},{"id":"420540","messageId":"YGJlR57DGV+KGysB@google.com","threadId":"55286","inReplyTo":"xmqqy2es7jro.fsf@gitster.g","subject":"Re: [PATCH v8 19/37] am: convert applypatch hooks to use config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-29T23:39:51Z","receivedAt":"2021-03-29T23:41:03Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 02:23:39AM -0800, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > @@ -1558,8 +1563,10 @@ static void do_commit(const struct am_state *state)\n> >  \tstruct commit_list *parents = NULL;\n> >  \tconst char *reflog_msg, *author, *committer = NULL;\n> >  \tstruct strbuf sb = STRBUF_INIT;\n> > +\tstruct run_hooks_opt hook_opt;\n> > +\trun_hooks_opt_init_async(&hook_opt);\n> >  \n> > -\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n> > +\tif (run_hooks(\"pre-applypatch\", &hook_opt))\n> >  \t\texit(1);\n> >  \n> >  \tif (write_cache_as_tree(&tree, 0, NULL))\n> > @@ -1611,8 +1618,9 @@ static void do_commit(const struct am_state *state)\n> >  \t\tfclose(fp);\n> >  \t}\n> >  \n> > -\trun_hook_le(NULL, \"post-applypatch\", NULL);\n> > +\trun_hooks(\"post-applypatch\", &hook_opt);\n> >  \n> > +\trun_hooks_opt_clear(&hook_opt);\n> >  \tstrbuf_release(&sb);\n> >  }\n> \n> This one does opt_init(), run_hooks(), and another run_hooks() and\n> then opt_clear().  If run_hooks() is a read-only operation on the\n> hook_opt, then that would be alright, but it just smells iffy that\n> it is not done as two separate opt_init(), run_hooks(), opt_clear()\n> sequences for two separate run_hooks() invocations.  The same worry\n> about future safety I meantioned elsewhere in the series also\n> applies.\n\nInteresting observation. I think the only thing that could be mutated in\nthe run_hooks_opt struct today is the caller-provided callback data\n(run_hooks_opt.feed_pipe_ctx) - which presumably is being manipulated by the\ncaller in a callback they wrote. But I don't think it hurts particularly\nto clear/init again between the two invocations, to be safe - so I will\nchange the code here.\n\n - Emily\n"},{"id":"420542","messageId":"YGJpGvqOIPZc+nYP@google.com","threadId":"55286","inReplyTo":"xmqqr1kk8yen.fsf@gitster.g","subject":"Re: [PATCH v8 23/37] read-cache: convert post-index-change hook to use config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-29T23:56:10Z","receivedAt":"2021-03-29T23:56:54Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 02:22:08AM -0800, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > @@ -3070,6 +3071,8 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n> >  \t\t\t\t unsigned flags)\n> >  {\n> >  \tint ret;\n> > +\tstruct run_hooks_opt hook_opt;\n> > +\trun_hooks_opt_init_async(&hook_opt);\n> >  \n> \n> Nit. blank line between the last of decls and the first stmt (many\n> identical nits exist everywhere in this series).\n> \n> >  \t/*\n> >  \t * TODO trace2: replace \"the_repository\" with the actual repo instance\n> > @@ -3088,9 +3091,13 @@ static int do_write_locked_index(s\n> >  \telse\n> >  \t\tret = close_lock_file_gently(lock);\n> >  \n> > -\trun_hook_le(NULL, \"post-index-change\",\n> > -\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n> > -\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n> > +\tstrvec_pushl(&hook_opt.args,\n> > +\t\t     istate->updated_workdir ? \"1\" : \"0\",\n> > +\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n> > +\t\t     NULL);\n> > +\trun_hooks(\"post-index-change\", &hook_opt);\n> > +\trun_hooks_opt_clear(&hook_opt);\n> \n> There is one early return before the precontext of this hunk that\n> bypasses this opt_clear() call.  It is before any member of hook_opt\n> structure that was opt_init()'ed gets touched, so with the current\n> code, there is no leak, but it probably is laying a landmine for the\n> future, where opt_init() may allocate some resource to its member,\n> with the expectation that all users of the API would call\n> opt_clear() to release.  Or the caller of the API (like this one) may\n> start mucking with the opt structure before the existing early return,\n> at which point the current assumption that it is safe to return from\n> that point without opt_clear() would be broken.\n> \n> I saw that there are other early returns in the series that are safe\n> right now but may become unsafe when the API implementation gets\n> extended that way.  If it does not involve too much code churning,\n> we may want to restructure the code to make these early returns into\n> \"goto\"s that jump to a single exit point, so that we can always\n> match opt_init() with opt_clear(), like the structure of the\n> existing code allowed cmd_rebase() to use the hooks API cleanly in\n> [v8 22/37].\n\nOK. I'll audit this second half of the series looking for this type of\nthing and try to clean up/use gotos if appropriate/etc. Thanks for\npointing it out.\n\n - Emily\n"},{"id":"420544","messageId":"YGJp2VcFZ8nrtho8@google.com","threadId":"55286","inReplyTo":"xmqqk0qc7jpy.fsf@gitster.g","subject":"Re: [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-29T23:59:21Z","receivedAt":"2021-03-30T00:00:13Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 02:24:41AM -0800, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > @@ -1435,12 +1436,19 @@ static const char *push_to_checkout(unsigned char *hash,\n> >  \t\t\t\t    struct strvec *env,\n> >  \t\t\t\t    const char *work_tree)\n> >  {\n> > +\tstruct run_hooks_opt opt;\n> > +\trun_hooks_opt_init_sync(&opt);\n> > +\n> >  \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n> > -\tif (run_hook_le(env->v, push_to_checkout_hook,\n> > -\t\t\thash_to_hex(hash), NULL))\n> > +\tstrvec_pushv(&opt.env, env->v);\n> > +\tstrvec_push(&opt.args, hash_to_hex(hash));\n> > +\tif (run_hooks(push_to_checkout_hook, &opt)) {\n> > +\t\trun_hooks_opt_clear(&opt);\n> >  \t\treturn \"push-to-checkout hook declined\";\n> > -\telse\n> > +\t} else {\n> > +\t\trun_hooks_opt_clear(&opt);\n> >  \t\treturn NULL;\n> > +\t}\n> >  }\n> \n> OK, we opt_init(), futz with opt, call run_hooks() and opt_clear()\n> regardless of the outcome from run_hooks().  Narrow-sighted me\n> wonders if it makes the use of the API easier if run_hooks() did the\n> opt_clear() before it returns, but I haven't yet seen enough use at\n> this point to judge.\n\nHrm, is that idiomatic? I guess it would be convenient, and as long as\nit doesn't touch explicitly caller-managed context pointer it should be\nsafe, but wouldn't it be surprising?\n\n - Emily\n"},{"id":"420545","messageId":"YGJqTIjxm3e4GXcQ@google.com","threadId":"55286","inReplyTo":"871rckhgy0.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 32/37] post-update: use hook.h library","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-30T00:01:16Z","receivedAt":"2021-03-30T00:02:29Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 10:14:31AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> > By using run_hooks() instead of run_hook_le(), 'post-update' hooks can\n> > be specified in the config as well as the hookdir.\n> \n> Looking ahead in the series no tests for this, seems like a good thing\n> to have some at least trivial tests for each hook and their config\n> invocation.\n\nI'll look through the series and make sure the hooks being converted\nhave at least some test to make sure they worked; I think I checked that\nfor some of the early ones but got lazy :) I'll try and add some\nconfig-specified tests too. Thanks, it's on my todo list.\n\n - Emily\n"},{"id":"420546","messageId":"YGJq7gRx152w4N8E@google.com","threadId":"55286","inReplyTo":"87y2esg22j.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-30T00:03:58Z","receivedAt":"2021-03-30T00:05:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 10:21:08AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> > By using the new 'git hook run' subcommand to run 'sendemail-validate',\n> > we can reduce the boilerplate needed to run this hook in perl. Using\n> > config-based hooks also allows us to run 'sendemail-validate' hooks that\n> > were configured globally when running 'git send-email' from outside of a\n> > Git directory, alongside other benefits like multihooks and\n> > parallelization.\n> >\n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >  git-send-email.perl   | 21 ++++-----------------\n> >  t/t9001-send-email.sh | 11 +----------\n> >  2 files changed, 5 insertions(+), 27 deletions(-)\n> >\n> > diff --git a/git-send-email.perl b/git-send-email.perl\n> > index 1f425c0809..73e1e0b51a 100755\n> > --- a/git-send-email.perl\n> > +++ b/git-send-email.perl\n> > @@ -1941,23 +1941,10 @@ sub unique_email_list {\n> >  sub validate_patch {\n> >  \tmy ($fn, $xfer_encoding) = @_;\n> >  \n> > -\tif ($repo) {\n> > -\t\tmy $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),\n> > -\t\t\t\t\t    'sendemail-validate');\n> > -\t\tmy $hook_error;\n> > -\t\tif (-x $validate_hook) {\n> > -\t\t\tmy $target = abs_path($fn);\n> > -\t\t\t# The hook needs a correct cwd and GIT_DIR.\n> > -\t\t\tmy $cwd_save = cwd();\n> > -\t\t\tchdir($repo->wc_path() or $repo->repo_path())\n> > -\t\t\t\tor die(\"chdir: $!\");\n> > -\t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n> > -\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n> > -\t\t\t\tif system($validate_hook, $target);\n> > -\t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n> > -\t\t}\n> > -\t\treturn $hook_error if $hook_error;\n> > -\t}\n> > +\tmy $target = abs_path($fn);\n> > +\treturn \"rejected by sendemail-validate hook\"\n> > +\t\tif system((\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-a\",\n> > +\t\t\t\t$target));\n> \n> I see it's just moving code around, but since we're touching this:\n> \n> This conflates the hook exit code with a general failure to invoke it,\n> Perl's system().\n> \n> Not a big deal in this case, but there's two other existing system()\n> invocations which use the right blurb for it:\n> \n> \n> \tsystem('sh', '-c', $editor.' \"$@\"', $editor, $_);\n> \tif (($? & 127) || ($? >> 8)) {\n> \t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n> \t}\n> \n> Makes sense to do something similar here for consistency. See \"perldoc\n> -f system\" for an example.\n\nOh cool, thanks. I'll do that.\n\n> \n> >  \n> >  \t# Any long lines will be automatically fixed if we use a suitable transfer\n> >  \t# encoding.\n> > diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\n> > index 4eee9c3dcb..456b471c5c 100755\n> > --- a/t/t9001-send-email.sh\n> > +++ b/t/t9001-send-email.sh\n> > @@ -2101,16 +2101,7 @@ test_expect_success $PREREQ 'invoke hook' '\n> >  \tmkdir -p .git/hooks &&\n> >  \n> >  \twrite_script .git/hooks/sendemail-validate <<-\\EOF &&\n> > -\t# test that we have the correct environment variable, pwd, and\n> > -\t# argument\n> > -\tcase \"$GIT_DIR\" in\n> > -\t*.git)\n> > -\t\ttrue\n> > -\t\t;;\n> > -\t*)\n> > -\t\tfalse\n> > -\t\t;;\n> > -\tesac &&\n> > +\t# test that we have the correct argument\n> \n> This and getting rid of these Perl/Python/whatever special cases is very\n> nice.\n\nI thought so too :D :D\n\n - Emily\n"},{"id":"420547","messageId":"YGJrpnCVMnM1KNjM@google.com","threadId":"55286","inReplyTo":"87v99wg1xw.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 36/37] run-command: stop thinking about hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-30T00:07:02Z","receivedAt":"2021-03-30T00:07:48Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 10:23:55AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> > hook.h has replaced all run-command.h hook-related functionality.\n> > run-command.h:run_hooks_le/ve and find_hook are no longer used anywhere\n> > in the codebase. So, let's delete the dead code - or, in the one case\n> > where it's still needed, move it to an internal function in hook.c.\n> \n> Similar to other comments about squashing, I think just having this\n> happen incrementally as we remove whatever is the last user of the\n> function would be better.\n> \n> E.g. find_hook() is last used in one commit, run_hook*() in another...\n\nHm. I could see it, coupled with a blurb like, \"Nobody is using this\nanymore so delete.\"\n\nBut it feels odd to move the find_hook() impl from here to hook.c\ninternal in a commit about, say, bugreport.\n\nI'll consider this, thanks. Maybe it fits in one case (like run_hook_*)\nbetter than in another (like find_hook). I'll play with it :)\n\n - Emily\n"},{"id":"420548","messageId":"xmqqlfa5fqlz.fsf@gitster.g","threadId":"55286","inReplyTo":"YGJp2VcFZ8nrtho8@google.com","subject":"Re: [PATCH v8 24/37] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-30T00:10:48Z","receivedAt":"2021-03-30T00:11:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n>> OK, we opt_init(), futz with opt, call run_hooks() and opt_clear()\n>> regardless of the outcome from run_hooks().  Narrow-sighted me\n>> wonders if it makes the use of the API easier if run_hooks() did the\n>> opt_clear() before it returns, but I haven't yet seen enough use at\n>> this point to judge.\n>\n> Hrm, is that idiomatic? I guess it would be convenient, and as long as\n> it doesn't touch explicitly caller-managed context pointer it should be\n> safe, but wouldn't it be surprising?\n\nThe precedent (at this point, I will not judge if it is a good\npattern to emulate or an anti-pattern to stay away from) I had in\nmind was the run_command() which clears child_process structure\nas the side effect of internally calling finish_command().\n\nLeaving them separate is of course more flexible, but depending on\nhow small we can keep down the number of call patterns of this new\nAPI, always having to clear after run might become an unnecessary\nsource of leaks.  When I gave that comment, I didn't have enough\ninput to decide, and now it has been so long since I gave my\nreviews, I do not quite remember what my impression after reading\nall the patches through was X-<.\n\n\n"},{"id":"420549","messageId":"YGJsjgEngx8z6wDk@google.com","threadId":"55286","inReplyTo":"87sg50g1nz.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-30T00:10:54Z","receivedAt":"2021-03-30T00:12:06Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 10:29:52AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Mar 11 2021, Emily Shaffer wrote:\n> \n> > By showing the list of all hooks in 'git help hook' for users to refer\n> > to, 'git help hook' becomes a one-stop shop for hook authorship. Since\n> > some may still have muscle memory for 'git help githooks', though,\n> > reference the 'git hook' commands and otherwise don't remove content.\n> \n> I think this should at least have something like what my b6a8d09f6d8 (gc\n> docs: include the \"gc.*\" section from \"config\" in \"gc\", 2019-04-07) has\n> on top, i.e.:\n\nYeah, this seems reasonable.\n\n>     \n>     diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n>     index 4ad31ac360a..5c9af30b43e 100644\n>     --- a/Documentation/git-hook.txt\n>     +++ b/Documentation/git-hook.txt\n>     @@ -150,10 +150,18 @@ message body and cannot be parallelized.\n>      \n>      CONFIGURATION\n>      -------------\n>     +\n>     +The below documentation is the same as what's found in\n>     +linkgit:git-config[1]:\n>     +\n>      include::config/hook.txt[]\n>      \n>      HOOKS\n>      -----\n>     +\n>     +The below documentation is the same as what's found in\n>     +linkgit:githooks[5]:\n>     +\n>      include::native-hooks.txt[]\n>      \n>      GIT\n> \n> But I also don't think we should demote githooks(5) as the canonical doc\n> page for the hooks themselves.\n> \n> If you run this in your terminal:\n> \n>     man 5 git<TAB>\n> \n> You'll get:\n> \n>     gitattributes         gitignore             gitmailmap            gitrepository-layout  \n>     githooks              git-lfs-config        gitmodules            gitweb.conf \n> \n> (Well, maybe not the lfs-part, but whatever...).\n> \n> We should move more in the direction of splitting up our \"file format\"\n> docs from implementation, like the git-hook runner.\n> \n> I'm somewhat negative on including it at all in git-hook(1). For the\n> config section it makes sense, and it's consistent with established doc\n> convention.\n> \n> But including githooks(5) is around 2/3 of the resulting manpage, I\n> think just a link is better.\n\nMaybe so. What I really would like would be if `git help githooks` //\n`man githooks` opened `git-hook` manpage, but I had trouble getting it to do\nthat and still publish to the `githooks` manpage (because the command\ndoc format doesn't match the guide format). (Or, really, if `git help\ngithooks` didn't exist so we didn't need to split the docs up. But that\nship has sailed.)\n\nRegardless, I won't complain that much about using a link instead. I'll\nmake this change for v9.\n\n - Emily\n\n"},{"id":"420750","messageId":"YGTt2cNwq3BlpB7n@google.com","threadId":"55286","inReplyTo":"87y2esg22j.fsf@evledraar.gmail.com","subject":"Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-03-31T21:47:05Z","receivedAt":"2021-03-31T21:47:59Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Mar 12, 2021 at 10:21:08AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> > +\tmy $target = abs_path($fn);\n> > +\treturn \"rejected by sendemail-validate hook\"\n> > +\t\tif system((\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-a\",\n> > +\t\t\t\t$target));\n> \n> I see it's just moving code around, but since we're touching this:\n> \n> This conflates the hook exit code with a general failure to invoke it,\n> Perl's system().\n\nAh, at first I thought you meant \"hook exit code vs. failure in 'git\nhook run'\" - but I think you are saying \"system() can also exit\nunhappily\".\n\nI had a look in 'perldoc -f system' like you suggested and saw that in\naddition to $? & 127, it seems like I also should check $? == -1\n(\"system() couldn't start the child process\") and ($? >> 8) (the rc\nfrom the child hangs out in the top byte). So then it seems like I want\nsomething like so:\n\n  system(\"git\", \"hook\", \"run\", \"sendemail-validate\",\n          \"-j1\", \"-a\", $target);\n\n  return \"git-send-email failed to launch hook process: $!\"\n          if ($? == -1) || ($? & 127))\n  return \"git-send-email invoked git-hook run incorrectly\"\n          if (($? >> 8) == 129);\n  return \"Rejected by 'sendemail-validate' hook\"\n          if ($? >> 8);\n\nThat seems really verbose, though. I guess ($? >> 8) includes -1 as well (since\n0xFF... will meet that conditional), but do we care about the difference between\n\"system() couldn't run my thing\" and \"my thing returned upset\"?\n\nIn this case, \"my thing returned upset\" - that is, $? >> 8 reflects an\nerror code from the hook exec - should already have some user output\nassociated with it, from the hook exec itself, but it's not guaranteed -\nneither builtin/hook.c:run() nor hook.c:run_hooks() prints anything to\nthe user if rc != 0, because they're counting on either the hook execs\nor the process that invoked the hook to do the tattling.\n\nI think that means that it's a good idea to differentiate all these\nthings to the user:\n\n 1. system() broke or your hook got a SIGINT (write a bug report or take\n    out the infinite loop/memory violation from the hook you're\n    developing)\n 2. builtin/hook.c:run() wasn't invoked properly (fix the change you made\n    to git-send-email.perl)\n 3. your hook rejected your email (working as intended, fix the file you\n    want to email)\n\nI'd not expect users to encounter (1) or (2) so it seems fine to me to\ninclude them; if (3) isn't present *and* the hook author did a bad job\ncommunicating what failed, then I think the user experience would be\nvery confusing - even though they'd see some warning telling them their\npatches didn't send, it wouldn't be clear whether it's because of an\nissue in git-send-email or an issue with their patch.\n\nPhew. I think I convinced myself that the wordy rc checking is OK. But I\nam a perl noob so please correct me if I am wrong :)\n\n - Emily\n"},{"id":"420751","messageId":"xmqqy2e3567f.fsf@gitster.g","threadId":"55286","inReplyTo":"YGTt2cNwq3BlpB7n@google.com","subject":"Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-03-31T22:06:12Z","receivedAt":"2021-03-31T22:07:11Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> On Fri, Mar 12, 2021 at 10:21:08AM +0100, Ævar Arnfjörð Bjarmason wrote:\n>> > +\tmy $target = abs_path($fn);\n>> > +\treturn \"rejected by sendemail-validate hook\"\n>> > +\t\tif system((\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-a\",\n>> > +\t\t\t\t$target));\n>> \n>> I see it's just moving code around, but since we're touching this:\n>> \n>> This conflates the hook exit code with a general failure to invoke it,\n>> Perl's system().\n>\n> Ah, at first I thought you meant \"hook exit code vs. failure in 'git\n> hook run'\" - but I think you are saying \"system() can also exit\n> unhappily\".\n>\n> I had a look in 'perldoc -f system' like you suggested and saw that in\n> addition to $? & 127, it seems like I also should check $? == -1\n> (\"system() couldn't start the child process\") and ($? >> 8) (the rc\n> from the child hangs out in the top byte). So then it seems like I want\n> something like so:\n>\n>   system(\"git\", \"hook\", \"run\", \"sendemail-validate\",\n>           \"-j1\", \"-a\", $target);\n>\n>   return \"git-send-email failed to launch hook process: $!\"\n>           if ($? == -1) || ($? & 127))\n>   return \"git-send-email invoked git-hook run incorrectly\"\n>           if (($? >> 8) == 129);\n>   return \"Rejected by 'sendemail-validate' hook\"\n>           if ($? >> 8);\n>\n\nThe example in \"perldoc -f system\" distinguishes these two like so:\n\n        if ($? == -1) {\n                print \"failed to execute: $!\\n\";\n        }\n        elsif ($? & 127) {\n                printf \"child died with signal %d, %s coredump\\n\",\n                    ($? & 127), ($? & 128) ? 'with' : 'without';\n        }\n        else {\n                printf \"child exited with value %d\\n\", $? >> 8;\n        }\n\n> That seems really verbose, though. I guess ($? >> 8) includes -1 as well (since\n> 0xFF... will meet that conditional), but do we care about the difference between\n> \"system() couldn't run my thing\" and \"my thing returned upset\"?\n\nIf we classify the failure cases into three using the sample code in\nthe doc, I think the last one is the only case that we know the\nlogic in the hook is making a decision for us.  In the first case,\nthe hook did not even have a chance to decide for us, and in the\nsecond case, the hook died with signal, most likely before it had a\nchance to make a decision.  If we want to be conservative (sending\na message out is something you cannot easily undo), then it may make\nsense to take the first two failure cases, even though the hook may\nhave said it is OK to send it out if it ran successfully, as a denial\nto be safe, I would think.\n\nThanks.\n\n"},{"id":"420842","messageId":"xmqqr1jt3kcr.fsf@gitster.g","threadId":"55286","inReplyTo":"YGYMG3mArYSeMr9D@google.com","subject":"Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-01T18:55:48Z","receivedAt":"2021-04-01T18:58:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n>> If we classify the failure cases into three using the sample code in\n>> the doc, I think the last one is the only case that we know the\n>> logic in the hook is making a decision for us.  In the first case,\n>> the hook did not even have a chance to decide for us, and in the\n>> second case, the hook died with signal, most likely before it had a\n>> chance to make a decision.  If we want to be conservative (sending\n>> a message out is something you cannot easily undo), then it may make\n>> sense to take the first two failure cases, even though the hook may\n>> have said it is OK to send it out if it ran successfully, as a denial\n>> to be safe, I would think.\n>\n> Yeah, I tend to agree. In that case I think you are saying: \"Please\n> split the first case into two and differentiate launch failure from\n> signal, but otherwise continue to return all these cases as errors and\n> halt the email.\"\n\nNot exactly.  I do not have a strong opinion either way to split the\nfirst two cases apart or lump them together.  If I were pressed, I\nprobably would vote for the latter.\n\nThe doc's example classfies into three and I think that\nclassification is logical:\n\n * Lumping the first two together would make sense with respect to\n   deciding what to do when we see a failure. The first two are\n   \"hook failed to approve or disapprove\" case, while the last one\n   is \"the hook actively disapproved\".  The former is not under\n   hook's control.\n\n * Further, treating a failure even from the first \"hook failed to\n   approve or disapprove\" as a signal to stop sending would be more\n   conservative.\n\n * Which leads us to say, with respect to deciding what to do, any\n   failure just stops the program from sending.\n\nIt is a separate matter how to phrase the diagnoses and hints for\nrecovery.  It could be that sendmail-validate hook failed to run due\nto a simple misconfiguration (e.g. because it lacked the executable\nbit).  Giving an error message with strerr would be helpful for the\n\"hook failed to approve or disapprove\" case.\n\n"},{"id":"420843","messageId":"YGYMG3mArYSeMr9D@google.com","threadId":"55286","inReplyTo":"xmqqy2e3567f.fsf@gitster.g","subject":"Re: [PATCH v8 35/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-04-01T18:08:27Z","receivedAt":"2021-04-01T19:19:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Mar 31, 2021 at 03:06:12PM -0700, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > On Fri, Mar 12, 2021 at 10:21:08AM +0100, Ævar Arnfjörð Bjarmason wrote:\n> >> > +\tmy $target = abs_path($fn);\n> >> > +\treturn \"rejected by sendemail-validate hook\"\n> >> > +\t\tif system((\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-a\",\n> >> > +\t\t\t\t$target));\n> >> \n> >> I see it's just moving code around, but since we're touching this:\n> >> \n> >> This conflates the hook exit code with a general failure to invoke it,\n> >> Perl's system().\n> >\n> > Ah, at first I thought you meant \"hook exit code vs. failure in 'git\n> > hook run'\" - but I think you are saying \"system() can also exit\n> > unhappily\".\n> >\n> > I had a look in 'perldoc -f system' like you suggested and saw that in\n> > addition to $? & 127, it seems like I also should check $? == -1\n> > (\"system() couldn't start the child process\") and ($? >> 8) (the rc\n> > from the child hangs out in the top byte). So then it seems like I want\n> > something like so:\n> >\n> >   system(\"git\", \"hook\", \"run\", \"sendemail-validate\",\n> >           \"-j1\", \"-a\", $target);\n> >\n> >   return \"git-send-email failed to launch hook process: $!\"\n> >           if ($? == -1) || ($? & 127))\n> >   return \"git-send-email invoked git-hook run incorrectly\"\n> >           if (($? >> 8) == 129);\n> >   return \"Rejected by 'sendemail-validate' hook\"\n> >           if ($? >> 8);\n> >\n> \n> The example in \"perldoc -f system\" distinguishes these two like so:\n> \n>         if ($? == -1) {\n>                 print \"failed to execute: $!\\n\";\n>         }\n>         elsif ($? & 127) {\n>                 printf \"child died with signal %d, %s coredump\\n\",\n>                     ($? & 127), ($? & 128) ? 'with' : 'without';\n>         }\n>         else {\n>                 printf \"child exited with value %d\\n\", $? >> 8;\n>         }\n> \n> > That seems really verbose, though. I guess ($? >> 8) includes -1 as well (since\n> > 0xFF... will meet that conditional), but do we care about the difference between\n> > \"system() couldn't run my thing\" and \"my thing returned upset\"?\n> \n> If we classify the failure cases into three using the sample code in\n> the doc, I think the last one is the only case that we know the\n> logic in the hook is making a decision for us.  In the first case,\n> the hook did not even have a chance to decide for us, and in the\n> second case, the hook died with signal, most likely before it had a\n> chance to make a decision.  If we want to be conservative (sending\n> a message out is something you cannot easily undo), then it may make\n> sense to take the first two failure cases, even though the hook may\n> have said it is OK to send it out if it ran successfully, as a denial\n> to be safe, I would think.\n\nYeah, I tend to agree. In that case I think you are saying: \"Please\nsplit the first case into two and differentiate launch failure from\nsignal, but otherwise continue to return all these cases as errors and\nhalt the email.\"\n\n - Emily\n"},{"id":"420874","messageId":"cover-0.3-00000000000-20210402T112946Z-avarab@gmail.com","threadId":"55286","inReplyTo":"YGTt2cNwq3BlpB7n@google.com","subject":"[PATCH 0/2] git-send-email: refactor duplicate $? checks into a function","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-02T11:34:32Z","receivedAt":"2021-04-02T11:35:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"A tiny series to help along the config-based hooks series[1]. Its\npatch dealing with git-send-email.perl can now trivially be based on\ntop of this instead of adding another system() wrapper to\ngit-send-email.perl.\n\nAs alluded to in the TODO comment in 2/2 it's probably best to fix\nthings while we're at it to call validate_patch_error() instead of\njust emitting the more brief \"rejected by sendemail-validate hook\".\n\nBut for now this series is just aiming for bug-for-bug compatibility\nwith the existing code, and to just reduce code duplication.\n\nhttp://lore.kernel.org/git/20210311021037.3001235-1-emilyshaffer@google.com\n\nÆvar Arnfjörð Bjarmason (2):\n  git-send-email: replace \"map\" in void context with \"for\"\n  git-send-email: refactor duplicate $? checks into a function\n\n git-send-email.perl | 49 +++++++++++++++++++++++++++++----------------\n 1 file changed, 32 insertions(+), 17 deletions(-)\n\n-- \n2.31.1.482.g6691c1be520\n\n"},{"id":"420875","messageId":"patch-2.3-f4bace5607c-20210402T112946Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.3-00000000000-20210402T112946Z-avarab@gmail.com","subject":"[PATCH 2/2] git-send-email: refactor duplicate $? checks into a function","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-02T11:34:34Z","receivedAt":"2021-04-02T11:35:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the duplicate checking of $? into a function. There's an\noutstanding series[1] wanting to add a third use of system() in this\nfile, let's not copy this boilerplate anymore when that happens.\n\n1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl | 49 +++++++++++++++++++++++++++++----------------\n 1 file changed, 32 insertions(+), 17 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 6893c8e5808..901c935455d 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -212,22 +212,30 @@ sub format_2822_time {\n my $multiedit;\n my $editor;\n \n+sub system_or_msg {\n+\tmy ($args, $msg) = @_;\n+\tsystem(@$args);\n+\treturn unless (($? & 127) || ($? >> 8));\n+\n+\tdie $msg if $msg;\n+\treturn sprintf(__(\"failed to run command %s, died with code %d\"),\n+\t\t       \"@$args\", $? >> 8);\n+}\n+\n+sub system_or_die {\n+\tmy $msg = system_or_msg(@_);\n+\tdie $msg if $msg;\n+}\n+\n sub do_edit {\n \tif (!defined($editor)) {\n \t\t$editor = Git::command_oneline('var', 'GIT_EDITOR');\n \t}\n+\tmy $die_msg = __(\"the editor exited uncleanly, aborting everything\");\n \tif (defined($multiedit) && !$multiedit) {\n-\t\tfor (@_) {\n-\t\t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, $_);\n-\t\t\tif (($? & 127) || ($? >> 8)) {\n-\t\t\t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n-\t\t\t}\n-\t\t}\n+\t\tsystem_or_die(['sh', '-c', $editor.' \"$@\"', $editor, $_], $die_msg) for @_;\n \t} else {\n-\t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, @_);\n-\t\tif (($? & 127) || ($? >> 8)) {\n-\t\t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n-\t\t}\n+\t\tsystem_or_die(['sh', '-c', $editor.' \"$@\"', $editor, @_], $die_msg);\n \t}\n }\n \n@@ -698,9 +706,7 @@ sub is_format_patch_arg {\n if ($validate) {\n \tforeach my $f (@files) {\n \t\tunless (-p $f) {\n-\t\t\tmy $error = validate_patch($f, $target_xfer_encoding);\n-\t\t\t$error and die sprintf(__(\"fatal: %s: %s\\nwarning: no patches were sent\\n\"),\n-\t\t\t\t\t\t  $f, $error);\n+\t\t\tvalidate_patch($f, $target_xfer_encoding);\n \t\t}\n \t}\n }\n@@ -1938,6 +1944,12 @@ sub unique_email_list {\n \treturn @emails;\n }\n \n+sub validate_patch_error {\n+\tmy ($fn, $error) = @_;\n+\tdie sprintf(__(\"fatal: %s: %s\\nwarning: no patches were sent\\n\"),\n+\t\t    $fn, $error);\n+}\n+\n sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n@@ -1952,11 +1964,14 @@ sub validate_patch {\n \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n \t\t\t\tor die(\"chdir: $!\");\n \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n-\t\t\t\tif system($validate_hook, $target);\n+\t\t\tif (my $msg = system_or_msg([$validate_hook, $target])) {\n+\t\t\t\t# TODO Use $msg and emit exit code on\n+\t\t\t\t# hook failures?\n+\t\t\t\t$hook_error = __(\"rejected by sendemail-validate hook\");\n+\t\t\t}\n \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n \t\t}\n-\t\treturn $hook_error if $hook_error;\n+\t\tvalidate_patch_error($fn, $hook_error) if $hook_error;\n \t}\n \n \t# Any long lines will be automatically fixed if we use a suitable transfer\n@@ -1966,7 +1981,7 @@ sub validate_patch {\n \t\t\tor die sprintf(__(\"unable to open %s: %s\\n\"), $fn, $!);\n \t\twhile (my $line = <$fh>) {\n \t\t\tif (length($line) > 998) {\n-\t\t\t\treturn sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.);\n+\t\t\t\tvalidate_patch_error($fn, sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.));\n \t\t\t}\n \t\t}\n \t}\n-- \n2.31.1.482.g6691c1be520\n\n"},{"id":"420876","messageId":"patch-1.3-bea11504a67-20210402T112946Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.3-00000000000-20210402T112946Z-avarab@gmail.com","subject":"[PATCH 1/2] git-send-email: replace \"map\" in void context with \"for\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-02T11:34:33Z","receivedAt":"2021-04-02T11:35:34Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"While using \"map\" instead of \"for\" or \"map\" instead of \"grep\" and\nvice-versa makes for interesting trivia questions when interviewing\nPerl programmers, it doesn't make for very readable code. Let's\nrefactor this loop initially added in 8fd5bb7f44b (git send-email: add\n--annotate option, 2008-11-11) to be a for-loop instead.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex f5bbf1647e3..6893c8e5808 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -217,12 +217,12 @@ sub do_edit {\n \t\t$editor = Git::command_oneline('var', 'GIT_EDITOR');\n \t}\n \tif (defined($multiedit) && !$multiedit) {\n-\t\tmap {\n+\t\tfor (@_) {\n \t\t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, $_);\n \t\t\tif (($? & 127) || ($? >> 8)) {\n \t\t\t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n \t\t\t}\n-\t\t} @_;\n+\t\t}\n \t} else {\n \t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, @_);\n \t\tif (($? & 127) || ($? >> 8)) {\n-- \n2.31.1.482.g6691c1be520\n\n"},{"id":"420900","messageId":"xmqqo8ew2x1h.fsf@gitster.g","threadId":"55286","inReplyTo":"patch-1.3-bea11504a67-20210402T112946Z-avarab@gmail.com","subject":"Re: [PATCH 1/2] git-send-email: replace \"map\" in void context with \"for\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-02T21:31:38Z","receivedAt":"2021-04-02T21:31:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> While using \"map\" instead of \"for\" or \"map\" instead of \"grep\" and\n> vice-versa makes for interesting trivia questions when interviewing\n> Perl programmers, it doesn't make for very readable code. Let's\n> refactor this loop initially added in 8fd5bb7f44b (git send-email: add\n> --annotate option, 2008-11-11) to be a for-loop instead.\n\n;-)\n\nWill queue.  Thanks.\n"},{"id":"420902","messageId":"xmqqk0pk2wsx.fsf@gitster.g","threadId":"55286","inReplyTo":"patch-2.3-f4bace5607c-20210402T112946Z-avarab@gmail.com","subject":"Re: [PATCH 2/2] git-send-email: refactor duplicate $? checks into a function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-02T21:36:46Z","receivedAt":"2021-04-02T21:36:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> Refactor the duplicate checking of $? into a function. There's an\n> outstanding series[1] wanting to add a third use of system() in this\n> file, let's not copy this boilerplate anymore when that happens.\n>\n> 1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com\n>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  git-send-email.perl | 49 +++++++++++++++++++++++++++++----------------\n>  1 file changed, 32 insertions(+), 17 deletions(-)\n>\n> diff --git a/git-send-email.perl b/git-send-email.perl\n> index 6893c8e5808..901c935455d 100755\n> --- a/git-send-email.perl\n> +++ b/git-send-email.perl\n> @@ -212,22 +212,30 @@ sub format_2822_time {\n>  my $multiedit;\n>  my $editor;\n>  \n> +sub system_or_msg {\n> +\tmy ($args, $msg) = @_;\n> +\tsystem(@$args);\n> +\treturn unless (($? & 127) || ($? >> 8));\n> +\n> +\tdie $msg if $msg;\n> +\treturn sprintf(__(\"failed to run command %s, died with code %d\"),\n> +\t\t       \"@$args\", $? >> 8);\n> +}\n\nThat sounds more like system_and_die_or_msg to me.  More\nimportantly, the name of the helper makes it clear what difference\nthis has with ...\n\n> +sub system_or_die {\n> +\tmy $msg = system_or_msg(@_);\n> +\tdie $msg if $msg;\n> +}\n\n... this one.  The former does nto die but returns message only when\nX?  If that X were in its name, readers who look at the caller of\nsystem_or_msg vs system_or_die would immediately know that why the\ncallsite is using one and not the other variant.\n"},{"id":"420903","messageId":"YGeOm+VNro9yofo6@google.com","threadId":"55286","inReplyTo":"xmqqo8ew2x1h.fsf@gitster.g","subject":"Re: [PATCH 1/2] git-send-email: replace \"map\" in void context with \"for\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-04-02T21:37:31Z","receivedAt":"2021-04-02T21:37:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Apr 02, 2021 at 02:31:38PM -0700, Junio C Hamano wrote:\n> \n> Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n> \n> > While using \"map\" instead of \"for\" or \"map\" instead of \"grep\" and\n> > vice-versa makes for interesting trivia questions when interviewing\n> > Perl programmers, it doesn't make for very readable code. Let's\n> > refactor this loop initially added in 8fd5bb7f44b (git send-email: add\n> > --annotate option, 2008-11-11) to be a for-loop instead.\n> \n> ;-)\n> \n> Will queue.  Thanks.\n\nOh cool, thanks both :)\n"},{"id":"420964","messageId":"cover-0.5-00000000000-20210404T091649Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.3-00000000000-20210402T112946Z-avarab@gmail.com","subject":"[PATCH v2 0/4] refactor duplicate $? checks into a function + improve errors","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-04T09:19:42Z","receivedAt":"2021-04-04T09:19:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"There was a silly error in v1 noted by Junio. In practice we didn't\nhit that \"die $msg if $die\" in system_or_msg(), but it didn't belong\nthere.\n\nI see 1/2 of v1 of this series was merged to \"next\". I'm sending the\nfull thing anyway, but presumably just 3-4 will be picked up. They\napply cleanly on \"next\".\n\nI added two patches at the end to improve the error output, the first\ntwo patches in both v1 and v2 just reproduced the current output\nbug-for-bug, but I've now made it more sensible.\n\nÆvar Arnfjörð Bjarmason (4):\n  git-send-email: replace \"map\" in void context with \"for\"\n  git-send-email: refactor duplicate $? checks into a function\n  git-send-email: test full --validate output\n  git-send-email: improve --validate error output\n\n git-send-email.perl   | 45 +++++++++++++++++++++++++++----------------\n t/t9001-send-email.sh | 35 +++++++++++++++++++++++++--------\n 2 files changed, 55 insertions(+), 25 deletions(-)\n\nRange-diff:\n1:  bea11504a67 = 1:  e37b861f239 git-send-email: replace \"map\" in void context with \"for\"\n2:  f4bace5607c ! 2:  f236f083e36 git-send-email: refactor duplicate $? checks into a function\n    @@ git-send-email.perl: sub format_2822_time {\n     +sub system_or_msg {\n     +\tmy ($args, $msg) = @_;\n     +\tsystem(@$args);\n    -+\treturn unless (($? & 127) || ($? >> 8));\n    ++\tmy $signalled = $? & 127;\n    ++\tmy $exit_code = $? >> 8;\n    ++\treturn unless $signalled or $exit_code;\n     +\n    -+\tdie $msg if $msg;\n     +\treturn sprintf(__(\"failed to run command %s, died with code %d\"),\n    -+\t\t       \"@$args\", $? >> 8);\n    ++\t\t       \"@$args\", $exit_code);\n     +}\n     +\n     +sub system_or_die {\n    @@ git-send-email.perl: sub validate_patch {\n     -\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n     -\t\t\t\tif system($validate_hook, $target);\n     +\t\t\tif (my $msg = system_or_msg([$validate_hook, $target])) {\n    -+\t\t\t\t# TODO Use $msg and emit exit code on\n    -+\t\t\t\t# hook failures?\n     +\t\t\t\t$hook_error = __(\"rejected by sendemail-validate hook\");\n     +\t\t\t}\n      \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n-:  ----------- > 3:  15b59c226d4 git-send-email: test full --validate output\n-:  ----------- > 4:  a1edceb4913 git-send-email: improve --validate error output\n-- \n2.31.1.482.g6691c1be520\n\n"},{"id":"420965","messageId":"patch-1.5-e37b861f239-20210404T091649Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.5-00000000000-20210404T091649Z-avarab@gmail.com","subject":"[PATCH v2 1/4] git-send-email: replace \"map\" in void context with \"for\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-04T09:19:43Z","receivedAt":"2021-04-04T09:19:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"While using \"map\" instead of \"for\" or \"map\" instead of \"grep\" and\nvice-versa makes for interesting trivia questions when interviewing\nPerl programmers, it doesn't make for very readable code. Let's\nrefactor this loop initially added in 8fd5bb7f44b (git send-email: add\n--annotate option, 2008-11-11) to be a for-loop instead.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex f5bbf1647e3..6893c8e5808 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -217,12 +217,12 @@ sub do_edit {\n \t\t$editor = Git::command_oneline('var', 'GIT_EDITOR');\n \t}\n \tif (defined($multiedit) && !$multiedit) {\n-\t\tmap {\n+\t\tfor (@_) {\n \t\t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, $_);\n \t\t\tif (($? & 127) || ($? >> 8)) {\n \t\t\t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n \t\t\t}\n-\t\t} @_;\n+\t\t}\n \t} else {\n \t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, @_);\n \t\tif (($? & 127) || ($? >> 8)) {\n-- \n2.31.1.482.g6691c1be520\n\n"},{"id":"420966","messageId":"patch-3.5-15b59c226d4-20210404T091649Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.5-00000000000-20210404T091649Z-avarab@gmail.com","subject":"[PATCH v2 3/4] git-send-email: test full --validate output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-04T09:19:45Z","receivedAt":"2021-04-04T09:19:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Change the tests that grep substrings out of the output to use a full\ntest_cmp, in preparation for improving the output.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t9001-send-email.sh | 24 ++++++++++++++++++------\n 1 file changed, 18 insertions(+), 6 deletions(-)\n\ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 1a1caf8f2ed..74225e3dc7a 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -422,8 +422,12 @@ test_expect_success $PREREQ 'reject long lines' '\n \t\t--smtp-server=\"$(pwd)/fake.sendmail\" \\\n \t\t--transfer-encoding=8bit \\\n \t\t$patches longline.patch \\\n-\t\t2>errors &&\n-\tgrep longline.patch errors\n+\t\t2>actual &&\n+\tcat >expect <<-\\EOF &&\n+\tfatal: longline.patch: 35: patch contains a line longer than 998 characters\n+\twarning: no patches were sent\n+\tEOF\n+\ttest_cmp expect actual\n '\n \n test_expect_success $PREREQ 'no patch was sent' '\n@@ -527,9 +531,13 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \t\t--to=nobody@example.com \\\n \t\t--smtp-server=\"$(pwd)/fake.sendmail\" \\\n \t\t--validate \\\n-\t\tlongline.patch 2>err &&\n+\t\tlongline.patch 2>actual &&\n \ttest_path_is_file my-hooks.ran &&\n-\tgrep \"rejected by sendemail-validate\" err\n+\tcat >expect <<-\\EOF &&\n+\tfatal: longline.patch: rejected by sendemail-validate hook\n+\twarning: no patches were sent\n+\tEOF\n+\ttest_cmp expect actual\n '\n \n test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n@@ -540,9 +548,13 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \t\t--to=nobody@example.com \\\n \t\t--smtp-server=\"$(pwd)/fake.sendmail\" \\\n \t\t--validate \\\n-\t\tlongline.patch 2>err &&\n+\t\tlongline.patch 2>actual &&\n \ttest_path_is_file my-hooks.ran &&\n-\tgrep \"rejected by sendemail-validate\" err\n+\tcat >expect <<-\\EOF &&\n+\tfatal: longline.patch: rejected by sendemail-validate hook\n+\twarning: no patches were sent\n+\tEOF\n+\ttest_cmp expect actual\n '\n \n for enc in 7bit 8bit quoted-printable base64\n-- \n2.31.1.482.g6691c1be520\n\n"},{"id":"420967","messageId":"patch-4.5-a1edceb4913-20210404T091649Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.5-00000000000-20210404T091649Z-avarab@gmail.com","subject":"[PATCH v2 4/4] git-send-email: improve --validate error output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-04T09:19:46Z","receivedAt":"2021-04-04T09:20:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Improve the output we emit on --validate error to:\n\n * Say \"FILE:LINE\" instead of \"FILE: LINE\".\n\n * Don't say \"patch contains a\" after just mentioning the filename,\n   just leave it at \"FILE:LINE: is longer than[...]. The \"contains a\"\n   sounded like we were talking about the file in general, when we're\n   actually checking it line-by-line.\n\n * Don't just say \"rejected by sendemail-validate hook\", but combine\n   that with the system_or_msg() output to say what exit code the hook\n   died with.\n\nI had an aborted attempt to make the line length checker note all\nlines that were longer than the limit. I didn't think that was worth\nthe effort, but I've left in the testing change to check that we die\nas soon as we spot the first long line.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl   | 23 ++++++++++-------------\n t/t9001-send-email.sh | 17 ++++++++++++-----\n 2 files changed, 22 insertions(+), 18 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 9724a9cae27..175da07d946 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -219,8 +219,8 @@ sub system_or_msg {\n \tmy $exit_code = $? >> 8;\n \treturn unless $signalled or $exit_code;\n \n-\treturn sprintf(__(\"failed to run command %s, died with code %d\"),\n-\t\t       \"@$args\", $exit_code);\n+\treturn sprintf(__(\"fatal: command '%s' died with exit code %d\"),\n+\t\t       $args->[0], $exit_code);\n }\n \n sub system_or_die {\n@@ -1945,12 +1945,6 @@ sub unique_email_list {\n \treturn @emails;\n }\n \n-sub validate_patch_error {\n-\tmy ($fn, $error) = @_;\n-\tdie sprintf(__(\"fatal: %s: %s\\nwarning: no patches were sent\\n\"),\n-\t\t    $fn, $error);\n-}\n-\n sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n@@ -1965,12 +1959,14 @@ sub validate_patch {\n \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n \t\t\t\tor die(\"chdir: $!\");\n \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\tif (my $msg = system_or_msg([$validate_hook, $target])) {\n-\t\t\t\t$hook_error = __(\"rejected by sendemail-validate hook\");\n-\t\t\t}\n+\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n \t\t}\n-\t\tvalidate_patch_error($fn, $hook_error) if $hook_error;\n+\t\tif ($hook_error) {\n+\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n+\t\t\t\t       \"%s\\n\" .\n+\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n+\t\t}\n \t}\n \n \t# Any long lines will be automatically fixed if we use a suitable transfer\n@@ -1980,7 +1976,8 @@ sub validate_patch {\n \t\t\tor die sprintf(__(\"unable to open %s: %s\\n\"), $fn, $!);\n \t\twhile (my $line = <$fh>) {\n \t\t\tif (length($line) > 998) {\n-\t\t\t\tvalidate_patch_error($fn, sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.));\n+\t\t\t\tdie sprintf(__(\"fatal: %s:%d is longer than 998 characters\\n\" .\n+\t\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $.);\n \t\t\t}\n \t\t}\n \t}\ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 74225e3dc7a..65b30353719 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -415,7 +415,11 @@ test_expect_success $PREREQ 'reject long lines' '\n \tz512=$z64$z64$z64$z64$z64$z64$z64$z64 &&\n \tclean_fake_sendmail &&\n \tcp $patches longline.patch &&\n-\techo $z512$z512 >>longline.patch &&\n+\tcat >>longline.patch <<-EOF &&\n+\t$z512$z512\n+\tnot a long line\n+\t$z512$z512\n+\tEOF\n \ttest_must_fail git send-email \\\n \t\t--from=\"Example <nobody@example.com>\" \\\n \t\t--to=nobody@example.com \\\n@@ -424,7 +428,7 @@ test_expect_success $PREREQ 'reject long lines' '\n \t\t$patches longline.patch \\\n \t\t2>actual &&\n \tcat >expect <<-\\EOF &&\n-\tfatal: longline.patch: 35: patch contains a line longer than 998 characters\n+\tfatal: longline.patch:35 is longer than 998 characters\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -533,15 +537,17 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \t\t--validate \\\n \t\tlongline.patch 2>actual &&\n \ttest_path_is_file my-hooks.ran &&\n-\tcat >expect <<-\\EOF &&\n+\tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n+\tfatal: command '\"'\"'$(pwd)/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n-\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n+\thooks_path=\"$(pwd)/my-hooks\" &&\n+\ttest_config core.hooksPath \"$hooks_path\" &&\n \ttest_when_finished \"rm my-hooks.ran\" &&\n \ttest_must_fail git send-email \\\n \t\t--from=\"Example <nobody@example.com>\" \\\n@@ -550,8 +556,9 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \t\t--validate \\\n \t\tlongline.patch 2>actual &&\n \ttest_path_is_file my-hooks.ran &&\n-\tcat >expect <<-\\EOF &&\n+\tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n+\tfatal: command '\"'\"'$hooks_path/sendemail-validate'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n-- \n2.31.1.482.g6691c1be520\n\n"},{"id":"420968","messageId":"patch-2.5-f236f083e36-20210404T091649Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.5-00000000000-20210404T091649Z-avarab@gmail.com","subject":"[PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-04T09:19:44Z","receivedAt":"2021-04-04T09:20:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the duplicate checking of $? into a function. There's an\noutstanding series[1] wanting to add a third use of system() in this\nfile, let's not copy this boilerplate anymore when that happens.\n\n1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl | 48 +++++++++++++++++++++++++++++----------------\n 1 file changed, 31 insertions(+), 17 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 6893c8e5808..9724a9cae27 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -212,22 +212,31 @@ sub format_2822_time {\n my $multiedit;\n my $editor;\n \n+sub system_or_msg {\n+\tmy ($args, $msg) = @_;\n+\tsystem(@$args);\n+\tmy $signalled = $? & 127;\n+\tmy $exit_code = $? >> 8;\n+\treturn unless $signalled or $exit_code;\n+\n+\treturn sprintf(__(\"failed to run command %s, died with code %d\"),\n+\t\t       \"@$args\", $exit_code);\n+}\n+\n+sub system_or_die {\n+\tmy $msg = system_or_msg(@_);\n+\tdie $msg if $msg;\n+}\n+\n sub do_edit {\n \tif (!defined($editor)) {\n \t\t$editor = Git::command_oneline('var', 'GIT_EDITOR');\n \t}\n+\tmy $die_msg = __(\"the editor exited uncleanly, aborting everything\");\n \tif (defined($multiedit) && !$multiedit) {\n-\t\tfor (@_) {\n-\t\t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, $_);\n-\t\t\tif (($? & 127) || ($? >> 8)) {\n-\t\t\t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n-\t\t\t}\n-\t\t}\n+\t\tsystem_or_die(['sh', '-c', $editor.' \"$@\"', $editor, $_], $die_msg) for @_;\n \t} else {\n-\t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, @_);\n-\t\tif (($? & 127) || ($? >> 8)) {\n-\t\t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n-\t\t}\n+\t\tsystem_or_die(['sh', '-c', $editor.' \"$@\"', $editor, @_], $die_msg);\n \t}\n }\n \n@@ -698,9 +707,7 @@ sub is_format_patch_arg {\n if ($validate) {\n \tforeach my $f (@files) {\n \t\tunless (-p $f) {\n-\t\t\tmy $error = validate_patch($f, $target_xfer_encoding);\n-\t\t\t$error and die sprintf(__(\"fatal: %s: %s\\nwarning: no patches were sent\\n\"),\n-\t\t\t\t\t\t  $f, $error);\n+\t\t\tvalidate_patch($f, $target_xfer_encoding);\n \t\t}\n \t}\n }\n@@ -1938,6 +1945,12 @@ sub unique_email_list {\n \treturn @emails;\n }\n \n+sub validate_patch_error {\n+\tmy ($fn, $error) = @_;\n+\tdie sprintf(__(\"fatal: %s: %s\\nwarning: no patches were sent\\n\"),\n+\t\t    $fn, $error);\n+}\n+\n sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n@@ -1952,11 +1965,12 @@ sub validate_patch {\n \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n \t\t\t\tor die(\"chdir: $!\");\n \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n-\t\t\t\tif system($validate_hook, $target);\n+\t\t\tif (my $msg = system_or_msg([$validate_hook, $target])) {\n+\t\t\t\t$hook_error = __(\"rejected by sendemail-validate hook\");\n+\t\t\t}\n \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n \t\t}\n-\t\treturn $hook_error if $hook_error;\n+\t\tvalidate_patch_error($fn, $hook_error) if $hook_error;\n \t}\n \n \t# Any long lines will be automatically fixed if we use a suitable transfer\n@@ -1966,7 +1980,7 @@ sub validate_patch {\n \t\t\tor die sprintf(__(\"unable to open %s: %s\\n\"), $fn, $!);\n \t\twhile (my $line = <$fh>) {\n \t\t\tif (length($line) > 998) {\n-\t\t\t\treturn sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.);\n+\t\t\t\tvalidate_patch_error($fn, sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.));\n \t\t\t}\n \t\t}\n \t}\n-- \n2.31.1.482.g6691c1be520\n\n"},{"id":"421003","messageId":"xmqqmtuc5yx7.fsf@gitster.g","threadId":"55286","inReplyTo":"patch-2.5-f236f083e36-20210404T091649Z-avarab@gmail.com","subject":"Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-05T19:11:48Z","receivedAt":"2021-04-05T19:11:54Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> @@ -1938,6 +1945,12 @@ sub unique_email_list {\n>  \treturn @emails;\n>  }\n>  \n> +sub validate_patch_error {\n> +\tmy ($fn, $error) = @_;\n> +\tdie sprintf(__(\"fatal: %s: %s\\nwarning: no patches were sent\\n\"),\n> +\t\t    $fn, $error);\n> +}\n> +\n>  sub validate_patch {\n>  \tmy ($fn, $xfer_encoding) = @_;\n\nI like the overall direction of this series, but this change will\nsoon be reverted back to have the die/sprintf in the two callsite\nin 4/4 anyway, so this hunk looks more like \"I thought this would\nbe a good way, but in the end I had to change my mind\".\n\n> @@ -1952,11 +1965,12 @@ sub validate_patch {\n>  \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n>  \t\t\t\tor die(\"chdir: $!\");\n>  \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n> -\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n> -\t\t\t\tif system($validate_hook, $target);\n> +\t\t\tif (my $msg = system_or_msg([$validate_hook, $target])) {\n> +\t\t\t\t$hook_error = __(\"rejected by sendemail-validate hook\");\n> +\t\t\t}\n>  \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n>  \t\t}\n> -\t\treturn $hook_error if $hook_error;\n> +\t\tvalidate_patch_error($fn, $hook_error) if $hook_error;\n>  \t}\n>  \n>  \t# Any long lines will be automatically fixed if we use a suitable transfer\n> @@ -1966,7 +1980,7 @@ sub validate_patch {\n>  \t\t\tor die sprintf(__(\"unable to open %s: %s\\n\"), $fn, $!);\n>  \t\twhile (my $line = <$fh>) {\n>  \t\t\tif (length($line) > 998) {\n> -\t\t\t\treturn sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.);\n> +\t\t\t\tvalidate_patch_error($fn, sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.));\n>  \t\t\t}\n>  \t\t}\n>  \t}\n"},{"id":"421004","messageId":"xmqqim505ys0.fsf@gitster.g","threadId":"55286","inReplyTo":"patch-4.5-a1edceb4913-20210404T091649Z-avarab@gmail.com","subject":"Re: [PATCH v2 4/4] git-send-email: improve --validate error output","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-05T19:14:55Z","receivedAt":"2021-04-05T19:14:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> Improve the output we emit on --validate error to:\n>\n>  * Say \"FILE:LINE\" instead of \"FILE: LINE\".\n\nOK, that is an improvement because it matches \"grep -n\" hits,\ncompiler error messages, etc., to help the editors to jump to these\nlines.\n\n>  * Don't say \"patch contains a\" after just mentioning the filename,\n>    just leave it at \"FILE:LINE: is longer than[...]. The \"contains a\"\n>    sounded like we were talking about the file in general, when we're\n>    actually checking it line-by-line.\n\nThis, too.\n\n>  * Don't just say \"rejected by sendemail-validate hook\", but combine\n>    that with the system_or_msg() output to say what exit code the hook\n>    died with.\n>\n> I had an aborted attempt to make the line length checker note all\n> lines that were longer than the limit. I didn't think that was worth\n> the effort, but I've left in the testing change to check that we die\n> as soon as we spot the first long line.\n>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  git-send-email.perl   | 23 ++++++++++-------------\n>  t/t9001-send-email.sh | 17 ++++++++++++-----\n>  2 files changed, 22 insertions(+), 18 deletions(-)\n\nWill queue.  I like the end result, but left a comment about\nflipping-and-flopping between 2/4 and this step on an extra\n\"validate_patch_error\" helper sub.\n\nThanks.\n"},{"id":"421028","messageId":"xmqqft0447lu.fsf@gitster.g","threadId":"55286","inReplyTo":"patch-2.5-f236f083e36-20210404T091649Z-avarab@gmail.com","subject":"Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-05T23:47:09Z","receivedAt":"2021-04-05T23:47:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> Refactor the duplicate checking of $? into a function. There's an\n> outstanding series[1] wanting to add a third use of system() in this\n> file, let's not copy this boilerplate anymore when that happens.\n>\n> 1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com\n>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  git-send-email.perl | 48 +++++++++++++++++++++++++++++----------------\n>  1 file changed, 31 insertions(+), 17 deletions(-)\n\n>  sub validate_patch {\n>  \tmy ($fn, $xfer_encoding) = @_;\n>  \n> @@ -1952,11 +1965,12 @@ sub validate_patch {\n>  \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n>  \t\t\t\tor die(\"chdir: $!\");\n>  \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n> -\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n> -\t\t\t\tif system($validate_hook, $target);\n> +\t\t\tif (my $msg = system_or_msg([$validate_hook, $target])) {\n> +\t\t\t\t$hook_error = __(\"rejected by sendemail-validate hook\");\n> +\t\t\t}\n>  \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n>  \t\t}\n> -\t\treturn $hook_error if $hook_error;\n> +\t\tvalidate_patch_error($fn, $hook_error) if $hook_error;\n>  \t}\n\nOne big thing that is different between this version and the one in\nEmily's \"config hook\" topic is that this is still limited to the\ncase where $repo exists.  In the new world order, it will not matter\nin what directory the command runs, as long as \"git hook\" finds the\nhook, and details of the invocation is hidden behind the command.\n\nI presume that Emily's series is expected to be updated soonish?\nPlease figure out who to go first and other details to work well\ntogether between you two.\n\nI'd drop the \"config hook\" topic for now, and I think the endpoint\nof these four-patch series (the first \"map vs for\" can move more or\nless independently) are more-or-less in a good shape (even though as\nI said already, I think 2/4 and 4/4 want to be updated not to\nintroduce the intermediate \"validate_patch_error()\" sub in 2/4 only\nto get rid of it in 4/4) and would require only one update.\n\nThanks.\n"},{"id":"421063","messageId":"cover-0.4-00000000000-20210406T135715Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.5-00000000000-20210404T091649Z-avarab@gmail.com","subject":"[PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-06T14:00:34Z","receivedAt":"2021-04-06T14:00:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Version 3 yields the TREESAME end result as v2[1], but re-arranges the\nway we get there to make the progression more understandable, along\nwith a minor commit message update.\n\nI also peeled off the previous 1st patch, as Junio's picked it up\nseparately and marged it into \"next\" already.\n\n1. http://lore.kernel.org/git/cover-0.5-00000000000-20210404T091649Z-avarab@gmail.com\n\nÆvar Arnfjörð Bjarmason (3):\n  git-send-email: test full --validate output\n  git-send-email: refactor duplicate $? checks into a function\n  git-send-email: improve --validate error output\n\n git-send-email.perl   | 45 +++++++++++++++++++++++++++----------------\n t/t9001-send-email.sh | 35 +++++++++++++++++++++++++--------\n 2 files changed, 55 insertions(+), 25 deletions(-)\n\nRange-diff:\n2:  15b59c226d4 = 1:  6e1009e5bed git-send-email: test full --validate output\n1:  f236f083e36 ! 2:  4ee582d8301 git-send-email: refactor duplicate $? checks into a function\n    @@ git-send-email.perl: sub is_format_patch_arg {\n      \t\t}\n      \t}\n      }\n    -@@ git-send-email.perl: sub unique_email_list {\n    - \treturn @emails;\n    - }\n    - \n    -+sub validate_patch_error {\n    -+\tmy ($fn, $error) = @_;\n    -+\tdie sprintf(__(\"fatal: %s: %s\\nwarning: no patches were sent\\n\"),\n    -+\t\t    $fn, $error);\n    -+}\n    -+\n    - sub validate_patch {\n    - \tmy ($fn, $xfer_encoding) = @_;\n    - \n     @@ git-send-email.perl: sub validate_patch {\n      \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n      \t\t\t\tor die(\"chdir: $!\");\n      \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n     -\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n     -\t\t\t\tif system($validate_hook, $target);\n    -+\t\t\tif (my $msg = system_or_msg([$validate_hook, $target])) {\n    -+\t\t\t\t$hook_error = __(\"rejected by sendemail-validate hook\");\n    -+\t\t\t}\n    ++\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n      \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n      \t\t}\n     -\t\treturn $hook_error if $hook_error;\n    -+\t\tvalidate_patch_error($fn, $hook_error) if $hook_error;\n    ++\t\tif ($hook_error) {\n    ++\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n    ++\t\t\t\t       \"warning: no patches were sent\\n\"), $fn);\n    ++\t\t}\n      \t}\n      \n      \t# Any long lines will be automatically fixed if we use a suitable transfer\n    @@ git-send-email.perl: sub validate_patch {\n      \t\twhile (my $line = <$fh>) {\n      \t\t\tif (length($line) > 998) {\n     -\t\t\t\treturn sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.);\n    -+\t\t\t\tvalidate_patch_error($fn, sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.));\n    ++\t\t\t\tdie sprintf(__(\"fatal: %s: %d: patch contains a line longer than 998 characters\\n\" .\n    ++\t\t\t\t\t       \"warning: no patches were sent\\n\"),\n    ++\t\t\t\t\t    $fn, $.);\n      \t\t\t}\n      \t\t}\n      \t}\n3:  a1edceb4913 ! 3:  8a67afd3404 git-send-email: improve --validate error output\n    @@ Commit message\n     \n         Improve the output we emit on --validate error to:\n     \n    -     * Say \"FILE:LINE\" instead of \"FILE: LINE\".\n    +     * Say \"FILE:LINE\" instead of \"FILE: LINE\", to match \"grep -n\",\n    +       compiler error messages etc.\n     \n          * Don't say \"patch contains a\" after just mentioning the filename,\n            just leave it at \"FILE:LINE: is longer than[...]. The \"contains a\"\n    @@ git-send-email.perl: sub system_or_msg {\n      }\n      \n      sub system_or_die {\n    -@@ git-send-email.perl: sub unique_email_list {\n    - \treturn @emails;\n    - }\n    - \n    --sub validate_patch_error {\n    --\tmy ($fn, $error) = @_;\n    --\tdie sprintf(__(\"fatal: %s: %s\\nwarning: no patches were sent\\n\"),\n    --\t\t    $fn, $error);\n    --}\n    --\n    - sub validate_patch {\n    - \tmy ($fn, $xfer_encoding) = @_;\n    - \n     @@ git-send-email.perl: sub validate_patch {\n    - \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n    - \t\t\t\tor die(\"chdir: $!\");\n    - \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n    --\t\t\tif (my $msg = system_or_msg([$validate_hook, $target])) {\n    --\t\t\t\t$hook_error = __(\"rejected by sendemail-validate hook\");\n    --\t\t\t}\n    -+\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n    - \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n      \t\t}\n    --\t\tvalidate_patch_error($fn, $hook_error) if $hook_error;\n    -+\t\tif ($hook_error) {\n    -+\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n    + \t\tif ($hook_error) {\n    + \t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n    +-\t\t\t\t       \"warning: no patches were sent\\n\"), $fn);\n     +\t\t\t\t       \"%s\\n\" .\n     +\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n    -+\t\t}\n    + \t\t}\n      \t}\n      \n    - \t# Any long lines will be automatically fixed if we use a suitable transfer\n     @@ git-send-email.perl: sub validate_patch {\n      \t\t\tor die sprintf(__(\"unable to open %s: %s\\n\"), $fn, $!);\n      \t\twhile (my $line = <$fh>) {\n      \t\t\tif (length($line) > 998) {\n    --\t\t\t\tvalidate_patch_error($fn, sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.));\n    +-\t\t\t\tdie sprintf(__(\"fatal: %s: %d: patch contains a line longer than 998 characters\\n\" .\n    +-\t\t\t\t\t       \"warning: no patches were sent\\n\"),\n    +-\t\t\t\t\t    $fn, $.);\n     +\t\t\t\tdie sprintf(__(\"fatal: %s:%d is longer than 998 characters\\n\" .\n     +\t\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $.);\n      \t\t\t}\n-- \n2.31.1.527.g9b8f7de2547\n\n"},{"id":"421064","messageId":"patch-1.4-6e1009e5bed-20210406T135716Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.4-00000000000-20210406T135715Z-avarab@gmail.com","subject":"[PATCH v3 1/3] git-send-email: test full --validate output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-06T14:00:35Z","receivedAt":"2021-04-06T14:00:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Change the tests that grep substrings out of the output to use a full\ntest_cmp, in preparation for improving the output.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t9001-send-email.sh | 24 ++++++++++++++++++------\n 1 file changed, 18 insertions(+), 6 deletions(-)\n\ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 1a1caf8f2ed..74225e3dc7a 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -422,8 +422,12 @@ test_expect_success $PREREQ 'reject long lines' '\n \t\t--smtp-server=\"$(pwd)/fake.sendmail\" \\\n \t\t--transfer-encoding=8bit \\\n \t\t$patches longline.patch \\\n-\t\t2>errors &&\n-\tgrep longline.patch errors\n+\t\t2>actual &&\n+\tcat >expect <<-\\EOF &&\n+\tfatal: longline.patch: 35: patch contains a line longer than 998 characters\n+\twarning: no patches were sent\n+\tEOF\n+\ttest_cmp expect actual\n '\n \n test_expect_success $PREREQ 'no patch was sent' '\n@@ -527,9 +531,13 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \t\t--to=nobody@example.com \\\n \t\t--smtp-server=\"$(pwd)/fake.sendmail\" \\\n \t\t--validate \\\n-\t\tlongline.patch 2>err &&\n+\t\tlongline.patch 2>actual &&\n \ttest_path_is_file my-hooks.ran &&\n-\tgrep \"rejected by sendemail-validate\" err\n+\tcat >expect <<-\\EOF &&\n+\tfatal: longline.patch: rejected by sendemail-validate hook\n+\twarning: no patches were sent\n+\tEOF\n+\ttest_cmp expect actual\n '\n \n test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n@@ -540,9 +548,13 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \t\t--to=nobody@example.com \\\n \t\t--smtp-server=\"$(pwd)/fake.sendmail\" \\\n \t\t--validate \\\n-\t\tlongline.patch 2>err &&\n+\t\tlongline.patch 2>actual &&\n \ttest_path_is_file my-hooks.ran &&\n-\tgrep \"rejected by sendemail-validate\" err\n+\tcat >expect <<-\\EOF &&\n+\tfatal: longline.patch: rejected by sendemail-validate hook\n+\twarning: no patches were sent\n+\tEOF\n+\ttest_cmp expect actual\n '\n \n for enc in 7bit 8bit quoted-printable base64\n-- \n2.31.1.527.g9b8f7de2547\n\n"},{"id":"421065","messageId":"patch-2.4-4ee582d8301-20210406T135716Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.4-00000000000-20210406T135715Z-avarab@gmail.com","subject":"[PATCH v3 2/3] git-send-email: refactor duplicate $? checks into a function","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-06T14:00:36Z","receivedAt":"2021-04-06T14:00:51Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Refactor the duplicate checking of $? into a function. There's an\noutstanding series[1] wanting to add a third use of system() in this\nfile, let's not copy this boilerplate anymore when that happens.\n\n1. http://lore.kernel.org/git/87y2esg22j.fsf@evledraar.gmail.com\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl | 45 ++++++++++++++++++++++++++++-----------------\n 1 file changed, 28 insertions(+), 17 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 6893c8e5808..2dd48621759 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -212,22 +212,31 @@ sub format_2822_time {\n my $multiedit;\n my $editor;\n \n+sub system_or_msg {\n+\tmy ($args, $msg) = @_;\n+\tsystem(@$args);\n+\tmy $signalled = $? & 127;\n+\tmy $exit_code = $? >> 8;\n+\treturn unless $signalled or $exit_code;\n+\n+\treturn sprintf(__(\"failed to run command %s, died with code %d\"),\n+\t\t       \"@$args\", $exit_code);\n+}\n+\n+sub system_or_die {\n+\tmy $msg = system_or_msg(@_);\n+\tdie $msg if $msg;\n+}\n+\n sub do_edit {\n \tif (!defined($editor)) {\n \t\t$editor = Git::command_oneline('var', 'GIT_EDITOR');\n \t}\n+\tmy $die_msg = __(\"the editor exited uncleanly, aborting everything\");\n \tif (defined($multiedit) && !$multiedit) {\n-\t\tfor (@_) {\n-\t\t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, $_);\n-\t\t\tif (($? & 127) || ($? >> 8)) {\n-\t\t\t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n-\t\t\t}\n-\t\t}\n+\t\tsystem_or_die(['sh', '-c', $editor.' \"$@\"', $editor, $_], $die_msg) for @_;\n \t} else {\n-\t\tsystem('sh', '-c', $editor.' \"$@\"', $editor, @_);\n-\t\tif (($? & 127) || ($? >> 8)) {\n-\t\t\tdie(__(\"the editor exited uncleanly, aborting everything\"));\n-\t\t}\n+\t\tsystem_or_die(['sh', '-c', $editor.' \"$@\"', $editor, @_], $die_msg);\n \t}\n }\n \n@@ -698,9 +707,7 @@ sub is_format_patch_arg {\n if ($validate) {\n \tforeach my $f (@files) {\n \t\tunless (-p $f) {\n-\t\t\tmy $error = validate_patch($f, $target_xfer_encoding);\n-\t\t\t$error and die sprintf(__(\"fatal: %s: %s\\nwarning: no patches were sent\\n\"),\n-\t\t\t\t\t\t  $f, $error);\n+\t\t\tvalidate_patch($f, $target_xfer_encoding);\n \t\t}\n \t}\n }\n@@ -1952,11 +1959,13 @@ sub validate_patch {\n \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n \t\t\t\tor die(\"chdir: $!\");\n \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n-\t\t\t\tif system($validate_hook, $target);\n+\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n \t\t}\n-\t\treturn $hook_error if $hook_error;\n+\t\tif ($hook_error) {\n+\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n+\t\t\t\t       \"warning: no patches were sent\\n\"), $fn);\n+\t\t}\n \t}\n \n \t# Any long lines will be automatically fixed if we use a suitable transfer\n@@ -1966,7 +1975,9 @@ sub validate_patch {\n \t\t\tor die sprintf(__(\"unable to open %s: %s\\n\"), $fn, $!);\n \t\twhile (my $line = <$fh>) {\n \t\t\tif (length($line) > 998) {\n-\t\t\t\treturn sprintf(__(\"%s: patch contains a line longer than 998 characters\"), $.);\n+\t\t\t\tdie sprintf(__(\"fatal: %s: %d: patch contains a line longer than 998 characters\\n\" .\n+\t\t\t\t\t       \"warning: no patches were sent\\n\"),\n+\t\t\t\t\t    $fn, $.);\n \t\t\t}\n \t\t}\n \t}\n-- \n2.31.1.527.g9b8f7de2547\n\n"},{"id":"421066","messageId":"patch-3.4-8a67afd3404-20210406T135716Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-0.4-00000000000-20210406T135715Z-avarab@gmail.com","subject":"[PATCH v3 3/3] git-send-email: improve --validate error output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-06T14:00:37Z","receivedAt":"2021-04-06T14:00:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Improve the output we emit on --validate error to:\n\n * Say \"FILE:LINE\" instead of \"FILE: LINE\", to match \"grep -n\",\n   compiler error messages etc.\n\n * Don't say \"patch contains a\" after just mentioning the filename,\n   just leave it at \"FILE:LINE: is longer than[...]. The \"contains a\"\n   sounded like we were talking about the file in general, when we're\n   actually checking it line-by-line.\n\n * Don't just say \"rejected by sendemail-validate hook\", but combine\n   that with the system_or_msg() output to say what exit code the hook\n   died with.\n\nI had an aborted attempt to make the line length checker note all\nlines that were longer than the limit. I didn't think that was worth\nthe effort, but I've left in the testing change to check that we die\nas soon as we spot the first long line.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl   | 12 ++++++------\n t/t9001-send-email.sh | 17 ++++++++++++-----\n 2 files changed, 18 insertions(+), 11 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 2dd48621759..175da07d946 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -219,8 +219,8 @@ sub system_or_msg {\n \tmy $exit_code = $? >> 8;\n \treturn unless $signalled or $exit_code;\n \n-\treturn sprintf(__(\"failed to run command %s, died with code %d\"),\n-\t\t       \"@$args\", $exit_code);\n+\treturn sprintf(__(\"fatal: command '%s' died with exit code %d\"),\n+\t\t       $args->[0], $exit_code);\n }\n \n sub system_or_die {\n@@ -1964,7 +1964,8 @@ sub validate_patch {\n \t\t}\n \t\tif ($hook_error) {\n \t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n-\t\t\t\t       \"warning: no patches were sent\\n\"), $fn);\n+\t\t\t\t       \"%s\\n\" .\n+\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n \t\t}\n \t}\n \n@@ -1975,9 +1976,8 @@ sub validate_patch {\n \t\t\tor die sprintf(__(\"unable to open %s: %s\\n\"), $fn, $!);\n \t\twhile (my $line = <$fh>) {\n \t\t\tif (length($line) > 998) {\n-\t\t\t\tdie sprintf(__(\"fatal: %s: %d: patch contains a line longer than 998 characters\\n\" .\n-\t\t\t\t\t       \"warning: no patches were sent\\n\"),\n-\t\t\t\t\t    $fn, $.);\n+\t\t\t\tdie sprintf(__(\"fatal: %s:%d is longer than 998 characters\\n\" .\n+\t\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $.);\n \t\t\t}\n \t\t}\n \t}\ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 74225e3dc7a..65b30353719 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -415,7 +415,11 @@ test_expect_success $PREREQ 'reject long lines' '\n \tz512=$z64$z64$z64$z64$z64$z64$z64$z64 &&\n \tclean_fake_sendmail &&\n \tcp $patches longline.patch &&\n-\techo $z512$z512 >>longline.patch &&\n+\tcat >>longline.patch <<-EOF &&\n+\t$z512$z512\n+\tnot a long line\n+\t$z512$z512\n+\tEOF\n \ttest_must_fail git send-email \\\n \t\t--from=\"Example <nobody@example.com>\" \\\n \t\t--to=nobody@example.com \\\n@@ -424,7 +428,7 @@ test_expect_success $PREREQ 'reject long lines' '\n \t\t$patches longline.patch \\\n \t\t2>actual &&\n \tcat >expect <<-\\EOF &&\n-\tfatal: longline.patch: 35: patch contains a line longer than 998 characters\n+\tfatal: longline.patch:35 is longer than 998 characters\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -533,15 +537,17 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \t\t--validate \\\n \t\tlongline.patch 2>actual &&\n \ttest_path_is_file my-hooks.ran &&\n-\tcat >expect <<-\\EOF &&\n+\tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n+\tfatal: command '\"'\"'$(pwd)/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n-\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n+\thooks_path=\"$(pwd)/my-hooks\" &&\n+\ttest_config core.hooksPath \"$hooks_path\" &&\n \ttest_when_finished \"rm my-hooks.ran\" &&\n \ttest_must_fail git send-email \\\n \t\t--from=\"Example <nobody@example.com>\" \\\n@@ -550,8 +556,9 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \t\t--validate \\\n \t\tlongline.patch 2>actual &&\n \ttest_path_is_file my-hooks.ran &&\n-\tcat >expect <<-\\EOF &&\n+\tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n+\tfatal: command '\"'\"'$hooks_path/sendemail-validate'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n-- \n2.31.1.527.g9b8f7de2547\n\n"},{"id":"421093","messageId":"xmqq35w32lx8.fsf@gitster.g","threadId":"55286","inReplyTo":"cover-0.4-00000000000-20210406T135715Z-avarab@gmail.com","subject":"Re: [PATCH v3 0/3] refactor duplicate $? checks into a function + improve errors","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-06T20:33:07Z","receivedAt":"2021-04-06T20:33:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason  <avarab@gmail.com> writes:\n\n> Version 3 yields the TREESAME end result as v2[1], but re-arranges the\n> way we get there to make the progression more understandable, along\n> with a minor commit message update.\n\nNice.  I see no more nits to pick ;-)\n"},{"id":"421107","messageId":"xmqq5z0y2540.fsf@gitster.g","threadId":"55286","inReplyTo":"20210311021037.3001235-38-emilyshaffer@google.com","subject":"Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-07T02:36:15Z","receivedAt":"2021-04-07T02:36:22Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> By showing the list of all hooks in 'git help hook' for users to refer\n> to, 'git help hook' becomes a one-stop shop for hook authorship. Since\n> some may still have muscle memory for 'git help githooks', though,\n> reference the 'git hook' commands and otherwise don't remove content.\n>\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  Documentation/git-hook.txt     |  11 +\n>  Documentation/githooks.txt     | 716 +--------------------------------\n>  Documentation/native-hooks.txt | 708 ++++++++++++++++++++++++++++++++\n>  3 files changed, 724 insertions(+), 711 deletions(-)\n>  create mode 100644 Documentation/native-hooks.txt\n\nWhile this would be a very good move when this were the only topic\njuggling the hook related documentation, in the real world, it\ncreates rather nasty \"ouch, the original hooks document was updated,\nand we need to carry these changes over to the new native-hooks\nfile\" conflicts with multiple commits on different topics.\n\n$ git log --oneline --no-merges es/config-hooks..seen Documentation/githooks.txt\n2d4e48b8ee fsmonitor--daemon: man page and documentation\n23c781f173 githooks.txt: clarify documentation on reference-transaction hook\n5f308a89d8 githooks.txt: replace mentions of SHA-1 specific properties\n7efc378205 doc: fix some typos\n\n$ git log --oneline --no-merges ^master es/config-hooks..seen Documentation/githooks.txt\n2d4e48b8ee fsmonitor--daemon: man page and documentation\n\nAs three of the four changes are already in master, it probably is a\ngood idea to rebase this topic (and redo this step) to update the\nnative-hooks.txt\n\nI am not sure offhand how ready fsmonitor--daemon stuff is, but if\nit takes longer to stabilize than this topic, it might make sense to\nhold off the changes to githooks.txt in that topic, until this topic\nstabilizes enough to hit at least 'next', preferrably 'master', and\nthen base that topic (or at least the documentation part of it) on\nthe final shape of the native-hooks.txt.\n\nOr better ideas?\n\nThanks.\n"},{"id":"421261","messageId":"7b651fbf-7f54-21b3-3775-d18c20f85a1a@jeffhostetler.com","threadId":"55286","inReplyTo":"xmqq5z0y2540.fsf@gitster.g","subject":"Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages","fromName":"Jeff Hostetler","fromEmail":"git@jeffhostetler.com","sentAt":"2021-04-08T20:20:56Z","receivedAt":"2021-04-08T20:20:58Z","isPatch":true,"sender":{"key":"git@jeffhostetler.com","avatar":null},"body":"\n\nOn 4/6/21 10:36 PM, Junio C Hamano wrote:\n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n>> By showing the list of all hooks in 'git help hook' for users to refer\n>> to, 'git help hook' becomes a one-stop shop for hook authorship. Since\n>> some may still have muscle memory for 'git help githooks', though,\n>> reference the 'git hook' commands and otherwise don't remove content.\n>>\n>> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n>> ---\n>>   Documentation/git-hook.txt     |  11 +\n>>   Documentation/githooks.txt     | 716 +--------------------------------\n>>   Documentation/native-hooks.txt | 708 ++++++++++++++++++++++++++++++++\n>>   3 files changed, 724 insertions(+), 711 deletions(-)\n>>   create mode 100644 Documentation/native-hooks.txt\n> \n> While this would be a very good move when this were the only topic\n> juggling the hook related documentation, in the real world, it\n> creates rather nasty \"ouch, the original hooks document was updated,\n> and we need to carry these changes over to the new native-hooks\n> file\" conflicts with multiple commits on different topics.\n> \n> $ git log --oneline --no-merges es/config-hooks..seen Documentation/githooks.txt\n> 2d4e48b8ee fsmonitor--daemon: man page and documentation\n> 23c781f173 githooks.txt: clarify documentation on reference-transaction hook\n> 5f308a89d8 githooks.txt: replace mentions of SHA-1 specific properties\n> 7efc378205 doc: fix some typos\n> \n> $ git log --oneline --no-merges ^master es/config-hooks..seen Documentation/githooks.txt\n> 2d4e48b8ee fsmonitor--daemon: man page and documentation\n> \n> As three of the four changes are already in master, it probably is a\n> good idea to rebase this topic (and redo this step) to update the\n> native-hooks.txt\n> \n> I am not sure offhand how ready fsmonitor--daemon stuff is, but if\n> it takes longer to stabilize than this topic, it might make sense to\n> hold off the changes to githooks.txt in that topic, until this topic\n> stabilizes enough to hit at least 'next', preferrably 'master', and\n> then base that topic (or at least the documentation part of it) on\n> the final shape of the native-hooks.txt.\n> \n> Or better ideas?\n> \n> Thanks.\n> \n\nI expect the fsmonitor stuff to take a while.  It is rather large\nand complicated.  My changes in the Documentation are rather minor.\nAnd I wouldn't want to be the sole reason to hold up Emily's changes.\n\nIf it would be helpful, you can add a \"revert\" commit on top of my\nbranch for my documentation commit -or- just drop it completely from\nmy series.  Then I can re-adjust/rebase my doc changes before\nI send a V2.\n\nJeff\n"},{"id":"421272","messageId":"xmqqim4wsch8.fsf@gitster.g","threadId":"55286","inReplyTo":"7b651fbf-7f54-21b3-3775-d18c20f85a1a@jeffhostetler.com","subject":"Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-08T21:17:07Z","receivedAt":"2021-04-08T21:17:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff Hostetler <git@jeffhostetler.com> writes:\n\n> I expect the fsmonitor stuff to take a while.  It is rather large\n> and complicated.  My changes in the Documentation are rather minor.\n> And I wouldn't want to be the sole reason to hold up Emily's changes.\n>\n> If it would be helpful, you can add a \"revert\" commit on top of my\n> branch for my documentation commit -or- just drop it completely from\n> my series.  Then I can re-adjust/rebase my doc changes before\n> I send a V2.\n\nSounds like a plan.  I'll drop that step for now before the next\nintegration cycle.\n\nThere is another topic that interacts with es/config-hooks topic\nbadly (which I haven't resolved) in flight, though.\n\nThanks.\n"},{"id":"421285","messageId":"xmqqpmz4qtxi.fsf@gitster.g","threadId":"55286","inReplyTo":"xmqqft0447lu.fsf@gitster.g","subject":"Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-08T22:43:05Z","receivedAt":"2021-04-08T22:43:11Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> One big thing that is different between this version and the one in\n> Emily's \"config hook\" topic is that this is still limited to the\n> case where $repo exists.  In the new world order, it will not matter\n> in what directory the command runs, as long as \"git hook\" finds the\n> hook, and details of the invocation is hidden behind the command.\n>\n> I presume that Emily's series is expected to be updated soonish?\n> Please figure out who to go first and other details to work well\n> together between you two.\n\nSince I didn't hear from either of you, I'll queue with this\npossibly bogus conflict resolution for now.\n\nThanks.\n\n\ndiff --cc git-send-email.perl\nindex 175da07d94,73e1e0b51a..0000000000\n--- i/git-send-email.perl\n+++ w/git-send-email.perl\n@@@ -1947,27 -1940,11 +1947,13 @@@ sub unique_email_list \n  \n  sub validate_patch {\n  \tmy ($fn, $xfer_encoding) = @_;\n--\n- \tif ($repo) {\n- \t\tmy $validate_hook = catfile($repo->hooks_path(),\n- \t\t\t\t\t    'sendemail-validate');\n- \t\tmy $hook_error;\n- \t\tif (-x $validate_hook) {\n- \t\t\tmy $target = abs_path($fn);\n- \t\t\t# The hook needs a correct cwd and GIT_DIR.\n- \t\t\tmy $cwd_save = cwd();\n- \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n- \t\t\t\tor die(\"chdir: $!\");\n- \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n- \t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n- \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n- \t\t}\n- \t\tif ($hook_error) {\n- \t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n- \t\t\t\t       \"%s\\n\" .\n- \t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n- \t\t}\n+ \tmy $target = abs_path($fn);\n -\treturn \"rejected by sendemail-validate hook\"\n -\t\tif system((\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-a\",\n -\t\t\t\t$target));\n++\t$hook_error = system_or_msg([qw(git hook run sendemail-validate -a), $target]);\n++\tif ($hook_error) {\n++\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n++\t\t\t       \"%s\\n\" .\n++\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n +\t}\n  \n  \t# Any long lines will be automatically fixed if we use a suitable transfer\n  \t# encoding.\n"},{"id":"421289","messageId":"xmqqft00qtrj.fsf@gitster.g","threadId":"55286","inReplyTo":"xmqqpmz4qtxi.fsf@gitster.g","subject":"Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-08T22:46:40Z","receivedAt":"2021-04-08T22:46:47Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> One big thing that is different between this version and the one in\n>> Emily's \"config hook\" topic is that this is still limited to the\n>> case where $repo exists.  In the new world order, it will not matter\n>> in what directory the command runs, as long as \"git hook\" finds the\n>> hook, and details of the invocation is hidden behind the command.\n>>\n>> I presume that Emily's series is expected to be updated soonish?\n>> Please figure out who to go first and other details to work well\n>> together between you two.\n>\n> Since I didn't hear from either of you, I'll queue with this\n> possibly bogus conflict resolution for now.\n>\n\nWell, I retract it.  This makes many steps in send-email tests\nfail.  For now, es/config-hooks topic is excluded from 'seen'.\n\nWhat's the status of that topic, if there weren't other topics in\nflight that interfere with it, by the way?  Is it otherwise a good\nenough shape to be given priority and stable enough to get other\ntopics rebased on top of it?\n\nThanks.\n"},{"id":"421307","messageId":"YG+VxDvlov81PfK3@google.com","threadId":"55286","inReplyTo":"xmqq5z0y2540.fsf@gitster.g","subject":"Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-04-08T23:46:12Z","receivedAt":"2021-04-08T23:46:22Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Apr 06, 2021 at 07:36:15PM -0700, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > By showing the list of all hooks in 'git help hook' for users to refer\n> > to, 'git help hook' becomes a one-stop shop for hook authorship. Since\n> > some may still have muscle memory for 'git help githooks', though,\n> > reference the 'git hook' commands and otherwise don't remove content.\n> >\n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >  Documentation/git-hook.txt     |  11 +\n> >  Documentation/githooks.txt     | 716 +--------------------------------\n> >  Documentation/native-hooks.txt | 708 ++++++++++++++++++++++++++++++++\n> >  3 files changed, 724 insertions(+), 711 deletions(-)\n> >  create mode 100644 Documentation/native-hooks.txt\n> \n> While this would be a very good move when this were the only topic\n> juggling the hook related documentation, in the real world, it\n> creates rather nasty \"ouch, the original hooks document was updated,\n> and we need to carry these changes over to the new native-hooks\n> file\" conflicts with multiple commits on different topics.\n> \n> $ git log --oneline --no-merges es/config-hooks..seen Documentation/githooks.txt\n> 2d4e48b8ee fsmonitor--daemon: man page and documentation\n> 23c781f173 githooks.txt: clarify documentation on reference-transaction hook\n> 5f308a89d8 githooks.txt: replace mentions of SHA-1 specific properties\n> 7efc378205 doc: fix some typos\n> \n> $ git log --oneline --no-merges ^master es/config-hooks..seen Documentation/githooks.txt\n> 2d4e48b8ee fsmonitor--daemon: man page and documentation\n> \n> As three of the four changes are already in master, it probably is a\n> good idea to rebase this topic (and redo this step) to update the\n> native-hooks.txt\n> \n> I am not sure offhand how ready fsmonitor--daemon stuff is, but if\n> it takes longer to stabilize than this topic, it might make sense to\n> hold off the changes to githooks.txt in that topic, until this topic\n> stabilizes enough to hit at least 'next', preferrably 'master', and\n> then base that topic (or at least the documentation part of it) on\n> the final shape of the native-hooks.txt.\n> \n> Or better ideas?\n> \n> Thanks.\n\nI got bitten by this same issue with native-hooks.txt while addressing\ncomments, too. Another commenter suggested to not inline those hook\ndefinitions into \"git help hook\" - so I plan to drop that part of this\npatch. If it makes it easier for you, I think you could revert this last\ncommit; if we decide later that we want to have \"git help hook\" share\nthe hook definitions after all, I think we should do that separately and\nas a quick change not stuck behind 36 other complicated patches.\n\n - Emily\n"},{"id":"421309","messageId":"87tuoggwmy.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"xmqqft00qtrj.fsf@gitster.g","subject":"Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-04-08T23:54:45Z","receivedAt":"2021-04-08T23:54:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Apr 09 2021, Junio C Hamano wrote:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> Junio C Hamano <gitster@pobox.com> writes:\n>>\n>>> One big thing that is different between this version and the one in\n>>> Emily's \"config hook\" topic is that this is still limited to the\n>>> case where $repo exists.  In the new world order, it will not matter\n>>> in what directory the command runs, as long as \"git hook\" finds the\n>>> hook, and details of the invocation is hidden behind the command.\n>>>\n>>> I presume that Emily's series is expected to be updated soonish?\n>>> Please figure out who to go first and other details to work well\n>>> together between you two.\n>>\n>> Since I didn't hear from either of you, I'll queue with this\n>> possibly bogus conflict resolution for now.\n>>\n>\n> Well, I retract it.  This makes many steps in send-email tests\n> fail.  For now, es/config-hooks topic is excluded from 'seen'.\n\nSorry about not replying earlier upthread. FWIW I didn't look deeply\ninto how the chdir etc. might interact with Emily's topic. I figured\nwe'd want the $? etc. cleanup first, and that just deleting most of that\ncode once we had some hook runner to shell out to would be easy.\n\n> What's the status of that topic, if there weren't other topics in\n> flight that interfere with it, by the way?  Is it otherwise a good\n> enough shape to be given priority and stable enough to get other\n> topics rebased on top of it?\n\nI see I've mentioned [1] in passing to you before, but in summary I have\nsome major qualms about parts of it, but very much like the overall\ndirection/goal of having hooks in config.\n\nElevator pitch summary of the lengthy [1]: hooks in config: good, but\nhaving a \"git hook\" command introduce some nascent UI for managing a\nsubset of git-config: somewhere between \"meh\" / \"bad idea\" (see security\nconcerns in [1]) / \"not needed\". I.e. I demonstrated that we can replace\nit with a trivial git-config wrapper, if the series doesn't go out of\nits way to make it difficult (i.e. we can/should stick all config for a\ngiven hook in the same <prefix>, and not re-invent the\n\"sendemail.identity\" special-case).\n\nI'd very much like the author to respond to that :) And/or for others to\nchime in with what they think.\n\n1. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n"},{"id":"421313","messageId":"xmqqft00pbnd.fsf@gitster.g","threadId":"55286","inReplyTo":"YG+VxDvlov81PfK3@google.com","subject":"Re: [PATCH v8 37/37] docs: unify githooks and git-hook manpages","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-09T00:03:18Z","receivedAt":"2021-04-09T00:03:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> I got bitten by this same issue with native-hooks.txt while addressing\n> comments, too. Another commenter suggested to not inline those hook\n> definitions into \"git help hook\" - so I plan to drop that part of this\n> patch. If it makes it easier for you, I think you could revert this last\n> commit; if we decide later that we want to have \"git help hook\" share\n> the hook definitions after all, I think we should do that separately and\n> as a quick change not stuck behind 36 other complicated patches.\n\nI've already discarded the step, and then I had to eject the whole\ntopic from 'seen' for now (see my other message to you earlier\ntoday).  The \"other complicated patches\" need to be whipped into\nshape to be at least in 'next' first; I do not know how close the\nlast round is from that state.\n\nThanks.\n\n"},{"id":"421314","messageId":"xmqqblaopbep.fsf@gitster.g","threadId":"55286","inReplyTo":"87tuoggwmy.fsf@evledraar.gmail.com","subject":"Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-04-09T00:08:30Z","receivedAt":"2021-04-09T00:08:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Fri, Apr 09 2021, Junio C Hamano wrote:\n> ...\n>> What's the status of that topic, if there weren't other topics in\n>> flight that interfere with it, by the way?  Is it otherwise a good\n>> enough shape to be given priority and stable enough to get other\n>> topics rebased on top of it?\n>\n> I see I've mentioned [1] in passing to you before, but in summary I have\n> some major qualms about parts of it, but very much like the overall\n> direction/goal of having hooks in config.\n>\n> Elevator pitch summary of the lengthy [1]: hooks in config: good, but\n> having a \"git hook\" command introduce some nascent UI for managing a\n> subset of git-config: somewhere between \"meh\" / \"bad idea\" (see security\n> concerns in [1]) / \"not needed\". I.e. I demonstrated that we can replace\n> it with a trivial git-config wrapper, if the series doesn't go out of\n> its way to make it difficult (i.e. we can/should stick all config for a\n> given hook in the same <prefix>, and not re-invent the\n> \"sendemail.identity\" special-case).\n>\n> I'd very much like the author to respond to that :) And/or for others to\n> chime in with what they think.\n>\n> 1. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n\nOK, Emily, I guess the ball is in your court now?\n"},{"id":"423521","messageId":"YJBdbi50Hz+ekOtt@google.com","threadId":"55286","inReplyTo":"xmqqblaopbep.fsf@gitster.g","subject":"Re: [PATCH v2 2/4] git-send-email: refactor duplicate $? checks into a function","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-03T20:30:38Z","receivedAt":"2021-05-03T20:30:47Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Apr 08, 2021 at 05:08:30PM -0700, Junio C Hamano wrote:\n> \n> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n> \n> > On Fri, Apr 09 2021, Junio C Hamano wrote:\n> > ...\n> >> What's the status of that topic, if there weren't other topics in\n> >> flight that interfere with it, by the way?  Is it otherwise a good\n> >> enough shape to be given priority and stable enough to get other\n> >> topics rebased on top of it?\n> >\n> > I see I've mentioned [1] in passing to you before, but in summary I have\n> > some major qualms about parts of it, but very much like the overall\n> > direction/goal of having hooks in config.\n> >\n> > Elevator pitch summary of the lengthy [1]: hooks in config: good, but\n> > having a \"git hook\" command introduce some nascent UI for managing a\n> > subset of git-config: somewhere between \"meh\" / \"bad idea\" (see security\n> > concerns in [1]) / \"not needed\". I.e. I demonstrated that we can replace\n> > it with a trivial git-config wrapper, if the series doesn't go out of\n> > its way to make it difficult (i.e. we can/should stick all config for a\n> > given hook in the same <prefix>, and not re-invent the\n> > \"sendemail.identity\" special-case).\n> >\n> > I'd very much like the author to respond to that :) And/or for others to\n> > chime in with what they think.\n> >\n> > 1. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n> \n> OK, Emily, I guess the ball is in your court now?\n\nThe topic is not ready for submission besides interference. I have a\nlist of things to do and was sidetracked with other work (the submodule\nRFC, etc.). This week I am working on getting this series polished and\nready to go.\n\n - Emily\n"},{"id":"425602","messageId":"20210527000856.695702-1-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210311021037.3001235-1-emilyshaffer@google.com","subject":"[PATCH v9 00/37] propose config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:19Z","receivedAt":"2021-05-27T00:09:07Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"After much delay and $DAYJOB, here is v9.\n\n- Addressed nits in reviews on v8\n- sendemail-validate hook becomes non-parallelized; updated to use\n  Ævar's updated system_or_die() function\n- changed strbuf to char* in hooks_list\n  - Attempted to do so in run_command's stdout callback, but this made\n    length protection difficult, so stuck with strbuf there.\n- test_i18ncmp -> test_cmp\n- Stop doing i18n lego in run_hooks()\n- Checked that run_hooks_opt_init() is always separated by a space from\n  variable decl blocks\n- Checked for early returns which may skip run_hooks_opt_clear(); this\n  resulted in minimizing the scope of run_hooks_opt in most places\n- Got rid of native-hooks.txt. It was a nice idea, but not attached to a\n  large and slow series like this one.\n- In traces, log the name of the hook (e.g. \"pre-commit\") instead of the\n  name of the executable (e.g. \"/home/emily/check-for-debug-strings\");\n  the executable name is tracelogged as part of argv anyways, and we\n  want to be able to tell which hook was responsible for invoking the\n  executable in question.\n\nThanks.\n - Emily\n\nEmily Shaffer (37):\n  doc: propose hooks managed by the config\n  hook: introduce git-hook subcommand\n  hook: include hookdir hook in list\n  hook: teach hook.runHookDir\n  hook: implement hookcmd.<name>.skip\n  parse-options: parse into strvec\n  hook: add 'run' subcommand\n  hook: introduce hook_exists()\n  hook: support passing stdin to hooks\n  run-command: allow stdin for run_processes_parallel\n  hook: allow parallel hook execution\n  hook: allow specifying working directory for hooks\n  run-command: add stdin callback for parallelization\n  hook: provide stdin by string_list or callback\n  run-command: allow capturing of collated output\n  hooks: allow callers to capture output\n  commit: use config-based hooks\n  am: convert applypatch hooks to use config\n  merge: use config-based hooks for post-merge hook\n  gc: use hook library for pre-auto-gc hook\n  rebase: teach pre-rebase to use hook.h\n  read-cache: convert post-index-change hook to use config\n  receive-pack: convert push-to-checkout hook to hook.h\n  git-p4: use 'git hook' to run hooks\n  hooks: convert 'post-checkout' hook to hook library\n  hook: convert 'post-rewrite' hook to config\n  transport: convert pre-push hook to use config\n  reference-transaction: look for hooks in config\n  receive-pack: convert 'update' hook to hook.h\n  proc-receive: acquire hook list from hook.h\n  post-update: use hook.h library\n  receive-pack: convert receive hooks to hook.h\n  bugreport: use hook_exists instead of find_hook\n  git-send-email: use 'git hook run' for 'sendemail-validate'\n  run-command: stop thinking about hooks\n  doc: clarify fsmonitor-watchman specification\n  docs: link githooks and git-hook manpages\n\n .gitignore                                    |   1 +\n Documentation/Makefile                        |   1 +\n Documentation/config/hook.txt                 |  27 +\n Documentation/git-hook.txt                    | 162 ++++++\n Documentation/githooks.txt                    |  77 ++-\n Documentation/technical/api-parse-options.txt |   7 +\n .../technical/config-based-hooks.txt          | 369 +++++++++++++\n Makefile                                      |   2 +\n builtin.h                                     |   1 +\n builtin/am.c                                  |  39 +-\n builtin/bugreport.c                           |   4 +-\n builtin/checkout.c                            |  19 +-\n builtin/clone.c                               |   8 +-\n builtin/commit.c                              |  11 +-\n builtin/fetch.c                               |   1 +\n builtin/gc.c                                  |   9 +-\n builtin/hook.c                                | 190 +++++++\n builtin/merge.c                               |  15 +-\n builtin/rebase.c                              |  10 +-\n builtin/receive-pack.c                        | 326 ++++++------\n builtin/submodule--helper.c                   |   2 +-\n builtin/worktree.c                            |  32 +-\n command-list.txt                              |   1 +\n commit.c                                      |  22 +-\n commit.h                                      |   3 +-\n git-p4.py                                     |  67 +--\n git-send-email.perl                           |  26 +-\n git.c                                         |   1 +\n hook.c                                        | 483 ++++++++++++++++++\n hook.h                                        | 139 +++++\n parse-options-cb.c                            |  16 +\n parse-options.h                               |   4 +\n read-cache.c                                  |  13 +-\n refs.c                                        |  43 +-\n reset.c                                       |  17 +-\n run-command.c                                 | 156 +++---\n run-command.h                                 |  55 +-\n sequencer.c                                   |  92 ++--\n submodule.c                                   |   1 +\n t/helper/test-parse-options.c                 |   6 +\n t/helper/test-run-command.c                   |  46 +-\n t/t0040-parse-options.sh                      |  27 +\n t/t0061-run-command.sh                        |  37 ++\n t/t1360-config-based-hooks.sh                 | 329 ++++++++++++\n t/t1416-ref-transaction-hooks.sh              |  12 +-\n t/t5411/test-0015-too-many-hooks-error.sh     |  47 ++\n ...3-pre-commit-and-pre-merge-commit-hooks.sh |  17 +-\n t/t9001-send-email.sh                         |  13 +-\n transport.c                                   |  58 +--\n 49 files changed, 2505 insertions(+), 539 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 Documentation/technical/config-based-hooks.txt\n create mode 100644 builtin/hook.c\n create mode 100644 hook.c\n create mode 100644 hook.h\n create mode 100755 t/t1360-config-based-hooks.sh\n create mode 100644 t/t5411/test-0015-too-many-hooks-error.sh\n\n\n\n 1:  85b99369f1 !  1:  d2b7ee8317 doc: propose hooks managed by the config\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    +\n    + ## Notes ##\n    +    Since v6, checked for inconsistencies with implementation and added lots of\n    +    caveats about whether 'git hook add' and 'git hook edit' will ever materialize.\n    +\n    +    Hopefully this reflects reality now; please review accordingly.\n    +\n    +    Since v6, checked for inconsistencies with implementation and added lots of\n    +    caveats about whether 'git hook add' and 'git hook edit' will ever materialize.\n    +\n    +    Hopefully this reflects reality now; please review accordingly.\n    +\n    +    Since v4, addressed comments from Jonathan Tan about wording. However, I have\n    +    not addressed AEvar's comments or done a full re-review of this document.\n    +    I wanted to get the rest of the series out for initial review first.\n    +\n    +     - Emily\n    +\n    +    Since v4, addressed comments from Jonathan Tan about wording.\n    +\n      ## Documentation/Makefile ##\n     @@ Documentation/Makefile: SP_ARTICLES += $(API_DOCS)\n      TECH_DOCS += MyFirstContribution\n 2:  1d19f1477c <  -:  ---------- hook: scaffolding for git-hook subcommand\n 3:  c125c63880 !  2:  112a809f02 hook: add list command\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    hook: add list command\n    +    hook: introduce git-hook subcommand\n     \n    -    Teach 'git hook list <hookname>', which checks the known configs in\n    +    Add a new subcommand, git-hook, which will be used to ease config-based\n    +    hook management. This command will handle parsing configs to compose a\n    +    list of hooks to run for a given event, as well as adding or modifying\n    +    hook configs in an interactive fashion.\n    +\n    +    Start with 'git hook list <hookname>', which checks the known configs in\n         order to create an ordered list of hooks to run on a given hook event.\n     \n         Multiple commands can be specified for a given hook by providing\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    +\n    + ## Notes ##\n    +    Since v4, mainly changed to RUN_SETUP_GENTLY so that 'git hook list' can\n    +    be executed outside of a repo.\n    +\n    + ## .gitignore ##\n    +@@\n    + /git-grep\n    + /git-hash-object\n    + /git-help\n    ++/git-hook\n    + /git-http-backend\n    + /git-http-fetch\n    + /git-http-push\n    +\n      ## Documentation/config/hook.txt (new) ##\n     @@\n     +hook.<command>.command::\n    @@ Documentation/config/hook.txt (new)\n     +\tas a command. This can be an executable on your device or a oneliner for\n     +\tyour shell. See linkgit:git-hook[1].\n     \n    - ## Documentation/git-hook.txt ##\n    -@@ Documentation/git-hook.txt: git-hook - Manage configured hooks\n    - SYNOPSIS\n    - --------\n    - [verse]\n    --'git hook'\n    + ## Documentation/git-hook.txt (new) ##\n    +@@\n    ++git-hook(1)\n    ++===========\n    ++\n    ++NAME\n    ++----\n    ++git-hook - Manage configured hooks\n    ++\n    ++SYNOPSIS\n    ++--------\n    ++[verse]\n     +'git hook' list <hook-name>\n    - \n    - DESCRIPTION\n    - -----------\n    --A placeholder command. Later, you will be able to list, add, and modify hooks\n    --with this command.\n    ++\n    ++DESCRIPTION\n    ++-----------\n     +You can list configured hooks with this command. Later, you will be able to run,\n     +add, and modify hooks with this command.\n     +\n    @@ Documentation/git-hook.txt: git-hook - Manage configured hooks\n     +CONFIGURATION\n     +-------------\n     +include::config/hook.txt[]\n    - \n    - GIT\n    - ---\n    ++\n    ++GIT\n    ++---\n    ++Part of the linkgit:git[1] suite\n     \n      ## Makefile ##\n     @@ Makefile: LIB_OBJS += hash-lookup.o\n    @@ Makefile: LIB_OBJS += hash-lookup.o\n      LIB_OBJS += ident.o\n      LIB_OBJS += json-writer.o\n      LIB_OBJS += kwset.o\n    +@@ Makefile: BUILTIN_OBJS += builtin/get-tar-commit-id.o\n    + BUILTIN_OBJS += builtin/grep.o\n    + BUILTIN_OBJS += builtin/hash-object.o\n    + BUILTIN_OBJS += builtin/help.o\n    ++BUILTIN_OBJS += builtin/hook.o\n    + BUILTIN_OBJS += builtin/index-pack.o\n    + BUILTIN_OBJS += builtin/init-db.o\n    + BUILTIN_OBJS += builtin/interpret-trailers.o\n     \n    - ## builtin/hook.c ##\n    + ## builtin.h ##\n    +@@ builtin.h: int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);\n    + int cmd_grep(int argc, const char **argv, const char *prefix);\n    + int cmd_hash_object(int argc, const char **argv, const char *prefix);\n    + int cmd_help(int argc, const char **argv, const char *prefix);\n    ++int cmd_hook(int argc, const char **argv, const char *prefix);\n    + int cmd_index_pack(int argc, const char **argv, const char *prefix);\n    + int cmd_init_db(int argc, const char **argv, const char *prefix);\n    + int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);\n    +\n    + ## builtin/hook.c (new) ##\n     @@\n    - #include \"cache.h\"\n    --\n    - #include \"builtin.h\"\n    ++#include \"cache.h\"\n    ++#include \"builtin.h\"\n     +#include \"config.h\"\n     +#include \"hook.h\"\n    - #include \"parse-options.h\"\n    ++#include \"parse-options.h\"\n     +#include \"strbuf.h\"\n    - \n    - static const char * const builtin_hook_usage[] = {\n    --\tN_(\"git hook\"),\n    ++\n    ++static const char * const builtin_hook_usage[] = {\n     +\tN_(\"git hook list <hookname>\"),\n    - \tNULL\n    - };\n    - \n    --int cmd_hook(int argc, const char **argv, const char *prefix)\n    ++\tNULL\n    ++};\n    ++\n     +static int list(int argc, const char **argv, const char *prefix)\n    - {\n    --\tstruct option builtin_hook_options[] = {\n    ++{\n     +\tstruct list_head *head, *pos;\n    -+\tstruct strbuf hookname = STRBUF_INIT;\n    ++\tconst char *hookname = NULL;\n     +\n     +\tstruct option list_options[] = {\n    - \t\tOPT_END(),\n    - \t};\n    - \n    --\targc = parse_options(argc, argv, prefix, builtin_hook_options,\n    ++\t\tOPT_END(),\n    ++\t};\n    ++\n     +\targc = parse_options(argc, argv, prefix, list_options,\n    - \t\t\t     builtin_hook_usage, 0);\n    - \n    ++\t\t\t     builtin_hook_usage, 0);\n    ++\n     +\tif (argc < 1) {\n     +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n     +\t\t\t      builtin_hook_usage, list_options);\n     +\t}\n     +\n    -+\tstrbuf_addstr(&hookname, argv[0]);\n    ++\thookname = argv[0];\n     +\n    -+\thead = hook_list(&hookname);\n    ++\thead = hook_list(hookname);\n     +\n     +\tif (list_empty(head)) {\n     +\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n    -+\t\t       hookname.buf);\n    -+\t\tstrbuf_release(&hookname);\n    ++\t\t       hookname);\n     +\t\treturn 0;\n     +\t}\n     +\n    @@ builtin/hook.c\n     +\t}\n     +\n     +\tclear_hook_list(head);\n    -+\tstrbuf_release(&hookname);\n     +\n    - \treturn 0;\n    - }\n    ++\treturn 0;\n    ++}\n     +\n     +int cmd_hook(int argc, const char **argv, const char *prefix)\n     +{\n    @@ builtin/hook.c\n     +\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n     +}\n     \n    + ## command-list.txt ##\n    +@@ command-list.txt: git-grep                                mainporcelain           info\n    + git-gui                                 mainporcelain\n    + git-hash-object                         plumbingmanipulators\n    + git-help                                ancillaryinterrogators          complete\n    ++git-hook                                mainporcelain\n    + git-http-backend                        synchingrepositories\n    + git-http-fetch                          synchelpers\n    + git-http-push                           synchelpers\n    +\n    + ## git.c ##\n    +@@ git.c: static struct cmd_struct commands[] = {\n    + \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n    + \t{ \"hash-object\", cmd_hash_object },\n    + \t{ \"help\", cmd_help },\n    ++\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n    + \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n    + \t{ \"init\", cmd_init_db },\n    + \t{ \"init-db\", cmd_init_db },\n    +\n      ## hook.c (new) ##\n     @@\n     +#include \"cache.h\"\n    @@ hook.c (new)\n     +\treturn 0;\n     +}\n     +\n    -+struct list_head* hook_list(const struct strbuf* hookname)\n    ++struct list_head* hook_list(const char* hookname)\n     +{\n     +\tstruct strbuf hook_key = STRBUF_INIT;\n     +\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n    @@ hook.c (new)\n     +\tif (!hookname)\n     +\t\treturn NULL;\n     +\n    -+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname->buf);\n    ++\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n     +\n     +\tgit_config(hook_config_lookup, &cb_data);\n     +\n    @@ hook.h (new)\n     + * Provides a linked list of 'struct hook' detailing commands which should run\n     + * in response to the 'hookname' event, in execution order.\n     + */\n    -+struct list_head* hook_list(const struct strbuf *hookname);\n    ++struct list_head* hook_list(const char *hookname);\n     +\n     +/* Free memory associated with a 'struct hook' */\n     +void free_hook(struct hook *ptr);\n     +/* Empties the list at 'head', calling 'free_hook()' on each entry */\n     +void clear_hook_list(struct list_head *head);\n     \n    - ## t/t1360-config-based-hooks.sh ##\n    -@@ t/t1360-config-based-hooks.sh: test_description='config-managed multihooks, including git-hook command'\n    - \n    - . ./test-lib.sh\n    - \n    --test_expect_success 'git hook command does not crash' '\n    --\tgit hook\n    + ## t/t1360-config-based-hooks.sh (new) ##\n    +@@\n    ++#!/bin/bash\n    ++\n    ++test_description='config-managed multihooks, including git-hook command'\n    ++\n    ++. ./test-lib.sh\n    ++\n     +ROOT=\n     +if test_have_prereq MINGW\n     +then\n    @@ t/t1360-config-based-hooks.sh: test_description='config-managed multihooks, incl\n     +\n     +\tgit hook list pre-commit >actual &&\n     +\ttest_cmp expected actual\n    - '\n    - \n    - test_done\n    ++'\n    ++\n    ++test_done\n 4:  0b8cd46ff9 !  3:  3114306368 hook: include hookdir hook in list\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n      \tlist_for_each(pos, head) {\n      \t\tstruct hook *item = list_entry(pos, struct hook, list);\n     -\t\tif (item)\n    +-\t\t\tprintf(\"%s: %s\\n\",\n    +-\t\t\t       config_scope_name(item->origin),\n     +\t\titem = list_entry(pos, struct hook, list);\n     +\t\tif (item) {\n    -+\t\t\t/* Don't translate 'hookdir' - it matches the config */\n    - \t\t\tprintf(\"%s: %s\\n\",\n    --\t\t\t       config_scope_name(item->origin),\n    ++\t\t\t/*\n    ++\t\t\t * TRANSLATORS: \"<config scope>: <path>\". Both fields\n    ++\t\t\t * should be left untranslated; config scope matches the\n    ++\t\t\t * output of 'git config --show-scope'. Marked for\n    ++\t\t\t * translation to provide better RTL support later.\n    ++\t\t\t */\n    ++\t\t\tprintf(_(\"%s: %s\\n\"),\n     +\t\t\t       (item->from_hookdir\n     +\t\t\t\t? \"hookdir\"\n     +\t\t\t\t: config_scope_name(item->origin)),\n    @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *comm\n      \t}\n      \n      \t/* re-set the scope so we show where an override was specified */\n    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n    +@@ hook.c: struct list_head* hook_list(const char* hookname)\n      \n      \tgit_config(hook_config_lookup, &cb_data);\n      \n     +\tif (have_git_dir()) {\n    -+\t\tconst char *legacy_hook_path = find_hook(hookname->buf);\n    ++\t\tconst char *legacy_hook_path = find_hook(hookname);\n     +\n     +\t\t/* Unconditionally add legacy hook, but annotate it. */\n     +\t\tif (legacy_hook_path) {\n 5:  05c503fbe1 !  4:  681013c32a hook: teach hook.runHookDir\n    @@ Commit message\n         list'. Later on, though, we will pay attention to this enum when running\n         the hooks.\n     \n    +\n    + ## Notes ##\n    +    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to\n    +    enforce it - now it matches the design doc much better.\n    +\n    +    Also, thanks Jonathan Tan for pointing out that the commit message made no sense\n    +    and was targeted for a different change. Rewrote the commit message now.\n    +\n    +    Plus, added HOOK_ERROR flag per Junio and Jonathan Nieder.\n    +\n    +    Newly split into its own commit since v4, and taking place much sooner.\n    +\n    +    An unfortunate side effect of adding this support *before* the\n    +    hook.runHookDir support is that the labels on the list are not clear -\n    +    because we aren't yet flagging which hooks are from the hookdir versus\n    +    the config. I suppose we could move the addition of that field to the\n    +    struct hook up to this patch, but it didn't make a lot of sense to me to\n    +    do it just for cosmetic purposes.\n    +\n    +    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to\n    +    enforce it - now it matches the design doc much better.\n    +\n    +    Also, thanks Jonathan Tan for pointing out that the commit message made no sense\n    +    and was targeted for a different change. Rewrote the commit message now.\n    +\n    +    Newly split into its own commit since v4, and taking place much sooner.\n    +\n    +    An unfortunate side effect of adding this support *before* the\n    +    hook.runHookDir support is that the labels on the list are not clear -\n    +    because we aren't yet flagging which hooks are from the hookdir versus\n    +    the config. I suppose we could move the addition of that field to the\n    +    struct hook up to this patch, but it didn't make a lot of sense to me to\n    +    do it just for cosmetic purposes.\n    +\n      ## Documentation/config/hook.txt ##\n     @@ Documentation/config/hook.txt: hookcmd.<name>.command::\n      \tA command to execute during a hook for which <name> has been specified\n    @@ builtin/hook.c: static const char * const builtin_hook_usage[] = {\n      static int list(int argc, const char **argv, const char *prefix)\n      {\n      \tstruct list_head *head, *pos;\n    - \tstruct strbuf hookname = STRBUF_INIT;\n    + \tconst char *hookname = NULL;\n     +\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n      \n      \tstruct option list_options[] = {\n      \t\tOPT_END(),\n     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n    - \t\treturn 0;\n    - \t}\n    - \n    -+\tswitch (should_run_hookdir) {\n    -+\t\tcase HOOKDIR_NO:\n    -+\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will not run)\"));\n    -+\t\t\tbreak;\n    -+\t\tcase HOOKDIR_ERROR:\n    -+\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will error and not run)\"));\n    -+\t\t\tbreak;\n    -+\t\tcase HOOKDIR_INTERACTIVE:\n    -+\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will prompt)\"));\n    -+\t\t\tbreak;\n    -+\t\tcase HOOKDIR_WARN:\n    -+\t\t\tstrbuf_addstr(&hookdir_annotation, _(\" (will warn but run)\"));\n    -+\t\t\tbreak;\n    -+\t\tcase HOOKDIR_YES:\n    -+\t\t/*\n    -+\t\t * The default behavior should agree with\n    -+\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n    -+\t\t * do the default behavior.\n    -+\t\t */\n    -+\t\tcase HOOKDIR_UNKNOWN:\n    -+\t\tdefault:\n    -+\t\t\tbreak;\n    -+\t}\n    -+\n    - \tlist_for_each(pos, head) {\n      \t\tstruct hook *item = list_entry(pos, struct hook, list);\n      \t\titem = list_entry(pos, struct hook, list);\n      \t\tif (item) {\n    - \t\t\t/* Don't translate 'hookdir' - it matches the config */\n    --\t\t\tprintf(\"%s: %s\\n\",\n    -+\t\t\tprintf(\"%s: %s%s\\n\",\n    - \t\t\t       (item->from_hookdir\n    - \t\t\t\t? \"hookdir\"\n    - \t\t\t\t: config_scope_name(item->origin)),\n    +-\t\t\t/*\n    +-\t\t\t * TRANSLATORS: \"<config scope>: <path>\". Both fields\n    +-\t\t\t * should be left untranslated; config scope matches the\n    +-\t\t\t * output of 'git config --show-scope'. Marked for\n    +-\t\t\t * translation to provide better RTL support later.\n    +-\t\t\t */\n    +-\t\t\tprintf(_(\"%s: %s\\n\"),\n    +-\t\t\t       (item->from_hookdir\n    +-\t\t\t\t? \"hookdir\"\n    +-\t\t\t\t: config_scope_name(item->origin)),\n     -\t\t\t       item->command.buf);\n    -+\t\t\t       item->command.buf,\n    -+\t\t\t       (item->from_hookdir\n    -+\t\t\t\t? hookdir_annotation.buf\n    -+\t\t\t\t: \"\"));\n    ++\t\t\tif (item->from_hookdir) {\n    ++\t\t\t\t/*\n    ++\t\t\t\t * TRANSLATORS: do not translate 'hookdir' as\n    ++\t\t\t\t * it matches the config setting.\n    ++\t\t\t\t */\n    ++\t\t\t\tswitch (should_run_hookdir) {\n    ++\t\t\t\tcase HOOKDIR_NO:\n    ++\t\t\t\t\tprintf(_(\"hookdir: %s (will not run)\\n\"),\n    ++\t\t\t\t\t       item->command.buf);\n    ++\t\t\t\t\tbreak;\n    ++\t\t\t\tcase HOOKDIR_ERROR:\n    ++\t\t\t\t\tprintf(_(\"hookdir: %s (will error and not run)\\n\"),\n    ++\t\t\t\t\t       item->command.buf);\n    ++\t\t\t\t\tbreak;\n    ++\t\t\t\tcase HOOKDIR_INTERACTIVE:\n    ++\t\t\t\t\tprintf(_(\"hookdir: %s (will prompt)\\n\"),\n    ++\t\t\t\t\t       item->command.buf);\n    ++\t\t\t\t\tbreak;\n    ++\t\t\t\tcase HOOKDIR_WARN:\n    ++\t\t\t\t\tprintf(_(\"hookdir: %s (will warn but run)\\n\"),\n    ++\t\t\t\t\t       item->command.buf);\n    ++\t\t\t\t\tbreak;\n    ++\t\t\t\tcase HOOKDIR_YES:\n    ++\t\t\t\t/*\n    ++\t\t\t\t * The default behavior should agree with\n    ++\t\t\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n    ++\t\t\t\t * do the default behavior.\n    ++\t\t\t\t */\n    ++\t\t\t\tcase HOOKDIR_UNKNOWN:\n    ++\t\t\t\tdefault:\n    ++\t\t\t\t\tprintf(_(\"hookdir: %s\\n\"),\n    ++\t\t\t\t\t\t item->command.buf);\n    ++\t\t\t\t\tbreak;\n    ++\t\t\t\t}\n    ++\t\t\t} else {\n    ++\t\t\t\t/*\n    ++\t\t\t\t * TRANSLATORS: \"<config scope>: <path>\". Both fields\n    ++\t\t\t\t * should be left untranslated; config scope matches the\n    ++\t\t\t\t * output of 'git config --show-scope'. Marked for\n    ++\t\t\t\t * translation to provide better RTL support later.\n    ++\t\t\t\t */\n    ++\t\t\t\tprintf(_(\"%s: %s\\n\"),\n    ++\t\t\t\t\tconfig_scope_name(item->origin),\n    ++\t\t\t\t\titem->command.buf);\n    ++\t\t\t}\n      \t\t}\n      \t}\n      \n      \tclear_hook_list(head);\n     +\tstrbuf_release(&hookdir_annotation);\n    - \tstrbuf_release(&hookname);\n      \n      \treturn 0;\n    -@@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n    + }\n      \n      int cmd_hook(int argc, const char **argv, const char *prefix)\n      {\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n     +\t\telse if (!strcmp(run_hookdir, \"interactive\"))\n     +\t\t\tshould_run_hookdir = HOOKDIR_INTERACTIVE;\n     +\t\telse\n    ++\t\t\t/*\n    ++\t\t\t * TRANSLATORS: leave \"yes/warn/interactive/no\"\n    ++\t\t\t * untranslated; the strings are compared literally.\n    ++\t\t\t */\n     +\t\t\tdie(_(\"'%s' is not a valid option for --run-hookdir \"\n     +\t\t\t      \"(yes, warn, interactive, no)\"), run_hookdir);\n     +\telse\n    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *\n     +\treturn HOOKDIR_UNKNOWN;\n     +}\n     +\n    - struct list_head* hook_list(const struct strbuf* hookname)\n    + struct list_head* hook_list(const char* hookname)\n      {\n      \tstruct strbuf hook_key = STRBUF_INIT;\n     \n      ## hook.h ##\n     @@ hook.h: struct hook {\n       */\n    - struct list_head* hook_list(const struct strbuf *hookname);\n    + struct list_head* hook_list(const char *hookname);\n      \n     +enum hookdir_opt\n     +{\n    @@ hook.h: struct hook {\n     + * command line arguments.\n     + */\n     +enum hookdir_opt configured_hookdir_opt(void);\n    ++\n    ++/*\n    ++ * Provides the hookdir_opt specified in the config without consulting any\n    ++ * command line arguments.\n    ++ */\n    ++enum hookdir_opt configured_hookdir_opt(void);\n     +\n      /* Free memory associated with a 'struct hook' */\n      void free_hook(struct hook *ptr);\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr\n     +\n     +\tgit hook list pre-commit >actual &&\n     +\t# the hookdir annotation is translated\n    -+\ttest_i18ncmp expected actual\n    ++\ttest_cmp expected actual\n     +'\n     +\n     +test_expect_success 'hook.runHookDir = error is respected by list' '\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr\n     +\n     +\tgit hook list pre-commit >actual &&\n     +\t# the hookdir annotation is translated\n    -+\ttest_i18ncmp expected actual\n    ++\ttest_cmp expected actual\n     +'\n     +\n     +test_expect_success 'hook.runHookDir = warn is respected by list' '\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr\n     +\n     +\tgit hook list pre-commit >actual &&\n     +\t# the hookdir annotation is translated\n    -+\ttest_i18ncmp expected actual\n    ++\ttest_cmp expected actual\n     +'\n     +\n     +\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr\n     +\n     +\tgit hook list pre-commit >actual &&\n     +\t# the hookdir annotation is translated\n    -+\ttest_i18ncmp expected actual\n    ++\ttest_cmp expected actual\n     +'\n     +\n     +test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list shows hooks fr\n     +\n     +\tgit hook list pre-commit >actual &&\n     +\t# the hookdir annotation is translated\n    -+\ttest_i18ncmp expected actual\n    ++\ttest_cmp expected actual\n     +'\n     +\n      test_done\n 6:  e86025853a !  5:  0a4b9f27b3 hook: implement hookcmd.<name>.skip\n    @@ hook.c: static int hook_config_lookup(const char *key, const char *value, void *\n     \n      ## t/t1360-config-based-hooks.sh ##\n     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is respected by list' '\n    - \ttest_i18ncmp expected actual\n    + \ttest_cmp expected actual\n      '\n      \n     +test_expect_success 'git hook list removes skipped hookcmd' '\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is re\n     +\tEOF\n     +\n     +\tgit hook list pre-commit >actual &&\n    -+\ttest_i18ncmp expected actual\n    ++\ttest_cmp expected actual\n     +'\n     +\n     +test_expect_success 'git hook list ignores skip referring to unused hookcmd' '\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is re\n     +\tEOF\n     +\n     +\tgit hook list pre-commit >actual &&\n    -+\ttest_i18ncmp expected actual\n    ++\ttest_cmp expected actual\n     +'\n     +\n     +test_expect_success 'git hook list removes skipped inlined hook' '\n 7:  6e10593d75 !  6:  2ad4f44d08 parse-options: parse into strvec\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    +\n    + ## Notes ##\n    +    Since v7, updated the reference doc to make the intended usage for OPT_STRVEC\n    +    more clear.\n    +\n    +    Since v4, fixed one or two more places where I missed the argv_array->strvec\n    +    rename.\n    +\n      ## Documentation/technical/api-parse-options.txt ##\n     @@ Documentation/technical/api-parse-options.txt: There are some macros to easily define options:\n      \tThe string argument is stored as an element in `string_list`.\n    @@ parse-options.h: int parse_opt_commits(const struct option *, const char *, int)\n      int parse_opt_noop_cb(const struct option *, const char *, int);\n      enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx,\n      \t\t\t\t\t   const struct option *,\n    +\n    + ## t/helper/test-parse-options.c ##\n    +@@\n    + #include \"cache.h\"\n    + #include \"parse-options.h\"\n    + #include \"string-list.h\"\n    ++#include \"strvec.h\"\n    + #include \"trace2.h\"\n    + \n    + static int boolean = 0;\n    +@@ t/helper/test-parse-options.c: static char *string = NULL;\n    + static char *file = NULL;\n    + static int ambiguous;\n    + static struct string_list list = STRING_LIST_INIT_NODUP;\n    ++static struct strvec vector = STRVEC_INIT;\n    + \n    + static struct {\n    + \tint called;\n    +@@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **argv)\n    + \t\tOPT_STRING('o', NULL, &string, \"str\", \"get another string\"),\n    + \t\tOPT_NOOP_NOARG(0, \"obsolete\"),\n    + \t\tOPT_STRING_LIST(0, \"list\", &list, \"str\", \"add str to list\"),\n    ++\t\tOPT_STRVEC(0, \"vector\", &vector, \"str\", \"add str to strvec\"),\n    + \t\tOPT_GROUP(\"Magic arguments\"),\n    + \t\tOPT_ARGUMENT(\"quux\", NULL, \"means --quux\"),\n    + \t\tOPT_NUMBER_CALLBACK(&integer, \"set integer to NUM\",\n    +@@ t/helper/test-parse-options.c: int cmd__parse_options(int argc, const char **argv)\n    + \tfor (i = 0; i < list.nr; i++)\n    + \t\tshow(&expect, &ret, \"list: %s\", list.items[i].string);\n    + \n    ++\tfor (i = 0; i < vector.nr; i++)\n    ++\t\tshow(&expect, &ret, \"vector: %s\", vector.v[i]);\n    ++\n    + \tfor (i = 0; i < argc; i++)\n    + \t\tshow(&expect, &ret, \"arg %02d: %s\", i, argv[i]);\n    + \n    +\n    + ## t/t0040-parse-options.sh ##\n    +@@ t/t0040-parse-options.sh: String options\n    +     --st <st>             get another string (pervert ordering)\n    +     -o <str>              get another string\n    +     --list <str>          add str to list\n    ++    --vector <str>        add str to strvec\n    + \n    + Magic arguments\n    +     --quux                means --quux\n    +@@ t/t0040-parse-options.sh: test_expect_success '--no-list resets list' '\n    + \ttest_cmp expect output\n    + '\n    + \n    ++cat >expect <<\\EOF\n    ++boolean: 0\n    ++integer: 0\n    ++magnitude: 0\n    ++timestamp: 0\n    ++string: (not set)\n    ++abbrev: 7\n    ++verbose: -1\n    ++quiet: 0\n    ++dry run: no\n    ++file: (not set)\n    ++vector: foo\n    ++vector: bar\n    ++vector: baz\n    ++EOF\n    ++test_expect_success '--vector keeps list of strings' '\n    ++\ttest-tool parse-options --vector foo --vector=bar --vector=baz >output &&\n    ++\ttest_cmp expect output\n    ++'\n    ++\n    ++test_expect_success '--no-vector resets list' '\n    ++\ttest-tool parse-options --vector=other --vector=irrelevant --vector=options \\\n    ++\t\t--no-vector --vector=foo --vector=bar --vector=baz >output &&\n    ++\ttest_cmp expect output\n    ++'\n    ++\n    + test_expect_success 'multiple quiet levels' '\n    + \ttest-tool parse-options --expect=\"quiet: 3\" -q -q -q\n    + '\n 8:  0dc9284057 !  7:  27dd8e3edf hook: add 'run' subcommand\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    +\n    + ## Notes ##\n    +    Since v7, added support for \"error\" hook.runHookDir setting.\n    +\n    +    Since v4, updated the docs, and did less local application of single\n    +    quotes. In order for hookdir hooks to run successfully with a space in\n    +    the path, though, they must not be run with 'sh -c'. So we can treat the\n    +    hookdir hooks specially, and warn users via doc about special\n    +    considerations for configured hooks with spaces in their path.\n    +\n      ## Documentation/git-hook.txt ##\n     @@ Documentation/git-hook.txt: SYNOPSIS\n      --------\n    @@ hook.c\n      \n      void free_hook(struct hook *ptr)\n      {\n    +-\tif (ptr) {\n    ++\tif (ptr)\n    + \t\tstrbuf_release(&ptr->command);\n    +-\t\tfree(ptr);\n    +-\t}\n    ++\tfree(ptr);\n    + }\n    + \n    + static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n     @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)\n      \treturn HOOKDIR_UNKNOWN;\n      }\n    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)\n     +\n     +\tswitch (cfg)\n     +\t{\n    -+\t\tcase HOOKDIR_ERROR:\n    -+\t\t\tfprintf(stderr, _(\"Skipping legacy hook at '%s'\\n\"),\n    -+\t\t\t\tpath);\n    -+\t\t\t/* FALLTHROUGH */\n    -+\t\tcase HOOKDIR_NO:\n    -+\t\t\treturn 0;\n    -+\t\tcase HOOKDIR_WARN:\n    -+\t\t\tfprintf(stderr, _(\"Running legacy hook at '%s'\\n\"),\n    -+\t\t\t\tpath);\n    -+\t\t\treturn 1;\n    -+\t\tcase HOOKDIR_INTERACTIVE:\n    -+\t\t\tdo {\n    -+\t\t\t\t/*\n    -+\t\t\t\t * TRANSLATORS: Make sure to include [Y] and [n]\n    -+\t\t\t\t * in your translation. Only English input is\n    -+\t\t\t\t * accepted. Default option is \"yes\".\n    -+\t\t\t\t */\n    -+\t\t\t\tfprintf(stderr, _(\"Run '%s'? [Yn] \"), path);\n    -+\t\t\t\tgit_read_line_interactively(&prompt);\n    -+\t\t\t\tstrbuf_tolower(&prompt);\n    -+\t\t\t\tif (starts_with(prompt.buf, \"n\")) {\n    -+\t\t\t\t\tstrbuf_release(&prompt);\n    -+\t\t\t\t\treturn 0;\n    -+\t\t\t\t} else if (starts_with(prompt.buf, \"y\")) {\n    -+\t\t\t\t\tstrbuf_release(&prompt);\n    -+\t\t\t\t\treturn 1;\n    -+\t\t\t\t}\n    -+\t\t\t\t/* otherwise, we didn't understand the input */\n    -+\t\t\t} while (prompt.len); /* an empty reply means \"Yes\" */\n    -+\t\t\tstrbuf_release(&prompt);\n    -+\t\t\treturn 1;\n    -+\t\t/*\n    -+\t\t * HOOKDIR_UNKNOWN should match the default behavior, but let's\n    -+\t\t * give a heads up to the user.\n    -+\t\t */\n    -+\t\tcase HOOKDIR_UNKNOWN:\n    -+\t\t\tfprintf(stderr,\n    -+\t\t\t\t_(\"Unrecognized value for 'hook.runHookDir'. \"\n    -+\t\t\t\t  \"Is there a typo? \"));\n    -+\t\t\t/* FALLTHROUGH */\n    -+\t\tcase HOOKDIR_YES:\n    -+\t\tdefault:\n    -+\t\t\treturn 1;\n    ++\tcase HOOKDIR_ERROR:\n    ++\t\tfprintf(stderr, _(\"Skipping legacy hook at '%s'\\n\"),\n    ++\t\t\tpath);\n    ++\t\t/* FALLTHROUGH */\n    ++\tcase HOOKDIR_NO:\n    ++\t\treturn 0;\n    ++\tcase HOOKDIR_WARN:\n    ++\t\tfprintf(stderr, _(\"Running legacy hook at '%s'\\n\"),\n    ++\t\t\tpath);\n    ++\t\treturn 1;\n    ++\tcase HOOKDIR_INTERACTIVE:\n    ++\t\tdo {\n    ++\t\t\t/*\n    ++\t\t\t * TRANSLATORS: Make sure to include [Y] and [n]\n    ++\t\t\t * in your translation. Only English input is\n    ++\t\t\t * accepted. Default option is \"yes\".\n    ++\t\t\t */\n    ++\t\t\tfprintf(stderr, _(\"Run '%s'? [Y/n] \"), path);\n    ++\t\t\tgit_read_line_interactively(&prompt);\n    ++\t\t\t/*\n    ++\t\t\t * In case of prompt = '' - that is, user hit enter,\n    ++\t\t\t * saying \"yes I want the default\" - strncasecmp will\n    ++\t\t\t * return 0 regardless. So list the default first.\n    ++\t\t\t *\n    ++\t\t\t * Case insensitively, accept \"y\", \"ye\", or \"yes\" as\n    ++\t\t\t * \"yes\"; accept \"n\" or \"no\" as \"no\".\n    ++\t\t\t */\n    ++\t\t\tif (!strncasecmp(prompt.buf, \"yes\", prompt.len)) {\n    ++\t\t\t\tstrbuf_release(&prompt);\n    ++\t\t\t\treturn 1;\n    ++\t\t\t} else if (!strncasecmp(prompt.buf, \"no\", prompt.len)) {\n    ++\t\t\t\tstrbuf_release(&prompt);\n    ++\t\t\t\treturn 0;\n    ++\t\t\t}\n    ++\t\t\t/* otherwise, we didn't understand the input */\n    ++\t\t} while (prompt.len); /* an empty reply means default (yes) */\n    ++\t\treturn 1;\n    ++\t/*\n    ++\t * HOOKDIR_UNKNOWN should match the default behavior, but let's\n    ++\t * give a heads up to the user.\n    ++\t */\n    ++\tcase HOOKDIR_UNKNOWN:\n    ++\t\tfprintf(stderr,\n    ++\t\t\t_(\"Unrecognized value for 'hook.runHookDir'. \"\n    ++\t\t\t  \"Is there a typo? \"));\n    ++\t\t/* FALLTHROUGH */\n    ++\tcase HOOKDIR_YES:\n    ++\tdefault:\n    ++\t\treturn 1;\n     +\t}\n     +}\n     +\n    - struct list_head* hook_list(const struct strbuf* hookname)\n    + struct list_head* hook_list(const char* hookname)\n      {\n      \tstruct strbuf hook_key = STRBUF_INIT;\n    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n    +@@ hook.c: struct list_head* hook_list(const char* hookname)\n      \tstrbuf_release(&hook_key);\n      \treturn hook_head;\n      }\n    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n     +\tstrvec_clear(&o->args);\n     +}\n     +\n    -+static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n    ++static void prepare_hook_cp(const char *hookname, struct hook *hook,\n    ++\t\t\t    struct run_hooks_opt *options,\n     +\t\t\t    struct child_process *cp)\n     +{\n     +\tif (!hook)\n    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n     +\tcp->no_stdin = 1;\n     +\tcp->env = options->env.v;\n     +\tcp->stdout_to_stderr = 1;\n    -+\tcp->trace2_hook_name = hook->command.buf;\n    ++\tcp->trace2_hook_name = hookname;\n     +\n     +\t/*\n     +\t * Commands from the config could be oneliners, but we know\n    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n     +\n     +int run_hooks(const char *hookname, struct run_hooks_opt *options)\n     +{\n    -+\tstruct strbuf hookname_str = STRBUF_INIT;\n     +\tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n     +\tint rc = 0;\n     +\n     +\tif (!options)\n     +\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n     +\n    -+\tstrbuf_addstr(&hookname_str, hookname);\n    -+\n    -+\tto_run = hook_list(&hookname_str);\n    ++\tto_run = hook_list(hookname);\n     +\n     +\tlist_for_each_safe(pos, tmp, to_run) {\n     +\t\tstruct child_process hook_proc = CHILD_PROCESS_INIT;\n    @@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n     +\t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n     +\t\t\tcontinue;\n     +\n    -+\t\tprepare_hook_cp(hook, options, &hook_proc);\n    ++\t\tprepare_hook_cp(hookname, hook, options, &hook_proc);\n     +\n     +\t\trc |= run_command(&hook_proc);\n     +\t}\n    @@ hook.h: enum hookdir_opt\n     +void run_hooks_opt_init(struct run_hooks_opt *o);\n     +void run_hooks_opt_clear(struct run_hooks_opt *o);\n     +\n    -+/*\n    + /*\n    +- * Provides the hookdir_opt specified in the config without consulting any\n    +- * command line arguments.\n     + * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n     + * passed 'env' and 'args'.\n    -+ */\n    +  */\n    +-enum hookdir_opt configured_hookdir_opt(void);\n     +int run_hooks(const char *hookname, struct run_hooks_opt *options);\n    -+\n    + \n      /* Free memory associated with a 'struct hook' */\n      void free_hook(struct hook *ptr);\n    - /* Empties the list at 'head', calling 'free_hook()' on each entry */\n     \n      ## t/t1360-config-based-hooks.sh ##\n     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = no is respected by list' '\n      \n      \tgit hook list pre-commit >actual &&\n      \t# the hookdir annotation is translated\n    --\ttest_i18ncmp expected actual\n    -+\ttest_i18ncmp expected actual &&\n    +-\ttest_cmp expected actual\n    ++\ttest_cmp expected actual &&\n     +\n     +\tgit hook run pre-commit 2>actual &&\n     +\ttest_must_be_empty actual\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = error is r\n      \n      \tgit hook list pre-commit >actual &&\n      \t# the hookdir annotation is translated\n    -+\ttest_i18ncmp expected actual &&\n    ++\ttest_cmp expected actual &&\n     +\n     +\tcat >expected <<-EOF &&\n     +\tSkipping legacy hook at '\\''$(pwd)/.git/hooks/pre-commit'\\''\n     +\tEOF\n     +\n     +\tgit hook run pre-commit 2>actual &&\n    - \ttest_i18ncmp expected actual\n    + \ttest_cmp expected actual\n      '\n      \n     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is respected by list' '\n      \n      \tgit hook list pre-commit >actual &&\n      \t# the hookdir annotation is translated\n    -+\ttest_i18ncmp expected actual &&\n    ++\ttest_cmp expected actual &&\n     +\n     +\tcat >expected <<-EOF &&\n     +\tRunning legacy hook at '\\''$(pwd)/.git/hooks/pre-commit'\\''\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = warn is re\n     +\tEOF\n     +\n     +\tgit hook run pre-commit 2>actual &&\n    - \ttest_i18ncmp expected actual\n    + \ttest_cmp expected actual\n      '\n      \n     @@ t/t1360-config-based-hooks.sh: test_expect_success 'git hook list removes skipped inlined hook' '\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = interactiv\n      \n      \tgit hook list pre-commit >actual &&\n      \t# the hookdir annotation is translated\n    --\ttest_i18ncmp expected actual\n    -+\ttest_i18ncmp expected actual &&\n    ++\ttest_cmp expected actual &&\n     +\n     +\ttest_write_lines n | git hook run pre-commit 2>actual &&\n     +\t! grep \"Legacy Hook\" actual &&\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = interactiv\n     +\ttest_cmp expected actual\n     +'\n     +\n    ++test_expect_success 'git hook run can pass args and env vars' '\n    ++\twrite_script sample-hook.sh <<-\\EOF &&\n    ++\techo $1\n    ++\techo $2\n    ++\techo $TEST_ENV_1\n    ++\techo $TEST_ENV_2\n    ++\tEOF\n    ++\n    ++\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n    ++\n    ++\tcat >expected <<-EOF &&\n    ++\targ1\n    ++\targ2\n    ++\tenv1\n    ++\tenv2\n    ++\tEOF\n    ++\n    ++\tgit hook run --arg arg1 \\\n    ++\t\t--env TEST_ENV_1=env1 \\\n    ++\t\t-a arg2 \\\n    ++\t\t-e TEST_ENV_2=env2 \\\n    ++\t\tpre-commit 2>actual &&\n    ++\n    + \ttest_cmp expected actual\n    + '\n    + \n     +test_expect_success 'hookdir hook included in git hook run' '\n     +\tsetup_hookdir &&\n     +\n    @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir = interactiv\n     +\tsetup_hooks &&\n     +\n     +\tnongit test_must_fail git hook run pre-commit\n    - '\n    - \n    ++'\n    ++\n      test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n    + \tsetup_hookdir &&\n    + \n 9:  92c67ed9da !  8:  46975c11c8 hook: introduce hook_exists()\n    @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)\n      \tstrvec_clear(&o->env);\n     \n      ## hook.h ##\n    -@@ hook.h: struct list_head* hook_list(const struct strbuf *hookname);\n    +@@ hook.h: struct list_head* hook_list(const char *hookname);\n      \n      enum hookdir_opt\n      {\n10:  9b3bb0b655 !  9:  e11f9e28a3 hook: support passing stdin to hooks\n    @@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)\n      \to->run_hookdir = configured_hookdir_opt();\n      }\n      \n    -@@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n    +@@ hook.c: static void prepare_hook_cp(const char *hookname, struct hook *hook,\n      \tif (!hook)\n      \t\treturn;\n      \n    @@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *opt\n     +\n      \tcp->env = options->env.v;\n      \tcp->stdout_to_stderr = 1;\n    - \tcp->trace2_hook_name = hook->command.buf;\n    + \tcp->trace2_hook_name = hookname;\n     \n      ## hook.h ##\n     @@ hook.h: struct run_hooks_opt\n    @@ hook.h: int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdi\n     \n      ## t/t1360-config-based-hooks.sh ##\n     @@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n    - \ttest_i18ncmp expected actual\n    + \ttest_cmp expected actual\n      '\n      \n     +test_expect_success 'stdin to multiple hooks' '\n11:  9933985e78 = 10:  3ceb4156fd run-command: allow stdin for run_processes_parallel\n12:  43caafe656 ! 11:  93a47f5242 hook: allow parallel hook execution\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    +\n    + ## Notes ##\n    +    Per AEvar's request - parallel hook execution on day zero.\n    +\n    +    In most ways run_processes_parallel() worked great for me - but it didn't\n    +    have great support for hooks where we pipe to and from. I had to add this\n    +    support later in the series.\n    +\n    +    Since I modified an existing and in-use library I'd appreciate a keen look on\n    +    these patches.\n    +\n    +     - Emily\n    +\n      ## Documentation/config/hook.txt ##\n     @@ Documentation/config/hook.txt: hook.runHookDir::\n      \tControls how hooks contained in your hookdir are executed. Can be any of\n    @@ hook.c: enum hookdir_opt configured_hookdir_opt(void)\n      static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n      {\n      \tstruct strbuf prompt = STRBUF_INIT;\n    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n    +@@ hook.c: struct list_head* hook_list(const char* hookname)\n      \treturn hook_head;\n      }\n      \n    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)\n      \tstrvec_clear(&o->args);\n      }\n      \n    --static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n    +-static void prepare_hook_cp(const char *hookname, struct hook *hook,\n    +-\t\t\t    struct run_hooks_opt *options,\n     -\t\t\t    struct child_process *cp)\n     +static int pick_next_hook(struct child_process *cp,\n     +\t\t\t  struct strbuf *out,\n    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)\n     -\tcp->env = options->env.v;\n     +\tcp->env = hook_cb->options->env.v;\n      \tcp->stdout_to_stderr = 1;\n    - \tcp->trace2_hook_name = hook->command.buf;\n    +-\tcp->trace2_hook_name = hookname;\n    ++\tcp->trace2_hook_name = hook_cb->hookname;\n      \n    -@@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *options,\n    + \t/*\n    + \t * Commands from the config could be oneliners, but we know\n    +@@ hook.c: static void prepare_hook_cp(const char *hookname, struct hook *hook,\n      \t * add passed-in argv, without expanding - let the user get back\n      \t * exactly what they put in\n      \t */\n    @@ hook.c: static void prepare_hook_cp(struct hook *hook, struct run_hooks_opt *opt\n      \n      int run_hooks(const char *hookname, struct run_hooks_opt *options)\n      {\n    - \tstruct strbuf hookname_str = STRBUF_INIT;\n      \tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n     -\tint rc = 0;\n    -+\tstruct hook_cb_data cb_data = { 0, NULL, NULL, options };\n    ++\tstruct hook_cb_data cb_data = { 0, hookname, NULL, NULL, options };\n      \n      \tif (!options)\n      \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n     @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n    - \tto_run = hook_list(&hookname_str);\n    + \tto_run = hook_list(hookname);\n      \n      \tlist_for_each_safe(pos, tmp, to_run) {\n     -\t\tstruct child_process hook_proc = CHILD_PROCESS_INIT;\n    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n     +\tif (list_empty(to_run))\n     +\t\treturn 0;\n      \n    --\t\tprepare_hook_cp(hook, options, &hook_proc);\n    +-\t\tprepare_hook_cp(hookname, hook, options, &hook_proc);\n     +\tcb_data.head = to_run;\n     +\tcb_data.run_me = list_entry(to_run->next, struct hook, list);\n      \n    @@ hook.h: struct run_hooks_opt\n     + */\n     +struct hook_cb_data {\n     +\tint rc;\n    ++\tconst char *hookname;\n     +\tstruct list_head *head;\n     +\tstruct hook *run_me;\n     +\tstruct run_hooks_opt *options;\n13:  2e189a7566 ! 12:  7f8c886d3f hook: allow specifying working directory for hooks\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    +\n    + ## Notes ##\n    +    Needed later for \"post-checkout\" conversion.\n    +\n      ## hook.c ##\n     @@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n      \to->path_to_stdin = NULL;\n    @@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n     @@ hook.c: static int pick_next_hook(struct child_process *cp,\n      \tcp->env = hook_cb->options->env.v;\n      \tcp->stdout_to_stderr = 1;\n    - \tcp->trace2_hook_name = hook->command.buf;\n    + \tcp->trace2_hook_name = hook_cb->hookname;\n     +\tcp->dir = hook_cb->options->dir;\n      \n      \t/*\n14:  07899ad346 = 13:  8930baa9db run-command: add stdin callback for parallelization\n15:  d3f18e433f ! 14:  d0f362591a hook: provide stdin by string_list or callback\n    @@ Commit message\n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## hook.c ##\n    -@@ hook.c: void free_hook(struct hook *ptr)\n    +@@\n    + \n    + void free_hook(struct hook *ptr)\n      {\n    - \tif (ptr) {\n    +-\tif (ptr)\n    ++\tif (ptr) {\n      \t\tstrbuf_release(&ptr->command);\n     +\t\tfree(ptr->feed_pipe_cb_data);\n    - \t\tfree(ptr);\n    - \t}\n    ++\t}\n    + \tfree(ptr);\n      }\n    + \n     @@ hook.c: static void append_or_move_hook(struct list_head *head, const char *command)\n      \t\tstrbuf_init(&to_add->command, 0);\n      \t\tstrbuf_addstr(&to_add->command, command);\n    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n     +\tif (options->path_to_stdin && options->feed_pipe)\n     +\t\tBUG(\"choose only one method to populate stdin\");\n     +\n    - \tstrbuf_addstr(&hookname_str, hookname);\n    + \tto_run = hook_list(hookname);\n      \n    - \tto_run = hook_list(&hookname_str);\n    + \tlist_for_each_safe(pos, tmp, to_run) {\n     @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n      \trun_processes_parallel_tr2(options->jobs,\n      \t\t\t\t   pick_next_hook,\n16:  417c3f054e ! 15:  83bbb405a5 run-command: allow capturing of collated output\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    +\n    + ## Notes ##\n    +    Originally when writing this patch I attempted to use a pipe in memory -\n    +    but managing its lifetime was actually pretty tricky, and I found I could\n    +    achieve the same thing with less code by doing it this way. Critique welcome,\n    +    including \"no, you really need to do it with a pipe\".\n    +\n      ## builtin/fetch.c ##\n     @@ builtin/fetch.c: static int fetch_multiple(struct string_list *list, int max_children)\n      \t\tresult = run_processes_parallel_tr2(max_children,\n17:  c0f3471bd1 ! 16:  73ed5de54c hooks: allow callers to capture output\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    +\n    + ## Notes ##\n    +    You can see this in practice in the conversions for some of the push hooks,\n    +    like 'receive-pack'.\n    +\n      ## hook.c ##\n     @@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n      \to->dir = NULL;\n18:  13446e4273 = 17:  900c4d8963 commit: use config-based hooks\n19:  9380c43180 ! 18:  a562120e22 am: convert applypatch hooks to use config\n    @@ builtin/am.c: static void am_destroy(const struct am_state *state)\n      {\n      \tint ret;\n     +\tstruct run_hooks_opt opt;\n    ++\n     +\trun_hooks_opt_init_sync(&opt);\n      \n      \tassert(state->msg);\n    @@ builtin/am.c: static void do_commit(const struct am_state *state)\n      \tconst char *reflog_msg, *author, *committer = NULL;\n      \tstruct strbuf sb = STRBUF_INIT;\n     +\tstruct run_hooks_opt hook_opt;\n    ++\n     +\trun_hooks_opt_init_async(&hook_opt);\n      \n     -\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n    -+\tif (run_hooks(\"pre-applypatch\", &hook_opt))\n    ++\tif (run_hooks(\"pre-applypatch\", &hook_opt)) {\n    ++\t\trun_hooks_opt_clear(&hook_opt);\n      \t\texit(1);\n    ++\t}\n    ++\n    ++\trun_hooks_opt_clear(&hook_opt);\n      \n      \tif (write_cache_as_tree(&tree, 0, NULL))\n    + \t\tdie(_(\"git write-tree failed to write a tree\"));\n     @@ builtin/am.c: static void do_commit(const struct am_state *state)\n      \t\tfclose(fp);\n      \t}\n      \n     -\trun_hook_le(NULL, \"post-applypatch\", NULL);\n    ++\trun_hooks_opt_init_async(&hook_opt);\n     +\trun_hooks(\"post-applypatch\", &hook_opt);\n      \n     +\trun_hooks_opt_clear(&hook_opt);\n20:  316a605606 ! 19:  e841ed4384 merge: use config-based hooks for post-merge hook\n    @@ builtin/merge.c: static void finish(struct commit *head_commit,\n      \tstruct strbuf reflog_message = STRBUF_INIT;\n     +\tstruct run_hooks_opt opt;\n      \tconst struct object_id *head = &head_commit->object.oid;\n    -+\trun_hooks_opt_init_async(&opt);\n      \n      \tif (!msg)\n    - \t\tstrbuf_addstr(&reflog_message, getenv(\"GIT_REFLOG_ACTION\"));\n     @@ builtin/merge.c: static void finish(struct commit *head_commit,\n      \t}\n      \n      \t/* Run a post-merge hook */\n     -\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n    ++\trun_hooks_opt_init_async(&opt);\n     +\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n     +\trun_hooks(\"post-merge\", &opt);\n     +\trun_hooks_opt_clear(&opt);\n21:  a5132f14b3 ! 20:  7e99398f7d gc: use hook library for pre-auto-gc hook\n    @@ builtin/gc.c: static void add_repack_incremental_option(void)\n      static int need_to_gc(void)\n      {\n     +\tstruct run_hooks_opt hook_opt;\n    -+\trun_hooks_opt_init_async(&hook_opt);\n    ++\n      \t/*\n      \t * Setting gc.auto to 0 or negative can disable the\n      \t * automatic gc.\n    @@ builtin/gc.c: static int need_to_gc(void)\n      \t\treturn 0;\n      \n     -\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n    -+\tif (run_hooks(\"pre-auto-gc\", &hook_opt))\n    ++\trun_hooks_opt_init_async(&hook_opt);\n    ++\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n    ++\t\trun_hooks_opt_clear(&hook_opt);\n      \t\treturn 0;\n    ++\t}\n    ++\trun_hooks_opt_clear(&hook_opt);\n      \treturn 1;\n      }\n    + \n22:  01f1331cc9 ! 21:  5423217ef2 rebase: teach pre-rebase to use hook.h\n    @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix\n      \tstruct option builtin_rebase_options[] = {\n      \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n      \t\t\t   N_(\"revision\"),\n    -@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)\n    - \t};\n    - \tint i;\n    - \n    -+\trun_hooks_opt_init_async(&hook_opt);\n    -+\n    - \tif (argc == 2 && !strcmp(argv[1], \"-h\"))\n    - \t\tusage_with_options(builtin_rebase_usage,\n    - \t\t\t\t   builtin_rebase_options);\n     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)\n      \t}\n      \n      \t/* If a hook exists, give it a chance to interrupt*/\n    ++\trun_hooks_opt_init_async(&hook_opt);\n     +\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n      \tif (!ok_to_skip_pre_rebase &&\n     -\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n     -\t\t\targc ? argv[0] : NULL, NULL))\n    -+\t    run_hooks(\"pre-rebase\", &hook_opt))\n    ++\t    run_hooks(\"pre-rebase\", &hook_opt)) {\n    ++\t\trun_hooks_opt_clear(&hook_opt);\n      \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n    ++\t}\n    ++\trun_hooks_opt_clear(&hook_opt);\n      \n      \tif (options.flags & REBASE_DIFFSTAT) {\n    -@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)\n    - \tret = !!run_specific_rebase(&options, action);\n    - \n    - cleanup:\n    -+\trun_hooks_opt_clear(&hook_opt);\n    - \tstrbuf_release(&buf);\n    - \tstrbuf_release(&revisions);\n    - \tfree(options.head_name);\n    + \t\tstruct diff_options opts;\n23:  85a7721adc ! 22:  1c0c5ad129 read-cache: convert post-index-change hook to use config\n    @@ Documentation/githooks.txt: and \"0\" meaning they were not.\n     \n      ## read-cache.c ##\n     @@\n    - #include \"fsmonitor.h\"\n      #include \"thread-utils.h\"\n      #include \"progress.h\"\n    + #include \"sparse-index.h\"\n     +#include \"hook.h\"\n    ++>>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)\n      \n      /* Mask for the name length in ce_flags in the on-disk index */\n      \n     @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n    - \t\t\t\t unsigned flags)\n      {\n      \tint ret;\n    + \tint was_full = !istate->sparse_index;\n     +\tstruct run_hooks_opt hook_opt;\n    -+\trun_hooks_opt_init_async(&hook_opt);\n      \n    - \t/*\n    - \t * TODO trace2: replace \"the_repository\" with the actual repo instance\n    + \tret = convert_to_sparse(istate);\n    + \n     @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n      \telse\n      \t\tret = close_lock_file_gently(lock);\n    @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struc\n     -\trun_hook_le(NULL, \"post-index-change\",\n     -\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n     -\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n    ++\trun_hooks_opt_init_async(&hook_opt);\n     +\tstrvec_pushl(&hook_opt.args,\n     +\t\t     istate->updated_workdir ? \"1\" : \"0\",\n     +\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n24:  21ec3e1a9d ! 23:  1193e856e6 receive-pack: convert push-to-checkout hook to hook.h\n    @@ builtin/receive-pack.c: static const char *push_to_checkout(unsigned char *hash,\n      \t\t\t\t    const char *work_tree)\n      {\n     +\tstruct run_hooks_opt opt;\n    ++\n     +\trun_hooks_opt_init_sync(&opt);\n     +\n      \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n25:  e0405e96ad ! 24:  1817b6851b git-p4: use 'git hook' to run hooks\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    +\n    + ## Notes ##\n    +    Maybe there is a better way to do this - I had a hard time getting this to run\n    +    locally, and Python is not my forte, so if anybody has a better approach I'd\n    +    love to just take that patch instead :)\n    +\n    +    Since v6, removed the developer debug print statements.... :X\n    +\n    +    Maybe there is a better way to do this - I had a hard time getting this to run\n    +    locally, and Python is not my forte, so if anybody has a better approach I'd\n    +    love to just take that patch instead :)\n    +\n      ## git-p4.py ##\n     @@ git-p4.py: def decode_path(path):\n      \n26:  c52578e078 ! 25:  b3a354e4a8 hooks: convert 'post-checkout' hook to hook library\n    @@ builtin/checkout.c: struct branch_info {\n      \t\t\t      int changed)\n      {\n     -\treturn run_hook_le(NULL, \"post-checkout\",\n    --\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : &null_oid),\n    --\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : &null_oid),\n    +-\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n    +-\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n     -\t\t\t   changed ? \"1\" : \"0\", NULL);\n     +\tstruct run_hooks_opt opt;\n     +\tint rc;\n    @@ builtin/checkout.c: struct branch_info {\n      \t   a commit exists. */\n     -\n     +\tstrvec_pushl(&opt.args,\n    -+\t\t     oid_to_hex(old_commit ? &old_commit->object.oid : &null_oid),\n    -+\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : &null_oid),\n    ++\t\t     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n    ++\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n     +\t\t     changed ? \"1\" : \"0\",\n     +\t\t     NULL);\n     +\trc = run_hooks(\"post-checkout\", &opt);\n    @@ builtin/clone.c: static int checkout(int submodule_progress)\n      \tstruct tree_desc t;\n      \tint err = 0;\n     +\tstruct run_hooks_opt hook_opt;\n    -+\trun_hooks_opt_init_sync(&hook_opt);\n      \n      \tif (option_no_checkout)\n      \t\treturn 0;\n    @@ builtin/clone.c: static int checkout(int submodule_progress)\n      \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n      \t\tdie(_(\"unable to write new index file\"));\n      \n    --\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(&null_oid),\n    +-\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(null_oid()),\n     -\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n    -+\tstrvec_pushl(&hook_opt.args, oid_to_hex(&null_oid), oid_to_hex(&oid), \"1\", NULL);\n    ++\trun_hooks_opt_init_sync(&hook_opt);\n    ++\tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n     +\terr |= run_hooks(\"post-checkout\", &hook_opt);\n     +\trun_hooks_opt_clear(&hook_opt);\n      \n    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam\n     -\t\t\tcp.argv = NULL;\n     -\t\t\tcp.trace2_hook_name = \"post-checkout\";\n     -\t\t\tstrvec_pushl(&cp.args, absolute_path(hook),\n    --\t\t\t\t     oid_to_hex(&null_oid),\n    +-\t\t\t\t     oid_to_hex(null_oid()),\n     -\t\t\t\t     oid_to_hex(&commit->object.oid),\n     -\t\t\t\t     \"1\", NULL);\n     -\t\t\tret = run_command(&cp);\n     -\t\t}\n     +\t\tstruct run_hooks_opt opt;\n    ++\n     +\t\trun_hooks_opt_init_sync(&opt);\n     +\n     +\t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n     +\t\tstrvec_pushl(&opt.args,\n    -+\t\t\t     oid_to_hex(&null_oid),\n    ++\t\t\t     oid_to_hex(null_oid()),\n     +\t\t\t     oid_to_hex(&commit->object.oid),\n     +\t\t\t     \"1\",\n     +\t\t\t     NULL);\n    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam\n      \n      \tstrvec_clear(&child_env);\n     \n    + ## read-cache.c ##\n    +@@\n    + #include \"progress.h\"\n    + #include \"sparse-index.h\"\n    + #include \"hook.h\"\n    +->>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)\n    + \n    + /* Mask for the name length in ce_flags in the on-disk index */\n    + \n    +\n      ## reset.c ##\n     @@\n      #include \"tree-walk.h\"\n    @@ reset.c: int reset_head(struct repository *r, struct object_id *oid, const char\n      \t}\n     -\tif (run_hook)\n     -\t\trun_hook_le(NULL, \"post-checkout\",\n    --\t\t\t    oid_to_hex(orig ? orig : &null_oid),\n    +-\t\t\t    oid_to_hex(orig ? orig : null_oid()),\n     -\t\t\t    oid_to_hex(oid), \"1\", NULL);\n     +\tif (run_hook) {\n     +\t\tstruct run_hooks_opt opt;\n    ++\n     +\t\trun_hooks_opt_init_sync(&opt);\n     +\t\tstrvec_pushl(&opt.args,\n    -+\t\t\t     oid_to_hex(orig ? orig : &null_oid),\n    ++\t\t\t     oid_to_hex(orig ? orig : null_oid()),\n     +\t\t\t     oid_to_hex(oid),\n     +\t\t\t     \"1\",\n     +\t\t\t     NULL);\n27:  316cb6f584 ! 26:  692352f9aa hook: convert 'post-rewrite' hook to config\n    @@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)\n     -\tconst char *hook = find_hook(\"post-rewrite\");\n     +\tstruct run_hooks_opt opt;\n      \tint ret;\n    -+\trun_hooks_opt_init_async(&opt);\n      \n     -\tif (!hook)\n     -\t\treturn 0;\n    --\n    ++\trun_hooks_opt_init_async(&opt);\n    + \n     -\tstrvec_push(&cp.args, hook);\n     -\tstrvec_push(&cp.args, \"rebase\");\n    --\n    --\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n    --\tcp.stdout_to_stderr = 1;\n    --\tcp.trace2_hook_name = \"post-rewrite\";\n     +\tstrvec_push(&opt.args, \"rebase\");\n     +\topt.path_to_stdin = am_path(state, \"rewritten\");\n      \n    --\tret = run_command(&cp);\n    +-\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n    +-\tcp.stdout_to_stderr = 1;\n    +-\tcp.trace2_hook_name = \"post-rewrite\";\n     +\tret = run_hooks(\"post-rewrite\", &opt);\n      \n    +-\tret = run_command(&cp);\n    +-\n     -\tclose(cp.in);\n     +\trun_hooks_opt_clear(&opt);\n      \treturn ret;\n    @@ sequencer.c: int update_head_with_reflog(const struct commit *old_head,\n     +\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n      \tint code;\n     -\tstruct strbuf sb = STRBUF_INIT;\n    -+\trun_hooks_opt_init_async(&opt);\n      \n     -\targv[0] = find_hook(\"post-rewrite\");\n     -\tif (!argv[0])\n     -\t\treturn 0;\n    -+\tstrvec_push(&opt.args, \"amend\");\n    ++\trun_hooks_opt_init_async(&opt);\n      \n     -\targv[1] = \"amend\";\n     -\targv[2] = NULL;\n    @@ sequencer.c: int update_head_with_reflog(const struct commit *old_head,\n     -\tstrbuf_release(&sb);\n     -\tsigchain_pop(SIGPIPE);\n     -\treturn finish_command(&proc);\n    ++\tstrvec_push(&opt.args, \"amend\");\n    ++\n     +\tstrbuf_addf(&tmp,\n     +\t\t    \"%s %s\",\n     +\t\t    oid_to_hex(oldoid),\n    @@ sequencer.c: static int pick_commits(struct repository *r,\n     -\t\t\tstrvec_push(&child.args, \"--for-rewrite=rebase\");\n     +\t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n     +\t\t\tstruct run_hooks_opt hook_opt;\n    ++\n     +\t\t\trun_hooks_opt_init_async(&hook_opt);\n     +\n     +\t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n28:  0ab776068d = 27:  964011dfdd transport: convert pre-push hook to use config\n29:  601dada804 ! 28:  c04822add9 reference-transaction: look for hooks in config\n    @@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'interleaving hook calls s\n      \tEOF\n      \n      \tgit push ./target-repo.git PRE POST &&\n    +\n    + ## transport.c ##\n    +@@ transport.c: static int run_pre_push_hook(struct transport *transport,\n    + \tstruct strbuf tmp = STRBUF_INIT;\n    + \tstruct ref *r;\n    + \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n    ++\n    + \trun_hooks_opt_init_async(&opt);\n    + \n    + \tstrvec_push(&opt.args, transport->remote->name);\n30:  d60f2b146e ! 29:  ddc6f56bec receive-pack: convert 'update' hook to hook.h\n    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n      \treturn status;\n      }\n      \n    --static int run_update_hook(struct command *cmd)\n     +static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n    - {\n    --\tconst char *argv[5];\n    --\tstruct child_process proc = CHILD_PROCESS_INIT;\n    --\tint code;\n    ++{\n     +\tint keepalive_active = 0;\n    - \n    --\targv[0] = find_hook(\"update\");\n    --\tif (!argv[0])\n    --\t\treturn 0;\n    ++\n     +\tif (keepalive_in_sec <= 0)\n     +\t\tuse_keepalive = KEEPALIVE_NEVER;\n     +\tif (use_keepalive == KEEPALIVE_ALWAYS)\n     +\t\tkeepalive_active = 1;\n    - \n    --\targv[1] = cmd->ref_name;\n    --\targv[2] = oid_to_hex(&cmd->old_oid);\n    --\targv[3] = oid_to_hex(&cmd->new_oid);\n    --\targv[4] = NULL;\n    ++\n     +\t/* send a keepalive if there is no data to write */\n     +\tif (keepalive_active && !output->len) {\n     +\t\tstatic const char buf[] = \"0005\\1\";\n     +\t\twrite_or_die(1, buf, sizeof(buf) - 1);\n     +\t\treturn;\n     +\t}\n    - \n    --\tproc.no_stdin = 1;\n    --\tproc.stdout_to_stderr = 1;\n    --\tproc.err = use_sideband ? -1 : 0;\n    --\tproc.argv = argv;\n    --\tproc.trace2_hook_name = \"update\";\n    ++\n     +\tif (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {\n     +\t\tconst char *first_null = memchr(output->buf, '\\0', output->len);\n     +\t\tif (first_null) {\n    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n     +\tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n     +}\n     +\n    -+static int run_update_hook(struct command *cmd)\n    -+{\n    + static int run_update_hook(struct command *cmd)\n    + {\n    +-\tconst char *argv[5];\n    +-\tstruct child_process proc = CHILD_PROCESS_INIT;\n     +\tstruct run_hooks_opt opt;\n    -+\tint code;\n    + \tint code;\n    + \n    +-\targv[0] = find_hook(\"update\");\n    +-\tif (!argv[0])\n    +-\t\treturn 0;\n     +\trun_hooks_opt_init_async(&opt);\n    -+\n    + \n    +-\targv[1] = cmd->ref_name;\n    +-\targv[2] = oid_to_hex(&cmd->old_oid);\n    +-\targv[3] = oid_to_hex(&cmd->new_oid);\n    +-\targv[4] = NULL;\n     +\tstrvec_pushl(&opt.args,\n     +\t\t     cmd->ref_name,\n     +\t\t     oid_to_hex(&cmd->old_oid),\n     +\t\t     oid_to_hex(&cmd->new_oid),\n     +\t\t     NULL);\n      \n    +-\tproc.no_stdin = 1;\n    +-\tproc.stdout_to_stderr = 1;\n    +-\tproc.err = use_sideband ? -1 : 0;\n    +-\tproc.argv = argv;\n    +-\tproc.trace2_hook_name = \"update\";\n    +-\n     -\tcode = start_command(&proc);\n     -\tif (code)\n     -\t\treturn code;\n31:  1e6898670b ! 30:  e1e810869f proc-receive: acquire hook list from hook.h\n    @@ builtin/receive-pack.c: static int run_proc_receive_hook(struct command *command\n      \n     -\targv[0] = find_hook(\"proc-receive\");\n     -\tif (!argv[0]) {\n    -+\tstruct strbuf hookname = STRBUF_INIT;\n     +\tstruct hook *proc_receive = NULL;\n     +\tstruct list_head *pos, *hooks;\n     +\n    -+\tstrbuf_addstr(&hookname, \"proc-receive\");\n    -+\thooks = hook_list(&hookname);\n    ++\thooks = hook_list(\"proc-receive\");\n     +\n     +\tlist_for_each(pos, hooks) {\n     +\t\tif (proc_receive) {\n32:  012e3a7a79 ! 31:  b8be5a2288 post-update: use hook.h library\n    @@ builtin/receive-pack.c: static const char *update(struct command *cmd, struct sh\n      \tstruct command *cmd;\n     -\tstruct child_process proc = CHILD_PROCESS_INIT;\n     -\tconst char *hook;\n    --\n    ++\tstruct run_hooks_opt opt;\n    + \n     -\thook = find_hook(\"post-update\");\n     -\tif (!hook)\n     -\t\treturn;\n    -+\tstruct run_hooks_opt opt;\n     +\trun_hooks_opt_init_async(&opt);\n      \n      \tfor (cmd = commands; cmd; cmd = cmd->next) {\n33:  2740bcda6d = 32:  1cc1384eae receive-pack: convert receive hooks to hook.h\n34:  f201f3af5f = 33:  1bb9a3810c bugreport: use hook_exists instead of find_hook\n35:  0956a94cc7 ! 34:  3db7bf3b0d git-send-email: use 'git hook run' for 'sendemail-validate'\n    @@ git-send-email.perl: sub unique_email_list {\n      \tmy ($fn, $xfer_encoding) = @_;\n      \n     -\tif ($repo) {\n    --\t\tmy $validate_hook = catfile(catdir($repo->repo_path(), 'hooks'),\n    +-\t\tmy $validate_hook = catfile($repo->hooks_path(),\n     -\t\t\t\t\t    'sendemail-validate');\n     -\t\tmy $hook_error;\n     -\t\tif (-x $validate_hook) {\n    @@ git-send-email.perl: sub unique_email_list {\n     -\t\t\tchdir($repo->wc_path() or $repo->repo_path())\n     -\t\t\t\tor die(\"chdir: $!\");\n     -\t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n    --\t\t\t$hook_error = \"rejected by sendemail-validate hook\"\n    --\t\t\t\tif system($validate_hook, $target);\n    +-\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n     -\t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n     -\t\t}\n    --\t\treturn $hook_error if $hook_error;\n    +-\t\tif ($hook_error) {\n    +-\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n    +-\t\t\t\t       \"%s\\n\" .\n    +-\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n    +-\t\t}\n     -\t}\n     +\tmy $target = abs_path($fn);\n    -+\treturn \"rejected by sendemail-validate hook\"\n    -+\t\tif system((\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-a\",\n    -+\t\t\t\t$target));\n    ++\n    ++\tsystem_or_die([\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-j1\", \"-a\", $target],\n    ++\t\tsprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n    ++\t\t\t   \"warning: no patches were sent\\n\"),\n    ++\t\t        $fn));\n      \n      \t# Any long lines will be automatically fixed if we use a suitable transfer\n      \t# encoding.\n     \n      ## t/t9001-send-email.sh ##\n    +@@ t/t9001-send-email.sh: test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n    + \ttest_path_is_file my-hooks.ran &&\n    + \tcat >expect <<-EOF &&\n    + \tfatal: longline.patch: rejected by sendemail-validate hook\n    +-\tfatal: command '\"'\"'$PWD/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n    + \twarning: no patches were sent\n    + \tEOF\n    + \ttest_cmp expect actual\n    +@@ t/t9001-send-email.sh: test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n    + \ttest_path_is_file my-hooks.ran &&\n    + \tcat >expect <<-EOF &&\n    + \tfatal: longline.patch: rejected by sendemail-validate hook\n    +-\tfatal: command '\"'\"'$PWD/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n    + \twarning: no patches were sent\n    + \tEOF\n    + \ttest_cmp expect actual\n     @@ t/t9001-send-email.sh: test_expect_success $PREREQ 'invoke hook' '\n      \tmkdir -p .git/hooks &&\n      \n36:  7f05b25392 ! 35:  d2a477d9e3 run-command: stop thinking about hooks\n    @@ hook.c: static int should_include_hookdir(const char *path, enum hookdir_opt cfg\n     +}\n     +\n     +\n    - struct list_head* hook_list(const struct strbuf* hookname)\n    + struct list_head* hook_list(const char* hookname)\n      {\n      \tstruct strbuf hook_key = STRBUF_INIT;\n    -@@ hook.c: struct list_head* hook_list(const struct strbuf* hookname)\n    +@@ hook.c: struct list_head* hook_list(const char* hookname)\n      \tgit_config(hook_config_lookup, &cb_data);\n      \n      \tif (have_git_dir()) {\n    --\t\tconst char *legacy_hook_path = find_hook(hookname->buf);\n    -+\t\tconst char *legacy_hook_path = find_legacy_hook(hookname->buf);\n    +-\t\tconst char *legacy_hook_path = find_hook(hookname);\n    ++\t\tconst char *legacy_hook_path = find_legacy_hook(hookname);\n      \n      \t\t/* Unconditionally add legacy hook, but annotate it. */\n      \t\tif (legacy_hook_path) {\n37:  e9b1f847f2 <  -:  ---------- docs: unify githooks and git-hook manpages\n -:  ---------- > 36:  62a3e3b419 doc: clarify fsmonitor-watchman specification\n -:  ---------- > 37:  5c864de1aa docs: link githooks and git-hook manpages\n\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425603","messageId":"20210527000856.695702-2-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 01/37] doc: propose hooks managed by the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:20Z","receivedAt":"2021-05-27T00:09:11Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Begin a design document for config-based hooks, managed via git-hook.\nFocus on an overview of the implementation and motivation for design\ndecisions. Briefly discuss the alternatives considered before this\npoint. Also, attempt to redefine terms to fit into a multihook world.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Since v6, checked for inconsistencies with implementation and added lots of\n    caveats about whether 'git hook add' and 'git hook edit' will ever materialize.\n    \n    Hopefully this reflects reality now; please review accordingly.\n    \n    Since v6, checked for inconsistencies with implementation and added lots of\n    caveats about whether 'git hook add' and 'git hook edit' will ever materialize.\n    \n    Hopefully this reflects reality now; please review accordingly.\n    \n    Since v4, addressed comments from Jonathan Tan about wording. However, I have\n    not addressed AEvar's comments or done a full re-review of this document.\n    I wanted to get the rest of the series out for initial review first.\n    \n     - Emily\n    \n    Since v4, addressed comments from Jonathan Tan about wording.\n\n Documentation/Makefile                        |   1 +\n .../technical/config-based-hooks.txt          | 369 ++++++++++++++++++\n 2 files changed, 370 insertions(+)\n create mode 100644 Documentation/technical/config-based-hooks.txt\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex 2aae4c9cbb..5d19eddb0e 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -90,6 +90,7 @@ SP_ARTICLES += $(API_DOCS)\n TECH_DOCS += MyFirstContribution\n TECH_DOCS += MyFirstObjectWalk\n TECH_DOCS += SubmittingPatches\n+TECH_DOCS += technical/config-based-hooks\n TECH_DOCS += technical/hash-function-transition\n TECH_DOCS += technical/http-protocol\n TECH_DOCS += technical/index-format\ndiff --git a/Documentation/technical/config-based-hooks.txt b/Documentation/technical/config-based-hooks.txt\nnew file mode 100644\nindex 0000000000..1f973117e4\n--- /dev/null\n+++ b/Documentation/technical/config-based-hooks.txt\n@@ -0,0 +1,369 @@\n+Configuration-based hook management\n+===================================\n+:sectanchors:\n+\n+[[motivation]]\n+== Motivation\n+\n+Replace the `.git/hook/hookname` path as the only source of hooks to execute;\n+allow users to define hooks using config files, in a way which is friendly to\n+users with multiple repos which have similar needs - hooks can be easily shared\n+between multiple Git repos.\n+\n+Redefine \"hook\" as an event rather than a single script, allowing users to\n+perform multiple unrelated actions on a single event.\n+\n+Make it easier for users to discover Git's hook feature and automate their\n+workflows.\n+\n+[[user-interfaces]]\n+== User interfaces\n+\n+[[config-schema]]\n+=== Config schema\n+\n+Hooks can be introduced by editing the configuration manually. There are two new\n+sections added, `hook` and `hookcmd`.\n+\n+[[config-schema-hook]]\n+==== `hook`\n+\n+Primarily contains subsections for each hook event. The order of variables in\n+these subsections defines the hook command execution order; hook commands can be\n+specified by setting the value directly to the command if no additional\n+configuration is needed, or by setting the value as the name of a `hookcmd`. If\n+Git does not find a `hookcmd` whose subsection matches the value of the given\n+command string, Git will try to execute the string directly. Hooks are executed\n+by passing the resolved command string to the shell. In the future, hook event\n+subsections could also contain per-hook-event settings; see\n+<<per-hook-event-settings,the section in Future Work>> for more details.\n+\n+Also contains top-level hook execution settings, for example, `hook.runHookDir`.\n+(These settings are described more in <<library,Library>>.)\n+\n+----\n+[hook \"pre-commit\"]\n+  command = perl-linter\n+  command = /usr/bin/git-secrets --pre-commit\n+\n+[hook \"pre-applypatch\"]\n+  command = perl-linter\n+  # for illustration purposes; error behavior isn't planned yet\n+  error = ignore\n+\n+[hook]\n+  runHookDir = interactive\n+----\n+\n+[[config-schema-hookcmd]]\n+==== `hookcmd`\n+\n+Defines a hook command and its attributes, which will be used when a hook event\n+occurs. Unqualified attributes are assumed to apply to this hook during all hook\n+events, but event-specific attributes can also be supplied. The example runs\n+`/usr/bin/lint-it --language=perl <args passed by Git>`, but for repos which\n+include this config, the hook command will be skipped for all events.\n+Theoretically, the last line could be used to \"un-skip\" the hook command for\n+`pre-commit` hooks, but this hasn't been scoped or implemented yet.\n+\n+----\n+[hookcmd \"perl-linter\"]\n+  command = /usr/bin/lint-it --language=perl\n+  skip = true\n+  # for illustration purposes; below hasn't been defined yet\n+  pre-commit-skip = false\n+----\n+\n+[[command-line-api]]\n+=== Command-line API\n+\n+Users should be able to view, run, reorder, and create hook commands via the\n+command line. External tools should be able to view a list of hooks in the\n+correct order to run. Modifier commands (`edit` and `add`) have not been\n+implemented yet and may not be if manually editing the config proves usable\n+enough.\n+\n+*`git hook list <hook-event>`*\n+\n+*`git hook run <hook-event> [-a <arg>]... [-e <env-var>]...`*\n+\n+*`git hook edit <hook-event>`*\n+\n+*`git hook add <hook-command> <hook-event> <options...>`*\n+\n+[[hook-editor]]\n+=== Hook editor\n+\n+The tool which is presented by `git hook edit <hook-command>`. Ideally, this\n+tool should be easier to use than manually editing the config, and then produce\n+a concise config afterwards. It may take a form similar to `git rebase\n+--interactive`. This has not been designed or implemented yet and may not be if\n+the config proves usable enough.\n+\n+[[implementation]]\n+== Implementation\n+\n+[[library]]\n+=== Library\n+\n+`hook.c` and `hook.h` are responsible for interacting with the config files. The\n+hook library provides a basic API to call all hooks in config order with more\n+complex options passed via `struct run_hooks_opt`:\n+\n+*`int run_hooks(const char *hookname, struct run_hooks_opt *options)`*\n+\n+`struct run_hooks_opt` allows callers to set:\n+\n+- environment variables\n+- command-line arguments\n+- behavior for the hook command provided by `run-command.h:find_hook()` (see\n+  below)\n+- a method to provide stdin to each hook, either via a file containing stdin, a\n+  `struct string_list` containing a list of lines to print, or a callback\n+  function to allow the caller to populate stdin manually\n+- a method to process stdout from each hook, e.g. for printing to sideband\n+  during a network operation\n+- parallelism\n+- a custom working directory for hooks to execute in\n+\n+And this struct can be extended with more options as necessary in the future.\n+\n+The \"legacy\" hook provided by `run-command.h:find_hook()` - that is, the hook\n+present in `.git/hooks/<hookname>` or\n+`$(git config --get core.hooksPath)/<hookname>` - can be handled in a number of\n+ways, providing an avenue to deprecate these \"legacy\" hooks if desired. The\n+handling is based on a config `hook.runHookDir`, which is checked against a\n+number of cases:\n+\n+- \"no\": the legacy hook will not be run\n+- \"error\": Git will print a warning to stderr before ignoring the legacy hook\n+- \"interactive\": Git will prompt the user before running the legacy hook\n+- \"warn\": Git will print a warning to stderr before running the legacy hook\n+- \"yes\" (default): Git will silently run the legacy hook\n+\n+In case this list is expanded in the future, if a value for `hook.runHookDir` is\n+given which Git does not recognize, Git should discard that config entry. For\n+example, if \"warn\" was specified at system level and \"junk\" was specified at\n+global level, Git would resolve the value to \"warn\"; if the only time the config\n+was set was to \"junk\", Git would use the default value of \"yes\" (but print a\n+warning to the user first to let them know their value is wrong).\n+\n+`struct hookcmd` is expected to grow in size over time as more functionality is\n+added to hooks; so that other parts of the code don't need to understand the\n+config schema, `struct hookcmd` should contain logical values instead of string\n+pairs.\n+\n+By default, hook parallelism is chosen based on the semantics of each hook;\n+callsites initialize their `struct run_hooks_opt` via one of two macros,\n+`RUN_HOOKS_OPT_INIT_SYNC` or `RUN_HOOKS_OPT_INIT_ASYNC`. The default number of\n+jobs can be configured in `hook.jobs`; this config applies across all hook\n+events. If unset, the value of `online_cpus()` (equivalent to `nproc`) is used.\n+\n+[[builtin]]\n+=== Builtin\n+\n+`builtin/hook.c` is responsible for providing the frontend. It's responsible for\n+formatting user-provided data and then calling the library API to set the\n+configs as appropriate. The builtin frontend is not responsible for calling the\n+config directly, so that other areas of Git can rely on the hook library to\n+understand the most recent config schema for hooks.\n+\n+[[migration]]\n+=== Migration path\n+\n+[[stage-0]]\n+==== Stage 0\n+\n+Hooks are called by running `run-command.h:find_hook()` with the hookname and\n+executing the result. The hook library and builtin do not exist. Hooks only\n+exist as specially named scripts within `.git/hooks/`.\n+\n+[[stage-1]]\n+==== Stage 1\n+\n+`git hook list --porcelain <hook-event>` is implemented. `hook.h:run_hooks()` is\n+taught to include `run-command.h:find_hook()` at the end; calls to `find_hook()`\n+are replaced with calls to `run_hooks()`. Users can opt-in to config-based hooks\n+simply by creating some in their config; otherwise users should remain\n+unaffected by the change.\n+\n+[[stage-2]]\n+==== Stage 2\n+\n+The call to `find_hook()` inside of `run_hooks()` learns to check for a config,\n+`hook.runHookDir`. Users can opt into managing their hooks completely via the\n+config this way.\n+\n+[[stage-3]]\n+==== Stage 3\n+\n+`.git/hooks` is removed from the template and the hook directory is considered\n+deprecated. To avoid breaking older repos, the default of `hook.runHookDir` is\n+not changed, and `find_hook()` is not removed.\n+\n+[[caveats]]\n+== Caveats\n+\n+[[security]]\n+=== Security and repo config\n+\n+Part of the motivation behind this refactor is to mitigate hooks as an attack\n+vector.footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/]\n+However, as the design stands, users can still provide hooks in the repo-level\n+config, which is included when a repo is zipped and sent elsewhere. The\n+security of the repo-level config is still under discussion; this design\n+generally assumes the repo-level config is secure, which is not true yet. This\n+assumption was made to avoid overcomplicating the design. So, this series\n+doesn't particularly improve security or resistance to zip attacks.\n+\n+[[ease-of-use]]\n+=== Ease of use\n+\n+The config schema is nontrivial; that's why it's important for the `git hook`\n+modifier commands to be usable. Contributors with UX expertise are encouraged to\n+share their suggestions.\n+\n+[[alternatives]]\n+== Alternative approaches\n+\n+A previous summary of alternatives exists in the\n+archives.footnote:[https://lore.kernel.org/git/20191116011125.GG22855@google.com]\n+\n+The table below shows a number of goals and how they might be achieved with\n+config-based hooks, by implementing directory support (i.e.\n+'.git/hooks/pre-commit.d'), or as hooks are run today.\n+\n+.Comparison of alternatives\n+|===\n+|Feature |Config-based hooks |Hook directories |Status quo\n+\n+|Supports multiple hooks\n+|Natively\n+|Natively\n+|With user effort\n+\n+|Supports parallelization\n+|Natively\n+|Natively\n+|No (user's multihook trampoline script would need to handle parallelism)\n+\n+|Safer for zipped repos\n+|A little\n+|No\n+|No\n+\n+|Previous hooks just work\n+|If configured\n+|Yes\n+|Yes\n+\n+|Can install one hook to many repos\n+|Yes\n+|With symlinks or core.hooksPath\n+|With symlinks or core.hooksPath\n+\n+|Discoverability\n+|Findable with 'git help git' or tab-completion via 'git hook' subcommand\n+|Findable via improved documentation\n+|Same as before\n+\n+|Hard to run unexpected hook\n+|If configured\n+|Could be made to warn or look for a config\n+|No\n+|===\n+\n+[[status-quo]]\n+=== Status quo\n+\n+Today users can implement multihooks themselves by using a \"trampoline script\"\n+as their hook, and pointing that script to a directory or list of other scripts\n+they wish to run.\n+\n+[[hook-directories]]\n+=== Hook directories\n+\n+Other contributors have suggested Git learn about the existence of a directory\n+such as `.git/hooks/<hookname>.d` and execute those hooks in alphabetical order.\n+\n+[[future-work]]\n+== Future work\n+\n+[[execution-ordering]]\n+=== Execution ordering\n+\n+We may find that config order is insufficient for some users; for example,\n+config order makes it difficult to add a new hook to the system or global config\n+which runs at the end of the hook list. A new ordering schema should be:\n+\n+1) Specified by a `hook.order` config, so that users will not unexpectedly see\n+their order change;\n+\n+2) Either dependency or numerically based.\n+\n+Dependency-based ordering is prone to classic linked-list problems, like a\n+cycles and handling of missing dependencies. But, it paves the way for enabling\n+parallelization if some tasks truly depend on others.\n+\n+Numerical ordering makes it tricky for Git to generate suggested ordering\n+numbers for each command, but is easy to determine a definitive order.\n+\n+[[parallelization]]\n+=== Parallelization with dependencies\n+\n+Currently hooks use a naive parallelization scheme or are run in series.  But if\n+one hook depends on another's output, then users will want to specify those\n+dependencies. If we decide to solve this problem, we may want to look to modern\n+build systems for inspiration on how to manage dependencies and parallel tasks.\n+\n+[[nontrivial-hooks]]\n+=== Multihooks and nontrivial output\n+\n+Some hooks - like 'proc-receive' - don't lend themselves well to multihooks at\n+all. In the case of 'proc-receive', for now, multiple hook definitions are\n+disallowed. In the future we might be able to conceive a better approach, for\n+example, running the hooks in series and using the output from one hook as the\n+input to the next.\n+\n+[[securing-hookdir-hooks]]\n+=== Securing hookdir hooks\n+\n+With the design as written in this doc, it's still possible for a malicious user\n+to modify `.git/config` to include `hook.pre-receive.command = rm -rf /`, then\n+zip their repo and send it to another user. It may be necessary to teach Git to\n+only allow inlined hooks like this if they were configured outside of the local\n+scope (in other words, only run hookcmds, and only allow hookcmds to be\n+configured in global or system scope); or another approach, like a list of safe\n+projects, might be useful. It may also be sufficient (or at least useful) to\n+teach a `hook.disableAll` config or similar flag to the Git executable.\n+\n+[[submodule-inheritance]]\n+=== Submodule inheritance\n+\n+It's possible some submodules may want to run the identical set of hooks that\n+their superrepo runs. While a globally-configured hook set is helpful, it's not\n+a great solution for users who have multiple repos-with-submodules under the\n+same user. It would be useful for submodules to learn how to run hooks from\n+their superrepo's config, or inherit that hook setting.\n+\n+[[per-hook-event-settings]]\n+=== Per-hook-event settings\n+\n+It might be desirable to keep settings specifically for some hook events, but\n+not for others - for example, a user may wish to disable hookdir hooks for all\n+events but pre-commit, which they haven't had time to convert yet; or, a user\n+may wish for execution order settings to differ based on hook event. In that\n+case, it would be useful to set something like `hook.pre-commit.executionOrder`\n+which would not apply to the 'prepare-commit-msg' hook, for example.\n+\n+[[glossary]]\n+== Glossary\n+\n+*hook event*\n+\n+A point during Git's execution where user scripts may be run, for example,\n+_prepare-commit-msg_ or _pre-push_.\n+\n+*hook command*\n+\n+A user script or executable which will be run on one or more hook events.\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425604","messageId":"20210527000856.695702-3-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 02/37] hook: introduce git-hook subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:21Z","receivedAt":"2021-05-27T00:09:19Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Add a new subcommand, git-hook, which will be used to ease config-based\nhook management. This command will handle parsing configs to compose a\nlist of hooks to run for a given event, as well as adding or modifying\nhook configs in an interactive fashion.\n\nStart with 'git hook list <hookname>', which checks the known configs in\norder to create an ordered list of hooks to run on a given hook event.\n\nMultiple commands can be specified for a given hook by providing\nmultiple \"hook.<hookname>.command = <path-to-hook>\" lines. Hooks will be\nrun in config order. If more properties need to be set on a given hook\nin the future, commands can also be specified by providing\n\"hook.<hookname>.command = <hookcmd-name>\", as well as a \"[hookcmd\n<hookcmd-name>]\" subsection; this subsection should contain a\n\"hookcmd.<hookcmd-name>.command = <path-to-hook>\" line.\n\nFor example:\n\n  $ git config --list | grep ^hook\n  hook.pre-commit.command=baz\n  hook.pre-commit.command=~/bar.sh\n  hookcmd.baz.command=~/baz/from/hookcmd.sh\n\n  $ git hook list pre-commit\n  global: ~/baz/from/hookcmd.sh\n  local: ~/bar.sh\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Since v4, mainly changed to RUN_SETUP_GENTLY so that 'git hook list' can\n    be executed outside of a repo.\n\n .gitignore                    |   1 +\n Documentation/config/hook.txt |   9 +++\n Documentation/git-hook.txt    |  73 +++++++++++++++++++++\n Makefile                      |   2 +\n builtin.h                     |   1 +\n builtin/hook.c                |  65 ++++++++++++++++++\n command-list.txt              |   1 +\n git.c                         |   1 +\n hook.c                        | 120 ++++++++++++++++++++++++++++++++++\n hook.h                        |  25 +++++++\n t/t1360-config-based-hooks.sh |  88 +++++++++++++++++++++++++\n 11 files changed, 386 insertions(+)\n create mode 100644 Documentation/config/hook.txt\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100644 hook.c\n create mode 100644 hook.h\n create mode 100755 t/t1360-config-based-hooks.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 311841f9be..de39dc9961 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-hook\n /git-http-backend\n /git-http-fetch\n /git-http-push\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nnew file mode 100644\nindex 0000000000..71449ecbc7\n--- /dev/null\n+++ b/Documentation/config/hook.txt\n@@ -0,0 +1,9 @@\n+hook.<command>.command::\n+\tA command to execute during the <command> hook event. This can be an\n+\texecutable on your device, a oneliner for your shell, or the name of a\n+\thookcmd. See linkgit:git-hook[1].\n+\n+hookcmd.<name>.command::\n+\tA command to execute during a hook for which <name> has been specified\n+\tas a command. This can be an executable on your device or a oneliner for\n+\tyour shell. See linkgit:git-hook[1].\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nnew file mode 100644\nindex 0000000000..f19875ed68\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,73 @@\n+git-hook(1)\n+===========\n+\n+NAME\n+----\n+git-hook - Manage configured hooks\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git hook' list <hook-name>\n+\n+DESCRIPTION\n+-----------\n+You can list configured hooks with this command. Later, you will be able to run,\n+add, and modify hooks with this command.\n+\n+This command parses the default configuration files for sections `hook` and\n+`hookcmd`. `hook` is used to describe the commands which will be run during a\n+particular hook event; commands are run in the order Git encounters them during\n+the configuration parse (see linkgit:git-config[1]). `hookcmd` is used to\n+describe attributes of a specific command. If additional attributes don't need\n+to be specified, a command to run can be specified directly in the `hook`\n+section; if a `hookcmd` by that name isn't found, Git will attempt to run the\n+provided value directly. For example:\n+\n+Global config\n+----\n+  [hook \"post-commit\"]\n+    command = \"linter\"\n+    command = \"~/typocheck.sh\"\n+\n+  [hookcmd \"linter\"]\n+    command = \"/bin/linter --c\"\n+----\n+\n+Local config\n+----\n+  [hook \"prepare-commit-msg\"]\n+    command = \"linter\"\n+  [hook \"post-commit\"]\n+    command = \"python ~/run-test-suite.py\"\n+----\n+\n+With these configs, you'd then see:\n+\n+----\n+$ git hook list \"post-commit\"\n+global: /bin/linter --c\n+global: ~/typocheck.sh\n+local: python ~/run-test-suite.py\n+\n+$ git hook list \"prepare-commit-msg\"\n+local: /bin/linter --c\n+----\n+\n+COMMANDS\n+--------\n+\n+list `<hook-name>`::\n+\n+List the hooks which have been configured for `<hook-name>`. Hooks appear\n+in the order they should be run, and print the config scope where the relevant\n+`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n+This output is human-readable and the format is subject to change over time.\n+\n+CONFIGURATION\n+-------------\n+include::config/hook.txt[]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex c3565fc0f8..a6b71a0fbe 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -901,6 +901,7 @@ LIB_OBJS += hash-lookup.o\n LIB_OBJS += hashmap.o\n LIB_OBJS += help.o\n LIB_OBJS += hex.o\n+LIB_OBJS += hook.o\n LIB_OBJS += ident.o\n LIB_OBJS += json-writer.o\n LIB_OBJS += kwset.o\n@@ -1101,6 +1102,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\ndiff --git a/builtin.h b/builtin.h\nindex 16ecd5586f..91740c1514 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);\n int cmd_grep(int argc, const char **argv, const char *prefix);\n int cmd_hash_object(int argc, const char **argv, const char *prefix);\n int cmd_help(int argc, const char **argv, const char *prefix);\n+int cmd_hook(int argc, const char **argv, const char *prefix);\n int cmd_index_pack(int argc, const char **argv, const char *prefix);\n int cmd_init_db(int argc, const char **argv, const char *prefix);\n int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/hook.c b/builtin/hook.c\nnew file mode 100644\nindex 0000000000..79e150437e\n--- /dev/null\n+++ b/builtin/hook.c\n@@ -0,0 +1,65 @@\n+#include \"cache.h\"\n+#include \"builtin.h\"\n+#include \"config.h\"\n+#include \"hook.h\"\n+#include \"parse-options.h\"\n+#include \"strbuf.h\"\n+\n+static const char * const builtin_hook_usage[] = {\n+\tN_(\"git hook list <hookname>\"),\n+\tNULL\n+};\n+\n+static int list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct list_head *head, *pos;\n+\tconst char *hookname = NULL;\n+\n+\tstruct option list_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, list_options,\n+\t\t\t     builtin_hook_usage, 0);\n+\n+\tif (argc < 1) {\n+\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\t\t      builtin_hook_usage, list_options);\n+\t}\n+\n+\thookname = argv[0];\n+\n+\thead = hook_list(hookname);\n+\n+\tif (list_empty(head)) {\n+\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n+\t\t       hookname);\n+\t\treturn 0;\n+\t}\n+\n+\tlist_for_each(pos, head) {\n+\t\tstruct hook *item = list_entry(pos, struct hook, list);\n+\t\tif (item)\n+\t\t\tprintf(\"%s: %s\\n\",\n+\t\t\t       config_scope_name(item->origin),\n+\t\t\t       item->command.buf);\n+\t}\n+\n+\tclear_hook_list(head);\n+\n+\treturn 0;\n+}\n+\n+int cmd_hook(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option builtin_hook_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tif (argc < 2)\n+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+\n+\tif (!strcmp(argv[1], \"list\"))\n+\t\treturn list(argc - 1, argv + 1, prefix);\n+\n+\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex a289f09ed6..9ccd8e5aeb 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-hook                                mainporcelain\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\n git-http-push                           synchelpers\ndiff --git a/git.c b/git.c\nindex 18bed9a996..39988ee3b0 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nnew file mode 100644\nindex 0000000000..d3e28aa73a\n--- /dev/null\n+++ b/hook.c\n@@ -0,0 +1,120 @@\n+#include \"cache.h\"\n+\n+#include \"hook.h\"\n+#include \"config.h\"\n+\n+void free_hook(struct hook *ptr)\n+{\n+\tif (ptr) {\n+\t\tstrbuf_release(&ptr->command);\n+\t\tfree(ptr);\n+\t}\n+}\n+\n+static void append_or_move_hook(struct list_head *head, const char *command)\n+{\n+\tstruct list_head *pos = NULL, *tmp = NULL;\n+\tstruct hook *to_add = NULL;\n+\n+\t/*\n+\t * remove the prior entry with this command; we'll replace it at the\n+\t * end.\n+\t */\n+\tlist_for_each_safe(pos, tmp, head) {\n+\t\tstruct hook *it = list_entry(pos, struct hook, list);\n+\t\tif (!strcmp(it->command.buf, command)) {\n+\t\t    list_del(pos);\n+\t\t    /* we'll simply move the hook to the end */\n+\t\t    to_add = it;\n+\t\t    break;\n+\t\t}\n+\t}\n+\n+\tif (!to_add) {\n+\t\t/* adding a new hook, not moving an old one */\n+\t\tto_add = xmalloc(sizeof(*to_add));\n+\t\tstrbuf_init(&to_add->command, 0);\n+\t\tstrbuf_addstr(&to_add->command, command);\n+\t}\n+\n+\t/* re-set the scope so we show where an override was specified */\n+\tto_add->origin = current_config_scope();\n+\n+\tlist_add_tail(&to_add->list, head);\n+}\n+\n+static void remove_hook(struct list_head *to_remove)\n+{\n+\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n+\tlist_del(to_remove);\n+\tfree_hook(hook_to_remove);\n+}\n+\n+void clear_hook_list(struct list_head *head)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, head)\n+\t\tremove_hook(pos);\n+}\n+\n+struct hook_config_cb\n+{\n+\tstruct strbuf *hookname;\n+\tstruct list_head *list;\n+};\n+\n+static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n+{\n+\tstruct hook_config_cb *data = cb_data;\n+\tconst char *hook_key = data->hookname->buf;\n+\tstruct list_head *head = data->list;\n+\n+\tif (!strcmp(key, hook_key)) {\n+\t\tconst char *command = value;\n+\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\n+\t\t/*\n+\t\t * Check if a hookcmd with that name exists. If it doesn't,\n+\t\t * 'git_config_get_value()' is documented not to touch &command,\n+\t\t * so we don't need to do anything.\n+\t\t */\n+\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n+\t\tgit_config_get_value(hookcmd_name.buf, &command);\n+\n+\t\tif (!command) {\n+\t\t\tstrbuf_release(&hookcmd_name);\n+\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n+\t\t}\n+\n+\t\t/*\n+\t\t * TODO: implement an option-getting callback, e.g.\n+\t\t *   get configs by pattern hookcmd.$value.*\n+\t\t *   for each key+value, do_callback(key, value, cb_data)\n+\t\t */\n+\n+\t\tappend_or_move_hook(head, command);\n+\n+\t\tstrbuf_release(&hookcmd_name);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+struct list_head* hook_list(const char* hookname)\n+{\n+\tstruct strbuf hook_key = STRBUF_INIT;\n+\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n+\n+\tINIT_LIST_HEAD(hook_head);\n+\n+\tif (!hookname)\n+\t\treturn NULL;\n+\n+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n+\n+\tgit_config(hook_config_lookup, &cb_data);\n+\n+\tstrbuf_release(&hook_key);\n+\treturn hook_head;\n+}\ndiff --git a/hook.h b/hook.h\nnew file mode 100644\nindex 0000000000..042cab8446\n--- /dev/null\n+++ b/hook.h\n@@ -0,0 +1,25 @@\n+#include \"config.h\"\n+#include \"list.h\"\n+#include \"strbuf.h\"\n+\n+struct hook {\n+\tstruct list_head list;\n+\t/*\n+\t * Config file which holds the hook.*.command definition.\n+\t * (This has nothing to do with the hookcmd.<name>.* configs.)\n+\t */\n+\tenum config_scope origin;\n+\t/* The literal command to run. */\n+\tstruct strbuf command;\n+};\n+\n+/*\n+ * Provides a linked list of 'struct hook' detailing commands which should run\n+ * in response to the 'hookname' event, in execution order.\n+ */\n+struct list_head* hook_list(const char *hookname);\n+\n+/* Free memory associated with a 'struct hook' */\n+void free_hook(struct hook *ptr);\n+/* Empties the list at 'head', calling 'free_hook()' on each entry */\n+void clear_hook_list(struct list_head *head);\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nnew file mode 100755\nindex 0000000000..6e4a3e763f\n--- /dev/null\n+++ b/t/t1360-config-based-hooks.sh\n@@ -0,0 +1,88 @@\n+#!/bin/bash\n+\n+test_description='config-managed multihooks, including git-hook command'\n+\n+. ./test-lib.sh\n+\n+ROOT=\n+if test_have_prereq MINGW\n+then\n+\t# In Git for Windows, Unix-like paths work only in shell scripts;\n+\t# `git.exe`, however, will prefix them with the pseudo root directory\n+\t# (of the Unix shell). Let's accommodate for that.\n+\tROOT=\"$(cd / && pwd)\"\n+fi\n+\n+setup_hooks () {\n+\ttest_config hook.pre-commit.command \"/path/ghi\" --add\n+\ttest_config_global hook.pre-commit.command \"/path/def\" --add\n+}\n+\n+setup_hookcmd () {\n+\ttest_config hook.pre-commit.command \"abc\" --add\n+\ttest_config_global hookcmd.abc.command \"/path/abc\" --add\n+}\n+\n+test_expect_success 'git hook rejects commands without a mode' '\n+\ttest_must_fail git hook pre-commit\n+'\n+\n+\n+test_expect_success 'git hook rejects commands without a hookname' '\n+\ttest_must_fail git hook list\n+'\n+\n+test_expect_success 'git hook runs outside of a repo' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tEOF\n+\n+\tnongit git config --list --global &&\n+\n+\tnongit git hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list orders by config order' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tlocal: $ROOT/path/ghi\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list dereferences a hookcmd' '\n+\tsetup_hooks &&\n+\tsetup_hookcmd &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tlocal: $ROOT/path/ghi\n+\tlocal: $ROOT/path/abc\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list reorders on duplicate commands' '\n+\tsetup_hooks &&\n+\n+\ttest_config hook.pre-commit.command \"/path/def\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tlocal: $ROOT/path/ghi\n+\tlocal: $ROOT/path/def\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_done\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425605","messageId":"20210527000856.695702-4-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 03/37] hook: include hookdir hook in list","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:22Z","receivedAt":"2021-05-27T00:09:25Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Historically, hooks are declared by placing an executable into\n$GIT_DIR/hooks/$HOOKNAME (or $HOOKDIR/$HOOKNAME). Although hooks taken\nfrom the config are more featureful than hooks placed in the $HOOKDIR,\nthose hooks should not stop working for users who already have them.\nLet's list them to the user, but instead of displaying a config scope\n(e.g. \"global: blah\") we can prefix them with \"hookdir:\".\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/hook.c                | 18 +++++++++++++++---\n hook.c                        | 17 +++++++++++++++++\n hook.h                        |  1 +\n t/t1360-config-based-hooks.sh | 19 +++++++++++++++++++\n 4 files changed, 52 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 79e150437e..e82725f0a6 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -39,10 +39,20 @@ static int list(int argc, const char **argv, const char *prefix)\n \n \tlist_for_each(pos, head) {\n \t\tstruct hook *item = list_entry(pos, struct hook, list);\n-\t\tif (item)\n-\t\t\tprintf(\"%s: %s\\n\",\n-\t\t\t       config_scope_name(item->origin),\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item) {\n+\t\t\t/*\n+\t\t\t * TRANSLATORS: \"<config scope>: <path>\". Both fields\n+\t\t\t * should be left untranslated; config scope matches the\n+\t\t\t * output of 'git config --show-scope'. Marked for\n+\t\t\t * translation to provide better RTL support later.\n+\t\t\t */\n+\t\t\tprintf(_(\"%s: %s\\n\"),\n+\t\t\t       (item->from_hookdir\n+\t\t\t\t? \"hookdir\"\n+\t\t\t\t: config_scope_name(item->origin)),\n \t\t\t       item->command.buf);\n+\t\t}\n \t}\n \n \tclear_hook_list(head);\n@@ -58,6 +68,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \tif (argc < 2)\n \t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n \n+\tgit_config(git_default_config, NULL);\n+\n \tif (!strcmp(argv[1], \"list\"))\n \t\treturn list(argc - 1, argv + 1, prefix);\n \ndiff --git a/hook.c b/hook.c\nindex d3e28aa73a..b4994fc108 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -2,6 +2,7 @@\n \n #include \"hook.h\"\n #include \"config.h\"\n+#include \"run-command.h\"\n \n void free_hook(struct hook *ptr)\n {\n@@ -35,6 +36,7 @@ static void append_or_move_hook(struct list_head *head, const char *command)\n \t\tto_add = xmalloc(sizeof(*to_add));\n \t\tstrbuf_init(&to_add->command, 0);\n \t\tstrbuf_addstr(&to_add->command, command);\n+\t\tto_add->from_hookdir = 0;\n \t}\n \n \t/* re-set the scope so we show where an override was specified */\n@@ -115,6 +117,21 @@ struct list_head* hook_list(const char* hookname)\n \n \tgit_config(hook_config_lookup, &cb_data);\n \n+\tif (have_git_dir()) {\n+\t\tconst char *legacy_hook_path = find_hook(hookname);\n+\n+\t\t/* Unconditionally add legacy hook, but annotate it. */\n+\t\tif (legacy_hook_path) {\n+\t\t\tstruct hook *legacy_hook;\n+\n+\t\t\tappend_or_move_hook(hook_head,\n+\t\t\t\t\t    absolute_path(legacy_hook_path));\n+\t\t\tlegacy_hook = list_entry(hook_head->prev, struct hook,\n+\t\t\t\t\t\t list);\n+\t\t\tlegacy_hook->from_hookdir = 1;\n+\t\t}\n+\t}\n+\n \tstrbuf_release(&hook_key);\n \treturn hook_head;\n }\ndiff --git a/hook.h b/hook.h\nindex 042cab8446..b6c5480325 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -11,6 +11,7 @@ struct hook {\n \tenum config_scope origin;\n \t/* The literal command to run. */\n \tstruct strbuf command;\n+\tunsigned from_hookdir : 1;\n };\n \n /*\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 6e4a3e763f..0f12af4659 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -23,6 +23,14 @@ setup_hookcmd () {\n \ttest_config_global hookcmd.abc.command \"/path/abc\" --add\n }\n \n+setup_hookdir () {\n+\tmkdir .git/hooks\n+\twrite_script .git/hooks/pre-commit <<-EOF\n+\techo \\\"Legacy Hook\\\"\n+\tEOF\n+\ttest_when_finished rm -rf .git/hooks\n+}\n+\n test_expect_success 'git hook rejects commands without a mode' '\n \ttest_must_fail git hook pre-commit\n '\n@@ -85,4 +93,15 @@ test_expect_success 'git hook list reorders on duplicate commands' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list shows hooks from the hookdir' '\n+\tsetup_hookdir &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425606","messageId":"20210527000856.695702-5-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 04/37] hook: teach hook.runHookDir","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:23Z","receivedAt":"2021-05-27T00:09:27Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"For now, just give a hint about how these hooks will be run in 'git hook\nlist'. Later on, though, we will pay attention to this enum when running\nthe hooks.\n---\n\nNotes:\n    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to\n    enforce it - now it matches the design doc much better.\n    \n    Also, thanks Jonathan Tan for pointing out that the commit message made no sense\n    and was targeted for a different change. Rewrote the commit message now.\n    \n    Plus, added HOOK_ERROR flag per Junio and Jonathan Nieder.\n    \n    Newly split into its own commit since v4, and taking place much sooner.\n    \n    An unfortunate side effect of adding this support *before* the\n    hook.runHookDir support is that the labels on the list are not clear -\n    because we aren't yet flagging which hooks are from the hookdir versus\n    the config. I suppose we could move the addition of that field to the\n    struct hook up to this patch, but it didn't make a lot of sense to me to\n    do it just for cosmetic purposes.\n    \n    Since v7, tidied up the behavior of the HOOK_UNKNOWN flag and added a test to\n    enforce it - now it matches the design doc much better.\n    \n    Also, thanks Jonathan Tan for pointing out that the commit message made no sense\n    and was targeted for a different change. Rewrote the commit message now.\n    \n    Newly split into its own commit since v4, and taking place much sooner.\n    \n    An unfortunate side effect of adding this support *before* the\n    hook.runHookDir support is that the labels on the list are not clear -\n    because we aren't yet flagging which hooks are from the hookdir versus\n    the config. I suppose we could move the addition of that field to the\n    struct hook up to this patch, but it didn't make a lot of sense to me to\n    do it just for cosmetic purposes.\n\n Documentation/config/hook.txt |  5 ++\n builtin/hook.c                | 98 ++++++++++++++++++++++++++++++-----\n hook.c                        | 24 +++++++++\n hook.h                        | 22 ++++++++\n t/t1360-config-based-hooks.sh | 71 +++++++++++++++++++++++++\n 5 files changed, 206 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 71449ecbc7..75312754ae 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -7,3 +7,8 @@ hookcmd.<name>.command::\n \tA command to execute during a hook for which <name> has been specified\n \tas a command. This can be an executable on your device or a oneliner for\n \tyour shell. See linkgit:git-hook[1].\n+\n+hook.runHookDir::\n+\tControls how hooks contained in your hookdir are executed. Can be any of\n+\t\"yes\", \"warn\", \"interactive\", or \"no\". Defaults to \"yes\". See\n+\tlinkgit:git-hook[1] and linkgit:git-config[1] \"core.hooksPath\").\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex e82725f0a6..b1e63a9576 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -10,10 +10,13 @@ static const char * const builtin_hook_usage[] = {\n \tNULL\n };\n \n+static enum hookdir_opt should_run_hookdir;\n+\n static int list(int argc, const char **argv, const char *prefix)\n {\n \tstruct list_head *head, *pos;\n \tconst char *hookname = NULL;\n+\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n \n \tstruct option list_options[] = {\n \t\tOPT_END(),\n@@ -41,37 +44,104 @@ static int list(int argc, const char **argv, const char *prefix)\n \t\tstruct hook *item = list_entry(pos, struct hook, list);\n \t\titem = list_entry(pos, struct hook, list);\n \t\tif (item) {\n-\t\t\t/*\n-\t\t\t * TRANSLATORS: \"<config scope>: <path>\". Both fields\n-\t\t\t * should be left untranslated; config scope matches the\n-\t\t\t * output of 'git config --show-scope'. Marked for\n-\t\t\t * translation to provide better RTL support later.\n-\t\t\t */\n-\t\t\tprintf(_(\"%s: %s\\n\"),\n-\t\t\t       (item->from_hookdir\n-\t\t\t\t? \"hookdir\"\n-\t\t\t\t: config_scope_name(item->origin)),\n-\t\t\t       item->command.buf);\n+\t\t\tif (item->from_hookdir) {\n+\t\t\t\t/*\n+\t\t\t\t * TRANSLATORS: do not translate 'hookdir' as\n+\t\t\t\t * it matches the config setting.\n+\t\t\t\t */\n+\t\t\t\tswitch (should_run_hookdir) {\n+\t\t\t\tcase HOOKDIR_NO:\n+\t\t\t\t\tprintf(_(\"hookdir: %s (will not run)\\n\"),\n+\t\t\t\t\t       item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase HOOKDIR_ERROR:\n+\t\t\t\t\tprintf(_(\"hookdir: %s (will error and not run)\\n\"),\n+\t\t\t\t\t       item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase HOOKDIR_INTERACTIVE:\n+\t\t\t\t\tprintf(_(\"hookdir: %s (will prompt)\\n\"),\n+\t\t\t\t\t       item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase HOOKDIR_WARN:\n+\t\t\t\t\tprintf(_(\"hookdir: %s (will warn but run)\\n\"),\n+\t\t\t\t\t       item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase HOOKDIR_YES:\n+\t\t\t\t/*\n+\t\t\t\t * The default behavior should agree with\n+\t\t\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n+\t\t\t\t * do the default behavior.\n+\t\t\t\t */\n+\t\t\t\tcase HOOKDIR_UNKNOWN:\n+\t\t\t\tdefault:\n+\t\t\t\t\tprintf(_(\"hookdir: %s\\n\"),\n+\t\t\t\t\t\t item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\t/*\n+\t\t\t\t * TRANSLATORS: \"<config scope>: <path>\". Both fields\n+\t\t\t\t * should be left untranslated; config scope matches the\n+\t\t\t\t * output of 'git config --show-scope'. Marked for\n+\t\t\t\t * translation to provide better RTL support later.\n+\t\t\t\t */\n+\t\t\t\tprintf(_(\"%s: %s\\n\"),\n+\t\t\t\t\tconfig_scope_name(item->origin),\n+\t\t\t\t\titem->command.buf);\n+\t\t\t}\n \t\t}\n \t}\n \n \tclear_hook_list(head);\n+\tstrbuf_release(&hookdir_annotation);\n \n \treturn 0;\n }\n \n int cmd_hook(int argc, const char **argv, const char *prefix)\n {\n+\tconst char *run_hookdir = NULL;\n+\n \tstruct option builtin_hook_options[] = {\n+\t\tOPT_STRING(0, \"run-hookdir\", &run_hookdir, N_(\"option\"),\n+\t\t\t   N_(\"what to do with hooks found in the hookdir\")),\n \t\tOPT_END(),\n \t};\n-\tif (argc < 2)\n+\n+\targc = parse_options(argc, argv, prefix, builtin_hook_options,\n+\t\t\t     builtin_hook_usage, 0);\n+\n+\t/* after the parse, we should have \"<command> <hookname> <args...>\" */\n+\tif (argc < 1)\n \t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n \n \tgit_config(git_default_config, NULL);\n \n-\tif (!strcmp(argv[1], \"list\"))\n-\t\treturn list(argc - 1, argv + 1, prefix);\n+\n+\t/* argument > config */\n+\tif (run_hookdir)\n+\t\tif (!strcmp(run_hookdir, \"no\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_NO;\n+\t\telse if (!strcmp(run_hookdir, \"error\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_ERROR;\n+\t\telse if (!strcmp(run_hookdir, \"yes\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_YES;\n+\t\telse if (!strcmp(run_hookdir, \"warn\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_WARN;\n+\t\telse if (!strcmp(run_hookdir, \"interactive\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_INTERACTIVE;\n+\t\telse\n+\t\t\t/*\n+\t\t\t * TRANSLATORS: leave \"yes/warn/interactive/no\"\n+\t\t\t * untranslated; the strings are compared literally.\n+\t\t\t */\n+\t\t\tdie(_(\"'%s' is not a valid option for --run-hookdir \"\n+\t\t\t      \"(yes, warn, interactive, no)\"), run_hookdir);\n+\telse\n+\t\tshould_run_hookdir = configured_hookdir_opt();\n+\n+\tif (!strcmp(argv[0], \"list\"))\n+\t\treturn list(argc, argv, prefix);\n \n \tusage_with_options(builtin_hook_usage, builtin_hook_options);\n }\ndiff --git a/hook.c b/hook.c\nindex b4994fc108..030051cab2 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -102,6 +102,30 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n \treturn 0;\n }\n \n+enum hookdir_opt configured_hookdir_opt(void)\n+{\n+\tconst char *key;\n+\tif (git_config_get_value(\"hook.runhookdir\", &key))\n+\t\treturn HOOKDIR_YES; /* by default, just run it. */\n+\n+\tif (!strcmp(key, \"no\"))\n+\t\treturn HOOKDIR_NO;\n+\n+\tif (!strcmp(key, \"error\"))\n+\t\treturn HOOKDIR_ERROR;\n+\n+\tif (!strcmp(key, \"yes\"))\n+\t\treturn HOOKDIR_YES;\n+\n+\tif (!strcmp(key, \"warn\"))\n+\t\treturn HOOKDIR_WARN;\n+\n+\tif (!strcmp(key, \"interactive\"))\n+\t\treturn HOOKDIR_INTERACTIVE;\n+\n+\treturn HOOKDIR_UNKNOWN;\n+}\n+\n struct list_head* hook_list(const char* hookname)\n {\n \tstruct strbuf hook_key = STRBUF_INIT;\ndiff --git a/hook.h b/hook.h\nindex b6c5480325..7f2b2ee8f2 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -20,6 +20,28 @@ struct hook {\n  */\n struct list_head* hook_list(const char *hookname);\n \n+enum hookdir_opt\n+{\n+\tHOOKDIR_NO,\n+\tHOOKDIR_ERROR,\n+\tHOOKDIR_WARN,\n+\tHOOKDIR_INTERACTIVE,\n+\tHOOKDIR_YES,\n+\tHOOKDIR_UNKNOWN,\n+};\n+\n+/*\n+ * Provides the hookdir_opt specified in the config without consulting any\n+ * command line arguments.\n+ */\n+enum hookdir_opt configured_hookdir_opt(void);\n+\n+/*\n+ * Provides the hookdir_opt specified in the config without consulting any\n+ * command line arguments.\n+ */\n+enum hookdir_opt configured_hookdir_opt(void);\n+\n /* Free memory associated with a 'struct hook' */\n void free_hook(struct hook *ptr);\n /* Empties the list at 'head', calling 'free_hook()' on each entry */\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 0f12af4659..141e6f7590 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -104,4 +104,75 @@ test_expect_success 'git hook list shows hooks from the hookdir' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'hook.runHookDir = no is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"no\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will not run)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hook.runHookDir = error is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"error\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will error and not run)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hook.runHookDir = warn is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"warn\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will warn but run)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual\n+'\n+\n+\n+test_expect_success 'hook.runHookDir = interactive is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"interactive\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will prompt)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"junk\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425607","messageId":"20210527000856.695702-6-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 05/37] hook: implement hookcmd.<name>.skip","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:24Z","receivedAt":"2021-05-27T00:09:28Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"If a user wants a specific repo to skip execution of a hook which is set\nat a global or system level, they will be able to do so by specifying\n'skip' in their repo config:\n\n~/.gitconfig\n  [hook.pre-commit]\n    command = skippable-oneliner\n    command = skippable-hookcmd\n\n  [hookcmd.skippable-hookcmd]\n    command = foo.sh\n\n$GIT_DIR/.git/config\n  [hookcmd.skippable-oneliner]\n    skip = true\n  [hookcmd.skippable-hookcmd]\n    skip = true\n\nLater it may make sense to add an option like\n\"hookcmd.<name>.<hook-event>-skip\" - but for simplicity, let's start\nwith a universal skip setting like this.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/config/hook.txt |  8 ++++++++\n Documentation/git-hook.txt    | 33 +++++++++++++++++++++++++++++++++\n hook.c                        | 35 ++++++++++++++++++++++++++---------\n t/t1360-config-based-hooks.sh | 35 +++++++++++++++++++++++++++++++++++\n 4 files changed, 102 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 75312754ae..8b12512e33 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -8,6 +8,14 @@ hookcmd.<name>.command::\n \tas a command. This can be an executable on your device or a oneliner for\n \tyour shell. See linkgit:git-hook[1].\n \n+hookcmd.<name>.skip::\n+\tSpecify this boolean to remove a command from earlier in the execution\n+\torder. Useful if you want to make a single repo an exception to hook\n+\tconfigured at the system or global scope. If there is no hookcmd\n+\tspecified for the command you want to skip, you can use the value of\n+\t`hook.<command>.command` as <name> as a shortcut. The \"skip\" setting\n+\tmust be specified after the \"hook.<command>.command\" to have an effect.\n+\n hook.runHookDir::\n \tControls how hooks contained in your hookdir are executed. Can be any of\n \t\"yes\", \"warn\", \"interactive\", or \"no\". Defaults to \"yes\". See\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex f19875ed68..c84520cb38 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -54,6 +54,39 @@ $ git hook list \"prepare-commit-msg\"\n local: /bin/linter --c\n ----\n \n+If there is a command you wish to run in most cases but have one or two\n+exceptional repos where it should be skipped, you can use specify\n+`hookcmd.<name>.skip`, for example:\n+\n+System config\n+----\n+  [hook \"pre-commit\"]\n+    command = check-for-secrets\n+\n+  [hookcmd \"check-for-secrets\"]\n+    command = /bin/secret-checker --aggressive\n+----\n+\n+Local config\n+----\n+  [hookcmd \"check-for-secrets\"]\n+    skip = true\n+  # This works for inlined hook commands, too:\n+  [hookcmd \"~/typocheck.sh\"]\n+    skip = true\n+----\n+\n+After these configs are added, the hook list becomes:\n+\n+----\n+$ git hook list \"post-commit\"\n+global: /bin/linter --c\n+local: python ~/run-test-suite.py\n+\n+$ git hook list \"pre-commit\"\n+no commands configured for hook 'pre-commit'\n+----\n+\n COMMANDS\n --------\n \ndiff --git a/hook.c b/hook.c\nindex 030051cab2..65cbad8dba 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -12,24 +12,25 @@ void free_hook(struct hook *ptr)\n \t}\n }\n \n-static void append_or_move_hook(struct list_head *head, const char *command)\n+static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n {\n \tstruct list_head *pos = NULL, *tmp = NULL;\n-\tstruct hook *to_add = NULL;\n+\tstruct hook *found = NULL;\n \n-\t/*\n-\t * remove the prior entry with this command; we'll replace it at the\n-\t * end.\n-\t */\n \tlist_for_each_safe(pos, tmp, head) {\n \t\tstruct hook *it = list_entry(pos, struct hook, list);\n \t\tif (!strcmp(it->command.buf, command)) {\n \t\t    list_del(pos);\n-\t\t    /* we'll simply move the hook to the end */\n-\t\t    to_add = it;\n+\t\t    found = it;\n \t\t    break;\n \t\t}\n \t}\n+\treturn found;\n+}\n+\n+static void append_or_move_hook(struct list_head *head, const char *command)\n+{\n+\tstruct hook *to_add = find_hook_by_command(head, command);\n \n \tif (!to_add) {\n \t\t/* adding a new hook, not moving an old one */\n@@ -74,12 +75,22 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n \tif (!strcmp(key, hook_key)) {\n \t\tconst char *command = value;\n \t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\t\tint skip = 0;\n+\n+\t\t/*\n+\t\t * Check if we're removing that hook instead. Hookcmds are\n+\t\t * removed by name, and inlined hooks are removed by command\n+\t\t * content.\n+\t\t */\n+\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.skip\", command);\n+\t\tgit_config_get_bool(hookcmd_name.buf, &skip);\n \n \t\t/*\n \t\t * Check if a hookcmd with that name exists. If it doesn't,\n \t\t * 'git_config_get_value()' is documented not to touch &command,\n \t\t * so we don't need to do anything.\n \t\t */\n+\t\tstrbuf_reset(&hookcmd_name);\n \t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n \t\tgit_config_get_value(hookcmd_name.buf, &command);\n \n@@ -94,7 +105,13 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n \t\t *   for each key+value, do_callback(key, value, cb_data)\n \t\t */\n \n-\t\tappend_or_move_hook(head, command);\n+\t\tif (skip) {\n+\t\t\tstruct hook *to_remove = find_hook_by_command(head, command);\n+\t\t\tif (to_remove)\n+\t\t\t\tremove_hook(&(to_remove->list));\n+\t\t} else {\n+\t\t\tappend_or_move_hook(head, command);\n+\t\t}\n \n \t\tstrbuf_release(&hookcmd_name);\n \t}\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 141e6f7590..33ac27aa97 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -146,6 +146,41 @@ test_expect_success 'hook.runHookDir = warn is respected by list' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list removes skipped hookcmd' '\n+\tsetup_hookcmd &&\n+\ttest_config hookcmd.abc.skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tno commands configured for hook '\\''pre-commit'\\''\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list ignores skip referring to unused hookcmd' '\n+\ttest_config hookcmd.abc.command \"/path/abc\" --add &&\n+\ttest_config hookcmd.abc.skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tno commands configured for hook '\\''pre-commit'\\''\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list removes skipped inlined hook' '\n+\tsetup_hooks &&\n+\ttest_config hookcmd.\"$ROOT/path/ghi\".skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n \n test_expect_success 'hook.runHookDir = interactive is respected by list' '\n \tsetup_hookdir &&\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425608","messageId":"20210527000856.695702-7-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 06/37] parse-options: parse into strvec","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:25Z","receivedAt":"2021-05-27T00:09:31Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"parse-options already knows how to read into a string_list, and it knows\nhow to read into an strvec as a passthrough (that is, including the\nargument as well as its value). string_list and strvec serve similar\npurposes but are somewhat painful to convert between; so, let's teach\nparse-options to read values of string arguments directly into an\nstrvec without preserving the argument name.\n\nThis is useful if collecting generic arguments to pass through to\nanother command, for example, 'git hook run --arg \"--quiet\" --arg\n\"--format=pretty\" some-hook'. The resulting strvec would contain\n{ \"--quiet\", \"--format=pretty\" }.\n\nThe implementation is based on that of OPT_STRING_LIST.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Since v7, updated the reference doc to make the intended usage for OPT_STRVEC\n    more clear.\n    \n    Since v4, fixed one or two more places where I missed the argv_array->strvec\n    rename.\n\n Documentation/technical/api-parse-options.txt |  7 +++++\n parse-options-cb.c                            | 16 +++++++++++\n parse-options.h                               |  4 +++\n t/helper/test-parse-options.c                 |  6 +++++\n t/t0040-parse-options.sh                      | 27 +++++++++++++++++++\n 5 files changed, 60 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex 5a60bbfa7f..f79b17e7fc 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -173,6 +173,13 @@ There are some macros to easily define options:\n \tThe string argument is stored as an element in `string_list`.\n \tUse of `--no-option` will clear the list of preceding values.\n \n+`OPT_STRVEC(short, long, &struct strvec, arg_str, description)`::\n+\tIntroduce an option with a string argument, meant to be specified\n+\tmultiple times.\n+\tThe string argument is stored as an element in `strvec`, and later\n+\targuments are added to the same `strvec`.\n+\tUse of `--no-option` will clear the list of preceding values.\n+\n `OPT_INTEGER(short, long, &int_var, description)`::\n \tIntroduce an option with integer argument.\n \tThe integer is put into `int_var`.\ndiff --git a/parse-options-cb.c b/parse-options-cb.c\nindex 3c811e1e4a..8227499eb6 100644\n--- a/parse-options-cb.c\n+++ b/parse-options-cb.c\n@@ -207,6 +207,22 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset)\n \treturn 0;\n }\n \n+int parse_opt_strvec(const struct option *opt, const char *arg, int unset)\n+{\n+\tstruct strvec *v = opt->value;\n+\n+\tif (unset) {\n+\t\tstrvec_clear(v);\n+\t\treturn 0;\n+\t}\n+\n+\tif (!arg)\n+\t\treturn -1;\n+\n+\tstrvec_push(v, arg);\n+\treturn 0;\n+}\n+\n int parse_opt_noop_cb(const struct option *opt, const char *arg, int unset)\n {\n \treturn 0;\ndiff --git a/parse-options.h b/parse-options.h\nindex a845a9d952..fcb0f1f31e 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -178,6 +178,9 @@ struct option {\n #define OPT_STRING_LIST(s, l, v, a, h) \\\n \t\t\t\t    { OPTION_CALLBACK, (s), (l), (v), (a), \\\n \t\t\t\t      (h), 0, &parse_opt_string_list }\n+#define OPT_STRVEC(s, l, v, a, h) \\\n+\t\t\t\t    { OPTION_CALLBACK, (s), (l), (v), (a), \\\n+\t\t\t\t      (h), 0, &parse_opt_strvec }\n #define OPT_UYN(s, l, v, h)         { OPTION_CALLBACK, (s), (l), (v), NULL, \\\n \t\t\t\t      (h), PARSE_OPT_NOARG, &parse_opt_tertiary }\n #define OPT_EXPIRY_DATE(s, l, v, h) \\\n@@ -297,6 +300,7 @@ int parse_opt_commits(const struct option *, const char *, int);\n int parse_opt_commit(const struct option *, const char *, int);\n int parse_opt_tertiary(const struct option *, const char *, int);\n int parse_opt_string_list(const struct option *, const char *, int);\n+int parse_opt_strvec(const struct option *, const char *, int);\n int parse_opt_noop_cb(const struct option *, const char *, int);\n enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t   const struct option *,\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 2051ce57db..922af56156 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -2,6 +2,7 @@\n #include \"cache.h\"\n #include \"parse-options.h\"\n #include \"string-list.h\"\n+#include \"strvec.h\"\n #include \"trace2.h\"\n \n static int boolean = 0;\n@@ -15,6 +16,7 @@ static char *string = NULL;\n static char *file = NULL;\n static int ambiguous;\n static struct string_list list = STRING_LIST_INIT_NODUP;\n+static struct strvec vector = STRVEC_INIT;\n \n static struct {\n \tint called;\n@@ -133,6 +135,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_STRING('o', NULL, &string, \"str\", \"get another string\"),\n \t\tOPT_NOOP_NOARG(0, \"obsolete\"),\n \t\tOPT_STRING_LIST(0, \"list\", &list, \"str\", \"add str to list\"),\n+\t\tOPT_STRVEC(0, \"vector\", &vector, \"str\", \"add str to strvec\"),\n \t\tOPT_GROUP(\"Magic arguments\"),\n \t\tOPT_ARGUMENT(\"quux\", NULL, \"means --quux\"),\n \t\tOPT_NUMBER_CALLBACK(&integer, \"set integer to NUM\",\n@@ -183,6 +186,9 @@ int cmd__parse_options(int argc, const char **argv)\n \tfor (i = 0; i < list.nr; i++)\n \t\tshow(&expect, &ret, \"list: %s\", list.items[i].string);\n \n+\tfor (i = 0; i < vector.nr; i++)\n+\t\tshow(&expect, &ret, \"vector: %s\", vector.v[i]);\n+\n \tfor (i = 0; i < argc; i++)\n \t\tshow(&expect, &ret, \"arg %02d: %s\", i, argv[i]);\n \ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ad4746d899..485e0170bf 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -35,6 +35,7 @@ String options\n     --st <st>             get another string (pervert ordering)\n     -o <str>              get another string\n     --list <str>          add str to list\n+    --vector <str>        add str to strvec\n \n Magic arguments\n     --quux                means --quux\n@@ -386,6 +387,32 @@ test_expect_success '--no-list resets list' '\n \ttest_cmp expect output\n '\n \n+cat >expect <<\\EOF\n+boolean: 0\n+integer: 0\n+magnitude: 0\n+timestamp: 0\n+string: (not set)\n+abbrev: 7\n+verbose: -1\n+quiet: 0\n+dry run: no\n+file: (not set)\n+vector: foo\n+vector: bar\n+vector: baz\n+EOF\n+test_expect_success '--vector keeps list of strings' '\n+\ttest-tool parse-options --vector foo --vector=bar --vector=baz >output &&\n+\ttest_cmp expect output\n+'\n+\n+test_expect_success '--no-vector resets list' '\n+\ttest-tool parse-options --vector=other --vector=irrelevant --vector=options \\\n+\t\t--no-vector --vector=foo --vector=bar --vector=baz >output &&\n+\ttest_cmp expect output\n+'\n+\n test_expect_success 'multiple quiet levels' '\n \ttest-tool parse-options --expect=\"quiet: 3\" -q -q -q\n '\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425609","messageId":"20210527000856.695702-9-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 08/37] hook: introduce hook_exists()","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:27Z","receivedAt":"2021-05-27T00:09:33Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Add a helper to easily determine whether any hooks exist for a given\nhook event.\n\nMany callers want to check whether some state could be modified by a\nhook; that check should include the config-based hooks as well. Optimize\nby checking the config directly. Since commands which execute hooks\nmight want to take args to replace 'hook.runHookDir', let\n'hook_exists()' take a hookdir_opt to override that config.\n\nIn some cases, external callers today use find_hook() to discover the\nlocation of a hook and then run it manually with run-command.h (that is,\nnot with run_hook_le()). Later, those cases will call hook.h:run_hook()\ndirectly instead.\n\nOnce the entire codebase is using hook_exists() instead of find_hook(),\nfind_hook() can be safely rolled into hook_exists() and removed from\nrun-command.h.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n hook.c | 19 +++++++++++++++++++\n hook.h | 10 ++++++++++\n 2 files changed, 29 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex b631da659b..008167dbe5 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -248,6 +248,25 @@ void run_hooks_opt_init(struct run_hooks_opt *o)\n \to->run_hookdir = configured_hookdir_opt();\n }\n \n+int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n+{\n+\tconst char *value = NULL; /* throwaway */\n+\tstruct strbuf hook_key = STRBUF_INIT;\n+\tint could_run_hookdir;\n+\n+\tif (should_run_hookdir == HOOKDIR_USE_CONFIG)\n+\t\tshould_run_hookdir = configured_hookdir_opt();\n+\n+\tcould_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||\n+\t\t\t\tshould_run_hookdir == HOOKDIR_WARN ||\n+\t\t\t\tshould_run_hookdir == HOOKDIR_YES)\n+\t\t\t\t&& !!find_hook(hookname);\n+\n+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n+\n+\treturn (!git_config_get_value(hook_key.buf, &value)) || could_run_hookdir;\n+}\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o)\n {\n \tstrvec_clear(&o->env);\ndiff --git a/hook.h b/hook.h\nindex fb5132305f..5f770b53ed 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -23,6 +23,7 @@ struct list_head* hook_list(const char *hookname);\n \n enum hookdir_opt\n {\n+\tHOOKDIR_USE_CONFIG,\n \tHOOKDIR_NO,\n \tHOOKDIR_ERROR,\n \tHOOKDIR_WARN,\n@@ -56,6 +57,15 @@ struct run_hooks_opt\n void run_hooks_opt_init(struct run_hooks_opt *o);\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n+/*\n+ * Returns 1 if any hooks are specified in the config or if a hook exists in the\n+ * hookdir. Typically, invoke hook_exsts() like:\n+ *   hook_exists(hookname, configured_hookdir_opt());\n+ * Like with run_hooks, if you take a --run-hookdir flag, reflect that\n+ * user-specified behavior here instead.\n+ */\n+int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);\n+\n /*\n  * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n  * passed 'env' and 'args'.\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425610","messageId":"20210527000856.695702-8-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 07/37] hook: add 'run' subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:26Z","receivedAt":"2021-05-27T00:09:33Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"In order to enable hooks to be run as an external process, by a\nstandalone Git command, or by tools which wrap Git, provide an external\nmeans to run all configured hook commands for a given hook event.\n\nFor now, the hook commands will run in config order, in series. As\nalternate ordering or parallelism is supported in the future, we should\nadd knobs to use those to the command line as well.\n\nAs with the legacy hook implementation, all stdout generated by hook\ncommands is redirected to stderr. Piping from stdin is not yet\nsupported.\n\nLegacy hooks (those present in $GITDIR/hooks) are run at the end of the\nexecution list. They can be disabled, or made to print warnings, or to\nprompt before running, with the 'hook.runHookDir' config.\n\nUsers may wish to provide hook commands like 'git config\nhook.pre-commit.command \"~/linter.sh --pre-commit\"'. To enable this,\nconfig-defined hooks are run in a shell. (Since hooks in $GITDIR/hooks\ncan't be specified with included arguments or paths which need expansion\nlike this, they are run without a shell instead.)\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Since v7, added support for \"error\" hook.runHookDir setting.\n    \n    Since v4, updated the docs, and did less local application of single\n    quotes. In order for hookdir hooks to run successfully with a space in\n    the path, though, they must not be run with 'sh -c'. So we can treat the\n    hookdir hooks specially, and warn users via doc about special\n    considerations for configured hooks with spaces in their path.\n\n Documentation/git-hook.txt    |  31 +++++++-\n builtin/hook.c                |  42 ++++++++++-\n hook.c                        | 137 +++++++++++++++++++++++++++++++++-\n hook.h                        |  26 ++++++-\n t/t1360-config-based-hooks.sh |  96 +++++++++++++++++++++++-\n 5 files changed, 320 insertions(+), 12 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex c84520cb38..8f96c347ea 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -9,11 +9,12 @@ SYNOPSIS\n --------\n [verse]\n 'git hook' list <hook-name>\n+'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>\n \n DESCRIPTION\n -----------\n-You can list configured hooks with this command. Later, you will be able to run,\n-add, and modify hooks with this command.\n+You can list and run configured hooks with this command. Later, you will be able\n+to add and modify hooks with this command.\n \n This command parses the default configuration files for sections `hook` and\n `hookcmd`. `hook` is used to describe the commands which will be run during a\n@@ -97,6 +98,32 @@ in the order they should be run, and print the config scope where the relevant\n `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n This output is human-readable and the format is subject to change over time.\n \n+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::\n+\n+Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n+hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n+paths containing special characters or spaces should be wrapped in single\n+quotes: `command = '/my/path with spaces/script.sh' some args`.\n+\n+OPTIONS\n+-------\n+--run-hookdir::\n+\tOverrides the hook.runHookDir config. Must be 'yes', 'warn',\n+\t'interactive', or 'no'. Specifies how to handle hooks located in the Git\n+\thook directory (core.hooksPath).\n+\n+-a::\n+--arg::\n+\tOnly valid for `run`.\n++\n+Specify arguments to pass to every hook that is run.\n+\n+-e::\n+--env::\n+\tOnly valid for `run`.\n++\n+Specify environment variables to set for every hook that is run.\n+\n CONFIGURATION\n -------------\n include::config/hook.txt[]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex b1e63a9576..4673c9091c 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -4,9 +4,11 @@\n #include \"hook.h\"\n #include \"parse-options.h\"\n #include \"strbuf.h\"\n+#include \"strvec.h\"\n \n static const char * const builtin_hook_usage[] = {\n \tN_(\"git hook list <hookname>\"),\n+\tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hookname>\"),\n \tNULL\n };\n \n@@ -98,6 +100,40 @@ static int list(int argc, const char **argv, const char *prefix)\n \treturn 0;\n }\n \n+static int run(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct strbuf hookname = STRBUF_INIT;\n+\tstruct run_hooks_opt opt;\n+\tint rc = 0;\n+\n+\tstruct option run_options[] = {\n+\t\tOPT_STRVEC('e', \"env\", &opt.env, N_(\"var\"),\n+\t\t\t   N_(\"environment variables for hook to use\")),\n+\t\tOPT_STRVEC('a', \"arg\", &opt.args, N_(\"args\"),\n+\t\t\t   N_(\"argument to pass to hook\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\trun_hooks_opt_init(&opt);\n+\n+\targc = parse_options(argc, argv, prefix, run_options,\n+\t\t\t     builtin_hook_usage, 0);\n+\n+\tif (argc < 1)\n+\t\tusage_msg_opt(_(\"You must specify a hook event to run.\"),\n+\t\t\t      builtin_hook_usage, run_options);\n+\n+\tstrbuf_addstr(&hookname, argv[0]);\n+\topt.run_hookdir = should_run_hookdir;\n+\n+\trc = run_hooks(hookname.buf, &opt);\n+\n+\tstrbuf_release(&hookname);\n+\trun_hooks_opt_clear(&opt);\n+\n+\treturn rc;\n+}\n+\n int cmd_hook(int argc, const char **argv, const char *prefix)\n {\n \tconst char *run_hookdir = NULL;\n@@ -109,10 +145,10 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \t};\n \n \targc = parse_options(argc, argv, prefix, builtin_hook_options,\n-\t\t\t     builtin_hook_usage, 0);\n+\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN);\n \n \t/* after the parse, we should have \"<command> <hookname> <args...>\" */\n-\tif (argc < 1)\n+\tif (argc < 2)\n \t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n \n \tgit_config(git_default_config, NULL);\n@@ -142,6 +178,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \n \tif (!strcmp(argv[0], \"list\"))\n \t\treturn list(argc, argv, prefix);\n+\tif (!strcmp(argv[0], \"run\"))\n+\t\treturn run(argc, argv, prefix);\n \n \tusage_with_options(builtin_hook_usage, builtin_hook_options);\n }\ndiff --git a/hook.c b/hook.c\nindex 65cbad8dba..b631da659b 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -3,13 +3,13 @@\n #include \"hook.h\"\n #include \"config.h\"\n #include \"run-command.h\"\n+#include \"prompt.h\"\n \n void free_hook(struct hook *ptr)\n {\n-\tif (ptr) {\n+\tif (ptr)\n \t\tstrbuf_release(&ptr->command);\n-\t\tfree(ptr);\n-\t}\n+\tfree(ptr);\n }\n \n static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n@@ -143,6 +143,70 @@ enum hookdir_opt configured_hookdir_opt(void)\n \treturn HOOKDIR_UNKNOWN;\n }\n \n+static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n+{\n+\tstruct strbuf prompt = STRBUF_INIT;\n+\t/*\n+\t * If the path doesn't exist, don't bother adding the empty hook and\n+\t * don't bother checking the config or prompting the user.\n+\t */\n+\tif (!path)\n+\t\treturn 0;\n+\n+\tswitch (cfg)\n+\t{\n+\tcase HOOKDIR_ERROR:\n+\t\tfprintf(stderr, _(\"Skipping legacy hook at '%s'\\n\"),\n+\t\t\tpath);\n+\t\t/* FALLTHROUGH */\n+\tcase HOOKDIR_NO:\n+\t\treturn 0;\n+\tcase HOOKDIR_WARN:\n+\t\tfprintf(stderr, _(\"Running legacy hook at '%s'\\n\"),\n+\t\t\tpath);\n+\t\treturn 1;\n+\tcase HOOKDIR_INTERACTIVE:\n+\t\tdo {\n+\t\t\t/*\n+\t\t\t * TRANSLATORS: Make sure to include [Y] and [n]\n+\t\t\t * in your translation. Only English input is\n+\t\t\t * accepted. Default option is \"yes\".\n+\t\t\t */\n+\t\t\tfprintf(stderr, _(\"Run '%s'? [Y/n] \"), path);\n+\t\t\tgit_read_line_interactively(&prompt);\n+\t\t\t/*\n+\t\t\t * In case of prompt = '' - that is, user hit enter,\n+\t\t\t * saying \"yes I want the default\" - strncasecmp will\n+\t\t\t * return 0 regardless. So list the default first.\n+\t\t\t *\n+\t\t\t * Case insensitively, accept \"y\", \"ye\", or \"yes\" as\n+\t\t\t * \"yes\"; accept \"n\" or \"no\" as \"no\".\n+\t\t\t */\n+\t\t\tif (!strncasecmp(prompt.buf, \"yes\", prompt.len)) {\n+\t\t\t\tstrbuf_release(&prompt);\n+\t\t\t\treturn 1;\n+\t\t\t} else if (!strncasecmp(prompt.buf, \"no\", prompt.len)) {\n+\t\t\t\tstrbuf_release(&prompt);\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\t\t\t/* otherwise, we didn't understand the input */\n+\t\t} while (prompt.len); /* an empty reply means default (yes) */\n+\t\treturn 1;\n+\t/*\n+\t * HOOKDIR_UNKNOWN should match the default behavior, but let's\n+\t * give a heads up to the user.\n+\t */\n+\tcase HOOKDIR_UNKNOWN:\n+\t\tfprintf(stderr,\n+\t\t\t_(\"Unrecognized value for 'hook.runHookDir'. \"\n+\t\t\t  \"Is there a typo? \"));\n+\t\t/* FALLTHROUGH */\n+\tcase HOOKDIR_YES:\n+\tdefault:\n+\t\treturn 1;\n+\t}\n+}\n+\n struct list_head* hook_list(const char* hookname)\n {\n \tstruct strbuf hook_key = STRBUF_INIT;\n@@ -176,3 +240,70 @@ struct list_head* hook_list(const char* hookname)\n \tstrbuf_release(&hook_key);\n \treturn hook_head;\n }\n+\n+void run_hooks_opt_init(struct run_hooks_opt *o)\n+{\n+\tstrvec_init(&o->env);\n+\tstrvec_init(&o->args);\n+\to->run_hookdir = configured_hookdir_opt();\n+}\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o)\n+{\n+\tstrvec_clear(&o->env);\n+\tstrvec_clear(&o->args);\n+}\n+\n+static void prepare_hook_cp(const char *hookname, struct hook *hook,\n+\t\t\t    struct run_hooks_opt *options,\n+\t\t\t    struct child_process *cp)\n+{\n+\tif (!hook)\n+\t\treturn;\n+\n+\tcp->no_stdin = 1;\n+\tcp->env = options->env.v;\n+\tcp->stdout_to_stderr = 1;\n+\tcp->trace2_hook_name = hookname;\n+\n+\t/*\n+\t * Commands from the config could be oneliners, but we know\n+\t * for certain that hookdir commands are not.\n+\t */\n+\tcp->use_shell = !hook->from_hookdir;\n+\n+\t/* add command */\n+\tstrvec_push(&cp->args, hook->command.buf);\n+\n+\t/*\n+\t * add passed-in argv, without expanding - let the user get back\n+\t * exactly what they put in\n+\t */\n+\tstrvec_pushv(&cp->args, options->args.v);\n+}\n+\n+int run_hooks(const char *hookname, struct run_hooks_opt *options)\n+{\n+\tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n+\tint rc = 0;\n+\n+\tif (!options)\n+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n+\n+\tto_run = hook_list(hookname);\n+\n+\tlist_for_each_safe(pos, tmp, to_run) {\n+\t\tstruct child_process hook_proc = CHILD_PROCESS_INIT;\n+\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n+\n+\t\tif (hook->from_hookdir &&\n+\t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n+\t\t\tcontinue;\n+\n+\t\tprepare_hook_cp(hookname, hook, options, &hook_proc);\n+\n+\t\trc |= run_command(&hook_proc);\n+\t}\n+\n+\treturn rc;\n+}\ndiff --git a/hook.h b/hook.h\nindex 7f2b2ee8f2..fb5132305f 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,6 +1,7 @@\n #include \"config.h\"\n #include \"list.h\"\n #include \"strbuf.h\"\n+#include \"strvec.h\"\n \n struct hook {\n \tstruct list_head list;\n@@ -36,11 +37,30 @@ enum hookdir_opt\n  */\n enum hookdir_opt configured_hookdir_opt(void);\n \n+struct run_hooks_opt\n+{\n+\t/* Environment vars to be set for each hook */\n+\tstruct strvec env;\n+\n+\t/* Args to be passed to each hook */\n+\tstruct strvec args;\n+\n+\t/*\n+\t * How should the hookdir be handled?\n+\t * Leave the RUN_HOOKS_OPT_INIT default in most cases; this only needs\n+\t * to be overridden if the user can override it at the command line.\n+\t */\n+\tenum hookdir_opt run_hookdir;\n+};\n+\n+void run_hooks_opt_init(struct run_hooks_opt *o);\n+void run_hooks_opt_clear(struct run_hooks_opt *o);\n+\n /*\n- * Provides the hookdir_opt specified in the config without consulting any\n- * command line arguments.\n+ * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n+ * passed 'env' and 'args'.\n  */\n-enum hookdir_opt configured_hookdir_opt(void);\n+int run_hooks(const char *hookname, struct run_hooks_opt *options);\n \n /* Free memory associated with a 'struct hook' */\n void free_hook(struct hook *ptr);\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 33ac27aa97..3dddd41e4f 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -115,7 +115,10 @@ test_expect_success 'hook.runHookDir = no is respected by list' '\n \n \tgit hook list pre-commit >actual &&\n \t# the hookdir annotation is translated\n-\ttest_cmp expected actual\n+\ttest_cmp expected actual &&\n+\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_must_be_empty actual\n '\n \n test_expect_success 'hook.runHookDir = error is respected by list' '\n@@ -129,6 +132,13 @@ test_expect_success 'hook.runHookDir = error is respected by list' '\n \n \tgit hook list pre-commit >actual &&\n \t# the hookdir annotation is translated\n+\ttest_cmp expected actual &&\n+\n+\tcat >expected <<-EOF &&\n+\tSkipping legacy hook at '\\''$(pwd)/.git/hooks/pre-commit'\\''\n+\tEOF\n+\n+\tgit hook run pre-commit 2>actual &&\n \ttest_cmp expected actual\n '\n \n@@ -143,6 +153,14 @@ test_expect_success 'hook.runHookDir = warn is respected by list' '\n \n \tgit hook list pre-commit >actual &&\n \t# the hookdir annotation is translated\n+\ttest_cmp expected actual &&\n+\n+\tcat >expected <<-EOF &&\n+\tRunning legacy hook at '\\''$(pwd)/.git/hooks/pre-commit'\\''\n+\t\"Legacy Hook\"\n+\tEOF\n+\n+\tgit hook run pre-commit 2>actual &&\n \ttest_cmp expected actual\n '\n \n@@ -182,7 +200,7 @@ test_expect_success 'git hook list removes skipped inlined hook' '\n \ttest_cmp expected actual\n '\n \n-test_expect_success 'hook.runHookDir = interactive is respected by list' '\n+test_expect_success 'hook.runHookDir = interactive is respected by list and run' '\n \tsetup_hookdir &&\n \n \ttest_config hook.runHookDir \"interactive\" &&\n@@ -193,9 +211,83 @@ test_expect_success 'hook.runHookDir = interactive is respected by list' '\n \n \tgit hook list pre-commit >actual &&\n \t# the hookdir annotation is translated\n+\ttest_cmp expected actual &&\n+\n+\ttest_write_lines n | git hook run pre-commit 2>actual &&\n+\t! grep \"Legacy Hook\" actual &&\n+\n+\ttest_write_lines y | git hook run pre-commit 2>actual &&\n+\tgrep \"Legacy Hook\" actual\n+'\n+\n+test_expect_success 'inline hook definitions execute oneliners' '\n+\ttest_config hook.pre-commit.command \"echo \\\"Hello World\\\"\" &&\n+\n+\techo \"Hello World\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions resolve paths' '\n+\twrite_script sample-hook.sh <<-EOF &&\n+\techo \\\"Sample Hook\\\"\n+\tEOF\n+\n+\ttest_when_finished \"rm sample-hook.sh\" &&\n+\n+\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\techo \\\"Sample Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook run can pass args and env vars' '\n+\twrite_script sample-hook.sh <<-\\EOF &&\n+\techo $1\n+\techo $2\n+\techo $TEST_ENV_1\n+\techo $TEST_ENV_2\n+\tEOF\n+\n+\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\tcat >expected <<-EOF &&\n+\targ1\n+\targ2\n+\tenv1\n+\tenv2\n+\tEOF\n+\n+\tgit hook run --arg arg1 \\\n+\t\t--env TEST_ENV_1=env1 \\\n+\t\t-a arg2 \\\n+\t\t-e TEST_ENV_2=env2 \\\n+\t\tpre-commit 2>actual &&\n+\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'hookdir hook included in git hook run' '\n+\tsetup_hookdir &&\n+\n+\techo \\\"Legacy Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'out-of-repo runs excluded' '\n+\tsetup_hooks &&\n+\n+\tnongit test_must_fail git hook run pre-commit\n+'\n+\n test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n \tsetup_hookdir &&\n \n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425611","messageId":"20210527000856.695702-10-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 09/37] hook: support passing stdin to hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:28Z","receivedAt":"2021-05-27T00:09:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Some hooks (such as post-rewrite) need to take input via stdin.\nPreviously, callers provided stdin to hooks by setting\nrun-command.h:child_process.in, which takes a FD. Callers would open the\nfile in question themselves before calling run-command(). However, since\nwe will now need to seek to the front of the file and read it again for\nevery hook which runs, hook.h:run_command() takes a path and handles FD\nmanagement itself. Since this file is opened for read only, it should\nnot prevent later parallel execution support.\n\nOn the frontend, this is supported by asking for a file path, rather\nthan by reading stdin. Reading directly from stdin would involve caching\nthe entire stdin (to memory or to disk) and reading it back from the\nbeginning to each hook. We'd want to support cases like insufficient\nmemory or storage for the file. While this may prove useful later, for\nnow the path of least resistance is to just ask the user to make this\ninterim file themselves.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt    | 11 +++++++++--\n builtin/hook.c                |  5 ++++-\n hook.c                        |  8 +++++++-\n hook.h                        |  6 +++++-\n t/t1360-config-based-hooks.sh | 24 ++++++++++++++++++++++++\n 5 files changed, 49 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 8f96c347ea..96a857c682 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -9,7 +9,8 @@ SYNOPSIS\n --------\n [verse]\n 'git hook' list <hook-name>\n-'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>\n+'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>]\n+\t<hook-name>\n \n DESCRIPTION\n -----------\n@@ -98,7 +99,7 @@ in the order they should be run, and print the config scope where the relevant\n `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n This output is human-readable and the format is subject to change over time.\n \n-run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::\n+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] `<hook-name>`::\n \n Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n@@ -124,6 +125,12 @@ Specify arguments to pass to every hook that is run.\n +\n Specify environment variables to set for every hook that is run.\n \n+--to-stdin::\n+\tOnly valid for `run`.\n++\n+Specify a file which will be streamed into stdin for every hook that is run.\n+Each hook will receive the entire file from beginning to EOF.\n+\n CONFIGURATION\n -------------\n include::config/hook.txt[]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 4673c9091c..0d9c052e84 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -8,7 +8,8 @@\n \n static const char * const builtin_hook_usage[] = {\n \tN_(\"git hook list <hookname>\"),\n-\tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hookname>\"),\n+\tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...]\"\n+\t   \"[--to-stdin=<path>] <hookname>\"),\n \tNULL\n };\n \n@@ -111,6 +112,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \t\t\t   N_(\"environment variables for hook to use\")),\n \t\tOPT_STRVEC('a', \"arg\", &opt.args, N_(\"args\"),\n \t\t\t   N_(\"argument to pass to hook\")),\n+\t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n+\t\t\t   N_(\"file to read into hooks' stdin\")),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/hook.c b/hook.c\nindex 008167dbe5..a2eda57fb9 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -245,6 +245,7 @@ void run_hooks_opt_init(struct run_hooks_opt *o)\n {\n \tstrvec_init(&o->env);\n \tstrvec_init(&o->args);\n+\to->path_to_stdin = NULL;\n \to->run_hookdir = configured_hookdir_opt();\n }\n \n@@ -280,7 +281,12 @@ static void prepare_hook_cp(const char *hookname, struct hook *hook,\n \tif (!hook)\n \t\treturn;\n \n-\tcp->no_stdin = 1;\n+\t/* reopen the file for stdin; run_command closes it. */\n+\tif (options->path_to_stdin)\n+\t\tcp->in = xopen(options->path_to_stdin, O_RDONLY);\n+\telse\n+\t\tcp->no_stdin = 1;\n+\n \tcp->env = options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hookname;\ndiff --git a/hook.h b/hook.h\nindex 5f770b53ed..37147f4c5e 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -52,6 +52,9 @@ struct run_hooks_opt\n \t * to be overridden if the user can override it at the command line.\n \t */\n \tenum hookdir_opt run_hookdir;\n+\n+\t/* Path to file which should be piped to stdin for each hook */\n+\tconst char *path_to_stdin;\n };\n \n void run_hooks_opt_init(struct run_hooks_opt *o);\n@@ -68,7 +71,8 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);\n \n /*\n  * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n- * passed 'env' and 'args'.\n+ * passed 'env' and 'args'. The file at 'stdin_path' will be closed and reopened\n+ * for each hook that runs.\n  */\n int run_hooks(const char *hookname, struct run_hooks_opt *options);\n \ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 3dddd41e4f..43917172d7 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -302,4 +302,28 @@ test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'stdin to multiple hooks' '\n+\tgit config --add hook.test.command \"xargs -P1 -I% echo a%\" &&\n+\tgit config --add hook.test.command \"xargs -P1 -I% echo b%\" &&\n+\ttest_when_finished \"test_unconfig hook.test.command\" &&\n+\n+\tcat >input <<-EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tcat >expected <<-EOF &&\n+\ta1\n+\ta2\n+\ta3\n+\tb1\n+\tb2\n+\tb3\n+\tEOF\n+\n+\tgit hook run --to-stdin=input test 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425612","messageId":"20210527000856.695702-11-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 10/37] run-command: allow stdin for run_processes_parallel","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:29Z","receivedAt":"2021-05-27T00:09:41Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"While it makes sense not to inherit stdin from the parent process to\navoid deadlocking, it's not necessary to completely ban stdin to\nchildren. An informed user should be able to configure stdin safely. By\nsetting `some_child.process.no_stdin=1` before calling `get_next_task()`\nwe provide a reasonable default behavior but enable users to set up\nstdin streaming for themselves during the callback.\n\n`some_child.process.stdout_to_stderr`, however, remains unmodifiable by\n`get_next_task()` - the rest of the run_processes_parallel() API depends\non child output in stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n run-command.c | 9 ++++++++-\n 1 file changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex be6bc128cd..5237984ebe 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1693,6 +1693,14 @@ static int pp_start_one(struct parallel_processes *pp)\n \tif (i == pp->max_processes)\n \t\tBUG(\"bookkeeping is hard\");\n \n+\t/*\n+\t * By default, do not inherit stdin from the parent process - otherwise,\n+\t * all children would share stdin! Users may overwrite this to provide\n+\t * something to the child's stdin by having their 'get_next_task'\n+\t * callback assign 0 to .no_stdin and an appropriate integer to .in.\n+\t */\n+\tpp->children[i].process.no_stdin = 1;\n+\n \tcode = pp->get_next_task(&pp->children[i].process,\n \t\t\t\t &pp->children[i].err,\n \t\t\t\t pp->data,\n@@ -1704,7 +1712,6 @@ static int pp_start_one(struct parallel_processes *pp)\n \t}\n \tpp->children[i].process.err = -1;\n \tpp->children[i].process.stdout_to_stderr = 1;\n-\tpp->children[i].process.no_stdin = 1;\n \n \tif (start_command(&pp->children[i].process)) {\n \t\tcode = pp->start_failure(&pp->children[i].err,\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425613","messageId":"20210527000856.695702-12-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 11/37] hook: allow parallel hook execution","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:30Z","receivedAt":"2021-05-27T00:09:48Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"In many cases, there's no reason not to allow hooks to execute in\nparallel. run_processes_parallel() is well-suited - it's a task queue\nthat runs its housekeeping in series, which means users don't\nneed to worry about thread safety on their callback data. True\nmultithreaded execution with the async_* functions isn't necessary here.\nSynchronous hook execution can be achieved by only allowing 1 job to run\nat a time.\n\nTeach run_hooks() to use that function for simple hooks which don't\nrequire stdin or capture of stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Per AEvar's request - parallel hook execution on day zero.\n    \n    In most ways run_processes_parallel() worked great for me - but it didn't\n    have great support for hooks where we pipe to and from. I had to add this\n    support later in the series.\n    \n    Since I modified an existing and in-use library I'd appreciate a keen look on\n    these patches.\n    \n     - Emily\n\n Documentation/config/hook.txt |   5 ++\n Documentation/git-hook.txt    |  14 ++++-\n builtin/hook.c                |   6 +-\n hook.c                        | 111 ++++++++++++++++++++++++++++------\n hook.h                        |  22 ++++++-\n 5 files changed, 134 insertions(+), 24 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 8b12512e33..4f66bb35cf 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -20,3 +20,8 @@ hook.runHookDir::\n \tControls how hooks contained in your hookdir are executed. Can be any of\n \t\"yes\", \"warn\", \"interactive\", or \"no\". Defaults to \"yes\". See\n \tlinkgit:git-hook[1] and linkgit:git-config[1] \"core.hooksPath\").\n+\n+hook.jobs::\n+\tSpecifies how many hooks can be run simultaneously during parallelized\n+\thook execution. If unspecified, defaults to the number of processors on\n+\tthe current system.\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 96a857c682..81b8e94994 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -10,7 +10,7 @@ SYNOPSIS\n [verse]\n 'git hook' list <hook-name>\n 'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>]\n-\t<hook-name>\n+\t[(-j|--jobs) <n>] <hook-name>\n \n DESCRIPTION\n -----------\n@@ -99,7 +99,7 @@ in the order they should be run, and print the config scope where the relevant\n `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n This output is human-readable and the format is subject to change over time.\n \n-run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] `<hook-name>`::\n+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] [(-j|--jobs)<n>] `<hook-name>`::\n \n Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n@@ -131,6 +131,16 @@ Specify environment variables to set for every hook that is run.\n Specify a file which will be streamed into stdin for every hook that is run.\n Each hook will receive the entire file from beginning to EOF.\n \n+-j::\n+--jobs::\n+\tOnly valid for `run`.\n++\n+Specify how many hooks to run simultaneously. If this flag is not specified, use\n+the value of the `hook.jobs` config. If the config is not specified, use the\n+number of CPUs on the current system. Some hooks may be ineligible for\n+parallelization: for example, 'commit-msg' intends hooks modify the commit\n+message body and cannot be parallelized.\n+\n CONFIGURATION\n -------------\n include::config/hook.txt[]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 0d9c052e84..c79a961e80 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -9,7 +9,7 @@\n static const char * const builtin_hook_usage[] = {\n \tN_(\"git hook list <hookname>\"),\n \tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...]\"\n-\t   \"[--to-stdin=<path>] <hookname>\"),\n+\t   \"[--to-stdin=<path>] [(-j|--jobs) <count>] <hookname>\"),\n \tNULL\n };\n \n@@ -114,10 +114,12 @@ static int run(int argc, const char **argv, const char *prefix)\n \t\t\t   N_(\"argument to pass to hook\")),\n \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n \t\t\t   N_(\"file to read into hooks' stdin\")),\n+\t\tOPT_INTEGER('j', \"jobs\", &opt.jobs,\n+\t\t\t    N_(\"run up to <n> hooks simultaneously\")),\n \t\tOPT_END(),\n \t};\n \n-\trun_hooks_opt_init(&opt);\n+\trun_hooks_opt_init_async(&opt);\n \n \targc = parse_options(argc, argv, prefix, run_options,\n \t\t\t     builtin_hook_usage, 0);\ndiff --git a/hook.c b/hook.c\nindex a2eda57fb9..784529fe2b 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -143,6 +143,14 @@ enum hookdir_opt configured_hookdir_opt(void)\n \treturn HOOKDIR_UNKNOWN;\n }\n \n+int configured_hook_jobs(void)\n+{\n+\tint n = online_cpus();\n+\tgit_config_get_int(\"hook.jobs\", &n);\n+\n+\treturn n;\n+}\n+\n static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n {\n \tstruct strbuf prompt = STRBUF_INIT;\n@@ -241,12 +249,19 @@ struct list_head* hook_list(const char* hookname)\n \treturn hook_head;\n }\n \n-void run_hooks_opt_init(struct run_hooks_opt *o)\n+void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n {\n \tstrvec_init(&o->env);\n \tstrvec_init(&o->args);\n \to->path_to_stdin = NULL;\n \to->run_hookdir = configured_hookdir_opt();\n+\to->jobs = 1;\n+}\n+\n+void run_hooks_opt_init_async(struct run_hooks_opt *o)\n+{\n+\trun_hooks_opt_init_sync(o);\n+\to->jobs = configured_hook_jobs();\n }\n \n int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n@@ -274,22 +289,28 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)\n \tstrvec_clear(&o->args);\n }\n \n-static void prepare_hook_cp(const char *hookname, struct hook *hook,\n-\t\t\t    struct run_hooks_opt *options,\n-\t\t\t    struct child_process *cp)\n+static int pick_next_hook(struct child_process *cp,\n+\t\t\t  struct strbuf *out,\n+\t\t\t  void *pp_cb,\n+\t\t\t  void **pp_task_cb)\n {\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *hook = hook_cb->run_me;\n+\n \tif (!hook)\n-\t\treturn;\n+\t\treturn 0;\n \n \t/* reopen the file for stdin; run_command closes it. */\n-\tif (options->path_to_stdin)\n-\t\tcp->in = xopen(options->path_to_stdin, O_RDONLY);\n-\telse\n+\tif (hook_cb->options->path_to_stdin) {\n+\t\tcp->no_stdin = 0;\n+\t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else {\n \t\tcp->no_stdin = 1;\n+\t}\n \n-\tcp->env = options->env.v;\n+\tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n-\tcp->trace2_hook_name = hookname;\n+\tcp->trace2_hook_name = hook_cb->hookname;\n \n \t/*\n \t * Commands from the config could be oneliners, but we know\n@@ -304,13 +325,58 @@ static void prepare_hook_cp(const char *hookname, struct hook *hook,\n \t * add passed-in argv, without expanding - let the user get back\n \t * exactly what they put in\n \t */\n-\tstrvec_pushv(&cp->args, options->args.v);\n+\tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n+\n+\t/* Provide context for errors if necessary */\n+\t*pp_task_cb = hook;\n+\n+\t/* Get the next entry ready */\n+\tif (hook_cb->run_me->list.next == hook_cb->head)\n+\t\thook_cb->run_me = NULL;\n+\telse\n+\t\thook_cb->run_me = list_entry(hook_cb->run_me->list.next,\n+\t\t\t\t\t     struct hook, list);\n+\n+\treturn 1;\n+}\n+\n+static int notify_start_failure(struct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cp)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *attempted = pp_task_cp;\n+\n+\t/* |= rc in cb */\n+\thook_cb->rc |= 1;\n+\n+\tstrbuf_addf(out, _(\"Couldn't start '%s', configured in '%s'\\n\"),\n+\t\t    attempted->command.buf,\n+\t\t    attempted->from_hookdir ? \"hookdir\"\n+\t\t\t: config_scope_name(attempted->origin));\n+\n+\t/* NEEDSWORK: if halt_on_error is desired, do it here. */\n+\treturn 0;\n+}\n+\n+static int notify_hook_finished(int result,\n+\t\t\t\tstruct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\n+\t/* |= rc in cb */\n+\thook_cb->rc |= result;\n+\n+\t/* NEEDSWORK: if halt_on_error is desired, do it here. */\n+\treturn 0;\n }\n \n int run_hooks(const char *hookname, struct run_hooks_opt *options)\n {\n \tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n-\tint rc = 0;\n+\tstruct hook_cb_data cb_data = { 0, hookname, NULL, NULL, options };\n \n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n@@ -318,17 +384,26 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \tto_run = hook_list(hookname);\n \n \tlist_for_each_safe(pos, tmp, to_run) {\n-\t\tstruct child_process hook_proc = CHILD_PROCESS_INIT;\n \t\tstruct hook *hook = list_entry(pos, struct hook, list);\n \n \t\tif (hook->from_hookdir &&\n \t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n-\t\t\tcontinue;\n+\t\t\t    list_del(pos);\n+\t}\n+\n+\tif (list_empty(to_run))\n+\t\treturn 0;\n \n-\t\tprepare_hook_cp(hookname, hook, options, &hook_proc);\n+\tcb_data.head = to_run;\n+\tcb_data.run_me = list_entry(to_run->next, struct hook, list);\n \n-\t\trc |= run_command(&hook_proc);\n-\t}\n+\trun_processes_parallel_tr2(options->jobs,\n+\t\t\t\t   pick_next_hook,\n+\t\t\t\t   notify_start_failure,\n+\t\t\t\t   notify_hook_finished,\n+\t\t\t\t   &cb_data,\n+\t\t\t\t   \"hook\",\n+\t\t\t\t   hookname);\n \n-\treturn rc;\n+\treturn cb_data.rc;\n }\ndiff --git a/hook.h b/hook.h\nindex 37147f4c5e..cdb1ac5510 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -38,6 +38,9 @@ enum hookdir_opt\n  */\n enum hookdir_opt configured_hookdir_opt(void);\n \n+/* Provides the number of threads to use for parallel hook execution. */\n+int configured_hook_jobs(void);\n+\n struct run_hooks_opt\n {\n \t/* Environment vars to be set for each hook */\n@@ -48,16 +51,31 @@ struct run_hooks_opt\n \n \t/*\n \t * How should the hookdir be handled?\n-\t * Leave the RUN_HOOKS_OPT_INIT default in most cases; this only needs\n+\t * Leave the run_hooks_opt_init_*() default in most cases; this only needs\n \t * to be overridden if the user can override it at the command line.\n \t */\n \tenum hookdir_opt run_hookdir;\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n+\n+\t/* Number of threads to parallelize across */\n+\tint jobs;\n+};\n+\n+/*\n+ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n+ */\n+struct hook_cb_data {\n+\tint rc;\n+\tconst char *hookname;\n+\tstruct list_head *head;\n+\tstruct hook *run_me;\n+\tstruct run_hooks_opt *options;\n };\n \n-void run_hooks_opt_init(struct run_hooks_opt *o);\n+void run_hooks_opt_init_sync(struct run_hooks_opt *o);\n+void run_hooks_opt_init_async(struct run_hooks_opt *o);\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /*\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425614","messageId":"20210527000856.695702-13-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 12/37] hook: allow specifying working directory for hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:31Z","receivedAt":"2021-05-27T00:09:48Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Hooks like \"post-checkout\" require that hooks have a different working\ndirectory than the initial process. Pipe that directly through to struct\nchild_process.\n\nBecause we can just run 'git -C <some-dir> hook run ...' it shouldn't be\nnecessary to pipe this option through the frontend. In fact, this\nreduces the possibility of users running hooks which affect some part of\nthe filesystem outside of the repo in question.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Needed later for \"post-checkout\" conversion.\n\n hook.c | 2 ++\n hook.h | 3 +++\n 2 files changed, 5 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex 784529fe2b..920563607d 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -256,6 +256,7 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n \to->path_to_stdin = NULL;\n \to->run_hookdir = configured_hookdir_opt();\n \to->jobs = 1;\n+\to->dir = NULL;\n }\n \n void run_hooks_opt_init_async(struct run_hooks_opt *o)\n@@ -311,6 +312,7 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hookname;\n+\tcp->dir = hook_cb->options->dir;\n \n \t/*\n \t * Commands from the config could be oneliners, but we know\ndiff --git a/hook.h b/hook.h\nindex cdb1ac5510..218b9e1721 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -61,6 +61,9 @@ struct run_hooks_opt\n \n \t/* Number of threads to parallelize across */\n \tint jobs;\n+\n+\t/* Path to initial working directory for subprocess */\n+\tconst char *dir;\n };\n \n /*\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425615","messageId":"20210527000856.695702-14-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 13/37] run-command: add stdin callback for parallelization","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:32Z","receivedAt":"2021-05-27T00:09:49Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"If a user of the run_processes_parallel() API wants to pipe a large\namount of information to stdin of each parallel command, that\ninformation could exceed the buffer of the pipe allocated for that\nprocess's stdin.  Generally this is solved by repeatedly writing to\nchild_process.in between calls to start_command() and finish_command();\nrun_processes_parallel() did not provide users an opportunity to access\nchild_process at that time.\n\nBecause the data might be extremely large (for example, a list of all\nrefs received during a push from a client) simply taking a string_list\nor strbuf is not as scalable as using a callback; the rest of the\nrun_processes_parallel() API also uses callbacks, so making this feature\nmatch the rest of the API reduces mental load on the user.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/fetch.c             |  1 +\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 54 +++++++++++++++++++++++++++++++++++--\n run-command.h               | 17 +++++++++++-\n submodule.c                 |  1 +\n t/helper/test-run-command.c | 31 ++++++++++++++++++---\n t/t0061-run-command.sh      | 30 +++++++++++++++++++++\n 8 files changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex dfde96a435..a07816b650 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,6 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n+\t\t\t\t\t\t    NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex d55f6262e9..8d1e731073 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2295,7 +2295,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure,\n+\t\t\t\t   update_clone_start_failure, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex 920563607d..1fa7976583 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -402,6 +402,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 5237984ebe..6dd33caa57 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1558,6 +1558,7 @@ struct parallel_processes {\n \n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n+\tfeed_pipe_fn feed_pipe;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1585,6 +1586,13 @@ static int default_start_failure(struct strbuf *out,\n \treturn 0;\n }\n \n+static int default_feed_pipe(struct strbuf *pipe,\n+\t\t\t     void *pp_cb,\n+\t\t\t     void *pp_task_cb)\n+{\n+\treturn 1;\n+}\n+\n static int default_task_finished(int result,\n \t\t\t\t struct strbuf *out,\n \t\t\t\t void *pp_cb,\n@@ -1615,6 +1623,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    int n,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n+\t\t    feed_pipe_fn feed_pipe,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1633,6 +1642,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->get_next_task = get_next_task;\n \n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n+\tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n \n \tpp->nr_processes = 0;\n@@ -1730,6 +1740,37 @@ static int pp_start_one(struct parallel_processes *pp)\n \treturn 0;\n }\n \n+static void pp_buffer_stdin(struct parallel_processes *pp)\n+{\n+\tint i;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t/* Buffer stdin for each pipe. */\n+\tfor (i = 0; i < pp->max_processes; i++) {\n+\t\tif (pp->children[i].state == GIT_CP_WORKING &&\n+\t\t    pp->children[i].process.in > 0) {\n+\t\t\tint done;\n+\t\t\tstrbuf_reset(&sb);\n+\t\t\tdone = pp->feed_pipe(&sb, pp->data,\n+\t\t\t\t\t      pp->children[i].data);\n+\t\t\tif (sb.len) {\n+\t\t\t\tif (write_in_full(pp->children[i].process.in,\n+\t\t\t\t\t      sb.buf, sb.len) < 0) {\n+\t\t\t\t\tif (errno != EPIPE)\n+\t\t\t\t\t\tdie_errno(\"write\");\n+\t\t\t\t\tdone = 1;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif (done) {\n+\t\t\t\tclose(pp->children[i].process.in);\n+\t\t\t\tpp->children[i].process.in = 0;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&sb);\n+}\n+\n static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n {\n \tint i;\n@@ -1794,6 +1835,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\tpp->nr_processes--;\n \t\tpp->children[i].state = GIT_CP_FREE;\n \t\tpp->pfd[i].fd = -1;\n+\t\tpp->children[i].process.in = 0;\n \t\tchild_process_init(&pp->children[i].process);\n \n \t\tif (i != pp->output_owner) {\n@@ -1827,6 +1869,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n+\t\t\t   feed_pipe_fn feed_pipe,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1835,7 +1878,9 @@ int run_processes_parallel(int n,\n \tint spawn_cap = 4;\n \tstruct parallel_processes pp;\n \n-\tpp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb);\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1852,6 +1897,7 @@ int run_processes_parallel(int n,\n \t\t}\n \t\tif (!pp.nr_processes)\n \t\t\tbreak;\n+\t\tpp_buffer_stdin(&pp);\n \t\tpp_buffer_stderr(&pp, output_timeout);\n \t\tpp_output(&pp);\n \t\tcode = pp_collect_finished(&pp);\n@@ -1863,11 +1909,15 @@ int run_processes_parallel(int n,\n \t}\n \n \tpp_cleanup(&pp);\n+\n+\tsigchain_pop(SIGPIPE);\n+\n \treturn 0;\n }\n \n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n+\t\t\t       feed_pipe_fn feed_pipe,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1877,7 +1927,7 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\ttask_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex d08414a92e..1e3cf0999f 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -443,6 +443,20 @@ typedef int (*start_failure_fn)(struct strbuf *out,\n \t\t\t\tvoid *pp_cb,\n \t\t\t\tvoid *pp_task_cb);\n \n+/**\n+ * This callback is called repeatedly on every child process who requests\n+ * start_command() to create a pipe by setting child_process.in < 0.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel, and\n+ * pp_task_cb is the callback cookie as passed into get_next_task_fn.\n+ * The contents of 'send' will be read into the pipe and passed to the pipe.\n+ *\n+ * Return nonzero to close the pipe.\n+ */\n+typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n+\t\t\t    void *pp_cb,\n+\t\t\t    void *pp_task_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -477,10 +491,11 @@ typedef int (*task_finished_fn)(int result,\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n+\t\t\t   feed_pipe_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex 0b1d9c1dde..ea026a8195 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1645,6 +1645,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 7ae03dc712..9348184d30 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,\n \t\treturn 0;\n \n \tstrvec_pushv(&cp->args, d->argv);\n+\tcp->in = d->in;\n+\tcp->no_stdin = d->no_stdin;\n \tstrbuf_addstr(err, \"preloaded output of a child\\n\");\n \tnumber_callbacks++;\n+\n+\t*task_cb = xmalloc(sizeof(int));\n+\t*(int*)(*task_cb) = 2;\n \treturn 1;\n }\n \n@@ -55,6 +60,17 @@ static int task_finished(int result,\n \treturn 1;\n }\n \n+static int test_stdin(struct strbuf *pipe, void *cb, void *task_cb)\n+{\n+\tint *lines_remaining = task_cb;\n+\n+\tif (*lines_remaining)\n+\t\tstrbuf_addf(pipe, \"sample stdin %d\\n\", --(*lines_remaining));\n+\n+\treturn !(*lines_remaining);\n+}\n+\n+\n struct testsuite {\n \tstruct string_list tests, failed;\n \tint next;\n@@ -185,7 +201,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_finished, &suite);\n+\t\t\t\t     test_stdin, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -413,15 +429,22 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\n+\tif (!strcmp(argv[1], \"run-command-stdin\")) {\n+\t\tproc.in = -1;\n+\t\tproc.no_stdin = 0;\n+\t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n+\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t}\n \n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 7d599675e3..87759482ad 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,36 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+cat >expect <<-EOF\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+EOF\n+\n+test_expect_success 'run_command listens to stdin' '\n+\twrite_script stdin-script <<-\\EOF &&\n+\techo \"listening for stdin:\"\n+\twhile read line; do\n+\t\techo \"$line\"\n+\tdone\n+\tEOF\n+\ttest-tool run-command run-command-stdin 2 ./stdin-script 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n asking for a quick stop\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425616","messageId":"20210527000856.695702-15-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 14/37] hook: provide stdin by string_list or callback","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:33Z","receivedAt":"2021-05-27T00:09:56Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"In cases where a hook requires only a small amount of information via\nstdin, it should be simple for users to provide a string_list alone. But\nin more complicated cases where the stdin is too large to hold in\nmemory, let's provide a callback the users can populate line after line\nwith instead.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n hook.c | 38 ++++++++++++++++++++++++++++++++++++--\n hook.h | 28 ++++++++++++++++++++++++++++\n 2 files changed, 64 insertions(+), 2 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 1fa7976583..8aa0d4fdf2 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -7,8 +7,10 @@\n \n void free_hook(struct hook *ptr)\n {\n-\tif (ptr)\n+\tif (ptr) {\n \t\tstrbuf_release(&ptr->command);\n+\t\tfree(ptr->feed_pipe_cb_data);\n+\t}\n \tfree(ptr);\n }\n \n@@ -38,6 +40,7 @@ static void append_or_move_hook(struct list_head *head, const char *command)\n \t\tstrbuf_init(&to_add->command, 0);\n \t\tstrbuf_addstr(&to_add->command, command);\n \t\tto_add->from_hookdir = 0;\n+\t\tto_add->feed_pipe_cb_data = NULL;\n \t}\n \n \t/* re-set the scope so we show where an override was specified */\n@@ -257,6 +260,8 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n \to->run_hookdir = configured_hookdir_opt();\n \to->jobs = 1;\n \to->dir = NULL;\n+\to->feed_pipe = NULL;\n+\to->feed_pipe_ctx = NULL;\n }\n \n void run_hooks_opt_init_async(struct run_hooks_opt *o)\n@@ -290,6 +295,28 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)\n \tstrvec_clear(&o->args);\n }\n \n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n+{\n+\tint *item_idx;\n+\tstruct hook *ctx = pp_task_cb;\n+\tstruct string_list *to_pipe = ((struct hook_cb_data*)pp_cb)->options->feed_pipe_ctx;\n+\n+\t/* Bootstrap the state manager if necessary. */\n+\tif (!ctx->feed_pipe_cb_data) {\n+\t\tctx->feed_pipe_cb_data = xmalloc(sizeof(unsigned int));\n+\t\t*(int*)ctx->feed_pipe_cb_data = 0;\n+\t}\n+\n+\titem_idx = ctx->feed_pipe_cb_data;\n+\n+\tif (*item_idx < to_pipe->nr) {\n+\t\tstrbuf_addf(pipe, \"%s\\n\", to_pipe->items[*item_idx].string);\n+\t\t(*item_idx)++;\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n static int pick_next_hook(struct child_process *cp,\n \t\t\t  struct strbuf *out,\n \t\t\t  void *pp_cb,\n@@ -305,6 +332,10 @@ static int pick_next_hook(struct child_process *cp,\n \tif (hook_cb->options->path_to_stdin) {\n \t\tcp->no_stdin = 0;\n \t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else if (hook_cb->options->feed_pipe) {\n+\t\t/* ask for start_command() to make a pipe for us */\n+\t\tcp->in = -1;\n+\t\tcp->no_stdin = 0;\n \t} else {\n \t\tcp->no_stdin = 1;\n \t}\n@@ -383,6 +414,9 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n+\tif (options->path_to_stdin && options->feed_pipe)\n+\t\tBUG(\"choose only one method to populate stdin\");\n+\n \tto_run = hook_list(hookname);\n \n \tlist_for_each_safe(pos, tmp, to_run) {\n@@ -402,7 +436,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->feed_pipe,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/hook.h b/hook.h\nindex 218b9e1721..aba62d4112 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -2,6 +2,7 @@\n #include \"list.h\"\n #include \"strbuf.h\"\n #include \"strvec.h\"\n+#include \"run-command.h\"\n \n struct hook {\n \tstruct list_head list;\n@@ -13,6 +14,12 @@ struct hook {\n \t/* The literal command to run. */\n \tstruct strbuf command;\n \tunsigned from_hookdir : 1;\n+\n+\t/*\n+\t * Use this to keep state for your feed_pipe_fn if you are using\n+\t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n+\t */\n+\tvoid *feed_pipe_cb_data;\n };\n \n /*\n@@ -58,14 +65,35 @@ struct run_hooks_opt\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n+\t/*\n+\t * Callback and state pointer to ask for more content to pipe to stdin.\n+\t * Will be called repeatedly, for each hook. See\n+\t * hook.c:pipe_from_stdin() for an example. Keep per-hook state in\n+\t * hook.feed_pipe_cb_data (per process). Keep initialization context in\n+\t * feed_pipe_ctx (shared by all processes).\n+\t *\n+\t * See 'pipe_from_string_list()' for info about how to specify a\n+\t * string_list as the stdin input instead of writing your own handler.\n+\t */\n+\tfeed_pipe_fn feed_pipe;\n+\tvoid *feed_pipe_ctx;\n \n \t/* Number of threads to parallelize across */\n \tint jobs;\n \n \t/* Path to initial working directory for subprocess */\n \tconst char *dir;\n+\n };\n \n+/*\n+ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n+ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n+ * This will pipe each string in the list to stdin, separated by newlines.  (Do\n+ * not inject your own newlines.)\n+ */\n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n+\n /*\n  * Callback provided to feed_pipe_fn and consume_sideband_fn.\n  */\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425617","messageId":"20210527000856.695702-16-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 15/37] run-command: allow capturing of collated output","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:34Z","receivedAt":"2021-05-27T00:09:58Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Some callers, for example server-side hooks which wish to relay hook\noutput to clients across a transport, want to capture what would\nnormally print to stderr and do something else with it. Allow that via a\ncallback.\n\nBy calling the callback regardless of whether there's output available,\nwe allow clients to send e.g. a keepalive if necessary.\n\nBecause we expose a strbuf, not a fd or FILE*, there's no need to create\na temporary pipe or similar - we can just skip the print to stderr and\ninstead hand it to the caller.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Originally when writing this patch I attempted to use a pipe in memory -\n    but managing its lifetime was actually pretty tricky, and I found I could\n    achieve the same thing with less code by doing it this way. Critique welcome,\n    including \"no, you really need to do it with a pipe\".\n\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 33 +++++++++++++++++++++++++--------\n run-command.h               | 18 +++++++++++++++++-\n submodule.c                 |  2 +-\n t/helper/test-run-command.c | 25 ++++++++++++++++++++-----\n t/t0061-run-command.sh      |  7 +++++++\n 8 files changed, 73 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex a07816b650..769af53ca4 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,7 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n-\t\t\t\t\t\t    NULL,\n+\t\t\t\t\t\t    NULL, NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 8d1e731073..fef8392e1d 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2295,7 +2295,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure, NULL,\n+\t\t\t\t   update_clone_start_failure, NULL, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex 8aa0d4fdf2..d48071bb1d 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -437,6 +437,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 6dd33caa57..27135defb8 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1559,6 +1559,7 @@ struct parallel_processes {\n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n \tfeed_pipe_fn feed_pipe;\n+\tconsume_sideband_fn consume_sideband;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1624,6 +1625,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n \t\t    feed_pipe_fn feed_pipe,\n+\t\t    consume_sideband_fn consume_sideband,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1644,6 +1646,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n \tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n+\tpp->consume_sideband = consume_sideband;\n \n \tpp->nr_processes = 0;\n \tpp->output_owner = 0;\n@@ -1680,7 +1683,10 @@ static void pp_cleanup(struct parallel_processes *pp)\n \t * When get_next_task added messages to the buffer in its last\n \t * iteration, the buffered output is non empty.\n \t */\n-\tstrbuf_write(&pp->buffered_output, stderr);\n+\tif (pp->consume_sideband)\n+\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\telse\n+\t\tstrbuf_write(&pp->buffered_output, stderr);\n \tstrbuf_release(&pp->buffered_output);\n \n \tsigchain_pop_common();\n@@ -1801,9 +1807,13 @@ static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n static void pp_output(struct parallel_processes *pp)\n {\n \tint i = pp->output_owner;\n+\n \tif (pp->children[i].state == GIT_CP_WORKING &&\n \t    pp->children[i].err.len) {\n-\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\tif (pp->consume_sideband)\n+\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\telse\n+\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n \t\tstrbuf_reset(&pp->children[i].err);\n \t}\n }\n@@ -1842,11 +1852,15 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\t\tstrbuf_addbuf(&pp->buffered_output, &pp->children[i].err);\n \t\t\tstrbuf_reset(&pp->children[i].err);\n \t\t} else {\n-\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t/* Output errors, then all other finished child processes */\n+\t\t\tif (pp->consume_sideband) {\n+\t\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\t\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\t\t\t} else {\n+\t\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n+\t\t\t}\n \t\t\tstrbuf_reset(&pp->children[i].err);\n-\n-\t\t\t/* Output all other finished child processes */\n-\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n \t\t\tstrbuf_reset(&pp->buffered_output);\n \n \t\t\t/*\n@@ -1870,6 +1884,7 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n \t\t\t   feed_pipe_fn feed_pipe,\n+\t\t\t   consume_sideband_fn consume_sideband,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1880,7 +1895,7 @@ int run_processes_parallel(int n,\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n \n-\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, consume_sideband, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1918,6 +1933,7 @@ int run_processes_parallel(int n,\n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n \t\t\t       feed_pipe_fn feed_pipe,\n+\t\t\t       consume_sideband_fn consume_sideband,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1927,7 +1943,8 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, consume_sideband,\n+\t\t\t\t\ttask_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 1e3cf0999f..ebc4a95a94 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -457,6 +457,20 @@ typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n \t\t\t    void *pp_cb,\n \t\t\t    void *pp_task_cb);\n \n+/**\n+ * If this callback is provided, instead of collating process output to stderr,\n+ * they will be collated into a new pipe. consume_sideband_fn will be called\n+ * repeatedly. When output is available on that pipe, it will be contained in\n+ * 'output'. But it will be called with an empty 'output' too, to allow for\n+ * keepalives or similar operations if necessary.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel.\n+ *\n+ * Since this callback is provided with the collated output, no task cookie is\n+ * provided.\n+ */\n+typedef void (*consume_sideband_fn)(struct strbuf *output, void *pp_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -492,10 +506,12 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n \t\t\t   feed_pipe_fn,\n+\t\t\t   consume_sideband_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, consume_sideband_fn,\n+\t\t\t       task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex ea026a8195..7fe0c8f7c9 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1645,7 +1645,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   NULL, NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 9348184d30..d53db6d11c 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -51,6 +51,16 @@ static int no_job(struct child_process *cp,\n \treturn 0;\n }\n \n+static void test_consume_sideband(struct strbuf *output, void *cb)\n+{\n+\tFILE *sideband;\n+\n+\tsideband = fopen(\"./sideband\", \"a\");\n+\n+\tstrbuf_write(output, sideband);\n+\tfclose(sideband);\n+}\n+\n static int task_finished(int result,\n \t\t\t struct strbuf *err,\n \t\t\t void *pp_cb,\n@@ -201,7 +211,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_stdin, test_finished, &suite);\n+\t\t\t\t     test_stdin, NULL, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -429,23 +439,28 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-stdin\")) {\n \t\tproc.in = -1;\n \t\tproc.no_stdin = 0;\n \t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n-\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t\t\t\t\t     test_stdin, NULL, NULL, &proc));\n \t}\n \n+\tif (!strcmp(argv[1], \"run-command-sideband\"))\n+\t\texit(run_processes_parallel(jobs, parallel_next, NULL, NULL,\n+\t\t\t\t\t    test_consume_sideband, NULL,\n+\t\t\t\t\t    &proc));\n+\n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\n }\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 87759482ad..e99f6c7f44 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,13 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'run_command can divert output' '\n+\ttest_when_finished rm sideband &&\n+\ttest-tool run-command run-command-sideband 3 sh -c \"printf \\\"%s\\n%s\\n\\\" Hello World\" 2>actual &&\n+\ttest_must_be_empty actual &&\n+\ttest_cmp expect sideband\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n listening for stdin:\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425618","messageId":"20210527000856.695702-17-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 16/37] hooks: allow callers to capture output","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:35Z","receivedAt":"2021-05-27T00:10:00Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Some server-side hooks will require capturing output to send over\nsideband instead of printing directly to stderr. Expose that capability.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    You can see this in practice in the conversions for some of the push hooks,\n    like 'receive-pack'.\n\n hook.c | 3 ++-\n hook.h | 8 ++++++++\n 2 files changed, 10 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex d48071bb1d..27da1fdb32 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -262,6 +262,7 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n \to->dir = NULL;\n \to->feed_pipe = NULL;\n \to->feed_pipe_ctx = NULL;\n+\to->consume_sideband = NULL;\n }\n \n void run_hooks_opt_init_async(struct run_hooks_opt *o)\n@@ -437,7 +438,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->consume_sideband,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/hook.h b/hook.h\nindex aba62d4112..f32189380a 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -78,6 +78,14 @@ struct run_hooks_opt\n \tfeed_pipe_fn feed_pipe;\n \tvoid *feed_pipe_ctx;\n \n+\t/*\n+\t * Populate this to capture output and prevent it from being printed to\n+\t * stderr. This will be passed directly through to\n+\t * run_command:run_parallel_processes(). See t/helper/test-run-command.c\n+\t * for an example.\n+\t */\n+\tconsume_sideband_fn consume_sideband;\n+\n \t/* Number of threads to parallelize across */\n \tint jobs;\n \n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425619","messageId":"20210527000856.695702-18-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 17/37] commit: use config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:36Z","receivedAt":"2021-05-27T00:10:01Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"As part of the adoption of config-based hooks, teach run_commit_hook()\nto call hook.h instead of run-command.h. This covers 'pre-commit',\n'commit-msg', and 'prepare-commit-msg'. Additionally, ask the hook\nlibrary - not run-command - whether any hooks will be run, as it's\npossible hooks may exist in the config but not the hookdir.\n\nBecause all but 'post-commit' hooks are expected to make some state\nchange, force all but 'post-commit' hook to run in series. 'post-commit'\n\"is meant primarily for notification, and cannot affect the outcome of\n`git commit`,\" so it is fine to run in parallel.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt                    | 13 +++++++++++\n builtin/commit.c                              | 11 +++++-----\n builtin/merge.c                               |  9 ++++----\n commit.c                                      | 22 ++++++++++++++-----\n commit.h                                      |  3 ++-\n sequencer.c                                   |  7 +++---\n ...3-pre-commit-and-pre-merge-commit-hooks.sh | 17 ++++++++++++--\n 7 files changed, 61 insertions(+), 21 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex b51959ff94..4af202b366 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -103,6 +103,8 @@ The default 'pre-commit' hook, when enabled--and with the\n `hooks.allownonascii` config option unset or set to false--prevents\n the use of non-ASCII filenames.\n \n+Hooks executed during 'pre-commit' will not be parallelized.\n+\n pre-merge-commit\n ~~~~~~~~~~~~~~~~\n \n@@ -125,6 +127,8 @@ need to be resolved and the result committed separately (see\n linkgit:git-merge[1]). At that point, this hook will not be executed,\n but the 'pre-commit' hook will, if it is enabled.\n \n+Hooks executed during 'pre-merge-commit' will not be parallelized.\n+\n prepare-commit-msg\n ~~~~~~~~~~~~~~~~~~\n \n@@ -150,6 +154,9 @@ be used as replacement for pre-commit hook.\n The sample `prepare-commit-msg` hook that comes with Git removes the\n help message found in the commented portion of the commit template.\n \n+Hooks executed during 'prepare-commit-msg' will not be parallelized, because\n+hooks are expected to edit the file containing the commit log message.\n+\n commit-msg\n ~~~~~~~~~~\n \n@@ -166,6 +173,9 @@ file.\n The default 'commit-msg' hook, when enabled, detects duplicate\n `Signed-off-by` trailers, and aborts the commit if one is found.\n \n+Hooks executed during 'commit-msg' will not be parallelized, because hooks are\n+expected to edit the file containing the proposed commit log message.\n+\n post-commit\n ~~~~~~~~~~~\n \n@@ -175,6 +185,9 @@ invoked after a commit is made.\n This hook is meant primarily for notification, and cannot affect\n the outcome of `git commit`.\n \n+Hooks executed during 'post-commit' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n pre-rebase\n ~~~~~~~~~~\n \ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 190d215d43..7e01802961 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -36,6 +36,7 @@\n #include \"help.h\"\n #include \"commit-reach.h\"\n #include \"commit-graph.h\"\n+#include \"hook.h\"\n \n static const char * const builtin_commit_usage[] = {\n \tN_(\"git commit [<options>] [--] <pathspec>...\"),\n@@ -728,7 +729,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t/* This checks and barfs if author is badly specified */\n \tdetermine_author_info(author_ident);\n \n-\tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(use_editor, 0, index_file, \"pre-commit\", NULL))\n \t\treturn 0;\n \n \tif (squash_message) {\n@@ -1044,7 +1045,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && find_hook(\"pre-commit\")) {\n+\tif (!no_verify && hook_exists(\"pre-commit\", HOOKDIR_USE_CONFIG)) {\n \t\t/*\n \t\t * Re-read the index as pre-commit hook could have updated it,\n \t\t * and write it out as a tree.  We must do this before we invoke\n@@ -1059,7 +1060,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n+\tif (run_commit_hook(use_editor, 0, index_file, \"prepare-commit-msg\",\n \t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n \t\treturn 0;\n \n@@ -1076,7 +1077,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t}\n \n \tif (!no_verify &&\n-\t    run_commit_hook(use_editor, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n+\t    run_commit_hook(use_editor, 0, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n \t\treturn 0;\n \t}\n \n@@ -1829,7 +1830,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \n \trepo_rerere(the_repository, 0);\n \trun_auto_maintenance(quiet);\n-\trun_commit_hook(use_editor, get_index_file(), \"post-commit\", NULL);\n+\trun_commit_hook(use_editor, 1, get_index_file(), \"post-commit\", NULL);\n \tif (amend && !no_post_rewrite) {\n \t\tcommit_post_rewrite(the_repository, current_head, &oid);\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex eddb8ae70d..182dea418a 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -43,6 +43,7 @@\n #include \"commit-reach.h\"\n #include \"wt-status.h\"\n #include \"commit-graph.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_TWOHEAD (1<<0)\n #define DEFAULT_OCTOPUS (1<<1)\n@@ -841,14 +842,14 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *index_file = get_index_file();\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, index_file, \"pre-merge-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(0 < option_edit, 0, index_file, \"pre-merge-commit\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \t/*\n \t * Re-read the index as pre-merge-commit hook could have updated it,\n \t * and write it out as a tree.  We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (find_hook(\"pre-merge-commit\"))\n+\tif (hook_exists(\"pre-merge-commit\", HOOKDIR_USE_CONFIG))\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n@@ -869,7 +870,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tappend_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);\n \twrite_merge_heads(remoteheads);\n \twrite_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);\n-\tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n+\tif (run_commit_hook(0 < option_edit, 0, get_index_file(), \"prepare-commit-msg\",\n \t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \tif (0 < option_edit) {\n@@ -877,7 +878,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\t\tabort_commit(remoteheads, NULL);\n \t}\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n+\tif (!no_verify && run_commit_hook(0 < option_edit, 0, get_index_file(),\n \t\t\t\t\t  \"commit-msg\",\n \t\t\t\t\t  git_path_merge_msg(the_repository), NULL))\n \t\tabort_commit(remoteheads, NULL);\ndiff --git a/commit.c b/commit.c\nindex 8ea55a447f..0da5b7e7f1 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -21,6 +21,7 @@\n #include \"commit-reach.h\"\n #include \"run-command.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);\n \n@@ -1695,25 +1696,34 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n \treturn boc ? len - boc : len - cutoff;\n }\n \n-int run_commit_hook(int editor_is_used, const char *index_file,\n+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,\n \t\t    const char *name, ...)\n {\n-\tstruct strvec hook_env = STRVEC_INIT;\n+\tstruct run_hooks_opt opt;\n \tva_list args;\n+\tconst char *arg;\n \tint ret;\n \n-\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n+\trun_hooks_opt_init_sync(&opt);\n+\n+\tif (parallelize)\n+\t\topt.jobs = configured_hook_jobs();\n+\n+\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\n \t * Let the hook know that no editor will be launched.\n \t */\n \tif (!editor_is_used)\n-\t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n+\t\tstrvec_push(&opt.env, \"GIT_EDITOR=:\");\n \n \tva_start(args, name);\n-\tret = run_hook_ve(hook_env.v, name, args);\n+\twhile ((arg = va_arg(args, const char *)))\n+\t\tstrvec_push(&opt.args, arg);\n \tva_end(args);\n-\tstrvec_clear(&hook_env);\n+\n+\tret = run_hooks(name, &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \treturn ret;\n }\ndiff --git a/commit.h b/commit.h\nindex df42eb434f..a90c094ec2 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -363,7 +363,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\n \n LAST_ARG_MUST_BE_NULL\n-int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);\n+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,\n+\t\t    const char *name, ...);\n \n /* Sign a commit or tag buffer, storing the result in a header. */\n int sign_with_header(struct strbuf *buf, const char *keyid);\ndiff --git a/sequencer.c b/sequencer.c\nindex 0bec01cf38..72234af8ed 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -34,6 +34,7 @@\n #include \"commit-reach.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -1207,7 +1208,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n \t} else {\n \t\targ1 = \"message\";\n \t}\n-\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n+\tif (run_commit_hook(0, 0, r->index_file, \"prepare-commit-msg\", name,\n \t\t\t    arg1, arg2, NULL))\n \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n \n@@ -1445,7 +1446,7 @@ static int try_to_commit(struct repository *r,\n \t\t}\n \t}\n \n-\tif (find_hook(\"prepare-commit-msg\")) {\n+\tif (hook_exists(\"prepare-commit-msg\", HOOKDIR_USE_CONFIG)) {\n \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n \t\tif (res)\n \t\t\tgoto out;\n@@ -1537,7 +1538,7 @@ static int try_to_commit(struct repository *r,\n \t\tgoto out;\n \t}\n \n-\trun_commit_hook(0, r->index_file, \"post-commit\", NULL);\n+\trun_commit_hook(0, 1, r->index_file, \"post-commit\", NULL);\n \tif (flags & AMEND_MSG)\n \t\tcommit_post_rewrite(r, current_head, oid);\n \ndiff --git a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\nindex 606d8d0f08..e9e3713033 100755\n--- a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\n+++ b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\n@@ -8,8 +8,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n \n HOOKDIR=\"$(git rev-parse --git-dir)/hooks\"\n-PRECOMMIT=\"$HOOKDIR/pre-commit\"\n-PREMERGE=\"$HOOKDIR/pre-merge-commit\"\n+PRECOMMIT=\"$(pwd)/$HOOKDIR/pre-commit\"\n+PREMERGE=\"$(pwd)/$HOOKDIR/pre-merge-commit\"\n \n # Prepare sample scripts that write their $0 to actual_hooks\n test_expect_success 'sample script setup' '\n@@ -106,6 +106,19 @@ test_expect_success 'with succeeding hook' '\n \ttest_cmp expected_hooks actual_hooks\n '\n \n+# NEEDSWORK: when 'git hook add' and 'git hook remove' have been added, use that\n+# instead\n+test_expect_success 'with succeeding hook (config-based)' '\n+\ttest_when_finished \"git config --unset hook.pre-commit.command success.sample\" &&\n+\ttest_when_finished \"rm -f expected_hooks actual_hooks\" &&\n+\tgit config hook.pre-commit.command \"$HOOKDIR/success.sample\" &&\n+\techo \"$HOOKDIR/success.sample\" >expected_hooks &&\n+\techo \"more\" >>file &&\n+\tgit add file &&\n+\tgit commit -m \"more\" &&\n+\ttest_cmp expected_hooks actual_hooks\n+'\n+\n test_expect_success 'with succeeding hook (merge)' '\n \ttest_when_finished \"rm -f \\\"$PREMERGE\\\" expected_hooks actual_hooks\" &&\n \tcp \"$HOOKDIR/success.sample\" \"$PREMERGE\" &&\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425620","messageId":"20210527000856.695702-19-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 18/37] am: convert applypatch hooks to use config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:37Z","receivedAt":"2021-05-27T00:10:06Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach pre-applypatch, post-applypatch, and applypatch-msg to use the\nhook.h library instead of the run-command.h library. This enables use of\nhooks specified in the config, in addition to those in the hookdir.\nThese three hooks are called only by builtin/am.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  9 +++++++++\n builtin/am.c               | 21 ++++++++++++++++++---\n 2 files changed, 27 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 4af202b366..ec5020bfb6 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -58,6 +58,9 @@ the message file.\n The default 'applypatch-msg' hook, when enabled, runs the\n 'commit-msg' hook, if the latter is enabled.\n \n+Hooks run during 'applypatch-msg' will not be parallelized, because hooks are\n+expected to edit the file holding the commit log message.\n+\n pre-applypatch\n ~~~~~~~~~~~~~~\n \n@@ -73,6 +76,9 @@ make a commit if it does not pass certain test.\n The default 'pre-applypatch' hook, when enabled, runs the\n 'pre-commit' hook, if the latter is enabled.\n \n+Hooks run during 'pre-applypatch' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n post-applypatch\n ~~~~~~~~~~~~~~~\n \n@@ -82,6 +88,9 @@ and is invoked after the patch is applied and a commit is made.\n This hook is meant primarily for notification, and cannot affect\n the outcome of `git am`.\n \n+Hooks run during 'post-applypatch' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n pre-commit\n ~~~~~~~~~~\n \ndiff --git a/builtin/am.c b/builtin/am.c\nindex 0b2d886c81..d84791859c 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -33,6 +33,7 @@\n #include \"string-list.h\"\n #include \"packfile.h\"\n #include \"repository.h\"\n+#include \"hook.h\"\n \n /**\n  * Returns the length of the first line of msg.\n@@ -444,9 +445,14 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n+\tstruct run_hooks_opt opt;\n+\n+\trun_hooks_opt_init_sync(&opt);\n \n \tassert(state->msg);\n-\tret = run_hook_le(NULL, \"applypatch-msg\", am_path(state, \"final-commit\"), NULL);\n+\tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n+\tret = run_hooks(\"applypatch-msg\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tif (!ret) {\n \t\tFREE_AND_NULL(state->msg);\n@@ -1606,9 +1612,16 @@ static void do_commit(const struct am_state *state)\n \tstruct commit_list *parents = NULL;\n \tconst char *reflog_msg, *author, *committer = NULL;\n \tstruct strbuf sb = STRBUF_INIT;\n+\tstruct run_hooks_opt hook_opt;\n+\n+\trun_hooks_opt_init_async(&hook_opt);\n \n-\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n+\tif (run_hooks(\"pre-applypatch\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\texit(1);\n+\t}\n+\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (write_cache_as_tree(&tree, 0, NULL))\n \t\tdie(_(\"git write-tree failed to write a tree\"));\n@@ -1659,8 +1672,10 @@ static void do_commit(const struct am_state *state)\n \t\tfclose(fp);\n \t}\n \n-\trun_hook_le(NULL, \"post-applypatch\", NULL);\n+\trun_hooks_opt_init_async(&hook_opt);\n+\trun_hooks(\"post-applypatch\", &hook_opt);\n \n+\trun_hooks_opt_clear(&hook_opt);\n \tstrbuf_release(&sb);\n }\n \n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425621","messageId":"20210527000856.695702-20-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 19/37] merge: use config-based hooks for post-merge hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:38Z","receivedAt":"2021-05-27T00:10:18Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach post-merge to use the hook.h library instead of the run-command.h\nlibrary to run hooks. This means that post-merge hooks can come from the\nconfig as well as from the hookdir. post-merge is invoked only from\nbuiltin/merge.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt | 3 +++\n builtin/merge.c            | 6 +++++-\n 2 files changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex ec5020bfb6..c904b160dc 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -242,6 +242,9 @@ save and restore any form of metadata associated with the working tree\n (e.g.: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl\n for an example of how to do this.\n \n+Hooks executed during 'post-merge' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n pre-push\n ~~~~~~~~\n \ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 182dea418a..7a524cb3e3 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,6 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n+\tstruct run_hooks_opt opt;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\n@@ -489,7 +490,10 @@ static void finish(struct commit *head_commit,\n \t}\n \n \t/* Run a post-merge hook */\n-\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n+\trun_hooks_opt_init_async(&opt);\n+\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n+\trun_hooks(\"post-merge\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tapply_autostash(git_path_merge_autostash(the_repository));\n \tstrbuf_release(&reflog_message);\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425622","messageId":"20210527000856.695702-21-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 20/37] gc: use hook library for pre-auto-gc hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:39Z","receivedAt":"2021-05-27T00:10:20Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Using the hook.h library instead of the run-command.h library to run\npre-auto-gc means that those hooks can be set up in config files, as\nwell as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt | 3 +++\n builtin/gc.c               | 9 ++++++++-\n 2 files changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex c904b160dc..d77170dafb 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -569,6 +569,9 @@ This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It\n takes no parameter, and exiting with non-zero status from this script\n causes the `git gc --auto` to abort.\n \n+Hooks run during 'pre-auto-gc' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n post-rewrite\n ~~~~~~~~~~~~\n \ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1a..16890b097c 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -32,6 +32,7 @@\n #include \"remote.h\"\n #include \"object-store.h\"\n #include \"exec-cmd.h\"\n+#include \"hook.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -348,6 +349,8 @@ static void add_repack_incremental_option(void)\n \n static int need_to_gc(void)\n {\n+\tstruct run_hooks_opt hook_opt;\n+\n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n \t * automatic gc.\n@@ -394,8 +397,12 @@ static int need_to_gc(void)\n \telse\n \t\treturn 0;\n \n-\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n+\trun_hooks_opt_init_async(&hook_opt);\n+\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\treturn 0;\n+\t}\n+\trun_hooks_opt_clear(&hook_opt);\n \treturn 1;\n }\n \n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425623","messageId":"20210527000856.695702-22-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 21/37] rebase: teach pre-rebase to use hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:40Z","receivedAt":"2021-05-27T00:10:28Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using hook.h instead of run-command.h to run hooks, pre-rebase hooks\ncan now be specified in the config as well as in the hookdir. pre-rebase\nis not called anywhere besides builtin/rebase.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  3 +++\n builtin/rebase.c           | 10 ++++++++--\n 2 files changed, 11 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex d77170dafb..7ae24d65ec 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -206,6 +206,9 @@ two parameters.  The first parameter is the upstream from which\n the series was forked.  The second parameter is the branch being\n rebased, and is not set when rebasing the current branch.\n \n+Hooks executed during 'pre-rebase' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n post-checkout\n ~~~~~~~~~~~~~\n \ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 12f093121d..fe9f144cad 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -28,6 +28,7 @@\n #include \"sequencer.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_REFLOG_ACTION \"rebase\"\n \n@@ -1313,6 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n+\tstruct run_hooks_opt hook_opt;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\n@@ -2022,10 +2024,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t}\n \n \t/* If a hook exists, give it a chance to interrupt*/\n+\trun_hooks_opt_init_async(&hook_opt);\n+\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n \tif (!ok_to_skip_pre_rebase &&\n-\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n-\t\t\targc ? argv[0] : NULL, NULL))\n+\t    run_hooks(\"pre-rebase\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n+\t}\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (options.flags & REBASE_DIFFSTAT) {\n \t\tstruct diff_options opts;\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425624","messageId":"20210527000856.695702-23-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 22/37] read-cache: convert post-index-change hook to use config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:41Z","receivedAt":"2021-05-27T00:10:29Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using hook.h instead of run-command.h to run, post-index-change hooks\ncan now be specified in the config in addition to the hookdir.\npost-index-change is not run anywhere besides in read-cache.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  3 +++\n read-cache.c               | 14 +++++++++++---\n 2 files changed, 14 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 7ae24d65ec..5efa25a44a 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -729,6 +729,9 @@ and \"0\" meaning they were not.\n Only one parameter should be set to \"1\" when the hook runs.  The hook\n running passing \"1\", \"1\" should not be possible.\n \n+Hooks run during 'post-index-change' will be run in parallel, unless hook.jobs\n+is configured to 1.\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/read-cache.c b/read-cache.c\nindex 1b3c2eb408..6a5c9403f4 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -26,6 +26,8 @@\n #include \"thread-utils.h\"\n #include \"progress.h\"\n #include \"sparse-index.h\"\n+#include \"hook.h\"\n+>>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)\n \n /* Mask for the name length in ce_flags in the on-disk index */\n \n@@ -3131,6 +3133,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n+\tstruct run_hooks_opt hook_opt;\n \n \tret = convert_to_sparse(istate);\n \n@@ -3159,9 +3162,14 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \telse\n \t\tret = close_lock_file_gently(lock);\n \n-\trun_hook_le(NULL, \"post-index-change\",\n-\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n-\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n+\trun_hooks_opt_init_async(&hook_opt);\n+\tstrvec_pushl(&hook_opt.args,\n+\t\t     istate->updated_workdir ? \"1\" : \"0\",\n+\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trun_hooks(\"post-index-change\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n+\n \tistate->updated_workdir = 0;\n \tistate->updated_skipworktree = 0;\n \n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425625","messageId":"20210527000856.695702-24-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 23/37] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:42Z","receivedAt":"2021-05-27T00:10:30Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using hook.h instead of run-command.h to invoke push-to-checkout,\nhooks can now be specified in the config as well as in the hookdir.\npush-to-checkout is not called anywhere but in builtin/receive-pack.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  1 +\n builtin/receive-pack.c     | 17 +++++++++++++----\n 2 files changed, 14 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 5efa25a44a..17ffeebf07 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -564,6 +564,7 @@ that switches branches while\n keeping the local changes in the working tree that do not interfere\n with the difference between the branches.\n \n+Hooks executed during 'push-to-checkout' will not be parallelized.\n \n pre-auto-gc\n ~~~~~~~~~~~\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a34742513a..09e7cdee45 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -29,6 +29,7 @@\n #include \"commit-reach.h\"\n #include \"worktree.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static const char * const receive_pack_usage[] = {\n \tN_(\"git receive-pack <git-dir>\"),\n@@ -1435,12 +1436,20 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n+\tstruct run_hooks_opt opt;\n+\n+\trun_hooks_opt_init_sync(&opt);\n+\n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n-\tif (run_hook_le(env->v, push_to_checkout_hook,\n-\t\t\thash_to_hex(hash), NULL))\n+\tstrvec_pushv(&opt.env, env->v);\n+\tstrvec_push(&opt.args, hash_to_hex(hash));\n+\tif (run_hooks(push_to_checkout_hook, &opt)) {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn \"push-to-checkout hook declined\";\n-\telse\n+\t} else {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn NULL;\n+\t}\n }\n \n static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree)\n@@ -1464,7 +1473,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!find_hook(push_to_checkout_hook))\n+\tif (!hook_exists(push_to_checkout_hook, HOOKDIR_USE_CONFIG))\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n \telse\n \t\tretval = push_to_checkout(sha1, &env, work_tree);\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425626","messageId":"20210527000856.695702-25-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 24/37] git-p4: use 'git hook' to run hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:43Z","receivedAt":"2021-05-27T00:10:35Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Instead of duplicating the behavior of run-command.h:run_hook_le() in\nPython, we can directly call 'git hook run'. As a bonus, this means\ngit-p4 learns how to find hook specifications from the Git config as\nwell as from the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    Maybe there is a better way to do this - I had a hard time getting this to run\n    locally, and Python is not my forte, so if anybody has a better approach I'd\n    love to just take that patch instead :)\n    \n    Since v6, removed the developer debug print statements.... :X\n    \n    Maybe there is a better way to do this - I had a hard time getting this to run\n    locally, and Python is not my forte, so if anybody has a better approach I'd\n    love to just take that patch instead :)\n\n git-p4.py | 67 +++++--------------------------------------------------\n 1 file changed, 6 insertions(+), 61 deletions(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex d34a1946b7..b7192d1153 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -208,70 +208,15 @@ def decode_path(path):\n \n def run_git_hook(cmd, param=[]):\n     \"\"\"Execute a hook if the hook exists.\"\"\"\n-    if verbose:\n-        sys.stderr.write(\"Looking for hook: %s\\n\" % cmd)\n-        sys.stderr.flush()\n-\n-    hooks_path = gitConfig(\"core.hooksPath\")\n-    if len(hooks_path) <= 0:\n-        hooks_path = os.path.join(os.environ[\"GIT_DIR\"], \"hooks\")\n-\n-    if not isinstance(param, list):\n-        param=[param]\n-\n-    # resolve hook file name, OS depdenent\n-    hook_file = os.path.join(hooks_path, cmd)\n-    if platform.system() == 'Windows':\n-        if not os.path.isfile(hook_file):\n-            # look for the file with an extension\n-            files = glob.glob(hook_file + \".*\")\n-            if not files:\n-                return True\n-            files.sort()\n-            hook_file = files.pop()\n-            while hook_file.upper().endswith(\".SAMPLE\"):\n-                # The file is a sample hook. We don't want it\n-                if len(files) > 0:\n-                    hook_file = files.pop()\n-                else:\n-                    return True\n-\n-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):\n+    if not cmd:\n         return True\n \n-    return run_hook_command(hook_file, param) == 0\n-\n-def run_hook_command(cmd, param):\n-    \"\"\"Executes a git hook command\n-       cmd = the command line file to be executed. This can be\n-       a file that is run by OS association.\n-\n-       param = a list of parameters to pass to the cmd command\n-\n-       On windows, the extension is checked to see if it should\n-       be run with the Git for Windows Bash shell.  If there\n-       is no file extension, the file is deemed a bash shell\n-       and will be handed off to sh.exe. Otherwise, Windows\n-       will be called with the shell to handle the file assocation.\n-\n-       For non Windows operating systems, the file is called\n-       as an executable.\n-    \"\"\"\n-    cli = [cmd] + param\n-    use_shell = False\n-    if platform.system() == 'Windows':\n-        (root,ext) = os.path.splitext(cmd)\n-        if ext == \"\":\n-            exe_path = os.environ.get(\"EXEPATH\")\n-            if exe_path is None:\n-                exe_path = \"\"\n-            else:\n-                exe_path = os.path.join(exe_path, \"bin\")\n-            cli = [os.path.join(exe_path, \"SH.EXE\")] + cli\n-        else:\n-            use_shell = True\n-    return subprocess.call(cli, shell=use_shell)\n+    \"\"\"args are specified with -a <arg> -a <arg> -a <arg>\"\"\"\n+    args = (['git', 'hook', 'run'] +\n+\t    [\"-a\" + arg for arg in param] +\n+\t    [cmd])\n \n+    return subprocess.call(args) == 0\n \n def write_pipe(c, stdin):\n     if verbose:\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425627","messageId":"20210527000856.695702-26-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 25/37] hooks: convert 'post-checkout' hook to hook library","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:44Z","receivedAt":"2021-05-27T00:10:45Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the 'hook.h' library, 'post-checkout' hooks can now be\nspecified in the config as well as in the hook directory.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  2 ++\n builtin/checkout.c         | 19 ++++++++++++++-----\n builtin/clone.c            |  8 ++++++--\n builtin/worktree.c         | 32 ++++++++++++++++----------------\n read-cache.c               |  1 -\n reset.c                    | 17 +++++++++++++----\n 6 files changed, 51 insertions(+), 28 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 17ffeebf07..6a5ff036ff 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -231,6 +231,8 @@ This hook can be used to perform repository validity checks, auto-display\n differences from the previous HEAD if different, or set working dir metadata\n properties.\n \n+Hooks executed during 'post-checkout' will not be parallelized.\n+\n post-merge\n ~~~~~~~~~~\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f4cd7747d3..1797f05a50 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"diff.h\"\n #include \"dir.h\"\n+#include \"hook.h\"\n #include \"ll-merge.h\"\n #include \"lockfile.h\"\n #include \"merge-recursive.h\"\n@@ -106,13 +107,21 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\treturn run_hook_le(NULL, \"post-checkout\",\n-\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n-\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n-\t\t\t   changed ? \"1\" : \"0\", NULL);\n+\tstruct run_hooks_opt opt;\n+\tint rc;\n+\n+\trun_hooks_opt_init_sync(&opt);\n+\n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\n-\n+\tstrvec_pushl(&opt.args,\n+\t\t     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n+\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n+\t\t     changed ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trc = run_hooks(\"post-checkout\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n }\n \n static int update_some(const struct object_id *oid, struct strbuf *base,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex eeb74c0217..2a2a03bf76 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -32,6 +32,7 @@\n #include \"connected.h\"\n #include \"packfile.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"hook.h\"\n \n /*\n  * Overall FIXMEs:\n@@ -775,6 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n+\tstruct run_hooks_opt hook_opt;\n \n \tif (option_no_checkout)\n \t\treturn 0;\n@@ -820,8 +822,10 @@ static int checkout(int submodule_progress)\n \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n \t\tdie(_(\"unable to write new index file\"));\n \n-\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(null_oid()),\n-\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n+\trun_hooks_opt_init_sync(&hook_opt);\n+\tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n+\terr |= run_hooks(\"post-checkout\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (!err && (option_recurse_submodules.nr > 0)) {\n \t\tstruct strvec args = STRVEC_INIT;\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 976bf8ed06..017b2cfcb5 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -13,6 +13,7 @@\n #include \"utf8.h\"\n #include \"worktree.h\"\n #include \"quote.h\"\n+#include \"hook.h\"\n \n static const char * const worktree_usage[] = {\n \tN_(\"git worktree add [<options>] <path> [<commit-ish>]\"),\n@@ -381,22 +382,21 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tconst char *hook = find_hook(\"post-checkout\");\n-\t\tif (hook) {\n-\t\t\tconst char *env[] = { \"GIT_DIR\", \"GIT_WORK_TREE\", NULL };\n-\t\t\tcp.git_cmd = 0;\n-\t\t\tcp.no_stdin = 1;\n-\t\t\tcp.stdout_to_stderr = 1;\n-\t\t\tcp.dir = path;\n-\t\t\tcp.env = env;\n-\t\t\tcp.argv = NULL;\n-\t\t\tcp.trace2_hook_name = \"post-checkout\";\n-\t\t\tstrvec_pushl(&cp.args, absolute_path(hook),\n-\t\t\t\t     oid_to_hex(null_oid()),\n-\t\t\t\t     oid_to_hex(&commit->object.oid),\n-\t\t\t\t     \"1\", NULL);\n-\t\t\tret = run_command(&cp);\n-\t\t}\n+\t\tstruct run_hooks_opt opt;\n+\n+\t\trun_hooks_opt_init_sync(&opt);\n+\n+\t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(null_oid()),\n+\t\t\t     oid_to_hex(&commit->object.oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\topt.dir = path;\n+\n+\t\tret = run_hooks(\"post-checkout\", &opt);\n+\n+\t\trun_hooks_opt_clear(&opt);\n \t}\n \n \tstrvec_clear(&child_env);\ndiff --git a/read-cache.c b/read-cache.c\nindex 6a5c9403f4..ebb9c19056 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -27,7 +27,6 @@\n #include \"progress.h\"\n #include \"sparse-index.h\"\n #include \"hook.h\"\n->>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)\n \n /* Mask for the name length in ce_flags in the on-disk index */\n \ndiff --git a/reset.c b/reset.c\nindex 4bea758053..48d45f5b79 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -7,6 +7,7 @@\n #include \"tree-walk.h\"\n #include \"tree.h\"\n #include \"unpack-trees.h\"\n+#include \"hook.h\"\n \n int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t       const char *switch_to_branch, unsigned flags,\n@@ -126,10 +127,18 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\tret = create_symref(\"HEAD\", switch_to_branch,\n \t\t\t\t\t    reflog_head);\n \t}\n-\tif (run_hook)\n-\t\trun_hook_le(NULL, \"post-checkout\",\n-\t\t\t    oid_to_hex(orig ? orig : null_oid()),\n-\t\t\t    oid_to_hex(oid), \"1\", NULL);\n+\tif (run_hook) {\n+\t\tstruct run_hooks_opt opt;\n+\n+\t\trun_hooks_opt_init_sync(&opt);\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(orig ? orig : null_oid()),\n+\t\t\t     oid_to_hex(oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\trun_hooks(\"post-checkout\", &opt);\n+\t\trun_hooks_opt_clear(&opt);\n+\t}\n \n leave_reset_head:\n \tstrbuf_release(&msg);\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425628","messageId":"20210527000856.695702-27-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 26/37] hook: convert 'post-rewrite' hook to config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:45Z","receivedAt":"2021-05-27T00:10:47Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using 'hook.h' for 'post-rewrite', we simplify hook invocations by\nnot needing to put together our own 'struct child_process' and we also\nlearn to run hooks specified in the config as well as the hook dir.\n\nThe signal handling that's being removed by this commit now takes place\nin run-command.h:run_processes_parallel(), so it is OK to remove them\nhere.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  3 ++\n builtin/am.c               | 18 +++-----\n sequencer.c                | 85 +++++++++++++++++---------------------\n 3 files changed, 47 insertions(+), 59 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 6a5ff036ff..156e4809f6 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -603,6 +603,9 @@ The hook always runs after the automatic note copying (see\n \"notes.rewrite.<command>\" in linkgit:git-config[1]) has happened, and\n thus has access to these notes.\n \n+Hooks run during 'post-rewrite' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n The following command-specific comments apply:\n \n rebase::\ndiff --git a/builtin/am.c b/builtin/am.c\nindex d84791859c..d2534f9a1f 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -469,23 +469,17 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tconst char *hook = find_hook(\"post-rewrite\");\n+\tstruct run_hooks_opt opt;\n \tint ret;\n \n-\tif (!hook)\n-\t\treturn 0;\n+\trun_hooks_opt_init_async(&opt);\n \n-\tstrvec_push(&cp.args, hook);\n-\tstrvec_push(&cp.args, \"rebase\");\n+\tstrvec_push(&opt.args, \"rebase\");\n+\topt.path_to_stdin = am_path(state, \"rewritten\");\n \n-\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n-\tcp.stdout_to_stderr = 1;\n-\tcp.trace2_hook_name = \"post-rewrite\";\n+\tret = run_hooks(\"post-rewrite\", &opt);\n \n-\tret = run_command(&cp);\n-\n-\tclose(cp.in);\n+\trun_hooks_opt_clear(&opt);\n \treturn ret;\n }\n \ndiff --git a/sequencer.c b/sequencer.c\nindex 72234af8ed..3fa7668763 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -35,6 +35,7 @@\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n #include \"hook.h\"\n+#include \"string-list.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -1147,33 +1148,30 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *argv[3];\n+\tstruct run_hooks_opt opt;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n-\tstruct strbuf sb = STRBUF_INIT;\n \n-\targv[0] = find_hook(\"post-rewrite\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\trun_hooks_opt_init_async(&opt);\n \n-\targv[1] = \"amend\";\n-\targv[2] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"post-rewrite\";\n-\n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n-\tstrbuf_addf(&sb, \"%s %s\\n\", oid_to_hex(oldoid), oid_to_hex(newoid));\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\twrite_in_full(proc.in, sb.buf, sb.len);\n-\tclose(proc.in);\n-\tstrbuf_release(&sb);\n-\tsigchain_pop(SIGPIPE);\n-\treturn finish_command(&proc);\n+\tstrvec_push(&opt.args, \"amend\");\n+\n+\tstrbuf_addf(&tmp,\n+\t\t    \"%s %s\",\n+\t\t    oid_to_hex(oldoid),\n+\t\t    oid_to_hex(newoid));\n+\tstring_list_append(&to_stdin, tmp.buf);\n+\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tcode = run_hooks(\"post-rewrite\", &opt);\n+\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n+\treturn code;\n }\n \n void commit_post_rewrite(struct repository *r,\n@@ -4527,30 +4525,23 @@ static int pick_commits(struct repository *r,\n \t\tflush_rewritten_pending();\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n-\t\t\tstruct child_process child = CHILD_PROCESS_INIT;\n-\t\t\tconst char *post_rewrite_hook =\n-\t\t\t\tfind_hook(\"post-rewrite\");\n-\n-\t\t\tchild.in = open(rebase_path_rewritten_list(), O_RDONLY);\n-\t\t\tchild.git_cmd = 1;\n-\t\t\tstrvec_push(&child.args, \"notes\");\n-\t\t\tstrvec_push(&child.args, \"copy\");\n-\t\t\tstrvec_push(&child.args, \"--for-rewrite=rebase\");\n+\t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt;\n+\n+\t\t\trun_hooks_opt_init_async(&hook_opt);\n+\n+\t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n+\t\t\tnotes_cp.git_cmd = 1;\n+\t\t\tstrvec_push(&notes_cp.args, \"notes\");\n+\t\t\tstrvec_push(&notes_cp.args, \"copy\");\n+\t\t\tstrvec_push(&notes_cp.args, \"--for-rewrite=rebase\");\n \t\t\t/* we don't care if this copying failed */\n-\t\t\trun_command(&child);\n-\n-\t\t\tif (post_rewrite_hook) {\n-\t\t\t\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\n-\t\t\t\thook.in = open(rebase_path_rewritten_list(),\n-\t\t\t\t\tO_RDONLY);\n-\t\t\t\thook.stdout_to_stderr = 1;\n-\t\t\t\thook.trace2_hook_name = \"post-rewrite\";\n-\t\t\t\tstrvec_push(&hook.args, post_rewrite_hook);\n-\t\t\t\tstrvec_push(&hook.args, \"rebase\");\n-\t\t\t\t/* we don't care if this hook failed */\n-\t\t\t\trun_command(&hook);\n-\t\t\t}\n+\t\t\trun_command(&notes_cp);\n+\n+\t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n+\t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n+\t\t\trun_hooks(\"post-rewrite\", &hook_opt);\n+\t\t\trun_hooks_opt_clear(&hook_opt);\n \t\t}\n \t\tapply_autostash(rebase_path_autostash());\n \n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425629","messageId":"20210527000856.695702-28-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 27/37] transport: convert pre-push hook to use config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:46Z","receivedAt":"2021-05-27T00:10:53Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the hook.h:run_hooks API, pre-push hooks can be specified in\nthe config as well as in the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  3 ++\n transport.c                | 59 +++++++++++---------------------------\n 2 files changed, 20 insertions(+), 42 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 156e4809f6..543244ec0a 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -279,6 +279,9 @@ If this hook exits with a non-zero status, `git push` will abort without\n pushing anything.  Information about why the push is rejected may be sent\n to the user by writing to standard error.\n \n+Hooks executed during 'pre-push' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n [[pre-receive]]\n pre-receive\n ~~~~~~~~~~~\ndiff --git a/transport.c b/transport.c\nindex 6cf3da19eb..dbc7bb7820 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -22,6 +22,7 @@\n #include \"protocol.h\"\n #include \"object-store.h\"\n #include \"color.h\"\n+#include \"hook.h\"\n \n static int transport_use_color = -1;\n static char transport_colors[][COLOR_MAXLEN] = {\n@@ -1196,31 +1197,15 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)\n static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n-\tint ret = 0, x;\n+\tint ret = 0;\n+\tstruct run_hooks_opt opt;\n+\tstruct strbuf tmp = STRBUF_INIT;\n \tstruct ref *r;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct strbuf buf;\n-\tconst char *argv[4];\n-\n-\tif (!(argv[0] = find_hook(\"pre-push\")))\n-\t\treturn 0;\n-\n-\targv[1] = transport->remote->name;\n-\targv[2] = transport->url;\n-\targv[3] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.trace2_hook_name = \"pre-push\";\n-\n-\tif (start_command(&proc)) {\n-\t\tfinish_command(&proc);\n-\t\treturn -1;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n+\trun_hooks_opt_init_async(&opt);\n \n-\tstrbuf_init(&buf, 256);\n+\tstrvec_push(&opt.args, transport->remote->name);\n+\tstrvec_push(&opt.args, transport->url);\n \n \tfor (r = remote_refs; r; r = r->next) {\n \t\tif (!r->peer_ref) continue;\n@@ -1229,30 +1214,20 @@ static int run_pre_push_hook(struct transport *transport,\n \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n \n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n+\t\tstrbuf_reset(&tmp);\n+\t\tstrbuf_addf(&tmp, \"%s %s %s %s\",\n \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n \t\t\t r->name, oid_to_hex(&r->old_oid));\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\t/* We do not mind if a hook does not read all refs. */\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstring_list_append(&to_stdin, tmp.buf);\n \t}\n \n-\tstrbuf_release(&buf);\n-\n-\tx = close(proc.in);\n-\tif (!ret)\n-\t\tret = x;\n-\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n \n-\tx = finish_command(&proc);\n-\tif (!ret)\n-\t\tret = x;\n+\tret = run_hooks(\"pre-push\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n \n \treturn ret;\n }\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425630","messageId":"20210527000856.695702-29-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 28/37] reference-transaction: look for hooks in config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:47Z","receivedAt":"2021-05-27T00:10:53Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the hook.h library, reference-transaction hooks can be\nspecified in the config instead.\n\nThe expected output of the test is not fully updated to reflect the\nabsolute path of the hook called because the 'update' hook has not yet\nbeen converted to use hook.h.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt       |  3 +++\n refs.c                           | 43 +++++++++++++-------------------\n t/t1416-ref-transaction-hooks.sh |  8 +++---\n transport.c                      |  1 +\n 4 files changed, 25 insertions(+), 30 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 543244ec0a..edb840dcdd 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -539,6 +539,9 @@ The exit status of the hook is ignored for any state except for the\n cause the transaction to be aborted. The hook will not be called with\n \"aborted\" state in that case.\n \n+Hooks run during 'reference-transaction' will be run in parallel, unless\n+hook.jobs is configured to 1.\n+\n push-to-checkout\n ~~~~~~~~~~~~~~~~\n \ndiff --git a/refs.c b/refs.c\nindex 8c9490235e..32e993aaff 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -18,6 +18,7 @@\n #include \"strvec.h\"\n #include \"repository.h\"\n #include \"sigchain.h\"\n+#include \"hook.h\"\n \n /*\n  * List of all available backends\n@@ -2061,47 +2062,37 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n \tstruct strbuf buf = STRBUF_INIT;\n-\tconst char *hook;\n+\tstruct run_hooks_opt opt;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint ret = 0, i;\n+\tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n \n-\thook = find_hook(\"reference-transaction\");\n-\tif (!hook)\n-\t\treturn ret;\n-\n-\tstrvec_pushl(&proc.args, hook, state, NULL);\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"reference-transaction\";\n+\trun_hooks_opt_init_async(&opt);\n \n-\tret = start_command(&proc);\n-\tif (ret)\n+\tif (!hook_exists(\"reference-transaction\", HOOKDIR_USE_CONFIG))\n \t\treturn ret;\n \n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstrvec_push(&opt.args, state);\n \n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n+\t\toid_to_hex_r(o, &update->old_oid);\n+\t\toid_to_hex_r(n, &update->new_oid);\n \n \t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n-\t\t\t    oid_to_hex(&update->old_oid),\n-\t\t\t    oid_to_hex(&update->new_oid),\n-\t\t\t    update->refname);\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstrbuf_addf(&buf, \"%s %s %s\", o, n, update->refname);\n+\t\tstring_list_append(&to_stdin, buf.buf);\n \t}\n \n-\tclose(proc.in);\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tret = run_hooks(\"reference-transaction\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \tstrbuf_release(&buf);\n+\tstring_list_clear(&to_stdin, 0);\n \n-\tret |= finish_command(&proc);\n \treturn ret;\n }\n \ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 6c941027a8..3a90a59143 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -125,11 +125,11 @@ test_expect_success 'interleaving hook calls succeed' '\n \n \tcat >expect <<-EOF &&\n \t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n-\t\thooks/reference-transaction prepared\n-\t\thooks/reference-transaction committed\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n \t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n-\t\thooks/reference-transaction prepared\n-\t\thooks/reference-transaction committed\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n \tEOF\n \n \tgit push ./target-repo.git PRE POST &&\ndiff --git a/transport.c b/transport.c\nindex dbc7bb7820..9191107626 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1202,6 +1202,7 @@ static int run_pre_push_hook(struct transport *transport,\n \tstruct strbuf tmp = STRBUF_INIT;\n \tstruct ref *r;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n+\n \trun_hooks_opt_init_async(&opt);\n \n \tstrvec_push(&opt.args, transport->remote->name);\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425631","messageId":"20210527000856.695702-30-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 29/37] receive-pack: convert 'update' hook to hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:48Z","receivedAt":"2021-05-27T00:11:00Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using hook.h to invoke the 'update' hook, now hooks can be specified\nin the config in addition to the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt       |  3 ++\n builtin/receive-pack.c           | 65 ++++++++++++++++++++++----------\n t/t1416-ref-transaction-hooks.sh |  4 +-\n 3 files changed, 50 insertions(+), 22 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex edb840dcdd..1d731474ac 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -368,6 +368,9 @@ The default 'update' hook, when enabled--and with\n `hooks.allowunannotated` config option unset or set to false--prevents\n unannotated tags to be pushed.\n \n+Hooks executed during 'update' are run in parallel, unless hook.jobs is\n+configured to 1.\n+\n [[proc-receive]]\n proc-receive\n ~~~~~~~~~~~~\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 09e7cdee45..0f6bc8653f 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -938,33 +938,58 @@ static int run_receive_hook(struct command *commands,\n \treturn status;\n }\n \n+static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n+{\n+\tint keepalive_active = 0;\n+\n+\tif (keepalive_in_sec <= 0)\n+\t\tuse_keepalive = KEEPALIVE_NEVER;\n+\tif (use_keepalive == KEEPALIVE_ALWAYS)\n+\t\tkeepalive_active = 1;\n+\n+\t/* send a keepalive if there is no data to write */\n+\tif (keepalive_active && !output->len) {\n+\t\tstatic const char buf[] = \"0005\\1\";\n+\t\twrite_or_die(1, buf, sizeof(buf) - 1);\n+\t\treturn;\n+\t}\n+\n+\tif (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {\n+\t\tconst char *first_null = memchr(output->buf, '\\0', output->len);\n+\t\tif (first_null) {\n+\t\t\t/* The null bit is excluded. */\n+\t\t\tsize_t before_null = first_null - output->buf;\n+\t\t\tsize_t after_null = output->len - (before_null + 1);\n+\t\t\tkeepalive_active = 1;\n+\t\t\tsend_sideband(1, 2, output->buf, before_null, use_sideband);\n+\t\t\tsend_sideband(1, 2, first_null + 1, after_null, use_sideband);\n+\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n+}\n+\n static int run_update_hook(struct command *cmd)\n {\n-\tconst char *argv[5];\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n+\tstruct run_hooks_opt opt;\n \tint code;\n \n-\targv[0] = find_hook(\"update\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\trun_hooks_opt_init_async(&opt);\n \n-\targv[1] = cmd->ref_name;\n-\targv[2] = oid_to_hex(&cmd->old_oid);\n-\targv[3] = oid_to_hex(&cmd->new_oid);\n-\targv[4] = NULL;\n+\tstrvec_pushl(&opt.args,\n+\t\t     cmd->ref_name,\n+\t\t     oid_to_hex(&cmd->old_oid),\n+\t\t     oid_to_hex(&cmd->new_oid),\n+\t\t     NULL);\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.argv = argv;\n-\tproc.trace2_hook_name = \"update\";\n-\n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n \tif (use_sideband)\n-\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\treturn finish_command(&proc);\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\tcode = run_hooks(\"update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn code;\n }\n \n static struct command *find_command_by_refname(struct command *list,\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 3a90a59143..0a3c3e4a86 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -124,10 +124,10 @@ test_expect_success 'interleaving hook calls succeed' '\n \tEOF\n \n \tcat >expect <<-EOF &&\n-\t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n+\t\t$(pwd)/target-repo.git/hooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n \t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n \t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n-\t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n+\t\t$(pwd)/target-repo.git/hooks/update refs/tags/POST $ZERO_OID $POST_OID\n \t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n \t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n \tEOF\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425632","messageId":"20210527000856.695702-31-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 30/37] proc-receive: acquire hook list from hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:49Z","receivedAt":"2021-05-27T00:11:01Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"The proc-receive hook differs from most other hooks Git invokes because\nthe hook and the parent Git process engage in bidirectional\ncommunication via stdin/stdout. This bidirectional communication is\nunsuitable for multiple hooks, whether they are in series or in\nparallel, and is incompatible with run-command.h:run_processes_parallel:\n\n- The proc-receive hook is intended to modify the state of the Git repo.\n  From 'git help githooks':\n    This [proc-receive] hook is responsible for updating the relevant\n    references and reporting the results back to 'receive-pack'.\n  This prevents parallelization and implies, at least, specific ordering\n  of hook execution.\n- The proc-receive hook can reject a push by aborting early with an\n  error code. If a former hook ran through the entire push contents\n  successfully but a later hook rejects some of the push, the repo may\n  be left in a partially-updated (and corrupt) state.\n- The callback model of the run_processes_parallel() API is unsuited to\n  the current implementation of proc-receive, which loops through\n  \"send-receive-consider\" with the child process. proc-receive today\n  relies on stateful communication with the child process, which would be\n  unwieldy to implement with callbacks and saved state.\n- Additionally, run_processes_parallel() is designed to collate the\n  output of many child processes into a single output (stderr or callback),\n  and would require significant work to tell the caller which process sent\n  the output, and indeed to collect any output before the child process\n  has exited.\n\nSo, rather than using hook.h:run_hooks() to invoke the proc-receive\nhook, receive-pack.c can learn to ask hook.h:hook_list() for the\nlocation of a hook to run. This allows users to configure their\nproc-receive in a global config for all repos if they want, or a local\nconfig if they just don't want to use the hookdir. Because running more\nthan one proc-receive hook doesn't make sense from a repo state\nperspective, we can explicitly ban configuring more than one\nproc-receive hook at a time.\n\nIf a user wants to globally configure one proc-receive hook for most of\ntheir repos, but override that hook in a single repo, they should use\n'skip' to manually remove the global hook in their special repo:\n\n~/.gitconfig:\n[hook.proc-receive]\n  command = /usr/bin/usual-proc-receive\n\n~/special-repo/.git/config:\n[hookcmd./usr/bin/usual-proc-receive]\n  skip = true\n[hook.proc-receive]\n  command = /usr/bin/special-proc-receive\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt                |  4 ++\n builtin/receive-pack.c                    | 31 ++++++++++++++-\n t/t5411/test-0015-too-many-hooks-error.sh | 47 +++++++++++++++++++++++\n 3 files changed, 80 insertions(+), 2 deletions(-)\n create mode 100644 t/t5411/test-0015-too-many-hooks-error.sh\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 1d731474ac..b6b13afb36 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -433,6 +433,10 @@ the input.  The exit status of the 'proc-receive' hook only determines\n the success or failure of the group of commands sent to it, unless\n atomic push is in use.\n \n+It is forbidden to specify more than one hook for 'proc-receive'. If a\n+globally-configured 'proc-receive' must be overridden, use\n+'hookcmd.<global-hook>.skip = true' to ignore it.\n+\n [[post-receive]]\n post-receive\n ~~~~~~~~~~~~\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 0f6bc8653f..5ecc97d8ab 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1147,11 +1147,38 @@ static int run_proc_receive_hook(struct command *commands,\n \tint version = 0;\n \tint code;\n \n-\targv[0] = find_hook(\"proc-receive\");\n-\tif (!argv[0]) {\n+\tstruct hook *proc_receive = NULL;\n+\tstruct list_head *pos, *hooks;\n+\n+\thooks = hook_list(\"proc-receive\");\n+\n+\tlist_for_each(pos, hooks) {\n+\t\tif (proc_receive) {\n+\t\t\trp_error(\"only one 'proc-receive' hook can be specified\");\n+\t\t\treturn -1;\n+\t\t}\n+\t\tproc_receive = list_entry(pos, struct hook, list);\n+\t\t/* check if the hookdir hook should be ignored */\n+\t\tif (proc_receive->from_hookdir) {\n+\t\t\tswitch (configured_hookdir_opt()) {\n+\t\t\tcase HOOKDIR_INTERACTIVE:\n+\t\t\tcase HOOKDIR_NO:\n+\t\t\t\tproc_receive = NULL;\n+\t\t\t\tbreak;\n+\t\t\tdefault:\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\n+\t}\n+\n+\tif (!proc_receive) {\n \t\trp_error(\"cannot find hook 'proc-receive'\");\n \t\treturn -1;\n \t}\n+\n+\n+\targv[0] = proc_receive->command.buf;\n \targv[1] = NULL;\n \n \tproc.argv = argv;\ndiff --git a/t/t5411/test-0015-too-many-hooks-error.sh b/t/t5411/test-0015-too-many-hooks-error.sh\nnew file mode 100644\nindex 0000000000..2d64534510\n--- /dev/null\n+++ b/t/t5411/test-0015-too-many-hooks-error.sh\n@@ -0,0 +1,47 @@\n+test_expect_success \"setup too  many proc-receive hooks (ok, $PROTOCOL)\" '\n+\twrite_script \"proc-receive\" <<-EOF &&\n+\tprintf >&2 \"# proc-receive hook\\n\"\n+\ttest-tool proc-receive -v \\\n+\t\t-r \"ok refs/for/main/topic\"\n+\tEOF\n+\n+\tgit -C \"$upstream\" config --add \"hook.proc-receive.command\" proc-receive &&\n+\tcp proc-receive \"$upstream/hooks/proc-receive\"\n+'\n+\n+# Refs of upstream : main(A)\n+# Refs of workbench: main(A)  tags/v123\n+# git push         :                       next(A)  refs/for/main/topic(A)\n+test_expect_success \"proc-receive: reject more than one configured hook\" '\n+\ttest_must_fail git -C workbench push origin \\\n+\t\tHEAD:next \\\n+\t\tHEAD:refs/for/main/topic \\\n+\t\t>out 2>&1 &&\n+\tmake_user_friendly_and_stable_output <out >actual &&\n+\tcat >expect <<-EOF &&\n+\tremote: # pre-receive hook\n+\tremote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next\n+\tremote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic\n+\tremote: error: only one \"proc-receive\" hook can be specified\n+\tremote: # post-receive hook\n+\tremote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next\n+\tTo <URL/of/upstream.git>\n+\t * [new branch] HEAD -> next\n+\t ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)\n+\tEOF\n+\ttest_cmp expect actual &&\n+\tgit -C \"$upstream\" show-ref >out &&\n+\tmake_user_friendly_and_stable_output <out >actual &&\n+\tcat >expect <<-EOF &&\n+\t<COMMIT-A> refs/heads/main\n+\t<COMMIT-A> refs/heads/next\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+# Refs of upstream : main(A)             next(A)\n+# Refs of workbench: main(A)  tags/v123\n+test_expect_success \"cleanup ($PROTOCOL)\" '\n+\tgit -C \"$upstream\" config --unset \"hook.proc-receive.command\" \"proc-receive\" &&\n+\tgit -C \"$upstream\" update-ref -d refs/heads/next\n+'\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425633","messageId":"20210527000856.695702-32-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 31/37] post-update: use hook.h library","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:50Z","receivedAt":"2021-05-27T00:11:02Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using run_hooks() instead of run_hook_le(), 'post-update' hooks can\nbe specified in the config as well as the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |  3 +++\n builtin/receive-pack.c     | 26 ++++++++------------------\n 2 files changed, 11 insertions(+), 18 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex b6b13afb36..281970f910 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -508,6 +508,9 @@ Both standard output and standard error output are forwarded to\n `git send-pack` on the other end, so you can simply `echo` messages\n for the user.\n \n+Hooks run during 'post-update' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n reference-transaction\n ~~~~~~~~~~~~~~~~~~~~~\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 5ecc97d8ab..4236c0b268 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1688,33 +1688,23 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *hook;\n+\tstruct run_hooks_opt opt;\n \n-\thook = find_hook(\"post-update\");\n-\tif (!hook)\n-\t\treturn;\n+\trun_hooks_opt_init_async(&opt);\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\n \t\t\tcontinue;\n-\t\tif (!proc.args.nr)\n-\t\t\tstrvec_push(&proc.args, hook);\n-\t\tstrvec_push(&proc.args, cmd->ref_name);\n+\t\tstrvec_push(&opt.args, cmd->ref_name);\n \t}\n-\tif (!proc.args.nr)\n+\tif (!opt.args.nr)\n \t\treturn;\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.trace2_hook_name = \"post-update\";\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n \n-\tif (!start_command(&proc)) {\n-\t\tif (use_sideband)\n-\t\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\t\tfinish_command(&proc);\n-\t}\n+\trun_hooks(\"post-update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n }\n \n static void check_aliased_update_internal(struct command *cmd,\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425634","messageId":"20210527000856.695702-33-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 32/37] receive-pack: convert receive hooks to hook.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:51Z","receivedAt":"2021-05-27T00:11:07Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the hook.h library to run receive hooks, they can be specified\nin the config as well as in the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt |   5 +\n builtin/receive-pack.c     | 199 +++++++++++++++++--------------------\n 2 files changed, 97 insertions(+), 107 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 281970f910..78cc4e0872 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -323,6 +323,8 @@ will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n See the section on \"Quarantine Environment\" in\n linkgit:git-receive-pack[1] for some caveats.\n \n+Hooks executed during 'pre-receive' will not be parallelized.\n+\n [[update]]\n update\n ~~~~~~\n@@ -476,6 +478,9 @@ environment variables will not be set. If the client selects\n to use push options, but doesn't transmit any, the count variable\n will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n \n+Hooks executed during 'post-receive' are run in parallel, unless hook.jobs is\n+configured to 1.\n+\n [[post-update]]\n post-update\n ~~~~~~~~~~~\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 4236c0b268..f44b58e456 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -748,7 +748,7 @@ static int check_cert_push_options(const struct string_list *push_options)\n \treturn retval;\n }\n \n-static void prepare_push_cert_sha1(struct child_process *proc)\n+static void prepare_push_cert_sha1(struct run_hooks_opt *opt)\n {\n \tstatic int already_done;\n \n@@ -772,110 +772,42 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n \t}\n \tif (!is_null_oid(&push_cert_oid)) {\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT=%s\",\n \t\t\t     oid_to_hex(&push_cert_oid));\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_SIGNER=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_SIGNER=%s\",\n \t\t\t     sigcheck.signer ? sigcheck.signer : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_KEY=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_KEY=%s\",\n \t\t\t     sigcheck.key ? sigcheck.key : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_STATUS=%c\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_STATUS=%c\",\n \t\t\t     sigcheck.result);\n \t\tif (push_cert_nonce) {\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE=%s\",\n \t\t\t\t     push_cert_nonce);\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE_STATUS=%s\",\n \t\t\t\t     nonce_status);\n \t\t\tif (nonce_status == NONCE_SLOP)\n-\t\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t\t     \"GIT_PUSH_CERT_NONCE_SLOP=%ld\",\n \t\t\t\t\t     nonce_stamp_slop);\n \t\t}\n \t}\n }\n \n+struct receive_hook_feed_context {\n+\tstruct command *cmd;\n+\tint skip_broken;\n+};\n+\n struct receive_hook_feed_state {\n \tstruct command *cmd;\n \tstruct ref_push_report *report;\n \tint skip_broken;\n \tstruct strbuf buf;\n-\tconst struct string_list *push_options;\n };\n \n-typedef int (*feed_fn)(void *, const char **, size_t *);\n-static int run_and_feed_hook(const char *hook_name, feed_fn feed,\n-\t\t\t     struct receive_hook_feed_state *feed_state)\n-{\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct async muxer;\n-\tconst char *argv[2];\n-\tint code;\n-\n-\targv[0] = find_hook(hook_name);\n-\tif (!argv[0])\n-\t\treturn 0;\n-\n-\targv[1] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = hook_name;\n-\n-\tif (feed_state->push_options) {\n-\t\tint i;\n-\t\tfor (i = 0; i < feed_state->push_options->nr; i++)\n-\t\t\tstrvec_pushf(&proc.env_array,\n-\t\t\t\t     \"GIT_PUSH_OPTION_%d=%s\", i,\n-\t\t\t\t     feed_state->push_options->items[i].string);\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT=%d\",\n-\t\t\t     feed_state->push_options->nr);\n-\t} else\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT\");\n-\n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&proc.env_array, tmp_objdir_env(tmp_objdir));\n-\n-\tif (use_sideband) {\n-\t\tmemset(&muxer, 0, sizeof(muxer));\n-\t\tmuxer.proc = copy_to_sideband;\n-\t\tmuxer.in = -1;\n-\t\tcode = start_async(&muxer);\n-\t\tif (code)\n-\t\t\treturn code;\n-\t\tproc.err = muxer.in;\n-\t}\n-\n-\tprepare_push_cert_sha1(&proc);\n-\n-\tcode = start_command(&proc);\n-\tif (code) {\n-\t\tif (use_sideband)\n-\t\t\tfinish_async(&muxer);\n-\t\treturn code;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\n-\twhile (1) {\n-\t\tconst char *buf;\n-\t\tsize_t n;\n-\t\tif (feed(feed_state, &buf, &n))\n-\t\t\tbreak;\n-\t\tif (write_in_full(proc.in, buf, n) < 0)\n-\t\t\tbreak;\n-\t}\n-\tclose(proc.in);\n-\tif (use_sideband)\n-\t\tfinish_async(&muxer);\n-\n-\tsigchain_pop(SIGPIPE);\n-\n-\treturn finish_command(&proc);\n-}\n-\n-static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n+static int feed_receive_hook(void *state_)\n {\n \tstruct receive_hook_feed_state *state = state_;\n \tstruct command *cmd = state->cmd;\n@@ -884,9 +816,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t       state->skip_broken && (cmd->error_string || cmd->did_not_exist))\n \t\tcmd = cmd->next;\n \tif (!cmd)\n-\t\treturn -1; /* EOF */\n-\tif (!bufp)\n-\t\treturn 0; /* OK, can feed something. */\n+\t\treturn 1; /* EOF - close the pipe*/\n \tstrbuf_reset(&state->buf);\n \tif (!state->report)\n \t\tstate->report = cmd->report;\n@@ -910,32 +840,36 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t\t\t    cmd->ref_name);\n \t\tstate->cmd = cmd->next;\n \t}\n-\tif (bufp) {\n-\t\t*bufp = state->buf.buf;\n-\t\t*sizep = state->buf.len;\n-\t}\n \treturn 0;\n }\n \n-static int run_receive_hook(struct command *commands,\n-\t\t\t    const char *hook_name,\n-\t\t\t    int skip_broken,\n-\t\t\t    const struct string_list *push_options)\n+static int feed_receive_hook_cb(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n {\n-\tstruct receive_hook_feed_state state;\n-\tint status;\n-\n-\tstrbuf_init(&state.buf, 0);\n-\tstate.cmd = commands;\n-\tstate.skip_broken = skip_broken;\n-\tstate.report = NULL;\n-\tif (feed_receive_hook(&state, NULL, NULL))\n-\t\treturn 0;\n-\tstate.cmd = commands;\n-\tstate.push_options = push_options;\n-\tstatus = run_and_feed_hook(hook_name, feed_receive_hook, &state);\n-\tstrbuf_release(&state.buf);\n-\treturn status;\n+\tstruct hook *hook = pp_task_cb;\n+\tstruct receive_hook_feed_state *feed_state = hook->feed_pipe_cb_data;\n+\tint rc;\n+\n+\t/* first-time setup */\n+\tif (!feed_state) {\n+\t\tstruct hook_cb_data *hook_cb = pp_cb;\n+\t\tstruct run_hooks_opt *opt = hook_cb->options;\n+\t\tstruct receive_hook_feed_context *ctx = opt->feed_pipe_ctx;\n+\t\tif (!ctx)\n+\t\t\tBUG(\"run_hooks_opt.feed_pipe_ctx required for receive hook\");\n+\n+\t\tfeed_state = xmalloc(sizeof(struct receive_hook_feed_state));\n+\t\tstrbuf_init(&feed_state->buf, 0);\n+\t\tfeed_state->cmd = ctx->cmd;\n+\t\tfeed_state->skip_broken = ctx->skip_broken;\n+\t\tfeed_state->report = NULL;\n+\n+\t\thook->feed_pipe_cb_data = feed_state;\n+\t}\n+\n+\trc = feed_receive_hook(feed_state);\n+\tif (!rc)\n+\t\tstrbuf_addbuf(pipe, &feed_state->buf);\n+\treturn rc;\n }\n \n static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n@@ -971,6 +905,57 @@ static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n \tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n }\n \n+static int run_receive_hook(struct command *commands,\n+\t\t\t    const char *hook_name,\n+\t\t\t    int skip_broken,\n+\t\t\t    const struct string_list *push_options)\n+{\n+\tstruct run_hooks_opt opt;\n+\tstruct receive_hook_feed_context ctx;\n+\tint rc;\n+\tstruct command *iter = commands;\n+\n+\trun_hooks_opt_init_async(&opt);\n+\n+\t/* if there are no valid commands, don't invoke the hook at all. */\n+\twhile (iter && skip_broken && (iter->error_string || iter->did_not_exist))\n+\t\titer = iter->next;\n+\tif (!iter)\n+\t\treturn 0;\n+\n+\t/* pre-receive hooks should run in series as the hook updates refs */\n+\tif (!strcmp(hook_name, \"pre-receive\"))\n+\t\topt.jobs = 1;\n+\n+\tif (push_options) {\n+\t\tint i;\n+\t\tfor (i = 0; i < push_options->nr; i++)\n+\t\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_%d=%s\", i,\n+\t\t\t\t     push_options->items[i].string);\n+\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_COUNT=%d\", push_options->nr);\n+\t} else\n+\t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n+\n+\tif (tmp_objdir)\n+\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\n+\tprepare_push_cert_sha1(&opt);\n+\n+\t/* set up sideband printer */\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\t/* set up stdin callback */\n+\tctx.cmd = commands;\n+\tctx.skip_broken = skip_broken;\n+\topt.feed_pipe = feed_receive_hook_cb;\n+\topt.feed_pipe_ctx = &ctx;\n+\n+\trc = run_hooks(hook_name, &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n+}\n+\n static int run_update_hook(struct command *cmd)\n {\n \tstruct run_hooks_opt opt;\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425635","messageId":"20210527000856.695702-34-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 33/37] bugreport: use hook_exists instead of find_hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:52Z","receivedAt":"2021-05-27T00:11:15Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the helper in hook.h instead of the one in run-command.h, we\ncan also check whether a hook exists in the config - not just whether it\nexists in the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/bugreport.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 9915a5841d..190272ba70 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -3,7 +3,7 @@\n #include \"strbuf.h\"\n #include \"help.h\"\n #include \"compat/compiler.h\"\n-#include \"run-command.h\"\n+#include \"hook.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\n@@ -82,7 +82,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t}\n \n \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (find_hook(hook[i]))\n+\t\tif (hook_exists(hook[i], HOOKDIR_USE_CONFIG))\n \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n }\n \n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425636","messageId":"20210527000856.695702-35-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 34/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:53Z","receivedAt":"2021-05-27T00:11:20Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"By using the new 'git hook run' subcommand to run 'sendemail-validate',\nwe can reduce the boilerplate needed to run this hook in perl. Using\nconfig-based hooks also allows us to run 'sendemail-validate' hooks that\nwere configured globally when running 'git send-email' from outside of a\nGit directory, alongside other benefits like multihooks and\nparallelization.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n git-send-email.perl   | 26 ++++++--------------------\n t/t9001-send-email.sh | 13 +------------\n 2 files changed, 7 insertions(+), 32 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 170f42fe21..b55687453e 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -1958,26 +1958,12 @@ sub unique_email_list {\n sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n-\tif ($repo) {\n-\t\tmy $validate_hook = catfile($repo->hooks_path(),\n-\t\t\t\t\t    'sendemail-validate');\n-\t\tmy $hook_error;\n-\t\tif (-x $validate_hook) {\n-\t\t\tmy $target = abs_path($fn);\n-\t\t\t# The hook needs a correct cwd and GIT_DIR.\n-\t\t\tmy $cwd_save = cwd();\n-\t\t\tchdir($repo->wc_path() or $repo->repo_path())\n-\t\t\t\tor die(\"chdir: $!\");\n-\t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n-\t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n-\t\t}\n-\t\tif ($hook_error) {\n-\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n-\t\t\t\t       \"%s\\n\" .\n-\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n-\t\t}\n-\t}\n+\tmy $target = abs_path($fn);\n+\n+\tsystem_or_die([\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-j1\", \"-a\", $target],\n+\t\tsprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n+\t\t\t   \"warning: no patches were sent\\n\"),\n+\t\t        $fn));\n \n \t# Any long lines will be automatically fixed if we use a suitable transfer\n \t# encoding.\ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex aa603cf4d0..bdf6472871 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -539,7 +539,6 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'$PWD/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -557,7 +556,6 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'$PWD/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -2170,16 +2168,7 @@ test_expect_success $PREREQ 'invoke hook' '\n \tmkdir -p .git/hooks &&\n \n \twrite_script .git/hooks/sendemail-validate <<-\\EOF &&\n-\t# test that we have the correct environment variable, pwd, and\n-\t# argument\n-\tcase \"$GIT_DIR\" in\n-\t*.git)\n-\t\ttrue\n-\t\t;;\n-\t*)\n-\t\tfalse\n-\t\t;;\n-\tesac &&\n+\t# test that we have the correct argument\n \ttest -f 0001-add-main.patch &&\n \tgrep \"add main\" \"$1\"\n \tEOF\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425637","messageId":"20210527000856.695702-36-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 35/37] run-command: stop thinking about hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:54Z","receivedAt":"2021-05-27T00:11:28Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"hook.h has replaced all run-command.h hook-related functionality.\nrun-command.h:run_hooks_le/ve and find_hook are no longer used anywhere\nin the codebase. So, let's delete the dead code - or, in the one case\nwhere it's still needed, move it to an internal function in hook.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n hook.c        | 39 ++++++++++++++++++++++++++++--\n run-command.c | 66 ---------------------------------------------------\n run-command.h | 24 -------------------\n 3 files changed, 37 insertions(+), 92 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 27da1fdb32..ff80e52edd 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -218,6 +218,41 @@ static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n \t}\n }\n \n+static const char *find_legacy_hook(const char *name)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\n+\tstrbuf_reset(&path);\n+\tstrbuf_git_path(&path, \"hooks/%s\", name);\n+\tif (access(path.buf, X_OK) < 0) {\n+\t\tint err = errno;\n+\n+#ifdef STRIP_EXTENSION\n+\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n+\t\tif (access(path.buf, X_OK) >= 0)\n+\t\t\treturn path.buf;\n+\t\tif (errno == EACCES)\n+\t\t\terr = errno;\n+#endif\n+\n+\t\tif (err == EACCES && advice_ignored_hook) {\n+\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n+\n+\t\t\tif (!string_list_lookup(&advise_given, name)) {\n+\t\t\t\tstring_list_insert(&advise_given, name);\n+\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n+\t\t\t\t\t \"it's not set as executable.\\n\"\n+\t\t\t\t\t \"You can disable this warning with \"\n+\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n+\t\t\t\t       path.buf);\n+\t\t\t}\n+\t\t}\n+\t\treturn NULL;\n+\t}\n+\treturn path.buf;\n+}\n+\n+\n struct list_head* hook_list(const char* hookname)\n {\n \tstruct strbuf hook_key = STRBUF_INIT;\n@@ -234,7 +269,7 @@ struct list_head* hook_list(const char* hookname)\n \tgit_config(hook_config_lookup, &cb_data);\n \n \tif (have_git_dir()) {\n-\t\tconst char *legacy_hook_path = find_hook(hookname);\n+\t\tconst char *legacy_hook_path = find_legacy_hook(hookname);\n \n \t\t/* Unconditionally add legacy hook, but annotate it. */\n \t\tif (legacy_hook_path) {\n@@ -283,7 +318,7 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n \tcould_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||\n \t\t\t\tshould_run_hookdir == HOOKDIR_WARN ||\n \t\t\t\tshould_run_hookdir == HOOKDIR_YES)\n-\t\t\t\t&& !!find_hook(hookname);\n+\t\t\t\t&& !!find_legacy_hook(hookname);\n \n \tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n \ndiff --git a/run-command.c b/run-command.c\nindex 27135defb8..2ff76f3c2f 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1320,72 +1320,6 @@ int async_with_fork(void)\n #endif\n }\n \n-const char *find_hook(const char *name)\n-{\n-\tstatic struct strbuf path = STRBUF_INIT;\n-\n-\tstrbuf_reset(&path);\n-\tstrbuf_git_path(&path, \"hooks/%s\", name);\n-\tif (access(path.buf, X_OK) < 0) {\n-\t\tint err = errno;\n-\n-#ifdef STRIP_EXTENSION\n-\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n-\t\tif (access(path.buf, X_OK) >= 0)\n-\t\t\treturn path.buf;\n-\t\tif (errno == EACCES)\n-\t\t\terr = errno;\n-#endif\n-\n-\t\tif (err == EACCES && advice_ignored_hook) {\n-\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n-\n-\t\t\tif (!string_list_lookup(&advise_given, name)) {\n-\t\t\t\tstring_list_insert(&advise_given, name);\n-\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n-\t\t\t\t\t \"it's not set as executable.\\n\"\n-\t\t\t\t\t \"You can disable this warning with \"\n-\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n-\t\t\t\t       path.buf);\n-\t\t\t}\n-\t\t}\n-\t\treturn NULL;\n-\t}\n-\treturn path.buf;\n-}\n-\n-int run_hook_ve(const char *const *env, const char *name, va_list args)\n-{\n-\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\tconst char *p;\n-\n-\tp = find_hook(name);\n-\tif (!p)\n-\t\treturn 0;\n-\n-\tstrvec_push(&hook.args, p);\n-\twhile ((p = va_arg(args, const char *)))\n-\t\tstrvec_push(&hook.args, p);\n-\thook.env = env;\n-\thook.no_stdin = 1;\n-\thook.stdout_to_stderr = 1;\n-\thook.trace2_hook_name = name;\n-\n-\treturn run_command(&hook);\n-}\n-\n-int run_hook_le(const char *const *env, const char *name, ...)\n-{\n-\tva_list args;\n-\tint ret;\n-\n-\tva_start(args, name);\n-\tret = run_hook_ve(env, name, args);\n-\tva_end(args);\n-\n-\treturn ret;\n-}\n-\n struct io_pump {\n \t/* initialized by caller */\n \tint fd;\ndiff --git a/run-command.h b/run-command.h\nindex ebc4a95a94..7150da851a 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -201,30 +201,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/*\n- * Returns the path to the hook file, or NULL if the hook is missing\n- * or disabled. Note that this points to static storage that will be\n- * overwritten by further calls to find_hook and run_hook_*.\n- */\n-const char *find_hook(const char *name);\n-\n-/**\n- * Run a hook.\n- * The first argument is a pathname to an index file, or NULL\n- * if the hook uses the default index file or no index is needed.\n- * The second argument is the name of the hook.\n- * The further arguments correspond to the hook arguments.\n- * The last argument has to be NULL to terminate the arguments list.\n- * If the hook does not exist or is not executable, the return\n- * value will be zero.\n- * If it is executable, the hook will be executed and the exit\n- * status of the hook is returned.\n- * On execution, .stdout_to_stderr and .no_stdin will be set.\n- */\n-LAST_ARG_MUST_BE_NULL\n-int run_hook_le(const char *const *env, const char *name, ...);\n-int run_hook_ve(const char *const *env, const char *name, va_list args);\n-\n /*\n  * Trigger an auto-gc\n  */\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425638","messageId":"20210527000856.695702-37-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 36/37] doc: clarify fsmonitor-watchman specification","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:55Z","receivedAt":"2021-05-27T00:11:29Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"fsmonitor-watchman is not specified in the same way that other hooks\nare. In fsmonitor.c:query_fsmonitor(), the path stored in\n'core_fsmonitor' is executed directly via\nrun-command.h:capture_command(). 'core_fsmonitor' is set during 'git\nupdate-index' via config.c:git_config_get_fsmonitor(). Neither\nbuiltin/update-index.c, nor config.c, nor fsmonitor.c check  that the\npath given is in '.git/hooks'.\n\nNone of the existing hook execution code is used by fsmonitor.c to\ninvoke fsmonitor-watchman, because that executable isn't expected to\nreside in '.git/hooks'.\n\nFurthermore, it doesn't make sense to specify the fsmonitor-watchman\nhook more than once, and that hook itself may soon be superseded by\nnative logic to talk to an fsmonitor daemon directly from the Git\nexecutable. (See <e019cc71-ac39-44a4-0a23-b3b7decec754@jeffhostetler.com>\nfor more information.)\n\nTherefore, let's correct the user-facing documentation around\nfsmonitor-watchman and clarify that it won't be supported by\nconfig-based hooks.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/githooks.txt | 9 ++++++---\n 1 file changed, 6 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 78cc4e0872..42e66d4e2d 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -649,9 +649,12 @@ e-mails.\n fsmonitor-watchman\n ~~~~~~~~~~~~~~~~~~\n \n-This hook is invoked when the configuration option `core.fsmonitor` is\n-set to `.git/hooks/fsmonitor-watchman` or `.git/hooks/fsmonitor-watchmanv2`\n-depending on the version of the hook to use.\n+This hook is invoked when the configuration option `core.fsmonitor` is set to a\n+path containing an executable. It *cannot* be specified via the usual\n+`hook.fsmonitor-watchman.command` configuration or by providing an executable\n+in `.git/hooks/fsmonitor-watchman`. The arguments provided to the hook are\n+determined by the value of the `core.fsmonitorHookVersion` configuration\n+option.\n \n Version 1 takes two arguments, a version (1) and the time in elapsed\n nanoseconds since midnight, January 1, 1970.\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425639","messageId":"20210527000856.695702-38-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"[PATCH v9 37/37] docs: link githooks and git-hook manpages","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-05-27T00:08:56Z","receivedAt":"2021-05-27T00:11:32Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Since users may have an easier time finding 'man githooks' or 'git help\ngithooks' through tab-completion or muscle memory, reference the 'git\nhook' commands. And in the 'git hook' manual, point users back to 'man\ngithooks' for specifics about the hook events themselves.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt | 12 ++++++++++++\n Documentation/githooks.txt |  7 ++++---\n 2 files changed, 16 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 81b8e94994..24e00a6f4a 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -17,6 +17,13 @@ DESCRIPTION\n You can list and run configured hooks with this command. Later, you will be able\n to add and modify hooks with this command.\n \n+In general, when instructions suggest adding a script to\n+`.git/hooks/<something>`, you can specify it in the config instead by running\n+`git config --add hook.<something>.command <path-to-script>` - this way you can\n+share the script between multiple repos. That is, `cp ~/my-script.sh\n+~/project/.git/hooks/pre-commit` would become `git config --add\n+hook.pre-commit.command ~/my-script.sh`.\n+\n This command parses the default configuration files for sections `hook` and\n `hookcmd`. `hook` is used to describe the commands which will be run during a\n particular hook event; commands are run in the order Git encounters them during\n@@ -141,6 +148,11 @@ number of CPUs on the current system. Some hooks may be ineligible for\n parallelization: for example, 'commit-msg' intends hooks modify the commit\n message body and cannot be parallelized.\n \n+HOOKS\n+-----\n+For a list of hooks which can be configured and how they work, see\n+linkgit:githooks[5].\n+\n CONFIGURATION\n -------------\n include::config/hook.txt[]\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex 42e66d4e2d..d780cb3b18 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -7,15 +7,16 @@ githooks - Hooks used by Git\n \n SYNOPSIS\n --------\n+'git hook'\n $GIT_DIR/hooks/* (or \\`git config core.hooksPath`/*)\n \n \n DESCRIPTION\n -----------\n \n-Hooks are programs you can place in a hooks directory to trigger\n-actions at certain points in git's execution. Hooks that don't have\n-the executable bit set are ignored.\n+Hooks are programs you can specify in your config (see linkgit:git-hook[1]) or\n+place in a hooks directory to trigger actions at certain points in git's\n+execution. Hooks that don't have the executable bit set are ignored.\n \n By default the hooks directory is `$GIT_DIR/hooks`, but that can be\n changed via the `core.hooksPath` configuration variable (see\n-- \n2.31.1.818.g46aad6cb9e-goog\n\n"},{"id":"425647","messageId":"xmqqim34api3.fsf@gitster.g","threadId":"55286","inReplyTo":"20210527000856.695702-3-emilyshaffer@google.com","subject":"Re: [PATCH v9 02/37] hook: introduce git-hook subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-05-27T02:18:12Z","receivedAt":"2021-05-27T02:18:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> diff --git a/hook.c b/hook.c\n> new file mode 100644\n> index 0000000000..d3e28aa73a\n> --- /dev/null\n> +++ b/hook.c\n> @@ -0,0 +1,120 @@\n> +#include \"cache.h\"\n> +\n> +#include \"hook.h\"\n> +#include \"config.h\"\n> +\n> +void free_hook(struct hook *ptr)\n\nShould this be \"static\" (and be removed from hook.h)?  Even after\nthe whole series, nobody outside seems to use this function.\n"},{"id":"425666","messageId":"87zgwgl7hd.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"Re: [PATCH v9 00/37] propose config-based hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-27T11:49:42Z","receivedAt":"2021-05-27T11:52:06Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, May 26 2021, Emily Shaffer wrote:\n\n> After much delay and $DAYJOB, here is v9.\n\nThanks. Haven't done any deep review of this yet. Just skimming things\nfrom v8 & commenting as I go along...\n\n> - Addressed nits in reviews on v8\n> [...\n>   Ævar's updated system_or_die() function\n> - changed strbuf to char* in hooks_list\n>   - Attempted to do so in run_command's stdout callback, but this made\n>     length protection difficult, so stuck with strbuf there.\n\nI see there's still quite a bit of that strbuf churn still in this\nseries, e.g. unfixed issues noted in\nhttps://lore.kernel.org/git/87pn04g0r1.fsf@evledraar.gmail.com/ e.g. in\n07/37 you're still doing this:\n\t\n\t+\tstruct strbuf hookname = STRBUF_INIT;\n\t+ [...]\n\t+\tstrbuf_addstr(&hookname, argv[0]);\n\t+\topt.run_hookdir = should_run_hookdir;\n\t+\n\t+\trc = run_hooks(hookname.buf, &opt);\n\t+\n\t+\tstrbuf_release(&hookname);\n\nSo fair enough n the run_command's stdout callback, but it seems there's\nstill quite a bit of strbuf encapsulating for no apparent benefit.\n"},{"id":"425668","messageId":"87wnrkl70z.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210527000856.695702-35-emilyshaffer@google.com","subject":"Re: [PATCH v9 34/37] git-send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-27T11:56:24Z","receivedAt":"2021-05-27T12:02:00Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, May 26 2021, Emily Shaffer wrote:\n\n> By using the new 'git hook run' subcommand to run 'sendemail-validate',\n> we can reduce the boilerplate needed to run this hook in perl. Using\n> config-based hooks also allows us to run 'sendemail-validate' hooks that\n> were configured globally when running 'git send-email' from outside of a\n> Git directory, alongside other benefits like multihooks and\n> parallelization.\n>\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  git-send-email.perl   | 26 ++++++--------------------\n>  t/t9001-send-email.sh | 13 +------------\n>  2 files changed, 7 insertions(+), 32 deletions(-)\n>\n> diff --git a/git-send-email.perl b/git-send-email.perl\n> index 170f42fe21..b55687453e 100755\n> --- a/git-send-email.perl\n> +++ b/git-send-email.perl\n> @@ -1958,26 +1958,12 @@ sub unique_email_list {\n>  sub validate_patch {\n>  \tmy ($fn, $xfer_encoding) = @_;\n>  \n> -\tif ($repo) {\n> -\t\tmy $validate_hook = catfile($repo->hooks_path(),\n> -\t\t\t\t\t    'sendemail-validate');\n> -\t\tmy $hook_error;\n> -\t\tif (-x $validate_hook) {\n> -\t\t\tmy $target = abs_path($fn);\n> -\t\t\t# The hook needs a correct cwd and GIT_DIR.\n> -\t\t\tmy $cwd_save = cwd();\n> -\t\t\tchdir($repo->wc_path() or $repo->repo_path())\n> -\t\t\t\tor die(\"chdir: $!\");\n> -\t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n> -\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n> -\t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n> -\t\t}\n> -\t\tif ($hook_error) {\n> -\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n> -\t\t\t\t       \"%s\\n\" .\n> -\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n> -\t\t}\n> -\t}\n> +\tmy $target = abs_path($fn);\n> +\n> +\tsystem_or_die([\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-j1\", \"-a\", $target],\n\nHow do we get the \"benefit[ ... of ...] parallelization\" here if -j1 is\nhardcoded as parameter. Shouldn't that be removed so it'll use the\nhook.jobs config like everything else?\n\n> +\t\tsprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n> +\t\t\t   \"warning: no patches were sent\\n\"),\n> +\t\t        $fn));\n>  \n>  \t# Any long lines will be automatically fixed if we use a suitable transfer\n>  \t# encoding.\n> diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\n> index aa603cf4d0..bdf6472871 100755\n> --- a/t/t9001-send-email.sh\n> +++ b/t/t9001-send-email.sh\n> @@ -539,7 +539,6 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n>  \ttest_path_is_file my-hooks.ran &&\n>  \tcat >expect <<-EOF &&\n>  \tfatal: longline.patch: rejected by sendemail-validate hook\n> -\tfatal: command '\"'\"'$PWD/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n>  \twarning: no patches were sent\n>  \tEOF\n>  \ttest_cmp expect actual\n> @@ -557,7 +556,6 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n>  \ttest_path_is_file my-hooks.ran &&\n>  \tcat >expect <<-EOF &&\n>  \tfatal: longline.patch: rejected by sendemail-validate hook\n> -\tfatal: command '\"'\"'$PWD/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n>  \twarning: no patches were sent\n>  \tEOF\n>  \ttest_cmp expect actual\n\nWouldn't it be better to keep the \"died with exit code N\" here. I.e. something like:\n\n\tfatal: longline.patch: rejected by sendemail-validate hook, 'git hook run' died with exit code %d\n\nI.e. isn't that exit code carried forward by \"git hook run\"?\n\n> @@ -2170,16 +2168,7 @@ test_expect_success $PREREQ 'invoke hook' '\n>  \tmkdir -p .git/hooks &&\n>  \n>  \twrite_script .git/hooks/sendemail-validate <<-\\EOF &&\n> -\t# test that we have the correct environment variable, pwd, and\n> -\t# argument\n> -\tcase \"$GIT_DIR\" in\n> -\t*.git)\n> -\t\ttrue\n> -\t\t;;\n> -\t*)\n> -\t\tfalse\n> -\t\t;;\n> -\tesac &&\n> +\t# test that we have the correct argument\n>  \ttest -f 0001-add-main.patch &&\n>  \tgrep \"add main\" \"$1\"\n>  \tEOF\n\nWe don't want to or need to check $GIT_DIR at all? Maybe not, but the\ncommit message doesn't say anything about it...\n"},{"id":"425674","messageId":"87lf80l1m6.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210527000856.695702-1-emilyshaffer@google.com","subject":"Re: [PATCH v9 00/37] propose config-based hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-27T13:36:04Z","receivedAt":"2021-05-27T13:58:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, May 26 2021, Emily Shaffer wrote:\n\n> After much delay and $DAYJOB, here is v9.\n>\n> - Addressed nits in reviews on v8\n> - sendemail-validate hook becomes non-parallelized; updated to use\n>   Ævar's updated system_or_die() function\n> - changed strbuf to char* in hooks_list\n>   - Attempted to do so in run_command's stdout callback, but this made\n>     length protection difficult, so stuck with strbuf there.\n> - test_i18ncmp -> test_cmp\n> - Stop doing i18n lego in run_hooks()\n> - Checked that run_hooks_opt_init() is always separated by a space from\n>   variable decl blocks\n> - Checked for early returns which may skip run_hooks_opt_clear(); this\n>   resulted in minimizing the scope of run_hooks_opt in most places\n> - Got rid of native-hooks.txt. It was a nice idea, but not attached to a\n>   large and slow series like this one.\n> - In traces, log the name of the hook (e.g. \"pre-commit\") instead of the\n>   name of the executable (e.g. \"/home/emily/check-for-debug-strings\");\n>   the executable name is tracelogged as part of argv anyways, and we\n>   want to be able to tell which hook was responsible for invoking the\n>   executable in question.\n\nIn v8 I had some comments about the overall approach here. I've got to\nsay I'm disappointed that that's neither been replied to or in any way\naddressed:\n\n    https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n\nAlso referenced in several follow-up discussions, including Junio's\ncomment of \"OK, Emily, I guess the ball is in your court now?\":\n\n    https://lore.kernel.org/git/?q=87mtv8fww3.fsf%40evledraar.gmail.com\n\nBasically I think that this very large series could be easily split into\nmuch more digestable chunks if it were re-arranged. Right now it's\nessentially a 37 patch mix of, in order:\n\n 1. Design doc\n 2. Add \"git hook\" with \"run\", \"list\" etc.\n 3. Make that more fully featured, support config, legacy hooks et.c\n 4. Implement parallel hooks\n 5. Go through N existing hook callers and migrate them to \"git hook run\"\n 6. Some misc updates, e.g. to docs\n\nIf it were instead:\n\n 1. Add \"git hook run\", only supports \"old/legacy\" .git/hooks/ hooks\n 2. Go through N existing hook callers and migrate them to \"git hook run\"\n\nThat would be at least half of this series, and it would be much more\nnarrow change that would demonstrably retain all existing\nsemantics. We'd simply call our hooks via a command wrapper instead of\nvia run_command(<path to name>) as we do now. So we could have that land\nand then focus on the actual behavior changes later.\n\nIn earlier rounds/the above E-Mail I asked something to the effect of if\nyou thought that was a good approach, whether disagreed, or just thought\nyou didn't have time for it.\n\nI'm still keen to help get this in, but given the non-responses don't\nknow where you stand on it. I suppose I could re-arrange this myself and\nsubmit an alternate \"prep\" series to rebase this on, but wanted to get\nyour take first.\n\nAside from the suggestion of splitting it up:\n\nIn the above referenced correspondence I expressed concerns that the\nconfig layout you're proposing needlessly creates complexity requiring a\n\"git hook list\" etc. command to solve, as opposed to not having that, or\nhave it by a trivial synonym for a \"git config --show-origin --list\"\ninvocation.\n\nI'm still interested in what you think of that, as being able to\nnormalize that more invites getting rid of more complexity without\nimpacting the end-goal of hooks via config.\n\nBut in any case such a discussion could be had later and on a smaller\nseries if the refactoring of existing hook running was split up from the\nbig semantic changes here, which are currently tied up with that\nrefactoring.\n\nThanks!\n"},{"id":"425699","messageId":"60afdaef95fa9_2653020894@natae.notmuch","threadId":"55286","inReplyTo":"87lf80l1m6.fsf@evledraar.gmail.com","subject":"Re: [PATCH v9 00/37] propose config-based hooks","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-27T17:46:23Z","receivedAt":"2021-05-27T17:46:39Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> If it were instead:\n> \n>  1. Add \"git hook run\", only supports \"old/legacy\" .git/hooks/ hooks\n>  2. Go through N existing hook callers and migrate them to \"git hook run\"\n\nYes please. It would make the series much easier to review, and also to\nunderstand what the point of it is.\n\n-- \nFelipe Contreras"},{"id":"425716","messageId":"87fsy7lqve.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210527000856.695702-23-emilyshaffer@google.com","subject":"Re: [PATCH v9 22/37] read-cache: convert post-index-change hook to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-27T23:04:40Z","receivedAt":"2021-05-27T23:05:33Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, May 26 2021, Emily Shaffer wrote:\n\n>  Part of the linkgit:git[1] suite\n> diff --git a/read-cache.c b/read-cache.c\n> index 1b3c2eb408..6a5c9403f4 100644\n> --- a/read-cache.c\n> +++ b/read-cache.c\n> @@ -26,6 +26,8 @@\n>  #include \"thread-utils.h\"\n>  #include \"progress.h\"\n>  #include \"sparse-index.h\"\n> +#include \"hook.h\"\n> +>>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)\n\nThis adds a conflict marker, which is removed later in the series.\n\nObviously a trivial mistake, but it's a good idea to use git rebase -i\n-x 'make test' or equivalent for such a large series, perhaps there are\nother inter-patch issues lurking here...\n"},{"id":"425722","messageId":"YLBC5eNBDHwb4TvG@nand.local","threadId":"55286","inReplyTo":"87fsy7lqve.fsf@evledraar.gmail.com","subject":"Re: [PATCH v9 22/37] read-cache: convert post-index-change hook to use config","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2021-05-28T01:09:57Z","receivedAt":"2021-05-28T01:10:09Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Fri, May 28, 2021 at 01:04:40AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> Obviously a trivial mistake, but it's a good idea to use git rebase -i\n> -x 'make test' or equivalent for such a large series, perhaps there are\n> other inter-patch issues lurking here...\n\nThis is tangential to this series, but I would add that `git rebase -x\n'make DEVELOPER=1 git' @{u}` can be useful to run often while developing\nthe series and reorganizing patches to make sure that everything\ncompiles.\n\nThe benefit of `make git` is that it ensures everything still compiles\nwhile avoiding having to link everything together. This ends up being\nquick enough that I find myself running it often while developing a\nseries.\n\nI do `s/git/test` before submitting, though.\n\nThanks,\nTaylor\n"},{"id":"425766","messageId":"patch-01.31-8ac2efc71a0-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 01/31] hooks tests: don't leave \"actual\" nonexisting on failure","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:03Z","receivedAt":"2021-05-28T12:11:48Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Start by creating an \"actual\" file in a core.hooksPath test that has\nthe hook echoing to the \"actual\" file. We later test_cmp that file to\nsee what hooks were run. Having that list of files be empty if we fail\ninstead of throwing an error about it being nonexistent makes for\nfriendlier output, we'll see what hooks we should have run.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t1350-config-hooks-path.sh | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\nindex f1f9aee9f5d..fa9647a7c0b 100755\n--- a/t/t1350-config-hooks-path.sh\n+++ b/t/t1350-config-hooks-path.sh\n@@ -5,6 +5,7 @@ test_description='Test the core.hooksPath configuration variable'\n . ./test-lib.sh\n \n test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n+\t>actual &&\n \tmkdir -p .git/custom-hooks .git/hooks &&\n \twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n \techo CUSTOM >>actual\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425767","messageId":"patch-02.31-eb37693f7dc-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 02/31] gc tests: add a test for the \"pre-auto-gc\" hook","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:04Z","receivedAt":"2021-05-28T12:11:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Add a missing test for the behavior of the pre-auto-gc hook added in\n0b85d92661e (Documentation/hooks: add pre-auto-gc hook, 2008-04-02).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t6500-gc.sh | 46 ++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 46 insertions(+)\n\ndiff --git a/t/t6500-gc.sh b/t/t6500-gc.sh\nindex 60d961b5260..10c7ae7f09c 100755\n--- a/t/t6500-gc.sh\n+++ b/t/t6500-gc.sh\n@@ -95,6 +95,52 @@ test_expect_success 'gc --keep-largest-pack' '\n \t)\n '\n \n+test_expect_success 'pre-auto-gc hook can stop auto gc' '\n+\tcat >err.expect <<-\\EOF &&\n+\tno gc for you\n+\tEOF\n+\n+\tgit init pre-auto-gc-hook &&\n+\t(\n+\t\tcd pre-auto-gc-hook &&\n+\t\twrite_script \".git/hooks/pre-auto-gc\" <<-\\EOF &&\n+\t\techo >&2 no gc for you &&\n+\t\texit 1\n+\t\tEOF\n+\n+\t\tgit config gc.auto 3 &&\n+\t\tgit config gc.autoDetach false &&\n+\n+\t\t# We need to create two object whose sha1s start with 17\n+\t\t# since this is what git gc counts.  As it happens, these\n+\t\t# two blobs will do so.\n+\t\ttest_commit \"$(test_oid obj1)\" &&\n+\t\ttest_commit \"$(test_oid obj2)\" &&\n+\n+\t\tgit gc --auto >../out.actual 2>../err.actual\n+\t) &&\n+\ttest_must_be_empty out.actual &&\n+\ttest_cmp err.expect err.actual &&\n+\n+\tcat >err.expect <<-\\EOF &&\n+\twill gc for you\n+\tAuto packing the repository for optimum performance.\n+\tSee \"git help gc\" for manual housekeeping.\n+\tEOF\n+\n+\t(\n+\t\tcd pre-auto-gc-hook &&\n+\t\twrite_script \".git/hooks/pre-auto-gc\" <<-\\EOF &&\n+\t\techo >&2 will gc for you &&\n+\t\texit 0\n+\t\tEOF\n+\t\tgit gc --auto >../out.actual 2>../err.actual\n+\t) &&\n+\n+\ttest_must_be_empty out.actual &&\n+\ttest_cmp err.expect err.actual\n+'\n+\n test_expect_success 'auto gc with too many loose objects does not attempt to create bitmaps' '\n \ttest_config gc.auto 3 &&\n \ttest_config gc.autodetach false &&\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425768","messageId":"patch-03.31-1ad4e69f7da-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 03/31] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:05Z","receivedAt":"2021-05-28T12:11:51Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn order to enable hooks to be run as an external process, by a\nstandalone Git command, or by tools which wrap Git, provide an external\nmeans to run all configured hook commands for a given hook event.\n\nMost of our hooks require more complex functionality than this, but\nlet's start with the bare minimum required to support our simplest\nhooks.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n .gitignore                 |   1 +\n Documentation/git-hook.txt |  36 ++++++++++\n Documentation/githooks.txt |   4 ++\n Makefile                   |   2 +\n builtin.h                  |   1 +\n builtin/hook.c             |  65 ++++++++++++++++++\n command-list.txt           |   1 +\n git.c                      |   1 +\n hook.c                     | 114 +++++++++++++++++++++++++++++++\n hook.h                     |  54 +++++++++++++++\n t/t1800-hook.sh            | 135 +++++++++++++++++++++++++++++++++++++\n 11 files changed, 414 insertions(+)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100644 hook.c\n create mode 100644 hook.h\n create mode 100755 t/t1800-hook.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 311841f9bed..de39dc9961b 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-hook\n /git-http-backend\n /git-http-fetch\n /git-http-push\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nnew file mode 100644\nindex 00000000000..902b9cffaef\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,36 @@\n+git-hook(1)\n+===========\n+\n+NAME\n+----\n+git-hook - run git hooks\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git hook' run <hook-name> [-- <hook-args>]\n+\n+DESCRIPTION\n+-----------\n+\n+This command is an interface to git hooks (see linkgit:githooks[5]).\n+Currently it only provides a convenience wrapper for running hooks for\n+use by git itself. In the future it might gain other functionality.\n+\n+SUBCOMMANDS\n+-----------\n+\n+run::\n+\n+\tRun the `<hook-name>` hook. Any positional arguments to the\n+\thook should be passed after an optional \"--\" (or\n+\t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n+\tthis accepts.\n+\n+SEE ALSO\n+--------\n+linkgit:githooks[5]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex b51959ff941..a16e62bc8c8 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -698,6 +698,10 @@ and \"0\" meaning they were not.\n Only one parameter should be set to \"1\" when the hook runs.  The hook\n running passing \"1\", \"1\" should not be possible.\n \n+SEE ALSO\n+--------\n+linkgit:git-hook[1]\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex c3565fc0f8f..a6b71a0fbed 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -901,6 +901,7 @@ LIB_OBJS += hash-lookup.o\n LIB_OBJS += hashmap.o\n LIB_OBJS += help.o\n LIB_OBJS += hex.o\n+LIB_OBJS += hook.o\n LIB_OBJS += ident.o\n LIB_OBJS += json-writer.o\n LIB_OBJS += kwset.o\n@@ -1101,6 +1102,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\ndiff --git a/builtin.h b/builtin.h\nindex 16ecd5586f0..91740c15149 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);\n int cmd_grep(int argc, const char **argv, const char *prefix);\n int cmd_hash_object(int argc, const char **argv, const char *prefix);\n int cmd_help(int argc, const char **argv, const char *prefix);\n+int cmd_hook(int argc, const char **argv, const char *prefix);\n int cmd_index_pack(int argc, const char **argv, const char *prefix);\n int cmd_init_db(int argc, const char **argv, const char *prefix);\n int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/hook.c b/builtin/hook.c\nnew file mode 100644\nindex 00000000000..1b1a594fd00\n--- /dev/null\n+++ b/builtin/hook.c\n@@ -0,0 +1,65 @@\n+#include \"cache.h\"\n+#include \"builtin.h\"\n+#include \"config.h\"\n+#include \"hook.h\"\n+#include \"parse-options.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+static const char * const builtin_hook_usage[] = {\n+\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n+\tNULL\n+};\n+\n+static int run(int argc, const char **argv, const char *prefix)\n+{\n+\tint i;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint rc = 0;\n+\tconst char *hook_name;\n+\tconst char *hook_path;\n+\n+\tstruct option run_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, run_options,\n+\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (argc > 2) {\n+\t\tif (strcmp(argv[2], \"--\") &&\n+\t\t    strcmp(argv[2], \"--end-of-options\"))\n+\t\t\t/* Having a -- for \"run\" is mandatory */\n+\t\t\tusage_with_options(builtin_hook_usage, run_options);\n+\t\t/* Add our arguments, start after -- */\n+\t\tfor (i = 3 ; i < argc; i++)\n+\t\t\tstrvec_push(&opt.args, argv[i]);\n+\t}\n+\n+\t/* Need to take into account core.hooksPath */\n+\tgit_config(git_default_config, NULL);\n+\n+\thook_name = argv[1];\n+\thook_path = find_hook(hook_name);\n+\tif (!hook_path) {\n+\t\terror(\"cannot find a hook named %s\", hook_name);\n+\t\treturn 1;\n+\t}\n+\trc = run_found_hooks(hook_name, hook_path, &opt);\n+\n+\trun_hooks_opt_clear(&opt);\n+\n+\treturn rc;\n+}\n+\n+int cmd_hook(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option builtin_hook_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\tif (!strcmp(argv[1], \"run\"))\n+\t\treturn run(argc, argv, prefix);\n+\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+\treturn 1;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex a289f09ed6f..9ccd8e5aebe 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-hook                                mainporcelain\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\n git-http-push                           synchelpers\ndiff --git a/git.c b/git.c\nindex 18bed9a9964..540909c391f 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nnew file mode 100644\nindex 00000000000..aa66c968186\n--- /dev/null\n+++ b/hook.c\n@@ -0,0 +1,114 @@\n+#include \"cache.h\"\n+#include \"hook.h\"\n+#include \"run-command.h\"\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o)\n+{\n+\tstrvec_clear(&o->env);\n+\tstrvec_clear(&o->args);\n+}\n+\n+static int pick_next_hook(struct child_process *cp,\n+\t\t\t  struct strbuf *out,\n+\t\t\t  void *pp_cb,\n+\t\t\t  void **pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *run_me = hook_cb->run_me;\n+\n+\tif (!run_me)\n+\t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n+\n+\tcp->no_stdin = 1;\n+\tcp->env = hook_cb->options->env.v;\n+\tcp->stdout_to_stderr = 1;\n+\tcp->trace2_hook_name = hook_cb->hook_name;\n+\n+\t/* add command */\n+\tstrvec_push(&cp->args, run_me->hook_path);\n+\n+\t/*\n+\t * add passed-in argv, without expanding - let the user get back\n+\t * exactly what they put in\n+\t */\n+\tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n+\n+\t/* Provide context for errors if necessary */\n+\t*pp_task_cb = run_me;\n+\n+\treturn 1;\n+}\n+\n+static int notify_start_failure(struct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cp)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *attempted = pp_task_cp;\n+\n+\t/* |= rc in cb */\n+\thook_cb->rc |= 1;\n+\n+\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n+\t\t    attempted->hook_path);\n+\n+\treturn 1;\n+}\n+\n+static int notify_hook_finished(int result,\n+\t\t\t\tstruct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\n+\t/* |= rc in cb */\n+\thook_cb->rc |= result;\n+\n+\treturn 1;\n+}\n+\n+\n+int run_found_hooks(const char *hook_name, const char *hook_path,\n+\t\t    struct run_hooks_opt *options)\n+{\n+\tstruct hook my_hook = {\n+\t\t.hook_path = hook_path,\n+\t};\n+\tstruct hook_cb_data cb_data = {\n+\t\t.rc = 0,\n+\t\t.hook_name = hook_name,\n+\t\t.options = options,\n+\t};\n+\tcb_data.run_me = &my_hook;\n+\n+\tif (options->jobs != 1)\n+\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n+\n+\trun_processes_parallel_tr2(options->jobs,\n+\t\t\t\t   pick_next_hook,\n+\t\t\t\t   notify_start_failure,\n+\t\t\t\t   notify_hook_finished,\n+\t\t\t\t   &cb_data,\n+\t\t\t\t   \"hook\",\n+\t\t\t\t   hook_name);\n+\n+\treturn cb_data.rc;\n+}\n+\n+int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n+{\n+\tconst char *hook_path;\n+\tint ret;\n+\tif (!options)\n+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n+\n+\thook_path = find_hook(hook_name);\n+\n+\t/* Care about nonexistence? Use run_found_hooks() */\n+\tif (!hook_path)\n+\t\treturn 0;\n+\n+\tret = run_found_hooks(hook_name, hook_path, options);\n+\treturn ret;\n+}\ndiff --git a/hook.h b/hook.h\nnew file mode 100644\nindex 00000000000..ebfee26bcf2\n--- /dev/null\n+++ b/hook.h\n@@ -0,0 +1,54 @@\n+#ifndef HOOK_H\n+#define HOOK_H\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+#include \"run-command.h\"\n+\n+struct hook {\n+\t/* The path to the hook */\n+\tconst char *hook_path;\n+};\n+\n+struct run_hooks_opt\n+{\n+\t/* Environment vars to be set for each hook */\n+\tstruct strvec env;\n+\n+\t/* Args to be passed to each hook */\n+\tstruct strvec args;\n+\n+\t/* Number of threads to parallelize across */\n+\tint jobs;\n+};\n+\n+#define RUN_HOOKS_OPT_INIT { \\\n+\t.jobs = 1, \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n+/*\n+ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n+ */\n+struct hook_cb_data {\n+\tint rc;\n+\tconst char *hook_name;\n+\tstruct hook *run_me;\n+\tstruct run_hooks_opt *options;\n+};\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o);\n+\n+/*\n+ * Calls find_hook(hookname) and runs the hooks (if any) with\n+ * run_found_hooks().\n+ */\n+int run_hooks(const char *hook_name, struct run_hooks_opt *options);\n+\n+/*\n+ * Takes an already resolved hook and runs it. Internally the simpler\n+ * run_hooks() will call this.\n+ */\n+int run_found_hooks(const char *hookname, const char *hook_path,\n+\t\t    struct run_hooks_opt *options);\n+#endif\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nnew file mode 100755\nindex 00000000000..7cacd27c748\n--- /dev/null\n+++ b/t/t1800-hook.sh\n@@ -0,0 +1,135 @@\n+#!/bin/bash\n+\n+test_description='git-hook command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup .git/hooks' '\n+\tmkdir .git/hooks\n+'\n+\n+test_expect_success 'git hook run -- nonexistent hook' '\n+\tcat >stderr.expect <<-\\EOF &&\n+\terror: cannot find a hook named does-not-exist\n+\tEOF\n+\ttest_expect_code 1 git hook run does-not-exist 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual\n+'\n+\n+test_expect_success 'git hook run -- basic' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tEOF\n+\tgit hook run test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run -- stdout and stderr handling' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo >&1 Will end up on stderr\n+\techo >&2 Will end up on stderr\n+\tEOF\n+\n+\tcat >stderr.expect <<-\\EOF &&\n+\tWill end up on stderr\n+\tWill end up on stderr\n+\tEOF\n+\tgit hook run test-hook >stdout.actual 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual &&\n+\ttest_must_be_empty stdout.actual\n+'\n+\n+test_expect_success 'git hook run -- exit codes are passed along' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 1\n+\tEOF\n+\n+\ttest_expect_code 1 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 2\n+\tEOF\n+\n+\ttest_expect_code 2 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 128\n+\tEOF\n+\n+\ttest_expect_code 128 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 129\n+\tEOF\n+\n+\ttest_expect_code 129 git hook run test-hook\n+'\n+\n+test_expect_success 'git hook run arg u ments without -- is not allowed' '\n+\ttest_expect_code 129 git hook run test-hook arg u ments\n+'\n+\n+test_expect_success 'git hook run -- pass arguments' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo $1\n+\techo $2\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\targ\n+\tu ments\n+\tEOF\n+\n+\tgit hook run test-hook -- arg \"u ments\" 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run -- out-of-repo runs excluded' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tnongit test_must_fail git hook run test-hook\n+'\n+\n+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n+\tmkdir my-hooks &&\n+\twrite_script my-hooks/test-hook <<-EOF &&\n+\techo Hook ran >>actual\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tHook ran\n+\tHook ran\n+\tHook ran\n+\tHook ran\n+\tEOF\n+\n+\t# Test various ways of specifying the path. See also\n+\t# t1350-config-hooks-path.sh\n+\t>actual &&\n+\tgit hook run test-hook 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks hook run test-hook 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks/ hook run test-hook 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook 2>>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n+\t>actual &&\n+\tmkdir -p .git/custom-hooks .git/hooks &&\n+\twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n+\techo CUSTOM >>actual\n+\tEOF\n+\twrite_script .git/hooks/pre-commit <<-\\EOF\n+\techo NORMAL >>actual\n+\tEOF\n+'\n+\n+test_done\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425769","messageId":"patch-04.31-1a67a1cc065-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 04/31] run-command.h: move find_hook() to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:06Z","receivedAt":"2021-05-28T12:11:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Move the find_hook() command to hook.h. Eventually all the hook\nrelated code will live there, let's move this function over as-is.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c           |  1 +\n builtin/bugreport.c    |  2 +-\n builtin/commit.c       |  1 +\n builtin/merge.c        |  1 +\n builtin/receive-pack.c |  1 +\n builtin/worktree.c     |  1 +\n hook.c                 | 36 ++++++++++++++++++++++++++++++++++++\n hook.h                 |  7 +++++++\n refs.c                 |  1 +\n run-command.c          | 35 +----------------------------------\n run-command.h          |  7 -------\n sequencer.c            |  1 +\n transport.c            |  1 +\n 13 files changed, 53 insertions(+), 42 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 0b2d886c81b..1c8a5489035 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -11,6 +11,7 @@\n #include \"parse-options.h\"\n #include \"dir.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"quote.h\"\n #include \"tempfile.h\"\n #include \"lockfile.h\"\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 9915a5841de..596f079a7f9 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -3,7 +3,7 @@\n #include \"strbuf.h\"\n #include \"help.h\"\n #include \"compat/compiler.h\"\n-#include \"run-command.h\"\n+#include \"hook.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 190d215d43b..f1aafd67d46 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -19,6 +19,7 @@\n #include \"revision.h\"\n #include \"wt-status.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"refs.h\"\n #include \"log-tree.h\"\n #include \"strbuf.h\"\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex eddb8ae70d6..5a10f6e3c96 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -13,6 +13,7 @@\n #include \"builtin.h\"\n #include \"lockfile.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"diff.h\"\n #include \"diff-merges.h\"\n #include \"refs.h\"\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a34742513ac..1e0e04c62fc 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -7,6 +7,7 @@\n #include \"pkt-line.h\"\n #include \"sideband.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"commit.h\"\n #include \"object.h\"\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 976bf8ed063..b1350640fed 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -8,6 +8,7 @@\n #include \"branch.h\"\n #include \"refs.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"sigchain.h\"\n #include \"submodule.h\"\n #include \"utf8.h\"\ndiff --git a/hook.c b/hook.c\nindex aa66c968186..06842f50e5c 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -2,6 +2,42 @@\n #include \"hook.h\"\n #include \"run-command.h\"\n \n+const char *find_hook(const char *name)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\n+\tstrbuf_reset(&path);\n+\tstrbuf_git_path(&path, \"hooks/%s\", name);\n+\tif (access(path.buf, X_OK) < 0) {\n+\t\tint err = errno;\n+\n+#ifdef STRIP_EXTENSION\n+\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n+\t\tif (access(path.buf, X_OK) >= 0)\n+\t\t\treturn path.buf;\n+\t\tif (errno == EACCES)\n+\t\t\terr = errno;\n+#endif\n+\n+\t\tif (err == EACCES && advice_ignored_hook) {\n+\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n+\n+\t\t\tif (!string_list_lookup(&advise_given, name)) {\n+\t\t\t\tstring_list_insert(&advise_given, name);\n+\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n+\t\t\t\t\t \"it's not set as executable.\\n\"\n+\t\t\t\t\t \"You can disable this warning with \"\n+\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n+\t\t\t\t       path.buf);\n+\t\t\t}\n+\t\t}\n+\t\treturn NULL;\n+\t}\n+\treturn path.buf;\n+}\n+\n+\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o)\n {\n \tstrvec_clear(&o->env);\ndiff --git a/hook.h b/hook.h\nindex ebfee26bcf2..291ee19469a 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -37,6 +37,13 @@ struct hook_cb_data {\n \tstruct run_hooks_opt *options;\n };\n \n+/*\n+ * Returns the path to the hook file, or NULL if the hook is missing\n+ * or disabled. Note that this points to static storage that will be\n+ * overwritten by further calls to find_hook and run_hook_*.\n+ */\n+const char *find_hook(const char *name);\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /*\ndiff --git a/refs.c b/refs.c\nindex 8c9490235ea..59be29cf081 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -10,6 +10,7 @@\n #include \"refs.h\"\n #include \"refs/refs-internal.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"object-store.h\"\n #include \"object.h\"\n #include \"tag.h\"\ndiff --git a/run-command.c b/run-command.c\nindex be6bc128cd9..82fdf296569 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -8,6 +8,7 @@\n #include \"string-list.h\"\n #include \"quote.h\"\n #include \"config.h\"\n+#include \"hook.h\"\n \n void child_process_init(struct child_process *child)\n {\n@@ -1320,40 +1321,6 @@ int async_with_fork(void)\n #endif\n }\n \n-const char *find_hook(const char *name)\n-{\n-\tstatic struct strbuf path = STRBUF_INIT;\n-\n-\tstrbuf_reset(&path);\n-\tstrbuf_git_path(&path, \"hooks/%s\", name);\n-\tif (access(path.buf, X_OK) < 0) {\n-\t\tint err = errno;\n-\n-#ifdef STRIP_EXTENSION\n-\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n-\t\tif (access(path.buf, X_OK) >= 0)\n-\t\t\treturn path.buf;\n-\t\tif (errno == EACCES)\n-\t\t\terr = errno;\n-#endif\n-\n-\t\tif (err == EACCES && advice_ignored_hook) {\n-\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n-\n-\t\t\tif (!string_list_lookup(&advise_given, name)) {\n-\t\t\t\tstring_list_insert(&advise_given, name);\n-\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n-\t\t\t\t\t \"it's not set as executable.\\n\"\n-\t\t\t\t\t \"You can disable this warning with \"\n-\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n-\t\t\t\t       path.buf);\n-\t\t\t}\n-\t\t}\n-\t\treturn NULL;\n-\t}\n-\treturn path.buf;\n-}\n-\n int run_hook_ve(const char *const *env, const char *name, va_list args)\n {\n \tstruct child_process hook = CHILD_PROCESS_INIT;\ndiff --git a/run-command.h b/run-command.h\nindex d08414a92e7..b58531a7eb3 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -201,13 +201,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/*\n- * Returns the path to the hook file, or NULL if the hook is missing\n- * or disabled. Note that this points to static storage that will be\n- * overwritten by further calls to find_hook and run_hook_*.\n- */\n-const char *find_hook(const char *name);\n-\n /**\n  * Run a hook.\n  * The first argument is a pathname to an index file, or NULL\ndiff --git a/sequencer.c b/sequencer.c\nindex 0bec01cf38e..3de479f90e1 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -8,6 +8,7 @@\n #include \"sequencer.h\"\n #include \"tag.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"utf8.h\"\n #include \"cache-tree.h\"\ndiff --git a/transport.c b/transport.c\nindex 6cf3da19ebd..e6c46adf60c 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -2,6 +2,7 @@\n #include \"config.h\"\n #include \"transport.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"pkt-line.h\"\n #include \"fetch-pack.h\"\n #include \"remote.h\"\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425770","messageId":"patch-05.31-a6f0817ad81-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 05/31] hook.c: add a hook_exists() wrapper and use it in bugreport.c","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:07Z","receivedAt":"2021-05-28T12:11:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nAdd a boolean version of the find_hook() function for those callers\nwho are only interested in checking whether the hook exists, not what\nthe path to it is.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/bugreport.c | 2 +-\n hook.c              | 5 ++++-\n hook.h              | 5 +++++\n 3 files changed, 10 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 596f079a7f9..941c8d5e270 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -82,7 +82,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t}\n \n \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (find_hook(hook[i]))\n+\t\tif (hook_exists(hook[i]))\n \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n }\n \ndiff --git a/hook.c b/hook.c\nindex 06842f50e5c..c7da273822d 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -36,7 +36,10 @@ const char *find_hook(const char *name)\n \treturn path.buf;\n }\n \n-\n+int hook_exists(const char *name)\n+{\n+\treturn !!find_hook(name);\n+}\n \n void run_hooks_opt_clear(struct run_hooks_opt *o)\n {\ndiff --git a/hook.h b/hook.h\nindex 291ee19469a..cbda7746a5d 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -44,6 +44,11 @@ struct hook_cb_data {\n  */\n const char *find_hook(const char *name);\n \n+/*\n+ * A boolean version of find_hook()\n+ */\n+int hook_exists(const char *hookname);\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /*\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425772","messageId":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"87lf80l1m6.fsf@evledraar.gmail.com","subject":"[PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:02Z","receivedAt":"2021-05-28T12:11:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"After suggesting[1] an another round that the config-based-hook\ntopic[2] should take a more incremental approach to reach its end goal\nI thought I'd try hacking that up.\n\nSo this is a proposed restart of that topic which if the consensus\nfavors it should replace it, and the config-based hooks topic should\nbe rebased on top of this.\n\nAs noted in [1] there are no user-visible behavior changes here. All\nthat's being done is to consolidate hook dispatch into the new\nhook.[ch] library, and the \"git hook run\" command (used by\ngit-send-email and git-p4).\n\nThe range-diff below is scary, I recommend just reading this from\nscratch. Some incomplete notes on changed things from Emily's v9:\n\n * First off, we've gone from:\n\n    54 files changed, 2689 insertions(+), 713 deletions(-)\n\n   to:\n\n    41 files changed, 1293 insertions(+), 555 deletions(-)\n\n   So while there's a similar number of patches the change is much\n   smaller.\n\n * There's no config via hooks, design doc etc. in this series. It's\n   meant as a basis on which to build that. We still only support\n   running .git/hook/<hook>, and we run those with .jobs=1 by\n   definition.\n\n * There's absolutely no behavior change in how we run these hooks,\n   unlike in Emily's version. E.g. in hers sometimes we'd always pass\n   absolute paths when before we didn't, now we just do that for \"git\n   worktree\" via a flag as on the current \"master\". There were also\n   some other subtle changes as seen in the tests. Now the only test\n   changes are the addition of more and missing tests.\n\n   Some of this (e.g. git-send-email's insistance on GIT_DIR being\n   set) is something we'll probably need to change sooner than later,\n   but for now we've got bug-for-bug compatibility.\n\n * This has been re-arranged as much as possible to start using\n   minimal bits of the library as soon as we can. E.g. in Emily's the\n   whole of hook.c is implemented before any hook is migrated over to\n   it.\n\n   In this version we barely know how to do anything yet (no stdin\n   handling etc.) when we can move the pre-auto-gc hook over, we then\n   do a few more hooks and add stdin support, migrate the hooks that\n   need that etc.\n\n * In Emily's there's a mid-series switch from the run_command() API\n   to run_processes_parallel_tr2() as we learn parallel execution. I\n   thought this was more complex to read and just reflected the past\n   evolution of the topic.\n\n   That's now squashed together, we always have the parallel API use,\n   we just have .jobs=1 throughout.\n\n * I tried as much as possible to similarly squash together\n   e.g. removing an unused API along with the commit that removes the\n   last user of it, not introducing APIs without simultaneously\n   introducing users of them etc.\n\n * The \"git hook run\" command now takes arguments as:\n\n        git hook run a-hook -- some arg u ments\n\n   Not:\n\n        git hook run -a some -a arg -a u -a ments a-hook\n\n    The \"-e\" for \"pass this env variable\" is also gone, nothing\n    actually made use of it.\n\n    If we need it in the future surely we can just set them in the\n    environment instead, and ask the relevant command run APIs not to\n    clobber things.\n\n    I think the -- (or --end-of-options) is easier to work\n    with. Changing this allowed removing the whole\n    parse-options-strvec part of Emily's series. It's now replaced\n    with PARSE_OPT_KEEP_DASHDASH + 2-4 lines of code around that in\n    builtin/hook.c.\n\n * Even though this extracted the \"config based hooks\" part, and .jobs\n   != 1 support I've tried as much as possible to keep the same code\n   layout, to the point where hooks e.g. set .jobs=1 still if they\n   insist that one job should be run. It should thus be fairly drop-in\n   compatible for building .jobs > 1 support on top, config-based hooks\n   etc.\n\n * The two last patches are new, a couple of minor bugfixes of mine\n   that I noticed while hacking on this. One solves a long-standing\n   TOCTOU in run_a_hook() and then have_hook() to see if we ran it (we\n   now just remember if we ran it), and there's now a generated and\n   canonical hook-list.h similar to config-list.h, except this one's\n   more strict. We'll die on unknown hooks unless they're in\n   Documentation/githooks.txt.\n\n1. https://lore.kernel.org/git/87lf80l1m6.fsf@evledraar.gmail.com/\n2. https://lore.kernel.org/git/20210527000856.695702-1-emilyshaffer@google.com/\n\nEmily Shaffer (25):\n  hook: add 'run' subcommand\n  hook.c: add a hook_exists() wrapper and use it in bugreport.c\n  gc: use hook library for pre-auto-gc hook\n  rebase: teach pre-rebase to use hook.h\n  am: convert applypatch hooks to use config\n  hooks: convert 'post-checkout' hook to hook library\n  merge: use config-based hooks for post-merge hook\n  send-email: use 'git hook run' for 'sendemail-validate'\n  git-p4: use 'git hook' to run hooks\n  commit: use hook.h to execute hooks\n  read-cache: convert post-index-change hook to use config\n  receive-pack: convert push-to-checkout hook to hook.h\n  run-command: allow stdin for run_processes_parallel\n  hook: support passing stdin to hooks\n  am: convert 'post-rewrite' hook to hook.h\n  run-command: add stdin callback for parallelization\n  hook: provide stdin by string_list or callback\n  hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n  transport: convert pre-push hook to use config\n  reference-transaction: use hook.h to run hooks\n  run-command: allow capturing of collated output\n  hooks: allow callers to capture output\n  receive-pack: convert 'update' hook to hook.h\n  post-update: use hook.h library\n  receive-pack: convert receive hooks to hook.h\n\nÆvar Arnfjörð Bjarmason (6):\n  hooks tests: don't leave \"actual\" nonexisting on failure\n  gc tests: add a test for the \"pre-auto-gc\" hook\n  run-command.h: move find_hook() to hook.h\n  git hook run: add an --ignore-missing flag\n  hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n  hook-list.h: add a generated list of hooks, like config-list.h\n\n .gitignore                   |   2 +\n Documentation/git-hook.txt   |  49 ++++++\n Documentation/githooks.txt   |   4 +\n Makefile                     |  16 +-\n builtin.h                    |   1 +\n builtin/am.c                 |  34 ++--\n builtin/bugreport.c          |  46 ++----\n builtin/checkout.c           |  17 +-\n builtin/clone.c              |   7 +-\n builtin/commit.c             |  19 ++-\n builtin/fetch.c              |   1 +\n builtin/gc.c                 |   8 +-\n builtin/hook.c               |  72 +++++++++\n builtin/merge.c              |  22 ++-\n builtin/rebase.c             |   9 +-\n builtin/receive-pack.c       | 299 ++++++++++++++++++-----------------\n builtin/submodule--helper.c  |   2 +-\n builtin/worktree.c           |  31 ++--\n command-list.txt             |   1 +\n commit.c                     |  17 +-\n commit.h                     |   3 +-\n generate-hooklist.sh         |  20 +++\n git-p4.py                    |  72 +--------\n git-send-email.perl          |  20 ++-\n git.c                        |   1 +\n hook.c                       | 224 ++++++++++++++++++++++++++\n hook.h                       | 122 ++++++++++++++\n read-cache.c                 |  12 +-\n refs.c                       |  43 ++---\n reset.c                      |  15 +-\n run-command.c                | 157 +++++++++---------\n run-command.h                |  55 ++++---\n sequencer.c                  |  88 +++++------\n submodule.c                  |   1 +\n t/helper/test-run-command.c  |  46 +++++-\n t/t0061-run-command.sh       |  37 +++++\n t/t1350-config-hooks-path.sh |   1 +\n t/t1800-hook.sh              | 166 +++++++++++++++++++\n t/t6500-gc.sh                |  46 ++++++\n t/t9001-send-email.sh        |   4 +-\n transport.c                  |  58 ++-----\n 41 files changed, 1293 insertions(+), 555 deletions(-)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100755 generate-hooklist.sh\n create mode 100644 hook.c\n create mode 100644 hook.h\n create mode 100755 t/t1800-hook.sh\n\nRange-diff:\n 1:  9540c006dc0 <  -:  ----------- doc: propose hooks managed by the config\n -:  ----------- >  1:  8ac2efc71a0 hooks tests: don't leave \"actual\" nonexisting on failure\n -:  ----------- >  2:  eb37693f7dc gc tests: add a test for the \"pre-auto-gc\" hook\n 2:  2f50cfe7b92 !  3:  1ad4e69f7da hook: introduce git-hook subcommand\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    hook: introduce git-hook subcommand\n    +    hook: add 'run' subcommand\n     \n    -    Add a new subcommand, git-hook, which will be used to ease config-based\n    -    hook management. This command will handle parsing configs to compose a\n    -    list of hooks to run for a given event, as well as adding or modifying\n    -    hook configs in an interactive fashion.\n    +    In order to enable hooks to be run as an external process, by a\n    +    standalone Git command, or by tools which wrap Git, provide an external\n    +    means to run all configured hook commands for a given hook event.\n     \n    -    Start with 'git hook list <hookname>', which checks the known configs in\n    -    order to create an ordered list of hooks to run on a given hook event.\n    -\n    -    Multiple commands can be specified for a given hook by providing\n    -    multiple \"hook.<hookname>.command = <path-to-hook>\" lines. Hooks will be\n    -    run in config order. If more properties need to be set on a given hook\n    -    in the future, commands can also be specified by providing\n    -    \"hook.<hookname>.command = <hookcmd-name>\", as well as a \"[hookcmd\n    -    <hookcmd-name>]\" subsection; this subsection should contain a\n    -    \"hookcmd.<hookcmd-name>.command = <path-to-hook>\" line.\n    -\n    -    For example:\n    -\n    -      $ git config --list | grep ^hook\n    -      hook.pre-commit.command=baz\n    -      hook.pre-commit.command=~/bar.sh\n    -      hookcmd.baz.command=~/baz/from/hookcmd.sh\n    -\n    -      $ git hook list pre-commit\n    -      global: ~/baz/from/hookcmd.sh\n    -      local: ~/bar.sh\n    +    Most of our hooks require more complex functionality than this, but\n    +    let's start with the bare minimum required to support our simplest\n    +    hooks.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## .gitignore ##\n     @@\n    @@ .gitignore\n      /git-http-fetch\n      /git-http-push\n     \n    - ## Documentation/config/hook.txt (new) ##\n    -@@\n    -+hook.<command>.command::\n    -+\tA command to execute during the <command> hook event. This can be an\n    -+\texecutable on your device, a oneliner for your shell, or the name of a\n    -+\thookcmd. See linkgit:git-hook[1].\n    -+\n    -+hookcmd.<name>.command::\n    -+\tA command to execute during a hook for which <name> has been specified\n    -+\tas a command. This can be an executable on your device or a oneliner for\n    -+\tyour shell. See linkgit:git-hook[1].\n    -\n      ## Documentation/git-hook.txt (new) ##\n     @@\n     +git-hook(1)\n    @@ Documentation/git-hook.txt (new)\n     +\n     +NAME\n     +----\n    -+git-hook - Manage configured hooks\n    ++git-hook - run git hooks\n     +\n     +SYNOPSIS\n     +--------\n     +[verse]\n    -+'git hook' list <hook-name>\n    ++'git hook' run <hook-name> [-- <hook-args>]\n     +\n     +DESCRIPTION\n     +-----------\n    -+You can list configured hooks with this command. Later, you will be able to run,\n    -+add, and modify hooks with this command.\n    -+\n    -+This command parses the default configuration files for sections `hook` and\n    -+`hookcmd`. `hook` is used to describe the commands which will be run during a\n    -+particular hook event; commands are run in the order Git encounters them during\n    -+the configuration parse (see linkgit:git-config[1]). `hookcmd` is used to\n    -+describe attributes of a specific command. If additional attributes don't need\n    -+to be specified, a command to run can be specified directly in the `hook`\n    -+section; if a `hookcmd` by that name isn't found, Git will attempt to run the\n    -+provided value directly. For example:\n    -+\n    -+Global config\n    -+----\n    -+  [hook \"post-commit\"]\n    -+    command = \"linter\"\n    -+    command = \"~/typocheck.sh\"\n     +\n    -+  [hookcmd \"linter\"]\n    -+    command = \"/bin/linter --c\"\n    -+----\n    ++This command is an interface to git hooks (see linkgit:githooks[5]).\n    ++Currently it only provides a convenience wrapper for running hooks for\n    ++use by git itself. In the future it might gain other functionality.\n     +\n    -+Local config\n    -+----\n    -+  [hook \"prepare-commit-msg\"]\n    -+    command = \"linter\"\n    -+  [hook \"post-commit\"]\n    -+    command = \"python ~/run-test-suite.py\"\n    -+----\n    ++SUBCOMMANDS\n    ++-----------\n     +\n    -+With these configs, you'd then see:\n    ++run::\n     +\n    -+----\n    -+$ git hook list \"post-commit\"\n    -+global: /bin/linter --c\n    -+global: ~/typocheck.sh\n    -+local: python ~/run-test-suite.py\n    ++\tRun the `<hook-name>` hook. Any positional arguments to the\n    ++\thook should be passed after an optional \"--\" (or\n    ++\t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n    ++\tthis accepts.\n     +\n    -+$ git hook list \"prepare-commit-msg\"\n    -+local: /bin/linter --c\n    -+----\n    -+\n    -+COMMANDS\n    ++SEE ALSO\n     +--------\n    -+\n    -+list `<hook-name>`::\n    -+\n    -+List the hooks which have been configured for `<hook-name>`. Hooks appear\n    -+in the order they should be run, and print the config scope where the relevant\n    -+`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n    -+This output is human-readable and the format is subject to change over time.\n    -+\n    -+CONFIGURATION\n    -+-------------\n    -+include::config/hook.txt[]\n    ++linkgit:githooks[5]\n     +\n     +GIT\n     +---\n     +Part of the linkgit:git[1] suite\n     \n    + ## Documentation/githooks.txt ##\n    +@@ Documentation/githooks.txt: and \"0\" meaning they were not.\n    + Only one parameter should be set to \"1\" when the hook runs.  The hook\n    + running passing \"1\", \"1\" should not be possible.\n    + \n    ++SEE ALSO\n    ++--------\n    ++linkgit:git-hook[1]\n    ++\n    + GIT\n    + ---\n    + Part of the linkgit:git[1] suite\n    +\n      ## Makefile ##\n     @@ Makefile: LIB_OBJS += hash-lookup.o\n      LIB_OBJS += hashmap.o\n    @@ builtin/hook.c (new)\n     +#include \"hook.h\"\n     +#include \"parse-options.h\"\n     +#include \"strbuf.h\"\n    ++#include \"strvec.h\"\n     +\n     +static const char * const builtin_hook_usage[] = {\n    -+\tN_(\"git hook list <hookname>\"),\n    ++\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n     +\tNULL\n     +};\n     +\n    -+static int list(int argc, const char **argv, const char *prefix)\n    ++static int run(int argc, const char **argv, const char *prefix)\n     +{\n    -+\tstruct list_head *head, *pos;\n    -+\tconst char *hookname = NULL;\n    ++\tint i;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    ++\tint rc = 0;\n    ++\tconst char *hook_name;\n    ++\tconst char *hook_path;\n     +\n    -+\tstruct option list_options[] = {\n    ++\tstruct option run_options[] = {\n     +\t\tOPT_END(),\n     +\t};\n     +\n    -+\targc = parse_options(argc, argv, prefix, list_options,\n    -+\t\t\t     builtin_hook_usage, 0);\n    -+\n    -+\tif (argc < 1) {\n    -+\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n    -+\t\t\t      builtin_hook_usage, list_options);\n    ++\targc = parse_options(argc, argv, prefix, run_options,\n    ++\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n    ++\n    ++\tif (argc > 2) {\n    ++\t\tif (strcmp(argv[2], \"--\") &&\n    ++\t\t    strcmp(argv[2], \"--end-of-options\"))\n    ++\t\t\t/* Having a -- for \"run\" is mandatory */\n    ++\t\t\tusage_with_options(builtin_hook_usage, run_options);\n    ++\t\t/* Add our arguments, start after -- */\n    ++\t\tfor (i = 3 ; i < argc; i++)\n    ++\t\t\tstrvec_push(&opt.args, argv[i]);\n     +\t}\n     +\n    -+\thookname = argv[0];\n    -+\n    -+\thead = hook_list(hookname);\n    -+\n    -+\tif (list_empty(head)) {\n    -+\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n    -+\t\t       hookname);\n    -+\t\treturn 0;\n    -+\t}\n    ++\t/* Need to take into account core.hooksPath */\n    ++\tgit_config(git_default_config, NULL);\n     +\n    -+\tlist_for_each(pos, head) {\n    -+\t\tstruct hook *item = list_entry(pos, struct hook, list);\n    -+\t\tif (item)\n    -+\t\t\tprintf(\"%s: %s\\n\",\n    -+\t\t\t       config_scope_name(item->origin),\n    -+\t\t\t       item->command.buf);\n    ++\thook_name = argv[1];\n    ++\thook_path = find_hook(hook_name);\n    ++\tif (!hook_path) {\n    ++\t\terror(\"cannot find a hook named %s\", hook_name);\n    ++\t\treturn 1;\n     +\t}\n    ++\trc = run_found_hooks(hook_name, hook_path, &opt);\n     +\n    -+\tclear_hook_list(head);\n    ++\trun_hooks_opt_clear(&opt);\n     +\n    -+\treturn 0;\n    ++\treturn rc;\n     +}\n     +\n     +int cmd_hook(int argc, const char **argv, const char *prefix)\n    @@ builtin/hook.c (new)\n     +\tstruct option builtin_hook_options[] = {\n     +\t\tOPT_END(),\n     +\t};\n    -+\tif (argc < 2)\n    -+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n    -+\n    -+\tif (!strcmp(argv[1], \"list\"))\n    -+\t\treturn list(argc - 1, argv + 1, prefix);\n     +\n    ++\tif (!strcmp(argv[1], \"run\"))\n    ++\t\treturn run(argc, argv, prefix);\n     +\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n    ++\treturn 1;\n     +}\n     \n      ## command-list.txt ##\n    @@ git.c: static struct cmd_struct commands[] = {\n      \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n      \t{ \"hash-object\", cmd_hash_object },\n      \t{ \"help\", cmd_help },\n    -+\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n    ++\t{ \"hook\", cmd_hook, RUN_SETUP },\n      \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n      \t{ \"init\", cmd_init_db },\n      \t{ \"init-db\", cmd_init_db },\n    @@ git.c: static struct cmd_struct commands[] = {\n      ## hook.c (new) ##\n     @@\n     +#include \"cache.h\"\n    -+\n     +#include \"hook.h\"\n    -+#include \"config.h\"\n    ++#include \"run-command.h\"\n     +\n    -+void free_hook(struct hook *ptr)\n    ++void run_hooks_opt_clear(struct run_hooks_opt *o)\n     +{\n    -+\tif (ptr) {\n    -+\t\tstrbuf_release(&ptr->command);\n    -+\t\tfree(ptr);\n    -+\t}\n    ++\tstrvec_clear(&o->env);\n    ++\tstrvec_clear(&o->args);\n     +}\n     +\n    -+static void append_or_move_hook(struct list_head *head, const char *command)\n    ++static int pick_next_hook(struct child_process *cp,\n    ++\t\t\t  struct strbuf *out,\n    ++\t\t\t  void *pp_cb,\n    ++\t\t\t  void **pp_task_cb)\n     +{\n    -+\tstruct list_head *pos = NULL, *tmp = NULL;\n    -+\tstruct hook *to_add = NULL;\n    ++\tstruct hook_cb_data *hook_cb = pp_cb;\n    ++\tstruct hook *run_me = hook_cb->run_me;\n    ++\n    ++\tif (!run_me)\n    ++\t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n    ++\n    ++\tcp->no_stdin = 1;\n    ++\tcp->env = hook_cb->options->env.v;\n    ++\tcp->stdout_to_stderr = 1;\n    ++\tcp->trace2_hook_name = hook_cb->hook_name;\n    ++\n    ++\t/* add command */\n    ++\tstrvec_push(&cp->args, run_me->hook_path);\n     +\n     +\t/*\n    -+\t * remove the prior entry with this command; we'll replace it at the\n    -+\t * end.\n    ++\t * add passed-in argv, without expanding - let the user get back\n    ++\t * exactly what they put in\n     +\t */\n    -+\tlist_for_each_safe(pos, tmp, head) {\n    -+\t\tstruct hook *it = list_entry(pos, struct hook, list);\n    -+\t\tif (!strcmp(it->command.buf, command)) {\n    -+\t\t    list_del(pos);\n    -+\t\t    /* we'll simply move the hook to the end */\n    -+\t\t    to_add = it;\n    -+\t\t    break;\n    -+\t\t}\n    -+\t}\n    -+\n    -+\tif (!to_add) {\n    -+\t\t/* adding a new hook, not moving an old one */\n    -+\t\tto_add = xmalloc(sizeof(*to_add));\n    -+\t\tstrbuf_init(&to_add->command, 0);\n    -+\t\tstrbuf_addstr(&to_add->command, command);\n    -+\t}\n    ++\tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n     +\n    -+\t/* re-set the scope so we show where an override was specified */\n    -+\tto_add->origin = current_config_scope();\n    ++\t/* Provide context for errors if necessary */\n    ++\t*pp_task_cb = run_me;\n     +\n    -+\tlist_add_tail(&to_add->list, head);\n    ++\treturn 1;\n     +}\n     +\n    -+static void remove_hook(struct list_head *to_remove)\n    ++static int notify_start_failure(struct strbuf *out,\n    ++\t\t\t\tvoid *pp_cb,\n    ++\t\t\t\tvoid *pp_task_cp)\n     +{\n    -+\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n    -+\tlist_del(to_remove);\n    -+\tfree_hook(hook_to_remove);\n    -+}\n    ++\tstruct hook_cb_data *hook_cb = pp_cb;\n    ++\tstruct hook *attempted = pp_task_cp;\n     +\n    -+void clear_hook_list(struct list_head *head)\n    -+{\n    -+\tstruct list_head *pos, *tmp;\n    -+\tlist_for_each_safe(pos, tmp, head)\n    -+\t\tremove_hook(pos);\n    ++\t/* |= rc in cb */\n    ++\thook_cb->rc |= 1;\n    ++\n    ++\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n    ++\t\t    attempted->hook_path);\n    ++\n    ++\treturn 1;\n     +}\n     +\n    -+struct hook_config_cb\n    ++static int notify_hook_finished(int result,\n    ++\t\t\t\tstruct strbuf *out,\n    ++\t\t\t\tvoid *pp_cb,\n    ++\t\t\t\tvoid *pp_task_cb)\n     +{\n    -+\tstruct strbuf *hookname;\n    -+\tstruct list_head *list;\n    -+};\n    ++\tstruct hook_cb_data *hook_cb = pp_cb;\n     +\n    -+static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n    -+{\n    -+\tstruct hook_config_cb *data = cb_data;\n    -+\tconst char *hook_key = data->hookname->buf;\n    -+\tstruct list_head *head = data->list;\n    -+\n    -+\tif (!strcmp(key, hook_key)) {\n    -+\t\tconst char *command = value;\n    -+\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n    -+\n    -+\t\t/*\n    -+\t\t * Check if a hookcmd with that name exists. If it doesn't,\n    -+\t\t * 'git_config_get_value()' is documented not to touch &command,\n    -+\t\t * so we don't need to do anything.\n    -+\t\t */\n    -+\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n    -+\t\tgit_config_get_value(hookcmd_name.buf, &command);\n    -+\n    -+\t\tif (!command) {\n    -+\t\t\tstrbuf_release(&hookcmd_name);\n    -+\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n    -+\t\t}\n    -+\n    -+\t\t/*\n    -+\t\t * TODO: implement an option-getting callback, e.g.\n    -+\t\t *   get configs by pattern hookcmd.$value.*\n    -+\t\t *   for each key+value, do_callback(key, value, cb_data)\n    -+\t\t */\n    -+\n    -+\t\tappend_or_move_hook(head, command);\n    -+\n    -+\t\tstrbuf_release(&hookcmd_name);\n    -+\t}\n    ++\t/* |= rc in cb */\n    ++\thook_cb->rc |= result;\n     +\n    -+\treturn 0;\n    ++\treturn 1;\n     +}\n     +\n    -+struct list_head* hook_list(const char* hookname)\n    ++\n    ++int run_found_hooks(const char *hook_name, const char *hook_path,\n    ++\t\t    struct run_hooks_opt *options)\n     +{\n    -+\tstruct strbuf hook_key = STRBUF_INIT;\n    -+\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n    -+\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n    ++\tstruct hook my_hook = {\n    ++\t\t.hook_path = hook_path,\n    ++\t};\n    ++\tstruct hook_cb_data cb_data = {\n    ++\t\t.rc = 0,\n    ++\t\t.hook_name = hook_name,\n    ++\t\t.options = options,\n    ++\t};\n    ++\tcb_data.run_me = &my_hook;\n    ++\n    ++\tif (options->jobs != 1)\n    ++\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n     +\n    -+\tINIT_LIST_HEAD(hook_head);\n    ++\trun_processes_parallel_tr2(options->jobs,\n    ++\t\t\t\t   pick_next_hook,\n    ++\t\t\t\t   notify_start_failure,\n    ++\t\t\t\t   notify_hook_finished,\n    ++\t\t\t\t   &cb_data,\n    ++\t\t\t\t   \"hook\",\n    ++\t\t\t\t   hook_name);\n     +\n    -+\tif (!hookname)\n    -+\t\treturn NULL;\n    ++\treturn cb_data.rc;\n    ++}\n    ++\n    ++int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n    ++{\n    ++\tconst char *hook_path;\n    ++\tint ret;\n    ++\tif (!options)\n    ++\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n     +\n    -+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n    ++\thook_path = find_hook(hook_name);\n     +\n    -+\tgit_config(hook_config_lookup, &cb_data);\n    ++\t/* Care about nonexistence? Use run_found_hooks() */\n    ++\tif (!hook_path)\n    ++\t\treturn 0;\n     +\n    -+\tstrbuf_release(&hook_key);\n    -+\treturn hook_head;\n    ++\tret = run_found_hooks(hook_name, hook_path, options);\n    ++\treturn ret;\n     +}\n     \n      ## hook.h (new) ##\n     @@\n    -+#include \"config.h\"\n    -+#include \"list.h\"\n    ++#ifndef HOOK_H\n    ++#define HOOK_H\n     +#include \"strbuf.h\"\n    ++#include \"strvec.h\"\n    ++#include \"run-command.h\"\n     +\n     +struct hook {\n    -+\tstruct list_head list;\n    -+\t/*\n    -+\t * Config file which holds the hook.*.command definition.\n    -+\t * (This has nothing to do with the hookcmd.<name>.* configs.)\n    -+\t */\n    -+\tenum config_scope origin;\n    -+\t/* The literal command to run. */\n    -+\tstruct strbuf command;\n    ++\t/* The path to the hook */\n    ++\tconst char *hook_path;\n    ++};\n    ++\n    ++struct run_hooks_opt\n    ++{\n    ++\t/* Environment vars to be set for each hook */\n    ++\tstruct strvec env;\n    ++\n    ++\t/* Args to be passed to each hook */\n    ++\tstruct strvec args;\n    ++\n    ++\t/* Number of threads to parallelize across */\n    ++\tint jobs;\n     +};\n     +\n    ++#define RUN_HOOKS_OPT_INIT { \\\n    ++\t.jobs = 1, \\\n    ++\t.env = STRVEC_INIT, \\\n    ++\t.args = STRVEC_INIT, \\\n    ++}\n    ++\n     +/*\n    -+ * Provides a linked list of 'struct hook' detailing commands which should run\n    -+ * in response to the 'hookname' event, in execution order.\n    ++ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n     + */\n    -+struct list_head* hook_list(const char *hookname);\n    ++struct hook_cb_data {\n    ++\tint rc;\n    ++\tconst char *hook_name;\n    ++\tstruct hook *run_me;\n    ++\tstruct run_hooks_opt *options;\n    ++};\n     +\n    -+/* Free memory associated with a 'struct hook' */\n    -+void free_hook(struct hook *ptr);\n    -+/* Empties the list at 'head', calling 'free_hook()' on each entry */\n    -+void clear_hook_list(struct list_head *head);\n    ++void run_hooks_opt_clear(struct run_hooks_opt *o);\n    ++\n    ++/*\n    ++ * Calls find_hook(hookname) and runs the hooks (if any) with\n    ++ * run_found_hooks().\n    ++ */\n    ++int run_hooks(const char *hook_name, struct run_hooks_opt *options);\n    ++\n    ++/*\n    ++ * Takes an already resolved hook and runs it. Internally the simpler\n    ++ * run_hooks() will call this.\n    ++ */\n    ++int run_found_hooks(const char *hookname, const char *hook_path,\n    ++\t\t    struct run_hooks_opt *options);\n    ++#endif\n     \n    - ## t/t1360-config-based-hooks.sh (new) ##\n    + ## t/t1800-hook.sh (new) ##\n     @@\n     +#!/bin/bash\n     +\n    -+test_description='config-managed multihooks, including git-hook command'\n    ++test_description='git-hook command'\n     +\n     +. ./test-lib.sh\n     +\n    -+ROOT=\n    -+if test_have_prereq MINGW\n    -+then\n    -+\t# In Git for Windows, Unix-like paths work only in shell scripts;\n    -+\t# `git.exe`, however, will prefix them with the pseudo root directory\n    -+\t# (of the Unix shell). Let's accommodate for that.\n    -+\tROOT=\"$(cd / && pwd)\"\n    -+fi\n    -+\n    -+setup_hooks () {\n    -+\ttest_config hook.pre-commit.command \"/path/ghi\" --add\n    -+\ttest_config_global hook.pre-commit.command \"/path/def\" --add\n    -+}\n    ++test_expect_success 'setup .git/hooks' '\n    ++\tmkdir .git/hooks\n    ++'\n     +\n    -+setup_hookcmd () {\n    -+\ttest_config hook.pre-commit.command \"abc\" --add\n    -+\ttest_config_global hookcmd.abc.command \"/path/abc\" --add\n    -+}\n    ++test_expect_success 'git hook run -- nonexistent hook' '\n    ++\tcat >stderr.expect <<-\\EOF &&\n    ++\terror: cannot find a hook named does-not-exist\n    ++\tEOF\n    ++\ttest_expect_code 1 git hook run does-not-exist 2>stderr.actual &&\n    ++\ttest_cmp stderr.expect stderr.actual\n    ++'\n     +\n    -+test_expect_success 'git hook rejects commands without a mode' '\n    -+\ttest_must_fail git hook pre-commit\n    ++test_expect_success 'git hook run -- basic' '\n    ++\twrite_script .git/hooks/test-hook <<-EOF &&\n    ++\techo Test hook\n    ++\tEOF\n    ++\n    ++\tcat >expect <<-\\EOF &&\n    ++\tTest hook\n    ++\tEOF\n    ++\tgit hook run test-hook 2>actual &&\n    ++\ttest_cmp expect actual\n     +'\n     +\n    ++test_expect_success 'git hook run -- stdout and stderr handling' '\n    ++\twrite_script .git/hooks/test-hook <<-EOF &&\n    ++\techo >&1 Will end up on stderr\n    ++\techo >&2 Will end up on stderr\n    ++\tEOF\n     +\n    -+test_expect_success 'git hook rejects commands without a hookname' '\n    -+\ttest_must_fail git hook list\n    ++\tcat >stderr.expect <<-\\EOF &&\n    ++\tWill end up on stderr\n    ++\tWill end up on stderr\n    ++\tEOF\n    ++\tgit hook run test-hook >stdout.actual 2>stderr.actual &&\n    ++\ttest_cmp stderr.expect stderr.actual &&\n    ++\ttest_must_be_empty stdout.actual\n     +'\n     +\n    -+test_expect_success 'git hook runs outside of a repo' '\n    -+\tsetup_hooks &&\n    ++test_expect_success 'git hook run -- exit codes are passed along' '\n    ++\twrite_script .git/hooks/test-hook <<-EOF &&\n    ++\texit 1\n    ++\tEOF\n    ++\n    ++\ttest_expect_code 1 git hook run test-hook &&\n     +\n    -+\tcat >expected <<-EOF &&\n    -+\tglobal: $ROOT/path/def\n    ++\twrite_script .git/hooks/test-hook <<-EOF &&\n    ++\texit 2\n     +\tEOF\n     +\n    -+\tnongit git config --list --global &&\n    ++\ttest_expect_code 2 git hook run test-hook &&\n     +\n    -+\tnongit git hook list pre-commit >actual &&\n    -+\ttest_cmp expected actual\n    -+'\n    ++\twrite_script .git/hooks/test-hook <<-EOF &&\n    ++\texit 128\n    ++\tEOF\n     +\n    -+test_expect_success 'git hook list orders by config order' '\n    -+\tsetup_hooks &&\n    ++\ttest_expect_code 128 git hook run test-hook &&\n     +\n    -+\tcat >expected <<-EOF &&\n    -+\tglobal: $ROOT/path/def\n    -+\tlocal: $ROOT/path/ghi\n    ++\twrite_script .git/hooks/test-hook <<-EOF &&\n    ++\texit 129\n     +\tEOF\n     +\n    -+\tgit hook list pre-commit >actual &&\n    -+\ttest_cmp expected actual\n    ++\ttest_expect_code 129 git hook run test-hook\n     +'\n     +\n    -+test_expect_success 'git hook list dereferences a hookcmd' '\n    -+\tsetup_hooks &&\n    -+\tsetup_hookcmd &&\n    ++test_expect_success 'git hook run arg u ments without -- is not allowed' '\n    ++\ttest_expect_code 129 git hook run test-hook arg u ments\n    ++'\n    ++\n    ++test_expect_success 'git hook run -- pass arguments' '\n    ++\twrite_script .git/hooks/test-hook <<-\\EOF &&\n    ++\techo $1\n    ++\techo $2\n    ++\tEOF\n     +\n    -+\tcat >expected <<-EOF &&\n    -+\tglobal: $ROOT/path/def\n    -+\tlocal: $ROOT/path/ghi\n    -+\tlocal: $ROOT/path/abc\n    ++\tcat >expect <<-EOF &&\n    ++\targ\n    ++\tu ments\n     +\tEOF\n     +\n    -+\tgit hook list pre-commit >actual &&\n    -+\ttest_cmp expected actual\n    ++\tgit hook run test-hook -- arg \"u ments\" 2>actual &&\n    ++\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success 'git hook list reorders on duplicate commands' '\n    -+\tsetup_hooks &&\n    ++test_expect_success 'git hook run -- out-of-repo runs excluded' '\n    ++\twrite_script .git/hooks/test-hook <<-EOF &&\n    ++\techo Test hook\n    ++\tEOF\n     +\n    -+\ttest_config hook.pre-commit.command \"/path/def\" --add &&\n    ++\tnongit test_must_fail git hook run test-hook\n    ++'\n     +\n    -+\tcat >expected <<-EOF &&\n    -+\tlocal: $ROOT/path/ghi\n    -+\tlocal: $ROOT/path/def\n    ++test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n    ++\tmkdir my-hooks &&\n    ++\twrite_script my-hooks/test-hook <<-EOF &&\n    ++\techo Hook ran >>actual\n     +\tEOF\n     +\n    -+\tgit hook list pre-commit >actual &&\n    -+\ttest_cmp expected actual\n    ++\tcat >expect <<-\\EOF &&\n    ++\tTest hook\n    ++\tHook ran\n    ++\tHook ran\n    ++\tHook ran\n    ++\tHook ran\n    ++\tEOF\n    ++\n    ++\t# Test various ways of specifying the path. See also\n    ++\t# t1350-config-hooks-path.sh\n    ++\t>actual &&\n    ++\tgit hook run test-hook 2>>actual &&\n    ++\tgit -c core.hooksPath=my-hooks hook run test-hook 2>>actual &&\n    ++\tgit -c core.hooksPath=my-hooks/ hook run test-hook 2>>actual &&\n    ++\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook 2>>actual &&\n    ++\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook 2>>actual &&\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n    ++test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n    ++\t>actual &&\n    ++\tmkdir -p .git/custom-hooks .git/hooks &&\n    ++\twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n    ++\techo CUSTOM >>actual\n    ++\tEOF\n    ++\twrite_script .git/hooks/pre-commit <<-\\EOF\n    ++\techo NORMAL >>actual\n    ++\tEOF\n     +'\n     +\n     +test_done\n 3:  faa4a655183 <  -:  ----------- hook: include hookdir hook in list\n 4:  c43a7e0dd52 <  -:  ----------- hook: teach hook.runHookDir\n 5:  81e453baea2 <  -:  ----------- hook: implement hookcmd.<name>.skip\n 6:  71f9ee9ab82 <  -:  ----------- parse-options: parse into strvec\n 7:  e43a7a94163 <  -:  ----------- hook: add 'run' subcommand\n 8:  807ad9cf2f3 <  -:  ----------- hook: introduce hook_exists()\n35:  43d2383af49 !  4:  1a67a1cc065 run-command: stop thinking about hooks\n    @@\n      ## Metadata ##\n    -Author: Emily Shaffer <emilyshaffer@google.com>\n    +Author: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## Commit message ##\n    -    run-command: stop thinking about hooks\n    +    run-command.h: move find_hook() to hook.h\n     \n    -    hook.h has replaced all run-command.h hook-related functionality.\n    -    run-command.h:run_hooks_le/ve and find_hook are no longer used anywhere\n    -    in the codebase. So, let's delete the dead code - or, in the one case\n    -    where it's still needed, move it to an internal function in hook.c.\n    +    Move the find_hook() command to hook.h. Eventually all the hook\n    +    related code will live there, let's move this function over as-is.\n     \n    -    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n    +\n    + ## builtin/am.c ##\n    +@@\n    + #include \"parse-options.h\"\n    + #include \"dir.h\"\n    + #include \"run-command.h\"\n    ++#include \"hook.h\"\n    + #include \"quote.h\"\n    + #include \"tempfile.h\"\n    + #include \"lockfile.h\"\n    +\n    + ## builtin/bugreport.c ##\n    +@@\n    + #include \"strbuf.h\"\n    + #include \"help.h\"\n    + #include \"compat/compiler.h\"\n    +-#include \"run-command.h\"\n    ++#include \"hook.h\"\n    + \n    + \n    + static void get_system_info(struct strbuf *sys_info)\n    +\n    + ## builtin/commit.c ##\n    +@@\n    + #include \"revision.h\"\n    + #include \"wt-status.h\"\n    + #include \"run-command.h\"\n    ++#include \"hook.h\"\n    + #include \"refs.h\"\n    + #include \"log-tree.h\"\n    + #include \"strbuf.h\"\n    +\n    + ## builtin/merge.c ##\n    +@@\n    + #include \"builtin.h\"\n    + #include \"lockfile.h\"\n    + #include \"run-command.h\"\n    ++#include \"hook.h\"\n    + #include \"diff.h\"\n    + #include \"diff-merges.h\"\n    + #include \"refs.h\"\n    +\n    + ## builtin/receive-pack.c ##\n    +@@\n    + #include \"pkt-line.h\"\n    + #include \"sideband.h\"\n    + #include \"run-command.h\"\n    ++#include \"hook.h\"\n    + #include \"exec-cmd.h\"\n    + #include \"commit.h\"\n    + #include \"object.h\"\n    +\n    + ## builtin/worktree.c ##\n    +@@\n    + #include \"branch.h\"\n    + #include \"refs.h\"\n    + #include \"run-command.h\"\n    ++#include \"hook.h\"\n    + #include \"sigchain.h\"\n    + #include \"submodule.h\"\n    + #include \"utf8.h\"\n     \n      ## hook.c ##\n    -@@ hook.c: static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n    - \t}\n    - }\n    +@@\n    + #include \"hook.h\"\n    + #include \"run-command.h\"\n      \n    -+static const char *find_legacy_hook(const char *name)\n    ++const char *find_hook(const char *name)\n     +{\n     +\tstatic struct strbuf path = STRBUF_INIT;\n     +\n    @@ hook.c: static int should_include_hookdir(const char *path, enum hookdir_opt cfg\n     +}\n     +\n     +\n    - struct list_head* hook_list(const char* hookname)\n    ++\n    + void run_hooks_opt_clear(struct run_hooks_opt *o)\n      {\n    - \tstruct strbuf hook_key = STRBUF_INIT;\n    -@@ hook.c: struct list_head* hook_list(const char* hookname)\n    - \tgit_config(hook_config_lookup, &cb_data);\n    - \n    - \tif (have_git_dir()) {\n    --\t\tconst char *legacy_hook_path = find_hook(hookname);\n    -+\t\tconst char *legacy_hook_path = find_legacy_hook(hookname);\n    - \n    - \t\t/* Unconditionally add legacy hook, but annotate it. */\n    - \t\tif (legacy_hook_path) {\n    -@@ hook.c: int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n    - \tcould_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||\n    - \t\t\t\tshould_run_hookdir == HOOKDIR_WARN ||\n    - \t\t\t\tshould_run_hookdir == HOOKDIR_YES)\n    --\t\t\t\t&& !!find_hook(hookname);\n    -+\t\t\t\t&& !!find_legacy_hook(hookname);\n    + \tstrvec_clear(&o->env);\n    +\n    + ## hook.h ##\n    +@@ hook.h: struct hook_cb_data {\n    + \tstruct run_hooks_opt *options;\n    + };\n      \n    - \tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n    ++/*\n    ++ * Returns the path to the hook file, or NULL if the hook is missing\n    ++ * or disabled. Note that this points to static storage that will be\n    ++ * overwritten by further calls to find_hook and run_hook_*.\n    ++ */\n    ++const char *find_hook(const char *name);\n    ++\n    + void run_hooks_opt_clear(struct run_hooks_opt *o);\n      \n    + /*\n    +\n    + ## refs.c ##\n    +@@\n    + #include \"refs.h\"\n    + #include \"refs/refs-internal.h\"\n    + #include \"run-command.h\"\n    ++#include \"hook.h\"\n    + #include \"object-store.h\"\n    + #include \"object.h\"\n    + #include \"tag.h\"\n     \n      ## run-command.c ##\n    +@@\n    + #include \"string-list.h\"\n    + #include \"quote.h\"\n    + #include \"config.h\"\n    ++#include \"hook.h\"\n    + \n    + void child_process_init(struct child_process *child)\n    + {\n     @@ run-command.c: int async_with_fork(void)\n      #endif\n      }\n    @@ run-command.c: int async_with_fork(void)\n     -\treturn path.buf;\n     -}\n     -\n    --int run_hook_ve(const char *const *env, const char *name, va_list args)\n    --{\n    --\tstruct child_process hook = CHILD_PROCESS_INIT;\n    --\tconst char *p;\n    --\n    --\tp = find_hook(name);\n    --\tif (!p)\n    --\t\treturn 0;\n    --\n    --\tstrvec_push(&hook.args, p);\n    --\twhile ((p = va_arg(args, const char *)))\n    --\t\tstrvec_push(&hook.args, p);\n    --\thook.env = env;\n    --\thook.no_stdin = 1;\n    --\thook.stdout_to_stderr = 1;\n    --\thook.trace2_hook_name = name;\n    --\n    --\treturn run_command(&hook);\n    --}\n    --\n    --int run_hook_le(const char *const *env, const char *name, ...)\n    --{\n    --\tva_list args;\n    --\tint ret;\n    --\n    --\tva_start(args, name);\n    --\tret = run_hook_ve(env, name, args);\n    --\tva_end(args);\n    --\n    --\treturn ret;\n    --}\n    --\n    - struct io_pump {\n    - \t/* initialized by caller */\n    - \tint fd;\n    + int run_hook_ve(const char *const *env, const char *name, va_list args)\n    + {\n    + \tstruct child_process hook = CHILD_PROCESS_INIT;\n     \n      ## run-command.h ##\n     @@ run-command.h: int finish_command_in_signal(struct child_process *);\n    @@ run-command.h: int finish_command_in_signal(struct child_process *);\n     - */\n     -const char *find_hook(const char *name);\n     -\n    --/**\n    -- * Run a hook.\n    -- * The first argument is a pathname to an index file, or NULL\n    -- * if the hook uses the default index file or no index is needed.\n    -- * The second argument is the name of the hook.\n    -- * The further arguments correspond to the hook arguments.\n    -- * The last argument has to be NULL to terminate the arguments list.\n    -- * If the hook does not exist or is not executable, the return\n    -- * value will be zero.\n    -- * If it is executable, the hook will be executed and the exit\n    -- * status of the hook is returned.\n    -- * On execution, .stdout_to_stderr and .no_stdin will be set.\n    -- */\n    --LAST_ARG_MUST_BE_NULL\n    --int run_hook_le(const char *const *env, const char *name, ...);\n    --int run_hook_ve(const char *const *env, const char *name, va_list args);\n    --\n    - /*\n    -  * Trigger an auto-gc\n    -  */\n    + /**\n    +  * Run a hook.\n    +  * The first argument is a pathname to an index file, or NULL\n    +\n    + ## sequencer.c ##\n    +@@\n    + #include \"sequencer.h\"\n    + #include \"tag.h\"\n    + #include \"run-command.h\"\n    ++#include \"hook.h\"\n    + #include \"exec-cmd.h\"\n    + #include \"utf8.h\"\n    + #include \"cache-tree.h\"\n    +\n    + ## transport.c ##\n    +@@\n    + #include \"config.h\"\n    + #include \"transport.h\"\n    + #include \"run-command.h\"\n    ++#include \"hook.h\"\n    + #include \"pkt-line.h\"\n    + #include \"fetch-pack.h\"\n    + #include \"remote.h\"\n33:  33043b33b04 !  5:  a6f0817ad81 bugreport: use hook_exists instead of find_hook\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    bugreport: use hook_exists instead of find_hook\n    +    hook.c: add a hook_exists() wrapper and use it in bugreport.c\n     \n    -    By using the helper in hook.h instead of the one in run-command.h, we\n    -    can also check whether a hook exists in the config - not just whether it\n    -    exists in the hookdir.\n    +    Add a boolean version of the find_hook() function for those callers\n    +    who are only interested in checking whether the hook exists, not what\n    +    the path to it is.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/bugreport.c ##\n    -@@\n    - #include \"strbuf.h\"\n    - #include \"help.h\"\n    - #include \"compat/compiler.h\"\n    --#include \"run-command.h\"\n    -+#include \"hook.h\"\n    - \n    - \n    - static void get_system_info(struct strbuf *sys_info)\n     @@ builtin/bugreport.c: static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n      \t}\n      \n      \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n     -\t\tif (find_hook(hook[i]))\n    -+\t\tif (hook_exists(hook[i], HOOKDIR_USE_CONFIG))\n    ++\t\tif (hook_exists(hook[i]))\n      \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n      }\n      \n    +\n    + ## hook.c ##\n    +@@ hook.c: const char *find_hook(const char *name)\n    + \treturn path.buf;\n    + }\n    + \n    +-\n    ++int hook_exists(const char *name)\n    ++{\n    ++\treturn !!find_hook(name);\n    ++}\n    + \n    + void run_hooks_opt_clear(struct run_hooks_opt *o)\n    + {\n    +\n    + ## hook.h ##\n    +@@ hook.h: struct hook_cb_data {\n    +  */\n    + const char *find_hook(const char *name);\n    + \n    ++/*\n    ++ * A boolean version of find_hook()\n    ++ */\n    ++int hook_exists(const char *hookname);\n    ++\n    + void run_hooks_opt_clear(struct run_hooks_opt *o);\n    + \n    + /*\n20:  2006e2709f6 !  6:  b186fde43e1 gc: use hook library for pre-auto-gc hook\n    @@ Commit message\n         well as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It\n    - takes no parameter, and exiting with non-zero status from this script\n    - causes the `git gc --auto` to abort.\n    - \n    -+Hooks run during 'pre-auto-gc' will be run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - post-rewrite\n    - ~~~~~~~~~~~~\n    - \n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/gc.c ##\n     @@\n    @@ builtin/gc.c: static void add_repack_incremental_option(void)\n      \n      static int need_to_gc(void)\n      {\n    -+\tstruct run_hooks_opt hook_opt;\n    ++\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n     +\n      \t/*\n      \t * Setting gc.auto to 0 or negative can disable the\n    @@ builtin/gc.c: static int need_to_gc(void)\n      \t\treturn 0;\n      \n     -\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n    -+\trun_hooks_opt_init_async(&hook_opt);\n     +\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n     +\t\trun_hooks_opt_clear(&hook_opt);\n      \t\treturn 0;\n21:  62f6f9ab90d !  7:  528402fac69 rebase: teach pre-rebase to use hook.h\n    @@ Metadata\n      ## Commit message ##\n         rebase: teach pre-rebase to use hook.h\n     \n    -    By using hook.h instead of run-command.h to run hooks, pre-rebase hooks\n    -    can now be specified in the config as well as in the hookdir. pre-rebase\n    -    is not called anywhere besides builtin/rebase.c.\n    +    Move the pre-rebase hook away from run-command.h to and over to the\n    +    new hook.h library.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: two parameters.  The first parameter is the upstream from which\n    - the series was forked.  The second parameter is the branch being\n    - rebased, and is not set when rebasing the current branch.\n    - \n    -+Hooks executed during 'pre-rebase' will run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - post-checkout\n    - ~~~~~~~~~~~~~\n    - \n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/rebase.c ##\n     @@\n    @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix\n      \tchar *squash_onto_name = NULL;\n      \tint reschedule_failed_exec = -1;\n      \tint allow_preemptive_ff = 1;\n    -+\tstruct run_hooks_opt hook_opt;\n    ++\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n      \tstruct option builtin_rebase_options[] = {\n      \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n      \t\t\t   N_(\"revision\"),\n    @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix\n      \t}\n      \n      \t/* If a hook exists, give it a chance to interrupt*/\n    -+\trun_hooks_opt_init_async(&hook_opt);\n     +\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n      \tif (!ok_to_skip_pre_rebase &&\n     -\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n18:  744e156ae91 !  8:  69842c74383 am: convert applypatch hooks to use config\n    @@ Commit message\n         am: convert applypatch hooks to use config\n     \n         Teach pre-applypatch, post-applypatch, and applypatch-msg to use the\n    -    hook.h library instead of the run-command.h library. This enables use of\n    -    hooks specified in the config, in addition to those in the hookdir.\n    -    These three hooks are called only by builtin/am.c.\n    +    hook.h library instead of the run-command.h library.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: the message file.\n    - The default 'applypatch-msg' hook, when enabled, runs the\n    - 'commit-msg' hook, if the latter is enabled.\n    - \n    -+Hooks run during 'applypatch-msg' will not be parallelized, because hooks are\n    -+expected to edit the file holding the commit log message.\n    -+\n    - pre-applypatch\n    - ~~~~~~~~~~~~~~\n    - \n    -@@ Documentation/githooks.txt: make a commit if it does not pass certain test.\n    - The default 'pre-applypatch' hook, when enabled, runs the\n    - 'pre-commit' hook, if the latter is enabled.\n    - \n    -+Hooks run during 'pre-applypatch' will be run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - post-applypatch\n    - ~~~~~~~~~~~~~~~\n    - \n    -@@ Documentation/githooks.txt: and is invoked after the patch is applied and a commit is made.\n    - This hook is meant primarily for notification, and cannot affect\n    - the outcome of `git am`.\n    - \n    -+Hooks run during 'post-applypatch' will be run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - pre-commit\n    - ~~~~~~~~~~\n    - \n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/am.c ##\n    -@@\n    - #include \"string-list.h\"\n    - #include \"packfile.h\"\n    - #include \"repository.h\"\n    -+#include \"hook.h\"\n    - \n    - /**\n    -  * Returns the length of the first line of msg.\n     @@ builtin/am.c: static void am_destroy(const struct am_state *state)\n      static int run_applypatch_msg_hook(struct am_state *state)\n      {\n      \tint ret;\n    -+\tstruct run_hooks_opt opt;\n    -+\n    -+\trun_hooks_opt_init_sync(&opt);\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n      \n      \tassert(state->msg);\n     -\tret = run_hook_le(NULL, \"applypatch-msg\", am_path(state, \"final-commit\"), NULL);\n    @@ builtin/am.c: static void do_commit(const struct am_state *state)\n      \tstruct commit_list *parents = NULL;\n      \tconst char *reflog_msg, *author, *committer = NULL;\n      \tstruct strbuf sb = STRBUF_INIT;\n    -+\tstruct run_hooks_opt hook_opt;\n    -+\n    -+\trun_hooks_opt_init_async(&hook_opt);\n    ++\tstruct run_hooks_opt hook_opt_pre = RUN_HOOKS_OPT_INIT;\n    ++\tstruct run_hooks_opt hook_opt_post = RUN_HOOKS_OPT_INIT;\n      \n     -\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n    -+\tif (run_hooks(\"pre-applypatch\", &hook_opt)) {\n    -+\t\trun_hooks_opt_clear(&hook_opt);\n    ++\tif (run_hooks(\"pre-applypatch\", &hook_opt_pre)) {\n    ++\t\trun_hooks_opt_clear(&hook_opt_pre);\n      \t\texit(1);\n     +\t}\n    -+\n    -+\trun_hooks_opt_clear(&hook_opt);\n      \n      \tif (write_cache_as_tree(&tree, 0, NULL))\n      \t\tdie(_(\"git write-tree failed to write a tree\"));\n    @@ builtin/am.c: static void do_commit(const struct am_state *state)\n      \t}\n      \n     -\trun_hook_le(NULL, \"post-applypatch\", NULL);\n    -+\trun_hooks_opt_init_async(&hook_opt);\n    -+\trun_hooks(\"post-applypatch\", &hook_opt);\n    ++\trun_hooks(\"post-applypatch\", &hook_opt_post);\n      \n    -+\trun_hooks_opt_clear(&hook_opt);\n    ++\trun_hooks_opt_clear(&hook_opt_pre);\n    ++\trun_hooks_opt_clear(&hook_opt_post);\n      \tstrbuf_release(&sb);\n      }\n      \n25:  a13a690bebb !  9:  9b32c14669b hooks: convert 'post-checkout' hook to hook library\n    @@ Metadata\n      ## Commit message ##\n         hooks: convert 'post-checkout' hook to hook library\n     \n    -    By using the 'hook.h' library, 'post-checkout' hooks can now be\n    -    specified in the config as well as in the hook directory.\n    +    Move the running of the 'post-checkout' hook away from run-command.h\n    +    to the new hook.h library. For \"worktree\" this requires a change to it\n    +    to run the hooks from a given directory.\n     \n    -    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    We could strictly speaking skip the \"absolute_path\" flag and just\n    +    check if \"dir\" is specified, but let's split them up for clarity, as\n    +    well as for any future user who'd like to set \"dir\" but not implicitly\n    +    change the argument to an absolute path.\n     \n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: This hook can be used to perform repository validity checks, auto-display\n    - differences from the previous HEAD if different, or set working dir metadata\n    - properties.\n    - \n    -+Hooks executed during 'post-checkout' will not be parallelized.\n    -+\n    - post-merge\n    - ~~~~~~~~~~\n    - \n    +    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/checkout.c ##\n     @@\n    @@ builtin/checkout.c: struct branch_info {\n     -\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n     -\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n     -\t\t\t   changed ? \"1\" : \"0\", NULL);\n    -+\tstruct run_hooks_opt opt;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     +\tint rc;\n    -+\n    -+\trun_hooks_opt_init_sync(&opt);\n     +\n      \t/* \"new_commit\" can be NULL when checking out from the index before\n      \t   a commit exists. */\n    @@ builtin/clone.c: static int checkout(int submodule_progress)\n      \tstruct tree *tree;\n      \tstruct tree_desc t;\n      \tint err = 0;\n    -+\tstruct run_hooks_opt hook_opt;\n    ++\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n      \n      \tif (option_no_checkout)\n      \t\treturn 0;\n    @@ builtin/clone.c: static int checkout(int submodule_progress)\n      \n     -\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(null_oid()),\n     -\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n    -+\trun_hooks_opt_init_sync(&hook_opt);\n     +\tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n     +\terr |= run_hooks(\"post-checkout\", &hook_opt);\n     +\trun_hooks_opt_clear(&hook_opt);\n    @@ builtin/clone.c: static int checkout(int submodule_progress)\n      \t\tstruct strvec args = STRVEC_INIT;\n     \n      ## builtin/worktree.c ##\n    -@@\n    - #include \"utf8.h\"\n    - #include \"worktree.h\"\n    - #include \"quote.h\"\n    -+#include \"hook.h\"\n    - \n    - static const char * const worktree_usage[] = {\n    - \tN_(\"git worktree add [<options>] <path> [<commit-ish>]\"),\n     @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refname,\n      \t * is_junk is cleared, but do return appropriate code when hook fails.\n      \t */\n    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam\n     -\t\t\t\t     \"1\", NULL);\n     -\t\t\tret = run_command(&cp);\n     -\t\t}\n    -+\t\tstruct run_hooks_opt opt;\n    -+\n    -+\t\trun_hooks_opt_init_sync(&opt);\n    ++\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     +\n     +\t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n     +\t\tstrvec_pushl(&opt.args,\n    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam\n     +\t\t\t     \"1\",\n     +\t\t\t     NULL);\n     +\t\topt.dir = path;\n    ++\t\topt.absolute_path = 1;\n     +\n     +\t\tret = run_hooks(\"post-checkout\", &opt);\n     +\n    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam\n      \n      \tstrvec_clear(&child_env);\n     \n    + ## hook.c ##\n    +@@ hook.c: static int pick_next_hook(struct child_process *cp,\n    + \tcp->env = hook_cb->options->env.v;\n    + \tcp->stdout_to_stderr = 1;\n    + \tcp->trace2_hook_name = hook_cb->hook_name;\n    ++\tcp->dir = hook_cb->options->dir;\n    + \n    + \t/* add command */\n    + \tstrvec_push(&cp->args, run_me->hook_path);\n    +@@ hook.c: static int notify_hook_finished(int result,\n    + int run_found_hooks(const char *hook_name, const char *hook_path,\n    + \t\t    struct run_hooks_opt *options)\n    + {\n    ++\tstruct strbuf abs_path = STRBUF_INIT;\n    + \tstruct hook my_hook = {\n    + \t\t.hook_path = hook_path,\n    + \t};\n    +@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    + \t\t.hook_name = hook_name,\n    + \t\t.options = options,\n    + \t};\n    ++\tif (options->absolute_path) {\n    ++\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n    ++\t\tmy_hook.hook_path = abs_path.buf;\n    ++\t}\n    + \tcb_data.run_me = &my_hook;\n    + \n    + \tif (options->jobs != 1)\n    +@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    + \t\t\t\t   &cb_data,\n    + \t\t\t\t   \"hook\",\n    + \t\t\t\t   hook_name);\n    ++\tif (options->absolute_path)\n    ++\t\tstrbuf_release(&abs_path);\n    + \n    + \treturn cb_data.rc;\n    + }\n    +\n    + ## hook.h ##\n    +@@ hook.h: struct run_hooks_opt\n    + \n    + \t/* Number of threads to parallelize across */\n    + \tint jobs;\n    ++\n    ++\t/* Resolve and run the \"absolute_path(hook)\" instead of\n    ++\t * \"hook\". Used for \"git worktree\" hooks\n    ++\t */\n    ++\tint absolute_path;\n    ++\n    ++\t/* Path to initial working directory for subprocess */\n    ++\tconst char *dir;\n    ++\n    + };\n    + \n    + #define RUN_HOOKS_OPT_INIT { \\\n    +\n      ## read-cache.c ##\n     @@\n    + #include \"thread-utils.h\"\n      #include \"progress.h\"\n      #include \"sparse-index.h\"\n    - #include \"hook.h\"\n    -->>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)\n    ++#include \"hook.h\"\n      \n      /* Mask for the name length in ce_flags in the on-disk index */\n      \n    @@ reset.c: int reset_head(struct repository *r, struct object_id *oid, const char\n     -\t\t\t    oid_to_hex(orig ? orig : null_oid()),\n     -\t\t\t    oid_to_hex(oid), \"1\", NULL);\n     +\tif (run_hook) {\n    -+\t\tstruct run_hooks_opt opt;\n    -+\n    -+\t\trun_hooks_opt_init_sync(&opt);\n    ++\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     +\t\tstrvec_pushl(&opt.args,\n     +\t\t\t     oid_to_hex(orig ? orig : null_oid()),\n     +\t\t\t     oid_to_hex(oid),\n19:  be1d7c7636b ! 10:  201c654bb0c merge: use config-based hooks for post-merge hook\n    @@ Commit message\n         builtin/merge.c.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: save and restore any form of metadata associated with the working tree\n    - (e.g.: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl\n    - for an example of how to do this.\n    - \n    -+Hooks executed during 'post-merge' will run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - pre-push\n    - ~~~~~~~~\n    - \n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/merge.c ##\n     @@ builtin/merge.c: static void finish(struct commit *head_commit,\n      \t\t   const struct object_id *new_head, const char *msg)\n      {\n      \tstruct strbuf reflog_message = STRBUF_INIT;\n    -+\tstruct run_hooks_opt opt;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n      \tconst struct object_id *head = &head_commit->object.oid;\n      \n      \tif (!msg)\n    @@ builtin/merge.c: static void finish(struct commit *head_commit,\n      \n      \t/* Run a post-merge hook */\n     -\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n    -+\trun_hooks_opt_init_async(&opt);\n     +\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n     +\trun_hooks(\"post-merge\", &opt);\n     +\trun_hooks_opt_clear(&opt);\n      \n      \tapply_autostash(git_path_merge_autostash(the_repository));\n      \tstrbuf_release(&reflog_message);\n    +@@ builtin/merge.c: static void prepare_to_commit(struct commit_list *remoteheads)\n    + \t * and write it out as a tree.  We must do this before we invoke\n    + \t * the editor and after we invoke run_status above.\n    + \t */\n    +-\tif (find_hook(\"pre-merge-commit\"))\n    ++\tif (hook_exists(\"pre-merge-commit\"))\n    + \t\tdiscard_cache();\n    + \tread_cache_from(index_file);\n    + \tstrbuf_addbuf(&msg, &merge_msg);\n -:  ----------- > 11:  e65d8bd6e6f git hook run: add an --ignore-missing flag\n34:  832136eb930 ! 12:  8795e9ceab8 git-send-email: use 'git hook run' for 'sendemail-validate'\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    git-send-email: use 'git hook run' for 'sendemail-validate'\n    +    send-email: use 'git hook run' for 'sendemail-validate'\n     \n    -    By using the new 'git hook run' subcommand to run 'sendemail-validate',\n    -    we can reduce the boilerplate needed to run this hook in perl. Using\n    -    config-based hooks also allows us to run 'sendemail-validate' hooks that\n    -    were configured globally when running 'git send-email' from outside of a\n    -    Git directory, alongside other benefits like multihooks and\n    -    parallelization.\n    +    Change the \"sendmail-validate\" hook to be run via the \"git hook run\"\n    +    wrapper instead of via a direct invocation.\n    +\n    +    This is the smallest possibly change to get \"send-email\" using \"git\n    +    hook run\". We still check the hook itself with \"-x\", and set a\n    +    \"GIT_DIR\" variable, both of which are asserted by our tests. We'll\n    +    need to get rid of this special behavior if we start running N hooks,\n    +    but for now let's be as close to bug-for-bug compatible as possible.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## git-send-email.perl ##\n    -@@ git-send-email.perl: sub unique_email_list {\n    - sub validate_patch {\n    +@@ git-send-email.perl: sub format_2822_time {\n    + my $editor;\n    + \n    + sub system_or_msg {\n    +-\tmy ($args, $msg) = @_;\n    ++\tmy ($args, $msg, $cmd_name) = @_;\n    + \tsystem(@$args);\n    + \tmy $signalled = $? & 127;\n    + \tmy $exit_code = $? >> 8;\n    + \treturn unless $signalled or $exit_code;\n    + \n    +-\tmy @sprintf_args = ($args->[0], $exit_code);\n    ++\tmy @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);\n    + \tif (defined $msg) {\n    + \t\t# Quiet the 'redundant' warning category, except we\n    + \t\t# need to support down to Perl 5.8, so we can't do a\n    +@@ git-send-email.perl: sub validate_patch {\n      \tmy ($fn, $xfer_encoding) = @_;\n      \n    --\tif ($repo) {\n    --\t\tmy $validate_hook = catfile($repo->hooks_path(),\n    + \tif ($repo) {\n    ++\t\tmy $hook_name = 'sendemail-validate';\n    + \t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n    +-\t\tmy $validate_hook = catfile($hooks_path,\n     -\t\t\t\t\t    'sendemail-validate');\n    --\t\tmy $hook_error;\n    --\t\tif (-x $validate_hook) {\n    --\t\t\tmy $target = abs_path($fn);\n    --\t\t\t# The hook needs a correct cwd and GIT_DIR.\n    --\t\t\tmy $cwd_save = cwd();\n    --\t\t\tchdir($repo->wc_path() or $repo->repo_path())\n    --\t\t\t\tor die(\"chdir: $!\");\n    --\t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n    ++\t\tmy $validate_hook = catfile($hooks_path, $hook_name);\n    + \t\tmy $hook_error;\n    + \t\tif (-x $validate_hook) {\n    + \t\t\tmy $target = abs_path($fn);\n    +@@ git-send-email.perl: sub validate_patch {\n    + \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n    + \t\t\t\tor die(\"chdir: $!\");\n    + \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n     -\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n    --\t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n    --\t\t}\n    --\t\tif ($hook_error) {\n    ++\t\t\tmy @validate_hook = (\"git\", \"hook\", \"run\", \"--ignore-missing\", $hook_name, \"--\", $target);\n    ++\t\t\t$hook_error = system_or_msg(\\@validate_hook, undef,\n    ++\t\t\t\t\t\t       \"git hook run $hook_name -- <patch>\");\n    + \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n    + \t\t}\n    + \t\tif ($hook_error) {\n     -\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n     -\t\t\t\t       \"%s\\n\" .\n     -\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n    --\t\t}\n    --\t}\n    -+\tmy $target = abs_path($fn);\n    -+\n    -+\tsystem_or_die([\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-j1\", \"-a\", $target],\n    -+\t\tsprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n    -+\t\t\t   \"warning: no patches were sent\\n\"),\n    -+\t\t        $fn));\n    ++\t\t\t$hook_error = sprintf(__(\"fatal: %s: rejected by %s hook\\n\" .\n    ++\t\t\t\t\t\t $hook_error . \"\\n\" .\n    ++\t\t\t\t\t\t \"warning: no patches were sent\\n\"),\n    ++\t\t\t\t\t      $fn, $hook_name);\n    ++\t\t\tdie $hook_error;\n    + \t\t}\n    + \t}\n      \n    - \t# Any long lines will be automatically fixed if we use a suitable transfer\n    - \t# encoding.\n     \n      ## t/t9001-send-email.sh ##\n     @@ t/t9001-send-email.sh: test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n      \ttest_path_is_file my-hooks.ran &&\n      \tcat >expect <<-EOF &&\n      \tfatal: longline.patch: rejected by sendemail-validate hook\n    --\tfatal: command '\"'\"'$PWD/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n    +-\tfatal: command '\"'\"'my-hooks/sendemail-validate'\"'\"' died with exit code 1\n    ++\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n      \twarning: no patches were sent\n      \tEOF\n      \ttest_cmp expect actual\n    @@ t/t9001-send-email.sh: test_expect_success $PREREQ \"--validate respects absolute\n      \ttest_path_is_file my-hooks.ran &&\n      \tcat >expect <<-EOF &&\n      \tfatal: longline.patch: rejected by sendemail-validate hook\n    --\tfatal: command '\"'\"'$PWD/my-hooks/sendemail-validate'\"'\"' died with exit code 1\n    +-\tfatal: command '\"'\"'$hooks_path/sendemail-validate'\"'\"' died with exit code 1\n    ++\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n      \twarning: no patches were sent\n      \tEOF\n      \ttest_cmp expect actual\n    -@@ t/t9001-send-email.sh: test_expect_success $PREREQ 'invoke hook' '\n    - \tmkdir -p .git/hooks &&\n    - \n    - \twrite_script .git/hooks/sendemail-validate <<-\\EOF &&\n    --\t# test that we have the correct environment variable, pwd, and\n    --\t# argument\n    --\tcase \"$GIT_DIR\" in\n    --\t*.git)\n    --\t\ttrue\n    --\t\t;;\n    --\t*)\n    --\t\tfalse\n    --\t\t;;\n    --\tesac &&\n    -+\t# test that we have the correct argument\n    - \ttest -f 0001-add-main.patch &&\n    - \tgrep \"add main\" \"$1\"\n    - \tEOF\n24:  f4c92f0dbc1 ! 13:  03129460fd2 git-p4: use 'git hook' to run hooks\n    @@ Commit message\n         git-p4: use 'git hook' to run hooks\n     \n         Instead of duplicating the behavior of run-command.h:run_hook_le() in\n    -    Python, we can directly call 'git hook run'. As a bonus, this means\n    -    git-p4 learns how to find hook specifications from the Git config as\n    -    well as from the hookdir.\n    +    Python, we can directly call 'git hook run'. We emulate the existence\n    +    check with the --ignore-missing flag.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## git-p4.py ##\n     @@ git-p4.py: def decode_path(path):\n    +         return path\n      \n      def run_git_hook(cmd, param=[]):\n    -     \"\"\"Execute a hook if the hook exists.\"\"\"\n    +-    \"\"\"Execute a hook if the hook exists.\"\"\"\n     -    if verbose:\n     -        sys.stderr.write(\"Looking for hook: %s\\n\" % cmd)\n     -        sys.stderr.flush()\n    @@ git-p4.py: def decode_path(path):\n     -                    return True\n     -\n     -    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):\n    -+    if not cmd:\n    -         return True\n    - \n    +-        return True\n    +-\n     -    return run_hook_command(hook_file, param) == 0\n     -\n     -def run_hook_command(cmd, param):\n    @@ git-p4.py: def decode_path(path):\n     -        else:\n     -            use_shell = True\n     -    return subprocess.call(cli, shell=use_shell)\n    +-\n     +    \"\"\"args are specified with -a <arg> -a <arg> -a <arg>\"\"\"\n    -+    args = (['git', 'hook', 'run'] +\n    -+            [\"-a\" + arg for arg in param] +\n    -+            [cmd])\n    - \n    ++    args = ['git', 'hook', 'run', '--ignore-missing', cmd]\n    ++    if param:\n    ++        args.append(\"--\")\n    ++        for p in param:\n    ++            args.append(p)\n     +    return subprocess.call(args) == 0\n      \n      def write_pipe(c, stdin):\n -:  ----------- > 14:  3f3610f5ed3 commit: use hook.h to execute hooks\n22:  0e405276551 ! 15:  6482a3e4cb8 read-cache: convert post-index-change hook to use config\n    @@ Commit message\n         can now be specified in the config in addition to the hookdir.\n         post-index-change is not run anywhere besides in read-cache.c.\n     \n    -    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    This removes the last direct user of run_hook_ve(), so we can make the\n    +    function static now. It'll be removed entirely soon.\n     \n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: and \"0\" meaning they were not.\n    - Only one parameter should be set to \"1\" when the hook runs.  The hook\n    - running passing \"1\", \"1\" should not be possible.\n    - \n    -+Hooks run during 'post-index-change' will be run in parallel, unless hook.jobs\n    -+is configured to 1.\n    -+\n    - GIT\n    - ---\n    - Part of the linkgit:git[1] suite\n    +    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## read-cache.c ##\n    -@@\n    - #include \"thread-utils.h\"\n    - #include \"progress.h\"\n    - #include \"sparse-index.h\"\n    -+#include \"hook.h\"\n    -+>>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)\n    - \n    - /* Mask for the name length in ce_flags in the on-disk index */\n    - \n     @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n      {\n      \tint ret;\n      \tint was_full = !istate->sparse_index;\n    -+\tstruct run_hooks_opt hook_opt;\n    ++\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n      \n      \tret = convert_to_sparse(istate);\n      \n    @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struc\n     -\trun_hook_le(NULL, \"post-index-change\",\n     -\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n     -\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n    -+\trun_hooks_opt_init_async(&hook_opt);\n     +\tstrvec_pushl(&hook_opt.args,\n     +\t\t     istate->updated_workdir ? \"1\" : \"0\",\n     +\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n    @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struc\n      \tistate->updated_workdir = 0;\n      \tistate->updated_skipworktree = 0;\n      \n    +\n    + ## run-command.c ##\n    +@@ run-command.c: int async_with_fork(void)\n    + #endif\n    + }\n    + \n    +-int run_hook_ve(const char *const *env, const char *name, va_list args)\n    ++static int run_hook_ve(const char *const *env, const char *name, va_list args)\n    + {\n    + \tstruct child_process hook = CHILD_PROCESS_INIT;\n    + \tconst char *p;\n    +\n    + ## run-command.h ##\n    +@@ run-command.h: int run_command(struct child_process *);\n    +  */\n    + LAST_ARG_MUST_BE_NULL\n    + int run_hook_le(const char *const *env, const char *name, ...);\n    +-int run_hook_ve(const char *const *env, const char *name, va_list args);\n    + \n    + /*\n    +  * Trigger an auto-gc\n23:  a793508373e ! 16:  a16163d4fb5 receive-pack: convert push-to-checkout hook to hook.h\n    @@ Commit message\n         hooks can now be specified in the config as well as in the hookdir.\n         push-to-checkout is not called anywhere but in builtin/receive-pack.c.\n     \n    -    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    This is the last user of the run_hook_le() API, so let's remove it\n    +    while we're at it, since run_hook_le() itself is the last user of\n    +    run_hook_ve() we can remove that too. The last direct user of\n    +    run_hook_le() was removed in the commit preceding this one.\n     \n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: that switches branches while\n    - keeping the local changes in the working tree that do not interfere\n    - with the difference between the branches.\n    - \n    -+Hooks executed during 'push-to-checkout' will not be parallelized.\n    - \n    - pre-auto-gc\n    - ~~~~~~~~~~~\n    +    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/receive-pack.c ##\n    -@@\n    - #include \"commit-reach.h\"\n    - #include \"worktree.h\"\n    - #include \"shallow.h\"\n    -+#include \"hook.h\"\n    - \n    - static const char * const receive_pack_usage[] = {\n    - \tN_(\"git receive-pack <git-dir>\"),\n     @@ builtin/receive-pack.c: static const char *push_to_checkout(unsigned char *hash,\n      \t\t\t\t    struct strvec *env,\n      \t\t\t\t    const char *work_tree)\n      {\n    -+\tstruct run_hooks_opt opt;\n    -+\n    -+\trun_hooks_opt_init_sync(&opt);\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     +\n      \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n     -\tif (run_hook_le(env->v, push_to_checkout_hook,\n    @@ builtin/receive-pack.c: static const char *update_worktree(unsigned char *sha1,\n      \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n      \n     -\tif (!find_hook(push_to_checkout_hook))\n    -+\tif (!hook_exists(push_to_checkout_hook, HOOKDIR_USE_CONFIG))\n    ++\tif (!hook_exists(push_to_checkout_hook))\n      \t\tretval = push_to_deploy(sha1, &env, work_tree);\n      \telse\n      \t\tretval = push_to_checkout(sha1, &env, work_tree);\n    +\n    + ## run-command.c ##\n    +@@ run-command.c: int async_with_fork(void)\n    + #endif\n    + }\n    + \n    +-static int run_hook_ve(const char *const *env, const char *name, va_list args)\n    +-{\n    +-\tstruct child_process hook = CHILD_PROCESS_INIT;\n    +-\tconst char *p;\n    +-\n    +-\tp = find_hook(name);\n    +-\tif (!p)\n    +-\t\treturn 0;\n    +-\n    +-\tstrvec_push(&hook.args, p);\n    +-\twhile ((p = va_arg(args, const char *)))\n    +-\t\tstrvec_push(&hook.args, p);\n    +-\thook.env = env;\n    +-\thook.no_stdin = 1;\n    +-\thook.stdout_to_stderr = 1;\n    +-\thook.trace2_hook_name = name;\n    +-\n    +-\treturn run_command(&hook);\n    +-}\n    +-\n    +-int run_hook_le(const char *const *env, const char *name, ...)\n    +-{\n    +-\tva_list args;\n    +-\tint ret;\n    +-\n    +-\tva_start(args, name);\n    +-\tret = run_hook_ve(env, name, args);\n    +-\tva_end(args);\n    +-\n    +-\treturn ret;\n    +-}\n    +-\n    + struct io_pump {\n    + \t/* initialized by caller */\n    + \tint fd;\n    +\n    + ## run-command.h ##\n    +@@ run-command.h: int finish_command_in_signal(struct child_process *);\n    +  */\n    + int run_command(struct child_process *);\n    + \n    +-/**\n    +- * Run a hook.\n    +- * The first argument is a pathname to an index file, or NULL\n    +- * if the hook uses the default index file or no index is needed.\n    +- * The second argument is the name of the hook.\n    +- * The further arguments correspond to the hook arguments.\n    +- * The last argument has to be NULL to terminate the arguments list.\n    +- * If the hook does not exist or is not executable, the return\n    +- * value will be zero.\n    +- * If it is executable, the hook will be executed and the exit\n    +- * status of the hook is returned.\n    +- * On execution, .stdout_to_stderr and .no_stdin will be set.\n    +- */\n    +-LAST_ARG_MUST_BE_NULL\n    +-int run_hook_le(const char *const *env, const char *name, ...);\n    +-\n    + /*\n    +  * Trigger an auto-gc\n    +  */\n10:  744437273de ! 17:  7020cf10c8e run-command: allow stdin for run_processes_parallel\n    @@ Commit message\n         on child output in stderr.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## run-command.c ##\n     @@ run-command.c: static int pp_start_one(struct parallel_processes *pp)\n 9:  3e07045894f ! 18:  4745dcfce49 hook: support passing stdin to hooks\n    @@ Commit message\n         interim file themselves.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## Documentation/git-hook.txt ##\n    -@@ Documentation/git-hook.txt: SYNOPSIS\n    +@@ Documentation/git-hook.txt: git-hook - run git hooks\n    + SYNOPSIS\n      --------\n      [verse]\n    - 'git hook' list <hook-name>\n    --'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hook-name>\n    -+'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>]\n    -+\t<hook-name>\n    +-'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n    ++'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-hook.txt: in the order they should be run, and print the config scope where the relevant\n    - `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n    - This output is human-readable and the format is subject to change over time.\n    - \n    --run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] `<hook-name>`::\n    -+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] `<hook-name>`::\n    - \n    - Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n    - hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n    -@@ Documentation/git-hook.txt: Specify arguments to pass to every hook that is run.\n    - +\n    - Specify environment variables to set for every hook that is run.\n    +@@ Documentation/git-hook.txt: run::\n    + OPTIONS\n    + -------\n      \n     +--to-stdin::\n    -+\tOnly valid for `run`.\n    -++\n    -+Specify a file which will be streamed into stdin for every hook that is run.\n    -+Each hook will receive the entire file from beginning to EOF.\n    ++\tFor \"run\"; Specify a file which will be streamed into the\n    ++\thook's stdin. The hook will receive the entire file from\n    ++\tbeginning to EOF.\n     +\n    - CONFIGURATION\n    - -------------\n    - include::config/hook.txt[]\n    + --ignore-missing::\n    + \tIgnore any missing hook by quietly returning zero. Used for\n    + \ttools that want to do a blind one-shot run of a hook that may\n     \n      ## builtin/hook.c ##\n     @@\n    + #include \"strvec.h\"\n      \n      static const char * const builtin_hook_usage[] = {\n    - \tN_(\"git hook list <hookname>\"),\n    --\tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] <hookname>\"),\n    -+\tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...]\"\n    -+\t   \"[--to-stdin=<path>] <hookname>\"),\n    +-\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n    ++\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n      \tNULL\n      };\n      \n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n    - \t\t\t   N_(\"environment variables for hook to use\")),\n    - \t\tOPT_STRVEC('a', \"arg\", &opt.args, N_(\"args\"),\n    - \t\t\t   N_(\"argument to pass to hook\")),\n    + \tstruct option run_options[] = {\n    + \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n    + \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n     +\t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n     +\t\t\t   N_(\"file to read into hooks' stdin\")),\n      \t\tOPT_END(),\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \n     \n      ## hook.c ##\n    -@@ hook.c: void run_hooks_opt_init(struct run_hooks_opt *o)\n    - {\n    - \tstrvec_init(&o->env);\n    - \tstrvec_init(&o->args);\n    -+\to->path_to_stdin = NULL;\n    - \to->run_hookdir = configured_hookdir_opt();\n    - }\n    - \n    -@@ hook.c: static void prepare_hook_cp(const char *hookname, struct hook *hook,\n    - \tif (!hook)\n    - \t\treturn;\n    +@@ hook.c: static int pick_next_hook(struct child_process *cp,\n    + \tif (!run_me)\n    + \t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n      \n     -\tcp->no_stdin = 1;\n     +\t/* reopen the file for stdin; run_command closes it. */\n    -+\tif (options->path_to_stdin)\n    -+\t\tcp->in = xopen(options->path_to_stdin, O_RDONLY);\n    -+\telse\n    ++\tif (hook_cb->options->path_to_stdin) {\n    ++\t\tcp->no_stdin = 0;\n    ++\t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n    ++\t} else {\n     +\t\tcp->no_stdin = 1;\n    -+\n    - \tcp->env = options->env.v;\n    ++\t}\n    + \tcp->env = hook_cb->options->env.v;\n      \tcp->stdout_to_stderr = 1;\n    - \tcp->trace2_hook_name = hookname;\n    + \tcp->trace2_hook_name = hook_cb->hook_name;\n     \n      ## hook.h ##\n     @@ hook.h: struct run_hooks_opt\n    - \t * to be overridden if the user can override it at the command line.\n    - \t */\n    - \tenum hookdir_opt run_hookdir;\n    -+\n    + \t/* Path to initial working directory for subprocess */\n    + \tconst char *dir;\n    + \n     +\t/* Path to file which should be piped to stdin for each hook */\n     +\tconst char *path_to_stdin;\n      };\n      \n    - void run_hooks_opt_init(struct run_hooks_opt *o);\n    -@@ hook.h: int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);\n    - \n    - /*\n    -  * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n    -- * passed 'env' and 'args'.\n    -+ * passed 'env' and 'args'. The file at 'stdin_path' will be closed and reopened\n    -+ * for each hook that runs.\n    -  */\n    - int run_hooks(const char *hookname, struct run_hooks_opt *options);\n    - \n    + #define RUN_HOOKS_OPT_INIT { \\\n     \n    - ## t/t1360-config-based-hooks.sh ##\n    -@@ t/t1360-config-based-hooks.sh: test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n    - \ttest_cmp expected actual\n    + ## t/t1800-hook.sh ##\n    +@@ t/t1800-hook.sh: test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n    + \tEOF\n      '\n      \n    -+test_expect_success 'stdin to multiple hooks' '\n    -+\tgit config --add hook.test.command \"xargs -P1 -I% echo a%\" &&\n    -+\tgit config --add hook.test.command \"xargs -P1 -I% echo b%\" &&\n    -+\ttest_when_finished \"test_unconfig hook.test.command\" &&\n    -+\n    -+\tcat >input <<-EOF &&\n    -+\t1\n    -+\t2\n    -+\t3\n    ++test_expect_success 'stdin to hooks' '\n    ++\twrite_script .git/hooks/test-hook <<-\\EOF &&\n    ++\techo BEGIN stdin\n    ++\tcat\n    ++\techo END stdin\n     +\tEOF\n     +\n    -+\tcat >expected <<-EOF &&\n    -+\ta1\n    -+\ta2\n    -+\ta3\n    -+\tb1\n    -+\tb2\n    -+\tb3\n    ++\tcat >expect <<-EOF &&\n    ++\tBEGIN stdin\n    ++\thello\n    ++\tEND stdin\n     +\tEOF\n     +\n    -+\tgit hook run --to-stdin=input test 2>actual &&\n    -+\ttest_cmp expected actual\n    ++\techo hello >input &&\n    ++\tgit hook run --to-stdin=input test-hook 2>actual &&\n    ++\ttest_cmp expect actual\n     +'\n     +\n      test_done\n11:  e6c789629d6 <  -:  ----------- hook: allow parallel hook execution\n12:  26e2d14bc1a <  -:  ----------- hook: allow specifying working directory for hooks\n -:  ----------- > 19:  986bfd89a54 am: convert 'post-rewrite' hook to hook.h\n13:  e721d45efc6 ! 20:  756f52af22d run-command: add stdin callback for parallelization\n    @@ Commit message\n         match the rest of the API reduces mental load on the user.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/fetch.c ##\n     @@ builtin/fetch.c: static int fetch_multiple(struct string_list *list, int max_children)\n    @@ builtin/submodule--helper.c: static int update_submodules(struct submodule_updat\n      \n     \n      ## hook.c ##\n    -@@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n    +@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n      \trun_processes_parallel_tr2(options->jobs,\n      \t\t\t\t   pick_next_hook,\n      \t\t\t\t   notify_start_failure,\n14:  5d74ea3ed3f ! 21:  3748f128763 hook: provide stdin by string_list or callback\n    @@ Commit message\n         with instead.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## hook.c ##\n    -@@\n    - \n    - void free_hook(struct hook *ptr)\n    - {\n    --\tif (ptr)\n    -+\tif (ptr) {\n    - \t\tstrbuf_release(&ptr->command);\n    -+\t\tfree(ptr->feed_pipe_cb_data);\n    -+\t}\n    - \tfree(ptr);\n    - }\n    - \n    -@@ hook.c: static void append_or_move_hook(struct list_head *head, const char *command)\n    - \t\tstrbuf_init(&to_add->command, 0);\n    - \t\tstrbuf_addstr(&to_add->command, command);\n    - \t\tto_add->from_hookdir = 0;\n    -+\t\tto_add->feed_pipe_cb_data = NULL;\n    - \t}\n    - \n    - \t/* re-set the scope so we show where an override was specified */\n    -@@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n    - \to->run_hookdir = configured_hookdir_opt();\n    - \to->jobs = 1;\n    - \to->dir = NULL;\n    -+\to->feed_pipe = NULL;\n    -+\to->feed_pipe_ctx = NULL;\n    - }\n    - \n    - void run_hooks_opt_init_async(struct run_hooks_opt *o)\n     @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)\n      \tstrvec_clear(&o->args);\n      }\n    @@ hook.c: void run_hooks_opt_clear(struct run_hooks_opt *o)\n     +{\n     +\tint *item_idx;\n     +\tstruct hook *ctx = pp_task_cb;\n    -+\tstruct string_list *to_pipe = ((struct hook_cb_data*)pp_cb)->options->feed_pipe_ctx;\n    ++\tstruct hook_cb_data *hook_cb = pp_cb;\n    ++\tstruct string_list *to_pipe = hook_cb->options->feed_pipe_ctx;\n     +\n     +\t/* Bootstrap the state manager if necessary. */\n     +\tif (!ctx->feed_pipe_cb_data) {\n    @@ hook.c: static int pick_next_hook(struct child_process *cp,\n      \t} else {\n      \t\tcp->no_stdin = 1;\n      \t}\n    -@@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n    - \tif (!options)\n    - \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n    - \n    -+\tif (options->path_to_stdin && options->feed_pipe)\n    -+\t\tBUG(\"choose only one method to populate stdin\");\n    -+\n    - \tto_run = hook_list(hookname);\n    +@@ hook.c: static int notify_hook_finished(int result,\n    + \treturn 1;\n    + }\n      \n    - \tlist_for_each_safe(pos, tmp, to_run) {\n    -@@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n    +-\n    + int run_found_hooks(const char *hook_name, const char *hook_path,\n    + \t\t    struct run_hooks_opt *options)\n    + {\n    +@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n      \trun_processes_parallel_tr2(options->jobs,\n      \t\t\t\t   pick_next_hook,\n      \t\t\t\t   notify_start_failure,\n    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n      \t\t\t\t   notify_hook_finished,\n      \t\t\t\t   &cb_data,\n      \t\t\t\t   \"hook\",\n    +@@ hook.c: int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n    + \tif (!options)\n    + \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n    + \n    ++\tif (options->path_to_stdin && options->feed_pipe)\n    ++\t\tBUG(\"choose only one method to populate stdin\");\n    ++\n    + \thook_path = find_hook(hook_name);\n    + \n    + \t/* Care about nonexistence? Use run_found_hooks() */\n     \n      ## hook.h ##\n     @@\n    - #include \"list.h\"\n    - #include \"strbuf.h\"\n    - #include \"strvec.h\"\n    -+#include \"run-command.h\"\n    - \n      struct hook {\n    - \tstruct list_head list;\n    -@@ hook.h: struct hook {\n    - \t/* The literal command to run. */\n    - \tstruct strbuf command;\n    - \tunsigned from_hookdir : 1;\n    + \t/* The path to the hook */\n    + \tconst char *hook_path;\n     +\n     +\t/*\n     +\t * Use this to keep state for your feed_pipe_fn if you are using\n    @@ hook.h: struct hook {\n     +\tvoid *feed_pipe_cb_data;\n      };\n      \n    - /*\n    + struct run_hooks_opt\n     @@ hook.h: struct run_hooks_opt\n      \n      \t/* Path to file which should be piped to stdin for each hook */\n      \tconst char *path_to_stdin;\n    ++\n     +\t/*\n     +\t * Callback and state pointer to ask for more content to pipe to stdin.\n     +\t * Will be called repeatedly, for each hook. See\n    @@ hook.h: struct run_hooks_opt\n     +\t */\n     +\tfeed_pipe_fn feed_pipe;\n     +\tvoid *feed_pipe_ctx;\n    - \n    - \t/* Number of threads to parallelize across */\n    - \tint jobs;\n    - \n    - \t/* Path to initial working directory for subprocess */\n    - \tconst char *dir;\n    -+\n      };\n      \n    + #define RUN_HOOKS_OPT_INIT { \\\n    +@@ hook.h: struct run_hooks_opt\n    + \t.args = STRVEC_INIT, \\\n    + }\n    + \n     +/*\n     + * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n     + * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n26:  6d804ca20fd ! 22:  0cf0b1fea93 hook: convert 'post-rewrite' hook to config\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    hook: convert 'post-rewrite' hook to config\n    +    hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n     \n         By using 'hook.h' for 'post-rewrite', we simplify hook invocations by\n    -    not needing to put together our own 'struct child_process' and we also\n    -    learn to run hooks specified in the config as well as the hook dir.\n    +    not needing to put together our own 'struct child_process'.\n     \n    -    The signal handling that's being removed by this commit now takes place\n    -    in run-command.h:run_processes_parallel(), so it is OK to remove them\n    -    here.\n    +    The signal handling that's being removed by this commit now takes\n    +    place in run-command.h:run_processes_parallel(), so it is OK to remove\n    +    them here.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: The hook always runs after the automatic note copying (see\n    - \"notes.rewrite.<command>\" in linkgit:git-config[1]) has happened, and\n    - thus has access to these notes.\n    - \n    -+Hooks run during 'post-rewrite' will be run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - The following command-specific comments apply:\n    - \n    - rebase::\n    -\n    - ## builtin/am.c ##\n    -@@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)\n    -  */\n    - static int run_post_rewrite_hook(const struct am_state *state)\n    - {\n    --\tstruct child_process cp = CHILD_PROCESS_INIT;\n    --\tconst char *hook = find_hook(\"post-rewrite\");\n    -+\tstruct run_hooks_opt opt;\n    - \tint ret;\n    - \n    --\tif (!hook)\n    --\t\treturn 0;\n    -+\trun_hooks_opt_init_async(&opt);\n    - \n    --\tstrvec_push(&cp.args, hook);\n    --\tstrvec_push(&cp.args, \"rebase\");\n    -+\tstrvec_push(&opt.args, \"rebase\");\n    -+\topt.path_to_stdin = am_path(state, \"rewritten\");\n    - \n    --\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n    --\tcp.stdout_to_stderr = 1;\n    --\tcp.trace2_hook_name = \"post-rewrite\";\n    -+\tret = run_hooks(\"post-rewrite\", &opt);\n    - \n    --\tret = run_command(&cp);\n    --\n    --\tclose(cp.in);\n    -+\trun_hooks_opt_clear(&opt);\n    - \treturn ret;\n    - }\n    - \n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## sequencer.c ##\n     @@\n    + #include \"commit-reach.h\"\n      #include \"rebase-interactive.h\"\n      #include \"reset.h\"\n    - #include \"hook.h\"\n     +#include \"string-list.h\"\n      \n      #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n    @@ sequencer.c: int update_head_with_reflog(const struct commit *old_head,\n      {\n     -\tstruct child_process proc = CHILD_PROCESS_INIT;\n     -\tconst char *argv[3];\n    -+\tstruct run_hooks_opt opt;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     +\tstruct strbuf tmp = STRBUF_INIT;\n     +\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n      \tint code;\n    @@ sequencer.c: int update_head_with_reflog(const struct commit *old_head,\n     -\targv[0] = find_hook(\"post-rewrite\");\n     -\tif (!argv[0])\n     -\t\treturn 0;\n    -+\trun_hooks_opt_init_async(&opt);\n    ++\tstrvec_push(&opt.args, \"amend\");\n      \n     -\targv[1] = \"amend\";\n     -\targv[2] = NULL;\n    @@ sequencer.c: int update_head_with_reflog(const struct commit *old_head,\n     -\tstrbuf_release(&sb);\n     -\tsigchain_pop(SIGPIPE);\n     -\treturn finish_command(&proc);\n    -+\tstrvec_push(&opt.args, \"amend\");\n    -+\n     +\tstrbuf_addf(&tmp,\n     +\t\t    \"%s %s\",\n     +\t\t    oid_to_hex(oldoid),\n    @@ sequencer.c: static int pick_commits(struct repository *r,\n     -\t\t\tstrvec_push(&child.args, \"copy\");\n     -\t\t\tstrvec_push(&child.args, \"--for-rewrite=rebase\");\n     +\t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n    -+\t\t\tstruct run_hooks_opt hook_opt;\n    -+\n    -+\t\t\trun_hooks_opt_init_async(&hook_opt);\n    ++\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n     +\n     +\t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n     +\t\t\tnotes_cp.git_cmd = 1;\n27:  e761410a1e8 ! 23:  c59443a3b05 transport: convert pre-push hook to use config\n    @@ Commit message\n         the config as well as in the hookdir.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: If this hook exits with a non-zero status, `git push` will abort without\n    - pushing anything.  Information about why the push is rejected may be sent\n    - to the user by writing to standard error.\n    - \n    -+Hooks executed during 'pre-push' will run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - [[pre-receive]]\n    - pre-receive\n    - ~~~~~~~~~~~\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## transport.c ##\n    -@@\n    - #include \"protocol.h\"\n    - #include \"object-store.h\"\n    - #include \"color.h\"\n    -+#include \"hook.h\"\n    - \n    - static int transport_use_color = -1;\n    - static char transport_colors[][COLOR_MAXLEN] = {\n     @@ transport.c: static void die_with_unpushed_submodules(struct string_list *needs_pushing)\n      static int run_pre_push_hook(struct transport *transport,\n      \t\t\t     struct ref *remote_refs)\n      {\n     -\tint ret = 0, x;\n     +\tint ret = 0;\n    -+\tstruct run_hooks_opt opt;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     +\tstruct strbuf tmp = STRBUF_INIT;\n      \tstruct ref *r;\n     -\tstruct child_process proc = CHILD_PROCESS_INIT;\n    @@ transport.c: static void die_with_unpushed_submodules(struct string_list *needs_\n     -\n     -\tsigchain_push(SIGPIPE, SIG_IGN);\n     +\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n    -+\trun_hooks_opt_init_async(&opt);\n      \n     -\tstrbuf_init(&buf, 256);\n     +\tstrvec_push(&opt.args, transport->remote->name);\n28:  e43f9f93d7b ! 24:  f7c8c97cb81 reference-transaction: look for hooks in config\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    reference-transaction: look for hooks in config\n    +    reference-transaction: use hook.h to run hooks\n     \n    -    By using the hook.h library, reference-transaction hooks can be\n    -    specified in the config instead.\n    -\n    -    The expected output of the test is not fully updated to reflect the\n    -    absolute path of the hook called because the 'update' hook has not yet\n    -    been converted to use hook.h.\n    +    By using the hook.h library, we get closer to removing the hook code\n    +    in run-command.c.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: The exit status of the hook is ignored for any state except for the\n    - cause the transaction to be aborted. The hook will not be called with\n    - \"aborted\" state in that case.\n    - \n    -+Hooks run during 'reference-transaction' will be run in parallel, unless\n    -+hook.jobs is configured to 1.\n    -+\n    - push-to-checkout\n    - ~~~~~~~~~~~~~~~~\n    - \n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## refs.c ##\n    -@@\n    - #include \"strvec.h\"\n    - #include \"repository.h\"\n    - #include \"sigchain.h\"\n    -+#include \"hook.h\"\n    - \n    - /*\n    -  * List of all available backends\n     @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n      static int run_transaction_hook(struct ref_transaction *transaction,\n      \t\t\t\tconst char *state)\n    @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n     -\tstruct child_process proc = CHILD_PROCESS_INIT;\n      \tstruct strbuf buf = STRBUF_INIT;\n     -\tconst char *hook;\n    -+\tstruct run_hooks_opt opt;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     +\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n      \tint ret = 0, i;\n     +\tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n      \n     -\thook = find_hook(\"reference-transaction\");\n     -\tif (!hook)\n    --\t\treturn ret;\n    --\n    ++\tif (!hook_exists(\"reference-transaction\"))\n    + \t\treturn ret;\n    + \n     -\tstrvec_pushl(&proc.args, hook, state, NULL);\n     -\tproc.in = -1;\n     -\tproc.stdout_to_stderr = 1;\n     -\tproc.trace2_hook_name = \"reference-transaction\";\n    -+\trun_hooks_opt_init_async(&opt);\n    - \n    +-\n     -\tret = start_command(&proc);\n     -\tif (ret)\n    -+\tif (!hook_exists(\"reference-transaction\", HOOKDIR_USE_CONFIG))\n    - \t\treturn ret;\n    - \n    +-\t\treturn ret;\n    +-\n     -\tsigchain_push(SIGPIPE, SIG_IGN);\n     +\tstrvec_push(&opt.args, state);\n      \n    @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n      \treturn ret;\n      }\n      \n    -\n    - ## t/t1416-ref-transaction-hooks.sh ##\n    -@@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'interleaving hook calls succeed' '\n    - \n    - \tcat >expect <<-EOF &&\n    - \t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n    --\t\thooks/reference-transaction prepared\n    --\t\thooks/reference-transaction committed\n    -+\t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n    -+\t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n    - \t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n    --\t\thooks/reference-transaction prepared\n    --\t\thooks/reference-transaction committed\n    -+\t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n    -+\t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n    - \tEOF\n    - \n    - \tgit push ./target-repo.git PRE POST &&\n    -\n    - ## transport.c ##\n    -@@ transport.c: static int run_pre_push_hook(struct transport *transport,\n    - \tstruct strbuf tmp = STRBUF_INIT;\n    - \tstruct ref *r;\n    - \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n    -+\n    - \trun_hooks_opt_init_async(&opt);\n    - \n    - \tstrvec_push(&opt.args, transport->remote->name);\n15:  84fe7a21976 ! 25:  f240a51ec4e run-command: allow capturing of collated output\n    @@ Commit message\n         instead hand it to the caller.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/fetch.c ##\n     @@ builtin/fetch.c: static int fetch_multiple(struct string_list *list, int max_children)\n    @@ builtin/submodule--helper.c: static int update_submodules(struct submodule_updat\n      \n     \n      ## hook.c ##\n    -@@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n    +@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n      \t\t\t\t   pick_next_hook,\n      \t\t\t\t   notify_start_failure,\n      \t\t\t\t   options->feed_pipe,\n16:  f23bcba44a0 ! 26:  7f10efb7858 hooks: allow callers to capture output\n    @@ Commit message\n         sideband instead of printing directly to stderr. Expose that capability.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## hook.c ##\n    -@@ hook.c: void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n    - \to->dir = NULL;\n    - \to->feed_pipe = NULL;\n    - \to->feed_pipe_ctx = NULL;\n    -+\to->consume_sideband = NULL;\n    - }\n    - \n    - void run_hooks_opt_init_async(struct run_hooks_opt *o)\n    -@@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n    +@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n      \t\t\t\t   pick_next_hook,\n      \t\t\t\t   notify_start_failure,\n      \t\t\t\t   options->feed_pipe,\n    @@ hook.c: int run_hooks(const char *hookname, struct run_hooks_opt *options)\n     \n      ## hook.h ##\n     @@ hook.h: struct run_hooks_opt\n    + \t */\n      \tfeed_pipe_fn feed_pipe;\n      \tvoid *feed_pipe_ctx;\n    - \n    ++\n     +\t/*\n     +\t * Populate this to capture output and prevent it from being printed to\n     +\t * stderr. This will be passed directly through to\n    @@ hook.h: struct run_hooks_opt\n     +\t * for an example.\n     +\t */\n     +\tconsume_sideband_fn consume_sideband;\n    -+\n    - \t/* Number of threads to parallelize across */\n    - \tint jobs;\n    + };\n      \n    + #define RUN_HOOKS_OPT_INIT { \\\n29:  7931831dc6e ! 27:  c39c608e5cc receive-pack: convert 'update' hook to hook.h\n    @@ Metadata\n      ## Commit message ##\n         receive-pack: convert 'update' hook to hook.h\n     \n    -    By using hook.h to invoke the 'update' hook, now hooks can be specified\n    -    in the config in addition to the hookdir.\n    +    By using hook.h to invoke the 'update' hook we closer to removing the\n    +    hooks code in run-command.c.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: The default 'update' hook, when enabled--and with\n    - `hooks.allowunannotated` config option unset or set to false--prevents\n    - unannotated tags to be pushed.\n    - \n    -+Hooks executed during 'update' are run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - [[proc-receive]]\n    - proc-receive\n    - ~~~~~~~~~~~~\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/receive-pack.c ##\n     @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n      \treturn status;\n      }\n      \n    +-static int run_update_hook(struct command *cmd)\n     +static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n    -+{\n    + {\n    +-\tconst char *argv[5];\n    +-\tstruct child_process proc = CHILD_PROCESS_INIT;\n    +-\tint code;\n     +\tint keepalive_active = 0;\n    -+\n    + \n    +-\targv[0] = find_hook(\"update\");\n    +-\tif (!argv[0])\n    +-\t\treturn 0;\n     +\tif (keepalive_in_sec <= 0)\n     +\t\tuse_keepalive = KEEPALIVE_NEVER;\n     +\tif (use_keepalive == KEEPALIVE_ALWAYS)\n     +\t\tkeepalive_active = 1;\n    -+\n    + \n    +-\targv[1] = cmd->ref_name;\n    +-\targv[2] = oid_to_hex(&cmd->old_oid);\n    +-\targv[3] = oid_to_hex(&cmd->new_oid);\n    +-\targv[4] = NULL;\n     +\t/* send a keepalive if there is no data to write */\n     +\tif (keepalive_active && !output->len) {\n     +\t\tstatic const char buf[] = \"0005\\1\";\n     +\t\twrite_or_die(1, buf, sizeof(buf) - 1);\n     +\t\treturn;\n     +\t}\n    -+\n    + \n    +-\tproc.no_stdin = 1;\n    +-\tproc.stdout_to_stderr = 1;\n    +-\tproc.err = use_sideband ? -1 : 0;\n    +-\tproc.argv = argv;\n    +-\tproc.trace2_hook_name = \"update\";\n     +\tif (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {\n     +\t\tconst char *first_null = memchr(output->buf, '\\0', output->len);\n     +\t\tif (first_null) {\n    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n     +\tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n     +}\n     +\n    - static int run_update_hook(struct command *cmd)\n    - {\n    --\tconst char *argv[5];\n    --\tstruct child_process proc = CHILD_PROCESS_INIT;\n    -+\tstruct run_hooks_opt opt;\n    - \tint code;\n    - \n    --\targv[0] = find_hook(\"update\");\n    --\tif (!argv[0])\n    --\t\treturn 0;\n    -+\trun_hooks_opt_init_async(&opt);\n    - \n    --\targv[1] = cmd->ref_name;\n    --\targv[2] = oid_to_hex(&cmd->old_oid);\n    --\targv[3] = oid_to_hex(&cmd->new_oid);\n    --\targv[4] = NULL;\n    ++static int run_update_hook(struct command *cmd)\n    ++{\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    ++\tint code;\n    ++\n     +\tstrvec_pushl(&opt.args,\n     +\t\t     cmd->ref_name,\n     +\t\t     oid_to_hex(&cmd->old_oid),\n     +\t\t     oid_to_hex(&cmd->new_oid),\n     +\t\t     NULL);\n      \n    --\tproc.no_stdin = 1;\n    --\tproc.stdout_to_stderr = 1;\n    --\tproc.err = use_sideband ? -1 : 0;\n    --\tproc.argv = argv;\n    --\tproc.trace2_hook_name = \"update\";\n    --\n     -\tcode = start_command(&proc);\n     -\tif (code)\n     -\t\treturn code;\n    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n      }\n      \n      static struct command *find_command_by_refname(struct command *list,\n    -\n    - ## t/t1416-ref-transaction-hooks.sh ##\n    -@@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'interleaving hook calls succeed' '\n    - \tEOF\n    - \n    - \tcat >expect <<-EOF &&\n    --\t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n    -+\t\t$(pwd)/target-repo.git/hooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n    - \t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n    - \t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n    --\t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n    -+\t\t$(pwd)/target-repo.git/hooks/update refs/tags/POST $ZERO_OID $POST_OID\n    - \t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n    - \t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n    - \tEOF\n31:  efbe55fcb13 ! 28:  3519068a634 post-update: use hook.h library\n    @@ Commit message\n         be specified in the config as well as the hookdir.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: Both standard output and standard error output are forwarded to\n    - `git send-pack` on the other end, so you can simply `echo` messages\n    - for the user.\n    - \n    -+Hooks run during 'post-update' will be run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - reference-transaction\n    - ~~~~~~~~~~~~~~~~~~~~~\n    - \n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/receive-pack.c ##\n     @@ builtin/receive-pack.c: static const char *update(struct command *cmd, struct shallow_info *si)\n    @@ builtin/receive-pack.c: static const char *update(struct command *cmd, struct sh\n      \tstruct command *cmd;\n     -\tstruct child_process proc = CHILD_PROCESS_INIT;\n     -\tconst char *hook;\n    -+\tstruct run_hooks_opt opt;\n    - \n    +-\n     -\thook = find_hook(\"post-update\");\n     -\tif (!hook)\n     -\t\treturn;\n    -+\trun_hooks_opt_init_async(&opt);\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n      \n      \tfor (cmd = commands; cmd; cmd = cmd->next) {\n      \t\tif (cmd->error_string || cmd->did_not_exist)\n32:  26e08bd5257 ! 29:  3466f17af08 receive-pack: convert receive hooks to hook.h\n    @@ Metadata\n      ## Commit message ##\n         receive-pack: convert receive hooks to hook.h\n     \n    -    By using the hook.h library to run receive hooks, they can be specified\n    -    in the config as well as in the hookdir.\n    +    By using the hook.h library to run receive hooks we get closer to\n    +    deleting the hook functions in run-command.c\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    -\n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n    - See the section on \"Quarantine Environment\" in\n    - linkgit:git-receive-pack[1] for some caveats.\n    - \n    -+Hooks executed during 'pre-receive' will not be parallelized.\n    -+\n    - [[update]]\n    - update\n    - ~~~~~~\n    -@@ Documentation/githooks.txt: environment variables will not be set. If the client selects\n    - to use push options, but doesn't transmit any, the count variable\n    - will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n    - \n    -+Hooks executed during 'post-receive' are run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - [[post-update]]\n    - post-update\n    - ~~~~~~~~~~~\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/receive-pack.c ##\n     @@ builtin/receive-pack.c: static int check_cert_push_options(const struct string_list *push_options)\n    @@ builtin/receive-pack.c: static void hook_output_to_sideband(struct strbuf *outpu\n     +\t\t\t    int skip_broken,\n     +\t\t\t    const struct string_list *push_options)\n     +{\n    -+\tstruct run_hooks_opt opt;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     +\tstruct receive_hook_feed_context ctx;\n     +\tint rc;\n     +\tstruct command *iter = commands;\n     +\n    -+\trun_hooks_opt_init_async(&opt);\n    -+\n     +\t/* if there are no valid commands, don't invoke the hook at all. */\n     +\twhile (iter && skip_broken && (iter->error_string || iter->did_not_exist))\n     +\t\titer = iter->next;\n    @@ builtin/receive-pack.c: static void hook_output_to_sideband(struct strbuf *outpu\n     +\n      static int run_update_hook(struct command *cmd)\n      {\n    - \tstruct run_hooks_opt opt;\n    + \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n17:  edb22d675b7 ! 30:  d93bdc0c294 commit: use config-based hooks\n    @@\n      ## Metadata ##\n    -Author: Emily Shaffer <emilyshaffer@google.com>\n    +Author: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## Commit message ##\n    -    commit: use config-based hooks\n    +    hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n     \n    -    As part of the adoption of config-based hooks, teach run_commit_hook()\n    -    to call hook.h instead of run-command.h. This covers 'pre-commit',\n    -    'commit-msg', and 'prepare-commit-msg'. Additionally, ask the hook\n    -    library - not run-command - whether any hooks will be run, as it's\n    -    possible hooks may exist in the config but not the hookdir.\n    +    Fix a race in code added in 680ee550d72 (commit: skip discarding the\n    +    index if there is no pre-commit hook, 2017-08-14) by changing the\n    +    hook.c API to optionally indicate whether or not the requested hook\n    +    ran or not. This was suggested in the discussion around\n    +    680ee550d72[1].\n     \n    -    Because all but 'post-commit' hooks are expected to make some state\n    -    change, force all but 'post-commit' hook to run in series. 'post-commit'\n    -    \"is meant primarily for notification, and cannot affect the outcome of\n    -    `git commit`,\" so it is fine to run in parallel.\n    +    Let's also change this for the pre-merge-commit hook, see\n    +    6098817fd7f (git-merge: honor pre-merge-commit hook, 2019-08-07) for\n    +    the introduction of the previous behavior.\n     \n    -    Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    -    Signed-off-by: Junio C Hamano <gitster@pobox.com>\n    +    Let's also change this for the push-to-checkout hook. Now instead of\n    +    checking if the hook exists and either doing a push to checkout or a\n    +    push to deploy we'll always attempt a push to checkout. If the hook\n    +    doesn't exist we'll fall back on push to deploy. The same behavior as\n    +    before, without the TOCTOU race. See 0855331941b (receive-pack:\n    +    support push-to-checkout hook, 2014-12-01) for the introduction of the\n    +    previous behavior.\n     \n    - ## Documentation/githooks.txt ##\n    -@@ Documentation/githooks.txt: The default 'pre-commit' hook, when enabled--and with the\n    - `hooks.allownonascii` config option unset or set to false--prevents\n    - the use of non-ASCII filenames.\n    - \n    -+Hooks executed during 'pre-commit' will not be parallelized.\n    -+\n    - pre-merge-commit\n    - ~~~~~~~~~~~~~~~~\n    - \n    -@@ Documentation/githooks.txt: need to be resolved and the result committed separately (see\n    - linkgit:git-merge[1]). At that point, this hook will not be executed,\n    - but the 'pre-commit' hook will, if it is enabled.\n    - \n    -+Hooks executed during 'pre-merge-commit' will not be parallelized.\n    -+\n    - prepare-commit-msg\n    - ~~~~~~~~~~~~~~~~~~\n    - \n    -@@ Documentation/githooks.txt: be used as replacement for pre-commit hook.\n    - The sample `prepare-commit-msg` hook that comes with Git removes the\n    - help message found in the commented portion of the commit template.\n    - \n    -+Hooks executed during 'prepare-commit-msg' will not be parallelized, because\n    -+hooks are expected to edit the file containing the commit log message.\n    -+\n    - commit-msg\n    - ~~~~~~~~~~\n    - \n    -@@ Documentation/githooks.txt: file.\n    - The default 'commit-msg' hook, when enabled, detects duplicate\n    - `Signed-off-by` trailers, and aborts the commit if one is found.\n    - \n    -+Hooks executed during 'commit-msg' will not be parallelized, because hooks are\n    -+expected to edit the file containing the proposed commit log message.\n    -+\n    - post-commit\n    - ~~~~~~~~~~~\n    - \n    -@@ Documentation/githooks.txt: invoked after a commit is made.\n    - This hook is meant primarily for notification, and cannot affect\n    - the outcome of `git commit`.\n    - \n    -+Hooks executed during 'post-commit' will run in parallel, unless hook.jobs is\n    -+configured to 1.\n    -+\n    - pre-rebase\n    - ~~~~~~~~~~\n    - \n    +    This leaves uses of hook_exists() in two places that matter. The\n    +    \"reference-transaction\" check in refs.c, see 67541597670 (refs:\n    +    implement reference transaction hook, 2020-06-19), and the\n    +    prepare-commit-msg hook, see 66618a50f9c (sequencer: run\n    +    'prepare-commit-msg' hook, 2018-01-24).\n    +\n    +    In both of those cases we're saving ourselves CPU time by not\n    +    preparing data for the hook that we'll then do nothing with if we\n    +    don't have the hook, so using this \"invoked_hook\" pattern doesn't make\n    +    sense there purely for optimization purposes.\n    +\n    +    More importantly, in those cases the worst we'll do is miss that we\n    +    \"should\" run the hook because a new hook appeared, whereas in the\n    +    pre-commit and pre-merge-commit cases we'll skip an important\n    +    discard_cache() on the bases of our faulty guess.\n    +\n    +    I do think none of these races really matter in practice. It would be\n    +    some one-off issue as a hook was added or removed. I did think it was\n    +    stupid that we didn't pass a \"did this run?\" flag instead of doing\n    +    this guessing at a distance though, so now we're not guessing anymore.\n    +\n    +    1. https://lore.kernel.org/git/20170810191613.kpmhzg4seyxy3cpq@sigill.intra.peff.net/\n    +\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/commit.c ##\n    -@@\n    - #include \"help.h\"\n    - #include \"commit-reach.h\"\n    - #include \"commit-graph.h\"\n    -+#include \"hook.h\"\n    - \n    - static const char * const builtin_commit_usage[] = {\n    - \tN_(\"git commit [<options>] [--] <pathspec>...\"),\n     @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const char *prefix,\n    + \tint clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);\n    + \tint old_display_comment_prefix;\n    + \tint merge_contains_scissors = 0;\n    ++\tint invoked_hook = 0;\n    + \n      \t/* This checks and barfs if author is badly specified */\n      \tdetermine_author_info(author_ident);\n      \n     -\tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n    -+\tif (!no_verify && run_commit_hook(use_editor, 0, index_file, \"pre-commit\", NULL))\n    ++\tif (!no_verify && run_commit_hook(use_editor, index_file, &invoked_hook,\n    ++\t\t\t\t\t  \"pre-commit\", NULL))\n      \t\treturn 0;\n      \n      \tif (squash_message) {\n    @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const cha\n      \t\treturn 0;\n      \t}\n      \n    --\tif (!no_verify && find_hook(\"pre-commit\")) {\n    -+\tif (!no_verify && hook_exists(\"pre-commit\", HOOKDIR_USE_CONFIG)) {\n    +-\tif (!no_verify && hook_exists(\"pre-commit\")) {\n    ++\tif (!no_verify && invoked_hook) {\n      \t\t/*\n    - \t\t * Re-read the index as pre-commit hook could have updated it,\n    - \t\t * and write it out as a tree.  We must do this before we invoke\n    +-\t\t * Re-read the index as pre-commit hook could have updated it,\n    +-\t\t * and write it out as a tree.  We must do this before we invoke\n    ++\t\t * Re-read the index as the pre-commit-commit hook was invoked\n    ++\t\t * and could have updated it. We must do this before we invoke\n    + \t\t * the editor and after we invoke run_status above.\n    + \t\t */\n    + \t\tdiscard_cache();\n     @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const char *prefix,\n      \t\treturn 0;\n      \t}\n      \n     -\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n    -+\tif (run_commit_hook(use_editor, 0, index_file, \"prepare-commit-msg\",\n    ++\tif (run_commit_hook(use_editor, index_file, NULL, \"prepare-commit-msg\",\n      \t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n      \t\treturn 0;\n      \n    @@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const cha\n      \n      \tif (!no_verify &&\n     -\t    run_commit_hook(use_editor, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n    -+\t    run_commit_hook(use_editor, 0, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n    ++\t    run_commit_hook(use_editor, index_file, NULL, \"commit-msg\",\n    ++\t\t\t    git_path_commit_editmsg(), NULL)) {\n      \t\treturn 0;\n      \t}\n      \n    @@ builtin/commit.c: int cmd_commit(int argc, const char **argv, const char *prefix\n      \trepo_rerere(the_repository, 0);\n      \trun_auto_maintenance(quiet);\n     -\trun_commit_hook(use_editor, get_index_file(), \"post-commit\", NULL);\n    -+\trun_commit_hook(use_editor, 1, get_index_file(), \"post-commit\", NULL);\n    ++\trun_commit_hook(use_editor, get_index_file(), NULL, \"post-commit\",\n    ++\t\t\tNULL);\n      \tif (amend && !no_post_rewrite) {\n      \t\tcommit_post_rewrite(the_repository, current_head, &oid);\n      \t}\n     \n      ## builtin/merge.c ##\n    -@@\n    - #include \"commit-reach.h\"\n    - #include \"wt-status.h\"\n    - #include \"commit-graph.h\"\n    -+#include \"hook.h\"\n    - \n    - #define DEFAULT_TWOHEAD (1<<0)\n    - #define DEFAULT_OCTOPUS (1<<1)\n     @@ builtin/merge.c: static void prepare_to_commit(struct commit_list *remoteheads)\n    + {\n      \tstruct strbuf msg = STRBUF_INIT;\n      \tconst char *index_file = get_index_file();\n    ++\tint invoked_hook = 0;\n      \n     -\tif (!no_verify && run_commit_hook(0 < option_edit, index_file, \"pre-merge-commit\", NULL))\n    -+\tif (!no_verify && run_commit_hook(0 < option_edit, 0, index_file, \"pre-merge-commit\", NULL))\n    ++\tif (!no_verify && run_commit_hook(0 < option_edit, index_file,\n    ++\t\t\t\t\t  &invoked_hook, \"pre-merge-commit\",\n    ++\t\t\t\t\t  NULL))\n      \t\tabort_commit(remoteheads, NULL);\n      \t/*\n    - \t * Re-read the index as pre-merge-commit hook could have updated it,\n    - \t * and write it out as a tree.  We must do this before we invoke\n    +-\t * Re-read the index as pre-merge-commit hook could have updated it,\n    +-\t * and write it out as a tree.  We must do this before we invoke\n    ++\t * Re-read the index as the pre-merge-commit hook was invoked\n    ++\t * and could have updated it. We must do this before we invoke\n      \t * the editor and after we invoke run_status above.\n      \t */\n    --\tif (find_hook(\"pre-merge-commit\"))\n    -+\tif (hook_exists(\"pre-merge-commit\", HOOKDIR_USE_CONFIG))\n    +-\tif (hook_exists(\"pre-merge-commit\"))\n    ++\tif (invoked_hook)\n      \t\tdiscard_cache();\n      \tread_cache_from(index_file);\n      \tstrbuf_addbuf(&msg, &merge_msg);\n    @@ builtin/merge.c: static void prepare_to_commit(struct commit_list *remoteheads)\n      \twrite_merge_heads(remoteheads);\n      \twrite_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);\n     -\tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n    -+\tif (run_commit_hook(0 < option_edit, 0, get_index_file(), \"prepare-commit-msg\",\n    ++\tif (run_commit_hook(0 < option_edit, get_index_file(), NULL,\n    ++\t\t\t    \"prepare-commit-msg\",\n      \t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n      \t\tabort_commit(remoteheads, NULL);\n      \tif (0 < option_edit) {\n     @@ builtin/merge.c: static void prepare_to_commit(struct commit_list *remoteheads)\n    - \t\t\tabort_commit(remoteheads, NULL);\n      \t}\n      \n    --\tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n    -+\tif (!no_verify && run_commit_hook(0 < option_edit, 0, get_index_file(),\n    - \t\t\t\t\t  \"commit-msg\",\n    + \tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n    +-\t\t\t\t\t  \"commit-msg\",\n    ++\t\t\t\t\t  NULL, \"commit-msg\",\n      \t\t\t\t\t  git_path_merge_msg(the_repository), NULL))\n      \t\tabort_commit(remoteheads, NULL);\n    -\n    - ## commit.c ##\n    -@@\n    - #include \"commit-reach.h\"\n    - #include \"run-command.h\"\n    - #include \"shallow.h\"\n    -+#include \"hook.h\"\n      \n    - static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);\n    - \n    -@@ commit.c: size_t ignore_non_trailer(const char *buf, size_t len)\n    - \treturn boc ? len - boc : len - cutoff;\n    - }\n    +\n    + ## builtin/receive-pack.c ##\n    +@@ builtin/receive-pack.c: static const char *push_to_deploy(unsigned char *sha1,\n    + static const char *push_to_checkout_hook = \"push-to-checkout\";\n    + \n    + static const char *push_to_checkout(unsigned char *hash,\n    ++\t\t\t\t    int *invoked_hook,\n    + \t\t\t\t    struct strvec *env,\n    + \t\t\t\t    const char *work_tree)\n    + {\n    + \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    ++\topt.invoked_hook = invoked_hook;\n      \n    --int run_commit_hook(int editor_is_used, const char *index_file,\n    -+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,\n    - \t\t    const char *name, ...)\n    + \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n    + \tstrvec_pushv(&opt.env, env->v);\n    +@@ builtin/receive-pack.c: static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n      {\n    --\tstruct strvec hook_env = STRVEC_INIT;\n    -+\tstruct run_hooks_opt opt;\n    - \tva_list args;\n    -+\tconst char *arg;\n    - \tint ret;\n    + \tconst char *retval, *work_tree, *git_dir = NULL;\n    + \tstruct strvec env = STRVEC_INIT;\n    ++\tint invoked_hook = 0;\n      \n    --\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n    -+\trun_hooks_opt_init_sync(&opt);\n    -+\n    -+\tif (parallelize)\n    -+\t\topt.jobs = configured_hook_jobs();\n    -+\n    -+\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n    + \tif (worktree && worktree->path)\n    + \t\twork_tree = worktree->path;\n    +@@ builtin/receive-pack.c: static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n      \n    - \t/*\n    - \t * Let the hook know that no editor will be launched.\n    - \t */\n    - \tif (!editor_is_used)\n    --\t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n    -+\t\tstrvec_push(&opt.env, \"GIT_EDITOR=:\");\n    + \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n      \n    - \tva_start(args, name);\n    --\tret = run_hook_ve(hook_env.v, name, args);\n    -+\twhile ((arg = va_arg(args, const char *)))\n    -+\t\tstrvec_push(&opt.args, arg);\n    - \tva_end(args);\n    --\tstrvec_clear(&hook_env);\n    -+\n    -+\tret = run_hooks(name, &opt);\n    -+\trun_hooks_opt_clear(&opt);\n    +-\tif (!hook_exists(push_to_checkout_hook))\n    ++\tretval = push_to_checkout(sha1, &invoked_hook, &env, work_tree);\n    ++\tif (!invoked_hook)\n    + \t\tretval = push_to_deploy(sha1, &env, work_tree);\n    +-\telse\n    +-\t\tretval = push_to_checkout(sha1, &env, work_tree);\n      \n    - \treturn ret;\n    + \tstrvec_clear(&env);\n    + \treturn retval;\n    +\n    + ## commit.c ##\n    +@@ commit.c: size_t ignore_non_trailer(const char *buf, size_t len)\n      }\n    + \n    + int run_commit_hook(int editor_is_used, const char *index_file,\n    ++\t\t    int *invoked_hook,\n    + \t\t    const char *name, ...)\n    + {\n    + \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     \n      ## commit.h ##\n     @@ commit.h: int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)\n    @@ commit.h: int compare_commits_by_commit_date(const void *a_, const void *b_, voi\n      \n      LAST_ARG_MUST_BE_NULL\n     -int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);\n    -+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,\n    -+\t\t    const char *name, ...);\n    ++int run_commit_hook(int editor_is_used, const char *index_file,\n    ++\t\t    int *invoked_hook, const char *name, ...);\n      \n      /* Sign a commit or tag buffer, storing the result in a header. */\n      int sign_with_header(struct strbuf *buf, const char *keyid);\n     \n    - ## sequencer.c ##\n    -@@\n    - #include \"commit-reach.h\"\n    - #include \"rebase-interactive.h\"\n    - #include \"reset.h\"\n    -+#include \"hook.h\"\n    + ## hook.c ##\n    +@@ hook.c: static int notify_hook_finished(int result,\n    + \t/* |= rc in cb */\n    + \thook_cb->rc |= result;\n      \n    - #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n    ++\tif (hook_cb->invoked_hook)\n    ++\t\t*hook_cb->invoked_hook = 1;\n    ++\n    + \treturn 1;\n    + }\n      \n    +@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    + \t\t.rc = 0,\n    + \t\t.hook_name = hook_name,\n    + \t\t.options = options,\n    ++\t\t.invoked_hook = options->invoked_hook,\n    + \t};\n    + \tif (options->absolute_path) {\n    + \t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n    +\n    + ## hook.h ##\n    +@@ hook.h: struct run_hooks_opt\n    + \t * for an example.\n    + \t */\n    + \tconsume_sideband_fn consume_sideband;\n    ++\n    ++\t/*\n    ++\t * A pointer which if provided will be set to 1 or 0 depending\n    ++\t * on if a hook was invoked (i.e. existed), regardless of\n    ++\t * whether or not that was successful. Used for avoiding\n    ++\t * TOCTOU races in code that would otherwise call hook_exist()\n    ++\t * after a \"maybe hook run\" to see if a hook was invoked.\n    ++\t */\n    ++\tint *invoked_hook;\n    + };\n    + \n    + #define RUN_HOOKS_OPT_INIT { \\\n    +@@ hook.h: struct hook_cb_data {\n    + \tconst char *hook_name;\n    + \tstruct hook *run_me;\n    + \tstruct run_hooks_opt *options;\n    ++\tint *invoked_hook;\n    + };\n    + \n    + /*\n    +\n    + ## sequencer.c ##\n     @@ sequencer.c: static int run_prepare_commit_msg_hook(struct repository *r,\n      \t} else {\n      \t\targ1 = \"message\";\n      \t}\n     -\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n    -+\tif (run_commit_hook(0, 0, r->index_file, \"prepare-commit-msg\", name,\n    ++\tif (run_commit_hook(0, r->index_file, NULL, \"prepare-commit-msg\", name,\n      \t\t\t    arg1, arg2, NULL))\n      \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n      \n    -@@ sequencer.c: static int try_to_commit(struct repository *r,\n    - \t\t}\n    - \t}\n    - \n    --\tif (find_hook(\"prepare-commit-msg\")) {\n    -+\tif (hook_exists(\"prepare-commit-msg\", HOOKDIR_USE_CONFIG)) {\n    - \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n    - \t\tif (res)\n    - \t\t\tgoto out;\n     @@ sequencer.c: static int try_to_commit(struct repository *r,\n      \t\tgoto out;\n      \t}\n      \n     -\trun_commit_hook(0, r->index_file, \"post-commit\", NULL);\n    -+\trun_commit_hook(0, 1, r->index_file, \"post-commit\", NULL);\n    ++\trun_commit_hook(0, r->index_file, NULL, \"post-commit\", NULL);\n      \tif (flags & AMEND_MSG)\n      \t\tcommit_post_rewrite(r, current_head, oid);\n      \n    -\n    - ## t/t7503-pre-commit-and-pre-merge-commit-hooks.sh ##\n    -@@ t/t7503-pre-commit-and-pre-merge-commit-hooks.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n    - . ./test-lib.sh\n    - \n    - HOOKDIR=\"$(git rev-parse --git-dir)/hooks\"\n    --PRECOMMIT=\"$HOOKDIR/pre-commit\"\n    --PREMERGE=\"$HOOKDIR/pre-merge-commit\"\n    -+PRECOMMIT=\"$(pwd)/$HOOKDIR/pre-commit\"\n    -+PREMERGE=\"$(pwd)/$HOOKDIR/pre-merge-commit\"\n    - \n    - # Prepare sample scripts that write their $0 to actual_hooks\n    - test_expect_success 'sample script setup' '\n    -@@ t/t7503-pre-commit-and-pre-merge-commit-hooks.sh: test_expect_success 'with succeeding hook' '\n    - \ttest_cmp expected_hooks actual_hooks\n    - '\n    - \n    -+# NEEDSWORK: when 'git hook add' and 'git hook remove' have been added, use that\n    -+# instead\n    -+test_expect_success 'with succeeding hook (config-based)' '\n    -+\ttest_when_finished \"git config --unset hook.pre-commit.command success.sample\" &&\n    -+\ttest_when_finished \"rm -f expected_hooks actual_hooks\" &&\n    -+\tgit config hook.pre-commit.command \"$HOOKDIR/success.sample\" &&\n    -+\techo \"$HOOKDIR/success.sample\" >expected_hooks &&\n    -+\techo \"more\" >>file &&\n    -+\tgit add file &&\n    -+\tgit commit -m \"more\" &&\n    -+\ttest_cmp expected_hooks actual_hooks\n    -+'\n    -+\n    - test_expect_success 'with succeeding hook (merge)' '\n    - \ttest_when_finished \"rm -f \\\"$PREMERGE\\\" expected_hooks actual_hooks\" &&\n    - \tcp \"$HOOKDIR/success.sample\" \"$PREMERGE\" &&\n30:  c9d8c1581de <  -:  ----------- proc-receive: acquire hook list from hook.h\n36:  4caea3e6805 <  -:  ----------- doc: clarify fsmonitor-watchman specification\n37:  f2e1003b62a <  -:  ----------- docs: link githooks and git-hook manpages\n -:  ----------- > 31:  896956250f6 hook-list.h: add a generated list of hooks, like config-list.h\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425771","messageId":"patch-06.31-b186fde43e1-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 06/31] gc: use hook library for pre-auto-gc hook","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:08Z","receivedAt":"2021-05-28T12:11:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nUsing the hook.h library instead of the run-command.h library to run\npre-auto-gc means that those hooks can be set up in config files, as\nwell as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c | 8 +++++++-\n 1 file changed, 7 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1ac..a12641a691d 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -32,6 +32,7 @@\n #include \"remote.h\"\n #include \"object-store.h\"\n #include \"exec-cmd.h\"\n+#include \"hook.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -348,6 +349,8 @@ static void add_repack_incremental_option(void)\n \n static int need_to_gc(void)\n {\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n \t * automatic gc.\n@@ -394,8 +397,11 @@ static int need_to_gc(void)\n \telse\n \t\treturn 0;\n \n-\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n+\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\treturn 0;\n+\t}\n+\trun_hooks_opt_clear(&hook_opt);\n \treturn 1;\n }\n \n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425773","messageId":"patch-08.31-69842c74383-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 08/31] am: convert applypatch hooks to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:10Z","receivedAt":"2021-05-28T12:12:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach pre-applypatch, post-applypatch, and applypatch-msg to use the\nhook.h library instead of the run-command.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 15 ++++++++++++---\n 1 file changed, 12 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 1c8a5489035..9e9c1b5e9f2 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -445,9 +445,12 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tassert(state->msg);\n-\tret = run_hook_le(NULL, \"applypatch-msg\", am_path(state, \"final-commit\"), NULL);\n+\tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n+\tret = run_hooks(\"applypatch-msg\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tif (!ret) {\n \t\tFREE_AND_NULL(state->msg);\n@@ -1607,9 +1610,13 @@ static void do_commit(const struct am_state *state)\n \tstruct commit_list *parents = NULL;\n \tconst char *reflog_msg, *author, *committer = NULL;\n \tstruct strbuf sb = STRBUF_INIT;\n+\tstruct run_hooks_opt hook_opt_pre = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt_post = RUN_HOOKS_OPT_INIT;\n \n-\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n+\tif (run_hooks(\"pre-applypatch\", &hook_opt_pre)) {\n+\t\trun_hooks_opt_clear(&hook_opt_pre);\n \t\texit(1);\n+\t}\n \n \tif (write_cache_as_tree(&tree, 0, NULL))\n \t\tdie(_(\"git write-tree failed to write a tree\"));\n@@ -1660,8 +1667,10 @@ static void do_commit(const struct am_state *state)\n \t\tfclose(fp);\n \t}\n \n-\trun_hook_le(NULL, \"post-applypatch\", NULL);\n+\trun_hooks(\"post-applypatch\", &hook_opt_post);\n \n+\trun_hooks_opt_clear(&hook_opt_pre);\n+\trun_hooks_opt_clear(&hook_opt_post);\n \tstrbuf_release(&sb);\n }\n \n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425774","messageId":"patch-07.31-528402fac69-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 07/31] rebase: teach pre-rebase to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:09Z","receivedAt":"2021-05-28T12:12:05Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the pre-rebase hook away from run-command.h to and over to the\nnew hook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/rebase.c | 9 +++++++--\n 1 file changed, 7 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 12f093121d9..2081f6fa8db 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -28,6 +28,7 @@\n #include \"sequencer.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_REFLOG_ACTION \"rebase\"\n \n@@ -1313,6 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\n@@ -2022,10 +2024,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t}\n \n \t/* If a hook exists, give it a chance to interrupt*/\n+\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n \tif (!ok_to_skip_pre_rebase &&\n-\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n-\t\t\targc ? argv[0] : NULL, NULL))\n+\t    run_hooks(\"pre-rebase\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n+\t}\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (options.flags & REBASE_DIFFSTAT) {\n \t\tstruct diff_options opts;\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425775","messageId":"patch-09.31-9b32c14669b-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 09/31] hooks: convert 'post-checkout' hook to hook library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:11Z","receivedAt":"2021-05-28T12:12:09Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the running of the 'post-checkout' hook away from run-command.h\nto the new hook.h library. For \"worktree\" this requires a change to it\nto run the hooks from a given directory.\n\nWe could strictly speaking skip the \"absolute_path\" flag and just\ncheck if \"dir\" is specified, but let's split them up for clarity, as\nwell as for any future user who'd like to set \"dir\" but not implicitly\nchange the argument to an absolute path.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/checkout.c | 17 ++++++++++++-----\n builtin/clone.c    |  7 +++++--\n builtin/worktree.c | 30 ++++++++++++++----------------\n hook.c             |  8 ++++++++\n hook.h             |  9 +++++++++\n read-cache.c       |  1 +\n reset.c            | 15 +++++++++++----\n 7 files changed, 60 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f4cd7747d35..6205ace09f6 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"diff.h\"\n #include \"dir.h\"\n+#include \"hook.h\"\n #include \"ll-merge.h\"\n #include \"lockfile.h\"\n #include \"merge-recursive.h\"\n@@ -106,13 +107,19 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\treturn run_hook_le(NULL, \"post-checkout\",\n-\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n-\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n-\t\t\t   changed ? \"1\" : \"0\", NULL);\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint rc;\n+\n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\n-\n+\tstrvec_pushl(&opt.args,\n+\t\t     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n+\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n+\t\t     changed ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trc = run_hooks(\"post-checkout\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n }\n \n static int update_some(const struct object_id *oid, struct strbuf *base,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex eeb74c0217c..6687025bea5 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -32,6 +32,7 @@\n #include \"connected.h\"\n #include \"packfile.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"hook.h\"\n \n /*\n  * Overall FIXMEs:\n@@ -775,6 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tif (option_no_checkout)\n \t\treturn 0;\n@@ -820,8 +822,9 @@ static int checkout(int submodule_progress)\n \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n \t\tdie(_(\"unable to write new index file\"));\n \n-\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(null_oid()),\n-\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n+\tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n+\terr |= run_hooks(\"post-checkout\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (!err && (option_recurse_submodules.nr > 0)) {\n \t\tstruct strvec args = STRVEC_INIT;\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex b1350640fed..2ad26a76f4c 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -382,22 +382,20 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tconst char *hook = find_hook(\"post-checkout\");\n-\t\tif (hook) {\n-\t\t\tconst char *env[] = { \"GIT_DIR\", \"GIT_WORK_TREE\", NULL };\n-\t\t\tcp.git_cmd = 0;\n-\t\t\tcp.no_stdin = 1;\n-\t\t\tcp.stdout_to_stderr = 1;\n-\t\t\tcp.dir = path;\n-\t\t\tcp.env = env;\n-\t\t\tcp.argv = NULL;\n-\t\t\tcp.trace2_hook_name = \"post-checkout\";\n-\t\t\tstrvec_pushl(&cp.args, absolute_path(hook),\n-\t\t\t\t     oid_to_hex(null_oid()),\n-\t\t\t\t     oid_to_hex(&commit->object.oid),\n-\t\t\t\t     \"1\", NULL);\n-\t\t\tret = run_command(&cp);\n-\t\t}\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(null_oid()),\n+\t\t\t     oid_to_hex(&commit->object.oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\topt.dir = path;\n+\t\topt.absolute_path = 1;\n+\n+\t\tret = run_hooks(\"post-checkout\", &opt);\n+\n+\t\trun_hooks_opt_clear(&opt);\n \t}\n \n \tstrvec_clear(&child_env);\ndiff --git a/hook.c b/hook.c\nindex c7da273822d..51337f9798f 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -62,6 +62,7 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\n+\tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n \tstrvec_push(&cp->args, run_me->hook_path);\n@@ -111,6 +112,7 @@ static int notify_hook_finished(int result,\n int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t    struct run_hooks_opt *options)\n {\n+\tstruct strbuf abs_path = STRBUF_INIT;\n \tstruct hook my_hook = {\n \t\t.hook_path = hook_path,\n \t};\n@@ -119,6 +121,10 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t.hook_name = hook_name,\n \t\t.options = options,\n \t};\n+\tif (options->absolute_path) {\n+\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n+\t\tmy_hook.hook_path = abs_path.buf;\n+\t}\n \tcb_data.run_me = &my_hook;\n \n \tif (options->jobs != 1)\n@@ -131,6 +137,8 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n+\tif (options->absolute_path)\n+\t\tstrbuf_release(&abs_path);\n \n \treturn cb_data.rc;\n }\ndiff --git a/hook.h b/hook.h\nindex cbda7746a5d..2d7724bbb50 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -19,6 +19,15 @@ struct run_hooks_opt\n \n \t/* Number of threads to parallelize across */\n \tint jobs;\n+\n+\t/* Resolve and run the \"absolute_path(hook)\" instead of\n+\t * \"hook\". Used for \"git worktree\" hooks\n+\t */\n+\tint absolute_path;\n+\n+\t/* Path to initial working directory for subprocess */\n+\tconst char *dir;\n+\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/read-cache.c b/read-cache.c\nindex 1b3c2eb408b..775e970402c 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -26,6 +26,7 @@\n #include \"thread-utils.h\"\n #include \"progress.h\"\n #include \"sparse-index.h\"\n+#include \"hook.h\"\n \n /* Mask for the name length in ce_flags in the on-disk index */\n \ndiff --git a/reset.c b/reset.c\nindex 4bea758053b..e6af33b901c 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -7,6 +7,7 @@\n #include \"tree-walk.h\"\n #include \"tree.h\"\n #include \"unpack-trees.h\"\n+#include \"hook.h\"\n \n int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t       const char *switch_to_branch, unsigned flags,\n@@ -126,10 +127,16 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\tret = create_symref(\"HEAD\", switch_to_branch,\n \t\t\t\t\t    reflog_head);\n \t}\n-\tif (run_hook)\n-\t\trun_hook_le(NULL, \"post-checkout\",\n-\t\t\t    oid_to_hex(orig ? orig : null_oid()),\n-\t\t\t    oid_to_hex(oid), \"1\", NULL);\n+\tif (run_hook) {\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(orig ? orig : null_oid()),\n+\t\t\t     oid_to_hex(oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\trun_hooks(\"post-checkout\", &opt);\n+\t\trun_hooks_opt_clear(&opt);\n+\t}\n \n leave_reset_head:\n \tstrbuf_release(&msg);\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425776","messageId":"patch-10.31-201c654bb0c-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 10/31] merge: use config-based hooks for post-merge hook","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:12Z","receivedAt":"2021-05-28T12:12:11Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach post-merge to use the hook.h library instead of the run-command.h\nlibrary to run hooks. This means that post-merge hooks can come from the\nconfig as well as from the hookdir. post-merge is invoked only from\nbuiltin/merge.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/merge.c | 7 +++++--\n 1 file changed, 5 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 5a10f6e3c96..a9363b94065 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,6 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\n@@ -489,7 +490,9 @@ static void finish(struct commit *head_commit,\n \t}\n \n \t/* Run a post-merge hook */\n-\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n+\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n+\trun_hooks(\"post-merge\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tapply_autostash(git_path_merge_autostash(the_repository));\n \tstrbuf_release(&reflog_message);\n@@ -849,7 +852,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t * and write it out as a tree.  We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (find_hook(\"pre-merge-commit\"))\n+\tif (hook_exists(\"pre-merge-commit\"))\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425777","messageId":"patch-11.31-e65d8bd6e6f-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 11/31] git hook run: add an --ignore-missing flag","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:13Z","receivedAt":"2021-05-28T12:12:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"For certain one-shot hooks we'd like to optimistically run them, and\nnot complain if they don't exist. This will be used by send-email in a\nsubsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt | 10 +++++++++-\n builtin/hook.c             |  5 +++++\n t/t1800-hook.sh            |  5 +++++\n 3 files changed, 19 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 902b9cffaef..1528c860cf1 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run <hook-name> [-- <hook-args>]\n+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -27,6 +27,14 @@ run::\n \t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n \tthis accepts.\n \n+OPTIONS\n+-------\n+\n+--ignore-missing::\n+\tIgnore any missing hook by quietly returning zero. Used for\n+\ttools that want to do a blind one-shot run of a hook that may\n+\tor may not be present.\n+\n SEE ALSO\n --------\n linkgit:githooks[5]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 1b1a594fd00..275dd5b0ed0 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -16,10 +16,13 @@ static int run(int argc, const char **argv, const char *prefix)\n \tint i;\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint rc = 0;\n+\tint ignore_missing = 0;\n \tconst char *hook_name;\n \tconst char *hook_path;\n \n \tstruct option run_options[] = {\n+\t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n+\t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n \t\tOPT_END(),\n \t};\n \n@@ -42,6 +45,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \thook_name = argv[1];\n \thook_path = find_hook(hook_name);\n \tif (!hook_path) {\n+\t\tif (ignore_missing)\n+\t\t\treturn 0;\n \t\terror(\"cannot find a hook named %s\", hook_name);\n \t\treturn 1;\n \t}\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 7cacd27c748..9077afa1ed9 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -16,6 +16,11 @@ test_expect_success 'git hook run -- nonexistent hook' '\n \ttest_cmp stderr.expect stderr.actual\n '\n \n+test_expect_success 'git hook run -- nonexistent hook with --ignore-missing' '\n+\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n+\ttest_must_be_empty stderr.actual\n+'\n+\n test_expect_success 'git hook run -- basic' '\n \twrite_script .git/hooks/test-hook <<-EOF &&\n \techo Test hook\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425778","messageId":"patch-12.31-8795e9ceab8-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 12/31] send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:14Z","receivedAt":"2021-05-28T12:12:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nChange the \"sendmail-validate\" hook to be run via the \"git hook run\"\nwrapper instead of via a direct invocation.\n\nThis is the smallest possibly change to get \"send-email\" using \"git\nhook run\". We still check the hook itself with \"-x\", and set a\n\"GIT_DIR\" variable, both of which are asserted by our tests. We'll\nneed to get rid of this special behavior if we start running N hooks,\nbut for now let's be as close to bug-for-bug compatible as possible.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl   | 20 ++++++++++++--------\n t/t9001-send-email.sh |  4 ++--\n 2 files changed, 14 insertions(+), 10 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 25be2ebd2af..2ab8dfdbded 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -213,13 +213,13 @@ sub format_2822_time {\n my $editor;\n \n sub system_or_msg {\n-\tmy ($args, $msg) = @_;\n+\tmy ($args, $msg, $cmd_name) = @_;\n \tsystem(@$args);\n \tmy $signalled = $? & 127;\n \tmy $exit_code = $? >> 8;\n \treturn unless $signalled or $exit_code;\n \n-\tmy @sprintf_args = ($args->[0], $exit_code);\n+\tmy @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);\n \tif (defined $msg) {\n \t\t# Quiet the 'redundant' warning category, except we\n \t\t# need to support down to Perl 5.8, so we can't do a\n@@ -1959,9 +1959,9 @@ sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n \tif ($repo) {\n+\t\tmy $hook_name = 'sendemail-validate';\n \t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n-\t\tmy $validate_hook = catfile($hooks_path,\n-\t\t\t\t\t    'sendemail-validate');\n+\t\tmy $validate_hook = catfile($hooks_path, $hook_name);\n \t\tmy $hook_error;\n \t\tif (-x $validate_hook) {\n \t\t\tmy $target = abs_path($fn);\n@@ -1970,13 +1970,17 @@ sub validate_patch {\n \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n \t\t\t\tor die(\"chdir: $!\");\n \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n+\t\t\tmy @validate_hook = (\"git\", \"hook\", \"run\", \"--ignore-missing\", $hook_name, \"--\", $target);\n+\t\t\t$hook_error = system_or_msg(\\@validate_hook, undef,\n+\t\t\t\t\t\t       \"git hook run $hook_name -- <patch>\");\n \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n \t\t}\n \t\tif ($hook_error) {\n-\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n-\t\t\t\t       \"%s\\n\" .\n-\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n+\t\t\t$hook_error = sprintf(__(\"fatal: %s: rejected by %s hook\\n\" .\n+\t\t\t\t\t\t $hook_error . \"\\n\" .\n+\t\t\t\t\t\t \"warning: no patches were sent\\n\"),\n+\t\t\t\t\t      $fn, $hook_name);\n+\t\t\tdie $hook_error;\n \t\t}\n \t}\n \ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 3b7540050ca..35b513c015f 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -539,7 +539,7 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'my-hooks/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -558,7 +558,7 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'$hooks_path/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425779","messageId":"patch-13.31-03129460fd2-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 13/31] git-p4: use 'git hook' to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:15Z","receivedAt":"2021-05-28T12:12:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nInstead of duplicating the behavior of run-command.h:run_hook_le() in\nPython, we can directly call 'git hook run'. We emulate the existence\ncheck with the --ignore-missing flag.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-p4.py | 72 ++++++-------------------------------------------------\n 1 file changed, 7 insertions(+), 65 deletions(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex d34a1946b75..e76d8df3139 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -207,71 +207,13 @@ def decode_path(path):\n         return path\n \n def run_git_hook(cmd, param=[]):\n-    \"\"\"Execute a hook if the hook exists.\"\"\"\n-    if verbose:\n-        sys.stderr.write(\"Looking for hook: %s\\n\" % cmd)\n-        sys.stderr.flush()\n-\n-    hooks_path = gitConfig(\"core.hooksPath\")\n-    if len(hooks_path) <= 0:\n-        hooks_path = os.path.join(os.environ[\"GIT_DIR\"], \"hooks\")\n-\n-    if not isinstance(param, list):\n-        param=[param]\n-\n-    # resolve hook file name, OS depdenent\n-    hook_file = os.path.join(hooks_path, cmd)\n-    if platform.system() == 'Windows':\n-        if not os.path.isfile(hook_file):\n-            # look for the file with an extension\n-            files = glob.glob(hook_file + \".*\")\n-            if not files:\n-                return True\n-            files.sort()\n-            hook_file = files.pop()\n-            while hook_file.upper().endswith(\".SAMPLE\"):\n-                # The file is a sample hook. We don't want it\n-                if len(files) > 0:\n-                    hook_file = files.pop()\n-                else:\n-                    return True\n-\n-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):\n-        return True\n-\n-    return run_hook_command(hook_file, param) == 0\n-\n-def run_hook_command(cmd, param):\n-    \"\"\"Executes a git hook command\n-       cmd = the command line file to be executed. This can be\n-       a file that is run by OS association.\n-\n-       param = a list of parameters to pass to the cmd command\n-\n-       On windows, the extension is checked to see if it should\n-       be run with the Git for Windows Bash shell.  If there\n-       is no file extension, the file is deemed a bash shell\n-       and will be handed off to sh.exe. Otherwise, Windows\n-       will be called with the shell to handle the file assocation.\n-\n-       For non Windows operating systems, the file is called\n-       as an executable.\n-    \"\"\"\n-    cli = [cmd] + param\n-    use_shell = False\n-    if platform.system() == 'Windows':\n-        (root,ext) = os.path.splitext(cmd)\n-        if ext == \"\":\n-            exe_path = os.environ.get(\"EXEPATH\")\n-            if exe_path is None:\n-                exe_path = \"\"\n-            else:\n-                exe_path = os.path.join(exe_path, \"bin\")\n-            cli = [os.path.join(exe_path, \"SH.EXE\")] + cli\n-        else:\n-            use_shell = True\n-    return subprocess.call(cli, shell=use_shell)\n-\n+    \"\"\"args are specified with -a <arg> -a <arg> -a <arg>\"\"\"\n+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]\n+    if param:\n+        args.append(\"--\")\n+        for p in param:\n+            args.append(p)\n+    return subprocess.call(args) == 0\n \n def write_pipe(c, stdin):\n     if verbose:\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425780","messageId":"patch-14.31-3f3610f5ed3-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 14/31] commit: use hook.h to execute hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:16Z","receivedAt":"2021-05-28T12:12:30Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach run_commit_hook() to call hook.h instead of run-command.h. This\ncovers 'pre-commit', 'commit-msg', and\n'prepare-commit-msg'.\n\nAdditionally, ask the hook library - not run-command - whether any\nhooks will be run, as it's possible hooks may exist in the config but\nnot the hookdir.\n\nBecause all but 'post-commit' hooks are expected to make some state\nchange, force all but 'post-commit' hook to run in series. 'post-commit'\n\"is meant primarily for notification, and cannot affect the outcome of\n`git commit`,\" so it is fine to run in parallel.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c |  2 +-\n commit.c         | 16 ++++++++++------\n sequencer.c      |  2 +-\n 3 files changed, 12 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex f1aafd67d46..dad4e565443 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1045,7 +1045,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && find_hook(\"pre-commit\")) {\n+\tif (!no_verify && hook_exists(\"pre-commit\")) {\n \t\t/*\n \t\t * Re-read the index as pre-commit hook could have updated it,\n \t\t * and write it out as a tree.  We must do this before we invoke\ndiff --git a/commit.c b/commit.c\nindex 8ea55a447fa..e8147a88fc6 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -21,6 +21,7 @@\n #include \"commit-reach.h\"\n #include \"run-command.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);\n \n@@ -1698,22 +1699,25 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    const char *name, ...)\n {\n-\tstruct strvec hook_env = STRVEC_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tva_list args;\n+\tconst char *arg;\n \tint ret;\n-\n-\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n+\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\n \t * Let the hook know that no editor will be launched.\n \t */\n \tif (!editor_is_used)\n-\t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n+\t\tstrvec_push(&opt.env, \"GIT_EDITOR=:\");\n \n \tva_start(args, name);\n-\tret = run_hook_ve(hook_env.v, name, args);\n+\twhile ((arg = va_arg(args, const char *)))\n+\t\tstrvec_push(&opt.args, arg);\n \tva_end(args);\n-\tstrvec_clear(&hook_env);\n+\n+\tret = run_hooks(name, &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \treturn ret;\n }\ndiff --git a/sequencer.c b/sequencer.c\nindex 3de479f90e1..8f46984ffb7 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1446,7 +1446,7 @@ static int try_to_commit(struct repository *r,\n \t\t}\n \t}\n \n-\tif (find_hook(\"prepare-commit-msg\")) {\n+\tif (hook_exists(\"prepare-commit-msg\")) {\n \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n \t\tif (res)\n \t\t\tgoto out;\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425781","messageId":"patch-15.31-6482a3e4cb8-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 15/31] read-cache: convert post-index-change hook to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:17Z","receivedAt":"2021-05-28T12:12:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using hook.h instead of run-command.h to run, post-index-change hooks\ncan now be specified in the config in addition to the hookdir.\npost-index-change is not run anywhere besides in read-cache.c.\n\nThis removes the last direct user of run_hook_ve(), so we can make the\nfunction static now. It'll be removed entirely soon.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n read-cache.c  | 11 ++++++++---\n run-command.c |  2 +-\n run-command.h |  1 -\n 3 files changed, 9 insertions(+), 5 deletions(-)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex 775e970402c..a17bc30f870 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3132,6 +3132,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tret = convert_to_sparse(istate);\n \n@@ -3160,9 +3161,13 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \telse\n \t\tret = close_lock_file_gently(lock);\n \n-\trun_hook_le(NULL, \"post-index-change\",\n-\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n-\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n+\tstrvec_pushl(&hook_opt.args,\n+\t\t     istate->updated_workdir ? \"1\" : \"0\",\n+\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trun_hooks(\"post-index-change\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n+\n \tistate->updated_workdir = 0;\n \tistate->updated_skipworktree = 0;\n \ndiff --git a/run-command.c b/run-command.c\nindex 82fdf296569..eecdef5a0c8 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1321,7 +1321,7 @@ int async_with_fork(void)\n #endif\n }\n \n-int run_hook_ve(const char *const *env, const char *name, va_list args)\n+static int run_hook_ve(const char *const *env, const char *name, va_list args)\n {\n \tstruct child_process hook = CHILD_PROCESS_INIT;\n \tconst char *p;\ndiff --git a/run-command.h b/run-command.h\nindex b58531a7eb3..24ab5d63c4c 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -216,7 +216,6 @@ int run_command(struct child_process *);\n  */\n LAST_ARG_MUST_BE_NULL\n int run_hook_le(const char *const *env, const char *name, ...);\n-int run_hook_ve(const char *const *env, const char *name, va_list args);\n \n /*\n  * Trigger an auto-gc\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425782","messageId":"patch-16.31-a16163d4fb5-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 16/31] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:18Z","receivedAt":"2021-05-28T12:12:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using hook.h instead of run-command.h to invoke push-to-checkout,\nhooks can now be specified in the config as well as in the hookdir.\npush-to-checkout is not called anywhere but in builtin/receive-pack.c.\n\nThis is the last user of the run_hook_le() API, so let's remove it\nwhile we're at it, since run_hook_le() itself is the last user of\nrun_hook_ve() we can remove that too. The last direct user of\nrun_hook_le() was removed in the commit preceding this one.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 14 ++++++++++----\n run-command.c          | 32 --------------------------------\n run-command.h          | 16 ----------------\n 3 files changed, 10 insertions(+), 52 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 1e0e04c62fc..5248228ebfe 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1436,12 +1436,18 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n-\tif (run_hook_le(env->v, push_to_checkout_hook,\n-\t\t\thash_to_hex(hash), NULL))\n+\tstrvec_pushv(&opt.env, env->v);\n+\tstrvec_push(&opt.args, hash_to_hex(hash));\n+\tif (run_hooks(push_to_checkout_hook, &opt)) {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn \"push-to-checkout hook declined\";\n-\telse\n+\t} else {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn NULL;\n+\t}\n }\n \n static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree)\n@@ -1465,7 +1471,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!find_hook(push_to_checkout_hook))\n+\tif (!hook_exists(push_to_checkout_hook))\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n \telse\n \t\tretval = push_to_checkout(sha1, &env, work_tree);\ndiff --git a/run-command.c b/run-command.c\nindex eecdef5a0c8..95c950a4a2b 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1321,38 +1321,6 @@ int async_with_fork(void)\n #endif\n }\n \n-static int run_hook_ve(const char *const *env, const char *name, va_list args)\n-{\n-\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\tconst char *p;\n-\n-\tp = find_hook(name);\n-\tif (!p)\n-\t\treturn 0;\n-\n-\tstrvec_push(&hook.args, p);\n-\twhile ((p = va_arg(args, const char *)))\n-\t\tstrvec_push(&hook.args, p);\n-\thook.env = env;\n-\thook.no_stdin = 1;\n-\thook.stdout_to_stderr = 1;\n-\thook.trace2_hook_name = name;\n-\n-\treturn run_command(&hook);\n-}\n-\n-int run_hook_le(const char *const *env, const char *name, ...)\n-{\n-\tva_list args;\n-\tint ret;\n-\n-\tva_start(args, name);\n-\tret = run_hook_ve(env, name, args);\n-\tva_end(args);\n-\n-\treturn ret;\n-}\n-\n struct io_pump {\n \t/* initialized by caller */\n \tint fd;\ndiff --git a/run-command.h b/run-command.h\nindex 24ab5d63c4c..748d4fc2a72 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -201,22 +201,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/**\n- * Run a hook.\n- * The first argument is a pathname to an index file, or NULL\n- * if the hook uses the default index file or no index is needed.\n- * The second argument is the name of the hook.\n- * The further arguments correspond to the hook arguments.\n- * The last argument has to be NULL to terminate the arguments list.\n- * If the hook does not exist or is not executable, the return\n- * value will be zero.\n- * If it is executable, the hook will be executed and the exit\n- * status of the hook is returned.\n- * On execution, .stdout_to_stderr and .no_stdin will be set.\n- */\n-LAST_ARG_MUST_BE_NULL\n-int run_hook_le(const char *const *env, const char *name, ...);\n-\n /*\n  * Trigger an auto-gc\n  */\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425783","messageId":"patch-19.31-986bfd89a54-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 19/31] am: convert 'post-rewrite' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:21Z","receivedAt":"2021-05-28T12:12:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 18 +++++-------------\n 1 file changed, 5 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 9e9c1b5e9f2..6e4f9c80360 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -467,23 +467,15 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tconst char *hook = find_hook(\"post-rewrite\");\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint ret;\n \n-\tif (!hook)\n-\t\treturn 0;\n-\n-\tstrvec_push(&cp.args, hook);\n-\tstrvec_push(&cp.args, \"rebase\");\n-\n-\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n-\tcp.stdout_to_stderr = 1;\n-\tcp.trace2_hook_name = \"post-rewrite\";\n+\tstrvec_push(&opt.args, \"rebase\");\n+\topt.path_to_stdin = am_path(state, \"rewritten\");\n \n-\tret = run_command(&cp);\n+\tret = run_hooks(\"post-rewrite\", &opt);\n \n-\tclose(cp.in);\n+\trun_hooks_opt_clear(&opt);\n \treturn ret;\n }\n \n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425784","messageId":"patch-17.31-7020cf10c8e-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 17/31] run-command: allow stdin for run_processes_parallel","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:19Z","receivedAt":"2021-05-28T12:12:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nWhile it makes sense not to inherit stdin from the parent process to\navoid deadlocking, it's not necessary to completely ban stdin to\nchildren. An informed user should be able to configure stdin safely. By\nsetting `some_child.process.no_stdin=1` before calling `get_next_task()`\nwe provide a reasonable default behavior but enable users to set up\nstdin streaming for themselves during the callback.\n\n`some_child.process.stdout_to_stderr`, however, remains unmodifiable by\n`get_next_task()` - the rest of the run_processes_parallel() API depends\non child output in stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n run-command.c | 9 ++++++++-\n 1 file changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex 95c950a4a2b..0bf771845e4 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1628,6 +1628,14 @@ static int pp_start_one(struct parallel_processes *pp)\n \tif (i == pp->max_processes)\n \t\tBUG(\"bookkeeping is hard\");\n \n+\t/*\n+\t * By default, do not inherit stdin from the parent process - otherwise,\n+\t * all children would share stdin! Users may overwrite this to provide\n+\t * something to the child's stdin by having their 'get_next_task'\n+\t * callback assign 0 to .no_stdin and an appropriate integer to .in.\n+\t */\n+\tpp->children[i].process.no_stdin = 1;\n+\n \tcode = pp->get_next_task(&pp->children[i].process,\n \t\t\t\t &pp->children[i].err,\n \t\t\t\t pp->data,\n@@ -1639,7 +1647,6 @@ static int pp_start_one(struct parallel_processes *pp)\n \t}\n \tpp->children[i].process.err = -1;\n \tpp->children[i].process.stdout_to_stderr = 1;\n-\tpp->children[i].process.no_stdin = 1;\n \n \tif (start_command(&pp->children[i].process)) {\n \t\tcode = pp->start_failure(&pp->children[i].err,\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425785","messageId":"patch-18.31-4745dcfce49-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 18/31] hook: support passing stdin to hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:20Z","receivedAt":"2021-05-28T12:12:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome hooks (such as post-rewrite) need to take input via stdin.\nPreviously, callers provided stdin to hooks by setting\nrun-command.h:child_process.in, which takes a FD. Callers would open the\nfile in question themselves before calling run-command(). However, since\nwe will now need to seek to the front of the file and read it again for\nevery hook which runs, hook.h:run_command() takes a path and handles FD\nmanagement itself. Since this file is opened for read only, it should\nnot prevent later parallel execution support.\n\nOn the frontend, this is supported by asking for a file path, rather\nthan by reading stdin. Reading directly from stdin would involve caching\nthe entire stdin (to memory or to disk) and reading it back from the\nbeginning to each hook. We'd want to support cases like insufficient\nmemory or storage for the file. While this may prove useful later, for\nnow the path of least resistance is to just ask the user to make this\ninterim file themselves.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt |  7 ++++++-\n builtin/hook.c             |  4 +++-\n hook.c                     |  8 +++++++-\n hook.h                     |  2 ++\n t/t1800-hook.sh            | 18 ++++++++++++++++++\n 5 files changed, 36 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 1528c860cf1..816b3eda460 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -30,6 +30,11 @@ run::\n OPTIONS\n -------\n \n+--to-stdin::\n+\tFor \"run\"; Specify a file which will be streamed into the\n+\thook's stdin. The hook will receive the entire file from\n+\tbeginning to EOF.\n+\n --ignore-missing::\n \tIgnore any missing hook by quietly returning zero. Used for\n \ttools that want to do a blind one-shot run of a hook that may\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 275dd5b0ed0..baaef4dce49 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -7,7 +7,7 @@\n #include \"strvec.h\"\n \n static const char * const builtin_hook_usage[] = {\n-\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n+\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n \tNULL\n };\n \n@@ -23,6 +23,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \tstruct option run_options[] = {\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n+\t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n+\t\t\t   N_(\"file to read into hooks' stdin\")),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/hook.c b/hook.c\nindex 51337f9798f..daf39f61741 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -58,7 +58,13 @@ static int pick_next_hook(struct child_process *cp,\n \tif (!run_me)\n \t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n \n-\tcp->no_stdin = 1;\n+\t/* reopen the file for stdin; run_command closes it. */\n+\tif (hook_cb->options->path_to_stdin) {\n+\t\tcp->no_stdin = 0;\n+\t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else {\n+\t\tcp->no_stdin = 1;\n+\t}\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\ndiff --git a/hook.h b/hook.h\nindex 2d7724bbb50..74a8c76a94c 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -28,6 +28,8 @@ struct run_hooks_opt\n \t/* Path to initial working directory for subprocess */\n \tconst char *dir;\n \n+\t/* Path to file which should be piped to stdin for each hook */\n+\tconst char *path_to_stdin;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 9077afa1ed9..9e2dd64275c 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -137,4 +137,22 @@ test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n \tEOF\n '\n \n+test_expect_success 'stdin to hooks' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo BEGIN stdin\n+\tcat\n+\techo END stdin\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\tBEGIN stdin\n+\thello\n+\tEND stdin\n+\tEOF\n+\n+\techo hello >input &&\n+\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425786","messageId":"patch-20.31-756f52af22d-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 20/31] run-command: add stdin callback for parallelization","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:22Z","receivedAt":"2021-05-28T12:13:13Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIf a user of the run_processes_parallel() API wants to pipe a large\namount of information to stdin of each parallel command, that\ninformation could exceed the buffer of the pipe allocated for that\nprocess's stdin.  Generally this is solved by repeatedly writing to\nchild_process.in between calls to start_command() and finish_command();\nrun_processes_parallel() did not provide users an opportunity to access\nchild_process at that time.\n\nBecause the data might be extremely large (for example, a list of all\nrefs received during a push from a client) simply taking a string_list\nor strbuf is not as scalable as using a callback; the rest of the\nrun_processes_parallel() API also uses callbacks, so making this feature\nmatch the rest of the API reduces mental load on the user.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  1 +\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 54 +++++++++++++++++++++++++++++++++++--\n run-command.h               | 17 +++++++++++-\n submodule.c                 |  1 +\n t/helper/test-run-command.c | 31 ++++++++++++++++++---\n t/t0061-run-command.sh      | 30 +++++++++++++++++++++\n 8 files changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex dfde96a4354..a07816b6504 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,6 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n+\t\t\t\t\t\t    NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex d55f6262e9c..8d1e7310737 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2295,7 +2295,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure,\n+\t\t\t\t   update_clone_start_failure, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex daf39f61741..5f6335bac3f 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -139,6 +139,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 0bf771845e4..3392640f17b 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1493,6 +1493,7 @@ struct parallel_processes {\n \n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n+\tfeed_pipe_fn feed_pipe;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1520,6 +1521,13 @@ static int default_start_failure(struct strbuf *out,\n \treturn 0;\n }\n \n+static int default_feed_pipe(struct strbuf *pipe,\n+\t\t\t     void *pp_cb,\n+\t\t\t     void *pp_task_cb)\n+{\n+\treturn 1;\n+}\n+\n static int default_task_finished(int result,\n \t\t\t\t struct strbuf *out,\n \t\t\t\t void *pp_cb,\n@@ -1550,6 +1558,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    int n,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n+\t\t    feed_pipe_fn feed_pipe,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1568,6 +1577,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->get_next_task = get_next_task;\n \n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n+\tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n \n \tpp->nr_processes = 0;\n@@ -1665,6 +1675,37 @@ static int pp_start_one(struct parallel_processes *pp)\n \treturn 0;\n }\n \n+static void pp_buffer_stdin(struct parallel_processes *pp)\n+{\n+\tint i;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t/* Buffer stdin for each pipe. */\n+\tfor (i = 0; i < pp->max_processes; i++) {\n+\t\tif (pp->children[i].state == GIT_CP_WORKING &&\n+\t\t    pp->children[i].process.in > 0) {\n+\t\t\tint done;\n+\t\t\tstrbuf_reset(&sb);\n+\t\t\tdone = pp->feed_pipe(&sb, pp->data,\n+\t\t\t\t\t      pp->children[i].data);\n+\t\t\tif (sb.len) {\n+\t\t\t\tif (write_in_full(pp->children[i].process.in,\n+\t\t\t\t\t      sb.buf, sb.len) < 0) {\n+\t\t\t\t\tif (errno != EPIPE)\n+\t\t\t\t\t\tdie_errno(\"write\");\n+\t\t\t\t\tdone = 1;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif (done) {\n+\t\t\t\tclose(pp->children[i].process.in);\n+\t\t\t\tpp->children[i].process.in = 0;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&sb);\n+}\n+\n static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n {\n \tint i;\n@@ -1729,6 +1770,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\tpp->nr_processes--;\n \t\tpp->children[i].state = GIT_CP_FREE;\n \t\tpp->pfd[i].fd = -1;\n+\t\tpp->children[i].process.in = 0;\n \t\tchild_process_init(&pp->children[i].process);\n \n \t\tif (i != pp->output_owner) {\n@@ -1762,6 +1804,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n+\t\t\t   feed_pipe_fn feed_pipe,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1770,7 +1813,9 @@ int run_processes_parallel(int n,\n \tint spawn_cap = 4;\n \tstruct parallel_processes pp;\n \n-\tpp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb);\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1787,6 +1832,7 @@ int run_processes_parallel(int n,\n \t\t}\n \t\tif (!pp.nr_processes)\n \t\t\tbreak;\n+\t\tpp_buffer_stdin(&pp);\n \t\tpp_buffer_stderr(&pp, output_timeout);\n \t\tpp_output(&pp);\n \t\tcode = pp_collect_finished(&pp);\n@@ -1798,11 +1844,15 @@ int run_processes_parallel(int n,\n \t}\n \n \tpp_cleanup(&pp);\n+\n+\tsigchain_pop(SIGPIPE);\n+\n \treturn 0;\n }\n \n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n+\t\t\t       feed_pipe_fn feed_pipe,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1812,7 +1862,7 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\ttask_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 748d4fc2a72..41e36d26cb1 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -419,6 +419,20 @@ typedef int (*start_failure_fn)(struct strbuf *out,\n \t\t\t\tvoid *pp_cb,\n \t\t\t\tvoid *pp_task_cb);\n \n+/**\n+ * This callback is called repeatedly on every child process who requests\n+ * start_command() to create a pipe by setting child_process.in < 0.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel, and\n+ * pp_task_cb is the callback cookie as passed into get_next_task_fn.\n+ * The contents of 'send' will be read into the pipe and passed to the pipe.\n+ *\n+ * Return nonzero to close the pipe.\n+ */\n+typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n+\t\t\t    void *pp_cb,\n+\t\t\t    void *pp_task_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -453,10 +467,11 @@ typedef int (*task_finished_fn)(int result,\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n+\t\t\t   feed_pipe_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex 0b1d9c1dde5..ea026a8195f 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1645,6 +1645,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 7ae03dc7123..9348184d303 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,\n \t\treturn 0;\n \n \tstrvec_pushv(&cp->args, d->argv);\n+\tcp->in = d->in;\n+\tcp->no_stdin = d->no_stdin;\n \tstrbuf_addstr(err, \"preloaded output of a child\\n\");\n \tnumber_callbacks++;\n+\n+\t*task_cb = xmalloc(sizeof(int));\n+\t*(int*)(*task_cb) = 2;\n \treturn 1;\n }\n \n@@ -55,6 +60,17 @@ static int task_finished(int result,\n \treturn 1;\n }\n \n+static int test_stdin(struct strbuf *pipe, void *cb, void *task_cb)\n+{\n+\tint *lines_remaining = task_cb;\n+\n+\tif (*lines_remaining)\n+\t\tstrbuf_addf(pipe, \"sample stdin %d\\n\", --(*lines_remaining));\n+\n+\treturn !(*lines_remaining);\n+}\n+\n+\n struct testsuite {\n \tstruct string_list tests, failed;\n \tint next;\n@@ -185,7 +201,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_finished, &suite);\n+\t\t\t\t     test_stdin, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -413,15 +429,22 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\n+\tif (!strcmp(argv[1], \"run-command-stdin\")) {\n+\t\tproc.in = -1;\n+\t\tproc.no_stdin = 0;\n+\t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n+\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t}\n \n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 7d599675e35..87759482ad1 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,36 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+cat >expect <<-EOF\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+EOF\n+\n+test_expect_success 'run_command listens to stdin' '\n+\twrite_script stdin-script <<-\\EOF &&\n+\techo \"listening for stdin:\"\n+\twhile read line; do\n+\t\techo \"$line\"\n+\tdone\n+\tEOF\n+\ttest-tool run-command run-command-stdin 2 ./stdin-script 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n asking for a quick stop\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425787","messageId":"patch-21.31-3748f128763-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 21/31] hook: provide stdin by string_list or callback","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:23Z","receivedAt":"2021-05-28T12:13:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn cases where a hook requires only a small amount of information via\nstdin, it should be simple for users to provide a string_list alone. But\nin more complicated cases where the stdin is too large to hold in\nmemory, let's provide a callback the users can populate line after line\nwith instead.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 33 +++++++++++++++++++++++++++++++--\n hook.h | 27 +++++++++++++++++++++++++++\n 2 files changed, 58 insertions(+), 2 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 5f6335bac3f..ac5e3988fec 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -47,6 +47,29 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)\n \tstrvec_clear(&o->args);\n }\n \n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n+{\n+\tint *item_idx;\n+\tstruct hook *ctx = pp_task_cb;\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct string_list *to_pipe = hook_cb->options->feed_pipe_ctx;\n+\n+\t/* Bootstrap the state manager if necessary. */\n+\tif (!ctx->feed_pipe_cb_data) {\n+\t\tctx->feed_pipe_cb_data = xmalloc(sizeof(unsigned int));\n+\t\t*(int*)ctx->feed_pipe_cb_data = 0;\n+\t}\n+\n+\titem_idx = ctx->feed_pipe_cb_data;\n+\n+\tif (*item_idx < to_pipe->nr) {\n+\t\tstrbuf_addf(pipe, \"%s\\n\", to_pipe->items[*item_idx].string);\n+\t\t(*item_idx)++;\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n static int pick_next_hook(struct child_process *cp,\n \t\t\t  struct strbuf *out,\n \t\t\t  void *pp_cb,\n@@ -62,6 +85,10 @@ static int pick_next_hook(struct child_process *cp,\n \tif (hook_cb->options->path_to_stdin) {\n \t\tcp->no_stdin = 0;\n \t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else if (hook_cb->options->feed_pipe) {\n+\t\t/* ask for start_command() to make a pipe for us */\n+\t\tcp->in = -1;\n+\t\tcp->no_stdin = 0;\n \t} else {\n \t\tcp->no_stdin = 1;\n \t}\n@@ -114,7 +141,6 @@ static int notify_hook_finished(int result,\n \treturn 1;\n }\n \n-\n int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t    struct run_hooks_opt *options)\n {\n@@ -139,7 +165,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->feed_pipe,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n@@ -157,6 +183,9 @@ int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n+\tif (options->path_to_stdin && options->feed_pipe)\n+\t\tBUG(\"choose only one method to populate stdin\");\n+\n \thook_path = find_hook(hook_name);\n \n \t/* Care about nonexistence? Use run_found_hooks() */\ndiff --git a/hook.h b/hook.h\nindex 74a8c76a94c..ff1697d1087 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -7,6 +7,12 @@\n struct hook {\n \t/* The path to the hook */\n \tconst char *hook_path;\n+\n+\t/*\n+\t * Use this to keep state for your feed_pipe_fn if you are using\n+\t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n+\t */\n+\tvoid *feed_pipe_cb_data;\n };\n \n struct run_hooks_opt\n@@ -30,6 +36,19 @@ struct run_hooks_opt\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n+\n+\t/*\n+\t * Callback and state pointer to ask for more content to pipe to stdin.\n+\t * Will be called repeatedly, for each hook. See\n+\t * hook.c:pipe_from_stdin() for an example. Keep per-hook state in\n+\t * hook.feed_pipe_cb_data (per process). Keep initialization context in\n+\t * feed_pipe_ctx (shared by all processes).\n+\t *\n+\t * See 'pipe_from_string_list()' for info about how to specify a\n+\t * string_list as the stdin input instead of writing your own handler.\n+\t */\n+\tfeed_pipe_fn feed_pipe;\n+\tvoid *feed_pipe_ctx;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -38,6 +57,14 @@ struct run_hooks_opt\n \t.args = STRVEC_INIT, \\\n }\n \n+/*\n+ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n+ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n+ * This will pipe each string in the list to stdin, separated by newlines.  (Do\n+ * not inject your own newlines.)\n+ */\n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n+\n /*\n  * Callback provided to feed_pipe_fn and consume_sideband_fn.\n  */\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425788","messageId":"patch-22.31-0cf0b1fea93-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 22/31] hook: convert 'post-rewrite' hook in sequencer.c to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:24Z","receivedAt":"2021-05-28T12:13:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using 'hook.h' for 'post-rewrite', we simplify hook invocations by\nnot needing to put together our own 'struct child_process'.\n\nThe signal handling that's being removed by this commit now takes\nplace in run-command.h:run_processes_parallel(), so it is OK to remove\nthem here.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n sequencer.c | 81 ++++++++++++++++++++++-------------------------------\n 1 file changed, 34 insertions(+), 47 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 8f46984ffb7..ec2761e47d9 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -35,6 +35,7 @@\n #include \"commit-reach.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"string-list.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -1147,33 +1148,28 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *argv[3];\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n-\tstruct strbuf sb = STRBUF_INIT;\n \n-\targv[0] = find_hook(\"post-rewrite\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tstrvec_push(&opt.args, \"amend\");\n \n-\targv[1] = \"amend\";\n-\targv[2] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"post-rewrite\";\n-\n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n-\tstrbuf_addf(&sb, \"%s %s\\n\", oid_to_hex(oldoid), oid_to_hex(newoid));\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\twrite_in_full(proc.in, sb.buf, sb.len);\n-\tclose(proc.in);\n-\tstrbuf_release(&sb);\n-\tsigchain_pop(SIGPIPE);\n-\treturn finish_command(&proc);\n+\tstrbuf_addf(&tmp,\n+\t\t    \"%s %s\",\n+\t\t    oid_to_hex(oldoid),\n+\t\t    oid_to_hex(newoid));\n+\tstring_list_append(&to_stdin, tmp.buf);\n+\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tcode = run_hooks(\"post-rewrite\", &opt);\n+\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n+\treturn code;\n }\n \n void commit_post_rewrite(struct repository *r,\n@@ -4527,30 +4523,21 @@ static int pick_commits(struct repository *r,\n \t\tflush_rewritten_pending();\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n-\t\t\tstruct child_process child = CHILD_PROCESS_INIT;\n-\t\t\tconst char *post_rewrite_hook =\n-\t\t\t\tfind_hook(\"post-rewrite\");\n-\n-\t\t\tchild.in = open(rebase_path_rewritten_list(), O_RDONLY);\n-\t\t\tchild.git_cmd = 1;\n-\t\t\tstrvec_push(&child.args, \"notes\");\n-\t\t\tstrvec_push(&child.args, \"copy\");\n-\t\t\tstrvec_push(&child.args, \"--for-rewrite=rebase\");\n+\t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n+\t\t\tnotes_cp.git_cmd = 1;\n+\t\t\tstrvec_push(&notes_cp.args, \"notes\");\n+\t\t\tstrvec_push(&notes_cp.args, \"copy\");\n+\t\t\tstrvec_push(&notes_cp.args, \"--for-rewrite=rebase\");\n \t\t\t/* we don't care if this copying failed */\n-\t\t\trun_command(&child);\n-\n-\t\t\tif (post_rewrite_hook) {\n-\t\t\t\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\n-\t\t\t\thook.in = open(rebase_path_rewritten_list(),\n-\t\t\t\t\tO_RDONLY);\n-\t\t\t\thook.stdout_to_stderr = 1;\n-\t\t\t\thook.trace2_hook_name = \"post-rewrite\";\n-\t\t\t\tstrvec_push(&hook.args, post_rewrite_hook);\n-\t\t\t\tstrvec_push(&hook.args, \"rebase\");\n-\t\t\t\t/* we don't care if this hook failed */\n-\t\t\t\trun_command(&hook);\n-\t\t\t}\n+\t\t\trun_command(&notes_cp);\n+\n+\t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n+\t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n+\t\t\trun_hooks(\"post-rewrite\", &hook_opt);\n+\t\t\trun_hooks_opt_clear(&hook_opt);\n \t\t}\n \t\tapply_autostash(rebase_path_autostash());\n \n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425789","messageId":"patch-23.31-c59443a3b05-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 23/31] transport: convert pre-push hook to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:25Z","receivedAt":"2021-05-28T12:13:17Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using the hook.h:run_hooks API, pre-push hooks can be specified in\nthe config as well as in the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n transport.c | 57 ++++++++++++++---------------------------------------\n 1 file changed, 15 insertions(+), 42 deletions(-)\n\ndiff --git a/transport.c b/transport.c\nindex e6c46adf60c..1146ed3143c 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1197,31 +1197,14 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)\n static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n-\tint ret = 0, x;\n+\tint ret = 0;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct strbuf tmp = STRBUF_INIT;\n \tstruct ref *r;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct strbuf buf;\n-\tconst char *argv[4];\n-\n-\tif (!(argv[0] = find_hook(\"pre-push\")))\n-\t\treturn 0;\n-\n-\targv[1] = transport->remote->name;\n-\targv[2] = transport->url;\n-\targv[3] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.trace2_hook_name = \"pre-push\";\n-\n-\tif (start_command(&proc)) {\n-\t\tfinish_command(&proc);\n-\t\treturn -1;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \n-\tstrbuf_init(&buf, 256);\n+\tstrvec_push(&opt.args, transport->remote->name);\n+\tstrvec_push(&opt.args, transport->url);\n \n \tfor (r = remote_refs; r; r = r->next) {\n \t\tif (!r->peer_ref) continue;\n@@ -1230,30 +1213,20 @@ static int run_pre_push_hook(struct transport *transport,\n \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n \n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n+\t\tstrbuf_reset(&tmp);\n+\t\tstrbuf_addf(&tmp, \"%s %s %s %s\",\n \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n \t\t\t r->name, oid_to_hex(&r->old_oid));\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\t/* We do not mind if a hook does not read all refs. */\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstring_list_append(&to_stdin, tmp.buf);\n \t}\n \n-\tstrbuf_release(&buf);\n-\n-\tx = close(proc.in);\n-\tif (!ret)\n-\t\tret = x;\n-\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n \n-\tx = finish_command(&proc);\n-\tif (!ret)\n-\t\tret = x;\n+\tret = run_hooks(\"pre-push\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n \n \treturn ret;\n }\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425790","messageId":"patch-24.31-f7c8c97cb81-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 24/31] reference-transaction: use hook.h to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:26Z","receivedAt":"2021-05-28T12:13:24Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using the hook.h library, we get closer to removing the hook code\nin run-command.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n refs.c | 42 +++++++++++++++---------------------------\n 1 file changed, 15 insertions(+), 27 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 59be29cf081..1149e7e7dcb 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2062,47 +2062,35 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n \tstruct strbuf buf = STRBUF_INIT;\n-\tconst char *hook;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint ret = 0, i;\n+\tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n \n-\thook = find_hook(\"reference-transaction\");\n-\tif (!hook)\n+\tif (!hook_exists(\"reference-transaction\"))\n \t\treturn ret;\n \n-\tstrvec_pushl(&proc.args, hook, state, NULL);\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"reference-transaction\";\n-\n-\tret = start_command(&proc);\n-\tif (ret)\n-\t\treturn ret;\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstrvec_push(&opt.args, state);\n \n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n+\t\toid_to_hex_r(o, &update->old_oid);\n+\t\toid_to_hex_r(n, &update->new_oid);\n \n \t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n-\t\t\t    oid_to_hex(&update->old_oid),\n-\t\t\t    oid_to_hex(&update->new_oid),\n-\t\t\t    update->refname);\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstrbuf_addf(&buf, \"%s %s %s\", o, n, update->refname);\n+\t\tstring_list_append(&to_stdin, buf.buf);\n \t}\n \n-\tclose(proc.in);\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tret = run_hooks(\"reference-transaction\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \tstrbuf_release(&buf);\n+\tstring_list_clear(&to_stdin, 0);\n \n-\tret |= finish_command(&proc);\n \treturn ret;\n }\n \n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425791","messageId":"patch-25.31-f240a51ec4e-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 25/31] run-command: allow capturing of collated output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:27Z","receivedAt":"2021-05-28T12:13:29Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome callers, for example server-side hooks which wish to relay hook\noutput to clients across a transport, want to capture what would\nnormally print to stderr and do something else with it. Allow that via a\ncallback.\n\nBy calling the callback regardless of whether there's output available,\nwe allow clients to send e.g. a keepalive if necessary.\n\nBecause we expose a strbuf, not a fd or FILE*, there's no need to create\na temporary pipe or similar - we can just skip the print to stderr and\ninstead hand it to the caller.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 33 +++++++++++++++++++++++++--------\n run-command.h               | 18 +++++++++++++++++-\n submodule.c                 |  2 +-\n t/helper/test-run-command.c | 25 ++++++++++++++++++++-----\n t/t0061-run-command.sh      |  7 +++++++\n 8 files changed, 73 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex a07816b6504..769af53ca45 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,7 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n-\t\t\t\t\t\t    NULL,\n+\t\t\t\t\t\t    NULL, NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 8d1e7310737..fef8392e1da 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2295,7 +2295,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure, NULL,\n+\t\t\t\t   update_clone_start_failure, NULL, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex ac5e3988fec..0faa24ec825 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -166,6 +166,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 3392640f17b..4a1a7a10820 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1494,6 +1494,7 @@ struct parallel_processes {\n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n \tfeed_pipe_fn feed_pipe;\n+\tconsume_sideband_fn consume_sideband;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1559,6 +1560,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n \t\t    feed_pipe_fn feed_pipe,\n+\t\t    consume_sideband_fn consume_sideband,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1579,6 +1581,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n \tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n+\tpp->consume_sideband = consume_sideband;\n \n \tpp->nr_processes = 0;\n \tpp->output_owner = 0;\n@@ -1615,7 +1618,10 @@ static void pp_cleanup(struct parallel_processes *pp)\n \t * When get_next_task added messages to the buffer in its last\n \t * iteration, the buffered output is non empty.\n \t */\n-\tstrbuf_write(&pp->buffered_output, stderr);\n+\tif (pp->consume_sideband)\n+\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\telse\n+\t\tstrbuf_write(&pp->buffered_output, stderr);\n \tstrbuf_release(&pp->buffered_output);\n \n \tsigchain_pop_common();\n@@ -1736,9 +1742,13 @@ static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n static void pp_output(struct parallel_processes *pp)\n {\n \tint i = pp->output_owner;\n+\n \tif (pp->children[i].state == GIT_CP_WORKING &&\n \t    pp->children[i].err.len) {\n-\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\tif (pp->consume_sideband)\n+\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\telse\n+\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n \t\tstrbuf_reset(&pp->children[i].err);\n \t}\n }\n@@ -1777,11 +1787,15 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\t\tstrbuf_addbuf(&pp->buffered_output, &pp->children[i].err);\n \t\t\tstrbuf_reset(&pp->children[i].err);\n \t\t} else {\n-\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t/* Output errors, then all other finished child processes */\n+\t\t\tif (pp->consume_sideband) {\n+\t\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\t\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\t\t\t} else {\n+\t\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n+\t\t\t}\n \t\t\tstrbuf_reset(&pp->children[i].err);\n-\n-\t\t\t/* Output all other finished child processes */\n-\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n \t\t\tstrbuf_reset(&pp->buffered_output);\n \n \t\t\t/*\n@@ -1805,6 +1819,7 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n \t\t\t   feed_pipe_fn feed_pipe,\n+\t\t\t   consume_sideband_fn consume_sideband,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1815,7 +1830,7 @@ int run_processes_parallel(int n,\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n \n-\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, consume_sideband, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1853,6 +1868,7 @@ int run_processes_parallel(int n,\n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n \t\t\t       feed_pipe_fn feed_pipe,\n+\t\t\t       consume_sideband_fn consume_sideband,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1862,7 +1878,8 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, consume_sideband,\n+\t\t\t\t\ttask_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 41e36d26cb1..7150da851a8 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -433,6 +433,20 @@ typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n \t\t\t    void *pp_cb,\n \t\t\t    void *pp_task_cb);\n \n+/**\n+ * If this callback is provided, instead of collating process output to stderr,\n+ * they will be collated into a new pipe. consume_sideband_fn will be called\n+ * repeatedly. When output is available on that pipe, it will be contained in\n+ * 'output'. But it will be called with an empty 'output' too, to allow for\n+ * keepalives or similar operations if necessary.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel.\n+ *\n+ * Since this callback is provided with the collated output, no task cookie is\n+ * provided.\n+ */\n+typedef void (*consume_sideband_fn)(struct strbuf *output, void *pp_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -468,10 +482,12 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n \t\t\t   feed_pipe_fn,\n+\t\t\t   consume_sideband_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, consume_sideband_fn,\n+\t\t\t       task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex ea026a8195f..7fe0c8f7c9f 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1645,7 +1645,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   NULL, NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 9348184d303..d53db6d11c4 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -51,6 +51,16 @@ static int no_job(struct child_process *cp,\n \treturn 0;\n }\n \n+static void test_consume_sideband(struct strbuf *output, void *cb)\n+{\n+\tFILE *sideband;\n+\n+\tsideband = fopen(\"./sideband\", \"a\");\n+\n+\tstrbuf_write(output, sideband);\n+\tfclose(sideband);\n+}\n+\n static int task_finished(int result,\n \t\t\t struct strbuf *err,\n \t\t\t void *pp_cb,\n@@ -201,7 +211,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_stdin, test_finished, &suite);\n+\t\t\t\t     test_stdin, NULL, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -429,23 +439,28 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-stdin\")) {\n \t\tproc.in = -1;\n \t\tproc.no_stdin = 0;\n \t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n-\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t\t\t\t\t     test_stdin, NULL, NULL, &proc));\n \t}\n \n+\tif (!strcmp(argv[1], \"run-command-sideband\"))\n+\t\texit(run_processes_parallel(jobs, parallel_next, NULL, NULL,\n+\t\t\t\t\t    test_consume_sideband, NULL,\n+\t\t\t\t\t    &proc));\n+\n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\n }\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 87759482ad1..e99f6c7f445 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,13 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'run_command can divert output' '\n+\ttest_when_finished rm sideband &&\n+\ttest-tool run-command run-command-sideband 3 sh -c \"printf \\\"%s\\n%s\\n\\\" Hello World\" 2>actual &&\n+\ttest_must_be_empty actual &&\n+\ttest_cmp expect sideband\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n listening for stdin:\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425792","messageId":"patch-26.31-7f10efb7858-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 26/31] hooks: allow callers to capture output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:28Z","receivedAt":"2021-05-28T12:13:31Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome server-side hooks will require capturing output to send over\nsideband instead of printing directly to stderr. Expose that capability.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 2 +-\n hook.h | 8 ++++++++\n 2 files changed, 9 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 0faa24ec825..17ae65eca31 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -166,7 +166,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->consume_sideband,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/hook.h b/hook.h\nindex ff1697d1087..5f895032341 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -49,6 +49,14 @@ struct run_hooks_opt\n \t */\n \tfeed_pipe_fn feed_pipe;\n \tvoid *feed_pipe_ctx;\n+\n+\t/*\n+\t * Populate this to capture output and prevent it from being printed to\n+\t * stderr. This will be passed directly through to\n+\t * run_command:run_parallel_processes(). See t/helper/test-run-command.c\n+\t * for an example.\n+\t */\n+\tconsume_sideband_fn consume_sideband;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425793","messageId":"patch-28.31-3519068a634-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 28/31] post-update: use hook.h library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:30Z","receivedAt":"2021-05-28T12:13:32Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using run_hooks() instead of run_hook_le(), 'post-update' hooks can\nbe specified in the config as well as the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 26 +++++++-------------------\n 1 file changed, 7 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 378f8f6b5d1..b2ccdb66daa 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1657,33 +1657,21 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *hook;\n-\n-\thook = find_hook(\"post-update\");\n-\tif (!hook)\n-\t\treturn;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\n \t\t\tcontinue;\n-\t\tif (!proc.args.nr)\n-\t\t\tstrvec_push(&proc.args, hook);\n-\t\tstrvec_push(&proc.args, cmd->ref_name);\n+\t\tstrvec_push(&opt.args, cmd->ref_name);\n \t}\n-\tif (!proc.args.nr)\n+\tif (!opt.args.nr)\n \t\treturn;\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.trace2_hook_name = \"post-update\";\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n \n-\tif (!start_command(&proc)) {\n-\t\tif (use_sideband)\n-\t\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\t\tfinish_command(&proc);\n-\t}\n+\trun_hooks(\"post-update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n }\n \n static void check_aliased_update_internal(struct command *cmd,\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425794","messageId":"patch-27.31-c39c608e5cc-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 27/31] receive-pack: convert 'update' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:29Z","receivedAt":"2021-05-28T12:13:35Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using hook.h to invoke the 'update' hook we closer to removing the\nhooks code in run-command.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 65 ++++++++++++++++++++++++++++--------------\n 1 file changed, 44 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 5248228ebfe..378f8f6b5d1 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -938,33 +938,56 @@ static int run_receive_hook(struct command *commands,\n \treturn status;\n }\n \n-static int run_update_hook(struct command *cmd)\n+static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n {\n-\tconst char *argv[5];\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tint code;\n+\tint keepalive_active = 0;\n \n-\targv[0] = find_hook(\"update\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tif (keepalive_in_sec <= 0)\n+\t\tuse_keepalive = KEEPALIVE_NEVER;\n+\tif (use_keepalive == KEEPALIVE_ALWAYS)\n+\t\tkeepalive_active = 1;\n \n-\targv[1] = cmd->ref_name;\n-\targv[2] = oid_to_hex(&cmd->old_oid);\n-\targv[3] = oid_to_hex(&cmd->new_oid);\n-\targv[4] = NULL;\n+\t/* send a keepalive if there is no data to write */\n+\tif (keepalive_active && !output->len) {\n+\t\tstatic const char buf[] = \"0005\\1\";\n+\t\twrite_or_die(1, buf, sizeof(buf) - 1);\n+\t\treturn;\n+\t}\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.argv = argv;\n-\tproc.trace2_hook_name = \"update\";\n+\tif (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {\n+\t\tconst char *first_null = memchr(output->buf, '\\0', output->len);\n+\t\tif (first_null) {\n+\t\t\t/* The null bit is excluded. */\n+\t\t\tsize_t before_null = first_null - output->buf;\n+\t\t\tsize_t after_null = output->len - (before_null + 1);\n+\t\t\tkeepalive_active = 1;\n+\t\t\tsend_sideband(1, 2, output->buf, before_null, use_sideband);\n+\t\t\tsend_sideband(1, 2, first_null + 1, after_null, use_sideband);\n+\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n+}\n+\n+static int run_update_hook(struct command *cmd)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint code;\n+\n+\tstrvec_pushl(&opt.args,\n+\t\t     cmd->ref_name,\n+\t\t     oid_to_hex(&cmd->old_oid),\n+\t\t     oid_to_hex(&cmd->new_oid),\n+\t\t     NULL);\n \n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n \tif (use_sideband)\n-\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\treturn finish_command(&proc);\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\tcode = run_hooks(\"update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn code;\n }\n \n static struct command *find_command_by_refname(struct command *list,\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425795","messageId":"patch-29.31-3466f17af08-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 29/31] receive-pack: convert receive hooks to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:31Z","receivedAt":"2021-05-28T12:13:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using the hook.h library to run receive hooks we get closer to\ndeleting the hook functions in run-command.c\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 197 +++++++++++++++++++----------------------\n 1 file changed, 90 insertions(+), 107 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex b2ccdb66daa..ec90e10477a 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -748,7 +748,7 @@ static int check_cert_push_options(const struct string_list *push_options)\n \treturn retval;\n }\n \n-static void prepare_push_cert_sha1(struct child_process *proc)\n+static void prepare_push_cert_sha1(struct run_hooks_opt *opt)\n {\n \tstatic int already_done;\n \n@@ -772,110 +772,42 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n \t}\n \tif (!is_null_oid(&push_cert_oid)) {\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT=%s\",\n \t\t\t     oid_to_hex(&push_cert_oid));\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_SIGNER=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_SIGNER=%s\",\n \t\t\t     sigcheck.signer ? sigcheck.signer : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_KEY=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_KEY=%s\",\n \t\t\t     sigcheck.key ? sigcheck.key : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_STATUS=%c\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_STATUS=%c\",\n \t\t\t     sigcheck.result);\n \t\tif (push_cert_nonce) {\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE=%s\",\n \t\t\t\t     push_cert_nonce);\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE_STATUS=%s\",\n \t\t\t\t     nonce_status);\n \t\t\tif (nonce_status == NONCE_SLOP)\n-\t\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t\t     \"GIT_PUSH_CERT_NONCE_SLOP=%ld\",\n \t\t\t\t\t     nonce_stamp_slop);\n \t\t}\n \t}\n }\n \n+struct receive_hook_feed_context {\n+\tstruct command *cmd;\n+\tint skip_broken;\n+};\n+\n struct receive_hook_feed_state {\n \tstruct command *cmd;\n \tstruct ref_push_report *report;\n \tint skip_broken;\n \tstruct strbuf buf;\n-\tconst struct string_list *push_options;\n };\n \n-typedef int (*feed_fn)(void *, const char **, size_t *);\n-static int run_and_feed_hook(const char *hook_name, feed_fn feed,\n-\t\t\t     struct receive_hook_feed_state *feed_state)\n-{\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct async muxer;\n-\tconst char *argv[2];\n-\tint code;\n-\n-\targv[0] = find_hook(hook_name);\n-\tif (!argv[0])\n-\t\treturn 0;\n-\n-\targv[1] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = hook_name;\n-\n-\tif (feed_state->push_options) {\n-\t\tint i;\n-\t\tfor (i = 0; i < feed_state->push_options->nr; i++)\n-\t\t\tstrvec_pushf(&proc.env_array,\n-\t\t\t\t     \"GIT_PUSH_OPTION_%d=%s\", i,\n-\t\t\t\t     feed_state->push_options->items[i].string);\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT=%d\",\n-\t\t\t     feed_state->push_options->nr);\n-\t} else\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT\");\n-\n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&proc.env_array, tmp_objdir_env(tmp_objdir));\n-\n-\tif (use_sideband) {\n-\t\tmemset(&muxer, 0, sizeof(muxer));\n-\t\tmuxer.proc = copy_to_sideband;\n-\t\tmuxer.in = -1;\n-\t\tcode = start_async(&muxer);\n-\t\tif (code)\n-\t\t\treturn code;\n-\t\tproc.err = muxer.in;\n-\t}\n-\n-\tprepare_push_cert_sha1(&proc);\n-\n-\tcode = start_command(&proc);\n-\tif (code) {\n-\t\tif (use_sideband)\n-\t\t\tfinish_async(&muxer);\n-\t\treturn code;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\n-\twhile (1) {\n-\t\tconst char *buf;\n-\t\tsize_t n;\n-\t\tif (feed(feed_state, &buf, &n))\n-\t\t\tbreak;\n-\t\tif (write_in_full(proc.in, buf, n) < 0)\n-\t\t\tbreak;\n-\t}\n-\tclose(proc.in);\n-\tif (use_sideband)\n-\t\tfinish_async(&muxer);\n-\n-\tsigchain_pop(SIGPIPE);\n-\n-\treturn finish_command(&proc);\n-}\n-\n-static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n+static int feed_receive_hook(void *state_)\n {\n \tstruct receive_hook_feed_state *state = state_;\n \tstruct command *cmd = state->cmd;\n@@ -884,9 +816,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t       state->skip_broken && (cmd->error_string || cmd->did_not_exist))\n \t\tcmd = cmd->next;\n \tif (!cmd)\n-\t\treturn -1; /* EOF */\n-\tif (!bufp)\n-\t\treturn 0; /* OK, can feed something. */\n+\t\treturn 1; /* EOF - close the pipe*/\n \tstrbuf_reset(&state->buf);\n \tif (!state->report)\n \t\tstate->report = cmd->report;\n@@ -910,32 +840,36 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t\t\t    cmd->ref_name);\n \t\tstate->cmd = cmd->next;\n \t}\n-\tif (bufp) {\n-\t\t*bufp = state->buf.buf;\n-\t\t*sizep = state->buf.len;\n-\t}\n \treturn 0;\n }\n \n-static int run_receive_hook(struct command *commands,\n-\t\t\t    const char *hook_name,\n-\t\t\t    int skip_broken,\n-\t\t\t    const struct string_list *push_options)\n+static int feed_receive_hook_cb(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n {\n-\tstruct receive_hook_feed_state state;\n-\tint status;\n-\n-\tstrbuf_init(&state.buf, 0);\n-\tstate.cmd = commands;\n-\tstate.skip_broken = skip_broken;\n-\tstate.report = NULL;\n-\tif (feed_receive_hook(&state, NULL, NULL))\n-\t\treturn 0;\n-\tstate.cmd = commands;\n-\tstate.push_options = push_options;\n-\tstatus = run_and_feed_hook(hook_name, feed_receive_hook, &state);\n-\tstrbuf_release(&state.buf);\n-\treturn status;\n+\tstruct hook *hook = pp_task_cb;\n+\tstruct receive_hook_feed_state *feed_state = hook->feed_pipe_cb_data;\n+\tint rc;\n+\n+\t/* first-time setup */\n+\tif (!feed_state) {\n+\t\tstruct hook_cb_data *hook_cb = pp_cb;\n+\t\tstruct run_hooks_opt *opt = hook_cb->options;\n+\t\tstruct receive_hook_feed_context *ctx = opt->feed_pipe_ctx;\n+\t\tif (!ctx)\n+\t\t\tBUG(\"run_hooks_opt.feed_pipe_ctx required for receive hook\");\n+\n+\t\tfeed_state = xmalloc(sizeof(struct receive_hook_feed_state));\n+\t\tstrbuf_init(&feed_state->buf, 0);\n+\t\tfeed_state->cmd = ctx->cmd;\n+\t\tfeed_state->skip_broken = ctx->skip_broken;\n+\t\tfeed_state->report = NULL;\n+\n+\t\thook->feed_pipe_cb_data = feed_state;\n+\t}\n+\n+\trc = feed_receive_hook(feed_state);\n+\tif (!rc)\n+\t\tstrbuf_addbuf(pipe, &feed_state->buf);\n+\treturn rc;\n }\n \n static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n@@ -971,6 +905,55 @@ static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n \tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n }\n \n+static int run_receive_hook(struct command *commands,\n+\t\t\t    const char *hook_name,\n+\t\t\t    int skip_broken,\n+\t\t\t    const struct string_list *push_options)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct receive_hook_feed_context ctx;\n+\tint rc;\n+\tstruct command *iter = commands;\n+\n+\t/* if there are no valid commands, don't invoke the hook at all. */\n+\twhile (iter && skip_broken && (iter->error_string || iter->did_not_exist))\n+\t\titer = iter->next;\n+\tif (!iter)\n+\t\treturn 0;\n+\n+\t/* pre-receive hooks should run in series as the hook updates refs */\n+\tif (!strcmp(hook_name, \"pre-receive\"))\n+\t\topt.jobs = 1;\n+\n+\tif (push_options) {\n+\t\tint i;\n+\t\tfor (i = 0; i < push_options->nr; i++)\n+\t\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_%d=%s\", i,\n+\t\t\t\t     push_options->items[i].string);\n+\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_COUNT=%d\", push_options->nr);\n+\t} else\n+\t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n+\n+\tif (tmp_objdir)\n+\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\n+\tprepare_push_cert_sha1(&opt);\n+\n+\t/* set up sideband printer */\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\t/* set up stdin callback */\n+\tctx.cmd = commands;\n+\tctx.skip_broken = skip_broken;\n+\topt.feed_pipe = feed_receive_hook_cb;\n+\topt.feed_pipe_ctx = &ctx;\n+\n+\trc = run_hooks(hook_name, &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n+}\n+\n static int run_update_hook(struct command *cmd)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425796","messageId":"patch-30.31-d93bdc0c294-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 30/31] hooks: fix a TOCTOU in \"did we run a hook?\" heuristic","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:32Z","receivedAt":"2021-05-28T12:13:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a race in code added in 680ee550d72 (commit: skip discarding the\nindex if there is no pre-commit hook, 2017-08-14) by changing the\nhook.c API to optionally indicate whether or not the requested hook\nran or not. This was suggested in the discussion around\n680ee550d72[1].\n\nLet's also change this for the pre-merge-commit hook, see\n6098817fd7f (git-merge: honor pre-merge-commit hook, 2019-08-07) for\nthe introduction of the previous behavior.\n\nLet's also change this for the push-to-checkout hook. Now instead of\nchecking if the hook exists and either doing a push to checkout or a\npush to deploy we'll always attempt a push to checkout. If the hook\ndoesn't exist we'll fall back on push to deploy. The same behavior as\nbefore, without the TOCTOU race. See 0855331941b (receive-pack:\nsupport push-to-checkout hook, 2014-12-01) for the introduction of the\nprevious behavior.\n\nThis leaves uses of hook_exists() in two places that matter. The\n\"reference-transaction\" check in refs.c, see 67541597670 (refs:\nimplement reference transaction hook, 2020-06-19), and the\nprepare-commit-msg hook, see 66618a50f9c (sequencer: run\n'prepare-commit-msg' hook, 2018-01-24).\n\nIn both of those cases we're saving ourselves CPU time by not\npreparing data for the hook that we'll then do nothing with if we\ndon't have the hook, so using this \"invoked_hook\" pattern doesn't make\nsense there purely for optimization purposes.\n\nMore importantly, in those cases the worst we'll do is miss that we\n\"should\" run the hook because a new hook appeared, whereas in the\npre-commit and pre-merge-commit cases we'll skip an important\ndiscard_cache() on the bases of our faulty guess.\n\nI do think none of these races really matter in practice. It would be\nsome one-off issue as a hook was added or removed. I did think it was\nstupid that we didn't pass a \"did this run?\" flag instead of doing\nthis guessing at a distance though, so now we're not guessing anymore.\n\n1. https://lore.kernel.org/git/20170810191613.kpmhzg4seyxy3cpq@sigill.intra.peff.net/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c       | 18 +++++++++++-------\n builtin/merge.c        | 16 ++++++++++------\n builtin/receive-pack.c |  8 +++++---\n commit.c               |  1 +\n commit.h               |  3 ++-\n hook.c                 |  4 ++++\n hook.h                 | 10 ++++++++++\n sequencer.c            |  4 ++--\n 8 files changed, 45 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex dad4e565443..a66727a612a 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -725,11 +725,13 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \tint clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);\n \tint old_display_comment_prefix;\n \tint merge_contains_scissors = 0;\n+\tint invoked_hook = 0;\n \n \t/* This checks and barfs if author is badly specified */\n \tdetermine_author_info(author_ident);\n \n-\tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(use_editor, index_file, &invoked_hook,\n+\t\t\t\t\t  \"pre-commit\", NULL))\n \t\treturn 0;\n \n \tif (squash_message) {\n@@ -1045,10 +1047,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && hook_exists(\"pre-commit\")) {\n+\tif (!no_verify && invoked_hook) {\n \t\t/*\n-\t\t * Re-read the index as pre-commit hook could have updated it,\n-\t\t * and write it out as a tree.  We must do this before we invoke\n+\t\t * Re-read the index as the pre-commit-commit hook was invoked\n+\t\t * and could have updated it. We must do this before we invoke\n \t\t * the editor and after we invoke run_status above.\n \t\t */\n \t\tdiscard_cache();\n@@ -1060,7 +1062,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n+\tif (run_commit_hook(use_editor, index_file, NULL, \"prepare-commit-msg\",\n \t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n \t\treturn 0;\n \n@@ -1077,7 +1079,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t}\n \n \tif (!no_verify &&\n-\t    run_commit_hook(use_editor, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n+\t    run_commit_hook(use_editor, index_file, NULL, \"commit-msg\",\n+\t\t\t    git_path_commit_editmsg(), NULL)) {\n \t\treturn 0;\n \t}\n \n@@ -1830,7 +1833,8 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \n \trepo_rerere(the_repository, 0);\n \trun_auto_maintenance(quiet);\n-\trun_commit_hook(use_editor, get_index_file(), \"post-commit\", NULL);\n+\trun_commit_hook(use_editor, get_index_file(), NULL, \"post-commit\",\n+\t\t\tNULL);\n \tif (amend && !no_post_rewrite) {\n \t\tcommit_post_rewrite(the_repository, current_head, &oid);\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex a9363b94065..2bbdfd6080d 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -844,15 +844,18 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n {\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *index_file = get_index_file();\n+\tint invoked_hook = 0;\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, index_file, \"pre-merge-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(0 < option_edit, index_file,\n+\t\t\t\t\t  &invoked_hook, \"pre-merge-commit\",\n+\t\t\t\t\t  NULL))\n \t\tabort_commit(remoteheads, NULL);\n \t/*\n-\t * Re-read the index as pre-merge-commit hook could have updated it,\n-\t * and write it out as a tree.  We must do this before we invoke\n+\t * Re-read the index as the pre-merge-commit hook was invoked\n+\t * and could have updated it. We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (hook_exists(\"pre-merge-commit\"))\n+\tif (invoked_hook)\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n@@ -873,7 +876,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tappend_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);\n \twrite_merge_heads(remoteheads);\n \twrite_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);\n-\tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n+\tif (run_commit_hook(0 < option_edit, get_index_file(), NULL,\n+\t\t\t    \"prepare-commit-msg\",\n \t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \tif (0 < option_edit) {\n@@ -882,7 +886,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t}\n \n \tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n-\t\t\t\t\t  \"commit-msg\",\n+\t\t\t\t\t  NULL, \"commit-msg\",\n \t\t\t\t\t  git_path_merge_msg(the_repository), NULL))\n \t\tabort_commit(remoteheads, NULL);\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ec90e10477a..cd658f41d58 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1439,10 +1439,12 @@ static const char *push_to_deploy(unsigned char *sha1,\n static const char *push_to_checkout_hook = \"push-to-checkout\";\n \n static const char *push_to_checkout(unsigned char *hash,\n+\t\t\t\t    int *invoked_hook,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\topt.invoked_hook = invoked_hook;\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n \tstrvec_pushv(&opt.env, env->v);\n@@ -1460,6 +1462,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n {\n \tconst char *retval, *work_tree, *git_dir = NULL;\n \tstruct strvec env = STRVEC_INIT;\n+\tint invoked_hook = 0;\n \n \tif (worktree && worktree->path)\n \t\twork_tree = worktree->path;\n@@ -1477,10 +1480,9 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!hook_exists(push_to_checkout_hook))\n+\tretval = push_to_checkout(sha1, &invoked_hook, &env, work_tree);\n+\tif (!invoked_hook)\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n-\telse\n-\t\tretval = push_to_checkout(sha1, &env, work_tree);\n \n \tstrvec_clear(&env);\n \treturn retval;\ndiff --git a/commit.c b/commit.c\nindex e8147a88fc6..cf62ebceae5 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1697,6 +1697,7 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n }\n \n int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook,\n \t\t    const char *name, ...)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\ndiff --git a/commit.h b/commit.h\nindex df42eb434f3..b5a542993c6 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -363,7 +363,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\n \n LAST_ARG_MUST_BE_NULL\n-int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);\n+int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook, const char *name, ...);\n \n /* Sign a commit or tag buffer, storing the result in a header. */\n int sign_with_header(struct strbuf *buf, const char *keyid);\ndiff --git a/hook.c b/hook.c\nindex 17ae65eca31..3cf51460279 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -138,6 +138,9 @@ static int notify_hook_finished(int result,\n \t/* |= rc in cb */\n \thook_cb->rc |= result;\n \n+\tif (hook_cb->invoked_hook)\n+\t\t*hook_cb->invoked_hook = 1;\n+\n \treturn 1;\n }\n \n@@ -152,6 +155,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t.rc = 0,\n \t\t.hook_name = hook_name,\n \t\t.options = options,\n+\t\t.invoked_hook = options->invoked_hook,\n \t};\n \tif (options->absolute_path) {\n \t\tstrbuf_add_absolute_path(&abs_path, hook_path);\ndiff --git a/hook.h b/hook.h\nindex 5f895032341..9d163e6f992 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -57,6 +57,15 @@ struct run_hooks_opt\n \t * for an example.\n \t */\n \tconsume_sideband_fn consume_sideband;\n+\n+\t/*\n+\t * A pointer which if provided will be set to 1 or 0 depending\n+\t * on if a hook was invoked (i.e. existed), regardless of\n+\t * whether or not that was successful. Used for avoiding\n+\t * TOCTOU races in code that would otherwise call hook_exist()\n+\t * after a \"maybe hook run\" to see if a hook was invoked.\n+\t */\n+\tint *invoked_hook;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -81,6 +90,7 @@ struct hook_cb_data {\n \tconst char *hook_name;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n+\tint *invoked_hook;\n };\n \n /*\ndiff --git a/sequencer.c b/sequencer.c\nindex ec2761e47d9..2440b9dccd8 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1204,7 +1204,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n \t} else {\n \t\targ1 = \"message\";\n \t}\n-\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n+\tif (run_commit_hook(0, r->index_file, NULL, \"prepare-commit-msg\", name,\n \t\t\t    arg1, arg2, NULL))\n \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n \n@@ -1534,7 +1534,7 @@ static int try_to_commit(struct repository *r,\n \t\tgoto out;\n \t}\n \n-\trun_commit_hook(0, r->index_file, \"post-commit\", NULL);\n+\trun_commit_hook(0, r->index_file, NULL, \"post-commit\", NULL);\n \tif (flags & AMEND_MSG)\n \t\tcommit_post_rewrite(r, current_head, oid);\n \n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425797","messageId":"patch-31.31-896956250f6-20210528T110515Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH 31/31] hook-list.h: add a generated list of hooks, like config-list.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T12:11:33Z","receivedAt":"2021-05-28T12:13:58Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Resolve a long-standing TODO item in bugreport.c of there being no\ncentralized listing of hooks, this fixes a bug with the bugreport\nlisting only knowing about 1/4 of the p4 hooks. It didn't know about\nthe \"reference-transaction\" hook either.\n\nWe can now make sure this is kept up-to-date, as the hook.c library\nwill die if asked to find a hook we don't know about yet. The only\n(undocumented) exception is the artificial \"test-hook\" used in our own\ntest suite. Move some of the tests away from the \"does-not-exist\"\npseudo-hook, and test for the new behavior.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n .gitignore           |  1 +\n Makefile             | 14 +++++++++++---\n builtin/bugreport.c  | 44 ++++++++------------------------------------\n generate-hooklist.sh | 20 ++++++++++++++++++++\n hook.c               | 22 ++++++++++++++++++++++\n t/t1800-hook.sh      | 14 +++++++++++---\n 6 files changed, 73 insertions(+), 42 deletions(-)\n create mode 100755 generate-hooklist.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex de39dc9961b..66189ca3cdc 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -191,6 +191,7 @@\n /gitweb/static/gitweb.min.*\n /config-list.h\n /command-list.h\n+/hook-list.h\n *.tar.gz\n *.dsc\n *.deb\ndiff --git a/Makefile b/Makefile\nindex a6b71a0fbed..d0532f3c744 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -817,6 +817,7 @@ XDIFF_LIB = xdiff/lib.a\n \n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n+GENERATED_H += hook-list.h\n \n LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentation/' 2>/dev/null || \\\n \t$(FIND) . \\\n@@ -2207,7 +2208,9 @@ git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS) $(GITLIBS)\n \n help.sp help.s help.o: command-list.h\n \n-builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX\n+hook.sp hook.s hook.o: hook-list.h\n+\n+builtin/help.sp builtin/help.s builtin/help.o: config-list.h hook-list.h GIT-PREFIX\n builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \\\n \t'-DGIT_HTML_PATH=\"$(htmldir_relative_SQ)\"' \\\n \t'-DGIT_MAN_PATH=\"$(mandir_relative_SQ)\"' \\\n@@ -2240,6 +2243,11 @@ command-list.h: $(wildcard Documentation/git*.txt)\n \t\t$(patsubst %,--exclude-program %,$(EXCLUDED_PROGRAMS)) \\\n \t\tcommand-list.txt >$@+ && mv $@+ $@\n \n+hook-list.h: generate-hooklist.sh\n+hook-list.h: Documentation/githooks.txt\n+\t$(QUIET_GEN)$(SHELL_PATH) ./generate-hooklist.sh \\\n+\t\t>$@+ && mv $@+ $@\n+\n SCRIPT_DEFINES = $(SHELL_PATH_SQ):$(DIFF_SQ):$(GIT_VERSION):\\\n \t$(localedir_SQ):$(NO_CURL):$(USE_GETTEXT_SCHEME):$(SANE_TOOL_PATH_SQ):\\\n \t$(gitwebdir_SQ):$(PERL_PATH_SQ):$(SANE_TEXT_GREP):$(PAGER_ENV):\\\n@@ -2890,7 +2898,7 @@ $(SP_OBJ): %.sp: %.c GIT-CFLAGS FORCE\n .PHONY: sparse $(SP_OBJ)\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := command-list.h config-list.h unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := command-list.h config-list.h hook-list.h unicode-width.h compat/% xdiff/%\n ifndef GCRYPT_SHA256\n \tEXCEPT_HDRS += sha256/gcrypt.h\n endif\n@@ -2912,7 +2920,7 @@ hdr-check: $(HCO)\n style:\n \tgit clang-format --style file --diff --extensions c,h\n \n-check: config-list.h command-list.h\n+check: config-list.h command-list.h hook-list.h\n \t@if sparse; \\\n \tthen \\\n \t\techo >&2 \"Use 'make sparse' instead\"; \\\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 941c8d5e270..a7a1fcb8a7a 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -4,6 +4,7 @@\n #include \"help.h\"\n #include \"compat/compiler.h\"\n #include \"hook.h\"\n+#include \"hook-list.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\n@@ -41,39 +42,7 @@ static void get_system_info(struct strbuf *sys_info)\n \n static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n {\n-\t/*\n-\t * NEEDSWORK: Doesn't look like there is a list of all possible hooks;\n-\t * so below is a transcription of `git help hooks`. Later, this should\n-\t * be replaced with some programmatically generated list (generated from\n-\t * doc or else taken from some library which tells us about all the\n-\t * hooks)\n-\t */\n-\tstatic const char *hook[] = {\n-\t\t\"applypatch-msg\",\n-\t\t\"pre-applypatch\",\n-\t\t\"post-applypatch\",\n-\t\t\"pre-commit\",\n-\t\t\"pre-merge-commit\",\n-\t\t\"prepare-commit-msg\",\n-\t\t\"commit-msg\",\n-\t\t\"post-commit\",\n-\t\t\"pre-rebase\",\n-\t\t\"post-checkout\",\n-\t\t\"post-merge\",\n-\t\t\"pre-push\",\n-\t\t\"pre-receive\",\n-\t\t\"update\",\n-\t\t\"post-receive\",\n-\t\t\"post-update\",\n-\t\t\"push-to-checkout\",\n-\t\t\"pre-auto-gc\",\n-\t\t\"post-rewrite\",\n-\t\t\"sendemail-validate\",\n-\t\t\"fsmonitor-watchman\",\n-\t\t\"p4-pre-submit\",\n-\t\t\"post-index-change\",\n-\t};\n-\tint i;\n+\tconst char **p;\n \n \tif (nongit) {\n \t\tstrbuf_addstr(hook_info,\n@@ -81,9 +50,12 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t\treturn;\n \t}\n \n-\tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (hook_exists(hook[i]))\n-\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n+\tfor (p = hook_name_list; *p; p++) {\n+\t\tconst char *hook = *p;\n+\n+\t\tif (hook_exists(hook))\n+\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook);\n+\t}\n }\n \n static const char * const bugreport_usage[] = {\ndiff --git a/generate-hooklist.sh b/generate-hooklist.sh\nnew file mode 100755\nindex 00000000000..25a7207b276\n--- /dev/null\n+++ b/generate-hooklist.sh\n@@ -0,0 +1,20 @@\n+#!/bin/sh\n+\n+echo \"/* Automatically generated by generate-hooklist.sh */\"\n+\n+print_hook_list () {\n+\tcat <<EOF\n+static const char *hook_name_list[] = {\n+EOF\n+\tgrep -C1 -h '^~~~' Documentation/githooks.txt |\n+\tgrep '^[a-z0-9][a-z0-9-]*$' |\n+\tsort |\n+\tsed 's/^.*$/\t\"&\",/'\n+\tcat <<EOF\n+\tNULL,\n+};\n+EOF\n+}\n+\n+echo\n+print_hook_list\ndiff --git a/hook.c b/hook.c\nindex 3cf51460279..68e8c1ba868 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -1,11 +1,33 @@\n #include \"cache.h\"\n #include \"hook.h\"\n #include \"run-command.h\"\n+#include \"hook-list.h\"\n+\n+static int known_hook(const char *name)\n+{\n+\tconst char **p;\n+\tsize_t len = strlen(name);\n+\tfor (p = hook_name_list; *p; p++) {\n+\t\tconst char *hook = *p;\n+\n+\t\tif (!strncmp(name, hook, len) && hook[len] == '\\0')\n+\t\t\treturn 1;\n+\t}\n+\tif (!strcmp(name, \"test-hook\") ||\n+\t    !strcmp(name, \"does-not-exist\"))\n+\t\treturn 1;\n+\n+\treturn 0;\n+}\n \n const char *find_hook(const char *name)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \n+\tif (!known_hook(name))\n+\t\tdie(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n+\t\t    name);\n+\n \tstrbuf_reset(&path);\n \tstrbuf_git_path(&path, \"hooks/%s\", name);\n \tif (access(path.buf, X_OK) < 0) {\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 9e2dd64275c..80e985a81bd 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -10,17 +10,25 @@ test_expect_success 'setup .git/hooks' '\n \n test_expect_success 'git hook run -- nonexistent hook' '\n \tcat >stderr.expect <<-\\EOF &&\n-\terror: cannot find a hook named does-not-exist\n+\terror: cannot find a hook named test-hook\n \tEOF\n-\ttest_expect_code 1 git hook run does-not-exist 2>stderr.actual &&\n+\ttest_expect_code 1 git hook run test-hook 2>stderr.actual &&\n \ttest_cmp stderr.expect stderr.actual\n '\n \n test_expect_success 'git hook run -- nonexistent hook with --ignore-missing' '\n-\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n+\tgit hook run --ignore-missing test-hook 2>stderr.actual &&\n \ttest_must_be_empty stderr.actual\n '\n \n+test_expect_success 'git hook run -- nonexistent hook with --ignore-missing' '\n+\tcat >stderr.expect <<-\\EOF &&\n+\tfatal: the hook '\"'\"'unknown-hook'\"'\"' is not known to git, should be in hook-list.h via githooks(5)\n+\tEOF\n+\ttest_expect_code 128 git hook run unknown-hook 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual\n+'\n+\n test_expect_success 'git hook run -- basic' '\n \twrite_script .git/hooks/test-hook <<-EOF &&\n \techo Test hook\n-- \n2.32.0.rc1.458.gd885d4f985c\n\n"},{"id":"425799","messageId":"b5c0a071-a073-7153-a623-75642a79942e@gmail.com","threadId":"55286","inReplyTo":"patch-30.31-d93bdc0c294-20210528T110515Z-avarab@gmail.com","subject":"Re: [PATCH 30/31] hooks: fix a TOCTOU in \"did we run a hook?\" heuristic","fromName":"Bagas Sanjaya","fromEmail":"bagasdotme@gmail.com","sentAt":"2021-05-28T12:48:06Z","receivedAt":"2021-05-28T12:48:17Z","isPatch":true,"sender":{"key":"bagasdotme@gmail.com","avatar":"https://avatars.githubusercontent.com/u/40219486?v=4"},"body":"On 28/05/21 19.11, Ævar Arnfjörð Bjarmason wrote:\n> Let's also change this for the push-to-checkout hook. Now instead of\n> checking if the hook exists and either doing a push to checkout or a\n> push to deploy we'll always attempt a push to checkout. If the hook\n> doesn't exist we'll fall back on push to deploy. The same behavior as\n> before, without the TOCTOU race. See 0855331941b (receive-pack:\n> support push-to-checkout hook, 2014-12-01) for the introduction of the\n> previous behavior.\n\nWhat is TOCTOU? I never hear this term before.\n\n-- \nAn old man doll... just what I always wanted! - Clara\n"},{"id":"425802","messageId":"87lf7z2b88.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"b5c0a071-a073-7153-a623-75642a79942e@gmail.com","subject":"Re: [PATCH 30/31] hooks: fix a TOCTOU in \"did we run a hook?\" heuristic","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-05-28T14:11:21Z","receivedAt":"2021-05-28T14:18:28Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, May 28 2021, Bagas Sanjaya wrote:\n\n> On 28/05/21 19.11, Ævar Arnfjörð Bjarmason wrote:\n>> Let's also change this for the push-to-checkout hook. Now instead of\n>> checking if the hook exists and either doing a push to checkout or a\n>> push to deploy we'll always attempt a push to checkout. If the hook\n>> doesn't exist we'll fall back on push to deploy. The same behavior as\n>> before, without the TOCTOU race. See 0855331941b (receive-pack:\n>> support push-to-checkout hook, 2014-12-01) for the introduction of the\n>> previous behavior.\n>\n> What is TOCTOU? I never hear this term before.\n\nA common race condition[1], time of use, time of check. I.e. in this\ncase we use the hook first, and then later we check if there's a\nhook. Thus we've got a logic error where we're assuming we'll get the\nsame answer[2] in both instances.\n\nIn this case what we plainly want is to just save away whether we did X,\nnot do X, and then later check if we can do X to see if we did X\nearlier.\n\nI think this whole thing is still suspect even with my fix, there's an\ninherent assumption here of a single-thread view of the world. I.e. that\nyou have a user running a git command at a time, and that while our hook\nrun didn't change the index, a background process might have.\n\nBut this probably handles the most common case, i.e. a hook explicitly\nchanging the index and wanting its invoker to notice, and in any case we\nhave the single-thread assumption already, so we might as well make it\nless buggy/racy.\n\nIf you dig through the history we used to do this without the check for\n\"earlier X?\" before for at least one of the hooks, but it got changed to\nthe current state as an optimization.\n\n1. https://en.wikipedia.org/wiki/TOCTOU\n2. I mean, I don't think any programmer assumed it in this case, it was\n   just intentional lazyness or a legitimate trade-off, but now that\n   it's easy to eliminate the race...\n"},{"id":"426054","messageId":"60b5374298413_312ef208a0@natae.notmuch","threadId":"55286","inReplyTo":"87fsy7lqve.fsf@evledraar.gmail.com","subject":"Re: [PATCH v9 22/37] read-cache: convert post-index-change hook to use config","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-05-31T19:21:38Z","receivedAt":"2021-05-31T19:21:49Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> \n> On Wed, May 26 2021, Emily Shaffer wrote:\n> \n> >  Part of the linkgit:git[1] suite\n> > diff --git a/read-cache.c b/read-cache.c\n> > index 1b3c2eb408..6a5c9403f4 100644\n> > --- a/read-cache.c\n> > +++ b/read-cache.c\n> > @@ -26,6 +26,8 @@\n> >  #include \"thread-utils.h\"\n> >  #include \"progress.h\"\n> >  #include \"sparse-index.h\"\n> > +#include \"hook.h\"\n> > +>>>>>>> 9524a9d29d (read-cache: convert post-index-change hook to use config)\n> \n> This adds a conflict marker, which is removed later in the series.\n> \n> Obviously a trivial mistake, but it's a good idea to use git rebase -i\n> -x 'make test' or equivalent for such a large series, perhaps there are\n> other inter-patch issues lurking here...\n\nAhh, thanks so much for this. I've often wanted to do this and sometimes\nwrote simple scripts. It's good to know there a straightforward way from\nstandard commands.\n\nThat way I have less excuse not to do the check... and I just found a\ncouple of transitory issues on my latest patch series.\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"426089","messageId":"87mtsa1jq7.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"patch-31.31-896956250f6-20210528T110515Z-avarab@gmail.com","subject":"Re: [PATCH 31/31] hook-list.h: add a generated list of hooks, like config-list.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-01T01:00:04Z","receivedAt":"2021-06-01T01:01:43Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, May 28 2021, Ævar Arnfjörð Bjarmason wrote:\n\n> Resolve a long-standing TODO item in bugreport.c of there being no\n> centralized listing of hooks, this fixes a bug with the bugreport\n> listing only knowing about 1/4 of the p4 hooks. It didn't know about\n> the \"reference-transaction\" hook either.\n>\n> We can now make sure this is kept up-to-date, as the hook.c library\n> will die if asked to find a hook we don't know about yet. The only\n> (undocumented) exception is the artificial \"test-hook\" used in our own\n> test suite. Move some of the tests away from the \"does-not-exist\"\n> pseudo-hook, and test for the new behavior.\n>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  .gitignore           |  1 +\n>  Makefile             | 14 +++++++++++---\n>  builtin/bugreport.c  | 44 ++++++++------------------------------------\n>  generate-hooklist.sh | 20 ++++++++++++++++++++\n>  hook.c               | 22 ++++++++++++++++++++++\n>  t/t1800-hook.sh      | 14 +++++++++++---\n>  6 files changed, 73 insertions(+), 42 deletions(-)\n>  create mode 100755 generate-hooklist.sh\n>\n> diff --git a/.gitignore b/.gitignore\n> index de39dc9961b..66189ca3cdc 100644\n> --- a/.gitignore\n> +++ b/.gitignore\n> @@ -191,6 +191,7 @@\n>  /gitweb/static/gitweb.min.*\n>  /config-list.h\n>  /command-list.h\n> +/hook-list.h\n>  *.tar.gz\n>  *.dsc\n>  *.deb\n> diff --git a/Makefile b/Makefile\n> index a6b71a0fbed..d0532f3c744 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -817,6 +817,7 @@ XDIFF_LIB = xdiff/lib.a\n>  \n>  GENERATED_H += command-list.h\n>  GENERATED_H += config-list.h\n> +GENERATED_H += hook-list.h\n>  \n>  LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentation/' 2>/dev/null || \\\n>  \t$(FIND) . \\\n> @@ -2207,7 +2208,9 @@ git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS) $(GITLIBS)\n>  \n>  help.sp help.s help.o: command-list.h\n>  \n> -builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX\n> +hook.sp hook.s hook.o: hook-list.h\n> +\n> +builtin/help.sp builtin/help.s builtin/help.o: config-list.h hook-list.h GIT-PREFIX\n>  builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \\\n>  \t'-DGIT_HTML_PATH=\"$(htmldir_relative_SQ)\"' \\\n>  \t'-DGIT_MAN_PATH=\"$(mandir_relative_SQ)\"' \\\n> @@ -2240,6 +2243,11 @@ command-list.h: $(wildcard Documentation/git*.txt)\n>  \t\t$(patsubst %,--exclude-program %,$(EXCLUDED_PROGRAMS)) \\\n>  \t\tcommand-list.txt >$@+ && mv $@+ $@\n>  \n> +hook-list.h: generate-hooklist.sh\n> +hook-list.h: Documentation/githooks.txt\n> +\t$(QUIET_GEN)$(SHELL_PATH) ./generate-hooklist.sh \\\n> +\t\t>$@+ && mv $@+ $@\n> +\n>  SCRIPT_DEFINES = $(SHELL_PATH_SQ):$(DIFF_SQ):$(GIT_VERSION):\\\n>  \t$(localedir_SQ):$(NO_CURL):$(USE_GETTEXT_SCHEME):$(SANE_TOOL_PATH_SQ):\\\n>  \t$(gitwebdir_SQ):$(PERL_PATH_SQ):$(SANE_TEXT_GREP):$(PAGER_ENV):\\\n> @@ -2890,7 +2898,7 @@ $(SP_OBJ): %.sp: %.c GIT-CFLAGS FORCE\n>  .PHONY: sparse $(SP_OBJ)\n>  sparse: $(SP_OBJ)\n>  \n> -EXCEPT_HDRS := command-list.h config-list.h unicode-width.h compat/% xdiff/%\n> +EXCEPT_HDRS := command-list.h config-list.h hook-list.h unicode-width.h compat/% xdiff/%\n>  ifndef GCRYPT_SHA256\n>  \tEXCEPT_HDRS += sha256/gcrypt.h\n>  endif\n> @@ -2912,7 +2920,7 @@ hdr-check: $(HCO)\n>  style:\n>  \tgit clang-format --style file --diff --extensions c,h\n>  \n> -check: config-list.h command-list.h\n> +check: config-list.h command-list.h hook-list.h\n>  \t@if sparse; \\\n>  \tthen \\\n>  \t\techo >&2 \"Use 'make sparse' instead\"; \\\n> diff --git a/builtin/bugreport.c b/builtin/bugreport.c\n> index 941c8d5e270..a7a1fcb8a7a 100644\n> --- a/builtin/bugreport.c\n> +++ b/builtin/bugreport.c\n> @@ -4,6 +4,7 @@\n>  #include \"help.h\"\n>  #include \"compat/compiler.h\"\n>  #include \"hook.h\"\n> +#include \"hook-list.h\"\n>  \n>  \n>  static void get_system_info(struct strbuf *sys_info)\n> @@ -41,39 +42,7 @@ static void get_system_info(struct strbuf *sys_info)\n>  \n>  static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n>  {\n> -\t/*\n> -\t * NEEDSWORK: Doesn't look like there is a list of all possible hooks;\n> -\t * so below is a transcription of `git help hooks`. Later, this should\n> -\t * be replaced with some programmatically generated list (generated from\n> -\t * doc or else taken from some library which tells us about all the\n> -\t * hooks)\n> -\t */\n> -\tstatic const char *hook[] = {\n> -\t\t\"applypatch-msg\",\n> -\t\t\"pre-applypatch\",\n> -\t\t\"post-applypatch\",\n> -\t\t\"pre-commit\",\n> -\t\t\"pre-merge-commit\",\n> -\t\t\"prepare-commit-msg\",\n> -\t\t\"commit-msg\",\n> -\t\t\"post-commit\",\n> -\t\t\"pre-rebase\",\n> -\t\t\"post-checkout\",\n> -\t\t\"post-merge\",\n> -\t\t\"pre-push\",\n> -\t\t\"pre-receive\",\n> -\t\t\"update\",\n> -\t\t\"post-receive\",\n> -\t\t\"post-update\",\n> -\t\t\"push-to-checkout\",\n> -\t\t\"pre-auto-gc\",\n> -\t\t\"post-rewrite\",\n> -\t\t\"sendemail-validate\",\n> -\t\t\"fsmonitor-watchman\",\n> -\t\t\"p4-pre-submit\",\n> -\t\t\"post-index-change\",\n> -\t};\n> -\tint i;\n> +\tconst char **p;\n>  \n>  \tif (nongit) {\n>  \t\tstrbuf_addstr(hook_info,\n> @@ -81,9 +50,12 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n>  \t\treturn;\n>  \t}\n>  \n> -\tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n> -\t\tif (hook_exists(hook[i]))\n> -\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n> +\tfor (p = hook_name_list; *p; p++) {\n> +\t\tconst char *hook = *p;\n> +\n> +\t\tif (hook_exists(hook))\n> +\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook);\n> +\t}\n>  }\n>  \n>  static const char * const bugreport_usage[] = {\n> diff --git a/generate-hooklist.sh b/generate-hooklist.sh\n> new file mode 100755\n> index 00000000000..25a7207b276\n> --- /dev/null\n> +++ b/generate-hooklist.sh\n> @@ -0,0 +1,20 @@\n> +#!/bin/sh\n> +\n> +echo \"/* Automatically generated by generate-hooklist.sh */\"\n> +\n> +print_hook_list () {\n> +\tcat <<EOF\n> +static const char *hook_name_list[] = {\n> +EOF\n> +\tgrep -C1 -h '^~~~' Documentation/githooks.txt |\n> +\tgrep '^[a-z0-9][a-z0-9-]*$' |\n> +\tsort |\n> +\tsed 's/^.*$/\t\"&\",/'\n> +\tcat <<EOF\n> +\tNULL,\n> +};\n> +EOF\n> +}\n> +\n\nI found that grep -C isn't portable, so (pending any feedback on this\nseries) I've locally replaced it with:\n\ndiff --git a/generate-hooklist.sh b/generate-hooklist.sh\nnew file mode 100755\nindex 0000000000..5a3f7f849c\n--- /dev/null\n+++ b/generate-hooklist.sh\n@@ -0,0 +1,24 @@\n+#!/bin/sh\n+\n+echo \"/* Automatically generated by generate-hooklist.sh */\"\n+\n+print_hook_list () {\n+\tcat <<EOF\n+static const char *hook_name_list[] = {\n+EOF\n+\tperl -ne '\n+\t\tchomp;\n+\t\t@l[$.] = $_;\n+\t\tpush @h => $l[$. - 1] if /^~~~+$/s;\n+\t\tEND {\n+\t\t\tprint qq[\\t\"$_\",\\n] for sort @h;\n+\t\t}\n+\t' <Documentation/githooks.txt\n+\tcat <<EOF\n+\tNULL,\n+};\n+EOF\n+}\n+\n+echo\n+print_hook_list\n"},{"id":"426153","messageId":"YLZ5H2Muh39Q4M5T@google.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"Re: [PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-06-01T18:14:55Z","receivedAt":"2021-06-01T18:15:05Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, May 28, 2021 at 02:11:02PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> After suggesting[1] an another round that the config-based-hook\n> topic[2] should take a more incremental approach to reach its end goal\n> I thought I'd try hacking that up.\n> \n> So this is a proposed restart of that topic which if the consensus\n> favors it should replace it, and the config-based hooks topic should\n> be rebased on top of this.\n\nI'm not entirely sure what you're trying to achieve by sending this\nseries. It was my impression that the existing config-based-hooks topic\nwas close to being ready to submit anyway (since Junio mentioned\nsubmitting it a couple revisions ago); rather than churning by reviewing\na different 31-patch topic, and then re-rolling and re-reviewing a\n(reduced) config hook topic, wouldn't it be easier on everyone's time to\ndo a final incremental review on the existing topic and then start in on\nbugfixes/feature patches afterwards?\n\nIt would have been nice to see a more clear discussion of patch\norganization sometime much sooner in the past year and a half since the\nproject was proposed[3], like maybe in the few iterations of the design\ndoc which included a rollout plan in July of last year[4]. To me, it\nseems late to be overhauling the direction like this, especially after I\nasked for opinions and approval on the direction before I started work\nin earnest.\n\nAnyway, I'd personally rather spend effort getting the existing series\nthe last few yards to the finish line than to head most of the way back\nto the start.\n\n - Emily\n\n> 1. https://lore.kernel.org/git/87lf80l1m6.fsf@evledraar.gmail.com/\n> 2. https://lore.kernel.org/git/20210527000856.695702-1-emilyshaffer@google.com/\n3. https://lore.kernel.org/git/20191116011125.GG22855@google.com/\n4. https://lore.kernel.org/git/20200728222455.3023400-1-emilyshaffer@google.com/\n"},{"id":"426159","messageId":"179668e9-008a-0b70-a193-b0a479ba0146@gmail.com","threadId":"55286","inReplyTo":"YLZ5H2Muh39Q4M5T@google.com","subject":"Re: [PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2021-06-01T20:50:46Z","receivedAt":"2021-06-01T20:50:52Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 6/1/2021 2:14 PM, Emily Shaffer wrote:\n> On Fri, May 28, 2021 at 02:11:02PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>>\n>> After suggesting[1] an another round that the config-based-hook\n>> topic[2] should take a more incremental approach to reach its end goal\n>> I thought I'd try hacking that up.\n\nI think sending this complete reorganization of a long-lived topic\nis not helpful, especially because the end-to-end diff is significant.\nThis series has been extensively tested and incrementally improved\nfor months, and it would be a waste to start over and lose all of that\nhardening.\n\nIt's also a but rushed that this comes only a day after the previous\nmessage recommending a reorganization. It would be best to at least\ngive the original author an opportunity to comment on your idea before\nworking on this.\n\n>> So this is a proposed restart of that topic which if the consensus\n>> favors it should replace it, and the config-based hooks topic should\n>> be rebased on top of this.\n> \n> I'm not entirely sure what you're trying to achieve by sending this\n> series. It was my impression that the existing config-based-hooks topic\n> was close to being ready to submit anyway (since Junio mentioned\n> submitting it a couple revisions ago); rather than churning by reviewing\n> a different 31-patch topic, and then re-rolling and re-reviewing a\n> (reduced) config hook topic, wouldn't it be easier on everyone's time to\n> do a final incremental review on the existing topic and then start in on\n> bugfixes/feature patches afterwards?\n\nI completely agree here.\n\n> It would have been nice to see a more clear discussion of patch\n> organization sometime much sooner in the past year and a half since the\n> project was proposed[3], like maybe in the few iterations of the design\n> doc which included a rollout plan in July of last year[4]. To me, it\n> seems late to be overhauling the direction like this, especially after I\n> asked for opinions and approval on the direction before I started work\n> in earnest.\n\nI've also seen messages as early as January where Ævar mentioned\nwanting to review the series, but not finding the time to do so.\nIt is reasonable to expect that contributors attempt such major\nreorganizations according to reviewers feedback, as long as the\nreviewers are timely about delivering that feedback.\n\nThanks,\n-Stolee\n"},{"id":"426181","messageId":"60b6d5f985c40_4abd2081a@natae.notmuch","threadId":"55286","inReplyTo":"patch-16.31-a16163d4fb5-20210528T110515Z-avarab@gmail.com","subject":"RE: [PATCH 16/31] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-02T00:51:05Z","receivedAt":"2021-06-02T00:51:11Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> From: Emily Shaffer <emilyshaffer@google.com>\n> \n> By using hook.h instead of run-command.h to invoke push-to-checkout,\n> hooks can now be specified in the config as well as in the hookdir.\n> push-to-checkout is not called anywhere but in builtin/receive-pack.c.\n> \n> This is the last user of the run_hook_le() API, so let's remove it\n> while we're at it, since run_hook_le() itself is the last user of\n> run_hook_ve() we can remove that too. The last direct user of\n> run_hook_le() was removed in the commit preceding this one.\n\nI think this should be split in two patches.\n\nYes, fundamentally there's not much of a difference, but if you are\nalready at 31 patches one that removes the last user and nother that\nremoves the API will make the progression a little bit cleaner.\n\n-- \nFelipe Contreras"},{"id":"426182","messageId":"60b6d82c6982c_4abd208e1@natae.notmuch","threadId":"55286","inReplyTo":"patch-21.31-3748f128763-20210528T110515Z-avarab@gmail.com","subject":"RE: [PATCH 21/31] hook: provide stdin by string_list or callback","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-02T01:00:28Z","receivedAt":"2021-06-02T01:00:33Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> From: Emily Shaffer <emilyshaffer@google.com>\n> \n> In cases where a hook requires only a small amount of information via\n> stdin, it should be simple for users to provide a string_list alone. But\n> in more complicated cases where the stdin is too large to hold in\n> memory,\n\n> let's provide a callback the users can populate line after line\n> with instead.\n\nlet's instead provide a callback the users can populate line after line.\n\n-- \nFelipe Contreras"},{"id":"426183","messageId":"60b6d929e9d4a_4abd20874@natae.notmuch","threadId":"55286","inReplyTo":"patch-27.31-c39c608e5cc-20210528T110515Z-avarab@gmail.com","subject":"RE: [PATCH 27/31] receive-pack: convert 'update' hook to hook.h","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-02T01:04:41Z","receivedAt":"2021-06-02T01:04:47Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> From: Emily Shaffer <emilyshaffer@google.com>\n> \n> By using hook.h to invoke the 'update' hook we closer to removing the\n> hooks code in run-command.c.\n\nWe *are* closer? We _get_ closer?\n\n-- \nFelipe Contreras"},{"id":"426198","messageId":"60b71788c0e6d_67d0208d4@natae.notmuch","threadId":"55286","inReplyTo":"YLZ5H2Muh39Q4M5T@google.com","subject":"Re: [PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-02T05:30:48Z","receivedAt":"2021-06-02T05:31:02Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Emily Shaffer wrote:\n> On Fri, May 28, 2021 at 02:11:02PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > \n> > After suggesting[1] an another round that the config-based-hook\n> > topic[2] should take a more incremental approach to reach its end goal\n> > I thought I'd try hacking that up.\n> > \n> > So this is a proposed restart of that topic which if the consensus\n> > favors it should replace it, and the config-based hooks topic should\n> > be rebased on top of this.\n> \n> I'm not entirely sure what you're trying to achieve by sending this\n> series.\n\nHave a better review? By having a more reviwable series?\n\n> It was my impression that the existing config-based-hooks topic\n> was close to being ready to submit anyway (since Junio mentioned\n> submitting it a couple revisions ago); rather than churning by reviewing\n> a different 31-patch topic, and then re-rolling and re-reviewing a\n> (reduced) config hook topic, wouldn't it be easier on everyone's time to\n> do a final incremental review on the existing topic and then start in on\n> bugfixes/feature patches afterwards?\n\nNot to me. I tried to review your series and I just couldn't.\n\nMaybe it's something about me, but the whole point of a review is to\nhave as many eyes as possible on the code in order to spot any potential\nissues. If some eyes have trouble parsing the patches, that's not\noptimal.\n\nÆvar's approach is easy for me to follow.\n\n> It would have been nice to see a more clear discussion of patch\n> organization sometime much sooner in the past year and a half since the\n> project was proposed[3], like maybe in the few iterations of the design\n> doc which included a rollout plan in July of last year[4].\n\nNot all of us are being paid by a big corporation to work on Git.\n\nSome of us are doing the work on our own free time. You can't demand we\nspend our own free time on a certain patch series as soon as possible\nbecause it's more convenient for $corporation.\n\nGit is an open source community project.\n\n> To me, it seems late to be overhauling the direction like this,\n> especially after I asked for opinions and approval on the direction\n> before I started work in earnest.\n\nTo me it's completely the opposite; it's never too late to overwhaul a\npatch series.\n\nIn my opinion you should be thankful that somebody took the time to try\nto improve your series *for free*.\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"426199","messageId":"60b71a37cb4aa_67d0208bf@natae.notmuch","threadId":"55286","inReplyTo":"179668e9-008a-0b70-a193-b0a479ba0146@gmail.com","subject":"Re: [PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-02T05:42:15Z","receivedAt":"2021-06-02T05:42:19Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Derrick Stolee wrote:\n> On 6/1/2021 2:14 PM, Emily Shaffer wrote:\n> > On Fri, May 28, 2021 at 02:11:02PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> >>\n> >> After suggesting[1] an another round that the config-based-hook\n> >> topic[2] should take a more incremental approach to reach its end goal\n> >> I thought I'd try hacking that up.\n> \n> I think sending this complete reorganization of a long-lived topic\n> is not helpful, especially because the end-to-end diff is significant.\n\nThe end-to-end diff is significant because it's not the full patch\nseries.\n\nGive me pointers to the two branches and I'll get you a patch series on\ntop of Ævar's that gets you *exactly* zero end-to-end diff to Emily's\nseries.\n\nThere's many paths to end up with exactly the same code.\n\nI do it all the time.\n\n> I've also seen messages as early as January where Ævar mentioned\n> wanting to review the series, but not finding the time to do so.\n> It is reasonable to expect that contributors attempt such major\n> reorganizations according to reviewers feedback, as long as the\n> reviewers are timely about delivering that feedback.\n\nThe Git project doesn't have deadlines.\n\nCode should be merged when it's ready to be merged, not when it's\nconvenient for $company.\n\nI have patches that have been stuck for 7 years. Why should $company get\na fast-path, an exception, or preferential treatment?\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"426207","messageId":"871r9k8zui.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"60b71a37cb4aa_67d0208bf@natae.notmuch","subject":"Re: [PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-02T07:46:09Z","receivedAt":"2021-06-02T07:55:58Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Jun 02 2021, Felipe Contreras wrote:\n\n> Derrick Stolee wrote:\n>> On 6/1/2021 2:14 PM, Emily Shaffer wrote:\n>> > On Fri, May 28, 2021 at 02:11:02PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> >>\n>> >> After suggesting[1] an another round that the config-based-hook\n>> >> topic[2] should take a more incremental approach to reach its end goal\n>> >> I thought I'd try hacking that up.\n>> \n>> I think sending this complete reorganization of a long-lived topic\n>> is not helpful, especially because the end-to-end diff is significant.\n>\n> The end-to-end diff is significant because it's not the full patch\n> series.\n>\n> Give me pointers to the two branches and I'll get you a patch series on\n> top of Ævar's that gets you *exactly* zero end-to-end diff to Emily's\n> series.\n\nAt https://github.com/avar/git this series is at\nes-avar/config-based-hooks-2 (there's an unsubmitted *-3 with the minor\nfix I already noted, pending any other feedback).\n\nThen es/config-based-hooks-v9-basis-for-es-avar/config-based-hooks-2 is\nEmily's v9 as applied by Junio, which is what I started work on. Right\nnow it's identical to Junio's (at https://github.com/gitster/git)\ngitster/es/config-based-hooks, I just saved it away in case that branch\nmoves.\n\nA diff --stat -p between es-avar/config-based-hooks-2~2 and that topic\nfollows, i.e. my topic submitted here minus my last two fixes-on-top\npatches.\n\nI.e. this would be more-or-less the diff we'd apply to get to the\nend-state (minus reverting a few bugfixes etc. I made along the line,\ne.g. I forgot to mention that I added the \"git hook run\n--ignore-missing\" to fix another TOCTOU race, except one introduced by\nEmily's series).\n\n Documentation/Makefile                           |   1 +\n Documentation/config/hook.txt                    |  27 ++\n Documentation/git-hook.txt                       | 157 +++++++--\n Documentation/githooks.txt                       |  79 ++++-\n Documentation/technical/api-parse-options.txt    |   7 +\n Documentation/technical/config-based-hooks.txt   | 369 +++++++++++++++++++++\n builtin/am.c                                     |  27 +-\n builtin/bugreport.c                              |   2 +-\n builtin/checkout.c                               |   4 +-\n builtin/clone.c                                  |   3 +-\n builtin/commit.c                                 |  12 +-\n builtin/gc.c                                     |   3 +-\n builtin/hook.c                                   | 182 +++++++++--\n builtin/merge.c                                  |  13 +-\n builtin/pack-objects.c                           | 174 +---------\n builtin/rebase.c                                 |   3 +-\n builtin/receive-pack.c                           |  51 ++-\n builtin/worktree.c                               |   7 +-\n commit.c                                         |  10 +-\n commit.h                                         |   3 +-\n git-p4.py                                        |  13 +-\n git-send-email.perl                              |  35 +-\n git.c                                            |   2 +-\n hook.c                                           | 397 +++++++++++++++++++----\n hook.h                                           | 109 ++++---\n pack-objects.h                                   | 159 +++++++++\n parse-options-cb.c                               |  16 +\n parse-options.h                                  |   4 +\n perl/Git.pm                                      |  13 +\n read-cache.c                                     |   3 +-\n refs.c                                           |   8 +-\n reset.c                                          |   4 +-\n run-command.c                                    |   1 -\n sequencer.c                                      |  16 +-\n t/helper/test-parse-options.c                    |   6 +\n t/t0040-parse-options.sh                         |  27 ++\n t/t1092-sparse-checkout-compatibility.sh         |   6 +-\n t/t1350-config-hooks-path.sh                     |   1 -\n t/t1360-config-based-hooks.sh                    | 329 +++++++++++++++++++\n t/t1416-ref-transaction-hooks.sh                 |  12 +-\n t/t1800-hook.sh                                  | 158 ---------\n t/t2080-parallel-checkout-basics.sh              |   2 +-\n t/t5411/test-0015-too-many-hooks-error.sh        |  47 +++\n t/t6500-gc.sh                                    |  46 ---\n t/t7503-pre-commit-and-pre-merge-commit-hooks.sh |  17 +-\n t/t9001-send-email.sh                            |  16 +-\n transport.c                                      |   6 +-\n 47 files changed, 1941 insertions(+), 646 deletions(-)\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex 2aae4c9cbb3..5d19eddb0eb 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -90,6 +90,7 @@ SP_ARTICLES += $(API_DOCS)\n TECH_DOCS += MyFirstContribution\n TECH_DOCS += MyFirstObjectWalk\n TECH_DOCS += SubmittingPatches\n+TECH_DOCS += technical/config-based-hooks\n TECH_DOCS += technical/hash-function-transition\n TECH_DOCS += technical/http-protocol\n TECH_DOCS += technical/index-format\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nnew file mode 100644\nindex 00000000000..4f66bb35cf8\n--- /dev/null\n+++ b/Documentation/config/hook.txt\n@@ -0,0 +1,27 @@\n+hook.<command>.command::\n+\tA command to execute during the <command> hook event. This can be an\n+\texecutable on your device, a oneliner for your shell, or the name of a\n+\thookcmd. See linkgit:git-hook[1].\n+\n+hookcmd.<name>.command::\n+\tA command to execute during a hook for which <name> has been specified\n+\tas a command. This can be an executable on your device or a oneliner for\n+\tyour shell. See linkgit:git-hook[1].\n+\n+hookcmd.<name>.skip::\n+\tSpecify this boolean to remove a command from earlier in the execution\n+\torder. Useful if you want to make a single repo an exception to hook\n+\tconfigured at the system or global scope. If there is no hookcmd\n+\tspecified for the command you want to skip, you can use the value of\n+\t`hook.<command>.command` as <name> as a shortcut. The \"skip\" setting\n+\tmust be specified after the \"hook.<command>.command\" to have an effect.\n+\n+hook.runHookDir::\n+\tControls how hooks contained in your hookdir are executed. Can be any of\n+\t\"yes\", \"warn\", \"interactive\", or \"no\". Defaults to \"yes\". See\n+\tlinkgit:git-hook[1] and linkgit:git-config[1] \"core.hooksPath\").\n+\n+hook.jobs::\n+\tSpecifies how many hooks can be run simultaneously during parallelized\n+\thook execution. If unspecified, defaults to the number of processors on\n+\tthe current system.\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 816b3eda460..24e00a6f4af 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -3,46 +3,159 @@ git-hook(1)\n \n NAME\n ----\n-git-hook - run git hooks\n+git-hook - Manage configured hooks\n \n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' list <hook-name>\n+'git hook' run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>]\n+\t[(-j|--jobs) <n>] <hook-name>\n \n DESCRIPTION\n -----------\n+You can list and run configured hooks with this command. Later, you will be able\n+to add and modify hooks with this command.\n \n-This command is an interface to git hooks (see linkgit:githooks[5]).\n-Currently it only provides a convenience wrapper for running hooks for\n-use by git itself. In the future it might gain other functionality.\n+In general, when instructions suggest adding a script to\n+`.git/hooks/<something>`, you can specify it in the config instead by running\n+`git config --add hook.<something>.command <path-to-script>` - this way you can\n+share the script between multiple repos. That is, `cp ~/my-script.sh\n+~/project/.git/hooks/pre-commit` would become `git config --add\n+hook.pre-commit.command ~/my-script.sh`.\n \n-SUBCOMMANDS\n------------\n+This command parses the default configuration files for sections `hook` and\n+`hookcmd`. `hook` is used to describe the commands which will be run during a\n+particular hook event; commands are run in the order Git encounters them during\n+the configuration parse (see linkgit:git-config[1]). `hookcmd` is used to\n+describe attributes of a specific command. If additional attributes don't need\n+to be specified, a command to run can be specified directly in the `hook`\n+section; if a `hookcmd` by that name isn't found, Git will attempt to run the\n+provided value directly. For example:\n+\n+Global config\n+----\n+  [hook \"post-commit\"]\n+    command = \"linter\"\n+    command = \"~/typocheck.sh\"\n+\n+  [hookcmd \"linter\"]\n+    command = \"/bin/linter --c\"\n+----\n+\n+Local config\n+----\n+  [hook \"prepare-commit-msg\"]\n+    command = \"linter\"\n+  [hook \"post-commit\"]\n+    command = \"python ~/run-test-suite.py\"\n+----\n+\n+With these configs, you'd then see:\n+\n+----\n+$ git hook list \"post-commit\"\n+global: /bin/linter --c\n+global: ~/typocheck.sh\n+local: python ~/run-test-suite.py\n+\n+$ git hook list \"prepare-commit-msg\"\n+local: /bin/linter --c\n+----\n+\n+If there is a command you wish to run in most cases but have one or two\n+exceptional repos where it should be skipped, you can use specify\n+`hookcmd.<name>.skip`, for example:\n+\n+System config\n+----\n+  [hook \"pre-commit\"]\n+    command = check-for-secrets\n+\n+  [hookcmd \"check-for-secrets\"]\n+    command = /bin/secret-checker --aggressive\n+----\n \n-run::\n+Local config\n+----\n+  [hookcmd \"check-for-secrets\"]\n+    skip = true\n+  # This works for inlined hook commands, too:\n+  [hookcmd \"~/typocheck.sh\"]\n+    skip = true\n+----\n \n-\tRun the `<hook-name>` hook. Any positional arguments to the\n-\thook should be passed after an optional \"--\" (or\n-\t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n-\tthis accepts.\n+After these configs are added, the hook list becomes:\n+\n+----\n+$ git hook list \"post-commit\"\n+global: /bin/linter --c\n+local: python ~/run-test-suite.py\n+\n+$ git hook list \"pre-commit\"\n+no commands configured for hook 'pre-commit'\n+----\n+\n+COMMANDS\n+--------\n+\n+list `<hook-name>`::\n+\n+List the hooks which have been configured for `<hook-name>`. Hooks appear\n+in the order they should be run, and print the config scope where the relevant\n+`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n+This output is human-readable and the format is subject to change over time.\n+\n+run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...] [--to-stdin=<path>] [(-j|--jobs)<n>] `<hook-name>`::\n+\n+Runs hooks configured for `<hook-name>`, in the same order displayed by `git\n+hook list`. Hooks configured this way may be run prepended with `sh -c`, so\n+paths containing special characters or spaces should be wrapped in single\n+quotes: `command = '/my/path with spaces/script.sh' some args`.\n \n OPTIONS\n -------\n+--run-hookdir::\n+\tOverrides the hook.runHookDir config. Must be 'yes', 'warn',\n+\t'interactive', or 'no'. Specifies how to handle hooks located in the Git\n+\thook directory (core.hooksPath).\n+\n+-a::\n+--arg::\n+\tOnly valid for `run`.\n++\n+Specify arguments to pass to every hook that is run.\n+\n+-e::\n+--env::\n+\tOnly valid for `run`.\n++\n+Specify environment variables to set for every hook that is run.\n \n --to-stdin::\n-\tFor \"run\"; Specify a file which will be streamed into the\n-\thook's stdin. The hook will receive the entire file from\n-\tbeginning to EOF.\n+\tOnly valid for `run`.\n++\n+Specify a file which will be streamed into stdin for every hook that is run.\n+Each hook will receive the entire file from beginning to EOF.\n \n---ignore-missing::\n-\tIgnore any missing hook by quietly returning zero. Used for\n-\ttools that want to do a blind one-shot run of a hook that may\n-\tor may not be present.\n+-j::\n+--jobs::\n+\tOnly valid for `run`.\n++\n+Specify how many hooks to run simultaneously. If this flag is not specified, use\n+the value of the `hook.jobs` config. If the config is not specified, use the\n+number of CPUs on the current system. Some hooks may be ineligible for\n+parallelization: for example, 'commit-msg' intends hooks modify the commit\n+message body and cannot be parallelized.\n \n-SEE ALSO\n---------\n-linkgit:githooks[5]\n+HOOKS\n+-----\n+For a list of hooks which can be configured and how they work, see\n+linkgit:githooks[5].\n+\n+CONFIGURATION\n+-------------\n+include::config/hook.txt[]\n \n GIT\n ---\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex a16e62bc8c8..d780cb3b18d 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -7,15 +7,16 @@ githooks - Hooks used by Git\n \n SYNOPSIS\n --------\n+'git hook'\n $GIT_DIR/hooks/* (or \\`git config core.hooksPath`/*)\n \n \n DESCRIPTION\n -----------\n \n-Hooks are programs you can place in a hooks directory to trigger\n-actions at certain points in git's execution. Hooks that don't have\n-the executable bit set are ignored.\n+Hooks are programs you can specify in your config (see linkgit:git-hook[1]) or\n+place in a hooks directory to trigger actions at certain points in git's\n+execution. Hooks that don't have the executable bit set are ignored.\n \n By default the hooks directory is `$GIT_DIR/hooks`, but that can be\n changed via the `core.hooksPath` configuration variable (see\n@@ -58,6 +59,9 @@ the message file.\n The default 'applypatch-msg' hook, when enabled, runs the\n 'commit-msg' hook, if the latter is enabled.\n \n+Hooks run during 'applypatch-msg' will not be parallelized, because hooks are\n+expected to edit the file holding the commit log message.\n+\n pre-applypatch\n ~~~~~~~~~~~~~~\n \n@@ -73,6 +77,9 @@ make a commit if it does not pass certain test.\n The default 'pre-applypatch' hook, when enabled, runs the\n 'pre-commit' hook, if the latter is enabled.\n \n+Hooks run during 'pre-applypatch' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n post-applypatch\n ~~~~~~~~~~~~~~~\n \n@@ -82,6 +89,9 @@ and is invoked after the patch is applied and a commit is made.\n This hook is meant primarily for notification, and cannot affect\n the outcome of `git am`.\n \n+Hooks run during 'post-applypatch' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n pre-commit\n ~~~~~~~~~~\n \n@@ -103,6 +113,8 @@ The default 'pre-commit' hook, when enabled--and with the\n `hooks.allownonascii` config option unset or set to false--prevents\n the use of non-ASCII filenames.\n \n+Hooks executed during 'pre-commit' will not be parallelized.\n+\n pre-merge-commit\n ~~~~~~~~~~~~~~~~\n \n@@ -125,6 +137,8 @@ need to be resolved and the result committed separately (see\n linkgit:git-merge[1]). At that point, this hook will not be executed,\n but the 'pre-commit' hook will, if it is enabled.\n \n+Hooks executed during 'pre-merge-commit' will not be parallelized.\n+\n prepare-commit-msg\n ~~~~~~~~~~~~~~~~~~\n \n@@ -150,6 +164,9 @@ be used as replacement for pre-commit hook.\n The sample `prepare-commit-msg` hook that comes with Git removes the\n help message found in the commented portion of the commit template.\n \n+Hooks executed during 'prepare-commit-msg' will not be parallelized, because\n+hooks are expected to edit the file containing the commit log message.\n+\n commit-msg\n ~~~~~~~~~~\n \n@@ -166,6 +183,9 @@ file.\n The default 'commit-msg' hook, when enabled, detects duplicate\n `Signed-off-by` trailers, and aborts the commit if one is found.\n \n+Hooks executed during 'commit-msg' will not be parallelized, because hooks are\n+expected to edit the file containing the proposed commit log message.\n+\n post-commit\n ~~~~~~~~~~~\n \n@@ -175,6 +195,9 @@ invoked after a commit is made.\n This hook is meant primarily for notification, and cannot affect\n the outcome of `git commit`.\n \n+Hooks executed during 'post-commit' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n pre-rebase\n ~~~~~~~~~~\n \n@@ -184,6 +207,9 @@ two parameters.  The first parameter is the upstream from which\n the series was forked.  The second parameter is the branch being\n rebased, and is not set when rebasing the current branch.\n \n+Hooks executed during 'pre-rebase' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n post-checkout\n ~~~~~~~~~~~~~\n \n@@ -206,6 +232,8 @@ This hook can be used to perform repository validity checks, auto-display\n differences from the previous HEAD if different, or set working dir metadata\n properties.\n \n+Hooks executed during 'post-checkout' will not be parallelized.\n+\n post-merge\n ~~~~~~~~~~\n \n@@ -220,6 +248,9 @@ save and restore any form of metadata associated with the working tree\n (e.g.: permissions/ownership, ACLS, etc).  See contrib/hooks/setgitperms.perl\n for an example of how to do this.\n \n+Hooks executed during 'post-merge' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n pre-push\n ~~~~~~~~\n \n@@ -249,6 +280,9 @@ If this hook exits with a non-zero status, `git push` will abort without\n pushing anything.  Information about why the push is rejected may be sent\n to the user by writing to standard error.\n \n+Hooks executed during 'pre-push' will run in parallel, unless hook.jobs is\n+configured to 1.\n+\n [[pre-receive]]\n pre-receive\n ~~~~~~~~~~~\n@@ -290,6 +324,8 @@ will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n See the section on \"Quarantine Environment\" in\n linkgit:git-receive-pack[1] for some caveats.\n \n+Hooks executed during 'pre-receive' will not be parallelized.\n+\n [[update]]\n update\n ~~~~~~\n@@ -335,6 +371,9 @@ The default 'update' hook, when enabled--and with\n `hooks.allowunannotated` config option unset or set to false--prevents\n unannotated tags to be pushed.\n \n+Hooks executed during 'update' are run in parallel, unless hook.jobs is\n+configured to 1.\n+\n [[proc-receive]]\n proc-receive\n ~~~~~~~~~~~~\n@@ -397,6 +436,10 @@ the input.  The exit status of the 'proc-receive' hook only determines\n the success or failure of the group of commands sent to it, unless\n atomic push is in use.\n \n+It is forbidden to specify more than one hook for 'proc-receive'. If a\n+globally-configured 'proc-receive' must be overridden, use\n+'hookcmd.<global-hook>.skip = true' to ignore it.\n+\n [[post-receive]]\n post-receive\n ~~~~~~~~~~~~\n@@ -436,6 +479,9 @@ environment variables will not be set. If the client selects\n to use push options, but doesn't transmit any, the count variable\n will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.\n \n+Hooks executed during 'post-receive' are run in parallel, unless hook.jobs is\n+configured to 1.\n+\n [[post-update]]\n post-update\n ~~~~~~~~~~~\n@@ -468,6 +514,9 @@ Both standard output and standard error output are forwarded to\n `git send-pack` on the other end, so you can simply `echo` messages\n for the user.\n \n+Hooks run during 'post-update' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n reference-transaction\n ~~~~~~~~~~~~~~~~~~~~~\n \n@@ -506,6 +555,9 @@ The exit status of the hook is ignored for any state except for the\n cause the transaction to be aborted. The hook will not be called with\n \"aborted\" state in that case.\n \n+Hooks run during 'reference-transaction' will be run in parallel, unless\n+hook.jobs is configured to 1.\n+\n push-to-checkout\n ~~~~~~~~~~~~~~~~\n \n@@ -536,6 +588,7 @@ that switches branches while\n keeping the local changes in the working tree that do not interfere\n with the difference between the branches.\n \n+Hooks executed during 'push-to-checkout' will not be parallelized.\n \n pre-auto-gc\n ~~~~~~~~~~~\n@@ -544,6 +597,9 @@ This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It\n takes no parameter, and exiting with non-zero status from this script\n causes the `git gc --auto` to abort.\n \n+Hooks run during 'pre-auto-gc' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n post-rewrite\n ~~~~~~~~~~~~\n \n@@ -569,6 +625,9 @@ The hook always runs after the automatic note copying (see\n \"notes.rewrite.<command>\" in linkgit:git-config[1]) has happened, and\n thus has access to these notes.\n \n+Hooks run during 'post-rewrite' will be run in parallel, unless hook.jobs is\n+configured to 1.\n+\n The following command-specific comments apply:\n \n rebase::\n@@ -591,9 +650,12 @@ e-mails.\n fsmonitor-watchman\n ~~~~~~~~~~~~~~~~~~\n \n-This hook is invoked when the configuration option `core.fsmonitor` is\n-set to `.git/hooks/fsmonitor-watchman` or `.git/hooks/fsmonitor-watchmanv2`\n-depending on the version of the hook to use.\n+This hook is invoked when the configuration option `core.fsmonitor` is set to a\n+path containing an executable. It *cannot* be specified via the usual\n+`hook.fsmonitor-watchman.command` configuration or by providing an executable\n+in `.git/hooks/fsmonitor-watchman`. The arguments provided to the hook are\n+determined by the value of the `core.fsmonitorHookVersion` configuration\n+option.\n \n Version 1 takes two arguments, a version (1) and the time in elapsed\n nanoseconds since midnight, January 1, 1970.\n@@ -698,9 +760,8 @@ and \"0\" meaning they were not.\n Only one parameter should be set to \"1\" when the hook runs.  The hook\n running passing \"1\", \"1\" should not be possible.\n \n-SEE ALSO\n---------\n-linkgit:git-hook[1]\n+Hooks run during 'post-index-change' will be run in parallel, unless hook.jobs\n+is configured to 1.\n \n GIT\n ---\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex 5a60bbfa7f4..f79b17e7fcd 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -173,6 +173,13 @@ There are some macros to easily define options:\n \tThe string argument is stored as an element in `string_list`.\n \tUse of `--no-option` will clear the list of preceding values.\n \n+`OPT_STRVEC(short, long, &struct strvec, arg_str, description)`::\n+\tIntroduce an option with a string argument, meant to be specified\n+\tmultiple times.\n+\tThe string argument is stored as an element in `strvec`, and later\n+\targuments are added to the same `strvec`.\n+\tUse of `--no-option` will clear the list of preceding values.\n+\n `OPT_INTEGER(short, long, &int_var, description)`::\n \tIntroduce an option with integer argument.\n \tThe integer is put into `int_var`.\ndiff --git a/Documentation/technical/config-based-hooks.txt b/Documentation/technical/config-based-hooks.txt\nnew file mode 100644\nindex 00000000000..1f973117e44\n--- /dev/null\n+++ b/Documentation/technical/config-based-hooks.txt\n@@ -0,0 +1,369 @@\n+Configuration-based hook management\n+===================================\n+:sectanchors:\n+\n+[[motivation]]\n+== Motivation\n+\n+Replace the `.git/hook/hookname` path as the only source of hooks to execute;\n+allow users to define hooks using config files, in a way which is friendly to\n+users with multiple repos which have similar needs - hooks can be easily shared\n+between multiple Git repos.\n+\n+Redefine \"hook\" as an event rather than a single script, allowing users to\n+perform multiple unrelated actions on a single event.\n+\n+Make it easier for users to discover Git's hook feature and automate their\n+workflows.\n+\n+[[user-interfaces]]\n+== User interfaces\n+\n+[[config-schema]]\n+=== Config schema\n+\n+Hooks can be introduced by editing the configuration manually. There are two new\n+sections added, `hook` and `hookcmd`.\n+\n+[[config-schema-hook]]\n+==== `hook`\n+\n+Primarily contains subsections for each hook event. The order of variables in\n+these subsections defines the hook command execution order; hook commands can be\n+specified by setting the value directly to the command if no additional\n+configuration is needed, or by setting the value as the name of a `hookcmd`. If\n+Git does not find a `hookcmd` whose subsection matches the value of the given\n+command string, Git will try to execute the string directly. Hooks are executed\n+by passing the resolved command string to the shell. In the future, hook event\n+subsections could also contain per-hook-event settings; see\n+<<per-hook-event-settings,the section in Future Work>> for more details.\n+\n+Also contains top-level hook execution settings, for example, `hook.runHookDir`.\n+(These settings are described more in <<library,Library>>.)\n+\n+----\n+[hook \"pre-commit\"]\n+  command = perl-linter\n+  command = /usr/bin/git-secrets --pre-commit\n+\n+[hook \"pre-applypatch\"]\n+  command = perl-linter\n+  # for illustration purposes; error behavior isn't planned yet\n+  error = ignore\n+\n+[hook]\n+  runHookDir = interactive\n+----\n+\n+[[config-schema-hookcmd]]\n+==== `hookcmd`\n+\n+Defines a hook command and its attributes, which will be used when a hook event\n+occurs. Unqualified attributes are assumed to apply to this hook during all hook\n+events, but event-specific attributes can also be supplied. The example runs\n+`/usr/bin/lint-it --language=perl <args passed by Git>`, but for repos which\n+include this config, the hook command will be skipped for all events.\n+Theoretically, the last line could be used to \"un-skip\" the hook command for\n+`pre-commit` hooks, but this hasn't been scoped or implemented yet.\n+\n+----\n+[hookcmd \"perl-linter\"]\n+  command = /usr/bin/lint-it --language=perl\n+  skip = true\n+  # for illustration purposes; below hasn't been defined yet\n+  pre-commit-skip = false\n+----\n+\n+[[command-line-api]]\n+=== Command-line API\n+\n+Users should be able to view, run, reorder, and create hook commands via the\n+command line. External tools should be able to view a list of hooks in the\n+correct order to run. Modifier commands (`edit` and `add`) have not been\n+implemented yet and may not be if manually editing the config proves usable\n+enough.\n+\n+*`git hook list <hook-event>`*\n+\n+*`git hook run <hook-event> [-a <arg>]... [-e <env-var>]...`*\n+\n+*`git hook edit <hook-event>`*\n+\n+*`git hook add <hook-command> <hook-event> <options...>`*\n+\n+[[hook-editor]]\n+=== Hook editor\n+\n+The tool which is presented by `git hook edit <hook-command>`. Ideally, this\n+tool should be easier to use than manually editing the config, and then produce\n+a concise config afterwards. It may take a form similar to `git rebase\n+--interactive`. This has not been designed or implemented yet and may not be if\n+the config proves usable enough.\n+\n+[[implementation]]\n+== Implementation\n+\n+[[library]]\n+=== Library\n+\n+`hook.c` and `hook.h` are responsible for interacting with the config files. The\n+hook library provides a basic API to call all hooks in config order with more\n+complex options passed via `struct run_hooks_opt`:\n+\n+*`int run_hooks(const char *hookname, struct run_hooks_opt *options)`*\n+\n+`struct run_hooks_opt` allows callers to set:\n+\n+- environment variables\n+- command-line arguments\n+- behavior for the hook command provided by `run-command.h:find_hook()` (see\n+  below)\n+- a method to provide stdin to each hook, either via a file containing stdin, a\n+  `struct string_list` containing a list of lines to print, or a callback\n+  function to allow the caller to populate stdin manually\n+- a method to process stdout from each hook, e.g. for printing to sideband\n+  during a network operation\n+- parallelism\n+- a custom working directory for hooks to execute in\n+\n+And this struct can be extended with more options as necessary in the future.\n+\n+The \"legacy\" hook provided by `run-command.h:find_hook()` - that is, the hook\n+present in `.git/hooks/<hookname>` or\n+`$(git config --get core.hooksPath)/<hookname>` - can be handled in a number of\n+ways, providing an avenue to deprecate these \"legacy\" hooks if desired. The\n+handling is based on a config `hook.runHookDir`, which is checked against a\n+number of cases:\n+\n+- \"no\": the legacy hook will not be run\n+- \"error\": Git will print a warning to stderr before ignoring the legacy hook\n+- \"interactive\": Git will prompt the user before running the legacy hook\n+- \"warn\": Git will print a warning to stderr before running the legacy hook\n+- \"yes\" (default): Git will silently run the legacy hook\n+\n+In case this list is expanded in the future, if a value for `hook.runHookDir` is\n+given which Git does not recognize, Git should discard that config entry. For\n+example, if \"warn\" was specified at system level and \"junk\" was specified at\n+global level, Git would resolve the value to \"warn\"; if the only time the config\n+was set was to \"junk\", Git would use the default value of \"yes\" (but print a\n+warning to the user first to let them know their value is wrong).\n+\n+`struct hookcmd` is expected to grow in size over time as more functionality is\n+added to hooks; so that other parts of the code don't need to understand the\n+config schema, `struct hookcmd` should contain logical values instead of string\n+pairs.\n+\n+By default, hook parallelism is chosen based on the semantics of each hook;\n+callsites initialize their `struct run_hooks_opt` via one of two macros,\n+`RUN_HOOKS_OPT_INIT_SYNC` or `RUN_HOOKS_OPT_INIT_ASYNC`. The default number of\n+jobs can be configured in `hook.jobs`; this config applies across all hook\n+events. If unset, the value of `online_cpus()` (equivalent to `nproc`) is used.\n+\n+[[builtin]]\n+=== Builtin\n+\n+`builtin/hook.c` is responsible for providing the frontend. It's responsible for\n+formatting user-provided data and then calling the library API to set the\n+configs as appropriate. The builtin frontend is not responsible for calling the\n+config directly, so that other areas of Git can rely on the hook library to\n+understand the most recent config schema for hooks.\n+\n+[[migration]]\n+=== Migration path\n+\n+[[stage-0]]\n+==== Stage 0\n+\n+Hooks are called by running `run-command.h:find_hook()` with the hookname and\n+executing the result. The hook library and builtin do not exist. Hooks only\n+exist as specially named scripts within `.git/hooks/`.\n+\n+[[stage-1]]\n+==== Stage 1\n+\n+`git hook list --porcelain <hook-event>` is implemented. `hook.h:run_hooks()` is\n+taught to include `run-command.h:find_hook()` at the end; calls to `find_hook()`\n+are replaced with calls to `run_hooks()`. Users can opt-in to config-based hooks\n+simply by creating some in their config; otherwise users should remain\n+unaffected by the change.\n+\n+[[stage-2]]\n+==== Stage 2\n+\n+The call to `find_hook()` inside of `run_hooks()` learns to check for a config,\n+`hook.runHookDir`. Users can opt into managing their hooks completely via the\n+config this way.\n+\n+[[stage-3]]\n+==== Stage 3\n+\n+`.git/hooks` is removed from the template and the hook directory is considered\n+deprecated. To avoid breaking older repos, the default of `hook.runHookDir` is\n+not changed, and `find_hook()` is not removed.\n+\n+[[caveats]]\n+== Caveats\n+\n+[[security]]\n+=== Security and repo config\n+\n+Part of the motivation behind this refactor is to mitigate hooks as an attack\n+vector.footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/]\n+However, as the design stands, users can still provide hooks in the repo-level\n+config, which is included when a repo is zipped and sent elsewhere. The\n+security of the repo-level config is still under discussion; this design\n+generally assumes the repo-level config is secure, which is not true yet. This\n+assumption was made to avoid overcomplicating the design. So, this series\n+doesn't particularly improve security or resistance to zip attacks.\n+\n+[[ease-of-use]]\n+=== Ease of use\n+\n+The config schema is nontrivial; that's why it's important for the `git hook`\n+modifier commands to be usable. Contributors with UX expertise are encouraged to\n+share their suggestions.\n+\n+[[alternatives]]\n+== Alternative approaches\n+\n+A previous summary of alternatives exists in the\n+archives.footnote:[https://lore.kernel.org/git/20191116011125.GG22855@google.com]\n+\n+The table below shows a number of goals and how they might be achieved with\n+config-based hooks, by implementing directory support (i.e.\n+'.git/hooks/pre-commit.d'), or as hooks are run today.\n+\n+.Comparison of alternatives\n+|===\n+|Feature |Config-based hooks |Hook directories |Status quo\n+\n+|Supports multiple hooks\n+|Natively\n+|Natively\n+|With user effort\n+\n+|Supports parallelization\n+|Natively\n+|Natively\n+|No (user's multihook trampoline script would need to handle parallelism)\n+\n+|Safer for zipped repos\n+|A little\n+|No\n+|No\n+\n+|Previous hooks just work\n+|If configured\n+|Yes\n+|Yes\n+\n+|Can install one hook to many repos\n+|Yes\n+|With symlinks or core.hooksPath\n+|With symlinks or core.hooksPath\n+\n+|Discoverability\n+|Findable with 'git help git' or tab-completion via 'git hook' subcommand\n+|Findable via improved documentation\n+|Same as before\n+\n+|Hard to run unexpected hook\n+|If configured\n+|Could be made to warn or look for a config\n+|No\n+|===\n+\n+[[status-quo]]\n+=== Status quo\n+\n+Today users can implement multihooks themselves by using a \"trampoline script\"\n+as their hook, and pointing that script to a directory or list of other scripts\n+they wish to run.\n+\n+[[hook-directories]]\n+=== Hook directories\n+\n+Other contributors have suggested Git learn about the existence of a directory\n+such as `.git/hooks/<hookname>.d` and execute those hooks in alphabetical order.\n+\n+[[future-work]]\n+== Future work\n+\n+[[execution-ordering]]\n+=== Execution ordering\n+\n+We may find that config order is insufficient for some users; for example,\n+config order makes it difficult to add a new hook to the system or global config\n+which runs at the end of the hook list. A new ordering schema should be:\n+\n+1) Specified by a `hook.order` config, so that users will not unexpectedly see\n+their order change;\n+\n+2) Either dependency or numerically based.\n+\n+Dependency-based ordering is prone to classic linked-list problems, like a\n+cycles and handling of missing dependencies. But, it paves the way for enabling\n+parallelization if some tasks truly depend on others.\n+\n+Numerical ordering makes it tricky for Git to generate suggested ordering\n+numbers for each command, but is easy to determine a definitive order.\n+\n+[[parallelization]]\n+=== Parallelization with dependencies\n+\n+Currently hooks use a naive parallelization scheme or are run in series.  But if\n+one hook depends on another's output, then users will want to specify those\n+dependencies. If we decide to solve this problem, we may want to look to modern\n+build systems for inspiration on how to manage dependencies and parallel tasks.\n+\n+[[nontrivial-hooks]]\n+=== Multihooks and nontrivial output\n+\n+Some hooks - like 'proc-receive' - don't lend themselves well to multihooks at\n+all. In the case of 'proc-receive', for now, multiple hook definitions are\n+disallowed. In the future we might be able to conceive a better approach, for\n+example, running the hooks in series and using the output from one hook as the\n+input to the next.\n+\n+[[securing-hookdir-hooks]]\n+=== Securing hookdir hooks\n+\n+With the design as written in this doc, it's still possible for a malicious user\n+to modify `.git/config` to include `hook.pre-receive.command = rm -rf /`, then\n+zip their repo and send it to another user. It may be necessary to teach Git to\n+only allow inlined hooks like this if they were configured outside of the local\n+scope (in other words, only run hookcmds, and only allow hookcmds to be\n+configured in global or system scope); or another approach, like a list of safe\n+projects, might be useful. It may also be sufficient (or at least useful) to\n+teach a `hook.disableAll` config or similar flag to the Git executable.\n+\n+[[submodule-inheritance]]\n+=== Submodule inheritance\n+\n+It's possible some submodules may want to run the identical set of hooks that\n+their superrepo runs. While a globally-configured hook set is helpful, it's not\n+a great solution for users who have multiple repos-with-submodules under the\n+same user. It would be useful for submodules to learn how to run hooks from\n+their superrepo's config, or inherit that hook setting.\n+\n+[[per-hook-event-settings]]\n+=== Per-hook-event settings\n+\n+It might be desirable to keep settings specifically for some hook events, but\n+not for others - for example, a user may wish to disable hookdir hooks for all\n+events but pre-commit, which they haven't had time to convert yet; or, a user\n+may wish for execution order settings to differ based on hook event. In that\n+case, it would be useful to set something like `hook.pre-commit.executionOrder`\n+which would not apply to the 'prepare-commit-msg' hook, for example.\n+\n+[[glossary]]\n+== Glossary\n+\n+*hook event*\n+\n+A point during Git's execution where user scripts may be run, for example,\n+_prepare-commit-msg_ or _pre-push_.\n+\n+*hook command*\n+\n+A user script or executable which will be run on one or more hook events.\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 6e4f9c80360..d2534f9a1ff 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -11,7 +11,6 @@\n #include \"parse-options.h\"\n #include \"dir.h\"\n #include \"run-command.h\"\n-#include \"hook.h\"\n #include \"quote.h\"\n #include \"tempfile.h\"\n #include \"lockfile.h\"\n@@ -34,6 +33,7 @@\n #include \"string-list.h\"\n #include \"packfile.h\"\n #include \"repository.h\"\n+#include \"hook.h\"\n \n /**\n  * Returns the length of the first line of msg.\n@@ -445,7 +445,9 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n+\n+\trun_hooks_opt_init_sync(&opt);\n \n \tassert(state->msg);\n \tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n@@ -467,9 +469,11 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tint ret;\n \n+\trun_hooks_opt_init_async(&opt);\n+\n \tstrvec_push(&opt.args, \"rebase\");\n \topt.path_to_stdin = am_path(state, \"rewritten\");\n \n@@ -1602,14 +1606,17 @@ static void do_commit(const struct am_state *state)\n \tstruct commit_list *parents = NULL;\n \tconst char *reflog_msg, *author, *committer = NULL;\n \tstruct strbuf sb = STRBUF_INIT;\n-\tstruct run_hooks_opt hook_opt_pre = RUN_HOOKS_OPT_INIT;\n-\tstruct run_hooks_opt hook_opt_post = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt;\n+\n+\trun_hooks_opt_init_async(&hook_opt);\n \n-\tif (run_hooks(\"pre-applypatch\", &hook_opt_pre)) {\n-\t\trun_hooks_opt_clear(&hook_opt_pre);\n+\tif (run_hooks(\"pre-applypatch\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\texit(1);\n \t}\n \n+\trun_hooks_opt_clear(&hook_opt);\n+\n \tif (write_cache_as_tree(&tree, 0, NULL))\n \t\tdie(_(\"git write-tree failed to write a tree\"));\n \n@@ -1659,10 +1666,10 @@ static void do_commit(const struct am_state *state)\n \t\tfclose(fp);\n \t}\n \n-\trun_hooks(\"post-applypatch\", &hook_opt_post);\n+\trun_hooks_opt_init_async(&hook_opt);\n+\trun_hooks(\"post-applypatch\", &hook_opt);\n \n-\trun_hooks_opt_clear(&hook_opt_pre);\n-\trun_hooks_opt_clear(&hook_opt_post);\n+\trun_hooks_opt_clear(&hook_opt);\n \tstrbuf_release(&sb);\n }\n \ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 941c8d5e270..190272ba70f 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -82,7 +82,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t}\n \n \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (hook_exists(hook[i]))\n+\t\tif (hook_exists(hook[i], HOOKDIR_USE_CONFIG))\n \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n }\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 6205ace09f6..1797f05a50e 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -107,9 +107,11 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tint rc;\n \n+\trun_hooks_opt_init_sync(&opt);\n+\n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\n \tstrvec_pushl(&opt.args,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 6687025bea5..2a2a03bf768 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -776,7 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt;\n \n \tif (option_no_checkout)\n \t\treturn 0;\n@@ -822,6 +822,7 @@ static int checkout(int submodule_progress)\n \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n \t\tdie(_(\"unable to write new index file\"));\n \n+\trun_hooks_opt_init_sync(&hook_opt);\n \tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n \terr |= run_hooks(\"post-checkout\", &hook_opt);\n \trun_hooks_opt_clear(&hook_opt);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex dad4e565443..7e01802961f 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -19,7 +19,6 @@\n #include \"revision.h\"\n #include \"wt-status.h\"\n #include \"run-command.h\"\n-#include \"hook.h\"\n #include \"refs.h\"\n #include \"log-tree.h\"\n #include \"strbuf.h\"\n@@ -37,6 +36,7 @@\n #include \"help.h\"\n #include \"commit-reach.h\"\n #include \"commit-graph.h\"\n+#include \"hook.h\"\n \n static const char * const builtin_commit_usage[] = {\n \tN_(\"git commit [<options>] [--] <pathspec>...\"),\n@@ -729,7 +729,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t/* This checks and barfs if author is badly specified */\n \tdetermine_author_info(author_ident);\n \n-\tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(use_editor, 0, index_file, \"pre-commit\", NULL))\n \t\treturn 0;\n \n \tif (squash_message) {\n@@ -1045,7 +1045,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && hook_exists(\"pre-commit\")) {\n+\tif (!no_verify && hook_exists(\"pre-commit\", HOOKDIR_USE_CONFIG)) {\n \t\t/*\n \t\t * Re-read the index as pre-commit hook could have updated it,\n \t\t * and write it out as a tree.  We must do this before we invoke\n@@ -1060,7 +1060,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n+\tif (run_commit_hook(use_editor, 0, index_file, \"prepare-commit-msg\",\n \t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n \t\treturn 0;\n \n@@ -1077,7 +1077,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t}\n \n \tif (!no_verify &&\n-\t    run_commit_hook(use_editor, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n+\t    run_commit_hook(use_editor, 0, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n \t\treturn 0;\n \t}\n \n@@ -1830,7 +1830,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \n \trepo_rerere(the_repository, 0);\n \trun_auto_maintenance(quiet);\n-\trun_commit_hook(use_editor, get_index_file(), \"post-commit\", NULL);\n+\trun_commit_hook(use_editor, 1, get_index_file(), \"post-commit\", NULL);\n \tif (amend && !no_post_rewrite) {\n \t\tcommit_post_rewrite(the_repository, current_head, &oid);\n \t}\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex a12641a691d..16890b097cd 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -349,7 +349,7 @@ static void add_repack_incremental_option(void)\n \n static int need_to_gc(void)\n {\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt;\n \n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n@@ -397,6 +397,7 @@ static int need_to_gc(void)\n \telse\n \t\treturn 0;\n \n+\trun_hooks_opt_init_async(&hook_opt);\n \tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n \t\trun_hooks_opt_clear(&hook_opt);\n \t\treturn 0;\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex baaef4dce49..c79a961e801 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -7,53 +7,133 @@\n #include \"strvec.h\"\n \n static const char * const builtin_hook_usage[] = {\n-\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n+\tN_(\"git hook list <hookname>\"),\n+\tN_(\"git hook run [(-e|--env)=<var>...] [(-a|--arg)=<arg>...]\"\n+\t   \"[--to-stdin=<path>] [(-j|--jobs) <count>] <hookname>\"),\n \tNULL\n };\n \n+static enum hookdir_opt should_run_hookdir;\n+\n+static int list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct list_head *head, *pos;\n+\tconst char *hookname = NULL;\n+\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n+\n+\tstruct option list_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, list_options,\n+\t\t\t     builtin_hook_usage, 0);\n+\n+\tif (argc < 1) {\n+\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\t\t      builtin_hook_usage, list_options);\n+\t}\n+\n+\thookname = argv[0];\n+\n+\thead = hook_list(hookname);\n+\n+\tif (list_empty(head)) {\n+\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n+\t\t       hookname);\n+\t\treturn 0;\n+\t}\n+\n+\tlist_for_each(pos, head) {\n+\t\tstruct hook *item = list_entry(pos, struct hook, list);\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item) {\n+\t\t\tif (item->from_hookdir) {\n+\t\t\t\t/*\n+\t\t\t\t * TRANSLATORS: do not translate 'hookdir' as\n+\t\t\t\t * it matches the config setting.\n+\t\t\t\t */\n+\t\t\t\tswitch (should_run_hookdir) {\n+\t\t\t\tcase HOOKDIR_NO:\n+\t\t\t\t\tprintf(_(\"hookdir: %s (will not run)\\n\"),\n+\t\t\t\t\t       item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase HOOKDIR_ERROR:\n+\t\t\t\t\tprintf(_(\"hookdir: %s (will error and not run)\\n\"),\n+\t\t\t\t\t       item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase HOOKDIR_INTERACTIVE:\n+\t\t\t\t\tprintf(_(\"hookdir: %s (will prompt)\\n\"),\n+\t\t\t\t\t       item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase HOOKDIR_WARN:\n+\t\t\t\t\tprintf(_(\"hookdir: %s (will warn but run)\\n\"),\n+\t\t\t\t\t       item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\tcase HOOKDIR_YES:\n+\t\t\t\t/*\n+\t\t\t\t * The default behavior should agree with\n+\t\t\t\t * hook.c:configured_hookdir_opt(). HOOKDIR_UNKNOWN should just\n+\t\t\t\t * do the default behavior.\n+\t\t\t\t */\n+\t\t\t\tcase HOOKDIR_UNKNOWN:\n+\t\t\t\tdefault:\n+\t\t\t\t\tprintf(_(\"hookdir: %s\\n\"),\n+\t\t\t\t\t\t item->command.buf);\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\t/*\n+\t\t\t\t * TRANSLATORS: \"<config scope>: <path>\". Both fields\n+\t\t\t\t * should be left untranslated; config scope matches the\n+\t\t\t\t * output of 'git config --show-scope'. Marked for\n+\t\t\t\t * translation to provide better RTL support later.\n+\t\t\t\t */\n+\t\t\t\tprintf(_(\"%s: %s\\n\"),\n+\t\t\t\t\tconfig_scope_name(item->origin),\n+\t\t\t\t\titem->command.buf);\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tclear_hook_list(head);\n+\tstrbuf_release(&hookdir_annotation);\n+\n+\treturn 0;\n+}\n+\n static int run(int argc, const char **argv, const char *prefix)\n {\n-\tint i;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct strbuf hookname = STRBUF_INIT;\n+\tstruct run_hooks_opt opt;\n \tint rc = 0;\n-\tint ignore_missing = 0;\n-\tconst char *hook_name;\n-\tconst char *hook_path;\n \n \tstruct option run_options[] = {\n-\t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n-\t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n+\t\tOPT_STRVEC('e', \"env\", &opt.env, N_(\"var\"),\n+\t\t\t   N_(\"environment variables for hook to use\")),\n+\t\tOPT_STRVEC('a', \"arg\", &opt.args, N_(\"args\"),\n+\t\t\t   N_(\"argument to pass to hook\")),\n \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n \t\t\t   N_(\"file to read into hooks' stdin\")),\n+\t\tOPT_INTEGER('j', \"jobs\", &opt.jobs,\n+\t\t\t    N_(\"run up to <n> hooks simultaneously\")),\n \t\tOPT_END(),\n \t};\n \n+\trun_hooks_opt_init_async(&opt);\n+\n \targc = parse_options(argc, argv, prefix, run_options,\n-\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n-\n-\tif (argc > 2) {\n-\t\tif (strcmp(argv[2], \"--\") &&\n-\t\t    strcmp(argv[2], \"--end-of-options\"))\n-\t\t\t/* Having a -- for \"run\" is mandatory */\n-\t\t\tusage_with_options(builtin_hook_usage, run_options);\n-\t\t/* Add our arguments, start after -- */\n-\t\tfor (i = 3 ; i < argc; i++)\n-\t\t\tstrvec_push(&opt.args, argv[i]);\n-\t}\n+\t\t\t     builtin_hook_usage, 0);\n \n-\t/* Need to take into account core.hooksPath */\n-\tgit_config(git_default_config, NULL);\n+\tif (argc < 1)\n+\t\tusage_msg_opt(_(\"You must specify a hook event to run.\"),\n+\t\t\t      builtin_hook_usage, run_options);\n \n-\thook_name = argv[1];\n-\thook_path = find_hook(hook_name);\n-\tif (!hook_path) {\n-\t\tif (ignore_missing)\n-\t\t\treturn 0;\n-\t\terror(\"cannot find a hook named %s\", hook_name);\n-\t\treturn 1;\n-\t}\n-\trc = run_found_hooks(hook_name, hook_path, &opt);\n+\tstrbuf_addstr(&hookname, argv[0]);\n+\topt.run_hookdir = should_run_hookdir;\n \n+\trc = run_hooks(hookname.buf, &opt);\n+\n+\tstrbuf_release(&hookname);\n \trun_hooks_opt_clear(&opt);\n \n \treturn rc;\n@@ -61,12 +141,50 @@ static int run(int argc, const char **argv, const char *prefix)\n \n int cmd_hook(int argc, const char **argv, const char *prefix)\n {\n+\tconst char *run_hookdir = NULL;\n+\n \tstruct option builtin_hook_options[] = {\n+\t\tOPT_STRING(0, \"run-hookdir\", &run_hookdir, N_(\"option\"),\n+\t\t\t   N_(\"what to do with hooks found in the hookdir\")),\n \t\tOPT_END(),\n \t};\n \n-\tif (!strcmp(argv[1], \"run\"))\n+\targc = parse_options(argc, argv, prefix, builtin_hook_options,\n+\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN);\n+\n+\t/* after the parse, we should have \"<command> <hookname> <args...>\" */\n+\tif (argc < 2)\n+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+\n+\tgit_config(git_default_config, NULL);\n+\n+\n+\t/* argument > config */\n+\tif (run_hookdir)\n+\t\tif (!strcmp(run_hookdir, \"no\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_NO;\n+\t\telse if (!strcmp(run_hookdir, \"error\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_ERROR;\n+\t\telse if (!strcmp(run_hookdir, \"yes\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_YES;\n+\t\telse if (!strcmp(run_hookdir, \"warn\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_WARN;\n+\t\telse if (!strcmp(run_hookdir, \"interactive\"))\n+\t\t\tshould_run_hookdir = HOOKDIR_INTERACTIVE;\n+\t\telse\n+\t\t\t/*\n+\t\t\t * TRANSLATORS: leave \"yes/warn/interactive/no\"\n+\t\t\t * untranslated; the strings are compared literally.\n+\t\t\t */\n+\t\t\tdie(_(\"'%s' is not a valid option for --run-hookdir \"\n+\t\t\t      \"(yes, warn, interactive, no)\"), run_hookdir);\n+\telse\n+\t\tshould_run_hookdir = configured_hookdir_opt();\n+\n+\tif (!strcmp(argv[0], \"list\"))\n+\t\treturn list(argc, argv, prefix);\n+\tif (!strcmp(argv[0], \"run\"))\n \t\treturn run(argc, argv, prefix);\n+\n \tusage_with_options(builtin_hook_usage, builtin_hook_options);\n-\treturn 1;\n }\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex a9363b94065..7a524cb3e35 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -13,7 +13,6 @@\n #include \"builtin.h\"\n #include \"lockfile.h\"\n #include \"run-command.h\"\n-#include \"hook.h\"\n #include \"diff.h\"\n #include \"diff-merges.h\"\n #include \"refs.h\"\n@@ -44,6 +43,7 @@\n #include \"commit-reach.h\"\n #include \"wt-status.h\"\n #include \"commit-graph.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_TWOHEAD (1<<0)\n #define DEFAULT_OCTOPUS (1<<1)\n@@ -448,7 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\n@@ -490,6 +490,7 @@ static void finish(struct commit *head_commit,\n \t}\n \n \t/* Run a post-merge hook */\n+\trun_hooks_opt_init_async(&opt);\n \tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n \trun_hooks(\"post-merge\", &opt);\n \trun_hooks_opt_clear(&opt);\n@@ -845,14 +846,14 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *index_file = get_index_file();\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, index_file, \"pre-merge-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(0 < option_edit, 0, index_file, \"pre-merge-commit\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \t/*\n \t * Re-read the index as pre-merge-commit hook could have updated it,\n \t * and write it out as a tree.  We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (hook_exists(\"pre-merge-commit\"))\n+\tif (hook_exists(\"pre-merge-commit\", HOOKDIR_USE_CONFIG))\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n@@ -873,7 +874,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tappend_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);\n \twrite_merge_heads(remoteheads);\n \twrite_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);\n-\tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n+\tif (run_commit_hook(0 < option_edit, 0, get_index_file(), \"prepare-commit-msg\",\n \t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \tif (0 < option_edit) {\n@@ -881,7 +882,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\t\tabort_commit(remoteheads, NULL);\n \t}\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n+\tif (!no_verify && run_commit_hook(0 < option_edit, 0, get_index_file(),\n \t\t\t\t\t  \"commit-msg\",\n \t\t\t\t\t  git_path_merge_msg(the_repository), NULL))\n \t\tabort_commit(remoteheads, NULL);\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex de00adbb9e0..6ded130e45b 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -37,134 +37,6 @@\n #include \"shallow.h\"\n #include \"promisor-remote.h\"\n \n-/*\n- * Objects we are going to pack are collected in the `to_pack` structure.\n- * It contains an array (dynamically expanded) of the object data, and a map\n- * that can resolve SHA1s to their position in the array.\n- */\n-static struct packing_data to_pack;\n-\n-static inline struct object_entry *oe_delta(\n-\t\tconst struct packing_data *pack,\n-\t\tconst struct object_entry *e)\n-{\n-\tif (!e->delta_idx)\n-\t\treturn NULL;\n-\tif (e->ext_base)\n-\t\treturn &pack->ext_bases[e->delta_idx - 1];\n-\telse\n-\t\treturn &pack->objects[e->delta_idx - 1];\n-}\n-\n-static inline unsigned long oe_delta_size(struct packing_data *pack,\n-\t\t\t\t\t  const struct object_entry *e)\n-{\n-\tif (e->delta_size_valid)\n-\t\treturn e->delta_size_;\n-\n-\t/*\n-\t * pack->delta_size[] can't be NULL because oe_set_delta_size()\n-\t * must have been called when a new delta is saved with\n-\t * oe_set_delta().\n-\t * If oe_delta() returns NULL (i.e. default state, which means\n-\t * delta_size_valid is also false), then the caller must never\n-\t * call oe_delta_size().\n-\t */\n-\treturn pack->delta_size[e - pack->objects];\n-}\n-\n-unsigned long oe_get_size_slow(struct packing_data *pack,\n-\t\t\t       const struct object_entry *e);\n-\n-static inline unsigned long oe_size(struct packing_data *pack,\n-\t\t\t\t    const struct object_entry *e)\n-{\n-\tif (e->size_valid)\n-\t\treturn e->size_;\n-\n-\treturn oe_get_size_slow(pack, e);\n-}\n-\n-static inline void oe_set_delta(struct packing_data *pack,\n-\t\t\t\tstruct object_entry *e,\n-\t\t\t\tstruct object_entry *delta)\n-{\n-\tif (delta)\n-\t\te->delta_idx = (delta - pack->objects) + 1;\n-\telse\n-\t\te->delta_idx = 0;\n-}\n-\n-static inline struct object_entry *oe_delta_sibling(\n-\t\tconst struct packing_data *pack,\n-\t\tconst struct object_entry *e)\n-{\n-\tif (e->delta_sibling_idx)\n-\t\treturn &pack->objects[e->delta_sibling_idx - 1];\n-\treturn NULL;\n-}\n-\n-static inline struct object_entry *oe_delta_child(\n-\t\tconst struct packing_data *pack,\n-\t\tconst struct object_entry *e)\n-{\n-\tif (e->delta_child_idx)\n-\t\treturn &pack->objects[e->delta_child_idx - 1];\n-\treturn NULL;\n-}\n-\n-static inline void oe_set_delta_child(struct packing_data *pack,\n-\t\t\t\t      struct object_entry *e,\n-\t\t\t\t      struct object_entry *delta)\n-{\n-\tif (delta)\n-\t\te->delta_child_idx = (delta - pack->objects) + 1;\n-\telse\n-\t\te->delta_child_idx = 0;\n-}\n-\n-static inline void oe_set_delta_sibling(struct packing_data *pack,\n-\t\t\t\t\tstruct object_entry *e,\n-\t\t\t\t\tstruct object_entry *delta)\n-{\n-\tif (delta)\n-\t\te->delta_sibling_idx = (delta - pack->objects) + 1;\n-\telse\n-\t\te->delta_sibling_idx = 0;\n-}\n-\n-static inline void oe_set_size(struct packing_data *pack,\n-\t\t\t       struct object_entry *e,\n-\t\t\t       unsigned long size)\n-{\n-\tif (size < pack->oe_size_limit) {\n-\t\te->size_ = size;\n-\t\te->size_valid = 1;\n-\t} else {\n-\t\te->size_valid = 0;\n-\t\tif (oe_get_size_slow(pack, e) != size)\n-\t\t\tBUG(\"'size' is supposed to be the object size!\");\n-\t}\n-}\n-\n-static inline void oe_set_delta_size(struct packing_data *pack,\n-\t\t\t\t     struct object_entry *e,\n-\t\t\t\t     unsigned long size)\n-{\n-\tif (size < pack->oe_delta_size_limit) {\n-\t\te->delta_size_ = size;\n-\t\te->delta_size_valid = 1;\n-\t} else {\n-\t\tpacking_data_lock(pack);\n-\t\tif (!pack->delta_size)\n-\t\t\tALLOC_ARRAY(pack->delta_size, pack->nr_alloc);\n-\t\tpacking_data_unlock(pack);\n-\n-\t\tpack->delta_size[e - pack->objects] = size;\n-\t\te->delta_size_valid = 0;\n-\t}\n-}\n-\n #define IN_PACK(obj) oe_in_pack(&to_pack, obj)\n #define SIZE(obj) oe_size(&to_pack, obj)\n #define SET_SIZE(obj,size) oe_set_size(&to_pack, obj, size)\n@@ -184,6 +56,13 @@ static const char *pack_usage[] = {\n \tNULL\n };\n \n+/*\n+ * Objects we are going to pack are collected in the `to_pack` structure.\n+ * It contains an array (dynamically expanded) of the object data, and a map\n+ * that can resolve SHA1s to their position in the array.\n+ */\n+static struct packing_data to_pack;\n+\n static struct pack_idx_entry **written_list;\n static uint32_t nr_result, nr_written, nr_seen;\n static struct bitmap_index *bitmap_git;\n@@ -422,17 +301,6 @@ static void copy_pack_data(struct hashfile *f,\n \t}\n }\n \n-static inline int oe_size_greater_than(struct packing_data *pack,\n-\t\t\t\t       const struct object_entry *lhs,\n-\t\t\t\t       unsigned long rhs)\n-{\n-\tif (lhs->size_valid)\n-\t\treturn lhs->size_ > rhs;\n-\tif (rhs < pack->oe_size_limit) /* rhs < 2^x <= lhs ? */\n-\t\treturn 1;\n-\treturn oe_get_size_slow(pack, lhs) > rhs;\n-}\n-\n /* Return 0 if we will bust the pack-size limit */\n static unsigned long write_no_reuse_object(struct hashfile *f, struct object_entry *entry,\n \t\t\t\t\t   unsigned long limit, int usable_delta)\n@@ -774,14 +642,6 @@ static int mark_tagged(const char *path, const struct object_id *oid, int flag,\n \treturn 0;\n }\n \n-static inline unsigned char oe_layer(struct packing_data *pack,\n-\t\t\t\t     struct object_entry *e)\n-{\n-\tif (!pack->layer)\n-\t\treturn 0;\n-\treturn pack->layer[e - pack->objects];\n-}\n-\n static inline void add_to_write_order(struct object_entry **wo,\n \t\t\t       unsigned int *endp,\n \t\t\t       struct object_entry *e)\n@@ -2371,26 +2231,6 @@ static pthread_mutex_t progress_mutex;\n  * progress_mutex for protection.\n  */\n \n-static inline int oe_size_less_than(struct packing_data *pack,\n-\t\t\t\t    const struct object_entry *lhs,\n-\t\t\t\t    unsigned long rhs)\n-{\n-\tif (lhs->size_valid)\n-\t\treturn lhs->size_ < rhs;\n-\tif (rhs < pack->oe_size_limit) /* rhs < 2^x <= lhs ? */\n-\t\treturn 0;\n-\treturn oe_get_size_slow(pack, lhs) < rhs;\n-}\n-\n-static inline void oe_set_tree_depth(struct packing_data *pack,\n-\t\t\t\t     struct object_entry *e,\n-\t\t\t\t     unsigned int tree_depth)\n-{\n-\tif (!pack->tree_depth)\n-\t\tCALLOC_ARRAY(pack->tree_depth, pack->nr_alloc);\n-\tpack->tree_depth[e - pack->objects] = tree_depth;\n-}\n-\n /*\n  * Return the size of the object without doing any delta\n  * reconstruction (so non-deltas are true object sizes, but deltas\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 2081f6fa8db..fe9f144cad6 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1314,7 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\n@@ -2024,6 +2024,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t}\n \n \t/* If a hook exists, give it a chance to interrupt*/\n+\trun_hooks_opt_init_async(&hook_opt);\n \tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n \tif (!ok_to_skip_pre_rebase &&\n \t    run_hooks(\"pre-rebase\", &hook_opt)) {\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ec90e10477a..f44b58e456d 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -7,7 +7,6 @@\n #include \"pkt-line.h\"\n #include \"sideband.h\"\n #include \"run-command.h\"\n-#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"commit.h\"\n #include \"object.h\"\n@@ -30,6 +29,7 @@\n #include \"commit-reach.h\"\n #include \"worktree.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static const char * const receive_pack_usage[] = {\n \tN_(\"git receive-pack <git-dir>\"),\n@@ -910,11 +910,13 @@ static int run_receive_hook(struct command *commands,\n \t\t\t    int skip_broken,\n \t\t\t    const struct string_list *push_options)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tstruct receive_hook_feed_context ctx;\n \tint rc;\n \tstruct command *iter = commands;\n \n+\trun_hooks_opt_init_async(&opt);\n+\n \t/* if there are no valid commands, don't invoke the hook at all. */\n \twhile (iter && skip_broken && (iter->error_string || iter->did_not_exist))\n \t\titer = iter->next;\n@@ -956,9 +958,11 @@ static int run_receive_hook(struct command *commands,\n \n static int run_update_hook(struct command *cmd)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tint code;\n \n+\trun_hooks_opt_init_async(&opt);\n+\n \tstrvec_pushl(&opt.args,\n \t\t     cmd->ref_name,\n \t\t     oid_to_hex(&cmd->old_oid),\n@@ -1128,11 +1132,38 @@ static int run_proc_receive_hook(struct command *commands,\n \tint version = 0;\n \tint code;\n \n-\targv[0] = find_hook(\"proc-receive\");\n-\tif (!argv[0]) {\n+\tstruct hook *proc_receive = NULL;\n+\tstruct list_head *pos, *hooks;\n+\n+\thooks = hook_list(\"proc-receive\");\n+\n+\tlist_for_each(pos, hooks) {\n+\t\tif (proc_receive) {\n+\t\t\trp_error(\"only one 'proc-receive' hook can be specified\");\n+\t\t\treturn -1;\n+\t\t}\n+\t\tproc_receive = list_entry(pos, struct hook, list);\n+\t\t/* check if the hookdir hook should be ignored */\n+\t\tif (proc_receive->from_hookdir) {\n+\t\t\tswitch (configured_hookdir_opt()) {\n+\t\t\tcase HOOKDIR_INTERACTIVE:\n+\t\t\tcase HOOKDIR_NO:\n+\t\t\t\tproc_receive = NULL;\n+\t\t\t\tbreak;\n+\t\t\tdefault:\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\n+\t}\n+\n+\tif (!proc_receive) {\n \t\trp_error(\"cannot find hook 'proc-receive'\");\n \t\treturn -1;\n \t}\n+\n+\n+\targv[0] = proc_receive->command.buf;\n \targv[1] = NULL;\n \n \tproc.argv = argv;\n@@ -1442,7 +1473,9 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n+\n+\trun_hooks_opt_init_sync(&opt);\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n \tstrvec_pushv(&opt.env, env->v);\n@@ -1477,7 +1510,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!hook_exists(push_to_checkout_hook))\n+\tif (!hook_exists(push_to_checkout_hook, HOOKDIR_USE_CONFIG))\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n \telse\n \t\tretval = push_to_checkout(sha1, &env, work_tree);\n@@ -1640,7 +1673,9 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n+\n+\trun_hooks_opt_init_async(&opt);\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 2ad26a76f4c..017b2cfcb58 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -8,12 +8,12 @@\n #include \"branch.h\"\n #include \"refs.h\"\n #include \"run-command.h\"\n-#include \"hook.h\"\n #include \"sigchain.h\"\n #include \"submodule.h\"\n #include \"utf8.h\"\n #include \"worktree.h\"\n #include \"quote.h\"\n+#include \"hook.h\"\n \n static const char * const worktree_usage[] = {\n \tN_(\"git worktree add [<options>] <path> [<commit-ish>]\"),\n@@ -382,7 +382,9 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt;\n+\n+\t\trun_hooks_opt_init_sync(&opt);\n \n \t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n \t\tstrvec_pushl(&opt.args,\n@@ -391,7 +393,6 @@ static int add_worktree(const char *path, const char *refname,\n \t\t\t     \"1\",\n \t\t\t     NULL);\n \t\topt.dir = path;\n-\t\topt.absolute_path = 1;\n \n \t\tret = run_hooks(\"post-checkout\", &opt);\n \ndiff --git a/commit.c b/commit.c\nindex e8147a88fc6..0da5b7e7f1a 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1696,13 +1696,19 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n \treturn boc ? len - boc : len - cutoff;\n }\n \n-int run_commit_hook(int editor_is_used, const char *index_file,\n+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,\n \t\t    const char *name, ...)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tva_list args;\n \tconst char *arg;\n \tint ret;\n+\n+\trun_hooks_opt_init_sync(&opt);\n+\n+\tif (parallelize)\n+\t\topt.jobs = configured_hook_jobs();\n+\n \tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\ndiff --git a/commit.h b/commit.h\nindex df42eb434f3..a90c094ec27 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -363,7 +363,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\n \n LAST_ARG_MUST_BE_NULL\n-int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);\n+int run_commit_hook(int editor_is_used, int parallelize, const char *index_file,\n+\t\t    const char *name, ...);\n \n /* Sign a commit or tag buffer, storing the result in a header. */\n int sign_with_header(struct strbuf *buf, const char *keyid);\ndiff --git a/git-p4.py b/git-p4.py\nindex e76d8df3139..7d770957719 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -207,12 +207,15 @@ def decode_path(path):\n         return path\n \n def run_git_hook(cmd, param=[]):\n+    \"\"\"Execute a hook if the hook exists.\"\"\"\n+    if not cmd:\n+        return True\n+\n     \"\"\"args are specified with -a <arg> -a <arg> -a <arg>\"\"\"\n-    args = ['git', 'hook', 'run', '--ignore-missing', cmd]\n-    if param:\n-        args.append(\"--\")\n-        for p in param:\n-            args.append(p)\n+    args = (['git', 'hook', 'run'] +\n+            [\"-a\" + arg for arg in param] +\n+            [cmd])\n+\n     return subprocess.call(args) == 0\n \n def write_pipe(c, stdin):\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 2ab8dfdbded..b55687453e0 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -213,13 +213,13 @@ sub format_2822_time {\n my $editor;\n \n sub system_or_msg {\n-\tmy ($args, $msg, $cmd_name) = @_;\n+\tmy ($args, $msg) = @_;\n \tsystem(@$args);\n \tmy $signalled = $? & 127;\n \tmy $exit_code = $? >> 8;\n \treturn unless $signalled or $exit_code;\n \n-\tmy @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);\n+\tmy @sprintf_args = ($args->[0], $exit_code);\n \tif (defined $msg) {\n \t\t# Quiet the 'redundant' warning category, except we\n \t\t# need to support down to Perl 5.8, so we can't do a\n@@ -1958,31 +1958,12 @@ sub unique_email_list {\n sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n-\tif ($repo) {\n-\t\tmy $hook_name = 'sendemail-validate';\n-\t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n-\t\tmy $validate_hook = catfile($hooks_path, $hook_name);\n-\t\tmy $hook_error;\n-\t\tif (-x $validate_hook) {\n-\t\t\tmy $target = abs_path($fn);\n-\t\t\t# The hook needs a correct cwd and GIT_DIR.\n-\t\t\tmy $cwd_save = cwd();\n-\t\t\tchdir($repo->wc_path() or $repo->repo_path())\n-\t\t\t\tor die(\"chdir: $!\");\n-\t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\tmy @validate_hook = (\"git\", \"hook\", \"run\", \"--ignore-missing\", $hook_name, \"--\", $target);\n-\t\t\t$hook_error = system_or_msg(\\@validate_hook, undef,\n-\t\t\t\t\t\t       \"git hook run $hook_name -- <patch>\");\n-\t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n-\t\t}\n-\t\tif ($hook_error) {\n-\t\t\t$hook_error = sprintf(__(\"fatal: %s: rejected by %s hook\\n\" .\n-\t\t\t\t\t\t $hook_error . \"\\n\" .\n-\t\t\t\t\t\t \"warning: no patches were sent\\n\"),\n-\t\t\t\t\t      $fn, $hook_name);\n-\t\t\tdie $hook_error;\n-\t\t}\n-\t}\n+\tmy $target = abs_path($fn);\n+\n+\tsystem_or_die([\"git\", \"hook\", \"run\", \"sendemail-validate\", \"-j1\", \"-a\", $target],\n+\t\tsprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n+\t\t\t   \"warning: no patches were sent\\n\"),\n+\t\t        $fn));\n \n \t# Any long lines will be automatically fixed if we use a suitable transfer\n \t# encoding.\ndiff --git a/git.c b/git.c\nindex 540909c391f..39988ee3b02 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,7 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n-\t{ \"hook\", cmd_hook, RUN_SETUP },\n+\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nindex 17ae65eca31..ff80e52eddd 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -1,8 +1,224 @@\n #include \"cache.h\"\n+\n #include \"hook.h\"\n+#include \"config.h\"\n #include \"run-command.h\"\n+#include \"prompt.h\"\n+\n+void free_hook(struct hook *ptr)\n+{\n+\tif (ptr) {\n+\t\tstrbuf_release(&ptr->command);\n+\t\tfree(ptr->feed_pipe_cb_data);\n+\t}\n+\tfree(ptr);\n+}\n+\n+static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n+{\n+\tstruct list_head *pos = NULL, *tmp = NULL;\n+\tstruct hook *found = NULL;\n+\n+\tlist_for_each_safe(pos, tmp, head) {\n+\t\tstruct hook *it = list_entry(pos, struct hook, list);\n+\t\tif (!strcmp(it->command.buf, command)) {\n+\t\t    list_del(pos);\n+\t\t    found = it;\n+\t\t    break;\n+\t\t}\n+\t}\n+\treturn found;\n+}\n+\n+static void append_or_move_hook(struct list_head *head, const char *command)\n+{\n+\tstruct hook *to_add = find_hook_by_command(head, command);\n+\n+\tif (!to_add) {\n+\t\t/* adding a new hook, not moving an old one */\n+\t\tto_add = xmalloc(sizeof(*to_add));\n+\t\tstrbuf_init(&to_add->command, 0);\n+\t\tstrbuf_addstr(&to_add->command, command);\n+\t\tto_add->from_hookdir = 0;\n+\t\tto_add->feed_pipe_cb_data = NULL;\n+\t}\n+\n+\t/* re-set the scope so we show where an override was specified */\n+\tto_add->origin = current_config_scope();\n+\n+\tlist_add_tail(&to_add->list, head);\n+}\n+\n+static void remove_hook(struct list_head *to_remove)\n+{\n+\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n+\tlist_del(to_remove);\n+\tfree_hook(hook_to_remove);\n+}\n+\n+void clear_hook_list(struct list_head *head)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, head)\n+\t\tremove_hook(pos);\n+}\n+\n+struct hook_config_cb\n+{\n+\tstruct strbuf *hookname;\n+\tstruct list_head *list;\n+};\n+\n+static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n+{\n+\tstruct hook_config_cb *data = cb_data;\n+\tconst char *hook_key = data->hookname->buf;\n+\tstruct list_head *head = data->list;\n+\n+\tif (!strcmp(key, hook_key)) {\n+\t\tconst char *command = value;\n+\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\t\tint skip = 0;\n+\n+\t\t/*\n+\t\t * Check if we're removing that hook instead. Hookcmds are\n+\t\t * removed by name, and inlined hooks are removed by command\n+\t\t * content.\n+\t\t */\n+\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.skip\", command);\n+\t\tgit_config_get_bool(hookcmd_name.buf, &skip);\n+\n+\t\t/*\n+\t\t * Check if a hookcmd with that name exists. If it doesn't,\n+\t\t * 'git_config_get_value()' is documented not to touch &command,\n+\t\t * so we don't need to do anything.\n+\t\t */\n+\t\tstrbuf_reset(&hookcmd_name);\n+\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n+\t\tgit_config_get_value(hookcmd_name.buf, &command);\n+\n+\t\tif (!command) {\n+\t\t\tstrbuf_release(&hookcmd_name);\n+\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n+\t\t}\n+\n+\t\t/*\n+\t\t * TODO: implement an option-getting callback, e.g.\n+\t\t *   get configs by pattern hookcmd.$value.*\n+\t\t *   for each key+value, do_callback(key, value, cb_data)\n+\t\t */\n+\n+\t\tif (skip) {\n+\t\t\tstruct hook *to_remove = find_hook_by_command(head, command);\n+\t\t\tif (to_remove)\n+\t\t\t\tremove_hook(&(to_remove->list));\n+\t\t} else {\n+\t\t\tappend_or_move_hook(head, command);\n+\t\t}\n+\n+\t\tstrbuf_release(&hookcmd_name);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+enum hookdir_opt configured_hookdir_opt(void)\n+{\n+\tconst char *key;\n+\tif (git_config_get_value(\"hook.runhookdir\", &key))\n+\t\treturn HOOKDIR_YES; /* by default, just run it. */\n+\n+\tif (!strcmp(key, \"no\"))\n+\t\treturn HOOKDIR_NO;\n+\n+\tif (!strcmp(key, \"error\"))\n+\t\treturn HOOKDIR_ERROR;\n+\n+\tif (!strcmp(key, \"yes\"))\n+\t\treturn HOOKDIR_YES;\n+\n+\tif (!strcmp(key, \"warn\"))\n+\t\treturn HOOKDIR_WARN;\n+\n+\tif (!strcmp(key, \"interactive\"))\n+\t\treturn HOOKDIR_INTERACTIVE;\n+\n+\treturn HOOKDIR_UNKNOWN;\n+}\n+\n+int configured_hook_jobs(void)\n+{\n+\tint n = online_cpus();\n+\tgit_config_get_int(\"hook.jobs\", &n);\n+\n+\treturn n;\n+}\n+\n+static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n+{\n+\tstruct strbuf prompt = STRBUF_INIT;\n+\t/*\n+\t * If the path doesn't exist, don't bother adding the empty hook and\n+\t * don't bother checking the config or prompting the user.\n+\t */\n+\tif (!path)\n+\t\treturn 0;\n+\n+\tswitch (cfg)\n+\t{\n+\tcase HOOKDIR_ERROR:\n+\t\tfprintf(stderr, _(\"Skipping legacy hook at '%s'\\n\"),\n+\t\t\tpath);\n+\t\t/* FALLTHROUGH */\n+\tcase HOOKDIR_NO:\n+\t\treturn 0;\n+\tcase HOOKDIR_WARN:\n+\t\tfprintf(stderr, _(\"Running legacy hook at '%s'\\n\"),\n+\t\t\tpath);\n+\t\treturn 1;\n+\tcase HOOKDIR_INTERACTIVE:\n+\t\tdo {\n+\t\t\t/*\n+\t\t\t * TRANSLATORS: Make sure to include [Y] and [n]\n+\t\t\t * in your translation. Only English input is\n+\t\t\t * accepted. Default option is \"yes\".\n+\t\t\t */\n+\t\t\tfprintf(stderr, _(\"Run '%s'? [Y/n] \"), path);\n+\t\t\tgit_read_line_interactively(&prompt);\n+\t\t\t/*\n+\t\t\t * In case of prompt = '' - that is, user hit enter,\n+\t\t\t * saying \"yes I want the default\" - strncasecmp will\n+\t\t\t * return 0 regardless. So list the default first.\n+\t\t\t *\n+\t\t\t * Case insensitively, accept \"y\", \"ye\", or \"yes\" as\n+\t\t\t * \"yes\"; accept \"n\" or \"no\" as \"no\".\n+\t\t\t */\n+\t\t\tif (!strncasecmp(prompt.buf, \"yes\", prompt.len)) {\n+\t\t\t\tstrbuf_release(&prompt);\n+\t\t\t\treturn 1;\n+\t\t\t} else if (!strncasecmp(prompt.buf, \"no\", prompt.len)) {\n+\t\t\t\tstrbuf_release(&prompt);\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\t\t\t/* otherwise, we didn't understand the input */\n+\t\t} while (prompt.len); /* an empty reply means default (yes) */\n+\t\treturn 1;\n+\t/*\n+\t * HOOKDIR_UNKNOWN should match the default behavior, but let's\n+\t * give a heads up to the user.\n+\t */\n+\tcase HOOKDIR_UNKNOWN:\n+\t\tfprintf(stderr,\n+\t\t\t_(\"Unrecognized value for 'hook.runHookDir'. \"\n+\t\t\t  \"Is there a typo? \"));\n+\t\t/* FALLTHROUGH */\n+\tcase HOOKDIR_YES:\n+\tdefault:\n+\t\treturn 1;\n+\t}\n+}\n \n-const char *find_hook(const char *name)\n+static const char *find_legacy_hook(const char *name)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \n@@ -36,9 +252,77 @@ const char *find_hook(const char *name)\n \treturn path.buf;\n }\n \n-int hook_exists(const char *name)\n+\n+struct list_head* hook_list(const char* hookname)\n+{\n+\tstruct strbuf hook_key = STRBUF_INIT;\n+\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n+\n+\tINIT_LIST_HEAD(hook_head);\n+\n+\tif (!hookname)\n+\t\treturn NULL;\n+\n+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n+\n+\tgit_config(hook_config_lookup, &cb_data);\n+\n+\tif (have_git_dir()) {\n+\t\tconst char *legacy_hook_path = find_legacy_hook(hookname);\n+\n+\t\t/* Unconditionally add legacy hook, but annotate it. */\n+\t\tif (legacy_hook_path) {\n+\t\t\tstruct hook *legacy_hook;\n+\n+\t\t\tappend_or_move_hook(hook_head,\n+\t\t\t\t\t    absolute_path(legacy_hook_path));\n+\t\t\tlegacy_hook = list_entry(hook_head->prev, struct hook,\n+\t\t\t\t\t\t list);\n+\t\t\tlegacy_hook->from_hookdir = 1;\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&hook_key);\n+\treturn hook_head;\n+}\n+\n+void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n {\n-\treturn !!find_hook(name);\n+\tstrvec_init(&o->env);\n+\tstrvec_init(&o->args);\n+\to->path_to_stdin = NULL;\n+\to->run_hookdir = configured_hookdir_opt();\n+\to->jobs = 1;\n+\to->dir = NULL;\n+\to->feed_pipe = NULL;\n+\to->feed_pipe_ctx = NULL;\n+\to->consume_sideband = NULL;\n+}\n+\n+void run_hooks_opt_init_async(struct run_hooks_opt *o)\n+{\n+\trun_hooks_opt_init_sync(o);\n+\to->jobs = configured_hook_jobs();\n+}\n+\n+int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n+{\n+\tconst char *value = NULL; /* throwaway */\n+\tstruct strbuf hook_key = STRBUF_INIT;\n+\tint could_run_hookdir;\n+\n+\tif (should_run_hookdir == HOOKDIR_USE_CONFIG)\n+\t\tshould_run_hookdir = configured_hookdir_opt();\n+\n+\tcould_run_hookdir = (should_run_hookdir == HOOKDIR_INTERACTIVE ||\n+\t\t\t\tshould_run_hookdir == HOOKDIR_WARN ||\n+\t\t\t\tshould_run_hookdir == HOOKDIR_YES)\n+\t\t\t\t&& !!find_legacy_hook(hookname);\n+\n+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n+\n+\treturn (!git_config_get_value(hook_key.buf, &value)) || could_run_hookdir;\n }\n \n void run_hooks_opt_clear(struct run_hooks_opt *o)\n@@ -51,8 +335,7 @@ int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n {\n \tint *item_idx;\n \tstruct hook *ctx = pp_task_cb;\n-\tstruct hook_cb_data *hook_cb = pp_cb;\n-\tstruct string_list *to_pipe = hook_cb->options->feed_pipe_ctx;\n+\tstruct string_list *to_pipe = ((struct hook_cb_data*)pp_cb)->options->feed_pipe_ctx;\n \n \t/* Bootstrap the state manager if necessary. */\n \tif (!ctx->feed_pipe_cb_data) {\n@@ -76,10 +359,10 @@ static int pick_next_hook(struct child_process *cp,\n \t\t\t  void **pp_task_cb)\n {\n \tstruct hook_cb_data *hook_cb = pp_cb;\n-\tstruct hook *run_me = hook_cb->run_me;\n+\tstruct hook *hook = hook_cb->run_me;\n \n-\tif (!run_me)\n-\t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n+\tif (!hook)\n+\t\treturn 0;\n \n \t/* reopen the file for stdin; run_command closes it. */\n \tif (hook_cb->options->path_to_stdin) {\n@@ -92,13 +375,20 @@ static int pick_next_hook(struct child_process *cp,\n \t} else {\n \t\tcp->no_stdin = 1;\n \t}\n+\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n-\tcp->trace2_hook_name = hook_cb->hook_name;\n+\tcp->trace2_hook_name = hook_cb->hookname;\n \tcp->dir = hook_cb->options->dir;\n \n+\t/*\n+\t * Commands from the config could be oneliners, but we know\n+\t * for certain that hookdir commands are not.\n+\t */\n+\tcp->use_shell = !hook->from_hookdir;\n+\n \t/* add command */\n-\tstrvec_push(&cp->args, run_me->hook_path);\n+\tstrvec_push(&cp->args, hook->command.buf);\n \n \t/*\n \t * add passed-in argv, without expanding - let the user get back\n@@ -107,7 +397,14 @@ static int pick_next_hook(struct child_process *cp,\n \tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n \n \t/* Provide context for errors if necessary */\n-\t*pp_task_cb = run_me;\n+\t*pp_task_cb = hook;\n+\n+\t/* Get the next entry ready */\n+\tif (hook_cb->run_me->list.next == hook_cb->head)\n+\t\thook_cb->run_me = NULL;\n+\telse\n+\t\thook_cb->run_me = list_entry(hook_cb->run_me->list.next,\n+\t\t\t\t\t     struct hook, list);\n \n \treturn 1;\n }\n@@ -122,10 +419,13 @@ static int notify_start_failure(struct strbuf *out,\n \t/* |= rc in cb */\n \thook_cb->rc |= 1;\n \n-\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n-\t\t    attempted->hook_path);\n+\tstrbuf_addf(out, _(\"Couldn't start '%s', configured in '%s'\\n\"),\n+\t\t    attempted->command.buf,\n+\t\t    attempted->from_hookdir ? \"hookdir\"\n+\t\t\t: config_scope_name(attempted->origin));\n \n-\treturn 1;\n+\t/* NEEDSWORK: if halt_on_error is desired, do it here. */\n+\treturn 0;\n }\n \n static int notify_hook_finished(int result,\n@@ -138,29 +438,36 @@ static int notify_hook_finished(int result,\n \t/* |= rc in cb */\n \thook_cb->rc |= result;\n \n-\treturn 1;\n+\t/* NEEDSWORK: if halt_on_error is desired, do it here. */\n+\treturn 0;\n }\n \n-int run_found_hooks(const char *hook_name, const char *hook_path,\n-\t\t    struct run_hooks_opt *options)\n-{\n-\tstruct strbuf abs_path = STRBUF_INIT;\n-\tstruct hook my_hook = {\n-\t\t.hook_path = hook_path,\n-\t};\n-\tstruct hook_cb_data cb_data = {\n-\t\t.rc = 0,\n-\t\t.hook_name = hook_name,\n-\t\t.options = options,\n-\t};\n-\tif (options->absolute_path) {\n-\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n-\t\tmy_hook.hook_path = abs_path.buf;\n+int run_hooks(const char *hookname, struct run_hooks_opt *options)\n+{\n+\tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n+\tstruct hook_cb_data cb_data = { 0, hookname, NULL, NULL, options };\n+\n+\tif (!options)\n+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n+\n+\tif (options->path_to_stdin && options->feed_pipe)\n+\t\tBUG(\"choose only one method to populate stdin\");\n+\n+\tto_run = hook_list(hookname);\n+\n+\tlist_for_each_safe(pos, tmp, to_run) {\n+\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n+\n+\t\tif (hook->from_hookdir &&\n+\t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n+\t\t\t    list_del(pos);\n \t}\n-\tcb_data.run_me = &my_hook;\n \n-\tif (options->jobs != 1)\n-\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n+\tif (list_empty(to_run))\n+\t\treturn 0;\n+\n+\tcb_data.head = to_run;\n+\tcb_data.run_me = list_entry(to_run->next, struct hook, list);\n \n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n@@ -170,29 +477,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n-\t\t\t\t   hook_name);\n-\tif (options->absolute_path)\n-\t\tstrbuf_release(&abs_path);\n+\t\t\t\t   hookname);\n \n \treturn cb_data.rc;\n }\n-\n-int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n-{\n-\tconst char *hook_path;\n-\tint ret;\n-\tif (!options)\n-\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n-\n-\tif (options->path_to_stdin && options->feed_pipe)\n-\t\tBUG(\"choose only one method to populate stdin\");\n-\n-\thook_path = find_hook(hook_name);\n-\n-\t/* Care about nonexistence? Use run_found_hooks() */\n-\tif (!hook_path)\n-\t\treturn 0;\n-\n-\tret = run_found_hooks(hook_name, hook_path, options);\n-\treturn ret;\n-}\ndiff --git a/hook.h b/hook.h\nindex 5f895032341..f32189380a8 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,12 +1,19 @@\n-#ifndef HOOK_H\n-#define HOOK_H\n+#include \"config.h\"\n+#include \"list.h\"\n #include \"strbuf.h\"\n #include \"strvec.h\"\n #include \"run-command.h\"\n \n struct hook {\n-\t/* The path to the hook */\n-\tconst char *hook_path;\n+\tstruct list_head list;\n+\t/*\n+\t * Config file which holds the hook.*.command definition.\n+\t * (This has nothing to do with the hookcmd.<name>.* configs.)\n+\t */\n+\tenum config_scope origin;\n+\t/* The literal command to run. */\n+\tstruct strbuf command;\n+\tunsigned from_hookdir : 1;\n \n \t/*\n \t * Use this to keep state for your feed_pipe_fn if you are using\n@@ -15,6 +22,32 @@ struct hook {\n \tvoid *feed_pipe_cb_data;\n };\n \n+/*\n+ * Provides a linked list of 'struct hook' detailing commands which should run\n+ * in response to the 'hookname' event, in execution order.\n+ */\n+struct list_head* hook_list(const char *hookname);\n+\n+enum hookdir_opt\n+{\n+\tHOOKDIR_USE_CONFIG,\n+\tHOOKDIR_NO,\n+\tHOOKDIR_ERROR,\n+\tHOOKDIR_WARN,\n+\tHOOKDIR_INTERACTIVE,\n+\tHOOKDIR_YES,\n+\tHOOKDIR_UNKNOWN,\n+};\n+\n+/*\n+ * Provides the hookdir_opt specified in the config without consulting any\n+ * command line arguments.\n+ */\n+enum hookdir_opt configured_hookdir_opt(void);\n+\n+/* Provides the number of threads to use for parallel hook execution. */\n+int configured_hook_jobs(void);\n+\n struct run_hooks_opt\n {\n \t/* Environment vars to be set for each hook */\n@@ -23,20 +56,15 @@ struct run_hooks_opt\n \t/* Args to be passed to each hook */\n \tstruct strvec args;\n \n-\t/* Number of threads to parallelize across */\n-\tint jobs;\n-\n-\t/* Resolve and run the \"absolute_path(hook)\" instead of\n-\t * \"hook\". Used for \"git worktree\" hooks\n+\t/*\n+\t * How should the hookdir be handled?\n+\t * Leave the run_hooks_opt_init_*() default in most cases; this only needs\n+\t * to be overridden if the user can override it at the command line.\n \t */\n-\tint absolute_path;\n-\n-\t/* Path to initial working directory for subprocess */\n-\tconst char *dir;\n+\tenum hookdir_opt run_hookdir;\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n-\n \t/*\n \t * Callback and state pointer to ask for more content to pipe to stdin.\n \t * Will be called repeatedly, for each hook. See\n@@ -57,13 +85,14 @@ struct run_hooks_opt\n \t * for an example.\n \t */\n \tconsume_sideband_fn consume_sideband;\n-};\n \n-#define RUN_HOOKS_OPT_INIT { \\\n-\t.jobs = 1, \\\n-\t.env = STRVEC_INIT, \\\n-\t.args = STRVEC_INIT, \\\n-}\n+\t/* Number of threads to parallelize across */\n+\tint jobs;\n+\n+\t/* Path to initial working directory for subprocess */\n+\tconst char *dir;\n+\n+};\n \n /*\n  * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n@@ -78,35 +107,33 @@ int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n  */\n struct hook_cb_data {\n \tint rc;\n-\tconst char *hook_name;\n+\tconst char *hookname;\n+\tstruct list_head *head;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n };\n \n-/*\n- * Returns the path to the hook file, or NULL if the hook is missing\n- * or disabled. Note that this points to static storage that will be\n- * overwritten by further calls to find_hook and run_hook_*.\n- */\n-const char *find_hook(const char *name);\n-\n-/*\n- * A boolean version of find_hook()\n- */\n-int hook_exists(const char *hookname);\n-\n+void run_hooks_opt_init_sync(struct run_hooks_opt *o);\n+void run_hooks_opt_init_async(struct run_hooks_opt *o);\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /*\n- * Calls find_hook(hookname) and runs the hooks (if any) with\n- * run_found_hooks().\n+ * Returns 1 if any hooks are specified in the config or if a hook exists in the\n+ * hookdir. Typically, invoke hook_exsts() like:\n+ *   hook_exists(hookname, configured_hookdir_opt());\n+ * Like with run_hooks, if you take a --run-hookdir flag, reflect that\n+ * user-specified behavior here instead.\n  */\n-int run_hooks(const char *hook_name, struct run_hooks_opt *options);\n+int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir);\n \n /*\n- * Takes an already resolved hook and runs it. Internally the simpler\n- * run_hooks() will call this.\n+ * Runs all hooks associated to the 'hookname' event in order. Each hook will be\n+ * passed 'env' and 'args'. The file at 'stdin_path' will be closed and reopened\n+ * for each hook that runs.\n  */\n-int run_found_hooks(const char *hookname, const char *hook_path,\n-\t\t    struct run_hooks_opt *options);\n-#endif\n+int run_hooks(const char *hookname, struct run_hooks_opt *options);\n+\n+/* Free memory associated with a 'struct hook' */\n+void free_hook(struct hook *ptr);\n+/* Empties the list at 'head', calling 'free_hook()' on each entry */\n+void clear_hook_list(struct list_head *head);\ndiff --git a/pack-objects.h b/pack-objects.h\nindex dca2351ef94..9d88e3e518f 100644\n--- a/pack-objects.h\n+++ b/pack-objects.h\n@@ -268,10 +268,152 @@ static inline void oe_set_in_pack(struct packing_data *pack,\n \tpack->in_pack[e - pack->objects] = p;\n }\n \n+static inline struct object_entry *oe_delta(\n+\t\tconst struct packing_data *pack,\n+\t\tconst struct object_entry *e)\n+{\n+\tif (!e->delta_idx)\n+\t\treturn NULL;\n+\tif (e->ext_base)\n+\t\treturn &pack->ext_bases[e->delta_idx - 1];\n+\telse\n+\t\treturn &pack->objects[e->delta_idx - 1];\n+}\n+\n+static inline void oe_set_delta(struct packing_data *pack,\n+\t\t\t\tstruct object_entry *e,\n+\t\t\t\tstruct object_entry *delta)\n+{\n+\tif (delta)\n+\t\te->delta_idx = (delta - pack->objects) + 1;\n+\telse\n+\t\te->delta_idx = 0;\n+}\n+\n void oe_set_delta_ext(struct packing_data *pack,\n \t\t      struct object_entry *e,\n \t\t      const struct object_id *oid);\n \n+static inline struct object_entry *oe_delta_child(\n+\t\tconst struct packing_data *pack,\n+\t\tconst struct object_entry *e)\n+{\n+\tif (e->delta_child_idx)\n+\t\treturn &pack->objects[e->delta_child_idx - 1];\n+\treturn NULL;\n+}\n+\n+static inline void oe_set_delta_child(struct packing_data *pack,\n+\t\t\t\t      struct object_entry *e,\n+\t\t\t\t      struct object_entry *delta)\n+{\n+\tif (delta)\n+\t\te->delta_child_idx = (delta - pack->objects) + 1;\n+\telse\n+\t\te->delta_child_idx = 0;\n+}\n+\n+static inline struct object_entry *oe_delta_sibling(\n+\t\tconst struct packing_data *pack,\n+\t\tconst struct object_entry *e)\n+{\n+\tif (e->delta_sibling_idx)\n+\t\treturn &pack->objects[e->delta_sibling_idx - 1];\n+\treturn NULL;\n+}\n+\n+static inline void oe_set_delta_sibling(struct packing_data *pack,\n+\t\t\t\t\tstruct object_entry *e,\n+\t\t\t\t\tstruct object_entry *delta)\n+{\n+\tif (delta)\n+\t\te->delta_sibling_idx = (delta - pack->objects) + 1;\n+\telse\n+\t\te->delta_sibling_idx = 0;\n+}\n+\n+unsigned long oe_get_size_slow(struct packing_data *pack,\n+\t\t\t       const struct object_entry *e);\n+static inline unsigned long oe_size(struct packing_data *pack,\n+\t\t\t\t    const struct object_entry *e)\n+{\n+\tif (e->size_valid)\n+\t\treturn e->size_;\n+\n+\treturn oe_get_size_slow(pack, e);\n+}\n+\n+static inline int oe_size_less_than(struct packing_data *pack,\n+\t\t\t\t    const struct object_entry *lhs,\n+\t\t\t\t    unsigned long rhs)\n+{\n+\tif (lhs->size_valid)\n+\t\treturn lhs->size_ < rhs;\n+\tif (rhs < pack->oe_size_limit) /* rhs < 2^x <= lhs ? */\n+\t\treturn 0;\n+\treturn oe_get_size_slow(pack, lhs) < rhs;\n+}\n+\n+static inline int oe_size_greater_than(struct packing_data *pack,\n+\t\t\t\t       const struct object_entry *lhs,\n+\t\t\t\t       unsigned long rhs)\n+{\n+\tif (lhs->size_valid)\n+\t\treturn lhs->size_ > rhs;\n+\tif (rhs < pack->oe_size_limit) /* rhs < 2^x <= lhs ? */\n+\t\treturn 1;\n+\treturn oe_get_size_slow(pack, lhs) > rhs;\n+}\n+\n+static inline void oe_set_size(struct packing_data *pack,\n+\t\t\t       struct object_entry *e,\n+\t\t\t       unsigned long size)\n+{\n+\tif (size < pack->oe_size_limit) {\n+\t\te->size_ = size;\n+\t\te->size_valid = 1;\n+\t} else {\n+\t\te->size_valid = 0;\n+\t\tif (oe_get_size_slow(pack, e) != size)\n+\t\t\tBUG(\"'size' is supposed to be the object size!\");\n+\t}\n+}\n+\n+static inline unsigned long oe_delta_size(struct packing_data *pack,\n+\t\t\t\t\t  const struct object_entry *e)\n+{\n+\tif (e->delta_size_valid)\n+\t\treturn e->delta_size_;\n+\n+\t/*\n+\t * pack->delta_size[] can't be NULL because oe_set_delta_size()\n+\t * must have been called when a new delta is saved with\n+\t * oe_set_delta().\n+\t * If oe_delta() returns NULL (i.e. default state, which means\n+\t * delta_size_valid is also false), then the caller must never\n+\t * call oe_delta_size().\n+\t */\n+\treturn pack->delta_size[e - pack->objects];\n+}\n+\n+static inline void oe_set_delta_size(struct packing_data *pack,\n+\t\t\t\t     struct object_entry *e,\n+\t\t\t\t     unsigned long size)\n+{\n+\tif (size < pack->oe_delta_size_limit) {\n+\t\te->delta_size_ = size;\n+\t\te->delta_size_valid = 1;\n+\t} else {\n+\t\tpacking_data_lock(pack);\n+\t\tif (!pack->delta_size)\n+\t\t\tALLOC_ARRAY(pack->delta_size, pack->nr_alloc);\n+\t\tpacking_data_unlock(pack);\n+\n+\t\tpack->delta_size[e - pack->objects] = size;\n+\t\te->delta_size_valid = 0;\n+\t}\n+}\n+\n static inline unsigned int oe_tree_depth(struct packing_data *pack,\n \t\t\t\t\t struct object_entry *e)\n {\n@@ -280,6 +422,23 @@ static inline unsigned int oe_tree_depth(struct packing_data *pack,\n \treturn pack->tree_depth[e - pack->objects];\n }\n \n+static inline void oe_set_tree_depth(struct packing_data *pack,\n+\t\t\t\t     struct object_entry *e,\n+\t\t\t\t     unsigned int tree_depth)\n+{\n+\tif (!pack->tree_depth)\n+\t\tCALLOC_ARRAY(pack->tree_depth, pack->nr_alloc);\n+\tpack->tree_depth[e - pack->objects] = tree_depth;\n+}\n+\n+static inline unsigned char oe_layer(struct packing_data *pack,\n+\t\t\t\t     struct object_entry *e)\n+{\n+\tif (!pack->layer)\n+\t\treturn 0;\n+\treturn pack->layer[e - pack->objects];\n+}\n+\n static inline void oe_set_layer(struct packing_data *pack,\n \t\t\t\tstruct object_entry *e,\n \t\t\t\tunsigned char layer)\ndiff --git a/parse-options-cb.c b/parse-options-cb.c\nindex 3c811e1e4a7..8227499eb6f 100644\n--- a/parse-options-cb.c\n+++ b/parse-options-cb.c\n@@ -207,6 +207,22 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset)\n \treturn 0;\n }\n \n+int parse_opt_strvec(const struct option *opt, const char *arg, int unset)\n+{\n+\tstruct strvec *v = opt->value;\n+\n+\tif (unset) {\n+\t\tstrvec_clear(v);\n+\t\treturn 0;\n+\t}\n+\n+\tif (!arg)\n+\t\treturn -1;\n+\n+\tstrvec_push(v, arg);\n+\treturn 0;\n+}\n+\n int parse_opt_noop_cb(const struct option *opt, const char *arg, int unset)\n {\n \treturn 0;\ndiff --git a/parse-options.h b/parse-options.h\nindex a845a9d9527..fcb0f1f31eb 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -178,6 +178,9 @@ struct option {\n #define OPT_STRING_LIST(s, l, v, a, h) \\\n \t\t\t\t    { OPTION_CALLBACK, (s), (l), (v), (a), \\\n \t\t\t\t      (h), 0, &parse_opt_string_list }\n+#define OPT_STRVEC(s, l, v, a, h) \\\n+\t\t\t\t    { OPTION_CALLBACK, (s), (l), (v), (a), \\\n+\t\t\t\t      (h), 0, &parse_opt_strvec }\n #define OPT_UYN(s, l, v, h)         { OPTION_CALLBACK, (s), (l), (v), NULL, \\\n \t\t\t\t      (h), PARSE_OPT_NOARG, &parse_opt_tertiary }\n #define OPT_EXPIRY_DATE(s, l, v, h) \\\n@@ -297,6 +300,7 @@ int parse_opt_commits(const struct option *, const char *, int);\n int parse_opt_commit(const struct option *, const char *, int);\n int parse_opt_tertiary(const struct option *, const char *, int);\n int parse_opt_string_list(const struct option *, const char *, int);\n+int parse_opt_strvec(const struct option *, const char *, int);\n int parse_opt_noop_cb(const struct option *, const char *, int);\n enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx,\n \t\t\t\t\t   const struct option *,\ndiff --git a/perl/Git.pm b/perl/Git.pm\nindex 02eacef0c2a..73ebbf80cc6 100644\n--- a/perl/Git.pm\n+++ b/perl/Git.pm\n@@ -619,6 +619,19 @@ sub _prompt {\n \n sub repo_path { $_[0]->{opts}->{Repository} }\n \n+=item hooks_path ()\n+\n+Return path to the hooks directory. Must be called on a repository instance.\n+\n+=cut\n+\n+sub hooks_path {\n+\tmy ($self) = @_;\n+\n+\tmy $dir = $self->command_oneline('rev-parse', '--git-path', 'hooks');\n+\tmy $abs = abs_path($dir);\n+\treturn $abs;\n+}\n \n =item wc_path ()\n \ndiff --git a/read-cache.c b/read-cache.c\nindex a17bc30f870..ebb9c190562 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3132,7 +3132,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt;\n \n \tret = convert_to_sparse(istate);\n \n@@ -3161,6 +3161,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \telse\n \t\tret = close_lock_file_gently(lock);\n \n+\trun_hooks_opt_init_async(&hook_opt);\n \tstrvec_pushl(&hook_opt.args,\n \t\t     istate->updated_workdir ? \"1\" : \"0\",\n \t\t     istate->updated_skipworktree ? \"1\" : \"0\",\ndiff --git a/refs.c b/refs.c\nindex 1149e7e7dcb..32e993aaff3 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -10,7 +10,6 @@\n #include \"refs.h\"\n #include \"refs/refs-internal.h\"\n #include \"run-command.h\"\n-#include \"hook.h\"\n #include \"object-store.h\"\n #include \"object.h\"\n #include \"tag.h\"\n@@ -19,6 +18,7 @@\n #include \"strvec.h\"\n #include \"repository.h\"\n #include \"sigchain.h\"\n+#include \"hook.h\"\n \n /*\n  * List of all available backends\n@@ -2063,12 +2063,14 @@ static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint ret = 0, i;\n \tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n \n-\tif (!hook_exists(\"reference-transaction\"))\n+\trun_hooks_opt_init_async(&opt);\n+\n+\tif (!hook_exists(\"reference-transaction\", HOOKDIR_USE_CONFIG))\n \t\treturn ret;\n \n \tstrvec_push(&opt.args, state);\ndiff --git a/reset.c b/reset.c\nindex e6af33b901c..48d45f5b792 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -128,7 +128,9 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\t\t\t    reflog_head);\n \t}\n \tif (run_hook) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt;\n+\n+\t\trun_hooks_opt_init_sync(&opt);\n \t\tstrvec_pushl(&opt.args,\n \t\t\t     oid_to_hex(orig ? orig : null_oid()),\n \t\t\t     oid_to_hex(oid),\ndiff --git a/run-command.c b/run-command.c\nindex 4a1a7a10820..2ff76f3c2f1 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -8,7 +8,6 @@\n #include \"string-list.h\"\n #include \"quote.h\"\n #include \"config.h\"\n-#include \"hook.h\"\n \n void child_process_init(struct child_process *child)\n {\ndiff --git a/sequencer.c b/sequencer.c\nindex ec2761e47d9..3fa76687639 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -8,7 +8,6 @@\n #include \"sequencer.h\"\n #include \"tag.h\"\n #include \"run-command.h\"\n-#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"utf8.h\"\n #include \"cache-tree.h\"\n@@ -35,6 +34,7 @@\n #include \"commit-reach.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n #include \"string-list.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n@@ -1148,11 +1148,13 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tstruct strbuf tmp = STRBUF_INIT;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n \n+\trun_hooks_opt_init_async(&opt);\n+\n \tstrvec_push(&opt.args, \"amend\");\n \n \tstrbuf_addf(&tmp,\n@@ -1204,7 +1206,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n \t} else {\n \t\targ1 = \"message\";\n \t}\n-\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n+\tif (run_commit_hook(0, 0, r->index_file, \"prepare-commit-msg\", name,\n \t\t\t    arg1, arg2, NULL))\n \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n \n@@ -1442,7 +1444,7 @@ static int try_to_commit(struct repository *r,\n \t\t}\n \t}\n \n-\tif (hook_exists(\"prepare-commit-msg\")) {\n+\tif (hook_exists(\"prepare-commit-msg\", HOOKDIR_USE_CONFIG)) {\n \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n \t\tif (res)\n \t\t\tgoto out;\n@@ -1534,7 +1536,7 @@ static int try_to_commit(struct repository *r,\n \t\tgoto out;\n \t}\n \n-\trun_commit_hook(0, r->index_file, \"post-commit\", NULL);\n+\trun_commit_hook(0, 1, r->index_file, \"post-commit\", NULL);\n \tif (flags & AMEND_MSG)\n \t\tcommit_post_rewrite(r, current_head, oid);\n \n@@ -4524,7 +4526,9 @@ static int pick_commits(struct repository *r,\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n \t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n-\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt;\n+\n+\t\t\trun_hooks_opt_init_async(&hook_opt);\n \n \t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n \t\t\tnotes_cp.git_cmd = 1;\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 2051ce57db7..922af561567 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -2,6 +2,7 @@\n #include \"cache.h\"\n #include \"parse-options.h\"\n #include \"string-list.h\"\n+#include \"strvec.h\"\n #include \"trace2.h\"\n \n static int boolean = 0;\n@@ -15,6 +16,7 @@ static char *string = NULL;\n static char *file = NULL;\n static int ambiguous;\n static struct string_list list = STRING_LIST_INIT_NODUP;\n+static struct strvec vector = STRVEC_INIT;\n \n static struct {\n \tint called;\n@@ -133,6 +135,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_STRING('o', NULL, &string, \"str\", \"get another string\"),\n \t\tOPT_NOOP_NOARG(0, \"obsolete\"),\n \t\tOPT_STRING_LIST(0, \"list\", &list, \"str\", \"add str to list\"),\n+\t\tOPT_STRVEC(0, \"vector\", &vector, \"str\", \"add str to strvec\"),\n \t\tOPT_GROUP(\"Magic arguments\"),\n \t\tOPT_ARGUMENT(\"quux\", NULL, \"means --quux\"),\n \t\tOPT_NUMBER_CALLBACK(&integer, \"set integer to NUM\",\n@@ -183,6 +186,9 @@ int cmd__parse_options(int argc, const char **argv)\n \tfor (i = 0; i < list.nr; i++)\n \t\tshow(&expect, &ret, \"list: %s\", list.items[i].string);\n \n+\tfor (i = 0; i < vector.nr; i++)\n+\t\tshow(&expect, &ret, \"vector: %s\", vector.v[i]);\n+\n \tfor (i = 0; i < argc; i++)\n \t\tshow(&expect, &ret, \"arg %02d: %s\", i, argv[i]);\n \ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ad4746d899a..485e0170bff 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -35,6 +35,7 @@ String options\n     --st <st>             get another string (pervert ordering)\n     -o <str>              get another string\n     --list <str>          add str to list\n+    --vector <str>        add str to strvec\n \n Magic arguments\n     --quux                means --quux\n@@ -386,6 +387,32 @@ test_expect_success '--no-list resets list' '\n \ttest_cmp expect output\n '\n \n+cat >expect <<\\EOF\n+boolean: 0\n+integer: 0\n+magnitude: 0\n+timestamp: 0\n+string: (not set)\n+abbrev: 7\n+verbose: -1\n+quiet: 0\n+dry run: no\n+file: (not set)\n+vector: foo\n+vector: bar\n+vector: baz\n+EOF\n+test_expect_success '--vector keeps list of strings' '\n+\ttest-tool parse-options --vector foo --vector=bar --vector=baz >output &&\n+\ttest_cmp expect output\n+'\n+\n+test_expect_success '--no-vector resets list' '\n+\ttest-tool parse-options --vector=other --vector=irrelevant --vector=options \\\n+\t\t--no-vector --vector=foo --vector=bar --vector=baz >output &&\n+\ttest_cmp expect output\n+'\n+\n test_expect_success 'multiple quiet levels' '\n \ttest-tool parse-options --expect=\"quiet: 3\" -q -q -q\n '\ndiff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\nindex e9a815ca7aa..12e6c453024 100755\n--- a/t/t1092-sparse-checkout-compatibility.sh\n+++ b/t/t1092-sparse-checkout-compatibility.sh\n@@ -106,18 +106,18 @@ init_repos () {\n run_on_sparse () {\n \t(\n \t\tcd sparse-checkout &&\n-\t\tGIT_PROGRESS_DELAY=100000 \"$@\" >../sparse-checkout-out 2>../sparse-checkout-err\n+\t\t\"$@\" >../sparse-checkout-out 2>../sparse-checkout-err\n \t) &&\n \t(\n \t\tcd sparse-index &&\n-\t\tGIT_PROGRESS_DELAY=100000 \"$@\" >../sparse-index-out 2>../sparse-index-err\n+\t\t\"$@\" >../sparse-index-out 2>../sparse-index-err\n \t)\n }\n \n run_on_all () {\n \t(\n \t\tcd full-checkout &&\n-\t\tGIT_PROGRESS_DELAY=100000 \"$@\" >../full-checkout-out 2>../full-checkout-err\n+\t\t\"$@\" >../full-checkout-out 2>../full-checkout-err\n \t) &&\n \trun_on_sparse \"$@\"\n }\ndiff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\nindex fa9647a7c0b..f1f9aee9f5d 100755\n--- a/t/t1350-config-hooks-path.sh\n+++ b/t/t1350-config-hooks-path.sh\n@@ -5,7 +5,6 @@ test_description='Test the core.hooksPath configuration variable'\n . ./test-lib.sh\n \n test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n-\t>actual &&\n \tmkdir -p .git/custom-hooks .git/hooks &&\n \twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n \techo CUSTOM >>actual\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nnew file mode 100755\nindex 00000000000..43917172d74\n--- /dev/null\n+++ b/t/t1360-config-based-hooks.sh\n@@ -0,0 +1,329 @@\n+#!/bin/bash\n+\n+test_description='config-managed multihooks, including git-hook command'\n+\n+. ./test-lib.sh\n+\n+ROOT=\n+if test_have_prereq MINGW\n+then\n+\t# In Git for Windows, Unix-like paths work only in shell scripts;\n+\t# `git.exe`, however, will prefix them with the pseudo root directory\n+\t# (of the Unix shell). Let's accommodate for that.\n+\tROOT=\"$(cd / && pwd)\"\n+fi\n+\n+setup_hooks () {\n+\ttest_config hook.pre-commit.command \"/path/ghi\" --add\n+\ttest_config_global hook.pre-commit.command \"/path/def\" --add\n+}\n+\n+setup_hookcmd () {\n+\ttest_config hook.pre-commit.command \"abc\" --add\n+\ttest_config_global hookcmd.abc.command \"/path/abc\" --add\n+}\n+\n+setup_hookdir () {\n+\tmkdir .git/hooks\n+\twrite_script .git/hooks/pre-commit <<-EOF\n+\techo \\\"Legacy Hook\\\"\n+\tEOF\n+\ttest_when_finished rm -rf .git/hooks\n+}\n+\n+test_expect_success 'git hook rejects commands without a mode' '\n+\ttest_must_fail git hook pre-commit\n+'\n+\n+\n+test_expect_success 'git hook rejects commands without a hookname' '\n+\ttest_must_fail git hook list\n+'\n+\n+test_expect_success 'git hook runs outside of a repo' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tEOF\n+\n+\tnongit git config --list --global &&\n+\n+\tnongit git hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list orders by config order' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tlocal: $ROOT/path/ghi\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list dereferences a hookcmd' '\n+\tsetup_hooks &&\n+\tsetup_hookcmd &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tlocal: $ROOT/path/ghi\n+\tlocal: $ROOT/path/abc\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list reorders on duplicate commands' '\n+\tsetup_hooks &&\n+\n+\ttest_config hook.pre-commit.command \"/path/def\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tlocal: $ROOT/path/ghi\n+\tlocal: $ROOT/path/def\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list shows hooks from the hookdir' '\n+\tsetup_hookdir &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hook.runHookDir = no is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"no\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will not run)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual &&\n+\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success 'hook.runHookDir = error is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"error\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will error and not run)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual &&\n+\n+\tcat >expected <<-EOF &&\n+\tSkipping legacy hook at '\\''$(pwd)/.git/hooks/pre-commit'\\''\n+\tEOF\n+\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hook.runHookDir = warn is respected by list' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"warn\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will warn but run)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual &&\n+\n+\tcat >expected <<-EOF &&\n+\tRunning legacy hook at '\\''$(pwd)/.git/hooks/pre-commit'\\''\n+\t\"Legacy Hook\"\n+\tEOF\n+\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list removes skipped hookcmd' '\n+\tsetup_hookcmd &&\n+\ttest_config hookcmd.abc.skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tno commands configured for hook '\\''pre-commit'\\''\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list ignores skip referring to unused hookcmd' '\n+\ttest_config hookcmd.abc.command \"/path/abc\" --add &&\n+\ttest_config hookcmd.abc.skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tno commands configured for hook '\\''pre-commit'\\''\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list removes skipped inlined hook' '\n+\tsetup_hooks &&\n+\ttest_config hookcmd.\"$ROOT/path/ghi\".skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal: $ROOT/path/def\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hook.runHookDir = interactive is respected by list and run' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"interactive\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit (will prompt)\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual &&\n+\n+\ttest_write_lines n | git hook run pre-commit 2>actual &&\n+\t! grep \"Legacy Hook\" actual &&\n+\n+\ttest_write_lines y | git hook run pre-commit 2>actual &&\n+\tgrep \"Legacy Hook\" actual\n+'\n+\n+test_expect_success 'inline hook definitions execute oneliners' '\n+\ttest_config hook.pre-commit.command \"echo \\\"Hello World\\\"\" &&\n+\n+\techo \"Hello World\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions resolve paths' '\n+\twrite_script sample-hook.sh <<-EOF &&\n+\techo \\\"Sample Hook\\\"\n+\tEOF\n+\n+\ttest_when_finished \"rm sample-hook.sh\" &&\n+\n+\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\techo \\\"Sample Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook run can pass args and env vars' '\n+\twrite_script sample-hook.sh <<-\\EOF &&\n+\techo $1\n+\techo $2\n+\techo $TEST_ENV_1\n+\techo $TEST_ENV_2\n+\tEOF\n+\n+\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\tcat >expected <<-EOF &&\n+\targ1\n+\targ2\n+\tenv1\n+\tenv2\n+\tEOF\n+\n+\tgit hook run --arg arg1 \\\n+\t\t--env TEST_ENV_1=env1 \\\n+\t\t-a arg2 \\\n+\t\t-e TEST_ENV_2=env2 \\\n+\t\tpre-commit 2>actual &&\n+\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hookdir hook included in git hook run' '\n+\tsetup_hookdir &&\n+\n+\techo \\\"Legacy Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'out-of-repo runs excluded' '\n+\tsetup_hooks &&\n+\n+\tnongit test_must_fail git hook run pre-commit\n+'\n+\n+test_expect_success 'hook.runHookDir is tolerant to unknown values' '\n+\tsetup_hookdir &&\n+\n+\ttest_config hook.runHookDir \"junk\" &&\n+\n+\tcat >expected <<-EOF &&\n+\thookdir: $(pwd)/.git/hooks/pre-commit\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\t# the hookdir annotation is translated\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'stdin to multiple hooks' '\n+\tgit config --add hook.test.command \"xargs -P1 -I% echo a%\" &&\n+\tgit config --add hook.test.command \"xargs -P1 -I% echo b%\" &&\n+\ttest_when_finished \"test_unconfig hook.test.command\" &&\n+\n+\tcat >input <<-EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tcat >expected <<-EOF &&\n+\ta1\n+\ta2\n+\ta3\n+\tb1\n+\tb2\n+\tb3\n+\tEOF\n+\n+\tgit hook run --to-stdin=input test 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_done\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex 6c941027a81..0a3c3e4a861 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -124,12 +124,12 @@ test_expect_success 'interleaving hook calls succeed' '\n \tEOF\n \n \tcat >expect <<-EOF &&\n-\t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n-\t\thooks/reference-transaction prepared\n-\t\thooks/reference-transaction committed\n-\t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n-\t\thooks/reference-transaction prepared\n-\t\thooks/reference-transaction committed\n+\t\t$(pwd)/target-repo.git/hooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n+\t\t$(pwd)/target-repo.git/hooks/update refs/tags/POST $ZERO_OID $POST_OID\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction prepared\n+\t\t$(pwd)/target-repo.git/hooks/reference-transaction committed\n \tEOF\n \n \tgit push ./target-repo.git PRE POST &&\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\ndeleted file mode 100755\nindex 9e2dd64275c..00000000000\n--- a/t/t1800-hook.sh\n+++ /dev/null\n@@ -1,158 +0,0 @@\n-#!/bin/bash\n-\n-test_description='git-hook command'\n-\n-. ./test-lib.sh\n-\n-test_expect_success 'setup .git/hooks' '\n-\tmkdir .git/hooks\n-'\n-\n-test_expect_success 'git hook run -- nonexistent hook' '\n-\tcat >stderr.expect <<-\\EOF &&\n-\terror: cannot find a hook named does-not-exist\n-\tEOF\n-\ttest_expect_code 1 git hook run does-not-exist 2>stderr.actual &&\n-\ttest_cmp stderr.expect stderr.actual\n-'\n-\n-test_expect_success 'git hook run -- nonexistent hook with --ignore-missing' '\n-\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n-\ttest_must_be_empty stderr.actual\n-'\n-\n-test_expect_success 'git hook run -- basic' '\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\techo Test hook\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tTest hook\n-\tEOF\n-\tgit hook run test-hook 2>actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'git hook run -- stdout and stderr handling' '\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\techo >&1 Will end up on stderr\n-\techo >&2 Will end up on stderr\n-\tEOF\n-\n-\tcat >stderr.expect <<-\\EOF &&\n-\tWill end up on stderr\n-\tWill end up on stderr\n-\tEOF\n-\tgit hook run test-hook >stdout.actual 2>stderr.actual &&\n-\ttest_cmp stderr.expect stderr.actual &&\n-\ttest_must_be_empty stdout.actual\n-'\n-\n-test_expect_success 'git hook run -- exit codes are passed along' '\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\texit 1\n-\tEOF\n-\n-\ttest_expect_code 1 git hook run test-hook &&\n-\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\texit 2\n-\tEOF\n-\n-\ttest_expect_code 2 git hook run test-hook &&\n-\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\texit 128\n-\tEOF\n-\n-\ttest_expect_code 128 git hook run test-hook &&\n-\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\texit 129\n-\tEOF\n-\n-\ttest_expect_code 129 git hook run test-hook\n-'\n-\n-test_expect_success 'git hook run arg u ments without -- is not allowed' '\n-\ttest_expect_code 129 git hook run test-hook arg u ments\n-'\n-\n-test_expect_success 'git hook run -- pass arguments' '\n-\twrite_script .git/hooks/test-hook <<-\\EOF &&\n-\techo $1\n-\techo $2\n-\tEOF\n-\n-\tcat >expect <<-EOF &&\n-\targ\n-\tu ments\n-\tEOF\n-\n-\tgit hook run test-hook -- arg \"u ments\" 2>actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'git hook run -- out-of-repo runs excluded' '\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\techo Test hook\n-\tEOF\n-\n-\tnongit test_must_fail git hook run test-hook\n-'\n-\n-test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n-\tmkdir my-hooks &&\n-\twrite_script my-hooks/test-hook <<-EOF &&\n-\techo Hook ran >>actual\n-\tEOF\n-\n-\tcat >expect <<-\\EOF &&\n-\tTest hook\n-\tHook ran\n-\tHook ran\n-\tHook ran\n-\tHook ran\n-\tEOF\n-\n-\t# Test various ways of specifying the path. See also\n-\t# t1350-config-hooks-path.sh\n-\t>actual &&\n-\tgit hook run test-hook 2>>actual &&\n-\tgit -c core.hooksPath=my-hooks hook run test-hook 2>>actual &&\n-\tgit -c core.hooksPath=my-hooks/ hook run test-hook 2>>actual &&\n-\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook 2>>actual &&\n-\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook 2>>actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n-\t>actual &&\n-\tmkdir -p .git/custom-hooks .git/hooks &&\n-\twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n-\techo CUSTOM >>actual\n-\tEOF\n-\twrite_script .git/hooks/pre-commit <<-\\EOF\n-\techo NORMAL >>actual\n-\tEOF\n-'\n-\n-test_expect_success 'stdin to hooks' '\n-\twrite_script .git/hooks/test-hook <<-\\EOF &&\n-\techo BEGIN stdin\n-\tcat\n-\techo END stdin\n-\tEOF\n-\n-\tcat >expect <<-EOF &&\n-\tBEGIN stdin\n-\thello\n-\tEND stdin\n-\tEOF\n-\n-\techo hello >input &&\n-\tgit hook run --to-stdin=input test-hook 2>actual &&\n-\ttest_cmp expect actual\n-'\n-\n-test_done\ndiff --git a/t/t2080-parallel-checkout-basics.sh b/t/t2080-parallel-checkout-basics.sh\nindex 3e0f8c675f7..7087818550f 100755\n--- a/t/t2080-parallel-checkout-basics.sh\n+++ b/t/t2080-parallel-checkout-basics.sh\n@@ -114,7 +114,7 @@ do\n \n \ttest_expect_success \"$mode checkout\" '\n \t\trepo=various_$mode &&\n-\t\tcp -R -P various $repo &&\n+\t\tcp -R various $repo &&\n \n \t\t# The just copied files have more recent timestamps than their\n \t\t# associated index entries. So refresh the cached timestamps\ndiff --git a/t/t5411/test-0015-too-many-hooks-error.sh b/t/t5411/test-0015-too-many-hooks-error.sh\nnew file mode 100644\nindex 00000000000..2d645345101\n--- /dev/null\n+++ b/t/t5411/test-0015-too-many-hooks-error.sh\n@@ -0,0 +1,47 @@\n+test_expect_success \"setup too  many proc-receive hooks (ok, $PROTOCOL)\" '\n+\twrite_script \"proc-receive\" <<-EOF &&\n+\tprintf >&2 \"# proc-receive hook\\n\"\n+\ttest-tool proc-receive -v \\\n+\t\t-r \"ok refs/for/main/topic\"\n+\tEOF\n+\n+\tgit -C \"$upstream\" config --add \"hook.proc-receive.command\" proc-receive &&\n+\tcp proc-receive \"$upstream/hooks/proc-receive\"\n+'\n+\n+# Refs of upstream : main(A)\n+# Refs of workbench: main(A)  tags/v123\n+# git push         :                       next(A)  refs/for/main/topic(A)\n+test_expect_success \"proc-receive: reject more than one configured hook\" '\n+\ttest_must_fail git -C workbench push origin \\\n+\t\tHEAD:next \\\n+\t\tHEAD:refs/for/main/topic \\\n+\t\t>out 2>&1 &&\n+\tmake_user_friendly_and_stable_output <out >actual &&\n+\tcat >expect <<-EOF &&\n+\tremote: # pre-receive hook\n+\tremote: pre-receive< <ZERO-OID> <COMMIT-A> refs/heads/next\n+\tremote: pre-receive< <ZERO-OID> <COMMIT-A> refs/for/main/topic\n+\tremote: error: only one \"proc-receive\" hook can be specified\n+\tremote: # post-receive hook\n+\tremote: post-receive< <ZERO-OID> <COMMIT-A> refs/heads/next\n+\tTo <URL/of/upstream.git>\n+\t * [new branch] HEAD -> next\n+\t ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)\n+\tEOF\n+\ttest_cmp expect actual &&\n+\tgit -C \"$upstream\" show-ref >out &&\n+\tmake_user_friendly_and_stable_output <out >actual &&\n+\tcat >expect <<-EOF &&\n+\t<COMMIT-A> refs/heads/main\n+\t<COMMIT-A> refs/heads/next\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+# Refs of upstream : main(A)             next(A)\n+# Refs of workbench: main(A)  tags/v123\n+test_expect_success \"cleanup ($PROTOCOL)\" '\n+\tgit -C \"$upstream\" config --unset \"hook.proc-receive.command\" \"proc-receive\" &&\n+\tgit -C \"$upstream\" update-ref -d refs/heads/next\n+'\ndiff --git a/t/t6500-gc.sh b/t/t6500-gc.sh\nindex 10c7ae7f09c..60d961b5260 100755\n--- a/t/t6500-gc.sh\n+++ b/t/t6500-gc.sh\n@@ -95,52 +95,6 @@ test_expect_success 'gc --keep-largest-pack' '\n \t)\n '\n \n-test_expect_success 'pre-auto-gc hook can stop auto gc' '\n-\tcat >err.expect <<-\\EOF &&\n-\tno gc for you\n-\tEOF\n-\n-\tgit init pre-auto-gc-hook &&\n-\t(\n-\t\tcd pre-auto-gc-hook &&\n-\t\twrite_script \".git/hooks/pre-auto-gc\" <<-\\EOF &&\n-\t\techo >&2 no gc for you &&\n-\t\texit 1\n-\t\tEOF\n-\n-\t\tgit config gc.auto 3 &&\n-\t\tgit config gc.autoDetach false &&\n-\n-\t\t# We need to create two object whose sha1s start with 17\n-\t\t# since this is what git gc counts.  As it happens, these\n-\t\t# two blobs will do so.\n-\t\ttest_commit \"$(test_oid obj1)\" &&\n-\t\ttest_commit \"$(test_oid obj2)\" &&\n-\n-\t\tgit gc --auto >../out.actual 2>../err.actual\n-\t) &&\n-\ttest_must_be_empty out.actual &&\n-\ttest_cmp err.expect err.actual &&\n-\n-\tcat >err.expect <<-\\EOF &&\n-\twill gc for you\n-\tAuto packing the repository for optimum performance.\n-\tSee \"git help gc\" for manual housekeeping.\n-\tEOF\n-\n-\t(\n-\t\tcd pre-auto-gc-hook &&\n-\t\twrite_script \".git/hooks/pre-auto-gc\" <<-\\EOF &&\n-\t\techo >&2 will gc for you &&\n-\t\texit 0\n-\t\tEOF\n-\t\tgit gc --auto >../out.actual 2>../err.actual\n-\t) &&\n-\n-\ttest_must_be_empty out.actual &&\n-\ttest_cmp err.expect err.actual\n-'\n-\n test_expect_success 'auto gc with too many loose objects does not attempt to create bitmaps' '\n \ttest_config gc.auto 3 &&\n \ttest_config gc.autodetach false &&\ndiff --git a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\nindex 606d8d0f089..e9e37130332 100755\n--- a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\n+++ b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\n@@ -8,8 +8,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n \n HOOKDIR=\"$(git rev-parse --git-dir)/hooks\"\n-PRECOMMIT=\"$HOOKDIR/pre-commit\"\n-PREMERGE=\"$HOOKDIR/pre-merge-commit\"\n+PRECOMMIT=\"$(pwd)/$HOOKDIR/pre-commit\"\n+PREMERGE=\"$(pwd)/$HOOKDIR/pre-merge-commit\"\n \n # Prepare sample scripts that write their $0 to actual_hooks\n test_expect_success 'sample script setup' '\n@@ -106,6 +106,19 @@ test_expect_success 'with succeeding hook' '\n \ttest_cmp expected_hooks actual_hooks\n '\n \n+# NEEDSWORK: when 'git hook add' and 'git hook remove' have been added, use that\n+# instead\n+test_expect_success 'with succeeding hook (config-based)' '\n+\ttest_when_finished \"git config --unset hook.pre-commit.command success.sample\" &&\n+\ttest_when_finished \"rm -f expected_hooks actual_hooks\" &&\n+\tgit config hook.pre-commit.command \"$HOOKDIR/success.sample\" &&\n+\techo \"$HOOKDIR/success.sample\" >expected_hooks &&\n+\techo \"more\" >>file &&\n+\tgit add file &&\n+\tgit commit -m \"more\" &&\n+\ttest_cmp expected_hooks actual_hooks\n+'\n+\n test_expect_success 'with succeeding hook (merge)' '\n \ttest_when_finished \"rm -f \\\"$PREMERGE\\\" expected_hooks actual_hooks\" &&\n \tcp \"$HOOKDIR/success.sample\" \"$PREMERGE\" &&\ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 35b513c015f..bdf64728710 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -539,15 +539,13 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n '\n \n test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n-\thooks_path=\"$(pwd)/my-hooks\" &&\n-\ttest_config core.hooksPath \"$hooks_path\" &&\n+\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n \ttest_when_finished \"rm my-hooks.ran\" &&\n \ttest_must_fail git send-email \\\n \t\t--from=\"Example <nobody@example.com>\" \\\n@@ -558,7 +556,6 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -2171,16 +2168,7 @@ test_expect_success $PREREQ 'invoke hook' '\n \tmkdir -p .git/hooks &&\n \n \twrite_script .git/hooks/sendemail-validate <<-\\EOF &&\n-\t# test that we have the correct environment variable, pwd, and\n-\t# argument\n-\tcase \"$GIT_DIR\" in\n-\t*.git)\n-\t\ttrue\n-\t\t;;\n-\t*)\n-\t\tfalse\n-\t\t;;\n-\tesac &&\n+\t# test that we have the correct argument\n \ttest -f 0001-add-main.patch &&\n \tgrep \"add main\" \"$1\"\n \tEOF\ndiff --git a/transport.c b/transport.c\nindex 1146ed3143c..91911076264 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -2,7 +2,6 @@\n #include \"config.h\"\n #include \"transport.h\"\n #include \"run-command.h\"\n-#include \"hook.h\"\n #include \"pkt-line.h\"\n #include \"fetch-pack.h\"\n #include \"remote.h\"\n@@ -23,6 +22,7 @@\n #include \"protocol.h\"\n #include \"object-store.h\"\n #include \"color.h\"\n+#include \"hook.h\"\n \n static int transport_use_color = -1;\n static char transport_colors[][COLOR_MAXLEN] = {\n@@ -1198,11 +1198,13 @@ static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n \tint ret = 0;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tstruct strbuf tmp = STRBUF_INIT;\n \tstruct ref *r;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \n+\trun_hooks_opt_init_async(&opt);\n+\n \tstrvec_push(&opt.args, transport->remote->name);\n \tstrvec_push(&opt.args, transport->url);\n \n"},{"id":"426210","messageId":"87y2bs7gyc.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YLZ5H2Muh39Q4M5T@google.com","subject":"Re: [PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-02T07:56:02Z","receivedAt":"2021-06-02T09:29:28Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Jun 01 2021, Emily Shaffer wrote:\n\n> On Fri, May 28, 2021 at 02:11:02PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> After suggesting[1] an another round that the config-based-hook\n>> topic[2] should take a more incremental approach to reach its end goal\n>> I thought I'd try hacking that up.\n>> \n>> So this is a proposed restart of that topic which if the consensus\n>> favors it should replace it, and the config-based hooks topic should\n>> be rebased on top of this.\n>\n> I'm not entirely sure what you're trying to achieve by sending this\n> series.\n\nI'm trying to convince you and others that an approach where we split up\nthe refactoring part of your large series from the behavior changes +\nnew behaviors it introduces is a better approach in getting us to your\ncurrently proposed (or some small variation thereof) desired end-state.\n\nI think the opening line of your reply doesn't bode for the \"convince\nyou\" part of that, maybe I'll do better on the \"and others\" front :)\n\nI'm a bit surprised at what seems to be some hostility or annoyance that\nI submitted this as a set of patches. That's ultimately something that\nsaves everyone involved time (well, except me by coming up with said\npatches). To borrow some words:\n\n    \"Talk is cheap. Show me the code.\" ― Linus Torvalds.\n\nIf I give you feedback suggesting that maybe we should reorganize this\nthing to split out refactorings from behavior changes I'm asking you to\ndo extra work. Ultimately neither I, you nor anyone else can really know\nif such a proposed effort is going to be better until it happens.\n\nWhen you get feedback in the form of restructured patches we skip past\nall that. We know it compiles, passes tests, and we can more concretely\nevaluate the proposal with diff, range-diff etc.\n\n> It was my impression that the existing config-based-hooks topic\n> was close to being ready to submit anyway (since Junio mentioned\n> submitting it a couple revisions ago); rather than churning by reviewing\n> a different 31-patch topic, and then re-rolling and re-reviewing a\n> (reduced) config hook topic, wouldn't it be easier on everyone's time to\n> do a final incremental review on the existing topic and then start in on\n> bugfixes/feature patches afterwards?\n\nI think it's fair to say that nobody's read your code in its current\nstate more thoroughly than I have at this point, but still, going back\nto it and paging through it even now a couple of days after reading the\nwhole thing line-by-line I find myself getting lost again.\n\nThat's because the whole structure of it is to conflate changing\nexisting behavior with the introduction of new behavior. This makes\nthings *much harder* for reviewers, because they need to be on toes\nabout regressions *and* evaluating the function/viability/sanity of new\nproposed semantics.\n\nI very much would like to see some approximation (sans my outstanding\ncomments, more on that below) of your topic land sooner than later.\n\nI think this approach gets us there faster, not slower. How long it\ntakes to review something isn't about the number of patches, it can be\nharder to review a 10 patch series than a 100 patch series. It's mainly\nabout structuring things in such a way as to make it readable to other\npeople.\n\nIf you've followed any of my own topics you'll probably correctly form\nthe opinion that that the last few paragraphs at best amount to throwing\nsome rather large pebbles from a glass house, which you'd be right about\n:)\n\nIt's can be really hard to see how/where to split things when you're the\nauthor of the code. It's really hard in the \"theory of mind\" sense of\nthings to explain an idea to someone who doesn't have the information\nyou have.\n\nStill, I do think there's a near-universal rules of thumb that the\nstructure of your series thoroughly runs afoul of, i.e. changing\nexisting behavior at the same time as introducing new behavior. We\nshould split such changes up whenever possible. This alternate approach\nshows that's it's possible.\n\n> their would have been nice to see a more clear discussion of patch\n> organization sometime much sooner in the past year and a half since the\n> project was proposed[3], like maybe in the few iterations of the design\n> doc which included a rollout plan in July of last year[4]. To me, it\n> seems late to be overhauling the direction like this, especially after I\n> asked for opinions and approval on the direction before I started work\n> in earnest.\n\nWe've had some version of this series going back to at least May last\nyear, and none of it has landed on \"master\" or \"next\" yet. To me that's\nmore reason, not less, for it benefiting from a more incremental\napproach.\n\nBut yes, I also wish I'd submitted this much earlier. Sorry I didn't\nhave time to review in this detail until now. I think Felipe's\ndownthread \"never too late\" comments apply here though[Æ.1]\n\nI've had outstanding significant feedback on the\n\"function/viability/sanity of new proposed semantics\" part of this\nalmost 3 months ago that you still haven't addressed in any way[Æ.2]\nexcept this comment[Æ.3] around a month ago on v8 (which I took to mean\nthat you would). So the lateness of us discussing this is at least\npartially a two-way street.\n\nThere was also the \"why do we need strbuf here?\" feedback[Æ.4] around the\nsame time from me, and as shown in the diff between our two versions\nthings like your run_hooks_opt_init_async() being better done as a\n\"struct ... = *_INIT\" idiom.\n\nSmall issues, but especially the latter to me suggests that your v9 is\ntoo big a chunk for reviewers to consider. I daresay if this was a\nsmaller series there's no way that wouldn't have been pointed out\nalready, ditto your \"git hook run\" introducing an \"--env\" which never\ngets used etc.\n\n> Anyway, I'd personally rather spend effort getting the existing series\n> the last few yards to the finish line than to head most of the way back\n> to the start.\n\nI think I've made a good argument above for why this takes you a step\nforward, not backwards, I'm hoping despite this initial reply that\nyou'll come to agree on that.\n\nIn any case, writing code is hard, but splitting it up like I've done\nhere is rather easily done. It took me about a day with waiting for\n\"rebase -i --exec='make test'\" equivalent, and that's from being mostly\nunfamiliar with the code in question beforehand.\n\nIf this alternate series were to go in ahead of yours that doesn't land\nyou back on square one, you still have a version you could relatively\neasily rebase on top. The patches here are mostly smaller versions (no\nhook config changes) of corresponding patches of yours.\n\nIf anything I don't see how it doesn't make things much easier for you,\nif you do get around to replying to my outstanding feedback in [Æ.1]\nthat'll involve rebasing/commit message rewording of patches that now\nconflate significant refactoring and behavior changes.\n\nYou'll instead cleanly have just the behavior changes, which at that\npoint will be both easier to justify in a briefer commit message, and\neasier for others to review.\n\n>> 1. https://lore.kernel.org/git/87lf80l1m6.fsf@evledraar.gmail.com/\n>> 2. https://lore.kernel.org/git/20210527000856.695702-1-emilyshaffer@google.com/\n> 3. https://lore.kernel.org/git/20191116011125.GG22855@google.com/\n> 4. https://lore.kernel.org/git/20200728222455.3023400-1-emilyshaffer@google.com/\n\nÆ.1. https://lore.kernel.org/git/60b71788c0e6d_67d0208d4@natae.notmuch/\nÆ.2. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\nÆ.3. https://lore.kernel.org/git/YJBdbi50Hz+ekOtt@google.com/\nÆ.4. https://lore.kernel.org/git/87pn04g0r1.fsf@evledraar.gmail.com/\n"},{"id":"426211","messageId":"87v96w7gg9.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"87y2bs7gyc.fsf@evledraar.gmail.com","subject":"Re: [PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-02T09:39:07Z","receivedAt":"2021-06-02T09:40:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Jun 02 2021, Ævar Arnfjörð Bjarmason wrote:\n\n> [...]\n> If anything I don't see how it doesn't make things much easier for you,\n> if you do get around to replying to my outstanding feedback in [Æ.1]\n\nHopefully obvious from context, but I meant [Æ.2] here.\n\n> [...]\n> Æ.1. https://lore.kernel.org/git/60b71788c0e6d_67d0208d4@natae.notmuch/\n> Æ.2. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n\ni.e. this.\n\n> Æ.3. https://lore.kernel.org/git/YJBdbi50Hz+ekOtt@google.com/\n> Æ.4. https://lore.kernel.org/git/87pn04g0r1.fsf@evledraar.gmail.com/\n\n"},{"id":"426212","messageId":"87sg207f9w.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"179668e9-008a-0b70-a193-b0a479ba0146@gmail.com","subject":"Re: [PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-02T09:34:21Z","receivedAt":"2021-06-02T10:05:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Jun 01 2021, Derrick Stolee wrote:\n\n> On 6/1/2021 2:14 PM, Emily Shaffer wrote:\n>> On Fri, May 28, 2021 at 02:11:02PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>>>\n>>> After suggesting[1] an another round that the config-based-hook\n>>> topic[2] should take a more incremental approach to reach its end goal\n>>> I thought I'd try hacking that up.\n>\n> I think sending this complete reorganization of a long-lived topic\n> is not helpful, especially because the end-to-end diff is significant.\n> This series has been extensively tested and incrementally improved\n> for months, and it would be a waste to start over and lose all of that\n> hardening.\n\nFelipe already commented downthread on the end-to-end diff aspect of\nthis. I replied in\nhttp://lore.kernel.org/git/871r9k8zui.fsf@evledraar.gmail.com with the\ndiff you're probably more interested in looking at.\n\n> It's also a but rushed that this comes only a day after the previous\n> message recommending a reorganization. It would be best to at least\n> give the original author an opportunity to comment on your idea before\n> working on this.\n\nI replied toth is as \"When you get feedback in the form of restructured\npatches[...]\" in  https://lore.kernel.org/git/87y2bs7gyc.fsf@evledraar.gmail.com/\n\nAs noted there I really don't see how sending \"here's patches that\nimplement my suggestion\" as opposed to \"maybe xyz would be better\" is\nworse for anyone.\n\n>>> So this is a proposed restart of that topic which if the consensus\n>>> favors it should replace it, and the config-based hooks topic should\n>>> be rebased on top of this.\n>> \n>> I'm not entirely sure what you're trying to achieve by sending this\n>> series. It was my impression that the existing config-based-hooks topic\n>> was close to being ready to submit anyway (since Junio mentioned\n>> submitting it a couple revisions ago); rather than churning by reviewing\n>> a different 31-patch topic, and then re-rolling and re-reviewing a\n>> (reduced) config hook topic, wouldn't it be easier on everyone's time to\n>> do a final incremental review on the existing topic and then start in on\n>> bugfixes/feature patches afterwards?\n>\n> I completely agree here.\n\nWhat do you think of the issues I raised in [1] ?\n\nEmily's series isn't a pure internal refactoring of existing\nfunctionality, but also an opinionated introduction of new user-facing\nfunctionality.\n\nNow, you may be 100% in agreement with the \"opinionated\" or\n\"user-facing\" part of that, I'm personally somewhere around 80%.\n\nBut to me that clearly makes it a case where we need to as much as\npossible get it right *before* it lands on master, because we can't just\nincrementally tweak it after it lands without backwards compatibility\nconcerns. Soon after landing it'll be in a release, have users in the\nwild etc.\n\nBy default that turns any suggestion of tweaking existing behavior from\na \"oh that makes sense, let's fix it\" into \"well, that's unfortunate,\nbut it's existing/documented functionality used in the wild\".\n\n[2] goes on to mention a couple of other such issues (e.g. the --env\nswitch to \"git hook run\"), and this time around I've just been narrowly\nfocusing on any such issues in the not-config-base-hooks part of this\ntopic. I.e. the \"no user-facing behavior changes yet\" part I carved out\ninto a separate topic.\n\nI'm not telling you which issues those are because I'd like you to tell\nme, and then we'll compare notes afterwards. I bet <insert round of\nbeers at git dev summit or equivalent here> that they're not the same\nlist.\n\nAnyway, I don't think you will, and I'm not entirely serious. But it's a\nreal rhetorical point about us being unlikely to come up with the same\nresult, and thus about the outstanding v9 series being too large to be\nreadily understood.\n\n>> It would have been nice to see a more clear discussion of patch\n>> organization sometime much sooner in the past year and a half since the\n>> project was proposed[3], like maybe in the few iterations of the design\n>> doc which included a rollout plan in July of last year[4]. To me, it\n>> seems late to be overhauling the direction like this, especially after I\n>> asked for opinions and approval on the direction before I started work\n>> in earnest.\n>\n> I've also seen messages as early as January where Ævar mentioned\n> wanting to review the series, but not finding the time to do so.\n> It is reasonable to expect that contributors attempt such major\n> reorganizations according to reviewers feedback, as long as the\n> reviewers are timely about delivering that feedback.\n\nI made a case in [2] for it not being too late.\n\nFor what it's worth (and some of which is noted in [2]) that's partially\nbecause I for one have found it really hard to keep track of this\nseries.\n\nMultiple re-rolls (including v8->v9) have some outstanding\ndiscussion/feedback, some of which is addressed in a re-roll, but other\nparts (as noted in my [2]) which aren't and are silently omitted.\n\nI'm not blaming Emily for that, I think it's a rather inevitable result\nof this thing just being too big to begin with and needing to be split\nup.\n\nBut that's at least part of the story of feedback at such a late\ntime. Whenever I've looked at this topic I've spent quite a lot of time\ndoing that re-reading of past discussions / comparing with the newest\ncover letter and noting any omissions etc. myself before even getting to\nthe point of reading the first patch, and I've sometimes just given up.\n\n1. https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/ \n2. https://lore.kernel.org/git/87y2bs7gyc.fsf@evledraar.gmail.com/\n"},{"id":"426310","messageId":"875yyv71lz.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210527000856.695702-8-emilyshaffer@google.com","subject":"Re: [PATCH v9 07/37] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-03T09:07:29Z","receivedAt":"2021-06-03T09:13:00Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, May 26 2021, Emily Shaffer wrote:\n\n> +void run_hooks_opt_init(struct run_hooks_opt *o)\n> +{\n> +\tstrvec_init(&o->env);\n> +\tstrvec_init(&o->args);\n> +\to->run_hookdir = configured_hookdir_opt();\n> +}\n\nI suggested in\nhttps://lore.kernel.org/git/87y2bs7gyc.fsf@evledraar.gmail.com/ that\nthis could and should be a RUN_HOOKS_OPT_INIT\n\nAfter some digging I see that was the case in an earlier version of your\nseries, i.e. before:\nhttps://lore.kernel.org/git/20210131042254.1032233-1-jonathantanmy@google.com/\n\nYou came up with this current pattern because of\nconfigured_hookdir_opt().\n\nBut a better option here is to use a RUN_HOOKS_OPT_INIT still and just\ndefer the initialization of this \"run_hookdir\" member. I.e. set it to\n\"we have not asked the config yet\" in the initializer. I.e. the diff on\ntop your series at the end of this E-Mail[1].\n\nThat along with doing the same for the \"jobs\" member means we can move\nback to a RUN_HOOKS_OPT_INIT, and also that the final version of this\nfunction in this series, i.e.:\n\t\n\tvoid run_hooks_opt_init_sync(struct run_hooks_opt *o)\n\t{\n\t\tstrvec_init(&o->env);\n\t\tstrvec_init(&o->args);\n\t\to->path_to_stdin = NULL;\n\t\to->run_hookdir = HOOKDIR_UNINITIALIZED;\n\t\to->jobs = 1;\n\t\to->dir = NULL;\n\t\to->feed_pipe = NULL;\n\t\to->feed_pipe_ctx = NULL;\n\t\to->consume_sideband = NULL;\n\t}\n\nIs now mostly redundant to a designated initializer. I.e. you don't need\nto NULL any of these out anymore. Then either don't set \"jobs\" and have\n\"0\" mean \"ask config\" or set it to \"-1\" or whatever for \"uninitialized\".\n\n1.\n\ndiff --git a/hook.c b/hook.c\nindex ff80e52eddd..daf3ddcc188 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -154,7 +154,7 @@ int configured_hook_jobs(void)\n \treturn n;\n }\n \n-static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n+static int should_include_hookdir(struct run_hooks_opt *options, const char *path)\n {\n \tstruct strbuf prompt = STRBUF_INIT;\n \t/*\n@@ -164,7 +164,10 @@ static int should_include_hookdir(const char *path, enum hookdir_opt cfg)\n \tif (!path)\n \t\treturn 0;\n \n-\tswitch (cfg)\n+\tif (options->run_hookdir == HOOKDIR_UNINITIALIZED)\n+\t\toptions->run_hookdir = configured_hookdir_opt();\n+\n+\tswitch (options->run_hookdir)\n \t{\n \tcase HOOKDIR_ERROR:\n \t\tfprintf(stderr, _(\"Skipping legacy hook at '%s'\\n\"),\n@@ -292,7 +295,7 @@ void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n \tstrvec_init(&o->env);\n \tstrvec_init(&o->args);\n \to->path_to_stdin = NULL;\n-\to->run_hookdir = configured_hookdir_opt();\n+\to->run_hookdir = HOOKDIR_UNINITIALIZED;\n \to->jobs = 1;\n \to->dir = NULL;\n \to->feed_pipe = NULL;\n@@ -312,6 +315,9 @@ int hook_exists(const char *hookname, enum hookdir_opt should_run_hookdir)\n \tstruct strbuf hook_key = STRBUF_INIT;\n \tint could_run_hookdir;\n \n+\tif (should_run_hookdir != HOOKDIR_USE_CONFIG)\n+\t\tBUG(\"no callers want !HOOKDIR_USE_CONFIG?\");\n+\n \tif (should_run_hookdir == HOOKDIR_USE_CONFIG)\n \t\tshould_run_hookdir = configured_hookdir_opt();\n \n@@ -459,7 +465,7 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options)\n \t\tstruct hook *hook = list_entry(pos, struct hook, list);\n \n \t\tif (hook->from_hookdir &&\n-\t\t    !should_include_hookdir(hook->command.buf, options->run_hookdir))\n+\t\t    !should_include_hookdir(options, hook->command.buf))\n \t\t\t    list_del(pos);\n \t}\n \ndiff --git a/hook.h b/hook.h\nindex f32189380a8..3c4491a74e7 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -30,6 +30,7 @@ struct list_head* hook_list(const char *hookname);\n \n enum hookdir_opt\n {\n+\tHOOKDIR_UNINITIALIZED,\n \tHOOKDIR_USE_CONFIG,\n \tHOOKDIR_NO,\n \tHOOKDIR_ERROR,\n"},{"id":"426311","messageId":"8735tz713a.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210527000856.695702-38-emilyshaffer@google.com","subject":"Re: [PATCH v9 37/37] docs: link githooks and git-hook manpages","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-03T09:18:43Z","receivedAt":"2021-06-03T09:25:17Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, May 26 2021, Emily Shaffer wrote:\n\n> Since users may have an easier time finding 'man githooks' or 'git help\n> githooks' through tab-completion or muscle memory, reference the 'git\n> hook' commands. And in the 'git hook' manual, point users back to 'man\n> githooks' for specifics about the hook events themselves.\n\nOk, there should be a cross-reference...\n\n> +HOOKS\n> +-----\n\nBut this should be a \"SEE ALSO\" section.\n\n> +For a list of hooks which can be configured and how they work, see\n> +linkgit:githooks[5].\n> +\n>  CONFIGURATION\n>  -------------\n>  include::config/hook.txt[]\n> diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\n> index 42e66d4e2d..d780cb3b18 100644\n> --- a/Documentation/githooks.txt\n> +++ b/Documentation/githooks.txt\n> @@ -7,15 +7,16 @@ githooks - Hooks used by Git\n>  \n>  SYNOPSIS\n>  --------\n> +'git hook'\n\nAnd ditto here, it makes no sense in a githooks(5) to put \"git hook\" in\nthe SYNOPSIS section (which is usually commands to be run), that'll just\nspew out the --help output for \"git hook\" itself.\n\n>  $GIT_DIR/hooks/* (or \\`git config core.hooksPath`/*)\n>  \n>  \n>  DESCRIPTION\n>  -----------\n>  \n> -Hooks are programs you can place in a hooks directory to trigger\n> -actions at certain points in git's execution. Hooks that don't have\n> -the executable bit set are ignored.\n> +Hooks are programs you can specify in your config (see linkgit:git-hook[1]) or\n\nFor most other things we link back to git-config[1] for such \"see\", even\nthough we have the included config in the specific command.\n\nI can see how this makes more sense in a way, but would prefer to have\nus be consistent.\n\n> +place in a hooks directory to trigger actions at certain points in git's\n> +execution. Hooks that don't have the executable bit set are ignored.\n>  \n>  By default the hooks directory is `$GIT_DIR/hooks`, but that can be\n>  changed via the `core.hooksPath` configuration variable (see\n\nNot a new issue, but is that \"are ignored\" not something that pre-dates\n\"advice.ignoredHook\"? I.e. we don't ignore them anymore, we warn about\nthem, no?\n"},{"id":"426368","messageId":"xmqqeedi4m6o.fsf@gitster.g","threadId":"55286","inReplyTo":"875yyv71lz.fsf@evledraar.gmail.com","subject":"Re: [PATCH v9 07/37] hook: add 'run' subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-06-03T22:29:03Z","receivedAt":"2021-06-03T22:29:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> On Wed, May 26 2021, Emily Shaffer wrote:\n>\n>> +void run_hooks_opt_init(struct run_hooks_opt *o)\n>> +{\n>> +\tstrvec_init(&o->env);\n>> +\tstrvec_init(&o->args);\n>> +\to->run_hookdir = configured_hookdir_opt();\n>> +}\n>\n> I suggested in\n> https://lore.kernel.org/git/87y2bs7gyc.fsf@evledraar.gmail.com/ that\n> this could and should be a RUN_HOOKS_OPT_INIT\n>\n> After some digging I see that was the case in an earlier version of your\n> series, i.e. before:\n> https://lore.kernel.org/git/20210131042254.1032233-1-jonathantanmy@google.com/\n>\n> You came up with this current pattern because of\n> configured_hookdir_opt().\n>\n> But a better option here is to use a RUN_HOOKS_OPT_INIT still and just\n> defer the initialization of this \"run_hookdir\" member. I.e. set it to\n> \"we have not asked the config yet\" in the initializer. I.e. the diff on\n> top your series at the end of this E-Mail[1].\n\nWhen I compared the result of applying your 31-patch series to\n2.32-rc2 and the result of rebasing this series on the same base,\nbefore sending out a responce to Emily's reaction, I found that the\n31-patch series did a good job of not stepping on the \"hook defined\nwith configuration\" part and concentrated on providing a clean base\nto build on with a better structure in the series, and there weren't\nmany changes that overlapped with Emily's series in a significant\nway.  The above was one of the \"overlapping\" differences that stood\nout.\n\nThanks.\n\n\n\n"},{"id":"427281","messageId":"875yyg24na.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210527000856.695702-32-emilyshaffer@google.com","subject":"Re: [PATCH v9 31/37] post-update: use hook.h library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T09:09:21Z","receivedAt":"2021-06-14T09:11:29Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, May 26 2021, Emily Shaffer wrote:\n\n> By using run_hooks() instead of run_hook_le(), 'post-update' hooks can\n> be specified in the config as well as the hookdir.\n\nBut even though we delete run_hook_le() in a later commit this is not\none of the users of that API, it's doing its own start_command(). This\nis exposed in my alternate restart series where I removed that API\nbefore this commit.\n\nSpotted while rephrasing the commit message for a re-roll of that, just\nleaving a note here...\n"},{"id":"427284","messageId":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com","subject":"[PATCH v2 00/30] Minimal restart of \"config-based-hooks\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:49Z","receivedAt":"2021-06-14T10:36:07Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"I proposed splitting Emily's \"hook config\" topic[1] into at least a\ntopic that retains all current behavior of the codebase, and merely\nrefactors existing behavior to new APIs, and then doing behavior\nchanges later.\n\nThis is a re-roll of an attempt to do that. See v1's CL [2] for much\nmore details.\n\nI was hoping to get more feedback from Emily in reply to [3] and\nrelated E-Mails on the v1, but as it's been almost 2 weeks with no\nreply, and both her topic and mine semantically conflict with changes\nsince merged to \"master\" I thought I'd send this re-roll.\n\nChanges since v2:\n\n A. Addressed comments by Felipe, thanks! The \"remove the [old] API\"\n    is now a separate commit / other minor nits fixed.\n\n B. Fixed a test failure due to \"mkdir .git/hooks\"; semantic conflict\n    with my now-landed f0d4d398e28 (test-lib: split up and deprecate\n    test_create_repo(), 2021-05-10)\n\n C. Fixed a portibility issue in the new generate-hooklist.sh.\n\n D. Clarified what a TOCTOU is, reworded commit message.\n\n E. I'd added various \"we get closer to removing the hook code\" in\n    commit messages after we'd removed the hook code. We were really\n    just porting bespoke behavior to hook.h.\n\n F. Fixed trivial conflict with master's 410334ed521 (read-cache: use\n    hashfile instead of git_hash_ctx, 2021-05-18) in read-cache.c.\n\n G. In my v1 I added two patches at the beginning to fix/add\n    hook-related tests. I'll just submit those as separate\n    patches. There's no dependency between those and this topic, other\n    than basic this one on top of them granting more assurances that\n    this one doesn't break any existing hook behavior than the current\n    tests on \"master\".\n\n1. http://lore.kernel.org/git/20210527000856.695702-1-emilyshaffer@google.com\n2. https://lore.kernel.org/git/cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com/\n3. https://lore.kernel.org/git/87y2bs7gyc.fsf@evledraar.gmail.com/\n\nEmily Shaffer (26):\n  hook: add 'run' subcommand\n  hook.c: add a hook_exists() wrapper and use it in bugreport.c\n  gc: use hook library for pre-auto-gc hook\n  rebase: teach pre-rebase to use hook.h\n  am: convert applypatch hooks to use config\n  hooks: convert 'post-checkout' hook to hook library\n  merge: use config-based hooks for post-merge hook\n  send-email: use 'git hook run' for 'sendemail-validate'\n  git-p4: use 'git hook' to run hooks\n  commit: use hook.h to execute hooks\n  read-cache: convert post-index-change hook to use config\n  receive-pack: convert push-to-checkout hook to hook.h\n  run-command: remove old run_hook_{le,ve}() hook API\n  run-command: allow stdin for run_processes_parallel\n  hook: support passing stdin to hooks\n  am: convert 'post-rewrite' hook to hook.h\n  run-command: add stdin callback for parallelization\n  hook: provide stdin by string_list or callback\n  hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n  transport: convert pre-push hook to use config\n  reference-transaction: use hook.h to run hooks\n  run-command: allow capturing of collated output\n  hooks: allow callers to capture output\n  receive-pack: convert 'update' hook to hook.h\n  post-update: use hook.h library\n  receive-pack: convert receive hooks to hook.h\n\nÆvar Arnfjörð Bjarmason (4):\n  run-command.h: move find_hook() to hook.h\n  git hook run: add an --ignore-missing flag\n  hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n  hook-list.h: add a generated list of hooks, like config-list.h\n\n .gitignore                  |   2 +\n Documentation/git-hook.txt  |  49 ++++++\n Documentation/githooks.txt  |   4 +\n Makefile                    |  16 +-\n builtin.h                   |   1 +\n builtin/am.c                |  34 ++--\n builtin/bugreport.c         |  46 ++----\n builtin/checkout.c          |  17 +-\n builtin/clone.c             |   7 +-\n builtin/commit.c            |  19 ++-\n builtin/fetch.c             |   1 +\n builtin/gc.c                |   8 +-\n builtin/hook.c              |  72 +++++++++\n builtin/merge.c             |  22 ++-\n builtin/rebase.c            |   9 +-\n builtin/receive-pack.c      | 299 ++++++++++++++++++------------------\n builtin/submodule--helper.c |   2 +-\n builtin/worktree.c          |  31 ++--\n command-list.txt            |   1 +\n commit.c                    |  17 +-\n commit.h                    |   3 +-\n generate-hooklist.sh        |  24 +++\n git-p4.py                   |  72 +--------\n git-send-email.perl         |  20 ++-\n git.c                       |   1 +\n hook.c                      | 224 +++++++++++++++++++++++++++\n hook.h                      | 122 +++++++++++++++\n read-cache.c                |  12 +-\n refs.c                      |  43 ++----\n reset.c                     |  15 +-\n run-command.c               | 157 ++++++++++---------\n run-command.h               |  55 ++++---\n sequencer.c                 |  88 +++++------\n submodule.c                 |   1 +\n t/helper/test-run-command.c |  46 +++++-\n t/t0061-run-command.sh      |  37 +++++\n t/t1800-hook.sh             | 162 +++++++++++++++++++\n t/t9001-send-email.sh       |   4 +-\n transport.c                 |  58 ++-----\n 39 files changed, 1246 insertions(+), 555 deletions(-)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100755 generate-hooklist.sh\n create mode 100644 hook.c\n create mode 100644 hook.h\n create mode 100755 t/t1800-hook.sh\n\nRange-diff against v1:\n 1:  8ac2efc71a0 <  -:  ----------- hooks tests: don't leave \"actual\" nonexisting on failure\n 2:  eb37693f7dc <  -:  ----------- gc tests: add a test for the \"pre-auto-gc\" hook\n 3:  1ad4e69f7da !  1:  447d349c738 hook: add 'run' subcommand\n    @@ t/t1800-hook.sh (new)\n     +\n     +. ./test-lib.sh\n     +\n    -+test_expect_success 'setup .git/hooks' '\n    -+\tmkdir .git/hooks\n    -+'\n    -+\n     +test_expect_success 'git hook run -- nonexistent hook' '\n     +\tcat >stderr.expect <<-\\EOF &&\n     +\terror: cannot find a hook named does-not-exist\n 4:  1a67a1cc065 =  2:  85195a78cfb run-command.h: move find_hook() to hook.h\n 5:  a6f0817ad81 =  3:  eb5bdd993c8 hook.c: add a hook_exists() wrapper and use it in bugreport.c\n 6:  b186fde43e1 =  4:  da2763192ae gc: use hook library for pre-auto-gc hook\n 7:  528402fac69 =  5:  51e6e72f239 rebase: teach pre-rebase to use hook.h\n 8:  69842c74383 =  6:  d2f3b26d464 am: convert applypatch hooks to use config\n 9:  9b32c14669b !  7:  d884465aab3 hooks: convert 'post-checkout' hook to hook library\n    @@ hook.h: struct run_hooks_opt\n     \n      ## read-cache.c ##\n     @@\n    - #include \"thread-utils.h\"\n      #include \"progress.h\"\n      #include \"sparse-index.h\"\n    + #include \"csum-file.h\"\n     +#include \"hook.h\"\n      \n      /* Mask for the name length in ce_flags in the on-disk index */\n10:  201c654bb0c =  8:  ad6e7507841 merge: use config-based hooks for post-merge hook\n11:  e65d8bd6e6f =  9:  8d8b2d26453 git hook run: add an --ignore-missing flag\n12:  8795e9ceab8 = 10:  1953326d1db send-email: use 'git hook run' for 'sendemail-validate'\n13:  03129460fd2 = 11:  aa970a81752 git-p4: use 'git hook' to run hooks\n14:  3f3610f5ed3 = 12:  7756f10aac9 commit: use hook.h to execute hooks\n15:  6482a3e4cb8 = 13:  0300607a9b4 read-cache: convert post-index-change hook to use config\n -:  ----------- > 14:  ec4ad437f86 receive-pack: convert push-to-checkout hook to hook.h\n16:  a16163d4fb5 ! 15:  477eb2245c3 receive-pack: convert push-to-checkout hook to hook.h\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    receive-pack: convert push-to-checkout hook to hook.h\n    +    run-command: remove old run_hook_{le,ve}() hook API\n     \n    -    By using hook.h instead of run-command.h to invoke push-to-checkout,\n    -    hooks can now be specified in the config as well as in the hookdir.\n    -    push-to-checkout is not called anywhere but in builtin/receive-pack.c.\n    -\n    -    This is the last user of the run_hook_le() API, so let's remove it\n    -    while we're at it, since run_hook_le() itself is the last user of\n    -    run_hook_ve() we can remove that too. The last direct user of\n    -    run_hook_le() was removed in the commit preceding this one.\n    +    The new hook.h library has replaced all run-command.h hook-related\n    +    functionality. So let's delete this dead code.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n    - ## builtin/receive-pack.c ##\n    -@@ builtin/receive-pack.c: static const char *push_to_checkout(unsigned char *hash,\n    - \t\t\t\t    struct strvec *env,\n    - \t\t\t\t    const char *work_tree)\n    - {\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\n    - \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n    --\tif (run_hook_le(env->v, push_to_checkout_hook,\n    --\t\t\thash_to_hex(hash), NULL))\n    -+\tstrvec_pushv(&opt.env, env->v);\n    -+\tstrvec_push(&opt.args, hash_to_hex(hash));\n    -+\tif (run_hooks(push_to_checkout_hook, &opt)) {\n    -+\t\trun_hooks_opt_clear(&opt);\n    - \t\treturn \"push-to-checkout hook declined\";\n    --\telse\n    -+\t} else {\n    -+\t\trun_hooks_opt_clear(&opt);\n    - \t\treturn NULL;\n    -+\t}\n    - }\n    - \n    - static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree)\n    -@@ builtin/receive-pack.c: static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n    - \n    - \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n    - \n    --\tif (!find_hook(push_to_checkout_hook))\n    -+\tif (!hook_exists(push_to_checkout_hook))\n    - \t\tretval = push_to_deploy(sha1, &env, work_tree);\n    - \telse\n    - \t\tretval = push_to_checkout(sha1, &env, work_tree);\n    -\n      ## run-command.c ##\n     @@ run-command.c: int async_with_fork(void)\n      #endif\n17:  7020cf10c8e = 16:  53a3877a476 run-command: allow stdin for run_processes_parallel\n18:  4745dcfce49 = 17:  c4f60db606d hook: support passing stdin to hooks\n19:  986bfd89a54 = 18:  febf05ef232 am: convert 'post-rewrite' hook to hook.h\n20:  756f52af22d = 19:  7baf2469d50 run-command: add stdin callback for parallelization\n21:  3748f128763 ! 20:  2edf9dea41a hook: provide stdin by string_list or callback\n    @@ Commit message\n         In cases where a hook requires only a small amount of information via\n         stdin, it should be simple for users to provide a string_list alone. But\n         in more complicated cases where the stdin is too large to hold in\n    -    memory, let's provide a callback the users can populate line after line\n    -    with instead.\n    +    memory, let's instead provide a callback the users can populate line\n    +    after line.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n22:  0cf0b1fea93 = 21:  303b31ee620 hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n23:  c59443a3b05 = 22:  62eecafb3ff transport: convert pre-push hook to use config\n24:  f7c8c97cb81 ! 23:  6049b1cdc74 reference-transaction: use hook.h to run hooks\n    @@ Metadata\n      ## Commit message ##\n         reference-transaction: use hook.h to run hooks\n     \n    -    By using the hook.h library, we get closer to removing the hook code\n    -    in run-command.c.\n    -\n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n25:  f240a51ec4e = 24:  26ebbe4c545 run-command: allow capturing of collated output\n26:  7f10efb7858 = 25:  251085b7525 hooks: allow callers to capture output\n27:  c39c608e5cc ! 26:  e11a8f60071 receive-pack: convert 'update' hook to hook.h\n    @@ Metadata\n      ## Commit message ##\n         receive-pack: convert 'update' hook to hook.h\n     \n    -    By using hook.h to invoke the 'update' hook we closer to removing the\n    -    hooks code in run-command.c.\n    +    This makes use of the new sideband API in hook.h added in the\n    +    preceding commit.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n28:  3519068a634 ! 27:  ecaedd13b89 post-update: use hook.h library\n    @@ Metadata\n      ## Commit message ##\n         post-update: use hook.h library\n     \n    -    By using run_hooks() instead of run_hook_le(), 'post-update' hooks can\n    -    be specified in the config as well as the hookdir.\n    -\n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n29:  3466f17af08 ! 28:  bb9d57f809a receive-pack: convert receive hooks to hook.h\n    @@ Metadata\n      ## Commit message ##\n         receive-pack: convert receive hooks to hook.h\n     \n    -    By using the hook.h library to run receive hooks we get closer to\n    -    deleting the hook functions in run-command.c\n    -\n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n30:  d93bdc0c294 ! 29:  793f112f7ab hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n    @@ Metadata\n      ## Commit message ##\n         hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n     \n    -    Fix a race in code added in 680ee550d72 (commit: skip discarding the\n    -    index if there is no pre-commit hook, 2017-08-14) by changing the\n    -    hook.c API to optionally indicate whether or not the requested hook\n    -    ran or not. This was suggested in the discussion around\n    -    680ee550d72[1].\n    +    Fix a Time-of-check to time-of-use (TOCTOU) race in code added in\n    +    680ee550d72 (commit: skip discarding the index if there is no\n    +    pre-commit hook, 2017-08-14).\n     \n    -    Let's also change this for the pre-merge-commit hook, see\n    +    We can fix the race passing around information about whether or not we\n    +    ran the hook in question, instead of running hook_exists() after the\n    +    fact to check if the hook in question exists. This problem has been\n    +    noted on-list when 680ee550d72 was discussed[1], but had not been\n    +    fixed.\n    +\n    +    In addition to fixing this for the pre-commit hook as suggested there\n    +    I'm also fixing this for the pre-merge-commit hook. See\n         6098817fd7f (git-merge: honor pre-merge-commit hook, 2019-08-07) for\n    -    the introduction of the previous behavior.\n    +    the introduction of its previous behavior.\n     \n         Let's also change this for the push-to-checkout hook. Now instead of\n         checking if the hook exists and either doing a push to checkout or a\n    @@ Commit message\n     \n         In both of those cases we're saving ourselves CPU time by not\n         preparing data for the hook that we'll then do nothing with if we\n    -    don't have the hook, so using this \"invoked_hook\" pattern doesn't make\n    -    sense there purely for optimization purposes.\n    +    don't have the hook. So using this \"invoked_hook\" pattern doesn't make\n    +    sense in those cases.\n     \n         More importantly, in those cases the worst we'll do is miss that we\n         \"should\" run the hook because a new hook appeared, whereas in the\n31:  896956250f6 ! 30:  bc086454d68 hook-list.h: add a generated list of hooks, like config-list.h\n    @@ generate-hooklist.sh (new)\n     +\tcat <<EOF\n     +static const char *hook_name_list[] = {\n     +EOF\n    -+\tgrep -C1 -h '^~~~' Documentation/githooks.txt |\n    -+\tgrep '^[a-z0-9][a-z0-9-]*$' |\n    -+\tsort |\n    -+\tsed 's/^.*$/\t\"&\",/'\n    ++\tperl -ne '\n    ++\t\tchomp;\n    ++\t\t@l[$.] = $_;\n    ++\t\tpush @h => $l[$. - 1] if /^~~~+$/s;\n    ++\t\tEND {\n    ++\t\t\tprint qq[\\t\"$_\",\\n] for sort @h;\n    ++\t\t}\n    ++\t' <Documentation/githooks.txt\n     +\tcat <<EOF\n     +\tNULL,\n     +};\n    @@ hook.c\n      \tif (access(path.buf, X_OK) < 0) {\n     \n      ## t/t1800-hook.sh ##\n    -@@ t/t1800-hook.sh: test_expect_success 'setup .git/hooks' '\n    +@@ t/t1800-hook.sh: test_description='git-hook command'\n      \n      test_expect_success 'git hook run -- nonexistent hook' '\n      \tcat >stderr.expect <<-\\EOF &&\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427285","messageId":"patch-03.30-eb5bdd993c8-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 03/30] hook.c: add a hook_exists() wrapper and use it in bugreport.c","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:52Z","receivedAt":"2021-06-14T10:36:26Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nAdd a boolean version of the find_hook() function for those callers\nwho are only interested in checking whether the hook exists, not what\nthe path to it is.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/bugreport.c | 2 +-\n hook.c              | 5 ++++-\n hook.h              | 5 +++++\n 3 files changed, 10 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 596f079a7f9..941c8d5e270 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -82,7 +82,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t}\n \n \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (find_hook(hook[i]))\n+\t\tif (hook_exists(hook[i]))\n \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n }\n \ndiff --git a/hook.c b/hook.c\nindex 06842f50e5c..c7da273822d 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -36,7 +36,10 @@ const char *find_hook(const char *name)\n \treturn path.buf;\n }\n \n-\n+int hook_exists(const char *name)\n+{\n+\treturn !!find_hook(name);\n+}\n \n void run_hooks_opt_clear(struct run_hooks_opt *o)\n {\ndiff --git a/hook.h b/hook.h\nindex 291ee19469a..cbda7746a5d 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -44,6 +44,11 @@ struct hook_cb_data {\n  */\n const char *find_hook(const char *name);\n \n+/*\n+ * A boolean version of find_hook()\n+ */\n+int hook_exists(const char *hookname);\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /*\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427286","messageId":"patch-01.30-447d349c738-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:50Z","receivedAt":"2021-06-14T10:36:31Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn order to enable hooks to be run as an external process, by a\nstandalone Git command, or by tools which wrap Git, provide an external\nmeans to run all configured hook commands for a given hook event.\n\nMost of our hooks require more complex functionality than this, but\nlet's start with the bare minimum required to support our simplest\nhooks.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n .gitignore                 |   1 +\n Documentation/git-hook.txt |  36 ++++++++++\n Documentation/githooks.txt |   4 ++\n Makefile                   |   2 +\n builtin.h                  |   1 +\n builtin/hook.c             |  65 ++++++++++++++++++\n command-list.txt           |   1 +\n git.c                      |   1 +\n hook.c                     | 114 ++++++++++++++++++++++++++++++++\n hook.h                     |  54 +++++++++++++++\n t/t1800-hook.sh            | 131 +++++++++++++++++++++++++++++++++++++\n 11 files changed, 410 insertions(+)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100644 hook.c\n create mode 100644 hook.h\n create mode 100755 t/t1800-hook.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 311841f9bed..de39dc9961b 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-hook\n /git-http-backend\n /git-http-fetch\n /git-http-push\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nnew file mode 100644\nindex 00000000000..902b9cffaef\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,36 @@\n+git-hook(1)\n+===========\n+\n+NAME\n+----\n+git-hook - run git hooks\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git hook' run <hook-name> [-- <hook-args>]\n+\n+DESCRIPTION\n+-----------\n+\n+This command is an interface to git hooks (see linkgit:githooks[5]).\n+Currently it only provides a convenience wrapper for running hooks for\n+use by git itself. In the future it might gain other functionality.\n+\n+SUBCOMMANDS\n+-----------\n+\n+run::\n+\n+\tRun the `<hook-name>` hook. Any positional arguments to the\n+\thook should be passed after an optional \"--\" (or\n+\t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n+\tthis accepts.\n+\n+SEE ALSO\n+--------\n+linkgit:githooks[5]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex b51959ff941..a16e62bc8c8 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -698,6 +698,10 @@ and \"0\" meaning they were not.\n Only one parameter should be set to \"1\" when the hook runs.  The hook\n running passing \"1\", \"1\" should not be possible.\n \n+SEE ALSO\n+--------\n+linkgit:git-hook[1]\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex c3565fc0f8f..a6b71a0fbed 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -901,6 +901,7 @@ LIB_OBJS += hash-lookup.o\n LIB_OBJS += hashmap.o\n LIB_OBJS += help.o\n LIB_OBJS += hex.o\n+LIB_OBJS += hook.o\n LIB_OBJS += ident.o\n LIB_OBJS += json-writer.o\n LIB_OBJS += kwset.o\n@@ -1101,6 +1102,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\ndiff --git a/builtin.h b/builtin.h\nindex 16ecd5586f0..91740c15149 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);\n int cmd_grep(int argc, const char **argv, const char *prefix);\n int cmd_hash_object(int argc, const char **argv, const char *prefix);\n int cmd_help(int argc, const char **argv, const char *prefix);\n+int cmd_hook(int argc, const char **argv, const char *prefix);\n int cmd_index_pack(int argc, const char **argv, const char *prefix);\n int cmd_init_db(int argc, const char **argv, const char *prefix);\n int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/hook.c b/builtin/hook.c\nnew file mode 100644\nindex 00000000000..1b1a594fd00\n--- /dev/null\n+++ b/builtin/hook.c\n@@ -0,0 +1,65 @@\n+#include \"cache.h\"\n+#include \"builtin.h\"\n+#include \"config.h\"\n+#include \"hook.h\"\n+#include \"parse-options.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+static const char * const builtin_hook_usage[] = {\n+\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n+\tNULL\n+};\n+\n+static int run(int argc, const char **argv, const char *prefix)\n+{\n+\tint i;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint rc = 0;\n+\tconst char *hook_name;\n+\tconst char *hook_path;\n+\n+\tstruct option run_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, run_options,\n+\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (argc > 2) {\n+\t\tif (strcmp(argv[2], \"--\") &&\n+\t\t    strcmp(argv[2], \"--end-of-options\"))\n+\t\t\t/* Having a -- for \"run\" is mandatory */\n+\t\t\tusage_with_options(builtin_hook_usage, run_options);\n+\t\t/* Add our arguments, start after -- */\n+\t\tfor (i = 3 ; i < argc; i++)\n+\t\t\tstrvec_push(&opt.args, argv[i]);\n+\t}\n+\n+\t/* Need to take into account core.hooksPath */\n+\tgit_config(git_default_config, NULL);\n+\n+\thook_name = argv[1];\n+\thook_path = find_hook(hook_name);\n+\tif (!hook_path) {\n+\t\terror(\"cannot find a hook named %s\", hook_name);\n+\t\treturn 1;\n+\t}\n+\trc = run_found_hooks(hook_name, hook_path, &opt);\n+\n+\trun_hooks_opt_clear(&opt);\n+\n+\treturn rc;\n+}\n+\n+int cmd_hook(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option builtin_hook_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\tif (!strcmp(argv[1], \"run\"))\n+\t\treturn run(argc, argv, prefix);\n+\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+\treturn 1;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex a289f09ed6f..9ccd8e5aebe 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-hook                                mainporcelain\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\n git-http-push                           synchelpers\ndiff --git a/git.c b/git.c\nindex 18bed9a9964..540909c391f 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nnew file mode 100644\nindex 00000000000..aa66c968186\n--- /dev/null\n+++ b/hook.c\n@@ -0,0 +1,114 @@\n+#include \"cache.h\"\n+#include \"hook.h\"\n+#include \"run-command.h\"\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o)\n+{\n+\tstrvec_clear(&o->env);\n+\tstrvec_clear(&o->args);\n+}\n+\n+static int pick_next_hook(struct child_process *cp,\n+\t\t\t  struct strbuf *out,\n+\t\t\t  void *pp_cb,\n+\t\t\t  void **pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *run_me = hook_cb->run_me;\n+\n+\tif (!run_me)\n+\t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n+\n+\tcp->no_stdin = 1;\n+\tcp->env = hook_cb->options->env.v;\n+\tcp->stdout_to_stderr = 1;\n+\tcp->trace2_hook_name = hook_cb->hook_name;\n+\n+\t/* add command */\n+\tstrvec_push(&cp->args, run_me->hook_path);\n+\n+\t/*\n+\t * add passed-in argv, without expanding - let the user get back\n+\t * exactly what they put in\n+\t */\n+\tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n+\n+\t/* Provide context for errors if necessary */\n+\t*pp_task_cb = run_me;\n+\n+\treturn 1;\n+}\n+\n+static int notify_start_failure(struct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cp)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *attempted = pp_task_cp;\n+\n+\t/* |= rc in cb */\n+\thook_cb->rc |= 1;\n+\n+\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n+\t\t    attempted->hook_path);\n+\n+\treturn 1;\n+}\n+\n+static int notify_hook_finished(int result,\n+\t\t\t\tstruct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\n+\t/* |= rc in cb */\n+\thook_cb->rc |= result;\n+\n+\treturn 1;\n+}\n+\n+\n+int run_found_hooks(const char *hook_name, const char *hook_path,\n+\t\t    struct run_hooks_opt *options)\n+{\n+\tstruct hook my_hook = {\n+\t\t.hook_path = hook_path,\n+\t};\n+\tstruct hook_cb_data cb_data = {\n+\t\t.rc = 0,\n+\t\t.hook_name = hook_name,\n+\t\t.options = options,\n+\t};\n+\tcb_data.run_me = &my_hook;\n+\n+\tif (options->jobs != 1)\n+\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n+\n+\trun_processes_parallel_tr2(options->jobs,\n+\t\t\t\t   pick_next_hook,\n+\t\t\t\t   notify_start_failure,\n+\t\t\t\t   notify_hook_finished,\n+\t\t\t\t   &cb_data,\n+\t\t\t\t   \"hook\",\n+\t\t\t\t   hook_name);\n+\n+\treturn cb_data.rc;\n+}\n+\n+int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n+{\n+\tconst char *hook_path;\n+\tint ret;\n+\tif (!options)\n+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n+\n+\thook_path = find_hook(hook_name);\n+\n+\t/* Care about nonexistence? Use run_found_hooks() */\n+\tif (!hook_path)\n+\t\treturn 0;\n+\n+\tret = run_found_hooks(hook_name, hook_path, options);\n+\treturn ret;\n+}\ndiff --git a/hook.h b/hook.h\nnew file mode 100644\nindex 00000000000..ebfee26bcf2\n--- /dev/null\n+++ b/hook.h\n@@ -0,0 +1,54 @@\n+#ifndef HOOK_H\n+#define HOOK_H\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+#include \"run-command.h\"\n+\n+struct hook {\n+\t/* The path to the hook */\n+\tconst char *hook_path;\n+};\n+\n+struct run_hooks_opt\n+{\n+\t/* Environment vars to be set for each hook */\n+\tstruct strvec env;\n+\n+\t/* Args to be passed to each hook */\n+\tstruct strvec args;\n+\n+\t/* Number of threads to parallelize across */\n+\tint jobs;\n+};\n+\n+#define RUN_HOOKS_OPT_INIT { \\\n+\t.jobs = 1, \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n+/*\n+ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n+ */\n+struct hook_cb_data {\n+\tint rc;\n+\tconst char *hook_name;\n+\tstruct hook *run_me;\n+\tstruct run_hooks_opt *options;\n+};\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o);\n+\n+/*\n+ * Calls find_hook(hookname) and runs the hooks (if any) with\n+ * run_found_hooks().\n+ */\n+int run_hooks(const char *hook_name, struct run_hooks_opt *options);\n+\n+/*\n+ * Takes an already resolved hook and runs it. Internally the simpler\n+ * run_hooks() will call this.\n+ */\n+int run_found_hooks(const char *hookname, const char *hook_path,\n+\t\t    struct run_hooks_opt *options);\n+#endif\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nnew file mode 100755\nindex 00000000000..f6ff6c4a493\n--- /dev/null\n+++ b/t/t1800-hook.sh\n@@ -0,0 +1,131 @@\n+#!/bin/bash\n+\n+test_description='git-hook command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'git hook run -- nonexistent hook' '\n+\tcat >stderr.expect <<-\\EOF &&\n+\terror: cannot find a hook named does-not-exist\n+\tEOF\n+\ttest_expect_code 1 git hook run does-not-exist 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual\n+'\n+\n+test_expect_success 'git hook run -- basic' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tEOF\n+\tgit hook run test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run -- stdout and stderr handling' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo >&1 Will end up on stderr\n+\techo >&2 Will end up on stderr\n+\tEOF\n+\n+\tcat >stderr.expect <<-\\EOF &&\n+\tWill end up on stderr\n+\tWill end up on stderr\n+\tEOF\n+\tgit hook run test-hook >stdout.actual 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual &&\n+\ttest_must_be_empty stdout.actual\n+'\n+\n+test_expect_success 'git hook run -- exit codes are passed along' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 1\n+\tEOF\n+\n+\ttest_expect_code 1 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 2\n+\tEOF\n+\n+\ttest_expect_code 2 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 128\n+\tEOF\n+\n+\ttest_expect_code 128 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 129\n+\tEOF\n+\n+\ttest_expect_code 129 git hook run test-hook\n+'\n+\n+test_expect_success 'git hook run arg u ments without -- is not allowed' '\n+\ttest_expect_code 129 git hook run test-hook arg u ments\n+'\n+\n+test_expect_success 'git hook run -- pass arguments' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo $1\n+\techo $2\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\targ\n+\tu ments\n+\tEOF\n+\n+\tgit hook run test-hook -- arg \"u ments\" 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run -- out-of-repo runs excluded' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tnongit test_must_fail git hook run test-hook\n+'\n+\n+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n+\tmkdir my-hooks &&\n+\twrite_script my-hooks/test-hook <<-EOF &&\n+\techo Hook ran >>actual\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tHook ran\n+\tHook ran\n+\tHook ran\n+\tHook ran\n+\tEOF\n+\n+\t# Test various ways of specifying the path. See also\n+\t# t1350-config-hooks-path.sh\n+\t>actual &&\n+\tgit hook run test-hook 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks hook run test-hook 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks/ hook run test-hook 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook 2>>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n+\t>actual &&\n+\tmkdir -p .git/custom-hooks .git/hooks &&\n+\twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n+\techo CUSTOM >>actual\n+\tEOF\n+\twrite_script .git/hooks/pre-commit <<-\\EOF\n+\techo NORMAL >>actual\n+\tEOF\n+'\n+\n+test_done\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427287","messageId":"patch-02.30-85195a78cfb-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 02/30] run-command.h: move find_hook() to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:51Z","receivedAt":"2021-06-14T10:36:32Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Move the find_hook() command to hook.h. Eventually all the hook\nrelated code will live there, let's move this function over as-is.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c           |  1 +\n builtin/bugreport.c    |  2 +-\n builtin/commit.c       |  1 +\n builtin/merge.c        |  1 +\n builtin/receive-pack.c |  1 +\n builtin/worktree.c     |  1 +\n hook.c                 | 36 ++++++++++++++++++++++++++++++++++++\n hook.h                 |  7 +++++++\n refs.c                 |  1 +\n run-command.c          | 35 +----------------------------------\n run-command.h          |  7 -------\n sequencer.c            |  1 +\n transport.c            |  1 +\n 13 files changed, 53 insertions(+), 42 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 0b2d886c81b..1c8a5489035 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -11,6 +11,7 @@\n #include \"parse-options.h\"\n #include \"dir.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"quote.h\"\n #include \"tempfile.h\"\n #include \"lockfile.h\"\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 9915a5841de..596f079a7f9 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -3,7 +3,7 @@\n #include \"strbuf.h\"\n #include \"help.h\"\n #include \"compat/compiler.h\"\n-#include \"run-command.h\"\n+#include \"hook.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 190d215d43b..f1aafd67d46 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -19,6 +19,7 @@\n #include \"revision.h\"\n #include \"wt-status.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"refs.h\"\n #include \"log-tree.h\"\n #include \"strbuf.h\"\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex a8a843b1f54..be98d66b0a8 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -13,6 +13,7 @@\n #include \"builtin.h\"\n #include \"lockfile.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"diff.h\"\n #include \"diff-merges.h\"\n #include \"refs.h\"\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a34742513ac..1e0e04c62fc 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -7,6 +7,7 @@\n #include \"pkt-line.h\"\n #include \"sideband.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"commit.h\"\n #include \"object.h\"\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 976bf8ed063..b1350640fed 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -8,6 +8,7 @@\n #include \"branch.h\"\n #include \"refs.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"sigchain.h\"\n #include \"submodule.h\"\n #include \"utf8.h\"\ndiff --git a/hook.c b/hook.c\nindex aa66c968186..06842f50e5c 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -2,6 +2,42 @@\n #include \"hook.h\"\n #include \"run-command.h\"\n \n+const char *find_hook(const char *name)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\n+\tstrbuf_reset(&path);\n+\tstrbuf_git_path(&path, \"hooks/%s\", name);\n+\tif (access(path.buf, X_OK) < 0) {\n+\t\tint err = errno;\n+\n+#ifdef STRIP_EXTENSION\n+\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n+\t\tif (access(path.buf, X_OK) >= 0)\n+\t\t\treturn path.buf;\n+\t\tif (errno == EACCES)\n+\t\t\terr = errno;\n+#endif\n+\n+\t\tif (err == EACCES && advice_ignored_hook) {\n+\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n+\n+\t\t\tif (!string_list_lookup(&advise_given, name)) {\n+\t\t\t\tstring_list_insert(&advise_given, name);\n+\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n+\t\t\t\t\t \"it's not set as executable.\\n\"\n+\t\t\t\t\t \"You can disable this warning with \"\n+\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n+\t\t\t\t       path.buf);\n+\t\t\t}\n+\t\t}\n+\t\treturn NULL;\n+\t}\n+\treturn path.buf;\n+}\n+\n+\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o)\n {\n \tstrvec_clear(&o->env);\ndiff --git a/hook.h b/hook.h\nindex ebfee26bcf2..291ee19469a 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -37,6 +37,13 @@ struct hook_cb_data {\n \tstruct run_hooks_opt *options;\n };\n \n+/*\n+ * Returns the path to the hook file, or NULL if the hook is missing\n+ * or disabled. Note that this points to static storage that will be\n+ * overwritten by further calls to find_hook and run_hook_*.\n+ */\n+const char *find_hook(const char *name);\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /*\ndiff --git a/refs.c b/refs.c\nindex 8c9490235ea..59be29cf081 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -10,6 +10,7 @@\n #include \"refs.h\"\n #include \"refs/refs-internal.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"object-store.h\"\n #include \"object.h\"\n #include \"tag.h\"\ndiff --git a/run-command.c b/run-command.c\nindex be6bc128cd9..82fdf296569 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -8,6 +8,7 @@\n #include \"string-list.h\"\n #include \"quote.h\"\n #include \"config.h\"\n+#include \"hook.h\"\n \n void child_process_init(struct child_process *child)\n {\n@@ -1320,40 +1321,6 @@ int async_with_fork(void)\n #endif\n }\n \n-const char *find_hook(const char *name)\n-{\n-\tstatic struct strbuf path = STRBUF_INIT;\n-\n-\tstrbuf_reset(&path);\n-\tstrbuf_git_path(&path, \"hooks/%s\", name);\n-\tif (access(path.buf, X_OK) < 0) {\n-\t\tint err = errno;\n-\n-#ifdef STRIP_EXTENSION\n-\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n-\t\tif (access(path.buf, X_OK) >= 0)\n-\t\t\treturn path.buf;\n-\t\tif (errno == EACCES)\n-\t\t\terr = errno;\n-#endif\n-\n-\t\tif (err == EACCES && advice_ignored_hook) {\n-\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n-\n-\t\t\tif (!string_list_lookup(&advise_given, name)) {\n-\t\t\t\tstring_list_insert(&advise_given, name);\n-\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n-\t\t\t\t\t \"it's not set as executable.\\n\"\n-\t\t\t\t\t \"You can disable this warning with \"\n-\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n-\t\t\t\t       path.buf);\n-\t\t\t}\n-\t\t}\n-\t\treturn NULL;\n-\t}\n-\treturn path.buf;\n-}\n-\n int run_hook_ve(const char *const *env, const char *name, va_list args)\n {\n \tstruct child_process hook = CHILD_PROCESS_INIT;\ndiff --git a/run-command.h b/run-command.h\nindex d08414a92e7..b58531a7eb3 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -201,13 +201,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/*\n- * Returns the path to the hook file, or NULL if the hook is missing\n- * or disabled. Note that this points to static storage that will be\n- * overwritten by further calls to find_hook and run_hook_*.\n- */\n-const char *find_hook(const char *name);\n-\n /**\n  * Run a hook.\n  * The first argument is a pathname to an index file, or NULL\ndiff --git a/sequencer.c b/sequencer.c\nindex 0bec01cf38e..3de479f90e1 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -8,6 +8,7 @@\n #include \"sequencer.h\"\n #include \"tag.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"utf8.h\"\n #include \"cache-tree.h\"\ndiff --git a/transport.c b/transport.c\nindex 50f5830eb6b..2ed270171f0 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -2,6 +2,7 @@\n #include \"config.h\"\n #include \"transport.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"pkt-line.h\"\n #include \"fetch-pack.h\"\n #include \"remote.h\"\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427288","messageId":"patch-12.30-7756f10aac9-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 12/30] commit: use hook.h to execute hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:01Z","receivedAt":"2021-06-14T10:36:33Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach run_commit_hook() to call hook.h instead of run-command.h. This\ncovers 'pre-commit', 'commit-msg', and\n'prepare-commit-msg'.\n\nAdditionally, ask the hook library - not run-command - whether any\nhooks will be run, as it's possible hooks may exist in the config but\nnot the hookdir.\n\nBecause all but 'post-commit' hooks are expected to make some state\nchange, force all but 'post-commit' hook to run in series. 'post-commit'\n\"is meant primarily for notification, and cannot affect the outcome of\n`git commit`,\" so it is fine to run in parallel.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c |  2 +-\n commit.c         | 16 ++++++++++------\n sequencer.c      |  2 +-\n 3 files changed, 12 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex f1aafd67d46..dad4e565443 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1045,7 +1045,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && find_hook(\"pre-commit\")) {\n+\tif (!no_verify && hook_exists(\"pre-commit\")) {\n \t\t/*\n \t\t * Re-read the index as pre-commit hook could have updated it,\n \t\t * and write it out as a tree.  We must do this before we invoke\ndiff --git a/commit.c b/commit.c\nindex 8ea55a447fa..e8147a88fc6 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -21,6 +21,7 @@\n #include \"commit-reach.h\"\n #include \"run-command.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);\n \n@@ -1698,22 +1699,25 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    const char *name, ...)\n {\n-\tstruct strvec hook_env = STRVEC_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tva_list args;\n+\tconst char *arg;\n \tint ret;\n-\n-\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n+\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\n \t * Let the hook know that no editor will be launched.\n \t */\n \tif (!editor_is_used)\n-\t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n+\t\tstrvec_push(&opt.env, \"GIT_EDITOR=:\");\n \n \tva_start(args, name);\n-\tret = run_hook_ve(hook_env.v, name, args);\n+\twhile ((arg = va_arg(args, const char *)))\n+\t\tstrvec_push(&opt.args, arg);\n \tva_end(args);\n-\tstrvec_clear(&hook_env);\n+\n+\tret = run_hooks(name, &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \treturn ret;\n }\ndiff --git a/sequencer.c b/sequencer.c\nindex 3de479f90e1..8f46984ffb7 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1446,7 +1446,7 @@ static int try_to_commit(struct repository *r,\n \t\t}\n \t}\n \n-\tif (find_hook(\"prepare-commit-msg\")) {\n+\tif (hook_exists(\"prepare-commit-msg\")) {\n \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n \t\tif (res)\n \t\t\tgoto out;\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427289","messageId":"patch-13.30-0300607a9b4-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 13/30] read-cache: convert post-index-change hook to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:02Z","receivedAt":"2021-06-14T10:36:35Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using hook.h instead of run-command.h to run, post-index-change hooks\ncan now be specified in the config in addition to the hookdir.\npost-index-change is not run anywhere besides in read-cache.c.\n\nThis removes the last direct user of run_hook_ve(), so we can make the\nfunction static now. It'll be removed entirely soon.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n read-cache.c  | 11 ++++++++---\n run-command.c |  2 +-\n run-command.h |  1 -\n 3 files changed, 9 insertions(+), 5 deletions(-)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex af5b97104cf..f801313cc95 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3063,6 +3063,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tret = convert_to_sparse(istate);\n \n@@ -3091,9 +3092,13 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \telse\n \t\tret = close_lock_file_gently(lock);\n \n-\trun_hook_le(NULL, \"post-index-change\",\n-\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n-\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n+\tstrvec_pushl(&hook_opt.args,\n+\t\t     istate->updated_workdir ? \"1\" : \"0\",\n+\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trun_hooks(\"post-index-change\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n+\n \tistate->updated_workdir = 0;\n \tistate->updated_skipworktree = 0;\n \ndiff --git a/run-command.c b/run-command.c\nindex 82fdf296569..eecdef5a0c8 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1321,7 +1321,7 @@ int async_with_fork(void)\n #endif\n }\n \n-int run_hook_ve(const char *const *env, const char *name, va_list args)\n+static int run_hook_ve(const char *const *env, const char *name, va_list args)\n {\n \tstruct child_process hook = CHILD_PROCESS_INIT;\n \tconst char *p;\ndiff --git a/run-command.h b/run-command.h\nindex b58531a7eb3..24ab5d63c4c 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -216,7 +216,6 @@ int run_command(struct child_process *);\n  */\n LAST_ARG_MUST_BE_NULL\n int run_hook_le(const char *const *env, const char *name, ...);\n-int run_hook_ve(const char *const *env, const char *name, va_list args);\n \n /*\n  * Trigger an auto-gc\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427290","messageId":"patch-09.30-8d8b2d26453-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 09/30] git hook run: add an --ignore-missing flag","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:58Z","receivedAt":"2021-06-14T10:36:39Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"For certain one-shot hooks we'd like to optimistically run them, and\nnot complain if they don't exist. This will be used by send-email in a\nsubsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt | 10 +++++++++-\n builtin/hook.c             |  5 +++++\n t/t1800-hook.sh            |  5 +++++\n 3 files changed, 19 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 902b9cffaef..1528c860cf1 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run <hook-name> [-- <hook-args>]\n+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -27,6 +27,14 @@ run::\n \t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n \tthis accepts.\n \n+OPTIONS\n+-------\n+\n+--ignore-missing::\n+\tIgnore any missing hook by quietly returning zero. Used for\n+\ttools that want to do a blind one-shot run of a hook that may\n+\tor may not be present.\n+\n SEE ALSO\n --------\n linkgit:githooks[5]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 1b1a594fd00..275dd5b0ed0 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -16,10 +16,13 @@ static int run(int argc, const char **argv, const char *prefix)\n \tint i;\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint rc = 0;\n+\tint ignore_missing = 0;\n \tconst char *hook_name;\n \tconst char *hook_path;\n \n \tstruct option run_options[] = {\n+\t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n+\t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n \t\tOPT_END(),\n \t};\n \n@@ -42,6 +45,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \thook_name = argv[1];\n \thook_path = find_hook(hook_name);\n \tif (!hook_path) {\n+\t\tif (ignore_missing)\n+\t\t\treturn 0;\n \t\terror(\"cannot find a hook named %s\", hook_name);\n \t\treturn 1;\n \t}\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex f6ff6c4a493..714dfd08cd8 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -12,6 +12,11 @@ test_expect_success 'git hook run -- nonexistent hook' '\n \ttest_cmp stderr.expect stderr.actual\n '\n \n+test_expect_success 'git hook run -- nonexistent hook with --ignore-missing' '\n+\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n+\ttest_must_be_empty stderr.actual\n+'\n+\n test_expect_success 'git hook run -- basic' '\n \twrite_script .git/hooks/test-hook <<-EOF &&\n \techo Test hook\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427291","messageId":"patch-07.30-d884465aab3-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 07/30] hooks: convert 'post-checkout' hook to hook library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:56Z","receivedAt":"2021-06-14T10:36:41Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the running of the 'post-checkout' hook away from run-command.h\nto the new hook.h library. For \"worktree\" this requires a change to it\nto run the hooks from a given directory.\n\nWe could strictly speaking skip the \"absolute_path\" flag and just\ncheck if \"dir\" is specified, but let's split them up for clarity, as\nwell as for any future user who'd like to set \"dir\" but not implicitly\nchange the argument to an absolute path.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/checkout.c | 17 ++++++++++++-----\n builtin/clone.c    |  7 +++++--\n builtin/worktree.c | 30 ++++++++++++++----------------\n hook.c             |  8 ++++++++\n hook.h             |  9 +++++++++\n read-cache.c       |  1 +\n reset.c            | 15 +++++++++++----\n 7 files changed, 60 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f4cd7747d35..6205ace09f6 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"diff.h\"\n #include \"dir.h\"\n+#include \"hook.h\"\n #include \"ll-merge.h\"\n #include \"lockfile.h\"\n #include \"merge-recursive.h\"\n@@ -106,13 +107,19 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\treturn run_hook_le(NULL, \"post-checkout\",\n-\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n-\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n-\t\t\t   changed ? \"1\" : \"0\", NULL);\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint rc;\n+\n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\n-\n+\tstrvec_pushl(&opt.args,\n+\t\t     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n+\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n+\t\t     changed ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trc = run_hooks(\"post-checkout\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n }\n \n static int update_some(const struct object_id *oid, struct strbuf *base,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 66fe66679c8..de57a3119b7 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -32,6 +32,7 @@\n #include \"connected.h\"\n #include \"packfile.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"hook.h\"\n \n /*\n  * Overall FIXMEs:\n@@ -775,6 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tif (option_no_checkout)\n \t\treturn 0;\n@@ -820,8 +822,9 @@ static int checkout(int submodule_progress)\n \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n \t\tdie(_(\"unable to write new index file\"));\n \n-\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(null_oid()),\n-\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n+\tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n+\terr |= run_hooks(\"post-checkout\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (!err && (option_recurse_submodules.nr > 0)) {\n \t\tstruct strvec args = STRVEC_INIT;\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex b1350640fed..2ad26a76f4c 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -382,22 +382,20 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tconst char *hook = find_hook(\"post-checkout\");\n-\t\tif (hook) {\n-\t\t\tconst char *env[] = { \"GIT_DIR\", \"GIT_WORK_TREE\", NULL };\n-\t\t\tcp.git_cmd = 0;\n-\t\t\tcp.no_stdin = 1;\n-\t\t\tcp.stdout_to_stderr = 1;\n-\t\t\tcp.dir = path;\n-\t\t\tcp.env = env;\n-\t\t\tcp.argv = NULL;\n-\t\t\tcp.trace2_hook_name = \"post-checkout\";\n-\t\t\tstrvec_pushl(&cp.args, absolute_path(hook),\n-\t\t\t\t     oid_to_hex(null_oid()),\n-\t\t\t\t     oid_to_hex(&commit->object.oid),\n-\t\t\t\t     \"1\", NULL);\n-\t\t\tret = run_command(&cp);\n-\t\t}\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(null_oid()),\n+\t\t\t     oid_to_hex(&commit->object.oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\topt.dir = path;\n+\t\topt.absolute_path = 1;\n+\n+\t\tret = run_hooks(\"post-checkout\", &opt);\n+\n+\t\trun_hooks_opt_clear(&opt);\n \t}\n \n \tstrvec_clear(&child_env);\ndiff --git a/hook.c b/hook.c\nindex c7da273822d..51337f9798f 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -62,6 +62,7 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\n+\tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n \tstrvec_push(&cp->args, run_me->hook_path);\n@@ -111,6 +112,7 @@ static int notify_hook_finished(int result,\n int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t    struct run_hooks_opt *options)\n {\n+\tstruct strbuf abs_path = STRBUF_INIT;\n \tstruct hook my_hook = {\n \t\t.hook_path = hook_path,\n \t};\n@@ -119,6 +121,10 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t.hook_name = hook_name,\n \t\t.options = options,\n \t};\n+\tif (options->absolute_path) {\n+\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n+\t\tmy_hook.hook_path = abs_path.buf;\n+\t}\n \tcb_data.run_me = &my_hook;\n \n \tif (options->jobs != 1)\n@@ -131,6 +137,8 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n+\tif (options->absolute_path)\n+\t\tstrbuf_release(&abs_path);\n \n \treturn cb_data.rc;\n }\ndiff --git a/hook.h b/hook.h\nindex cbda7746a5d..2d7724bbb50 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -19,6 +19,15 @@ struct run_hooks_opt\n \n \t/* Number of threads to parallelize across */\n \tint jobs;\n+\n+\t/* Resolve and run the \"absolute_path(hook)\" instead of\n+\t * \"hook\". Used for \"git worktree\" hooks\n+\t */\n+\tint absolute_path;\n+\n+\t/* Path to initial working directory for subprocess */\n+\tconst char *dir;\n+\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/read-cache.c b/read-cache.c\nindex 77961a38854..af5b97104cf 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -27,6 +27,7 @@\n #include \"progress.h\"\n #include \"sparse-index.h\"\n #include \"csum-file.h\"\n+#include \"hook.h\"\n \n /* Mask for the name length in ce_flags in the on-disk index */\n \ndiff --git a/reset.c b/reset.c\nindex 4bea758053b..e6af33b901c 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -7,6 +7,7 @@\n #include \"tree-walk.h\"\n #include \"tree.h\"\n #include \"unpack-trees.h\"\n+#include \"hook.h\"\n \n int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t       const char *switch_to_branch, unsigned flags,\n@@ -126,10 +127,16 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\tret = create_symref(\"HEAD\", switch_to_branch,\n \t\t\t\t\t    reflog_head);\n \t}\n-\tif (run_hook)\n-\t\trun_hook_le(NULL, \"post-checkout\",\n-\t\t\t    oid_to_hex(orig ? orig : null_oid()),\n-\t\t\t    oid_to_hex(oid), \"1\", NULL);\n+\tif (run_hook) {\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(orig ? orig : null_oid()),\n+\t\t\t     oid_to_hex(oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\trun_hooks(\"post-checkout\", &opt);\n+\t\trun_hooks_opt_clear(&opt);\n+\t}\n \n leave_reset_head:\n \tstrbuf_release(&msg);\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427292","messageId":"patch-06.30-d2f3b26d464-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 06/30] am: convert applypatch hooks to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:55Z","receivedAt":"2021-06-14T10:36:45Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach pre-applypatch, post-applypatch, and applypatch-msg to use the\nhook.h library instead of the run-command.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 15 ++++++++++++---\n 1 file changed, 12 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 1c8a5489035..9e9c1b5e9f2 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -445,9 +445,12 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tassert(state->msg);\n-\tret = run_hook_le(NULL, \"applypatch-msg\", am_path(state, \"final-commit\"), NULL);\n+\tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n+\tret = run_hooks(\"applypatch-msg\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tif (!ret) {\n \t\tFREE_AND_NULL(state->msg);\n@@ -1607,9 +1610,13 @@ static void do_commit(const struct am_state *state)\n \tstruct commit_list *parents = NULL;\n \tconst char *reflog_msg, *author, *committer = NULL;\n \tstruct strbuf sb = STRBUF_INIT;\n+\tstruct run_hooks_opt hook_opt_pre = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt_post = RUN_HOOKS_OPT_INIT;\n \n-\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n+\tif (run_hooks(\"pre-applypatch\", &hook_opt_pre)) {\n+\t\trun_hooks_opt_clear(&hook_opt_pre);\n \t\texit(1);\n+\t}\n \n \tif (write_cache_as_tree(&tree, 0, NULL))\n \t\tdie(_(\"git write-tree failed to write a tree\"));\n@@ -1660,8 +1667,10 @@ static void do_commit(const struct am_state *state)\n \t\tfclose(fp);\n \t}\n \n-\trun_hook_le(NULL, \"post-applypatch\", NULL);\n+\trun_hooks(\"post-applypatch\", &hook_opt_post);\n \n+\trun_hooks_opt_clear(&hook_opt_pre);\n+\trun_hooks_opt_clear(&hook_opt_post);\n \tstrbuf_release(&sb);\n }\n \n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427293","messageId":"patch-05.30-51e6e72f239-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 05/30] rebase: teach pre-rebase to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:54Z","receivedAt":"2021-06-14T10:36:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the pre-rebase hook away from run-command.h to and over to the\nnew hook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/rebase.c | 9 +++++++--\n 1 file changed, 7 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 12f093121d9..2081f6fa8db 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -28,6 +28,7 @@\n #include \"sequencer.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_REFLOG_ACTION \"rebase\"\n \n@@ -1313,6 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\n@@ -2022,10 +2024,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t}\n \n \t/* If a hook exists, give it a chance to interrupt*/\n+\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n \tif (!ok_to_skip_pre_rebase &&\n-\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n-\t\t\targc ? argv[0] : NULL, NULL))\n+\t    run_hooks(\"pre-rebase\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n+\t}\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (options.flags & REBASE_DIFFSTAT) {\n \t\tstruct diff_options opts;\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427294","messageId":"patch-04.30-da2763192ae-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 04/30] gc: use hook library for pre-auto-gc hook","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:53Z","receivedAt":"2021-06-14T10:36:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nUsing the hook.h library instead of the run-command.h library to run\npre-auto-gc means that those hooks can be set up in config files, as\nwell as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c | 8 +++++++-\n 1 file changed, 7 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1ac..a12641a691d 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -32,6 +32,7 @@\n #include \"remote.h\"\n #include \"object-store.h\"\n #include \"exec-cmd.h\"\n+#include \"hook.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -348,6 +349,8 @@ static void add_repack_incremental_option(void)\n \n static int need_to_gc(void)\n {\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n \t * automatic gc.\n@@ -394,8 +397,11 @@ static int need_to_gc(void)\n \telse\n \t\treturn 0;\n \n-\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n+\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\treturn 0;\n+\t}\n+\trun_hooks_opt_clear(&hook_opt);\n \treturn 1;\n }\n \n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427295","messageId":"patch-08.30-ad6e7507841-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 08/30] merge: use config-based hooks for post-merge hook","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:57Z","receivedAt":"2021-06-14T10:36:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach post-merge to use the hook.h library instead of the run-command.h\nlibrary to run hooks. This means that post-merge hooks can come from the\nconfig as well as from the hookdir. post-merge is invoked only from\nbuiltin/merge.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/merge.c | 7 +++++--\n 1 file changed, 5 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex be98d66b0a8..6128b60942f 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,6 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\n@@ -489,7 +490,9 @@ static void finish(struct commit *head_commit,\n \t}\n \n \t/* Run a post-merge hook */\n-\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n+\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n+\trun_hooks(\"post-merge\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tapply_autostash(git_path_merge_autostash(the_repository));\n \tstrbuf_release(&reflog_message);\n@@ -849,7 +852,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t * and write it out as a tree.  We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (find_hook(\"pre-merge-commit\"))\n+\tif (hook_exists(\"pre-merge-commit\"))\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427296","messageId":"patch-11.30-aa970a81752-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 11/30] git-p4: use 'git hook' to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:00Z","receivedAt":"2021-06-14T10:36:58Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nInstead of duplicating the behavior of run-command.h:run_hook_le() in\nPython, we can directly call 'git hook run'. We emulate the existence\ncheck with the --ignore-missing flag.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-p4.py | 72 ++++++-------------------------------------------------\n 1 file changed, 7 insertions(+), 65 deletions(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex d34a1946b75..e76d8df3139 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -207,71 +207,13 @@ def decode_path(path):\n         return path\n \n def run_git_hook(cmd, param=[]):\n-    \"\"\"Execute a hook if the hook exists.\"\"\"\n-    if verbose:\n-        sys.stderr.write(\"Looking for hook: %s\\n\" % cmd)\n-        sys.stderr.flush()\n-\n-    hooks_path = gitConfig(\"core.hooksPath\")\n-    if len(hooks_path) <= 0:\n-        hooks_path = os.path.join(os.environ[\"GIT_DIR\"], \"hooks\")\n-\n-    if not isinstance(param, list):\n-        param=[param]\n-\n-    # resolve hook file name, OS depdenent\n-    hook_file = os.path.join(hooks_path, cmd)\n-    if platform.system() == 'Windows':\n-        if not os.path.isfile(hook_file):\n-            # look for the file with an extension\n-            files = glob.glob(hook_file + \".*\")\n-            if not files:\n-                return True\n-            files.sort()\n-            hook_file = files.pop()\n-            while hook_file.upper().endswith(\".SAMPLE\"):\n-                # The file is a sample hook. We don't want it\n-                if len(files) > 0:\n-                    hook_file = files.pop()\n-                else:\n-                    return True\n-\n-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):\n-        return True\n-\n-    return run_hook_command(hook_file, param) == 0\n-\n-def run_hook_command(cmd, param):\n-    \"\"\"Executes a git hook command\n-       cmd = the command line file to be executed. This can be\n-       a file that is run by OS association.\n-\n-       param = a list of parameters to pass to the cmd command\n-\n-       On windows, the extension is checked to see if it should\n-       be run with the Git for Windows Bash shell.  If there\n-       is no file extension, the file is deemed a bash shell\n-       and will be handed off to sh.exe. Otherwise, Windows\n-       will be called with the shell to handle the file assocation.\n-\n-       For non Windows operating systems, the file is called\n-       as an executable.\n-    \"\"\"\n-    cli = [cmd] + param\n-    use_shell = False\n-    if platform.system() == 'Windows':\n-        (root,ext) = os.path.splitext(cmd)\n-        if ext == \"\":\n-            exe_path = os.environ.get(\"EXEPATH\")\n-            if exe_path is None:\n-                exe_path = \"\"\n-            else:\n-                exe_path = os.path.join(exe_path, \"bin\")\n-            cli = [os.path.join(exe_path, \"SH.EXE\")] + cli\n-        else:\n-            use_shell = True\n-    return subprocess.call(cli, shell=use_shell)\n-\n+    \"\"\"args are specified with -a <arg> -a <arg> -a <arg>\"\"\"\n+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]\n+    if param:\n+        args.append(\"--\")\n+        for p in param:\n+            args.append(p)\n+    return subprocess.call(args) == 0\n \n def write_pipe(c, stdin):\n     if verbose:\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427297","messageId":"patch-16.30-53a3877a476-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 16/30] run-command: allow stdin for run_processes_parallel","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:05Z","receivedAt":"2021-06-14T10:37:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nWhile it makes sense not to inherit stdin from the parent process to\navoid deadlocking, it's not necessary to completely ban stdin to\nchildren. An informed user should be able to configure stdin safely. By\nsetting `some_child.process.no_stdin=1` before calling `get_next_task()`\nwe provide a reasonable default behavior but enable users to set up\nstdin streaming for themselves during the callback.\n\n`some_child.process.stdout_to_stderr`, however, remains unmodifiable by\n`get_next_task()` - the rest of the run_processes_parallel() API depends\non child output in stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n run-command.c | 9 ++++++++-\n 1 file changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex 95c950a4a2b..0bf771845e4 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1628,6 +1628,14 @@ static int pp_start_one(struct parallel_processes *pp)\n \tif (i == pp->max_processes)\n \t\tBUG(\"bookkeeping is hard\");\n \n+\t/*\n+\t * By default, do not inherit stdin from the parent process - otherwise,\n+\t * all children would share stdin! Users may overwrite this to provide\n+\t * something to the child's stdin by having their 'get_next_task'\n+\t * callback assign 0 to .no_stdin and an appropriate integer to .in.\n+\t */\n+\tpp->children[i].process.no_stdin = 1;\n+\n \tcode = pp->get_next_task(&pp->children[i].process,\n \t\t\t\t &pp->children[i].err,\n \t\t\t\t pp->data,\n@@ -1639,7 +1647,6 @@ static int pp_start_one(struct parallel_processes *pp)\n \t}\n \tpp->children[i].process.err = -1;\n \tpp->children[i].process.stdout_to_stderr = 1;\n-\tpp->children[i].process.no_stdin = 1;\n \n \tif (start_command(&pp->children[i].process)) {\n \t\tcode = pp->start_failure(&pp->children[i].err,\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427298","messageId":"patch-17.30-c4f60db606d-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 17/30] hook: support passing stdin to hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:06Z","receivedAt":"2021-06-14T10:37:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome hooks (such as post-rewrite) need to take input via stdin.\nPreviously, callers provided stdin to hooks by setting\nrun-command.h:child_process.in, which takes a FD. Callers would open the\nfile in question themselves before calling run-command(). However, since\nwe will now need to seek to the front of the file and read it again for\nevery hook which runs, hook.h:run_command() takes a path and handles FD\nmanagement itself. Since this file is opened for read only, it should\nnot prevent later parallel execution support.\n\nOn the frontend, this is supported by asking for a file path, rather\nthan by reading stdin. Reading directly from stdin would involve caching\nthe entire stdin (to memory or to disk) and reading it back from the\nbeginning to each hook. We'd want to support cases like insufficient\nmemory or storage for the file. While this may prove useful later, for\nnow the path of least resistance is to just ask the user to make this\ninterim file themselves.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt |  7 ++++++-\n builtin/hook.c             |  4 +++-\n hook.c                     |  8 +++++++-\n hook.h                     |  2 ++\n t/t1800-hook.sh            | 18 ++++++++++++++++++\n 5 files changed, 36 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 1528c860cf1..816b3eda460 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -30,6 +30,11 @@ run::\n OPTIONS\n -------\n \n+--to-stdin::\n+\tFor \"run\"; Specify a file which will be streamed into the\n+\thook's stdin. The hook will receive the entire file from\n+\tbeginning to EOF.\n+\n --ignore-missing::\n \tIgnore any missing hook by quietly returning zero. Used for\n \ttools that want to do a blind one-shot run of a hook that may\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 275dd5b0ed0..baaef4dce49 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -7,7 +7,7 @@\n #include \"strvec.h\"\n \n static const char * const builtin_hook_usage[] = {\n-\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n+\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n \tNULL\n };\n \n@@ -23,6 +23,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \tstruct option run_options[] = {\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n+\t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n+\t\t\t   N_(\"file to read into hooks' stdin\")),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/hook.c b/hook.c\nindex 51337f9798f..daf39f61741 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -58,7 +58,13 @@ static int pick_next_hook(struct child_process *cp,\n \tif (!run_me)\n \t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n \n-\tcp->no_stdin = 1;\n+\t/* reopen the file for stdin; run_command closes it. */\n+\tif (hook_cb->options->path_to_stdin) {\n+\t\tcp->no_stdin = 0;\n+\t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else {\n+\t\tcp->no_stdin = 1;\n+\t}\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\ndiff --git a/hook.h b/hook.h\nindex 2d7724bbb50..74a8c76a94c 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -28,6 +28,8 @@ struct run_hooks_opt\n \t/* Path to initial working directory for subprocess */\n \tconst char *dir;\n \n+\t/* Path to file which should be piped to stdin for each hook */\n+\tconst char *path_to_stdin;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 714dfd08cd8..1a6b708137e 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -133,4 +133,22 @@ test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n \tEOF\n '\n \n+test_expect_success 'stdin to hooks' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo BEGIN stdin\n+\tcat\n+\techo END stdin\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\tBEGIN stdin\n+\thello\n+\tEND stdin\n+\tEOF\n+\n+\techo hello >input &&\n+\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427299","messageId":"patch-10.30-1953326d1db-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 10/30] send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:32:59Z","receivedAt":"2021-06-14T10:38:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nChange the \"sendmail-validate\" hook to be run via the \"git hook run\"\nwrapper instead of via a direct invocation.\n\nThis is the smallest possibly change to get \"send-email\" using \"git\nhook run\". We still check the hook itself with \"-x\", and set a\n\"GIT_DIR\" variable, both of which are asserted by our tests. We'll\nneed to get rid of this special behavior if we start running N hooks,\nbut for now let's be as close to bug-for-bug compatible as possible.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl   | 20 ++++++++++++--------\n t/t9001-send-email.sh |  4 ++--\n 2 files changed, 14 insertions(+), 10 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 7ba0b3433d7..9e474304036 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -214,13 +214,13 @@ sub format_2822_time {\n my $editor;\n \n sub system_or_msg {\n-\tmy ($args, $msg) = @_;\n+\tmy ($args, $msg, $cmd_name) = @_;\n \tsystem(@$args);\n \tmy $signalled = $? & 127;\n \tmy $exit_code = $? >> 8;\n \treturn unless $signalled or $exit_code;\n \n-\tmy @sprintf_args = ($args->[0], $exit_code);\n+\tmy @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);\n \tif (defined $msg) {\n \t\t# Quiet the 'redundant' warning category, except we\n \t\t# need to support down to Perl 5.8, so we can't do a\n@@ -1979,9 +1979,9 @@ sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n \tif ($repo) {\n+\t\tmy $hook_name = 'sendemail-validate';\n \t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n-\t\tmy $validate_hook = catfile($hooks_path,\n-\t\t\t\t\t    'sendemail-validate');\n+\t\tmy $validate_hook = catfile($hooks_path, $hook_name);\n \t\tmy $hook_error;\n \t\tif (-x $validate_hook) {\n \t\t\tmy $target = abs_path($fn);\n@@ -1990,13 +1990,17 @@ sub validate_patch {\n \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n \t\t\t\tor die(\"chdir: $!\");\n \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n+\t\t\tmy @validate_hook = (\"git\", \"hook\", \"run\", \"--ignore-missing\", $hook_name, \"--\", $target);\n+\t\t\t$hook_error = system_or_msg(\\@validate_hook, undef,\n+\t\t\t\t\t\t       \"git hook run $hook_name -- <patch>\");\n \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n \t\t}\n \t\tif ($hook_error) {\n-\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n-\t\t\t\t       \"%s\\n\" .\n-\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n+\t\t\t$hook_error = sprintf(__(\"fatal: %s: rejected by %s hook\\n\" .\n+\t\t\t\t\t\t $hook_error . \"\\n\" .\n+\t\t\t\t\t\t \"warning: no patches were sent\\n\"),\n+\t\t\t\t\t      $fn, $hook_name);\n+\t\t\tdie $hook_error;\n \t\t}\n \t}\n \ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 30eff725a96..6d4e25df8dc 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -539,7 +539,7 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'my-hooks/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -558,7 +558,7 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'$hooks_path/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427300","messageId":"patch-26.30-e11a8f60071-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 26/30] receive-pack: convert 'update' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:15Z","receivedAt":"2021-06-14T10:38:21Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nThis makes use of the new sideband API in hook.h added in the\npreceding commit.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 65 ++++++++++++++++++++++++++++--------------\n 1 file changed, 44 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 5248228ebfe..378f8f6b5d1 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -938,33 +938,56 @@ static int run_receive_hook(struct command *commands,\n \treturn status;\n }\n \n-static int run_update_hook(struct command *cmd)\n+static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n {\n-\tconst char *argv[5];\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tint code;\n+\tint keepalive_active = 0;\n \n-\targv[0] = find_hook(\"update\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tif (keepalive_in_sec <= 0)\n+\t\tuse_keepalive = KEEPALIVE_NEVER;\n+\tif (use_keepalive == KEEPALIVE_ALWAYS)\n+\t\tkeepalive_active = 1;\n \n-\targv[1] = cmd->ref_name;\n-\targv[2] = oid_to_hex(&cmd->old_oid);\n-\targv[3] = oid_to_hex(&cmd->new_oid);\n-\targv[4] = NULL;\n+\t/* send a keepalive if there is no data to write */\n+\tif (keepalive_active && !output->len) {\n+\t\tstatic const char buf[] = \"0005\\1\";\n+\t\twrite_or_die(1, buf, sizeof(buf) - 1);\n+\t\treturn;\n+\t}\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.argv = argv;\n-\tproc.trace2_hook_name = \"update\";\n+\tif (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {\n+\t\tconst char *first_null = memchr(output->buf, '\\0', output->len);\n+\t\tif (first_null) {\n+\t\t\t/* The null bit is excluded. */\n+\t\t\tsize_t before_null = first_null - output->buf;\n+\t\t\tsize_t after_null = output->len - (before_null + 1);\n+\t\t\tkeepalive_active = 1;\n+\t\t\tsend_sideband(1, 2, output->buf, before_null, use_sideband);\n+\t\t\tsend_sideband(1, 2, first_null + 1, after_null, use_sideband);\n+\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n+}\n+\n+static int run_update_hook(struct command *cmd)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint code;\n+\n+\tstrvec_pushl(&opt.args,\n+\t\t     cmd->ref_name,\n+\t\t     oid_to_hex(&cmd->old_oid),\n+\t\t     oid_to_hex(&cmd->new_oid),\n+\t\t     NULL);\n \n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n \tif (use_sideband)\n-\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\treturn finish_command(&proc);\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\tcode = run_hooks(\"update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn code;\n }\n \n static struct command *find_command_by_refname(struct command *list,\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427301","messageId":"patch-15.30-477eb2245c3-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 15/30] run-command: remove old run_hook_{le,ve}() hook API","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:04Z","receivedAt":"2021-06-14T10:38:24Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nThe new hook.h library has replaced all run-command.h hook-related\nfunctionality. So let's delete this dead code.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n run-command.c | 32 --------------------------------\n run-command.h | 16 ----------------\n 2 files changed, 48 deletions(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex eecdef5a0c8..95c950a4a2b 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1321,38 +1321,6 @@ int async_with_fork(void)\n #endif\n }\n \n-static int run_hook_ve(const char *const *env, const char *name, va_list args)\n-{\n-\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\tconst char *p;\n-\n-\tp = find_hook(name);\n-\tif (!p)\n-\t\treturn 0;\n-\n-\tstrvec_push(&hook.args, p);\n-\twhile ((p = va_arg(args, const char *)))\n-\t\tstrvec_push(&hook.args, p);\n-\thook.env = env;\n-\thook.no_stdin = 1;\n-\thook.stdout_to_stderr = 1;\n-\thook.trace2_hook_name = name;\n-\n-\treturn run_command(&hook);\n-}\n-\n-int run_hook_le(const char *const *env, const char *name, ...)\n-{\n-\tva_list args;\n-\tint ret;\n-\n-\tva_start(args, name);\n-\tret = run_hook_ve(env, name, args);\n-\tva_end(args);\n-\n-\treturn ret;\n-}\n-\n struct io_pump {\n \t/* initialized by caller */\n \tint fd;\ndiff --git a/run-command.h b/run-command.h\nindex 24ab5d63c4c..748d4fc2a72 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -201,22 +201,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/**\n- * Run a hook.\n- * The first argument is a pathname to an index file, or NULL\n- * if the hook uses the default index file or no index is needed.\n- * The second argument is the name of the hook.\n- * The further arguments correspond to the hook arguments.\n- * The last argument has to be NULL to terminate the arguments list.\n- * If the hook does not exist or is not executable, the return\n- * value will be zero.\n- * If it is executable, the hook will be executed and the exit\n- * status of the hook is returned.\n- * On execution, .stdout_to_stderr and .no_stdin will be set.\n- */\n-LAST_ARG_MUST_BE_NULL\n-int run_hook_le(const char *const *env, const char *name, ...);\n-\n /*\n  * Trigger an auto-gc\n  */\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427302","messageId":"patch-29.30-793f112f7ab-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 29/30] hooks: fix a TOCTOU in \"did we run a hook?\" heuristic","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:18Z","receivedAt":"2021-06-14T10:38:29Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a Time-of-check to time-of-use (TOCTOU) race in code added in\n680ee550d72 (commit: skip discarding the index if there is no\npre-commit hook, 2017-08-14).\n\nWe can fix the race passing around information about whether or not we\nran the hook in question, instead of running hook_exists() after the\nfact to check if the hook in question exists. This problem has been\nnoted on-list when 680ee550d72 was discussed[1], but had not been\nfixed.\n\nIn addition to fixing this for the pre-commit hook as suggested there\nI'm also fixing this for the pre-merge-commit hook. See\n6098817fd7f (git-merge: honor pre-merge-commit hook, 2019-08-07) for\nthe introduction of its previous behavior.\n\nLet's also change this for the push-to-checkout hook. Now instead of\nchecking if the hook exists and either doing a push to checkout or a\npush to deploy we'll always attempt a push to checkout. If the hook\ndoesn't exist we'll fall back on push to deploy. The same behavior as\nbefore, without the TOCTOU race. See 0855331941b (receive-pack:\nsupport push-to-checkout hook, 2014-12-01) for the introduction of the\nprevious behavior.\n\nThis leaves uses of hook_exists() in two places that matter. The\n\"reference-transaction\" check in refs.c, see 67541597670 (refs:\nimplement reference transaction hook, 2020-06-19), and the\nprepare-commit-msg hook, see 66618a50f9c (sequencer: run\n'prepare-commit-msg' hook, 2018-01-24).\n\nIn both of those cases we're saving ourselves CPU time by not\npreparing data for the hook that we'll then do nothing with if we\ndon't have the hook. So using this \"invoked_hook\" pattern doesn't make\nsense in those cases.\n\nMore importantly, in those cases the worst we'll do is miss that we\n\"should\" run the hook because a new hook appeared, whereas in the\npre-commit and pre-merge-commit cases we'll skip an important\ndiscard_cache() on the bases of our faulty guess.\n\nI do think none of these races really matter in practice. It would be\nsome one-off issue as a hook was added or removed. I did think it was\nstupid that we didn't pass a \"did this run?\" flag instead of doing\nthis guessing at a distance though, so now we're not guessing anymore.\n\n1. https://lore.kernel.org/git/20170810191613.kpmhzg4seyxy3cpq@sigill.intra.peff.net/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c       | 18 +++++++++++-------\n builtin/merge.c        | 16 ++++++++++------\n builtin/receive-pack.c |  8 +++++---\n commit.c               |  1 +\n commit.h               |  3 ++-\n hook.c                 |  4 ++++\n hook.h                 | 10 ++++++++++\n sequencer.c            |  4 ++--\n 8 files changed, 45 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex dad4e565443..a66727a612a 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -725,11 +725,13 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \tint clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);\n \tint old_display_comment_prefix;\n \tint merge_contains_scissors = 0;\n+\tint invoked_hook = 0;\n \n \t/* This checks and barfs if author is badly specified */\n \tdetermine_author_info(author_ident);\n \n-\tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(use_editor, index_file, &invoked_hook,\n+\t\t\t\t\t  \"pre-commit\", NULL))\n \t\treturn 0;\n \n \tif (squash_message) {\n@@ -1045,10 +1047,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && hook_exists(\"pre-commit\")) {\n+\tif (!no_verify && invoked_hook) {\n \t\t/*\n-\t\t * Re-read the index as pre-commit hook could have updated it,\n-\t\t * and write it out as a tree.  We must do this before we invoke\n+\t\t * Re-read the index as the pre-commit-commit hook was invoked\n+\t\t * and could have updated it. We must do this before we invoke\n \t\t * the editor and after we invoke run_status above.\n \t\t */\n \t\tdiscard_cache();\n@@ -1060,7 +1062,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n+\tif (run_commit_hook(use_editor, index_file, NULL, \"prepare-commit-msg\",\n \t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n \t\treturn 0;\n \n@@ -1077,7 +1079,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t}\n \n \tif (!no_verify &&\n-\t    run_commit_hook(use_editor, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n+\t    run_commit_hook(use_editor, index_file, NULL, \"commit-msg\",\n+\t\t\t    git_path_commit_editmsg(), NULL)) {\n \t\treturn 0;\n \t}\n \n@@ -1830,7 +1833,8 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \n \trepo_rerere(the_repository, 0);\n \trun_auto_maintenance(quiet);\n-\trun_commit_hook(use_editor, get_index_file(), \"post-commit\", NULL);\n+\trun_commit_hook(use_editor, get_index_file(), NULL, \"post-commit\",\n+\t\t\tNULL);\n \tif (amend && !no_post_rewrite) {\n \t\tcommit_post_rewrite(the_repository, current_head, &oid);\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 6128b60942f..0425c9bf2b5 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -844,15 +844,18 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n {\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *index_file = get_index_file();\n+\tint invoked_hook = 0;\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, index_file, \"pre-merge-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(0 < option_edit, index_file,\n+\t\t\t\t\t  &invoked_hook, \"pre-merge-commit\",\n+\t\t\t\t\t  NULL))\n \t\tabort_commit(remoteheads, NULL);\n \t/*\n-\t * Re-read the index as pre-merge-commit hook could have updated it,\n-\t * and write it out as a tree.  We must do this before we invoke\n+\t * Re-read the index as the pre-merge-commit hook was invoked\n+\t * and could have updated it. We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (hook_exists(\"pre-merge-commit\"))\n+\tif (invoked_hook)\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n@@ -873,7 +876,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tappend_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);\n \twrite_merge_heads(remoteheads);\n \twrite_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);\n-\tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n+\tif (run_commit_hook(0 < option_edit, get_index_file(), NULL,\n+\t\t\t    \"prepare-commit-msg\",\n \t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \tif (0 < option_edit) {\n@@ -882,7 +886,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t}\n \n \tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n-\t\t\t\t\t  \"commit-msg\",\n+\t\t\t\t\t  NULL, \"commit-msg\",\n \t\t\t\t\t  git_path_merge_msg(the_repository), NULL))\n \t\tabort_commit(remoteheads, NULL);\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ec90e10477a..cd658f41d58 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1439,10 +1439,12 @@ static const char *push_to_deploy(unsigned char *sha1,\n static const char *push_to_checkout_hook = \"push-to-checkout\";\n \n static const char *push_to_checkout(unsigned char *hash,\n+\t\t\t\t    int *invoked_hook,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\topt.invoked_hook = invoked_hook;\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n \tstrvec_pushv(&opt.env, env->v);\n@@ -1460,6 +1462,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n {\n \tconst char *retval, *work_tree, *git_dir = NULL;\n \tstruct strvec env = STRVEC_INIT;\n+\tint invoked_hook = 0;\n \n \tif (worktree && worktree->path)\n \t\twork_tree = worktree->path;\n@@ -1477,10 +1480,9 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!hook_exists(push_to_checkout_hook))\n+\tretval = push_to_checkout(sha1, &invoked_hook, &env, work_tree);\n+\tif (!invoked_hook)\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n-\telse\n-\t\tretval = push_to_checkout(sha1, &env, work_tree);\n \n \tstrvec_clear(&env);\n \treturn retval;\ndiff --git a/commit.c b/commit.c\nindex e8147a88fc6..cf62ebceae5 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1697,6 +1697,7 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n }\n \n int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook,\n \t\t    const char *name, ...)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\ndiff --git a/commit.h b/commit.h\nindex df42eb434f3..b5a542993c6 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -363,7 +363,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\n \n LAST_ARG_MUST_BE_NULL\n-int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);\n+int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook, const char *name, ...);\n \n /* Sign a commit or tag buffer, storing the result in a header. */\n int sign_with_header(struct strbuf *buf, const char *keyid);\ndiff --git a/hook.c b/hook.c\nindex 17ae65eca31..3cf51460279 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -138,6 +138,9 @@ static int notify_hook_finished(int result,\n \t/* |= rc in cb */\n \thook_cb->rc |= result;\n \n+\tif (hook_cb->invoked_hook)\n+\t\t*hook_cb->invoked_hook = 1;\n+\n \treturn 1;\n }\n \n@@ -152,6 +155,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t.rc = 0,\n \t\t.hook_name = hook_name,\n \t\t.options = options,\n+\t\t.invoked_hook = options->invoked_hook,\n \t};\n \tif (options->absolute_path) {\n \t\tstrbuf_add_absolute_path(&abs_path, hook_path);\ndiff --git a/hook.h b/hook.h\nindex 5f895032341..9d163e6f992 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -57,6 +57,15 @@ struct run_hooks_opt\n \t * for an example.\n \t */\n \tconsume_sideband_fn consume_sideband;\n+\n+\t/*\n+\t * A pointer which if provided will be set to 1 or 0 depending\n+\t * on if a hook was invoked (i.e. existed), regardless of\n+\t * whether or not that was successful. Used for avoiding\n+\t * TOCTOU races in code that would otherwise call hook_exist()\n+\t * after a \"maybe hook run\" to see if a hook was invoked.\n+\t */\n+\tint *invoked_hook;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -81,6 +90,7 @@ struct hook_cb_data {\n \tconst char *hook_name;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n+\tint *invoked_hook;\n };\n \n /*\ndiff --git a/sequencer.c b/sequencer.c\nindex ec2761e47d9..2440b9dccd8 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1204,7 +1204,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n \t} else {\n \t\targ1 = \"message\";\n \t}\n-\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n+\tif (run_commit_hook(0, r->index_file, NULL, \"prepare-commit-msg\", name,\n \t\t\t    arg1, arg2, NULL))\n \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n \n@@ -1534,7 +1534,7 @@ static int try_to_commit(struct repository *r,\n \t\tgoto out;\n \t}\n \n-\trun_commit_hook(0, r->index_file, \"post-commit\", NULL);\n+\trun_commit_hook(0, r->index_file, NULL, \"post-commit\", NULL);\n \tif (flags & AMEND_MSG)\n \t\tcommit_post_rewrite(r, current_head, oid);\n \n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427303","messageId":"patch-14.30-ec4ad437f86-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 14/30] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:03Z","receivedAt":"2021-06-14T10:38:43Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using hook.h instead of run-command.h to invoke push-to-checkout,\nhooks can now be specified in the config as well as in the hookdir.\npush-to-checkout is not called anywhere but in builtin/receive-pack.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 14 ++++++++++----\n 1 file changed, 10 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 1e0e04c62fc..5248228ebfe 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1436,12 +1436,18 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n-\tif (run_hook_le(env->v, push_to_checkout_hook,\n-\t\t\thash_to_hex(hash), NULL))\n+\tstrvec_pushv(&opt.env, env->v);\n+\tstrvec_push(&opt.args, hash_to_hex(hash));\n+\tif (run_hooks(push_to_checkout_hook, &opt)) {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn \"push-to-checkout hook declined\";\n-\telse\n+\t} else {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn NULL;\n+\t}\n }\n \n static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree)\n@@ -1465,7 +1471,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!find_hook(push_to_checkout_hook))\n+\tif (!hook_exists(push_to_checkout_hook))\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n \telse\n \t\tretval = push_to_checkout(sha1, &env, work_tree);\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427304","messageId":"patch-18.30-febf05ef232-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 18/30] am: convert 'post-rewrite' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:07Z","receivedAt":"2021-06-14T10:38:48Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 18 +++++-------------\n 1 file changed, 5 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 9e9c1b5e9f2..6e4f9c80360 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -467,23 +467,15 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tconst char *hook = find_hook(\"post-rewrite\");\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint ret;\n \n-\tif (!hook)\n-\t\treturn 0;\n-\n-\tstrvec_push(&cp.args, hook);\n-\tstrvec_push(&cp.args, \"rebase\");\n-\n-\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n-\tcp.stdout_to_stderr = 1;\n-\tcp.trace2_hook_name = \"post-rewrite\";\n+\tstrvec_push(&opt.args, \"rebase\");\n+\topt.path_to_stdin = am_path(state, \"rewritten\");\n \n-\tret = run_command(&cp);\n+\tret = run_hooks(\"post-rewrite\", &opt);\n \n-\tclose(cp.in);\n+\trun_hooks_opt_clear(&opt);\n \treturn ret;\n }\n \n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427305","messageId":"patch-23.30-6049b1cdc74-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 23/30] reference-transaction: use hook.h to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:12Z","receivedAt":"2021-06-14T10:38:51Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n refs.c | 42 +++++++++++++++---------------------------\n 1 file changed, 15 insertions(+), 27 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 59be29cf081..1149e7e7dcb 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2062,47 +2062,35 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n \tstruct strbuf buf = STRBUF_INIT;\n-\tconst char *hook;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint ret = 0, i;\n+\tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n \n-\thook = find_hook(\"reference-transaction\");\n-\tif (!hook)\n+\tif (!hook_exists(\"reference-transaction\"))\n \t\treturn ret;\n \n-\tstrvec_pushl(&proc.args, hook, state, NULL);\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"reference-transaction\";\n-\n-\tret = start_command(&proc);\n-\tif (ret)\n-\t\treturn ret;\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstrvec_push(&opt.args, state);\n \n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n+\t\toid_to_hex_r(o, &update->old_oid);\n+\t\toid_to_hex_r(n, &update->new_oid);\n \n \t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n-\t\t\t    oid_to_hex(&update->old_oid),\n-\t\t\t    oid_to_hex(&update->new_oid),\n-\t\t\t    update->refname);\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstrbuf_addf(&buf, \"%s %s %s\", o, n, update->refname);\n+\t\tstring_list_append(&to_stdin, buf.buf);\n \t}\n \n-\tclose(proc.in);\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tret = run_hooks(\"reference-transaction\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \tstrbuf_release(&buf);\n+\tstring_list_clear(&to_stdin, 0);\n \n-\tret |= finish_command(&proc);\n \treturn ret;\n }\n \n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427306","messageId":"patch-20.30-2edf9dea41a-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 20/30] hook: provide stdin by string_list or callback","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:09Z","receivedAt":"2021-06-14T10:38:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn cases where a hook requires only a small amount of information via\nstdin, it should be simple for users to provide a string_list alone. But\nin more complicated cases where the stdin is too large to hold in\nmemory, let's instead provide a callback the users can populate line\nafter line.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 33 +++++++++++++++++++++++++++++++--\n hook.h | 27 +++++++++++++++++++++++++++\n 2 files changed, 58 insertions(+), 2 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 5f6335bac3f..ac5e3988fec 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -47,6 +47,29 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)\n \tstrvec_clear(&o->args);\n }\n \n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n+{\n+\tint *item_idx;\n+\tstruct hook *ctx = pp_task_cb;\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct string_list *to_pipe = hook_cb->options->feed_pipe_ctx;\n+\n+\t/* Bootstrap the state manager if necessary. */\n+\tif (!ctx->feed_pipe_cb_data) {\n+\t\tctx->feed_pipe_cb_data = xmalloc(sizeof(unsigned int));\n+\t\t*(int*)ctx->feed_pipe_cb_data = 0;\n+\t}\n+\n+\titem_idx = ctx->feed_pipe_cb_data;\n+\n+\tif (*item_idx < to_pipe->nr) {\n+\t\tstrbuf_addf(pipe, \"%s\\n\", to_pipe->items[*item_idx].string);\n+\t\t(*item_idx)++;\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n static int pick_next_hook(struct child_process *cp,\n \t\t\t  struct strbuf *out,\n \t\t\t  void *pp_cb,\n@@ -62,6 +85,10 @@ static int pick_next_hook(struct child_process *cp,\n \tif (hook_cb->options->path_to_stdin) {\n \t\tcp->no_stdin = 0;\n \t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else if (hook_cb->options->feed_pipe) {\n+\t\t/* ask for start_command() to make a pipe for us */\n+\t\tcp->in = -1;\n+\t\tcp->no_stdin = 0;\n \t} else {\n \t\tcp->no_stdin = 1;\n \t}\n@@ -114,7 +141,6 @@ static int notify_hook_finished(int result,\n \treturn 1;\n }\n \n-\n int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t    struct run_hooks_opt *options)\n {\n@@ -139,7 +165,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->feed_pipe,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n@@ -157,6 +183,9 @@ int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n+\tif (options->path_to_stdin && options->feed_pipe)\n+\t\tBUG(\"choose only one method to populate stdin\");\n+\n \thook_path = find_hook(hook_name);\n \n \t/* Care about nonexistence? Use run_found_hooks() */\ndiff --git a/hook.h b/hook.h\nindex 74a8c76a94c..ff1697d1087 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -7,6 +7,12 @@\n struct hook {\n \t/* The path to the hook */\n \tconst char *hook_path;\n+\n+\t/*\n+\t * Use this to keep state for your feed_pipe_fn if you are using\n+\t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n+\t */\n+\tvoid *feed_pipe_cb_data;\n };\n \n struct run_hooks_opt\n@@ -30,6 +36,19 @@ struct run_hooks_opt\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n+\n+\t/*\n+\t * Callback and state pointer to ask for more content to pipe to stdin.\n+\t * Will be called repeatedly, for each hook. See\n+\t * hook.c:pipe_from_stdin() for an example. Keep per-hook state in\n+\t * hook.feed_pipe_cb_data (per process). Keep initialization context in\n+\t * feed_pipe_ctx (shared by all processes).\n+\t *\n+\t * See 'pipe_from_string_list()' for info about how to specify a\n+\t * string_list as the stdin input instead of writing your own handler.\n+\t */\n+\tfeed_pipe_fn feed_pipe;\n+\tvoid *feed_pipe_ctx;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -38,6 +57,14 @@ struct run_hooks_opt\n \t.args = STRVEC_INIT, \\\n }\n \n+/*\n+ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n+ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n+ * This will pipe each string in the list to stdin, separated by newlines.  (Do\n+ * not inject your own newlines.)\n+ */\n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n+\n /*\n  * Callback provided to feed_pipe_fn and consume_sideband_fn.\n  */\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427307","messageId":"patch-22.30-62eecafb3ff-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 22/30] transport: convert pre-push hook to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:11Z","receivedAt":"2021-06-14T10:39:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using the hook.h:run_hooks API, pre-push hooks can be specified in\nthe config as well as in the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n transport.c | 57 ++++++++++++++---------------------------------------\n 1 file changed, 15 insertions(+), 42 deletions(-)\n\ndiff --git a/transport.c b/transport.c\nindex 2ed270171f0..9969ed2cdde 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1199,31 +1199,14 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)\n static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n-\tint ret = 0, x;\n+\tint ret = 0;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct strbuf tmp = STRBUF_INIT;\n \tstruct ref *r;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct strbuf buf;\n-\tconst char *argv[4];\n-\n-\tif (!(argv[0] = find_hook(\"pre-push\")))\n-\t\treturn 0;\n-\n-\targv[1] = transport->remote->name;\n-\targv[2] = transport->url;\n-\targv[3] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.trace2_hook_name = \"pre-push\";\n-\n-\tif (start_command(&proc)) {\n-\t\tfinish_command(&proc);\n-\t\treturn -1;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \n-\tstrbuf_init(&buf, 256);\n+\tstrvec_push(&opt.args, transport->remote->name);\n+\tstrvec_push(&opt.args, transport->url);\n \n \tfor (r = remote_refs; r; r = r->next) {\n \t\tif (!r->peer_ref) continue;\n@@ -1232,30 +1215,20 @@ static int run_pre_push_hook(struct transport *transport,\n \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n \n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n+\t\tstrbuf_reset(&tmp);\n+\t\tstrbuf_addf(&tmp, \"%s %s %s %s\",\n \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n \t\t\t r->name, oid_to_hex(&r->old_oid));\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\t/* We do not mind if a hook does not read all refs. */\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstring_list_append(&to_stdin, tmp.buf);\n \t}\n \n-\tstrbuf_release(&buf);\n-\n-\tx = close(proc.in);\n-\tif (!ret)\n-\t\tret = x;\n-\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n \n-\tx = finish_command(&proc);\n-\tif (!ret)\n-\t\tret = x;\n+\tret = run_hooks(\"pre-push\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n \n \treturn ret;\n }\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427308","messageId":"patch-19.30-7baf2469d50-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 19/30] run-command: add stdin callback for parallelization","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:08Z","receivedAt":"2021-06-14T10:39:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIf a user of the run_processes_parallel() API wants to pipe a large\namount of information to stdin of each parallel command, that\ninformation could exceed the buffer of the pipe allocated for that\nprocess's stdin.  Generally this is solved by repeatedly writing to\nchild_process.in between calls to start_command() and finish_command();\nrun_processes_parallel() did not provide users an opportunity to access\nchild_process at that time.\n\nBecause the data might be extremely large (for example, a list of all\nrefs received during a push from a client) simply taking a string_list\nor strbuf is not as scalable as using a callback; the rest of the\nrun_processes_parallel() API also uses callbacks, so making this feature\nmatch the rest of the API reduces mental load on the user.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  1 +\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 54 +++++++++++++++++++++++++++++++++++--\n run-command.h               | 17 +++++++++++-\n submodule.c                 |  1 +\n t/helper/test-run-command.c | 31 ++++++++++++++++++---\n t/t0061-run-command.sh      | 30 +++++++++++++++++++++\n 8 files changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 9191620e50c..3d8f04b392d 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,6 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n+\t\t\t\t\t\t    NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex ae6174ab05b..818494dd18e 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2295,7 +2295,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure,\n+\t\t\t\t   update_clone_start_failure, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex daf39f61741..5f6335bac3f 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -139,6 +139,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 0bf771845e4..3392640f17b 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1493,6 +1493,7 @@ struct parallel_processes {\n \n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n+\tfeed_pipe_fn feed_pipe;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1520,6 +1521,13 @@ static int default_start_failure(struct strbuf *out,\n \treturn 0;\n }\n \n+static int default_feed_pipe(struct strbuf *pipe,\n+\t\t\t     void *pp_cb,\n+\t\t\t     void *pp_task_cb)\n+{\n+\treturn 1;\n+}\n+\n static int default_task_finished(int result,\n \t\t\t\t struct strbuf *out,\n \t\t\t\t void *pp_cb,\n@@ -1550,6 +1558,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    int n,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n+\t\t    feed_pipe_fn feed_pipe,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1568,6 +1577,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->get_next_task = get_next_task;\n \n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n+\tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n \n \tpp->nr_processes = 0;\n@@ -1665,6 +1675,37 @@ static int pp_start_one(struct parallel_processes *pp)\n \treturn 0;\n }\n \n+static void pp_buffer_stdin(struct parallel_processes *pp)\n+{\n+\tint i;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t/* Buffer stdin for each pipe. */\n+\tfor (i = 0; i < pp->max_processes; i++) {\n+\t\tif (pp->children[i].state == GIT_CP_WORKING &&\n+\t\t    pp->children[i].process.in > 0) {\n+\t\t\tint done;\n+\t\t\tstrbuf_reset(&sb);\n+\t\t\tdone = pp->feed_pipe(&sb, pp->data,\n+\t\t\t\t\t      pp->children[i].data);\n+\t\t\tif (sb.len) {\n+\t\t\t\tif (write_in_full(pp->children[i].process.in,\n+\t\t\t\t\t      sb.buf, sb.len) < 0) {\n+\t\t\t\t\tif (errno != EPIPE)\n+\t\t\t\t\t\tdie_errno(\"write\");\n+\t\t\t\t\tdone = 1;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif (done) {\n+\t\t\t\tclose(pp->children[i].process.in);\n+\t\t\t\tpp->children[i].process.in = 0;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&sb);\n+}\n+\n static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n {\n \tint i;\n@@ -1729,6 +1770,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\tpp->nr_processes--;\n \t\tpp->children[i].state = GIT_CP_FREE;\n \t\tpp->pfd[i].fd = -1;\n+\t\tpp->children[i].process.in = 0;\n \t\tchild_process_init(&pp->children[i].process);\n \n \t\tif (i != pp->output_owner) {\n@@ -1762,6 +1804,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n+\t\t\t   feed_pipe_fn feed_pipe,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1770,7 +1813,9 @@ int run_processes_parallel(int n,\n \tint spawn_cap = 4;\n \tstruct parallel_processes pp;\n \n-\tpp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb);\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1787,6 +1832,7 @@ int run_processes_parallel(int n,\n \t\t}\n \t\tif (!pp.nr_processes)\n \t\t\tbreak;\n+\t\tpp_buffer_stdin(&pp);\n \t\tpp_buffer_stderr(&pp, output_timeout);\n \t\tpp_output(&pp);\n \t\tcode = pp_collect_finished(&pp);\n@@ -1798,11 +1844,15 @@ int run_processes_parallel(int n,\n \t}\n \n \tpp_cleanup(&pp);\n+\n+\tsigchain_pop(SIGPIPE);\n+\n \treturn 0;\n }\n \n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n+\t\t\t       feed_pipe_fn feed_pipe,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1812,7 +1862,7 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\ttask_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 748d4fc2a72..41e36d26cb1 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -419,6 +419,20 @@ typedef int (*start_failure_fn)(struct strbuf *out,\n \t\t\t\tvoid *pp_cb,\n \t\t\t\tvoid *pp_task_cb);\n \n+/**\n+ * This callback is called repeatedly on every child process who requests\n+ * start_command() to create a pipe by setting child_process.in < 0.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel, and\n+ * pp_task_cb is the callback cookie as passed into get_next_task_fn.\n+ * The contents of 'send' will be read into the pipe and passed to the pipe.\n+ *\n+ * Return nonzero to close the pipe.\n+ */\n+typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n+\t\t\t    void *pp_cb,\n+\t\t\t    void *pp_task_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -453,10 +467,11 @@ typedef int (*task_finished_fn)(int result,\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n+\t\t\t   feed_pipe_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex 0b1d9c1dde5..ea026a8195f 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1645,6 +1645,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 7ae03dc7123..9348184d303 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,\n \t\treturn 0;\n \n \tstrvec_pushv(&cp->args, d->argv);\n+\tcp->in = d->in;\n+\tcp->no_stdin = d->no_stdin;\n \tstrbuf_addstr(err, \"preloaded output of a child\\n\");\n \tnumber_callbacks++;\n+\n+\t*task_cb = xmalloc(sizeof(int));\n+\t*(int*)(*task_cb) = 2;\n \treturn 1;\n }\n \n@@ -55,6 +60,17 @@ static int task_finished(int result,\n \treturn 1;\n }\n \n+static int test_stdin(struct strbuf *pipe, void *cb, void *task_cb)\n+{\n+\tint *lines_remaining = task_cb;\n+\n+\tif (*lines_remaining)\n+\t\tstrbuf_addf(pipe, \"sample stdin %d\\n\", --(*lines_remaining));\n+\n+\treturn !(*lines_remaining);\n+}\n+\n+\n struct testsuite {\n \tstruct string_list tests, failed;\n \tint next;\n@@ -185,7 +201,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_finished, &suite);\n+\t\t\t\t     test_stdin, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -413,15 +429,22 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\n+\tif (!strcmp(argv[1], \"run-command-stdin\")) {\n+\t\tproc.in = -1;\n+\t\tproc.no_stdin = 0;\n+\t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n+\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t}\n \n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 7d599675e35..87759482ad1 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,36 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+cat >expect <<-EOF\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+EOF\n+\n+test_expect_success 'run_command listens to stdin' '\n+\twrite_script stdin-script <<-\\EOF &&\n+\techo \"listening for stdin:\"\n+\twhile read line; do\n+\t\techo \"$line\"\n+\tdone\n+\tEOF\n+\ttest-tool run-command run-command-stdin 2 ./stdin-script 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n asking for a quick stop\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427309","messageId":"patch-21.30-303b31ee620-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 21/30] hook: convert 'post-rewrite' hook in sequencer.c to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:10Z","receivedAt":"2021-06-14T10:39:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using 'hook.h' for 'post-rewrite', we simplify hook invocations by\nnot needing to put together our own 'struct child_process'.\n\nThe signal handling that's being removed by this commit now takes\nplace in run-command.h:run_processes_parallel(), so it is OK to remove\nthem here.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n sequencer.c | 81 ++++++++++++++++++++++-------------------------------\n 1 file changed, 34 insertions(+), 47 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 8f46984ffb7..ec2761e47d9 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -35,6 +35,7 @@\n #include \"commit-reach.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"string-list.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -1147,33 +1148,28 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *argv[3];\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n-\tstruct strbuf sb = STRBUF_INIT;\n \n-\targv[0] = find_hook(\"post-rewrite\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tstrvec_push(&opt.args, \"amend\");\n \n-\targv[1] = \"amend\";\n-\targv[2] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"post-rewrite\";\n-\n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n-\tstrbuf_addf(&sb, \"%s %s\\n\", oid_to_hex(oldoid), oid_to_hex(newoid));\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\twrite_in_full(proc.in, sb.buf, sb.len);\n-\tclose(proc.in);\n-\tstrbuf_release(&sb);\n-\tsigchain_pop(SIGPIPE);\n-\treturn finish_command(&proc);\n+\tstrbuf_addf(&tmp,\n+\t\t    \"%s %s\",\n+\t\t    oid_to_hex(oldoid),\n+\t\t    oid_to_hex(newoid));\n+\tstring_list_append(&to_stdin, tmp.buf);\n+\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tcode = run_hooks(\"post-rewrite\", &opt);\n+\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n+\treturn code;\n }\n \n void commit_post_rewrite(struct repository *r,\n@@ -4527,30 +4523,21 @@ static int pick_commits(struct repository *r,\n \t\tflush_rewritten_pending();\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n-\t\t\tstruct child_process child = CHILD_PROCESS_INIT;\n-\t\t\tconst char *post_rewrite_hook =\n-\t\t\t\tfind_hook(\"post-rewrite\");\n-\n-\t\t\tchild.in = open(rebase_path_rewritten_list(), O_RDONLY);\n-\t\t\tchild.git_cmd = 1;\n-\t\t\tstrvec_push(&child.args, \"notes\");\n-\t\t\tstrvec_push(&child.args, \"copy\");\n-\t\t\tstrvec_push(&child.args, \"--for-rewrite=rebase\");\n+\t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n+\t\t\tnotes_cp.git_cmd = 1;\n+\t\t\tstrvec_push(&notes_cp.args, \"notes\");\n+\t\t\tstrvec_push(&notes_cp.args, \"copy\");\n+\t\t\tstrvec_push(&notes_cp.args, \"--for-rewrite=rebase\");\n \t\t\t/* we don't care if this copying failed */\n-\t\t\trun_command(&child);\n-\n-\t\t\tif (post_rewrite_hook) {\n-\t\t\t\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\n-\t\t\t\thook.in = open(rebase_path_rewritten_list(),\n-\t\t\t\t\tO_RDONLY);\n-\t\t\t\thook.stdout_to_stderr = 1;\n-\t\t\t\thook.trace2_hook_name = \"post-rewrite\";\n-\t\t\t\tstrvec_push(&hook.args, post_rewrite_hook);\n-\t\t\t\tstrvec_push(&hook.args, \"rebase\");\n-\t\t\t\t/* we don't care if this hook failed */\n-\t\t\t\trun_command(&hook);\n-\t\t\t}\n+\t\t\trun_command(&notes_cp);\n+\n+\t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n+\t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n+\t\t\trun_hooks(\"post-rewrite\", &hook_opt);\n+\t\t\trun_hooks_opt_clear(&hook_opt);\n \t\t}\n \t\tapply_autostash(rebase_path_autostash());\n \n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427310","messageId":"patch-30.30-bc086454d68-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 30/30] hook-list.h: add a generated list of hooks, like config-list.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:19Z","receivedAt":"2021-06-14T10:39:05Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Resolve a long-standing TODO item in bugreport.c of there being no\ncentralized listing of hooks, this fixes a bug with the bugreport\nlisting only knowing about 1/4 of the p4 hooks. It didn't know about\nthe \"reference-transaction\" hook either.\n\nWe can now make sure this is kept up-to-date, as the hook.c library\nwill die if asked to find a hook we don't know about yet. The only\n(undocumented) exception is the artificial \"test-hook\" used in our own\ntest suite. Move some of the tests away from the \"does-not-exist\"\npseudo-hook, and test for the new behavior.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n .gitignore           |  1 +\n Makefile             | 14 +++++++++++---\n builtin/bugreport.c  | 44 ++++++++------------------------------------\n generate-hooklist.sh | 24 ++++++++++++++++++++++++\n hook.c               | 22 ++++++++++++++++++++++\n t/t1800-hook.sh      | 14 +++++++++++---\n 6 files changed, 77 insertions(+), 42 deletions(-)\n create mode 100755 generate-hooklist.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex de39dc9961b..66189ca3cdc 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -191,6 +191,7 @@\n /gitweb/static/gitweb.min.*\n /config-list.h\n /command-list.h\n+/hook-list.h\n *.tar.gz\n *.dsc\n *.deb\ndiff --git a/Makefile b/Makefile\nindex a6b71a0fbed..d0532f3c744 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -817,6 +817,7 @@ XDIFF_LIB = xdiff/lib.a\n \n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n+GENERATED_H += hook-list.h\n \n LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentation/' 2>/dev/null || \\\n \t$(FIND) . \\\n@@ -2207,7 +2208,9 @@ git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS) $(GITLIBS)\n \n help.sp help.s help.o: command-list.h\n \n-builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX\n+hook.sp hook.s hook.o: hook-list.h\n+\n+builtin/help.sp builtin/help.s builtin/help.o: config-list.h hook-list.h GIT-PREFIX\n builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \\\n \t'-DGIT_HTML_PATH=\"$(htmldir_relative_SQ)\"' \\\n \t'-DGIT_MAN_PATH=\"$(mandir_relative_SQ)\"' \\\n@@ -2240,6 +2243,11 @@ command-list.h: $(wildcard Documentation/git*.txt)\n \t\t$(patsubst %,--exclude-program %,$(EXCLUDED_PROGRAMS)) \\\n \t\tcommand-list.txt >$@+ && mv $@+ $@\n \n+hook-list.h: generate-hooklist.sh\n+hook-list.h: Documentation/githooks.txt\n+\t$(QUIET_GEN)$(SHELL_PATH) ./generate-hooklist.sh \\\n+\t\t>$@+ && mv $@+ $@\n+\n SCRIPT_DEFINES = $(SHELL_PATH_SQ):$(DIFF_SQ):$(GIT_VERSION):\\\n \t$(localedir_SQ):$(NO_CURL):$(USE_GETTEXT_SCHEME):$(SANE_TOOL_PATH_SQ):\\\n \t$(gitwebdir_SQ):$(PERL_PATH_SQ):$(SANE_TEXT_GREP):$(PAGER_ENV):\\\n@@ -2890,7 +2898,7 @@ $(SP_OBJ): %.sp: %.c GIT-CFLAGS FORCE\n .PHONY: sparse $(SP_OBJ)\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := command-list.h config-list.h unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := command-list.h config-list.h hook-list.h unicode-width.h compat/% xdiff/%\n ifndef GCRYPT_SHA256\n \tEXCEPT_HDRS += sha256/gcrypt.h\n endif\n@@ -2912,7 +2920,7 @@ hdr-check: $(HCO)\n style:\n \tgit clang-format --style file --diff --extensions c,h\n \n-check: config-list.h command-list.h\n+check: config-list.h command-list.h hook-list.h\n \t@if sparse; \\\n \tthen \\\n \t\techo >&2 \"Use 'make sparse' instead\"; \\\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 941c8d5e270..a7a1fcb8a7a 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -4,6 +4,7 @@\n #include \"help.h\"\n #include \"compat/compiler.h\"\n #include \"hook.h\"\n+#include \"hook-list.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\n@@ -41,39 +42,7 @@ static void get_system_info(struct strbuf *sys_info)\n \n static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n {\n-\t/*\n-\t * NEEDSWORK: Doesn't look like there is a list of all possible hooks;\n-\t * so below is a transcription of `git help hooks`. Later, this should\n-\t * be replaced with some programmatically generated list (generated from\n-\t * doc or else taken from some library which tells us about all the\n-\t * hooks)\n-\t */\n-\tstatic const char *hook[] = {\n-\t\t\"applypatch-msg\",\n-\t\t\"pre-applypatch\",\n-\t\t\"post-applypatch\",\n-\t\t\"pre-commit\",\n-\t\t\"pre-merge-commit\",\n-\t\t\"prepare-commit-msg\",\n-\t\t\"commit-msg\",\n-\t\t\"post-commit\",\n-\t\t\"pre-rebase\",\n-\t\t\"post-checkout\",\n-\t\t\"post-merge\",\n-\t\t\"pre-push\",\n-\t\t\"pre-receive\",\n-\t\t\"update\",\n-\t\t\"post-receive\",\n-\t\t\"post-update\",\n-\t\t\"push-to-checkout\",\n-\t\t\"pre-auto-gc\",\n-\t\t\"post-rewrite\",\n-\t\t\"sendemail-validate\",\n-\t\t\"fsmonitor-watchman\",\n-\t\t\"p4-pre-submit\",\n-\t\t\"post-index-change\",\n-\t};\n-\tint i;\n+\tconst char **p;\n \n \tif (nongit) {\n \t\tstrbuf_addstr(hook_info,\n@@ -81,9 +50,12 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t\treturn;\n \t}\n \n-\tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (hook_exists(hook[i]))\n-\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n+\tfor (p = hook_name_list; *p; p++) {\n+\t\tconst char *hook = *p;\n+\n+\t\tif (hook_exists(hook))\n+\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook);\n+\t}\n }\n \n static const char * const bugreport_usage[] = {\ndiff --git a/generate-hooklist.sh b/generate-hooklist.sh\nnew file mode 100755\nindex 00000000000..5a3f7f849c8\n--- /dev/null\n+++ b/generate-hooklist.sh\n@@ -0,0 +1,24 @@\n+#!/bin/sh\n+\n+echo \"/* Automatically generated by generate-hooklist.sh */\"\n+\n+print_hook_list () {\n+\tcat <<EOF\n+static const char *hook_name_list[] = {\n+EOF\n+\tperl -ne '\n+\t\tchomp;\n+\t\t@l[$.] = $_;\n+\t\tpush @h => $l[$. - 1] if /^~~~+$/s;\n+\t\tEND {\n+\t\t\tprint qq[\\t\"$_\",\\n] for sort @h;\n+\t\t}\n+\t' <Documentation/githooks.txt\n+\tcat <<EOF\n+\tNULL,\n+};\n+EOF\n+}\n+\n+echo\n+print_hook_list\ndiff --git a/hook.c b/hook.c\nindex 3cf51460279..68e8c1ba868 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -1,11 +1,33 @@\n #include \"cache.h\"\n #include \"hook.h\"\n #include \"run-command.h\"\n+#include \"hook-list.h\"\n+\n+static int known_hook(const char *name)\n+{\n+\tconst char **p;\n+\tsize_t len = strlen(name);\n+\tfor (p = hook_name_list; *p; p++) {\n+\t\tconst char *hook = *p;\n+\n+\t\tif (!strncmp(name, hook, len) && hook[len] == '\\0')\n+\t\t\treturn 1;\n+\t}\n+\tif (!strcmp(name, \"test-hook\") ||\n+\t    !strcmp(name, \"does-not-exist\"))\n+\t\treturn 1;\n+\n+\treturn 0;\n+}\n \n const char *find_hook(const char *name)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \n+\tif (!known_hook(name))\n+\t\tdie(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n+\t\t    name);\n+\n \tstrbuf_reset(&path);\n \tstrbuf_git_path(&path, \"hooks/%s\", name);\n \tif (access(path.buf, X_OK) < 0) {\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 1a6b708137e..a4b089eb7fe 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -6,17 +6,25 @@ test_description='git-hook command'\n \n test_expect_success 'git hook run -- nonexistent hook' '\n \tcat >stderr.expect <<-\\EOF &&\n-\terror: cannot find a hook named does-not-exist\n+\terror: cannot find a hook named test-hook\n \tEOF\n-\ttest_expect_code 1 git hook run does-not-exist 2>stderr.actual &&\n+\ttest_expect_code 1 git hook run test-hook 2>stderr.actual &&\n \ttest_cmp stderr.expect stderr.actual\n '\n \n test_expect_success 'git hook run -- nonexistent hook with --ignore-missing' '\n-\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n+\tgit hook run --ignore-missing test-hook 2>stderr.actual &&\n \ttest_must_be_empty stderr.actual\n '\n \n+test_expect_success 'git hook run -- nonexistent hook with --ignore-missing' '\n+\tcat >stderr.expect <<-\\EOF &&\n+\tfatal: the hook '\"'\"'unknown-hook'\"'\"' is not known to git, should be in hook-list.h via githooks(5)\n+\tEOF\n+\ttest_expect_code 128 git hook run unknown-hook 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual\n+'\n+\n test_expect_success 'git hook run -- basic' '\n \twrite_script .git/hooks/test-hook <<-EOF &&\n \techo Test hook\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427311","messageId":"patch-27.30-ecaedd13b89-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 27/30] post-update: use hook.h library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:16Z","receivedAt":"2021-06-14T10:39:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 26 +++++++-------------------\n 1 file changed, 7 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 378f8f6b5d1..b2ccdb66daa 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1657,33 +1657,21 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *hook;\n-\n-\thook = find_hook(\"post-update\");\n-\tif (!hook)\n-\t\treturn;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\n \t\t\tcontinue;\n-\t\tif (!proc.args.nr)\n-\t\t\tstrvec_push(&proc.args, hook);\n-\t\tstrvec_push(&proc.args, cmd->ref_name);\n+\t\tstrvec_push(&opt.args, cmd->ref_name);\n \t}\n-\tif (!proc.args.nr)\n+\tif (!opt.args.nr)\n \t\treturn;\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.trace2_hook_name = \"post-update\";\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n \n-\tif (!start_command(&proc)) {\n-\t\tif (use_sideband)\n-\t\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\t\tfinish_command(&proc);\n-\t}\n+\trun_hooks(\"post-update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n }\n \n static void check_aliased_update_internal(struct command *cmd,\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427312","messageId":"patch-25.30-251085b7525-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 25/30] hooks: allow callers to capture output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:14Z","receivedAt":"2021-06-14T10:39:11Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome server-side hooks will require capturing output to send over\nsideband instead of printing directly to stderr. Expose that capability.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 2 +-\n hook.h | 8 ++++++++\n 2 files changed, 9 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 0faa24ec825..17ae65eca31 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -166,7 +166,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->consume_sideband,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/hook.h b/hook.h\nindex ff1697d1087..5f895032341 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -49,6 +49,14 @@ struct run_hooks_opt\n \t */\n \tfeed_pipe_fn feed_pipe;\n \tvoid *feed_pipe_ctx;\n+\n+\t/*\n+\t * Populate this to capture output and prevent it from being printed to\n+\t * stderr. This will be passed directly through to\n+\t * run_command:run_parallel_processes(). See t/helper/test-run-command.c\n+\t * for an example.\n+\t */\n+\tconsume_sideband_fn consume_sideband;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427313","messageId":"patch-24.30-26ebbe4c545-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 24/30] run-command: allow capturing of collated output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:13Z","receivedAt":"2021-06-14T10:39:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome callers, for example server-side hooks which wish to relay hook\noutput to clients across a transport, want to capture what would\nnormally print to stderr and do something else with it. Allow that via a\ncallback.\n\nBy calling the callback regardless of whether there's output available,\nwe allow clients to send e.g. a keepalive if necessary.\n\nBecause we expose a strbuf, not a fd or FILE*, there's no need to create\na temporary pipe or similar - we can just skip the print to stderr and\ninstead hand it to the caller.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 33 +++++++++++++++++++++++++--------\n run-command.h               | 18 +++++++++++++++++-\n submodule.c                 |  2 +-\n t/helper/test-run-command.c | 25 ++++++++++++++++++++-----\n t/t0061-run-command.sh      |  7 +++++++\n 8 files changed, 73 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 3d8f04b392d..0595b88b7cc 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,7 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n-\t\t\t\t\t\t    NULL,\n+\t\t\t\t\t\t    NULL, NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 818494dd18e..69782f62044 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2295,7 +2295,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure, NULL,\n+\t\t\t\t   update_clone_start_failure, NULL, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex ac5e3988fec..0faa24ec825 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -166,6 +166,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 3392640f17b..4a1a7a10820 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1494,6 +1494,7 @@ struct parallel_processes {\n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n \tfeed_pipe_fn feed_pipe;\n+\tconsume_sideband_fn consume_sideband;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1559,6 +1560,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n \t\t    feed_pipe_fn feed_pipe,\n+\t\t    consume_sideband_fn consume_sideband,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1579,6 +1581,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n \tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n+\tpp->consume_sideband = consume_sideband;\n \n \tpp->nr_processes = 0;\n \tpp->output_owner = 0;\n@@ -1615,7 +1618,10 @@ static void pp_cleanup(struct parallel_processes *pp)\n \t * When get_next_task added messages to the buffer in its last\n \t * iteration, the buffered output is non empty.\n \t */\n-\tstrbuf_write(&pp->buffered_output, stderr);\n+\tif (pp->consume_sideband)\n+\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\telse\n+\t\tstrbuf_write(&pp->buffered_output, stderr);\n \tstrbuf_release(&pp->buffered_output);\n \n \tsigchain_pop_common();\n@@ -1736,9 +1742,13 @@ static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n static void pp_output(struct parallel_processes *pp)\n {\n \tint i = pp->output_owner;\n+\n \tif (pp->children[i].state == GIT_CP_WORKING &&\n \t    pp->children[i].err.len) {\n-\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\tif (pp->consume_sideband)\n+\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\telse\n+\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n \t\tstrbuf_reset(&pp->children[i].err);\n \t}\n }\n@@ -1777,11 +1787,15 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\t\tstrbuf_addbuf(&pp->buffered_output, &pp->children[i].err);\n \t\t\tstrbuf_reset(&pp->children[i].err);\n \t\t} else {\n-\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t/* Output errors, then all other finished child processes */\n+\t\t\tif (pp->consume_sideband) {\n+\t\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\t\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\t\t\t} else {\n+\t\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n+\t\t\t}\n \t\t\tstrbuf_reset(&pp->children[i].err);\n-\n-\t\t\t/* Output all other finished child processes */\n-\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n \t\t\tstrbuf_reset(&pp->buffered_output);\n \n \t\t\t/*\n@@ -1805,6 +1819,7 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n \t\t\t   feed_pipe_fn feed_pipe,\n+\t\t\t   consume_sideband_fn consume_sideband,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1815,7 +1830,7 @@ int run_processes_parallel(int n,\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n \n-\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, consume_sideband, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1853,6 +1868,7 @@ int run_processes_parallel(int n,\n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n \t\t\t       feed_pipe_fn feed_pipe,\n+\t\t\t       consume_sideband_fn consume_sideband,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1862,7 +1878,8 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, consume_sideband,\n+\t\t\t\t\ttask_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 41e36d26cb1..7150da851a8 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -433,6 +433,20 @@ typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n \t\t\t    void *pp_cb,\n \t\t\t    void *pp_task_cb);\n \n+/**\n+ * If this callback is provided, instead of collating process output to stderr,\n+ * they will be collated into a new pipe. consume_sideband_fn will be called\n+ * repeatedly. When output is available on that pipe, it will be contained in\n+ * 'output'. But it will be called with an empty 'output' too, to allow for\n+ * keepalives or similar operations if necessary.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel.\n+ *\n+ * Since this callback is provided with the collated output, no task cookie is\n+ * provided.\n+ */\n+typedef void (*consume_sideband_fn)(struct strbuf *output, void *pp_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -468,10 +482,12 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n \t\t\t   feed_pipe_fn,\n+\t\t\t   consume_sideband_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, consume_sideband_fn,\n+\t\t\t       task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex ea026a8195f..7fe0c8f7c9f 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1645,7 +1645,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   NULL, NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 9348184d303..d53db6d11c4 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -51,6 +51,16 @@ static int no_job(struct child_process *cp,\n \treturn 0;\n }\n \n+static void test_consume_sideband(struct strbuf *output, void *cb)\n+{\n+\tFILE *sideband;\n+\n+\tsideband = fopen(\"./sideband\", \"a\");\n+\n+\tstrbuf_write(output, sideband);\n+\tfclose(sideband);\n+}\n+\n static int task_finished(int result,\n \t\t\t struct strbuf *err,\n \t\t\t void *pp_cb,\n@@ -201,7 +211,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_stdin, test_finished, &suite);\n+\t\t\t\t     test_stdin, NULL, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -429,23 +439,28 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-stdin\")) {\n \t\tproc.in = -1;\n \t\tproc.no_stdin = 0;\n \t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n-\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t\t\t\t\t     test_stdin, NULL, NULL, &proc));\n \t}\n \n+\tif (!strcmp(argv[1], \"run-command-sideband\"))\n+\t\texit(run_processes_parallel(jobs, parallel_next, NULL, NULL,\n+\t\t\t\t\t    test_consume_sideband, NULL,\n+\t\t\t\t\t    &proc));\n+\n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\n }\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 87759482ad1..e99f6c7f445 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,13 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'run_command can divert output' '\n+\ttest_when_finished rm sideband &&\n+\ttest-tool run-command run-command-sideband 3 sh -c \"printf \\\"%s\\n%s\\n\\\" Hello World\" 2>actual &&\n+\ttest_must_be_empty actual &&\n+\ttest_cmp expect sideband\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n listening for stdin:\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427314","messageId":"patch-28.30-bb9d57f809a-20210614T101920Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH v2 28/30] receive-pack: convert receive hooks to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-14T10:33:17Z","receivedAt":"2021-06-14T10:39:25Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 197 +++++++++++++++++++----------------------\n 1 file changed, 90 insertions(+), 107 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex b2ccdb66daa..ec90e10477a 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -748,7 +748,7 @@ static int check_cert_push_options(const struct string_list *push_options)\n \treturn retval;\n }\n \n-static void prepare_push_cert_sha1(struct child_process *proc)\n+static void prepare_push_cert_sha1(struct run_hooks_opt *opt)\n {\n \tstatic int already_done;\n \n@@ -772,110 +772,42 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n \t}\n \tif (!is_null_oid(&push_cert_oid)) {\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT=%s\",\n \t\t\t     oid_to_hex(&push_cert_oid));\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_SIGNER=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_SIGNER=%s\",\n \t\t\t     sigcheck.signer ? sigcheck.signer : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_KEY=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_KEY=%s\",\n \t\t\t     sigcheck.key ? sigcheck.key : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_STATUS=%c\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_STATUS=%c\",\n \t\t\t     sigcheck.result);\n \t\tif (push_cert_nonce) {\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE=%s\",\n \t\t\t\t     push_cert_nonce);\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE_STATUS=%s\",\n \t\t\t\t     nonce_status);\n \t\t\tif (nonce_status == NONCE_SLOP)\n-\t\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t\t     \"GIT_PUSH_CERT_NONCE_SLOP=%ld\",\n \t\t\t\t\t     nonce_stamp_slop);\n \t\t}\n \t}\n }\n \n+struct receive_hook_feed_context {\n+\tstruct command *cmd;\n+\tint skip_broken;\n+};\n+\n struct receive_hook_feed_state {\n \tstruct command *cmd;\n \tstruct ref_push_report *report;\n \tint skip_broken;\n \tstruct strbuf buf;\n-\tconst struct string_list *push_options;\n };\n \n-typedef int (*feed_fn)(void *, const char **, size_t *);\n-static int run_and_feed_hook(const char *hook_name, feed_fn feed,\n-\t\t\t     struct receive_hook_feed_state *feed_state)\n-{\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct async muxer;\n-\tconst char *argv[2];\n-\tint code;\n-\n-\targv[0] = find_hook(hook_name);\n-\tif (!argv[0])\n-\t\treturn 0;\n-\n-\targv[1] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = hook_name;\n-\n-\tif (feed_state->push_options) {\n-\t\tint i;\n-\t\tfor (i = 0; i < feed_state->push_options->nr; i++)\n-\t\t\tstrvec_pushf(&proc.env_array,\n-\t\t\t\t     \"GIT_PUSH_OPTION_%d=%s\", i,\n-\t\t\t\t     feed_state->push_options->items[i].string);\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT=%d\",\n-\t\t\t     feed_state->push_options->nr);\n-\t} else\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT\");\n-\n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&proc.env_array, tmp_objdir_env(tmp_objdir));\n-\n-\tif (use_sideband) {\n-\t\tmemset(&muxer, 0, sizeof(muxer));\n-\t\tmuxer.proc = copy_to_sideband;\n-\t\tmuxer.in = -1;\n-\t\tcode = start_async(&muxer);\n-\t\tif (code)\n-\t\t\treturn code;\n-\t\tproc.err = muxer.in;\n-\t}\n-\n-\tprepare_push_cert_sha1(&proc);\n-\n-\tcode = start_command(&proc);\n-\tif (code) {\n-\t\tif (use_sideband)\n-\t\t\tfinish_async(&muxer);\n-\t\treturn code;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\n-\twhile (1) {\n-\t\tconst char *buf;\n-\t\tsize_t n;\n-\t\tif (feed(feed_state, &buf, &n))\n-\t\t\tbreak;\n-\t\tif (write_in_full(proc.in, buf, n) < 0)\n-\t\t\tbreak;\n-\t}\n-\tclose(proc.in);\n-\tif (use_sideband)\n-\t\tfinish_async(&muxer);\n-\n-\tsigchain_pop(SIGPIPE);\n-\n-\treturn finish_command(&proc);\n-}\n-\n-static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n+static int feed_receive_hook(void *state_)\n {\n \tstruct receive_hook_feed_state *state = state_;\n \tstruct command *cmd = state->cmd;\n@@ -884,9 +816,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t       state->skip_broken && (cmd->error_string || cmd->did_not_exist))\n \t\tcmd = cmd->next;\n \tif (!cmd)\n-\t\treturn -1; /* EOF */\n-\tif (!bufp)\n-\t\treturn 0; /* OK, can feed something. */\n+\t\treturn 1; /* EOF - close the pipe*/\n \tstrbuf_reset(&state->buf);\n \tif (!state->report)\n \t\tstate->report = cmd->report;\n@@ -910,32 +840,36 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t\t\t    cmd->ref_name);\n \t\tstate->cmd = cmd->next;\n \t}\n-\tif (bufp) {\n-\t\t*bufp = state->buf.buf;\n-\t\t*sizep = state->buf.len;\n-\t}\n \treturn 0;\n }\n \n-static int run_receive_hook(struct command *commands,\n-\t\t\t    const char *hook_name,\n-\t\t\t    int skip_broken,\n-\t\t\t    const struct string_list *push_options)\n+static int feed_receive_hook_cb(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n {\n-\tstruct receive_hook_feed_state state;\n-\tint status;\n-\n-\tstrbuf_init(&state.buf, 0);\n-\tstate.cmd = commands;\n-\tstate.skip_broken = skip_broken;\n-\tstate.report = NULL;\n-\tif (feed_receive_hook(&state, NULL, NULL))\n-\t\treturn 0;\n-\tstate.cmd = commands;\n-\tstate.push_options = push_options;\n-\tstatus = run_and_feed_hook(hook_name, feed_receive_hook, &state);\n-\tstrbuf_release(&state.buf);\n-\treturn status;\n+\tstruct hook *hook = pp_task_cb;\n+\tstruct receive_hook_feed_state *feed_state = hook->feed_pipe_cb_data;\n+\tint rc;\n+\n+\t/* first-time setup */\n+\tif (!feed_state) {\n+\t\tstruct hook_cb_data *hook_cb = pp_cb;\n+\t\tstruct run_hooks_opt *opt = hook_cb->options;\n+\t\tstruct receive_hook_feed_context *ctx = opt->feed_pipe_ctx;\n+\t\tif (!ctx)\n+\t\t\tBUG(\"run_hooks_opt.feed_pipe_ctx required for receive hook\");\n+\n+\t\tfeed_state = xmalloc(sizeof(struct receive_hook_feed_state));\n+\t\tstrbuf_init(&feed_state->buf, 0);\n+\t\tfeed_state->cmd = ctx->cmd;\n+\t\tfeed_state->skip_broken = ctx->skip_broken;\n+\t\tfeed_state->report = NULL;\n+\n+\t\thook->feed_pipe_cb_data = feed_state;\n+\t}\n+\n+\trc = feed_receive_hook(feed_state);\n+\tif (!rc)\n+\t\tstrbuf_addbuf(pipe, &feed_state->buf);\n+\treturn rc;\n }\n \n static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n@@ -971,6 +905,55 @@ static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n \tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n }\n \n+static int run_receive_hook(struct command *commands,\n+\t\t\t    const char *hook_name,\n+\t\t\t    int skip_broken,\n+\t\t\t    const struct string_list *push_options)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct receive_hook_feed_context ctx;\n+\tint rc;\n+\tstruct command *iter = commands;\n+\n+\t/* if there are no valid commands, don't invoke the hook at all. */\n+\twhile (iter && skip_broken && (iter->error_string || iter->did_not_exist))\n+\t\titer = iter->next;\n+\tif (!iter)\n+\t\treturn 0;\n+\n+\t/* pre-receive hooks should run in series as the hook updates refs */\n+\tif (!strcmp(hook_name, \"pre-receive\"))\n+\t\topt.jobs = 1;\n+\n+\tif (push_options) {\n+\t\tint i;\n+\t\tfor (i = 0; i < push_options->nr; i++)\n+\t\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_%d=%s\", i,\n+\t\t\t\t     push_options->items[i].string);\n+\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_COUNT=%d\", push_options->nr);\n+\t} else\n+\t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n+\n+\tif (tmp_objdir)\n+\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\n+\tprepare_push_cert_sha1(&opt);\n+\n+\t/* set up sideband printer */\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\t/* set up stdin callback */\n+\tctx.cmd = commands;\n+\tctx.skip_broken = skip_broken;\n+\topt.feed_pipe = feed_receive_hook_cb;\n+\topt.feed_pipe_ctx = &ctx;\n+\n+\trc = run_hooks(hook_name, &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n+}\n+\n static int run_update_hook(struct command *cmd)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n-- \n2.32.0.rc3.434.gd8aed1f08a7\n\n"},{"id":"427383","messageId":"YMe6nJHxN2yO36wf@google.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"Re: [PATCH v2 00/30] Minimal restart of \"config-based-hooks\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-06-14T20:22:52Z","receivedAt":"2021-06-14T20:24:05Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Mon, Jun 14, 2021 at 12:32:49PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> I proposed splitting Emily's \"hook config\" topic[1] into at least a\n> topic that retains all current behavior of the codebase, and merely\n> refactors existing behavior to new APIs, and then doing behavior\n> changes later.\n> \n> This is a re-roll of an attempt to do that. See v1's CL [2] for much\n> more details.\n> \n> I was hoping to get more feedback from Emily in reply to [3] and\n> related E-Mails on the v1, but as it's been almost 2 weeks with no\n> reply, and both her topic and mine semantically conflict with changes\n> since merged to \"master\" I thought I'd send this re-roll.\n\nYes, sorry about this. I'm planning on reviewing your series this week.\nThanks for sending the v2.\n\n - Emily\n"},{"id":"427386","messageId":"YMfLO9CT+iIDR3OA@google.com","threadId":"55286","inReplyTo":"patch-01.30-447d349c738-20210614T101920Z-avarab@gmail.com","subject":"Re: [PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-06-14T21:33:47Z","receivedAt":"2021-06-14T21:35:15Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Mon, Jun 14, 2021 at 12:32:50PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> In order to enable hooks to be run as an external process, by a\n> standalone Git command, or by tools which wrap Git, provide an external\n> means to run all configured hook commands for a given hook event.\n\nFrom what it says on the box, I'm slightly worried about this patch\ndoing too much at once, but let's see... (I think this is also a common\nthing you and I disagree on - how much work to do per commit - so feel\nfree to ignore me ;) )\n\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n\nThanks for including attribution - I appreciate it.\n\n> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> new file mode 100644\n> index 00000000000..902b9cffaef\n> --- /dev/null\n> +++ b/Documentation/git-hook.txt\n> @@ -0,0 +1,36 @@\n> +git-hook(1)\n> +===========\n> +\n> +NAME\n> +----\n> +git-hook - run git hooks\n> +\n> +SYNOPSIS\n> +--------\n> +[verse]\n> +'git hook' run <hook-name> [-- <hook-args>]\n\nInteresting. This is definitely more user friendly than `-a foo -a bar -a\naagh` ;)\n\nCan we think of a scenario when a user might want to alias to part of\n'git hook run' with an argument provided, but still wish to use the hook\nmore generally? I thought maybe something like `git hook run -a\n\"--user=Bob\"` for a user who has a handful of hooks that can take some\nadditional argument, but then I realized that most hooks need to meet a\ncontract with which args they accept, so this isn't a reasonable use\ncase.\n\nI also wondered whether accepting hook args this way implied that we\ncan't also provide environment vars for the hooks later on, but I think\nit's fine to have those two interfaces be asymmetrical, e.g. `git hook\nrun -e \"USERID=Bob\" -- blah.txt`.\n\nSo I like this way of accepting them :)\n\n[snip]\n> +run::\n> +\n> +\tRun the `<hook-name>` hook. Any positional arguments to the\n> +\thook should be passed after an optional \"--\" (or\n> +\t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n> +\tthis accepts.\n\nIs it clear enough that users will need to provide arguments to certain\nhooks? (Should this have some reference to githooks.txt?)\n\nThe \"OPTIONS\" reference is stale - there is no OPTIONS header in the\nmanpage now.\n\n> --- /dev/null\n> +++ b/builtin/hook.c\n> @@ -0,0 +1,65 @@\n> +#include \"cache.h\"\n> +#include \"builtin.h\"\n> +#include \"config.h\"\n> +#include \"hook.h\"\n> +#include \"parse-options.h\"\n> +#include \"strbuf.h\"\n> +#include \"strvec.h\"\n> +\n> +static const char * const builtin_hook_usage[] = {\n> +\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n> +\tNULL\n> +};\n> +\n> +static int run(int argc, const char **argv, const char *prefix)\n> +{\n> +\tint i;\n> +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n> +\tint rc = 0;\n> +\tconst char *hook_name;\n> +\tconst char *hook_path;\n> +\n> +\tstruct option run_options[] = {\n> +\t\tOPT_END(),\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, prefix, run_options,\n> +\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n> +\n> +\tif (argc > 2) {\n> +\t\tif (strcmp(argv[2], \"--\") &&\n> +\t\t    strcmp(argv[2], \"--end-of-options\"))\n> +\t\t\t/* Having a -- for \"run\" is mandatory */\n> +\t\t\tusage_with_options(builtin_hook_usage, run_options);\n> +\t\t/* Add our arguments, start after -- */\n> +\t\tfor (i = 3 ; i < argc; i++)\n> +\t\t\tstrvec_push(&opt.args, argv[i]);\n> +\t}\n> +\n> +\t/* Need to take into account core.hooksPath */\n> +\tgit_config(git_default_config, NULL);\n> +\n> +\thook_name = argv[1];\n> +\thook_path = find_hook(hook_name);\n> +\tif (!hook_path) {\n> +\t\terror(\"cannot find a hook named %s\", hook_name);\n> +\t\treturn 1;\n> +\t}\n> +\trc = run_found_hooks(hook_name, hook_path, &opt);\n\nHum, what's the reasoning for not letting the hook.h call look up the\nhook path for itself? I scanned through the v1 cover and older version\nof this patch and didn't see any reasoning. To me, having the builtin\nlook up paths feels like incorrect layering.\n\n> +int cmd_hook(int argc, const char **argv, const char *prefix)\n> +{\n> +\tstruct option builtin_hook_options[] = {\n> +\t\tOPT_END(),\n> +\t};\n> +\n> +\tif (!strcmp(argv[1], \"run\"))\n> +\t\treturn run(argc, argv, prefix);\n\nHum. This means that 'run' will still be included in argv for run(),\nwhich I see that it works around silently. I personally find that to be\nconfusing - maybe at least a comment pointing it out, if you don't like\nthe idea of adjusting argv before passing it to run()?\n\n> diff --git a/git.c b/git.c\n> index 18bed9a9964..540909c391f 100644\n> --- a/git.c\n> +++ b/git.c\n> @@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {\n>  \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n>  \t{ \"hash-object\", cmd_hash_object },\n>  \t{ \"help\", cmd_help },\n> +\t{ \"hook\", cmd_hook, RUN_SETUP },\n\nHm. RUN_SETUP requires a gitdir, which I suppose makes sense as this is\na pre-config-hooks world.\n\nDoes this mean \"git send-email\" will abort if I try to run it with no\ngitdir (which I often do)? I looked ahead to patch 10 and it doesn't\nlook like there's a significant change to the error handling, so I guess\nthat if it works for me today, it will work for me with this change too.\n\nLater config-based hooks will mean that such hooks could exist without a\ngitdir, but we can cross that bridge when we get there :)\n\n> --- /dev/null\n> +++ b/hook.c\n> @@ -0,0 +1,114 @@\n> +#include \"cache.h\"\n> +#include \"hook.h\"\n> +#include \"run-command.h\"\n> +\n> +void run_hooks_opt_clear(struct run_hooks_opt *o)\n> +{\n> +\tstrvec_clear(&o->env);\n> +\tstrvec_clear(&o->args);\n\nMaybe more graceful to nullcheck within the _clear() function before\ndereferencing 'o'? That way callers don't need to worry about NULL\nchecking on their end.\n\n> +static int pick_next_hook(struct child_process *cp,\n> +\t\t\t  struct strbuf *out,\n> +\t\t\t  void *pp_cb,\n> +\t\t\t  void **pp_task_cb)\n> +{\n> +\tstruct hook_cb_data *hook_cb = pp_cb;\n> +\tstruct hook *run_me = hook_cb->run_me;\n> +\n> +\tif (!run_me)\n> +\t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n\nI'm not sure I like this message, even as a BUG(), although the things\nI'd rather say (\"run_me was NULL unexpectedly!\") are obvious as soon as\nyou grep the codebase. So I think I dislike it for no reason :)\n\n[...]\n\n> +static int notify_start_failure(struct strbuf *out,\n> +\t\t\t\tvoid *pp_cb,\n> +\t\t\t\tvoid *pp_task_cp)\n> +{\n> +\tstruct hook_cb_data *hook_cb = pp_cb;\n> +\tstruct hook *attempted = pp_task_cp;\n> +\n> +\t/* |= rc in cb */\n> +\thook_cb->rc |= 1;\n\nYuck, I think I wrote this comment... yikes. Maybe something like\n\"hook_cb->rc reflects cumulative failure state\" instead?\n\n> +static int notify_hook_finished(int result,\n> +\t\t\t\tstruct strbuf *out,\n> +\t\t\t\tvoid *pp_cb,\n> +\t\t\t\tvoid *pp_task_cb)\n> +{\n> +\tstruct hook_cb_data *hook_cb = pp_cb;\n> +\n> +\t/* |= rc in cb */\n> +\thook_cb->rc |= result;\n\n(And same as above.)\n\n> +int run_found_hooks(const char *hook_name, const char *hook_path,\n> +\t\t    struct run_hooks_opt *options)\n> +{\n> +\tstruct hook my_hook = {\n> +\t\t.hook_path = hook_path,\n\nAs mentioned earlier, I think it is neater - and better for config-based\nhooks in the future - if my_hook.hook_path is set by find_hooks()\ndirectly instead of by being passed in, here. (I expect you did it this\nway because one of the later hooks lives in an odd place - but I seem to\nremember that one being strange in other ways, too, and I ended up\nletting it manage its own affairs in my attempt. So I'll look forward to\nseeing whether you handled that differently.)\n\n[...]\n\n> +int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n> +{\n> +\tconst char *hook_path;\n> +\tint ret;\n> +\tif (!options)\n> +\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n> +\n> +\thook_path = find_hook(hook_name);\n> +\n> +\t/* Care about nonexistence? Use run_found_hooks() */\n> +\tif (!hook_path)\n> +\t\treturn 0;\n\nAh, I see - you've done it this way so that builtin/hook.c can complain\n\"You tried to run pre-commit hook but you don't even have one!\".\n\nHm. I think I dislike this comment for the same reason I dislike the one\nmuch earlier in this patch - it's different from how I would have\nwritten it. But I do think it still conveys the exact same information (I\nwould have said \"If you need to act on a missing hook, use\nrun_found_hooks() instead\") so chalk it up to difference in tone\npreferences and ignore me :)\n\n> diff --git a/hook.h b/hook.h\n[...]\n> +\t/* Number of threads to parallelize across */\n> +\tint jobs;\n\nI wonder whether it's worth changing the comments here...\n\n[...]\n> +/*\n> + * Callback provided to feed_pipe_fn and consume_sideband_fn.\n> + */\n\n...and here, since they don't mean anything in the context of this\nspecific commit? But they will mean something later on in the series.\n\n> +/*\n> + * Calls find_hook(hookname) and runs the hooks (if any) with\n> + * run_found_hooks().\n> + */\n> +int run_hooks(const char *hook_name, struct run_hooks_opt *options);\n> +\n> +/*\n> + * Takes an already resolved hook and runs it. Internally the simpler\n> + * run_hooks() will call this.\n> + */\n> +int run_found_hooks(const char *hookname, const char *hook_path,\n> +\t\t    struct run_hooks_opt *options);\n\nThe comments in the header here resolve any concerns I had about the\ncomments in the run_hooks() implementation. I like these a lot.\n\n> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> new file mode 100755\n> index 00000000000..f6ff6c4a493\n> --- /dev/null\n> +++ b/t/t1800-hook.sh\n> @@ -0,0 +1,131 @@\n> +#!/bin/bash\n> +\n> +test_description='git-hook command'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'git hook run -- nonexistent hook' '\n\nNit: Since you take '--' in 'git hook run' now, can you use something else as\na delimiter in the test names? I keep reading these as \"here we will\ncall `git hook run -- nonexistent hook`\" :/\n\n> +\tcat >stderr.expect <<-\\EOF &&\n> +\terror: cannot find a hook named does-not-exist\n> +\tEOF\n> +\ttest_expect_code 1 git hook run does-not-exist 2>stderr.actual &&\n> +\ttest_cmp stderr.expect stderr.actual\n\nI'm not wild about matching directly against the error message; that\nmeans that the test will be a pain to update any time we update the\nerror message language. I'd prefer an approach where we check that the\nerror is for the reason we expect (by ensuring .git/hooks/does-not-exist\nis not there in the fs) and then check that 'git hook run' fails, but do\nnot particularly care about the error message.\n\n> +test_expect_success 'git hook run -- stdout and stderr handling' '\n\nI have a slight preference towards \"the name of the test tells me\nexactly what is supposed to happen\" - which means I'd prefer to see this\nnamed \"stdout and stderr both write to hook's stderr\". Too chatty,\nmaybe, though.\n\n> +test_expect_success 'git hook run -- out-of-repo runs excluded' '\n> +\twrite_script .git/hooks/test-hook <<-EOF &&\n> +\techo Test hook\n> +\tEOF\n> +\n> +\tnongit test_must_fail git hook run test-hook\n\nI wonder if it's necessary to enforce this. I'm just thinking, in a\nconfig-based hook world later on, it will make sense to allow nongit\nruns - specifically, I'd use the heck out of a send-email hook to fixup\nmy In-Reply-To lines, and I always run git-send-email from a nongit dir,\nbecause I keep all my mailed patches stored away out of repo.\n\nWhat's the general feeling towards \"this is how it works, but we don't\nhave a good reason to require it\"?\n\n> +test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n> +\tmkdir my-hooks &&\n> +\twrite_script my-hooks/test-hook <<-EOF &&\n> +\techo Hook ran >>actual\n> +\tEOF\n> +\n> +\tcat >expect <<-\\EOF &&\n> +\tTest hook\n> +\tHook ran\n> +\tHook ran\n> +\tHook ran\n> +\tHook ran\n> +\tEOF\n\nI'm not sure I like this - collecting multiple runs into one \"actual\"\nand only comparing it once at the end. Are there other places in the\ncodebase that do this?\n\n> +\n> +\t# Test various ways of specifying the path. See also\n> +\t# t1350-config-hooks-path.sh\n> +\t>actual &&\n> +\tgit hook run test-hook 2>>actual &&\n> +\tgit -c core.hooksPath=my-hooks hook run test-hook 2>>actual &&\n> +\tgit -c core.hooksPath=my-hooks/ hook run test-hook 2>>actual &&\n> +\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook 2>>actual &&\n> +\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook 2>>actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n> +\t>actual &&\n> +\tmkdir -p .git/custom-hooks .git/hooks &&\n> +\twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n> +\techo CUSTOM >>actual\n> +\tEOF\n> +\twrite_script .git/hooks/pre-commit <<-\\EOF\n> +\techo NORMAL >>actual\n> +\tEOF\n> +'\n\nIs this setup test a leftover from a later commit?\n\n\nOverall, I think I like the direction your reroll is going - I've needed\nsome time to process it. Hopefully I'll be able to get through all or\nmost of the series this week, but there's a lot going on here, too. I'll\ndo what I can. Thanks for the help.\n\n - Emily\n"},{"id":"427389","messageId":"YMfs4Zr4Q22W5dnB@google.com","threadId":"55286","inReplyTo":"patch-04.30-da2763192ae-20210614T101920Z-avarab@gmail.com","subject":"Re: [PATCH v2 04/30] gc: use hook library for pre-auto-gc hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-06-14T23:57:21Z","receivedAt":"2021-06-14T23:58:45Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Mon, Jun 14, 2021 at 12:32:53PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> Using the hook.h library instead of the run-command.h library to run\n> pre-auto-gc means that those hooks can be set up in config files, as\n> well as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n\nI think all the commit messages in \"convert x to hook library\" commits I\nwrote extol the virtues of config-based hooks. Since that's not part of\nyour reroll, I expect we should change the commit messages too.\n\n - Emily\n"},{"id":"427438","messageId":"87lf7bzbrk.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YMfLO9CT+iIDR3OA@google.com","subject":"Re: [PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-15T09:36:26Z","receivedAt":"2021-06-15T10:02:29Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jun 14 2021, Emily Shaffer wrote:\n\n> On Mon, Jun 14, 2021 at 12:32:50PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> In order to enable hooks to be run as an external process, by a\n>> standalone Git command, or by tools which wrap Git, provide an external\n>> means to run all configured hook commands for a given hook event.\n>\n> From what it says on the box, I'm slightly worried about this patch\n> doing too much at once, but let's see... (I think this is also a common\n> thing you and I disagree on - how much work to do per commit - so feel\n> free to ignore me ;) )\n>\n>> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n>> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n>\n> Thanks for including attribution - I appreciate it.\n\nThey are almost entirely subsets of your patches, so I retained the\nauthorship, but added my own SOB. The only ones with my authorship are\nsomething where I added fixes in-between or on top.\n\n>> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n>> new file mode 100644\n>> index 00000000000..902b9cffaef\n>> --- /dev/null\n>> +++ b/Documentation/git-hook.txt\n>> @@ -0,0 +1,36 @@\n>> +git-hook(1)\n>> +===========\n>> +\n>> +NAME\n>> +----\n>> +git-hook - run git hooks\n>> +\n>> +SYNOPSIS\n>> +--------\n>> +[verse]\n>> +'git hook' run <hook-name> [-- <hook-args>]\n>\n> Interesting. This is definitely more user friendly than `-a foo -a bar -a\n> aagh` ;)\n>\n> Can we think of a scenario when a user might want to alias to part of\n> 'git hook run' with an argument provided, but still wish to use the hook\n> more generally? I thought maybe something like `git hook run -a\n> \"--user=Bob\"` for a user who has a handful of hooks that can take some\n> additional argument, but then I realized that most hooks need to meet a\n> contract with which args they accept, so this isn't a reasonable use\n> case.\n\nI can only think of ones where the [-- <hook-args>] would be sufficient.\n\n> I also wondered whether accepting hook args this way implied that we\n> can't also provide environment vars for the hooks later on, but I think\n> it's fine to have those two interfaces be asymmetrical, e.g. `git hook\n> run -e \"USERID=Bob\" -- blah.txt`.\n>\n> So I like this way of accepting them :)\n\nI carved out the \"-e\" support from \"run\" as part of a general removal of\nthings not needed for the bug-for-bug compatibility with existing hook\ninterfaces, i.e. nothing needed it in this phase.\n\nBut I also can't see why a \"hook run\" would need to have an \"-e\",\nanything that invokes the command has other more native ways of setting\nthe environment first, so the command just needs to avoid actively\nclearing its own environment, no?\n\n> [snip]\n>> +run::\n>> +\n>> +\tRun the `<hook-name>` hook. Any positional arguments to the\n>> +\thook should be passed after an optional \"--\" (or\n>> +\t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n>> +\tthis accepts.\n>\n> Is it clear enough that users will need to provide arguments to certain\n> hooks? (Should this have some reference to githooks.txt?)\n\nProbably, I also figured at this point it could be left as almost a\n*--helper command.\n\n> The \"OPTIONS\" reference is stale - there is no OPTIONS header in the\n> manpage now.\n\nWell spotted, will fix, or maybe you will :)\n\n>> --- /dev/null\n>> +++ b/builtin/hook.c\n>> @@ -0,0 +1,65 @@\n>> +#include \"cache.h\"\n>> +#include \"builtin.h\"\n>> +#include \"config.h\"\n>> +#include \"hook.h\"\n>> +#include \"parse-options.h\"\n>> +#include \"strbuf.h\"\n>> +#include \"strvec.h\"\n>> +\n>> +static const char * const builtin_hook_usage[] = {\n>> +\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>> +\tNULL\n>> +};\n>> +\n>> +static int run(int argc, const char **argv, const char *prefix)\n>> +{\n>> +\tint i;\n>> +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>> +\tint rc = 0;\n>> +\tconst char *hook_name;\n>> +\tconst char *hook_path;\n>> +\n>> +\tstruct option run_options[] = {\n>> +\t\tOPT_END(),\n>> +\t};\n>> +\n>> +\targc = parse_options(argc, argv, prefix, run_options,\n>> +\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n>> +\n>> +\tif (argc > 2) {\n>> +\t\tif (strcmp(argv[2], \"--\") &&\n>> +\t\t    strcmp(argv[2], \"--end-of-options\"))\n>> +\t\t\t/* Having a -- for \"run\" is mandatory */\n>> +\t\t\tusage_with_options(builtin_hook_usage, run_options);\n>> +\t\t/* Add our arguments, start after -- */\n>> +\t\tfor (i = 3 ; i < argc; i++)\n>> +\t\t\tstrvec_push(&opt.args, argv[i]);\n>> +\t}\n>> +\n>> +\t/* Need to take into account core.hooksPath */\n>> +\tgit_config(git_default_config, NULL);\n>> +\n>> +\thook_name = argv[1];\n>> +\thook_path = find_hook(hook_name);\n>> +\tif (!hook_path) {\n>> +\t\terror(\"cannot find a hook named %s\", hook_name);\n>> +\t\treturn 1;\n>> +\t}\n>> +\trc = run_found_hooks(hook_name, hook_path, &opt);\n>\n> Hum, what's the reasoning for not letting the hook.h call look up the\n> hook path for itself? I scanned through the v1 cover and older version\n> of this patch and didn't see any reasoning. To me, having the builtin\n> look up paths feels like incorrect layering.\n\nI see you end up addressing this yourself below, i.e. some users want to\nsilently ignore missing hooks, others want to error (as in this case).\n\n>> +int cmd_hook(int argc, const char **argv, const char *prefix)\n>> +{\n>> +\tstruct option builtin_hook_options[] = {\n>> +\t\tOPT_END(),\n>> +\t};\n>> +\n>> +\tif (!strcmp(argv[1], \"run\"))\n>> +\t\treturn run(argc, argv, prefix);\n>\n> Hum. This means that 'run' will still be included in argv for run(),\n> which I see that it works around silently. I personally find that to be\n> confusing - maybe at least a comment pointing it out, if you don't like\n> the idea of adjusting argv before passing it to run()?\n\nIt's not something I actively changed IIRC, i.e. it just came out of the\ngeneral refactoring of minimizing the command. I tihnk it would also\nmake sense (probably more sense) to adjust argv/argc as you suggest.\n\n>> diff --git a/git.c b/git.c\n>> index 18bed9a9964..540909c391f 100644\n>> --- a/git.c\n>> +++ b/git.c\n>> @@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {\n>>  \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n>>  \t{ \"hash-object\", cmd_hash_object },\n>>  \t{ \"help\", cmd_help },\n>> +\t{ \"hook\", cmd_hook, RUN_SETUP },\n>\n> Hm. RUN_SETUP requires a gitdir, which I suppose makes sense as this is\n> a pre-config-hooks world.\n>\n> Does this mean \"git send-email\" will abort if I try to run it with no\n> gitdir (which I often do)? I looked ahead to patch 10 and it doesn't\n> look like there's a significant change to the error handling, so I guess\n> that if it works for me today, it will work for me with this change too.\n>\n> Later config-based hooks will mean that such hooks could exist without a\n> gitdir, but we can cross that bridge when we get there :)\n\nRight, since it's a way more limited command used for only running hooks\nby git itself it's a RUN_SETUP. I had some preliminary WIP work (based\non yours, obviously) to make it a RUN_SETUP_GENTLY, but within this\nseries it doesn't become needed.\n\n>> --- /dev/null\n>> +++ b/hook.c\n>> @@ -0,0 +1,114 @@\n>> +#include \"cache.h\"\n>> +#include \"hook.h\"\n>> +#include \"run-command.h\"\n>> +\n>> +void run_hooks_opt_clear(struct run_hooks_opt *o)\n>> +{\n>> +\tstrvec_clear(&o->env);\n>> +\tstrvec_clear(&o->args);\n>\n> Maybe more graceful to nullcheck within the _clear() function before\n> dereferencing 'o'? That way callers don't need to worry about NULL\n> checking on their end.\n\nI don't feel strongly about it, but I tihnk doing it this way is\nconsistent with other APIs in git. I.e. you do:\n\n    struct something blah = BLAH_INIT;\n    [...]\n    blah_release(&blah);\n\nAnd we simply assume that \"blah\" isn't NULL, and that any field\nmandatory via the \"INIT\" initialization can be assumed not to be NULL.\n\n>> +static int pick_next_hook(struct child_process *cp,\n>> +\t\t\t  struct strbuf *out,\n>> +\t\t\t  void *pp_cb,\n>> +\t\t\t  void **pp_task_cb)\n>> +{\n>> +\tstruct hook_cb_data *hook_cb = pp_cb;\n>> +\tstruct hook *run_me = hook_cb->run_me;\n>> +\n>> +\tif (!run_me)\n>> +\t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n>\n> I'm not sure I like this message, even as a BUG(), although the things\n> I'd rather say (\"run_me was NULL unexpectedly!\") are obvious as soon as\n> you grep the codebase. So I think I dislike it for no reason :)\n\nI think I just added that as an assertion during development, could be\nturned into somethin gelse.\n\n> [...]\n>\n>> +static int notify_start_failure(struct strbuf *out,\n>> +\t\t\t\tvoid *pp_cb,\n>> +\t\t\t\tvoid *pp_task_cp)\n>> +{\n>> +\tstruct hook_cb_data *hook_cb = pp_cb;\n>> +\tstruct hook *attempted = pp_task_cp;\n>> +\n>> +\t/* |= rc in cb */\n>> +\thook_cb->rc |= 1;\n>\n> Yuck, I think I wrote this comment... yikes. Maybe something like\n> \"hook_cb->rc reflects cumulative failure state\" instead?\n\n*nod*, more on this below...\n\n>> +static int notify_hook_finished(int result,\n>> +\t\t\t\tstruct strbuf *out,\n>> +\t\t\t\tvoid *pp_cb,\n>> +\t\t\t\tvoid *pp_task_cb)\n>> +{\n>> +\tstruct hook_cb_data *hook_cb = pp_cb;\n>> +\n>> +\t/* |= rc in cb */\n>> +\thook_cb->rc |= result;\n>\n> (And same as above.)\n\n..and this...\n\n>> +int run_found_hooks(const char *hook_name, const char *hook_path,\n>> +\t\t    struct run_hooks_opt *options)\n>> +{\n>> +\tstruct hook my_hook = {\n>> +\t\t.hook_path = hook_path,\n>\n> As mentioned earlier, I think it is neater - and better for config-based\n> hooks in the future - if my_hook.hook_path is set by find_hooks()\n> directly instead of by being passed in, here. (I expect you did it this\n> way because one of the later hooks lives in an odd place - but I seem to\n> remember that one being strange in other ways, too, and I ended up\n> letting it manage its own affairs in my attempt. So I'll look forward to\n> seeing whether you handled that differently.)\n\nYeah, the designated initializers made a few things much nicer.\n\n\n>\n> [...]\n>\n>> +int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n>> +{\n>> +\tconst char *hook_path;\n>> +\tint ret;\n>> +\tif (!options)\n>> +\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n>> +\n>> +\thook_path = find_hook(hook_name);\n>> +\n>> +\t/* Care about nonexistence? Use run_found_hooks() */\n>> +\tif (!hook_path)\n>> +\t\treturn 0;\n>\n> Ah, I see - you've done it this way so that builtin/hook.c can complain\n> \"You tried to run pre-commit hook but you don't even have one!\".\n\nSounds better.\n\n> Hm. I think I dislike this comment for the same reason I dislike the one\n> much earlier in this patch - it's different from how I would have\n> written it. But I do think it still conveys the exact same information (I\n> would have said \"If you need to act on a missing hook, use\n> run_found_hooks() instead\") so chalk it up to difference in tone\n> preferences and ignore me :)\n>\n>> diff --git a/hook.h b/hook.h\n> [...]\n>> +\t/* Number of threads to parallelize across */\n>> +\tint jobs;\n>\n> I wonder whether it's worth changing the comments here...\n\nSo more generally (continuing the \"and this\" above), I really tried to\nnot change the comments, structure, names, order etc. of your initial\nseries unless it was necessary for the stated aims of minimizing and\nsimplifying this.\n\nThat was all in order to make it easier for you to eventually rebase on\ntop of this, or to pick it up. I.e. it leaves most things as subsets of\ncorresponding patches of yours.\n\nBut if you think this approach is good / would want to pick it up and\nrun with it that would make me happy, and I think at that point you\nshould continue tweaking/adjusting anything like this that you want,\nsince the non-tweaking of it was something I did for your benefit, but\nif you don't mind...\n\n> [...]\n>> +/*\n>> + * Callback provided to feed_pipe_fn and consume_sideband_fn.\n>> + */\n>\n> ...and here, since they don't mean anything in the context of this\n> specific commit? But they will mean something later on in the series.\n\n*ditto*\n\n>> +/*\n>> + * Calls find_hook(hookname) and runs the hooks (if any) with\n>> + * run_found_hooks().\n>> + */\n>> +int run_hooks(const char *hook_name, struct run_hooks_opt *options);\n>> +\n>> +/*\n>> + * Takes an already resolved hook and runs it. Internally the simpler\n>> + * run_hooks() will call this.\n>> + */\n>> +int run_found_hooks(const char *hookname, const char *hook_path,\n>> +\t\t    struct run_hooks_opt *options);\n>\n> The comments in the header here resolve any concerns I had about the\n> comments in the run_hooks() implementation. I like these a lot.\n>\n>> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n>> new file mode 100755\n>> index 00000000000..f6ff6c4a493\n>> --- /dev/null\n>> +++ b/t/t1800-hook.sh\n>> @@ -0,0 +1,131 @@\n>> +#!/bin/bash\n>> +\n>> +test_description='git-hook command'\n>> +\n>> +. ./test-lib.sh\n>> +\n>> +test_expect_success 'git hook run -- nonexistent hook' '\n>\n> Nit: Since you take '--' in 'git hook run' now, can you use something else as\n> a delimiter in the test names? I keep reading these as \"here we will\n> call `git hook run -- nonexistent hook`\" :/\n\nYeah, makes sense, maybe \": \" more generally in this test?\n\n>> +\tcat >stderr.expect <<-\\EOF &&\n>> +\terror: cannot find a hook named does-not-exist\n>> +\tEOF\n>> +\ttest_expect_code 1 git hook run does-not-exist 2>stderr.actual &&\n>> +\ttest_cmp stderr.expect stderr.actual\n>\n> I'm not wild about matching directly against the error message; that\n> means that the test will be a pain to update any time we update the\n> error message language. I'd prefer an approach where we check that the\n> error is for the reason we expect (by ensuring .git/hooks/does-not-exist\n> is not there in the fs) and then check that 'git hook run' fails, but do\n> not particularly care about the error message.\n\nI generally prefer it, but as noted above, \"if you want to pick it up...\" :)\n\nFWIW yes I agree it's a bit of a pain, but having run into a lot of\nthings of the form:\n\n    cmd 2>err &&\n    grep \"some message I expect\" err\n\nhiding issues like the message being duplicated, or an unexpected\nadditional error/warning being there, I think it's best just to test_cmp\nthe full output (inserting relevant OIDs if needed) when possible.\n\n>> +test_expect_success 'git hook run -- stdout and stderr handling' '\n>\n> I have a slight preference towards \"the name of the test tells me\n> exactly what is supposed to happen\" - which means I'd prefer to see this\n> named \"stdout and stderr both write to hook's stderr\". Too chatty,\n> maybe, though.\n>\n>> +test_expect_success 'git hook run -- out-of-repo runs excluded' '\n>> +\twrite_script .git/hooks/test-hook <<-EOF &&\n>> +\techo Test hook\n>> +\tEOF\n>> +\n>> +\tnongit test_must_fail git hook run test-hook\n>\n> I wonder if it's necessary to enforce this. I'm just thinking, in a\n> config-based hook world later on, it will make sense to allow nongit\n> runs - specifically, I'd use the heck out of a send-email hook to fixup\n> my In-Reply-To lines, and I always run git-send-email from a nongit dir,\n> because I keep all my mailed patches stored away out of repo.\n>\n> What's the general feeling towards \"this is how it works, but we don't\n> have a good reason to require it\"?\n\nI for one think tests should assert existing behavior, we now have\nRUN_SETUP so it would be a bug for thaht not to error, so a test is\nasserting it. It doesn't mean that we shouldn't willy-nilly change this\nlater on if and when it's expected that we have a RUN_SETUP_GENTLY.\n\n>> +test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n>> +\tmkdir my-hooks &&\n>> +\twrite_script my-hooks/test-hook <<-EOF &&\n>> +\techo Hook ran >>actual\n>> +\tEOF\n>> +\n>> +\tcat >expect <<-\\EOF &&\n>> +\tTest hook\n>> +\tHook ran\n>> +\tHook ran\n>> +\tHook ran\n>> +\tHook ran\n>> +\tEOF\n>\n> I'm not sure I like this - collecting multiple runs into one \"actual\"\n> and only comparing it once at the end. Are there other places in the\n> codebase that do this?\n\nYeah, that's some ad-hoc nastyness, would be better if it attributed\nspecific runs or otherwise asserted the \"we expected to run\" otherwise.\n\n>> +\n>> +\t# Test various ways of specifying the path. See also\n>> +\t# t1350-config-hooks-path.sh\n>> +\t>actual &&\n>> +\tgit hook run test-hook 2>>actual &&\n>> +\tgit -c core.hooksPath=my-hooks hook run test-hook 2>>actual &&\n>> +\tgit -c core.hooksPath=my-hooks/ hook run test-hook 2>>actual &&\n>> +\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook 2>>actual &&\n>> +\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook 2>>actual &&\n>> +\ttest_cmp expect actual\n>> +'\n>> +\n>> +test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n>> +\t>actual &&\n>> +\tmkdir -p .git/custom-hooks .git/hooks &&\n>> +\twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n>> +\techo CUSTOM >>actual\n>> +\tEOF\n>> +\twrite_script .git/hooks/pre-commit <<-\\EOF\n>> +\techo NORMAL >>actual\n>> +\tEOF\n>> +'\n>\n> Is this setup test a leftover from a later commit?\n\nHrm, maybe... :)\n\n> Overall, I think I like the direction your reroll is going - I've needed\n> some time to process it. Hopefully I'll be able to get through all or\n> most of the series this week, but there's a lot going on here, too. I'll\n> do what I can. Thanks for the help.\n\nYeah, will reply to any qusetions etc; and as noted above my initial\ngoal here was \"hey, what about this approach\", so if you wanted to pick\nthis up & run with it...\n\nThis particular version of the series is at github.com/avar/git.git's\nes-avar/config-based-hooks-3 b.t.w.\n"},{"id":"427439","messageId":"87im2fzbks.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"patch-30.30-bc086454d68-20210614T101920Z-avarab@gmail.com","subject":"Re: [PATCH v2 30/30] hook-list.h: add a generated list of hooks, like config-list.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-15T10:02:31Z","receivedAt":"2021-06-15T10:06:37Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Mon, Jun 14 2021, Ævar Arnfjörð Bjarmason wrote:\n\n> Resolve a long-standing TODO item in bugreport.c of there being no\n> centralized listing of hooks, this fixes a bug with the bugreport\n> listing only knowing about 1/4 of the p4 hooks. It didn't know about\n> the \"reference-transaction\" hook either.\n>\n> We can now make sure this is kept up-to-date, as the hook.c library\n> will die if asked to find a hook we don't know about yet. The only\n> (undocumented) exception is the artificial \"test-hook\" used in our own\n> test suite. Move some of the tests away from the \"does-not-exist\"\n> pseudo-hook, and test for the new behavior.\n>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  .gitignore           |  1 +\n>  Makefile             | 14 +++++++++++---\n>  builtin/bugreport.c  | 44 ++++++++------------------------------------\n>  generate-hooklist.sh | 24 ++++++++++++++++++++++++\n>  hook.c               | 22 ++++++++++++++++++++++\n>  t/t1800-hook.sh      | 14 +++++++++++---\n>  6 files changed, 77 insertions(+), 42 deletions(-)\n>  create mode 100755 generate-hooklist.sh\n>\n> diff --git a/.gitignore b/.gitignore\n> index de39dc9961b..66189ca3cdc 100644\n> --- a/.gitignore\n> +++ b/.gitignore\n> @@ -191,6 +191,7 @@\n>  /gitweb/static/gitweb.min.*\n>  /config-list.h\n>  /command-list.h\n> +/hook-list.h\n>  *.tar.gz\n>  *.dsc\n>  *.deb\n> diff --git a/Makefile b/Makefile\n> index a6b71a0fbed..d0532f3c744 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -817,6 +817,7 @@ XDIFF_LIB = xdiff/lib.a\n>  \n>  GENERATED_H += command-list.h\n>  GENERATED_H += config-list.h\n> +GENERATED_H += hook-list.h\n\nThis fails CI on the Windows boxes (or some of them...) because we hard\nrely on cmake there since semi-recently; See 4c2c38e800f (ci:\nmodification of main.yml to use cmake for vs-build job, 2020-06-26).\n\nFixing that seems to be a matter of copying some similar boilerplate\naround, but I haven't tried.\n\nI was somewhat surprised that cmake support being semi-mandatory seems\nto have snuck up on us (or at least on me). The docs in\ncompat/vcbuild/README still suggest you need make, so perhaps the CI on\nWindows could be split up into something that uses \"make\" and one that\njust checks for the cmake compatibility.\n\nIt's not that I much prefer the Makefile, but I do think the state of\naffairs of needing to duplicate any work of patching the Makefile would\nbe a step back for me, but maybe the benefit on Windows is so large as\nto be worth it...\n"},{"id":"427552","messageId":"xmqqeed2vdqk.fsf@gitster.g","threadId":"55286","inReplyTo":"YMe6nJHxN2yO36wf@google.com","subject":"Re: [PATCH v2 00/30] Minimal restart of \"config-based-hooks\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-06-16T00:45:39Z","receivedAt":"2021-06-16T00:45:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> On Mon, Jun 14, 2021 at 12:32:49PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> I proposed splitting Emily's \"hook config\" topic[1] into at least a\n>> topic that retains all current behavior of the codebase, and merely\n>> refactors existing behavior to new APIs, and then doing behavior\n>> changes later.\n>> \n>> This is a re-roll of an attempt to do that. See v1's CL [2] for much\n>> more details.\n>> \n>> I was hoping to get more feedback from Emily in reply to [3] and\n>> related E-Mails on the v1, but as it's been almost 2 weeks with no\n>> reply, and both her topic and mine semantically conflict with changes\n>> since merged to \"master\" I thought I'd send this re-roll.\n>\n> Yes, sorry about this. I'm planning on reviewing your series this week.\n> Thanks for sending the v2.\n\nThanks, both.  In the meantime, I will eject es/config-based-hooks\nout of 'seen' and replace it with these patches, to see if there are\nunexpected interactions with other topics in flight.\n\n"},{"id":"427693","messageId":"patch-01.27-cf4b06bfdf-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 01/27] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:35Z","receivedAt":"2021-06-17T10:23:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn order to enable hooks to be run as an external process, by a\nstandalone Git command, or by tools which wrap Git, provide an external\nmeans to run all configured hook commands for a given hook event.\n\nMost of our hooks require more complex functionality than this, but\nlet's start with the bare minimum required to support our simplest\nhooks.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n .gitignore                 |   1 +\n Documentation/git-hook.txt |  38 +++++++++++\n Documentation/githooks.txt |   4 ++\n Makefile                   |   1 +\n builtin.h                  |   1 +\n builtin/hook.c             |  81 +++++++++++++++++++++++\n command-list.txt           |   1 +\n git.c                      |   1 +\n hook.c                     | 119 +++++++++++++++++++++++++++++++++\n hook.h                     |  53 +++++++++++++++\n t/t1800-hook.sh            | 131 +++++++++++++++++++++++++++++++++++++\n 11 files changed, 431 insertions(+)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100755 t/t1800-hook.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 6be9de41ae..66189ca3cd 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-hook\n /git-http-backend\n /git-http-fetch\n /git-http-push\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nnew file mode 100644\nindex 0000000000..660d6a992a\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,38 @@\n+git-hook(1)\n+===========\n+\n+NAME\n+----\n+git-hook - run git hooks\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git hook' run <hook-name> [-- <hook-args>]\n+\n+DESCRIPTION\n+-----------\n+\n+This command is an interface to git hooks (see linkgit:githooks[5]).\n+Currently it only provides a convenience wrapper for running hooks for\n+use by git itself. In the future it might gain other functionality.\n+\n+SUBCOMMANDS\n+-----------\n+\n+run::\n+\tRun the `<hook-name>` hook. See linkgit:githooks[5] for\n+\tthe hook names we support.\n++\n+Any positional arguments to the hook should be passed after an\n+optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n+arguments (if any) differ by hook name, see linkgit:githooks[5] for\n+what those are.\n+\n+SEE ALSO\n+--------\n+linkgit:githooks[5]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex b51959ff94..a16e62bc8c 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -698,6 +698,10 @@ and \"0\" meaning they were not.\n Only one parameter should be set to \"1\" when the hook runs.  The hook\n running passing \"1\", \"1\" should not be possible.\n \n+SEE ALSO\n+--------\n+linkgit:git-hook[1]\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex 9bd31e1ac5..a52d509162 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1106,6 +1106,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\ndiff --git a/builtin.h b/builtin.h\nindex 16ecd5586f..91740c1514 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);\n int cmd_grep(int argc, const char **argv, const char *prefix);\n int cmd_hash_object(int argc, const char **argv, const char *prefix);\n int cmd_help(int argc, const char **argv, const char *prefix);\n+int cmd_hook(int argc, const char **argv, const char *prefix);\n int cmd_index_pack(int argc, const char **argv, const char *prefix);\n int cmd_init_db(int argc, const char **argv, const char *prefix);\n int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/hook.c b/builtin/hook.c\nnew file mode 100644\nindex 0000000000..7714d31ef1\n--- /dev/null\n+++ b/builtin/hook.c\n@@ -0,0 +1,81 @@\n+#include \"cache.h\"\n+#include \"builtin.h\"\n+#include \"config.h\"\n+#include \"hook.h\"\n+#include \"parse-options.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+static const char * const builtin_hook_usage[] = {\n+\tN_(\"git hook <command> [...]\"),\n+\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n+\tNULL\n+};\n+\n+static const char * const builtin_hook_run_usage[] = {\n+\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n+\tNULL\n+};\n+\n+static int run(int argc, const char **argv, const char *prefix)\n+{\n+\tint i;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint rc = 0;\n+\tconst char *hook_name;\n+\tconst char *hook_path;\n+\n+\tstruct option run_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, run_options,\n+\t\t\t     builtin_hook_run_usage,\n+\t\t\t     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (argc > 1) {\n+\t\tif (strcmp(argv[1], \"--\") &&\n+\t\t    strcmp(argv[1], \"--end-of-options\"))\n+\t\t\t/* Having a -- for \"run\" is mandatory */\n+\t\t\tusage_with_options(builtin_hook_usage, run_options);\n+\t\t/* Add our arguments, start after -- */\n+\t\tfor (i = 2 ; i < argc; i++)\n+\t\t\tstrvec_push(&opt.args, argv[i]);\n+\t}\n+\n+\t/* Need to take into account core.hooksPath */\n+\tgit_config(git_default_config, NULL);\n+\n+\t/*\n+\t * We are not using run_hooks() because we'd like to detect\n+\t * missing hooks. Let's find it ourselves and call\n+\t * run_found_hooks() instead.\n+\t */\n+\thook_name = argv[0];\n+\thook_path = find_hook(hook_name);\n+\tif (!hook_path) {\n+\t\terror(\"cannot find a hook named %s\", hook_name);\n+\t\treturn 1;\n+\t}\n+\trc = run_found_hooks(hook_name, hook_path, &opt);\n+\n+\trun_hooks_opt_clear(&opt);\n+\n+\treturn rc;\n+}\n+\n+int cmd_hook(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option builtin_hook_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\targc = parse_options(argc, argv, NULL, builtin_hook_options,\n+\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n+\tif (!argc)\n+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+\n+\tif (!strcmp(argv[0], \"run\"))\n+\t\treturn run(argc, argv, prefix);\n+\telse\n+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex a289f09ed6..9ccd8e5aeb 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-hook                                mainporcelain\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\n git-http-push                           synchelpers\ndiff --git a/git.c b/git.c\nindex 18bed9a996..540909c391 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nindex 1f1db1ec9b..4d87e98255 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -2,11 +2,14 @@\n #include \"hook.h\"\n #include \"run-command.h\"\n #include \"hook-list.h\"\n+#include \"config.h\"\n \n static int known_hook(const char *name)\n {\n \tconst char **p;\n \tsize_t len = strlen(name);\n+\tstatic int test_hooks_ok = -1;\n+\n \tfor (p = hook_name_list; *p; p++) {\n \t\tconst char *hook = *p;\n \n@@ -14,6 +17,14 @@ static int known_hook(const char *name)\n \t\t\treturn 1;\n \t}\n \n+\tif (test_hooks_ok == -1)\n+\t\ttest_hooks_ok = git_env_bool(\"GIT_TEST_FAKE_HOOKS\", 0);\n+\n+\tif (test_hooks_ok &&\n+\t    (!strcmp(name, \"test-hook\") ||\n+\t     !strcmp(name, \"does-not-exist\")))\n+\t\treturn 1;\n+\n \treturn 0;\n }\n \n@@ -59,3 +70,111 @@ int hook_exists(const char *name)\n {\n \treturn !!find_hook(name);\n }\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o)\n+{\n+\tstrvec_clear(&o->env);\n+\tstrvec_clear(&o->args);\n+}\n+\n+static int pick_next_hook(struct child_process *cp,\n+\t\t\t  struct strbuf *out,\n+\t\t\t  void *pp_cb,\n+\t\t\t  void **pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *run_me = hook_cb->run_me;\n+\n+\tcp->no_stdin = 1;\n+\tcp->env = hook_cb->options->env.v;\n+\tcp->stdout_to_stderr = 1;\n+\tcp->trace2_hook_name = hook_cb->hook_name;\n+\n+\t/* add command */\n+\tstrvec_push(&cp->args, run_me->hook_path);\n+\n+\t/*\n+\t * add passed-in argv, without expanding - let the user get back\n+\t * exactly what they put in\n+\t */\n+\tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n+\n+\t/* Provide context for errors if necessary */\n+\t*pp_task_cb = run_me;\n+\n+\treturn 1;\n+}\n+\n+static int notify_start_failure(struct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cp)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *attempted = pp_task_cp;\n+\n+\thook_cb->rc |= 1;\n+\n+\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n+\t\t    attempted->hook_path);\n+\n+\treturn 1;\n+}\n+\n+static int notify_hook_finished(int result,\n+\t\t\t\tstruct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\n+\thook_cb->rc |= result;\n+\n+\treturn 1;\n+}\n+\n+int run_found_hooks(const char *hook_name, const char *hook_path,\n+\t\t    struct run_hooks_opt *options)\n+{\n+\tstruct hook my_hook = {\n+\t\t.hook_path = hook_path,\n+\t};\n+\tstruct hook_cb_data cb_data = {\n+\t\t.rc = 0,\n+\t\t.hook_name = hook_name,\n+\t\t.options = options,\n+\t};\n+\tcb_data.run_me = &my_hook;\n+\n+\tif (options->jobs != 1)\n+\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n+\n+\trun_processes_parallel_tr2(options->jobs,\n+\t\t\t\t   pick_next_hook,\n+\t\t\t\t   notify_start_failure,\n+\t\t\t\t   notify_hook_finished,\n+\t\t\t\t   &cb_data,\n+\t\t\t\t   \"hook\",\n+\t\t\t\t   hook_name);\n+\n+\treturn cb_data.rc;\n+}\n+\n+int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n+{\n+\tconst char *hook_path;\n+\tint ret;\n+\tif (!options)\n+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n+\n+\thook_path = find_hook(hook_name);\n+\n+\t/*\n+\t * If you need to act on a missing hook, use run_found_hooks()\n+\t * instead\n+\t */\n+\tif (!hook_path)\n+\t\treturn 0;\n+\n+\tret = run_found_hooks(hook_name, hook_path, options);\n+\treturn ret;\n+}\ndiff --git a/hook.h b/hook.h\nindex 4c547ac15e..dcc2cc2112 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,5 +1,8 @@\n #ifndef HOOK_H\n #define HOOK_H\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+#include \"run-command.h\"\n \n /*\n  * Returns the path to the hook file, or NULL if the hook is missing\n@@ -13,4 +16,54 @@ const char *find_hook(const char *name);\n  */\n int hook_exists(const char *hookname);\n \n+struct hook {\n+\t/* The path to the hook */\n+\tconst char *hook_path;\n+};\n+\n+struct run_hooks_opt\n+{\n+\t/* Environment vars to be set for each hook */\n+\tstruct strvec env;\n+\n+\t/* Args to be passed to each hook */\n+\tstruct strvec args;\n+\n+\t/*\n+\t * Number of threads to parallelize across, currently a stub,\n+\t * we use the parallel API for future-proofing, but we always\n+\t * have one hook of a given name, so this is always an\n+\t * implicit 1 for now.\n+\t */\n+\tint jobs;\n+};\n+\n+#define RUN_HOOKS_OPT_INIT { \\\n+\t.jobs = 1, \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n+struct hook_cb_data {\n+\t/* rc reflects the cumulative failure state */\n+\tint rc;\n+\tconst char *hook_name;\n+\tstruct hook *run_me;\n+\tstruct run_hooks_opt *options;\n+};\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o);\n+\n+/*\n+ * Calls find_hook(hookname) and runs the hooks (if any) with\n+ * run_found_hooks().\n+ */\n+int run_hooks(const char *hook_name, struct run_hooks_opt *options);\n+\n+/*\n+ * Takes an already resolved hook and runs it. Internally the simpler\n+ * run_hooks() will call this.\n+ */\n+int run_found_hooks(const char *hookname, const char *hook_path,\n+\t\t    struct run_hooks_opt *options);\n #endif\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nnew file mode 100755\nindex 0000000000..ecd517b162\n--- /dev/null\n+++ b/t/t1800-hook.sh\n@@ -0,0 +1,131 @@\n+#!/bin/bash\n+\n+test_description='git-hook command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'git hook usage' '\n+\ttest_expect_code 129 git hook &&\n+\ttest_expect_code 129 git hook -h &&\n+\ttest_expect_code 129 git hook run -h\n+'\n+\n+test_expect_success 'setup GIT_TEST_FAKE_HOOKS=true to permit \"test-hook\" and \"does-not-exist\" names\"' '\n+\tGIT_TEST_FAKE_HOOKS=true &&\n+\texport GIT_TEST_FAKE_HOOKS\n+'\n+\n+test_expect_success 'git hook run: nonexistent hook' '\n+\tcat >stderr.expect <<-\\EOF &&\n+\terror: cannot find a hook named test-hook\n+\tEOF\n+\ttest_expect_code 1 git hook run test-hook 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual\n+'\n+\n+test_expect_success 'git hook run: basic' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tEOF\n+\tgit hook run test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run: stdout and stderr both write to our stderr' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo >&1 Will end up on stderr\n+\techo >&2 Will end up on stderr\n+\tEOF\n+\n+\tcat >stderr.expect <<-\\EOF &&\n+\tWill end up on stderr\n+\tWill end up on stderr\n+\tEOF\n+\tgit hook run test-hook >stdout.actual 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual &&\n+\ttest_must_be_empty stdout.actual\n+'\n+\n+test_expect_success 'git hook run: exit codes are passed along' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 1\n+\tEOF\n+\n+\ttest_expect_code 1 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 2\n+\tEOF\n+\n+\ttest_expect_code 2 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 128\n+\tEOF\n+\n+\ttest_expect_code 128 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 129\n+\tEOF\n+\n+\ttest_expect_code 129 git hook run test-hook\n+'\n+\n+test_expect_success 'git hook run arg u ments without -- is not allowed' '\n+\ttest_expect_code 129 git hook run test-hook arg u ments\n+'\n+\n+test_expect_success 'git hook run -- pass arguments' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo $1\n+\techo $2\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\targ\n+\tu ments\n+\tEOF\n+\n+\tgit hook run test-hook -- arg \"u ments\" 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run -- out-of-repo runs excluded' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tnongit test_must_fail git hook run test-hook\n+'\n+\n+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n+\tmkdir my-hooks &&\n+\twrite_script my-hooks/test-hook <<-\\EOF &&\n+\techo Hook ran $1 >>actual\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tHook ran one\n+\tHook ran two\n+\tHook ran three\n+\tHook ran four\n+\tEOF\n+\n+\t# Test various ways of specifying the path. See also\n+\t# t1350-config-hooks-path.sh\n+\t>actual &&\n+\tgit hook run test-hook -- ignored 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook -- three 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook -- four 2>>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427694","messageId":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"[PATCH 00/27] Base for \"config-based-hooks\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:34Z","receivedAt":"2021-06-17T10:23:21Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This v3 of the \"Base for 'config-based-hooks'\" topic is >95% a\nslimmed-down versio nof Emily Schaffer's work to introduce a mechanism\nto drive hooks via config.\n\nThis topic doesn't do that, but moves all hook execution (C libary and\nPerl|Python script) to either the new hook.[ch] library code, or the\n\"git hook run\" utility.\n\nSee previous iterations for more details:\n\n v0 (Emily's): http://lore.kernel.org/git/20210527000856.695702-1-emilyshaffer@google.com\n v1: https://lore.kernel.org/git/cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com/\n v2: http://lore.kernel.org/git/cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com\n\nThis series gained two new dependencies since v2, my just-submitted\npreparatory topics:\n\n    https://lore.kernel.org/git/cover-0.3-0000000000-20210617T095827Z-avarab@gmail.com/\n    https://lore.kernel.org/git/cover-0.3-0000000000-20210617T100239Z-avarab@gmail.com/\n\nAnd hopefully addreses all the feedback on v2, mainly/entirely from\nEmily at https://lore.kernel.org/git/YMfLO9CT+iIDR3OA@google.com\n\nI'd normally have waited longer for a v3, but as discussed in the\nsmall hook.[ch] topic that precedes this one building the new\nhook-list.h had an error on Windows CI due to a missing CMake change,\nthat's now fixed.\n\nOther changes can be seen in the range-diff, all rather trivial fixes\nlike comment fixes, trimming off \"argc/argv\" before we pass things to\n\"git hook run\", removing a redundant test setup etc.\n\nI also added a GIT_TEST_FAKE_HOOKS=true for use in the test suite to\nmake us support a \"test-hook\" and a \"does-not-exist\" hook, we'd\npreviously accept those outside the test environment.\n\nEmily Shaffer (25):\n  hook: add 'run' subcommand\n  gc: use hook library for pre-auto-gc hook\n  rebase: teach pre-rebase to use hook.h\n  am: convert applypatch hooks to use config\n  hooks: convert 'post-checkout' hook to hook library\n  merge: use config-based hooks for post-merge hook\n  send-email: use 'git hook run' for 'sendemail-validate'\n  git-p4: use 'git hook' to run hooks\n  commit: use hook.h to execute hooks\n  read-cache: convert post-index-change hook to use config\n  receive-pack: convert push-to-checkout hook to hook.h\n  run-command: remove old run_hook_{le,ve}() hook API\n  run-command: allow stdin for run_processes_parallel\n  hook: support passing stdin to hooks\n  am: convert 'post-rewrite' hook to hook.h\n  run-command: add stdin callback for parallelization\n  hook: provide stdin by string_list or callback\n  hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n  transport: convert pre-push hook to use config\n  reference-transaction: use hook.h to run hooks\n  run-command: allow capturing of collated output\n  hooks: allow callers to capture output\n  receive-pack: convert 'update' hook to hook.h\n  post-update: use hook.h library\n  receive-pack: convert receive hooks to hook.h\n\nÆvar Arnfjörð Bjarmason (2):\n  git hook run: add an --ignore-missing flag\n  hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n\n .gitignore                  |   1 +\n Documentation/git-hook.txt  |  51 ++++++\n Documentation/githooks.txt  |   4 +\n Makefile                    |   1 +\n builtin.h                   |   1 +\n builtin/am.c                |  33 ++--\n builtin/checkout.c          |  17 +-\n builtin/clone.c             |   7 +-\n builtin/commit.c            |  18 ++-\n builtin/fetch.c             |   1 +\n builtin/gc.c                |   8 +-\n builtin/hook.c              |  90 +++++++++++\n builtin/merge.c             |  21 ++-\n builtin/rebase.c            |   9 +-\n builtin/receive-pack.c      | 298 ++++++++++++++++++------------------\n builtin/submodule--helper.c |   2 +-\n builtin/worktree.c          |  30 ++--\n command-list.txt            |   1 +\n commit.c                    |  17 +-\n commit.h                    |   3 +-\n git-p4.py                   |  72 +--------\n git-send-email.perl         |  20 ++-\n git.c                       |   1 +\n hook.c                      | 170 ++++++++++++++++++++\n hook.h                      | 109 +++++++++++++\n read-cache.c                |  12 +-\n refs.c                      |  42 ++---\n reset.c                     |  15 +-\n run-command.c               | 122 ++++++++++-----\n run-command.h               |  48 ++++--\n sequencer.c                 |  87 +++++------\n submodule.c                 |   1 +\n t/helper/test-run-command.c |  46 +++++-\n t/t0061-run-command.sh      |  37 +++++\n t/t1800-hook.sh             | 154 +++++++++++++++++++\n t/t9001-send-email.sh       |   4 +-\n transport.c                 |  57 ++-----\n 37 files changed, 1136 insertions(+), 474 deletions(-)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100755 t/t1800-hook.sh\n\nRange-diff:\n 1:  447d349c73 !  1:  cf4b06bfdf hook: add 'run' subcommand\n    @@ Documentation/git-hook.txt (new)\n     +-----------\n     +\n     +run::\n    -+\n    -+\tRun the `<hook-name>` hook. Any positional arguments to the\n    -+\thook should be passed after an optional \"--\" (or\n    -+\t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n    -+\tthis accepts.\n    ++\tRun the `<hook-name>` hook. See linkgit:githooks[5] for\n    ++\tthe hook names we support.\n    +++\n    ++Any positional arguments to the hook should be passed after an\n    ++optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n    ++arguments (if any) differ by hook name, see linkgit:githooks[5] for\n    ++what those are.\n     +\n     +SEE ALSO\n     +--------\n    @@ Documentation/githooks.txt: and \"0\" meaning they were not.\n      Part of the linkgit:git[1] suite\n     \n      ## Makefile ##\n    -@@ Makefile: LIB_OBJS += hash-lookup.o\n    - LIB_OBJS += hashmap.o\n    - LIB_OBJS += help.o\n    - LIB_OBJS += hex.o\n    -+LIB_OBJS += hook.o\n    - LIB_OBJS += ident.o\n    - LIB_OBJS += json-writer.o\n    - LIB_OBJS += kwset.o\n     @@ Makefile: BUILTIN_OBJS += builtin/get-tar-commit-id.o\n      BUILTIN_OBJS += builtin/grep.o\n      BUILTIN_OBJS += builtin/hash-object.o\n    @@ builtin/hook.c (new)\n     +#include \"strvec.h\"\n     +\n     +static const char * const builtin_hook_usage[] = {\n    ++\tN_(\"git hook <command> [...]\"),\n    ++\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n    ++\tNULL\n    ++};\n    ++\n    ++static const char * const builtin_hook_run_usage[] = {\n     +\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n     +\tNULL\n     +};\n    @@ builtin/hook.c (new)\n     +\t};\n     +\n     +\targc = parse_options(argc, argv, prefix, run_options,\n    -+\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n    ++\t\t\t     builtin_hook_run_usage,\n    ++\t\t\t     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n     +\n    -+\tif (argc > 2) {\n    -+\t\tif (strcmp(argv[2], \"--\") &&\n    -+\t\t    strcmp(argv[2], \"--end-of-options\"))\n    ++\tif (argc > 1) {\n    ++\t\tif (strcmp(argv[1], \"--\") &&\n    ++\t\t    strcmp(argv[1], \"--end-of-options\"))\n     +\t\t\t/* Having a -- for \"run\" is mandatory */\n     +\t\t\tusage_with_options(builtin_hook_usage, run_options);\n     +\t\t/* Add our arguments, start after -- */\n    -+\t\tfor (i = 3 ; i < argc; i++)\n    ++\t\tfor (i = 2 ; i < argc; i++)\n     +\t\t\tstrvec_push(&opt.args, argv[i]);\n     +\t}\n     +\n     +\t/* Need to take into account core.hooksPath */\n     +\tgit_config(git_default_config, NULL);\n     +\n    -+\thook_name = argv[1];\n    ++\t/*\n    ++\t * We are not using run_hooks() because we'd like to detect\n    ++\t * missing hooks. Let's find it ourselves and call\n    ++\t * run_found_hooks() instead.\n    ++\t */\n    ++\thook_name = argv[0];\n     +\thook_path = find_hook(hook_name);\n     +\tif (!hook_path) {\n     +\t\terror(\"cannot find a hook named %s\", hook_name);\n    @@ builtin/hook.c (new)\n     +\tstruct option builtin_hook_options[] = {\n     +\t\tOPT_END(),\n     +\t};\n    ++\targc = parse_options(argc, argv, NULL, builtin_hook_options,\n    ++\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n    ++\tif (!argc)\n    ++\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n     +\n    -+\tif (!strcmp(argv[1], \"run\"))\n    ++\tif (!strcmp(argv[0], \"run\"))\n     +\t\treturn run(argc, argv, prefix);\n    -+\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n    -+\treturn 1;\n    ++\telse\n    ++\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n     +}\n     \n      ## command-list.txt ##\n    @@ git.c: static struct cmd_struct commands[] = {\n      \t{ \"init\", cmd_init_db },\n      \t{ \"init-db\", cmd_init_db },\n     \n    - ## hook.c (new) ##\n    + ## hook.c ##\n     @@\n    -+#include \"cache.h\"\n    -+#include \"hook.h\"\n    -+#include \"run-command.h\"\n    + #include \"hook.h\"\n    + #include \"run-command.h\"\n    + #include \"hook-list.h\"\n    ++#include \"config.h\"\n    + \n    + static int known_hook(const char *name)\n    + {\n    + \tconst char **p;\n    + \tsize_t len = strlen(name);\n    ++\tstatic int test_hooks_ok = -1;\n    ++\n    + \tfor (p = hook_name_list; *p; p++) {\n    + \t\tconst char *hook = *p;\n    + \n    +@@ hook.c: static int known_hook(const char *name)\n    + \t\t\treturn 1;\n    + \t}\n    + \n    ++\tif (test_hooks_ok == -1)\n    ++\t\ttest_hooks_ok = git_env_bool(\"GIT_TEST_FAKE_HOOKS\", 0);\n    ++\n    ++\tif (test_hooks_ok &&\n    ++\t    (!strcmp(name, \"test-hook\") ||\n    ++\t     !strcmp(name, \"does-not-exist\")))\n    ++\t\treturn 1;\n    ++\n    + \treturn 0;\n    + }\n    + \n    +@@ hook.c: int hook_exists(const char *name)\n    + {\n    + \treturn !!find_hook(name);\n    + }\n     +\n     +void run_hooks_opt_clear(struct run_hooks_opt *o)\n     +{\n    @@ hook.c (new)\n     +\tstruct hook_cb_data *hook_cb = pp_cb;\n     +\tstruct hook *run_me = hook_cb->run_me;\n     +\n    -+\tif (!run_me)\n    -+\t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n    -+\n     +\tcp->no_stdin = 1;\n     +\tcp->env = hook_cb->options->env.v;\n     +\tcp->stdout_to_stderr = 1;\n    @@ hook.c (new)\n     +\tstruct hook_cb_data *hook_cb = pp_cb;\n     +\tstruct hook *attempted = pp_task_cp;\n     +\n    -+\t/* |= rc in cb */\n     +\thook_cb->rc |= 1;\n     +\n     +\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n    @@ hook.c (new)\n     +{\n     +\tstruct hook_cb_data *hook_cb = pp_cb;\n     +\n    -+\t/* |= rc in cb */\n     +\thook_cb->rc |= result;\n     +\n     +\treturn 1;\n     +}\n     +\n    -+\n     +int run_found_hooks(const char *hook_name, const char *hook_path,\n     +\t\t    struct run_hooks_opt *options)\n     +{\n    @@ hook.c (new)\n     +\n     +\thook_path = find_hook(hook_name);\n     +\n    -+\t/* Care about nonexistence? Use run_found_hooks() */\n    ++\t/*\n    ++\t * If you need to act on a missing hook, use run_found_hooks()\n    ++\t * instead\n    ++\t */\n     +\tif (!hook_path)\n     +\t\treturn 0;\n     +\n    @@ hook.c (new)\n     +\treturn ret;\n     +}\n     \n    - ## hook.h (new) ##\n    + ## hook.h ##\n     @@\n    -+#ifndef HOOK_H\n    -+#define HOOK_H\n    + #ifndef HOOK_H\n    + #define HOOK_H\n     +#include \"strbuf.h\"\n     +#include \"strvec.h\"\n     +#include \"run-command.h\"\n    -+\n    + \n    + /*\n    +  * Returns the path to the hook file, or NULL if the hook is missing\n    +@@ hook.h: const char *find_hook(const char *name);\n    +  */\n    + int hook_exists(const char *hookname);\n    + \n     +struct hook {\n     +\t/* The path to the hook */\n     +\tconst char *hook_path;\n    @@ hook.h (new)\n     +\t/* Args to be passed to each hook */\n     +\tstruct strvec args;\n     +\n    -+\t/* Number of threads to parallelize across */\n    ++\t/*\n    ++\t * Number of threads to parallelize across, currently a stub,\n    ++\t * we use the parallel API for future-proofing, but we always\n    ++\t * have one hook of a given name, so this is always an\n    ++\t * implicit 1 for now.\n    ++\t */\n     +\tint jobs;\n     +};\n     +\n    @@ hook.h (new)\n     +\t.args = STRVEC_INIT, \\\n     +}\n     +\n    -+/*\n    -+ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n    -+ */\n     +struct hook_cb_data {\n    ++\t/* rc reflects the cumulative failure state */\n     +\tint rc;\n     +\tconst char *hook_name;\n     +\tstruct hook *run_me;\n    @@ hook.h (new)\n     + */\n     +int run_found_hooks(const char *hookname, const char *hook_path,\n     +\t\t    struct run_hooks_opt *options);\n    -+#endif\n    + #endif\n     \n      ## t/t1800-hook.sh (new) ##\n     @@\n    @@ t/t1800-hook.sh (new)\n     +\n     +. ./test-lib.sh\n     +\n    -+test_expect_success 'git hook run -- nonexistent hook' '\n    ++test_expect_success 'git hook usage' '\n    ++\ttest_expect_code 129 git hook &&\n    ++\ttest_expect_code 129 git hook -h &&\n    ++\ttest_expect_code 129 git hook run -h\n    ++'\n    ++\n    ++test_expect_success 'setup GIT_TEST_FAKE_HOOKS=true to permit \"test-hook\" and \"does-not-exist\" names\"' '\n    ++\tGIT_TEST_FAKE_HOOKS=true &&\n    ++\texport GIT_TEST_FAKE_HOOKS\n    ++'\n    ++\n    ++test_expect_success 'git hook run: nonexistent hook' '\n     +\tcat >stderr.expect <<-\\EOF &&\n    -+\terror: cannot find a hook named does-not-exist\n    ++\terror: cannot find a hook named test-hook\n     +\tEOF\n    -+\ttest_expect_code 1 git hook run does-not-exist 2>stderr.actual &&\n    ++\ttest_expect_code 1 git hook run test-hook 2>stderr.actual &&\n     +\ttest_cmp stderr.expect stderr.actual\n     +'\n     +\n    -+test_expect_success 'git hook run -- basic' '\n    ++test_expect_success 'git hook run: basic' '\n     +\twrite_script .git/hooks/test-hook <<-EOF &&\n     +\techo Test hook\n     +\tEOF\n    @@ t/t1800-hook.sh (new)\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success 'git hook run -- stdout and stderr handling' '\n    ++test_expect_success 'git hook run: stdout and stderr both write to our stderr' '\n     +\twrite_script .git/hooks/test-hook <<-EOF &&\n     +\techo >&1 Will end up on stderr\n     +\techo >&2 Will end up on stderr\n    @@ t/t1800-hook.sh (new)\n     +\ttest_must_be_empty stdout.actual\n     +'\n     +\n    -+test_expect_success 'git hook run -- exit codes are passed along' '\n    ++test_expect_success 'git hook run: exit codes are passed along' '\n     +\twrite_script .git/hooks/test-hook <<-EOF &&\n     +\texit 1\n     +\tEOF\n    @@ t/t1800-hook.sh (new)\n     +\n     +test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n     +\tmkdir my-hooks &&\n    -+\twrite_script my-hooks/test-hook <<-EOF &&\n    -+\techo Hook ran >>actual\n    ++\twrite_script my-hooks/test-hook <<-\\EOF &&\n    ++\techo Hook ran $1 >>actual\n     +\tEOF\n     +\n     +\tcat >expect <<-\\EOF &&\n     +\tTest hook\n    -+\tHook ran\n    -+\tHook ran\n    -+\tHook ran\n    -+\tHook ran\n    ++\tHook ran one\n    ++\tHook ran two\n    ++\tHook ran three\n    ++\tHook ran four\n     +\tEOF\n     +\n     +\t# Test various ways of specifying the path. See also\n     +\t# t1350-config-hooks-path.sh\n     +\t>actual &&\n    -+\tgit hook run test-hook 2>>actual &&\n    -+\tgit -c core.hooksPath=my-hooks hook run test-hook 2>>actual &&\n    -+\tgit -c core.hooksPath=my-hooks/ hook run test-hook 2>>actual &&\n    -+\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook 2>>actual &&\n    -+\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook 2>>actual &&\n    ++\tgit hook run test-hook -- ignored 2>>actual &&\n    ++\tgit -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&\n    ++\tgit -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&\n    ++\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook -- three 2>>actual &&\n    ++\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook -- four 2>>actual &&\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n    -+\t>actual &&\n    -+\tmkdir -p .git/custom-hooks .git/hooks &&\n    -+\twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n    -+\techo CUSTOM >>actual\n    -+\tEOF\n    -+\twrite_script .git/hooks/pre-commit <<-\\EOF\n    -+\techo NORMAL >>actual\n    -+\tEOF\n    -+'\n    -+\n     +test_done\n 2:  85195a78cf <  -:  ---------- run-command.h: move find_hook() to hook.h\n 3:  eb5bdd993c <  -:  ---------- hook.c: add a hook_exists() wrapper and use it in bugreport.c\n 4:  da2763192a =  2:  7209f73f28 gc: use hook library for pre-auto-gc hook\n 5:  51e6e72f23 =  3:  e9a1e7cf61 rebase: teach pre-rebase to use hook.h\n 6:  d2f3b26d46 =  4:  1d08726930 am: convert applypatch hooks to use config\n 7:  d884465aab !  5:  32eec5dc2f hooks: convert 'post-checkout' hook to hook library\n    @@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n     \n      ## hook.h ##\n     @@ hook.h: struct run_hooks_opt\n    - \n    - \t/* Number of threads to parallelize across */\n    + \t * implicit 1 for now.\n    + \t */\n      \tint jobs;\n     +\n     +\t/* Resolve and run the \"absolute_path(hook)\" instead of\n 8:  ad6e750784 =  6:  e9fa3f6759 merge: use config-based hooks for post-merge hook\n 9:  8d8b2d2645 !  7:  12347d901b git hook run: add an --ignore-missing flag\n    @@ Documentation/git-hook.txt: git-hook - run git hooks\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-hook.txt: run::\n    - \t\"--end-of-options\"). See \"OPTIONS\" below for the arguments\n    - \tthis accepts.\n    +@@ Documentation/git-hook.txt: optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n    + arguments (if any) differ by hook name, see linkgit:githooks[5] for\n    + what those are.\n      \n     +OPTIONS\n     +-------\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \t};\n      \n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n    - \thook_name = argv[1];\n    + \t/*\n    + \t * We are not using run_hooks() because we'd like to detect\n    + \t * missing hooks. Let's find it ourselves and call\n    +-\t * run_found_hooks() instead.\n    ++\t * run_found_hooks() instead...\n    + \t */\n    + \thook_name = argv[0];\n      \thook_path = find_hook(hook_name);\n      \tif (!hook_path) {\n    ++\t\t/* ... act like run_hooks() under --ignore-missing */\n     +\t\tif (ignore_missing)\n     +\t\t\treturn 0;\n      \t\terror(\"cannot find a hook named %s\", hook_name);\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \t}\n     \n      ## t/t1800-hook.sh ##\n    -@@ t/t1800-hook.sh: test_expect_success 'git hook run -- nonexistent hook' '\n    +@@ t/t1800-hook.sh: test_expect_success 'git hook run: nonexistent hook' '\n      \ttest_cmp stderr.expect stderr.actual\n      '\n      \n    -+test_expect_success 'git hook run -- nonexistent hook with --ignore-missing' '\n    +-test_expect_success 'git hook run: basic' '\n    ++test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '\n     +\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n     +\ttest_must_be_empty stderr.actual\n     +'\n     +\n    - test_expect_success 'git hook run -- basic' '\n    ++test_expect_success 'git hook run -- basic' '\n      \twrite_script .git/hooks/test-hook <<-EOF &&\n      \techo Test hook\n    + \tEOF\n10:  1953326d1d =  8:  71d209b407 send-email: use 'git hook run' for 'sendemail-validate'\n11:  aa970a8175 !  9:  246a82b55b git-p4: use 'git hook' to run hooks\n    @@ Commit message\n         Python, we can directly call 'git hook run'. We emulate the existence\n         check with the --ignore-missing flag.\n     \n    +    As this is the last hook execution in git.git to not go through \"git\n    +    hook run\" or the hook.[ch] library we can now be absolutely sure that\n    +    our assertion in hook.c that only hooks known by the generated (from\n    +    githooks(5)) hook-list.h are permitted.\n    +\n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n12:  7756f10aac = 10:  e3f8482d80 commit: use hook.h to execute hooks\n13:  0300607a9b = 11:  6ed61071c5 read-cache: convert post-index-change hook to use config\n14:  ec4ad437f8 = 12:  e4ef3f4548 receive-pack: convert push-to-checkout hook to hook.h\n15:  477eb2245c = 13:  e3dda367ec run-command: remove old run_hook_{le,ve}() hook API\n16:  53a3877a47 = 14:  477d75bf57 run-command: allow stdin for run_processes_parallel\n17:  c4f60db606 ! 15:  b7c0ee9719 hook: support passing stdin to hooks\n    @@ Documentation/git-hook.txt: git-hook - run git hooks\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-hook.txt: run::\n    +@@ Documentation/git-hook.txt: what those are.\n      OPTIONS\n      -------\n      \n    @@ Documentation/git-hook.txt: run::\n     \n      ## builtin/hook.c ##\n     @@\n    - #include \"strvec.h\"\n      \n      static const char * const builtin_hook_usage[] = {\n    + \tN_(\"git hook <command> [...]\"),\n     -\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n    ++\tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n    + \tNULL\n    + };\n    + \n    + static const char * const builtin_hook_run_usage[] = {\n    + \tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n     +\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n      \tNULL\n      };\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n     \n      ## hook.c ##\n     @@ hook.c: static int pick_next_hook(struct child_process *cp,\n    - \tif (!run_me)\n    - \t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n    + \tstruct hook_cb_data *hook_cb = pp_cb;\n    + \tstruct hook *run_me = hook_cb->run_me;\n      \n     -\tcp->no_stdin = 1;\n    ++\n     +\t/* reopen the file for stdin; run_command closes it. */\n     +\tif (hook_cb->options->path_to_stdin) {\n     +\t\tcp->no_stdin = 0;\n    @@ hook.h: struct run_hooks_opt\n      #define RUN_HOOKS_OPT_INIT { \\\n     \n      ## t/t1800-hook.sh ##\n    -@@ t/t1800-hook.sh: test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n    - \tEOF\n    +@@ t/t1800-hook.sh: test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n    + \ttest_cmp expect actual\n      '\n      \n     +test_expect_success 'stdin to hooks' '\n18:  febf05ef23 = 16:  4035069a98 am: convert 'post-rewrite' hook to hook.h\n19:  7baf2469d5 = 17:  c847a19581 run-command: add stdin callback for parallelization\n20:  2edf9dea41 ! 18:  da46c859c1 hook: provide stdin by string_list or callback\n    @@ hook.c: static int pick_next_hook(struct child_process *cp,\n      \t} else {\n      \t\tcp->no_stdin = 1;\n      \t}\n    -@@ hook.c: static int notify_hook_finished(int result,\n    - \treturn 1;\n    - }\n    - \n    --\n    - int run_found_hooks(const char *hook_name, const char *hook_path,\n    - \t\t    struct run_hooks_opt *options)\n    - {\n     @@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n      \trun_processes_parallel_tr2(options->jobs,\n      \t\t\t\t   pick_next_hook,\n    @@ hook.c: int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n     +\n      \thook_path = find_hook(hook_name);\n      \n    - \t/* Care about nonexistence? Use run_found_hooks() */\n    + \t/*\n     \n      ## hook.h ##\n    -@@\n    +@@ hook.h: int hook_exists(const char *hookname);\n      struct hook {\n      \t/* The path to the hook */\n      \tconst char *hook_path;\n    @@ hook.h: struct run_hooks_opt\n     + */\n     +int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n     +\n    - /*\n    -  * Callback provided to feed_pipe_fn and consume_sideband_fn.\n    -  */\n    + struct hook_cb_data {\n    + \t/* rc reflects the cumulative failure state */\n    + \tint rc;\n21:  303b31ee62 = 19:  7343be28ef hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n22:  62eecafb3f = 20:  85bf13a083 transport: convert pre-push hook to use config\n23:  6049b1cdc7 = 21:  331014bad1 reference-transaction: use hook.h to run hooks\n24:  26ebbe4c54 = 22:  f7f56d0a3b run-command: allow capturing of collated output\n25:  251085b752 = 23:  7f7fcc0688 hooks: allow callers to capture output\n26:  e11a8f6007 = 24:  e74d49e559 receive-pack: convert 'update' hook to hook.h\n27:  ecaedd13b8 = 25:  0bdc4878ac post-update: use hook.h library\n28:  bb9d57f809 = 26:  db70b59b3b receive-pack: convert receive hooks to hook.h\n29:  793f112f7a ! 27:  d86fedf041 hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n    @@ commit.h: int compare_commits_by_commit_date(const void *a_, const void *b_, voi\n     \n      ## hook.c ##\n     @@ hook.c: static int notify_hook_finished(int result,\n    - \t/* |= rc in cb */\n    + \n      \thook_cb->rc |= result;\n      \n     +\tif (hook_cb->invoked_hook)\n    @@ hook.h: struct hook_cb_data {\n     +\tint *invoked_hook;\n      };\n      \n    - /*\n    + void run_hooks_opt_clear(struct run_hooks_opt *o);\n     \n      ## sequencer.c ##\n     @@ sequencer.c: static int run_prepare_commit_msg_hook(struct repository *r,\n30:  bc086454d6 <  -:  ---------- hook-list.h: add a generated list of hooks, like config-list.h\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427695","messageId":"patch-04.27-1d08726930-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 04/27] am: convert applypatch hooks to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:38Z","receivedAt":"2021-06-17T10:23:24Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach pre-applypatch, post-applypatch, and applypatch-msg to use the\nhook.h library instead of the run-command.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 15 ++++++++++++---\n 1 file changed, 12 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 1c8a548903..9e9c1b5e9f 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -445,9 +445,12 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tassert(state->msg);\n-\tret = run_hook_le(NULL, \"applypatch-msg\", am_path(state, \"final-commit\"), NULL);\n+\tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n+\tret = run_hooks(\"applypatch-msg\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tif (!ret) {\n \t\tFREE_AND_NULL(state->msg);\n@@ -1607,9 +1610,13 @@ static void do_commit(const struct am_state *state)\n \tstruct commit_list *parents = NULL;\n \tconst char *reflog_msg, *author, *committer = NULL;\n \tstruct strbuf sb = STRBUF_INIT;\n+\tstruct run_hooks_opt hook_opt_pre = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt_post = RUN_HOOKS_OPT_INIT;\n \n-\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n+\tif (run_hooks(\"pre-applypatch\", &hook_opt_pre)) {\n+\t\trun_hooks_opt_clear(&hook_opt_pre);\n \t\texit(1);\n+\t}\n \n \tif (write_cache_as_tree(&tree, 0, NULL))\n \t\tdie(_(\"git write-tree failed to write a tree\"));\n@@ -1660,8 +1667,10 @@ static void do_commit(const struct am_state *state)\n \t\tfclose(fp);\n \t}\n \n-\trun_hook_le(NULL, \"post-applypatch\", NULL);\n+\trun_hooks(\"post-applypatch\", &hook_opt_post);\n \n+\trun_hooks_opt_clear(&hook_opt_pre);\n+\trun_hooks_opt_clear(&hook_opt_post);\n \tstrbuf_release(&sb);\n }\n \n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427696","messageId":"patch-06.27-e9fa3f6759-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 06/27] merge: use config-based hooks for post-merge hook","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:40Z","receivedAt":"2021-06-17T10:23:25Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach post-merge to use the hook.h library instead of the run-command.h\nlibrary to run hooks. This means that post-merge hooks can come from the\nconfig as well as from the hookdir. post-merge is invoked only from\nbuiltin/merge.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/merge.c | 7 +++++--\n 1 file changed, 5 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex be98d66b0a..6128b60942 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,6 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\n@@ -489,7 +490,9 @@ static void finish(struct commit *head_commit,\n \t}\n \n \t/* Run a post-merge hook */\n-\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n+\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n+\trun_hooks(\"post-merge\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \tapply_autostash(git_path_merge_autostash(the_repository));\n \tstrbuf_release(&reflog_message);\n@@ -849,7 +852,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t * and write it out as a tree.  We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (find_hook(\"pre-merge-commit\"))\n+\tif (hook_exists(\"pre-merge-commit\"))\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427697","messageId":"patch-02.27-7209f73f28-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 02/27] gc: use hook library for pre-auto-gc hook","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:36Z","receivedAt":"2021-06-17T10:23:33Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nUsing the hook.h library instead of the run-command.h library to run\npre-auto-gc means that those hooks can be set up in config files, as\nwell as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c | 8 +++++++-\n 1 file changed, 7 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1a..a12641a691 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -32,6 +32,7 @@\n #include \"remote.h\"\n #include \"object-store.h\"\n #include \"exec-cmd.h\"\n+#include \"hook.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -348,6 +349,8 @@ static void add_repack_incremental_option(void)\n \n static int need_to_gc(void)\n {\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n \t * automatic gc.\n@@ -394,8 +397,11 @@ static int need_to_gc(void)\n \telse\n \t\treturn 0;\n \n-\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n+\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\treturn 0;\n+\t}\n+\trun_hooks_opt_clear(&hook_opt);\n \treturn 1;\n }\n \n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427698","messageId":"patch-03.27-e9a1e7cf61-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 03/27] rebase: teach pre-rebase to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:37Z","receivedAt":"2021-06-17T10:23:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the pre-rebase hook away from run-command.h to and over to the\nnew hook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/rebase.c | 9 +++++++--\n 1 file changed, 7 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 12f093121d..2081f6fa8d 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -28,6 +28,7 @@\n #include \"sequencer.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_REFLOG_ACTION \"rebase\"\n \n@@ -1313,6 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\n@@ -2022,10 +2024,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t}\n \n \t/* If a hook exists, give it a chance to interrupt*/\n+\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n \tif (!ok_to_skip_pre_rebase &&\n-\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n-\t\t\targc ? argv[0] : NULL, NULL))\n+\t    run_hooks(\"pre-rebase\", &hook_opt)) {\n+\t\trun_hooks_opt_clear(&hook_opt);\n \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n+\t}\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (options.flags & REBASE_DIFFSTAT) {\n \t\tstruct diff_options opts;\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427699","messageId":"patch-05.27-32eec5dc2f-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 05/27] hooks: convert 'post-checkout' hook to hook library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:39Z","receivedAt":"2021-06-17T10:23:41Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the running of the 'post-checkout' hook away from run-command.h\nto the new hook.h library. For \"worktree\" this requires a change to it\nto run the hooks from a given directory.\n\nWe could strictly speaking skip the \"absolute_path\" flag and just\ncheck if \"dir\" is specified, but let's split them up for clarity, as\nwell as for any future user who'd like to set \"dir\" but not implicitly\nchange the argument to an absolute path.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/checkout.c | 17 ++++++++++++-----\n builtin/clone.c    |  7 +++++--\n builtin/worktree.c | 30 ++++++++++++++----------------\n hook.c             |  8 ++++++++\n hook.h             |  9 +++++++++\n read-cache.c       |  1 +\n reset.c            | 15 +++++++++++----\n 7 files changed, 60 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f4cd7747d3..6205ace09f 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"diff.h\"\n #include \"dir.h\"\n+#include \"hook.h\"\n #include \"ll-merge.h\"\n #include \"lockfile.h\"\n #include \"merge-recursive.h\"\n@@ -106,13 +107,19 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\treturn run_hook_le(NULL, \"post-checkout\",\n-\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n-\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n-\t\t\t   changed ? \"1\" : \"0\", NULL);\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint rc;\n+\n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\n-\n+\tstrvec_pushl(&opt.args,\n+\t\t     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n+\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n+\t\t     changed ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trc = run_hooks(\"post-checkout\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n }\n \n static int update_some(const struct object_id *oid, struct strbuf *base,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 66fe66679c..de57a3119b 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -32,6 +32,7 @@\n #include \"connected.h\"\n #include \"packfile.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"hook.h\"\n \n /*\n  * Overall FIXMEs:\n@@ -775,6 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tif (option_no_checkout)\n \t\treturn 0;\n@@ -820,8 +822,9 @@ static int checkout(int submodule_progress)\n \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n \t\tdie(_(\"unable to write new index file\"));\n \n-\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(null_oid()),\n-\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n+\tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n+\terr |= run_hooks(\"post-checkout\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n \n \tif (!err && (option_recurse_submodules.nr > 0)) {\n \t\tstruct strvec args = STRVEC_INIT;\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex b1350640fe..2ad26a76f4 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -382,22 +382,20 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tconst char *hook = find_hook(\"post-checkout\");\n-\t\tif (hook) {\n-\t\t\tconst char *env[] = { \"GIT_DIR\", \"GIT_WORK_TREE\", NULL };\n-\t\t\tcp.git_cmd = 0;\n-\t\t\tcp.no_stdin = 1;\n-\t\t\tcp.stdout_to_stderr = 1;\n-\t\t\tcp.dir = path;\n-\t\t\tcp.env = env;\n-\t\t\tcp.argv = NULL;\n-\t\t\tcp.trace2_hook_name = \"post-checkout\";\n-\t\t\tstrvec_pushl(&cp.args, absolute_path(hook),\n-\t\t\t\t     oid_to_hex(null_oid()),\n-\t\t\t\t     oid_to_hex(&commit->object.oid),\n-\t\t\t\t     \"1\", NULL);\n-\t\t\tret = run_command(&cp);\n-\t\t}\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(null_oid()),\n+\t\t\t     oid_to_hex(&commit->object.oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\topt.dir = path;\n+\t\topt.absolute_path = 1;\n+\n+\t\tret = run_hooks(\"post-checkout\", &opt);\n+\n+\t\trun_hooks_opt_clear(&opt);\n \t}\n \n \tstrvec_clear(&child_env);\ndiff --git a/hook.c b/hook.c\nindex 4d87e98255..cb39404ac8 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -89,6 +89,7 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\n+\tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n \tstrvec_push(&cp->args, run_me->hook_path);\n@@ -135,6 +136,7 @@ static int notify_hook_finished(int result,\n int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t    struct run_hooks_opt *options)\n {\n+\tstruct strbuf abs_path = STRBUF_INIT;\n \tstruct hook my_hook = {\n \t\t.hook_path = hook_path,\n \t};\n@@ -143,6 +145,10 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t.hook_name = hook_name,\n \t\t.options = options,\n \t};\n+\tif (options->absolute_path) {\n+\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n+\t\tmy_hook.hook_path = abs_path.buf;\n+\t}\n \tcb_data.run_me = &my_hook;\n \n \tif (options->jobs != 1)\n@@ -155,6 +161,8 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n+\tif (options->absolute_path)\n+\t\tstrbuf_release(&abs_path);\n \n \treturn cb_data.rc;\n }\ndiff --git a/hook.h b/hook.h\nindex dcc2cc2112..8bd0fc8d1f 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -36,6 +36,15 @@ struct run_hooks_opt\n \t * implicit 1 for now.\n \t */\n \tint jobs;\n+\n+\t/* Resolve and run the \"absolute_path(hook)\" instead of\n+\t * \"hook\". Used for \"git worktree\" hooks\n+\t */\n+\tint absolute_path;\n+\n+\t/* Path to initial working directory for subprocess */\n+\tconst char *dir;\n+\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/read-cache.c b/read-cache.c\nindex 77961a3885..af5b97104c 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -27,6 +27,7 @@\n #include \"progress.h\"\n #include \"sparse-index.h\"\n #include \"csum-file.h\"\n+#include \"hook.h\"\n \n /* Mask for the name length in ce_flags in the on-disk index */\n \ndiff --git a/reset.c b/reset.c\nindex 4bea758053..e6af33b901 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -7,6 +7,7 @@\n #include \"tree-walk.h\"\n #include \"tree.h\"\n #include \"unpack-trees.h\"\n+#include \"hook.h\"\n \n int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t       const char *switch_to_branch, unsigned flags,\n@@ -126,10 +127,16 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\tret = create_symref(\"HEAD\", switch_to_branch,\n \t\t\t\t\t    reflog_head);\n \t}\n-\tif (run_hook)\n-\t\trun_hook_le(NULL, \"post-checkout\",\n-\t\t\t    oid_to_hex(orig ? orig : null_oid()),\n-\t\t\t    oid_to_hex(oid), \"1\", NULL);\n+\tif (run_hook) {\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(orig ? orig : null_oid()),\n+\t\t\t     oid_to_hex(oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\trun_hooks(\"post-checkout\", &opt);\n+\t\trun_hooks_opt_clear(&opt);\n+\t}\n \n leave_reset_head:\n \tstrbuf_release(&msg);\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427700","messageId":"patch-09.27-246a82b55b-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 09/27] git-p4: use 'git hook' to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:43Z","receivedAt":"2021-06-17T10:23:43Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nInstead of duplicating the behavior of run-command.h:run_hook_le() in\nPython, we can directly call 'git hook run'. We emulate the existence\ncheck with the --ignore-missing flag.\n\nAs this is the last hook execution in git.git to not go through \"git\nhook run\" or the hook.[ch] library we can now be absolutely sure that\nour assertion in hook.c that only hooks known by the generated (from\ngithooks(5)) hook-list.h are permitted.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-p4.py | 72 ++++++-------------------------------------------------\n 1 file changed, 7 insertions(+), 65 deletions(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex d34a1946b7..e76d8df313 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -207,71 +207,13 @@ def decode_path(path):\n         return path\n \n def run_git_hook(cmd, param=[]):\n-    \"\"\"Execute a hook if the hook exists.\"\"\"\n-    if verbose:\n-        sys.stderr.write(\"Looking for hook: %s\\n\" % cmd)\n-        sys.stderr.flush()\n-\n-    hooks_path = gitConfig(\"core.hooksPath\")\n-    if len(hooks_path) <= 0:\n-        hooks_path = os.path.join(os.environ[\"GIT_DIR\"], \"hooks\")\n-\n-    if not isinstance(param, list):\n-        param=[param]\n-\n-    # resolve hook file name, OS depdenent\n-    hook_file = os.path.join(hooks_path, cmd)\n-    if platform.system() == 'Windows':\n-        if not os.path.isfile(hook_file):\n-            # look for the file with an extension\n-            files = glob.glob(hook_file + \".*\")\n-            if not files:\n-                return True\n-            files.sort()\n-            hook_file = files.pop()\n-            while hook_file.upper().endswith(\".SAMPLE\"):\n-                # The file is a sample hook. We don't want it\n-                if len(files) > 0:\n-                    hook_file = files.pop()\n-                else:\n-                    return True\n-\n-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):\n-        return True\n-\n-    return run_hook_command(hook_file, param) == 0\n-\n-def run_hook_command(cmd, param):\n-    \"\"\"Executes a git hook command\n-       cmd = the command line file to be executed. This can be\n-       a file that is run by OS association.\n-\n-       param = a list of parameters to pass to the cmd command\n-\n-       On windows, the extension is checked to see if it should\n-       be run with the Git for Windows Bash shell.  If there\n-       is no file extension, the file is deemed a bash shell\n-       and will be handed off to sh.exe. Otherwise, Windows\n-       will be called with the shell to handle the file assocation.\n-\n-       For non Windows operating systems, the file is called\n-       as an executable.\n-    \"\"\"\n-    cli = [cmd] + param\n-    use_shell = False\n-    if platform.system() == 'Windows':\n-        (root,ext) = os.path.splitext(cmd)\n-        if ext == \"\":\n-            exe_path = os.environ.get(\"EXEPATH\")\n-            if exe_path is None:\n-                exe_path = \"\"\n-            else:\n-                exe_path = os.path.join(exe_path, \"bin\")\n-            cli = [os.path.join(exe_path, \"SH.EXE\")] + cli\n-        else:\n-            use_shell = True\n-    return subprocess.call(cli, shell=use_shell)\n-\n+    \"\"\"args are specified with -a <arg> -a <arg> -a <arg>\"\"\"\n+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]\n+    if param:\n+        args.append(\"--\")\n+        for p in param:\n+            args.append(p)\n+    return subprocess.call(args) == 0\n \n def write_pipe(c, stdin):\n     if verbose:\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427701","messageId":"patch-07.27-12347d901b-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 07/27] git hook run: add an --ignore-missing flag","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:41Z","receivedAt":"2021-06-17T10:23:44Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"For certain one-shot hooks we'd like to optimistically run them, and\nnot complain if they don't exist. This will be used by send-email in a\nsubsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt | 10 +++++++++-\n builtin/hook.c             |  8 +++++++-\n t/t1800-hook.sh            |  7 ++++++-\n 3 files changed, 22 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 660d6a992a..097fb9de63 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run <hook-name> [-- <hook-args>]\n+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -29,6 +29,14 @@ optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n arguments (if any) differ by hook name, see linkgit:githooks[5] for\n what those are.\n \n+OPTIONS\n+-------\n+\n+--ignore-missing::\n+\tIgnore any missing hook by quietly returning zero. Used for\n+\ttools that want to do a blind one-shot run of a hook that may\n+\tor may not be present.\n+\n SEE ALSO\n --------\n linkgit:githooks[5]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 7714d31ef1..47e0de7bbc 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -22,10 +22,13 @@ static int run(int argc, const char **argv, const char *prefix)\n \tint i;\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint rc = 0;\n+\tint ignore_missing = 0;\n \tconst char *hook_name;\n \tconst char *hook_path;\n \n \tstruct option run_options[] = {\n+\t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n+\t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n \t\tOPT_END(),\n \t};\n \n@@ -49,11 +52,14 @@ static int run(int argc, const char **argv, const char *prefix)\n \t/*\n \t * We are not using run_hooks() because we'd like to detect\n \t * missing hooks. Let's find it ourselves and call\n-\t * run_found_hooks() instead.\n+\t * run_found_hooks() instead...\n \t */\n \thook_name = argv[0];\n \thook_path = find_hook(hook_name);\n \tif (!hook_path) {\n+\t\t/* ... act like run_hooks() under --ignore-missing */\n+\t\tif (ignore_missing)\n+\t\t\treturn 0;\n \t\terror(\"cannot find a hook named %s\", hook_name);\n \t\treturn 1;\n \t}\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex ecd517b162..542e551628 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -23,7 +23,12 @@ test_expect_success 'git hook run: nonexistent hook' '\n \ttest_cmp stderr.expect stderr.actual\n '\n \n-test_expect_success 'git hook run: basic' '\n+test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '\n+\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n+\ttest_must_be_empty stderr.actual\n+'\n+\n+test_expect_success 'git hook run -- basic' '\n \twrite_script .git/hooks/test-hook <<-EOF &&\n \techo Test hook\n \tEOF\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427702","messageId":"patch-08.27-71d209b407-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 08/27] send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:42Z","receivedAt":"2021-06-17T10:23:45Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nChange the \"sendmail-validate\" hook to be run via the \"git hook run\"\nwrapper instead of via a direct invocation.\n\nThis is the smallest possibly change to get \"send-email\" using \"git\nhook run\". We still check the hook itself with \"-x\", and set a\n\"GIT_DIR\" variable, both of which are asserted by our tests. We'll\nneed to get rid of this special behavior if we start running N hooks,\nbut for now let's be as close to bug-for-bug compatible as possible.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl   | 20 ++++++++++++--------\n t/t9001-send-email.sh |  4 ++--\n 2 files changed, 14 insertions(+), 10 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex 7ba0b3433d..9e47430403 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -214,13 +214,13 @@ sub format_2822_time {\n my $editor;\n \n sub system_or_msg {\n-\tmy ($args, $msg) = @_;\n+\tmy ($args, $msg, $cmd_name) = @_;\n \tsystem(@$args);\n \tmy $signalled = $? & 127;\n \tmy $exit_code = $? >> 8;\n \treturn unless $signalled or $exit_code;\n \n-\tmy @sprintf_args = ($args->[0], $exit_code);\n+\tmy @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);\n \tif (defined $msg) {\n \t\t# Quiet the 'redundant' warning category, except we\n \t\t# need to support down to Perl 5.8, so we can't do a\n@@ -1979,9 +1979,9 @@ sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n \tif ($repo) {\n+\t\tmy $hook_name = 'sendemail-validate';\n \t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n-\t\tmy $validate_hook = catfile($hooks_path,\n-\t\t\t\t\t    'sendemail-validate');\n+\t\tmy $validate_hook = catfile($hooks_path, $hook_name);\n \t\tmy $hook_error;\n \t\tif (-x $validate_hook) {\n \t\t\tmy $target = abs_path($fn);\n@@ -1990,13 +1990,17 @@ sub validate_patch {\n \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n \t\t\t\tor die(\"chdir: $!\");\n \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n+\t\t\tmy @validate_hook = (\"git\", \"hook\", \"run\", \"--ignore-missing\", $hook_name, \"--\", $target);\n+\t\t\t$hook_error = system_or_msg(\\@validate_hook, undef,\n+\t\t\t\t\t\t       \"git hook run $hook_name -- <patch>\");\n \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n \t\t}\n \t\tif ($hook_error) {\n-\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n-\t\t\t\t       \"%s\\n\" .\n-\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n+\t\t\t$hook_error = sprintf(__(\"fatal: %s: rejected by %s hook\\n\" .\n+\t\t\t\t\t\t $hook_error . \"\\n\" .\n+\t\t\t\t\t\t \"warning: no patches were sent\\n\"),\n+\t\t\t\t\t      $fn, $hook_name);\n+\t\t\tdie $hook_error;\n \t\t}\n \t}\n \ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 30eff725a9..6d4e25df8d 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -539,7 +539,7 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'my-hooks/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -558,7 +558,7 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'$hooks_path/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427703","messageId":"patch-10.27-e3f8482d80-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 10/27] commit: use hook.h to execute hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:44Z","receivedAt":"2021-06-17T10:23:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach run_commit_hook() to call hook.h instead of run-command.h. This\ncovers 'pre-commit', 'commit-msg', and\n'prepare-commit-msg'.\n\nAdditionally, ask the hook library - not run-command - whether any\nhooks will be run, as it's possible hooks may exist in the config but\nnot the hookdir.\n\nBecause all but 'post-commit' hooks are expected to make some state\nchange, force all but 'post-commit' hook to run in series. 'post-commit'\n\"is meant primarily for notification, and cannot affect the outcome of\n`git commit`,\" so it is fine to run in parallel.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c |  2 +-\n commit.c         | 16 ++++++++++------\n sequencer.c      |  2 +-\n 3 files changed, 12 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex f1aafd67d4..dad4e56544 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1045,7 +1045,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && find_hook(\"pre-commit\")) {\n+\tif (!no_verify && hook_exists(\"pre-commit\")) {\n \t\t/*\n \t\t * Re-read the index as pre-commit hook could have updated it,\n \t\t * and write it out as a tree.  We must do this before we invoke\ndiff --git a/commit.c b/commit.c\nindex 8ea55a447f..e8147a88fc 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -21,6 +21,7 @@\n #include \"commit-reach.h\"\n #include \"run-command.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);\n \n@@ -1698,22 +1699,25 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    const char *name, ...)\n {\n-\tstruct strvec hook_env = STRVEC_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tva_list args;\n+\tconst char *arg;\n \tint ret;\n-\n-\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n+\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\n \t * Let the hook know that no editor will be launched.\n \t */\n \tif (!editor_is_used)\n-\t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n+\t\tstrvec_push(&opt.env, \"GIT_EDITOR=:\");\n \n \tva_start(args, name);\n-\tret = run_hook_ve(hook_env.v, name, args);\n+\twhile ((arg = va_arg(args, const char *)))\n+\t\tstrvec_push(&opt.args, arg);\n \tva_end(args);\n-\tstrvec_clear(&hook_env);\n+\n+\tret = run_hooks(name, &opt);\n+\trun_hooks_opt_clear(&opt);\n \n \treturn ret;\n }\ndiff --git a/sequencer.c b/sequencer.c\nindex 3de479f90e..8f46984ffb 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1446,7 +1446,7 @@ static int try_to_commit(struct repository *r,\n \t\t}\n \t}\n \n-\tif (find_hook(\"prepare-commit-msg\")) {\n+\tif (hook_exists(\"prepare-commit-msg\")) {\n \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n \t\tif (res)\n \t\t\tgoto out;\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427704","messageId":"patch-12.27-e4ef3f4548-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 12/27] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:46Z","receivedAt":"2021-06-17T10:23:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using hook.h instead of run-command.h to invoke push-to-checkout,\nhooks can now be specified in the config as well as in the hookdir.\npush-to-checkout is not called anywhere but in builtin/receive-pack.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 14 ++++++++++----\n 1 file changed, 10 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 1e0e04c62f..5248228ebf 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1436,12 +1436,18 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n-\tif (run_hook_le(env->v, push_to_checkout_hook,\n-\t\t\thash_to_hex(hash), NULL))\n+\tstrvec_pushv(&opt.env, env->v);\n+\tstrvec_push(&opt.args, hash_to_hex(hash));\n+\tif (run_hooks(push_to_checkout_hook, &opt)) {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn \"push-to-checkout hook declined\";\n-\telse\n+\t} else {\n+\t\trun_hooks_opt_clear(&opt);\n \t\treturn NULL;\n+\t}\n }\n \n static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree)\n@@ -1465,7 +1471,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!find_hook(push_to_checkout_hook))\n+\tif (!hook_exists(push_to_checkout_hook))\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n \telse\n \t\tretval = push_to_checkout(sha1, &env, work_tree);\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427705","messageId":"patch-13.27-e3dda367ec-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 13/27] run-command: remove old run_hook_{le,ve}() hook API","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:47Z","receivedAt":"2021-06-17T10:23:51Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nThe new hook.h library has replaced all run-command.h hook-related\nfunctionality. So let's delete this dead code.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n run-command.c | 32 --------------------------------\n run-command.h | 16 ----------------\n 2 files changed, 48 deletions(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex eecdef5a0c..95c950a4a2 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1321,38 +1321,6 @@ int async_with_fork(void)\n #endif\n }\n \n-static int run_hook_ve(const char *const *env, const char *name, va_list args)\n-{\n-\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\tconst char *p;\n-\n-\tp = find_hook(name);\n-\tif (!p)\n-\t\treturn 0;\n-\n-\tstrvec_push(&hook.args, p);\n-\twhile ((p = va_arg(args, const char *)))\n-\t\tstrvec_push(&hook.args, p);\n-\thook.env = env;\n-\thook.no_stdin = 1;\n-\thook.stdout_to_stderr = 1;\n-\thook.trace2_hook_name = name;\n-\n-\treturn run_command(&hook);\n-}\n-\n-int run_hook_le(const char *const *env, const char *name, ...)\n-{\n-\tva_list args;\n-\tint ret;\n-\n-\tva_start(args, name);\n-\tret = run_hook_ve(env, name, args);\n-\tva_end(args);\n-\n-\treturn ret;\n-}\n-\n struct io_pump {\n \t/* initialized by caller */\n \tint fd;\ndiff --git a/run-command.h b/run-command.h\nindex 24ab5d63c4..748d4fc2a7 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -201,22 +201,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/**\n- * Run a hook.\n- * The first argument is a pathname to an index file, or NULL\n- * if the hook uses the default index file or no index is needed.\n- * The second argument is the name of the hook.\n- * The further arguments correspond to the hook arguments.\n- * The last argument has to be NULL to terminate the arguments list.\n- * If the hook does not exist or is not executable, the return\n- * value will be zero.\n- * If it is executable, the hook will be executed and the exit\n- * status of the hook is returned.\n- * On execution, .stdout_to_stderr and .no_stdin will be set.\n- */\n-LAST_ARG_MUST_BE_NULL\n-int run_hook_le(const char *const *env, const char *name, ...);\n-\n /*\n  * Trigger an auto-gc\n  */\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427706","messageId":"patch-14.27-477d75bf57-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 14/27] run-command: allow stdin for run_processes_parallel","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:48Z","receivedAt":"2021-06-17T10:23:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nWhile it makes sense not to inherit stdin from the parent process to\navoid deadlocking, it's not necessary to completely ban stdin to\nchildren. An informed user should be able to configure stdin safely. By\nsetting `some_child.process.no_stdin=1` before calling `get_next_task()`\nwe provide a reasonable default behavior but enable users to set up\nstdin streaming for themselves during the callback.\n\n`some_child.process.stdout_to_stderr`, however, remains unmodifiable by\n`get_next_task()` - the rest of the run_processes_parallel() API depends\non child output in stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n run-command.c | 9 ++++++++-\n 1 file changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex 95c950a4a2..0bf771845e 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1628,6 +1628,14 @@ static int pp_start_one(struct parallel_processes *pp)\n \tif (i == pp->max_processes)\n \t\tBUG(\"bookkeeping is hard\");\n \n+\t/*\n+\t * By default, do not inherit stdin from the parent process - otherwise,\n+\t * all children would share stdin! Users may overwrite this to provide\n+\t * something to the child's stdin by having their 'get_next_task'\n+\t * callback assign 0 to .no_stdin and an appropriate integer to .in.\n+\t */\n+\tpp->children[i].process.no_stdin = 1;\n+\n \tcode = pp->get_next_task(&pp->children[i].process,\n \t\t\t\t &pp->children[i].err,\n \t\t\t\t pp->data,\n@@ -1639,7 +1647,6 @@ static int pp_start_one(struct parallel_processes *pp)\n \t}\n \tpp->children[i].process.err = -1;\n \tpp->children[i].process.stdout_to_stderr = 1;\n-\tpp->children[i].process.no_stdin = 1;\n \n \tif (start_command(&pp->children[i].process)) {\n \t\tcode = pp->start_failure(&pp->children[i].err,\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427707","messageId":"patch-15.27-b7c0ee9719-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 15/27] hook: support passing stdin to hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:49Z","receivedAt":"2021-06-17T10:23:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome hooks (such as post-rewrite) need to take input via stdin.\nPreviously, callers provided stdin to hooks by setting\nrun-command.h:child_process.in, which takes a FD. Callers would open the\nfile in question themselves before calling run-command(). However, since\nwe will now need to seek to the front of the file and read it again for\nevery hook which runs, hook.h:run_command() takes a path and handles FD\nmanagement itself. Since this file is opened for read only, it should\nnot prevent later parallel execution support.\n\nOn the frontend, this is supported by asking for a file path, rather\nthan by reading stdin. Reading directly from stdin would involve caching\nthe entire stdin (to memory or to disk) and reading it back from the\nbeginning to each hook. We'd want to support cases like insufficient\nmemory or storage for the file. While this may prove useful later, for\nnow the path of least resistance is to just ask the user to make this\ninterim file themselves.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt |  7 ++++++-\n builtin/hook.c             |  5 ++++-\n hook.c                     |  9 ++++++++-\n hook.h                     |  2 ++\n t/t1800-hook.sh            | 18 ++++++++++++++++++\n 5 files changed, 38 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 097fb9de63..fa68c1f391 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -32,6 +32,11 @@ what those are.\n OPTIONS\n -------\n \n+--to-stdin::\n+\tFor \"run\"; Specify a file which will be streamed into the\n+\thook's stdin. The hook will receive the entire file from\n+\tbeginning to EOF.\n+\n --ignore-missing::\n \tIgnore any missing hook by quietly returning zero. Used for\n \ttools that want to do a blind one-shot run of a hook that may\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 47e0de7bbc..169a8dd08f 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -8,12 +8,13 @@\n \n static const char * const builtin_hook_usage[] = {\n \tN_(\"git hook <command> [...]\"),\n-\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n+\tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n \tNULL\n };\n \n static const char * const builtin_hook_run_usage[] = {\n \tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n+\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n \tNULL\n };\n \n@@ -29,6 +30,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \tstruct option run_options[] = {\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n+\t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n+\t\t\t   N_(\"file to read into hooks' stdin\")),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/hook.c b/hook.c\nindex cb39404ac8..cab9cced82 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -85,7 +85,14 @@ static int pick_next_hook(struct child_process *cp,\n \tstruct hook_cb_data *hook_cb = pp_cb;\n \tstruct hook *run_me = hook_cb->run_me;\n \n-\tcp->no_stdin = 1;\n+\n+\t/* reopen the file for stdin; run_command closes it. */\n+\tif (hook_cb->options->path_to_stdin) {\n+\t\tcp->no_stdin = 0;\n+\t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else {\n+\t\tcp->no_stdin = 1;\n+\t}\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\ndiff --git a/hook.h b/hook.h\nindex 8bd0fc8d1f..c4ac054ee3 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -45,6 +45,8 @@ struct run_hooks_opt\n \t/* Path to initial working directory for subprocess */\n \tconst char *dir;\n \n+\t/* Path to file which should be piped to stdin for each hook */\n+\tconst char *path_to_stdin;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 542e551628..f3510379e0 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -133,4 +133,22 @@ test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'stdin to hooks' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo BEGIN stdin\n+\tcat\n+\techo END stdin\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\tBEGIN stdin\n+\thello\n+\tEND stdin\n+\tEOF\n+\n+\techo hello >input &&\n+\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427708","messageId":"patch-11.27-6ed61071c5-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 11/27] read-cache: convert post-index-change hook to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:45Z","receivedAt":"2021-06-17T10:23:58Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using hook.h instead of run-command.h to run, post-index-change hooks\ncan now be specified in the config in addition to the hookdir.\npost-index-change is not run anywhere besides in read-cache.c.\n\nThis removes the last direct user of run_hook_ve(), so we can make the\nfunction static now. It'll be removed entirely soon.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n read-cache.c  | 11 ++++++++---\n run-command.c |  2 +-\n run-command.h |  1 -\n 3 files changed, 9 insertions(+), 5 deletions(-)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex af5b97104c..f801313cc9 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3063,6 +3063,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tret = convert_to_sparse(istate);\n \n@@ -3091,9 +3092,13 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \telse\n \t\tret = close_lock_file_gently(lock);\n \n-\trun_hook_le(NULL, \"post-index-change\",\n-\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n-\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n+\tstrvec_pushl(&hook_opt.args,\n+\t\t     istate->updated_workdir ? \"1\" : \"0\",\n+\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trun_hooks(\"post-index-change\", &hook_opt);\n+\trun_hooks_opt_clear(&hook_opt);\n+\n \tistate->updated_workdir = 0;\n \tistate->updated_skipworktree = 0;\n \ndiff --git a/run-command.c b/run-command.c\nindex 82fdf29656..eecdef5a0c 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1321,7 +1321,7 @@ int async_with_fork(void)\n #endif\n }\n \n-int run_hook_ve(const char *const *env, const char *name, va_list args)\n+static int run_hook_ve(const char *const *env, const char *name, va_list args)\n {\n \tstruct child_process hook = CHILD_PROCESS_INIT;\n \tconst char *p;\ndiff --git a/run-command.h b/run-command.h\nindex b58531a7eb..24ab5d63c4 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -216,7 +216,6 @@ int run_command(struct child_process *);\n  */\n LAST_ARG_MUST_BE_NULL\n int run_hook_le(const char *const *env, const char *name, ...);\n-int run_hook_ve(const char *const *env, const char *name, va_list args);\n \n /*\n  * Trigger an auto-gc\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427709","messageId":"patch-16.27-4035069a98-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 16/27] am: convert 'post-rewrite' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:50Z","receivedAt":"2021-06-17T10:24:00Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 18 +++++-------------\n 1 file changed, 5 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 9e9c1b5e9f..6e4f9c8036 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -467,23 +467,15 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tconst char *hook = find_hook(\"post-rewrite\");\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint ret;\n \n-\tif (!hook)\n-\t\treturn 0;\n-\n-\tstrvec_push(&cp.args, hook);\n-\tstrvec_push(&cp.args, \"rebase\");\n-\n-\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n-\tcp.stdout_to_stderr = 1;\n-\tcp.trace2_hook_name = \"post-rewrite\";\n+\tstrvec_push(&opt.args, \"rebase\");\n+\topt.path_to_stdin = am_path(state, \"rewritten\");\n \n-\tret = run_command(&cp);\n+\tret = run_hooks(\"post-rewrite\", &opt);\n \n-\tclose(cp.in);\n+\trun_hooks_opt_clear(&opt);\n \treturn ret;\n }\n \n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427710","messageId":"patch-17.27-c847a19581-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 17/27] run-command: add stdin callback for parallelization","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:51Z","receivedAt":"2021-06-17T10:24:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIf a user of the run_processes_parallel() API wants to pipe a large\namount of information to stdin of each parallel command, that\ninformation could exceed the buffer of the pipe allocated for that\nprocess's stdin.  Generally this is solved by repeatedly writing to\nchild_process.in between calls to start_command() and finish_command();\nrun_processes_parallel() did not provide users an opportunity to access\nchild_process at that time.\n\nBecause the data might be extremely large (for example, a list of all\nrefs received during a push from a client) simply taking a string_list\nor strbuf is not as scalable as using a callback; the rest of the\nrun_processes_parallel() API also uses callbacks, so making this feature\nmatch the rest of the API reduces mental load on the user.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  1 +\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 54 +++++++++++++++++++++++++++++++++++--\n run-command.h               | 17 +++++++++++-\n submodule.c                 |  1 +\n t/helper/test-run-command.c | 31 ++++++++++++++++++---\n t/t0061-run-command.sh      | 30 +++++++++++++++++++++\n 8 files changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 9191620e50..3d8f04b392 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,6 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n+\t\t\t\t\t\t    NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex ae6174ab05..818494dd18 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2295,7 +2295,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure,\n+\t\t\t\t   update_clone_start_failure, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex cab9cced82..a012c3d458 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -164,6 +164,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 0bf771845e..3392640f17 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1493,6 +1493,7 @@ struct parallel_processes {\n \n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n+\tfeed_pipe_fn feed_pipe;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1520,6 +1521,13 @@ static int default_start_failure(struct strbuf *out,\n \treturn 0;\n }\n \n+static int default_feed_pipe(struct strbuf *pipe,\n+\t\t\t     void *pp_cb,\n+\t\t\t     void *pp_task_cb)\n+{\n+\treturn 1;\n+}\n+\n static int default_task_finished(int result,\n \t\t\t\t struct strbuf *out,\n \t\t\t\t void *pp_cb,\n@@ -1550,6 +1558,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    int n,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n+\t\t    feed_pipe_fn feed_pipe,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1568,6 +1577,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->get_next_task = get_next_task;\n \n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n+\tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n \n \tpp->nr_processes = 0;\n@@ -1665,6 +1675,37 @@ static int pp_start_one(struct parallel_processes *pp)\n \treturn 0;\n }\n \n+static void pp_buffer_stdin(struct parallel_processes *pp)\n+{\n+\tint i;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t/* Buffer stdin for each pipe. */\n+\tfor (i = 0; i < pp->max_processes; i++) {\n+\t\tif (pp->children[i].state == GIT_CP_WORKING &&\n+\t\t    pp->children[i].process.in > 0) {\n+\t\t\tint done;\n+\t\t\tstrbuf_reset(&sb);\n+\t\t\tdone = pp->feed_pipe(&sb, pp->data,\n+\t\t\t\t\t      pp->children[i].data);\n+\t\t\tif (sb.len) {\n+\t\t\t\tif (write_in_full(pp->children[i].process.in,\n+\t\t\t\t\t      sb.buf, sb.len) < 0) {\n+\t\t\t\t\tif (errno != EPIPE)\n+\t\t\t\t\t\tdie_errno(\"write\");\n+\t\t\t\t\tdone = 1;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif (done) {\n+\t\t\t\tclose(pp->children[i].process.in);\n+\t\t\t\tpp->children[i].process.in = 0;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&sb);\n+}\n+\n static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n {\n \tint i;\n@@ -1729,6 +1770,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\tpp->nr_processes--;\n \t\tpp->children[i].state = GIT_CP_FREE;\n \t\tpp->pfd[i].fd = -1;\n+\t\tpp->children[i].process.in = 0;\n \t\tchild_process_init(&pp->children[i].process);\n \n \t\tif (i != pp->output_owner) {\n@@ -1762,6 +1804,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n+\t\t\t   feed_pipe_fn feed_pipe,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1770,7 +1813,9 @@ int run_processes_parallel(int n,\n \tint spawn_cap = 4;\n \tstruct parallel_processes pp;\n \n-\tpp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb);\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1787,6 +1832,7 @@ int run_processes_parallel(int n,\n \t\t}\n \t\tif (!pp.nr_processes)\n \t\t\tbreak;\n+\t\tpp_buffer_stdin(&pp);\n \t\tpp_buffer_stderr(&pp, output_timeout);\n \t\tpp_output(&pp);\n \t\tcode = pp_collect_finished(&pp);\n@@ -1798,11 +1844,15 @@ int run_processes_parallel(int n,\n \t}\n \n \tpp_cleanup(&pp);\n+\n+\tsigchain_pop(SIGPIPE);\n+\n \treturn 0;\n }\n \n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n+\t\t\t       feed_pipe_fn feed_pipe,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1812,7 +1862,7 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\ttask_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 748d4fc2a7..41e36d26cb 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -419,6 +419,20 @@ typedef int (*start_failure_fn)(struct strbuf *out,\n \t\t\t\tvoid *pp_cb,\n \t\t\t\tvoid *pp_task_cb);\n \n+/**\n+ * This callback is called repeatedly on every child process who requests\n+ * start_command() to create a pipe by setting child_process.in < 0.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel, and\n+ * pp_task_cb is the callback cookie as passed into get_next_task_fn.\n+ * The contents of 'send' will be read into the pipe and passed to the pipe.\n+ *\n+ * Return nonzero to close the pipe.\n+ */\n+typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n+\t\t\t    void *pp_cb,\n+\t\t\t    void *pp_task_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -453,10 +467,11 @@ typedef int (*task_finished_fn)(int result,\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n+\t\t\t   feed_pipe_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex 0b1d9c1dde..ea026a8195 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1645,6 +1645,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 7ae03dc712..9348184d30 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,\n \t\treturn 0;\n \n \tstrvec_pushv(&cp->args, d->argv);\n+\tcp->in = d->in;\n+\tcp->no_stdin = d->no_stdin;\n \tstrbuf_addstr(err, \"preloaded output of a child\\n\");\n \tnumber_callbacks++;\n+\n+\t*task_cb = xmalloc(sizeof(int));\n+\t*(int*)(*task_cb) = 2;\n \treturn 1;\n }\n \n@@ -55,6 +60,17 @@ static int task_finished(int result,\n \treturn 1;\n }\n \n+static int test_stdin(struct strbuf *pipe, void *cb, void *task_cb)\n+{\n+\tint *lines_remaining = task_cb;\n+\n+\tif (*lines_remaining)\n+\t\tstrbuf_addf(pipe, \"sample stdin %d\\n\", --(*lines_remaining));\n+\n+\treturn !(*lines_remaining);\n+}\n+\n+\n struct testsuite {\n \tstruct string_list tests, failed;\n \tint next;\n@@ -185,7 +201,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_finished, &suite);\n+\t\t\t\t     test_stdin, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -413,15 +429,22 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\n+\tif (!strcmp(argv[1], \"run-command-stdin\")) {\n+\t\tproc.in = -1;\n+\t\tproc.no_stdin = 0;\n+\t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n+\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t}\n \n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 7d599675e3..87759482ad 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,36 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+cat >expect <<-EOF\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+EOF\n+\n+test_expect_success 'run_command listens to stdin' '\n+\twrite_script stdin-script <<-\\EOF &&\n+\techo \"listening for stdin:\"\n+\twhile read line; do\n+\t\techo \"$line\"\n+\tdone\n+\tEOF\n+\ttest-tool run-command run-command-stdin 2 ./stdin-script 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n asking for a quick stop\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427711","messageId":"patch-18.27-da46c859c1-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 18/27] hook: provide stdin by string_list or callback","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:52Z","receivedAt":"2021-06-17T10:24:02Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn cases where a hook requires only a small amount of information via\nstdin, it should be simple for users to provide a string_list alone. But\nin more complicated cases where the stdin is too large to hold in\nmemory, let's instead provide a callback the users can populate line\nafter line.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 32 +++++++++++++++++++++++++++++++-\n hook.h | 27 +++++++++++++++++++++++++++\n 2 files changed, 58 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex a012c3d458..c4df787f13 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -77,6 +77,29 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)\n \tstrvec_clear(&o->args);\n }\n \n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n+{\n+\tint *item_idx;\n+\tstruct hook *ctx = pp_task_cb;\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct string_list *to_pipe = hook_cb->options->feed_pipe_ctx;\n+\n+\t/* Bootstrap the state manager if necessary. */\n+\tif (!ctx->feed_pipe_cb_data) {\n+\t\tctx->feed_pipe_cb_data = xmalloc(sizeof(unsigned int));\n+\t\t*(int*)ctx->feed_pipe_cb_data = 0;\n+\t}\n+\n+\titem_idx = ctx->feed_pipe_cb_data;\n+\n+\tif (*item_idx < to_pipe->nr) {\n+\t\tstrbuf_addf(pipe, \"%s\\n\", to_pipe->items[*item_idx].string);\n+\t\t(*item_idx)++;\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n static int pick_next_hook(struct child_process *cp,\n \t\t\t  struct strbuf *out,\n \t\t\t  void *pp_cb,\n@@ -90,6 +113,10 @@ static int pick_next_hook(struct child_process *cp,\n \tif (hook_cb->options->path_to_stdin) {\n \t\tcp->no_stdin = 0;\n \t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else if (hook_cb->options->feed_pipe) {\n+\t\t/* ask for start_command() to make a pipe for us */\n+\t\tcp->in = -1;\n+\t\tcp->no_stdin = 0;\n \t} else {\n \t\tcp->no_stdin = 1;\n \t}\n@@ -164,7 +191,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->feed_pipe,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n@@ -182,6 +209,9 @@ int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n+\tif (options->path_to_stdin && options->feed_pipe)\n+\t\tBUG(\"choose only one method to populate stdin\");\n+\n \thook_path = find_hook(hook_name);\n \n \t/*\ndiff --git a/hook.h b/hook.h\nindex c4ac054ee3..b78a8e4805 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -19,6 +19,12 @@ int hook_exists(const char *hookname);\n struct hook {\n \t/* The path to the hook */\n \tconst char *hook_path;\n+\n+\t/*\n+\t * Use this to keep state for your feed_pipe_fn if you are using\n+\t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n+\t */\n+\tvoid *feed_pipe_cb_data;\n };\n \n struct run_hooks_opt\n@@ -47,6 +53,19 @@ struct run_hooks_opt\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n+\n+\t/*\n+\t * Callback and state pointer to ask for more content to pipe to stdin.\n+\t * Will be called repeatedly, for each hook. See\n+\t * hook.c:pipe_from_stdin() for an example. Keep per-hook state in\n+\t * hook.feed_pipe_cb_data (per process). Keep initialization context in\n+\t * feed_pipe_ctx (shared by all processes).\n+\t *\n+\t * See 'pipe_from_string_list()' for info about how to specify a\n+\t * string_list as the stdin input instead of writing your own handler.\n+\t */\n+\tfeed_pipe_fn feed_pipe;\n+\tvoid *feed_pipe_ctx;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -55,6 +74,14 @@ struct run_hooks_opt\n \t.args = STRVEC_INIT, \\\n }\n \n+/*\n+ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n+ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n+ * This will pipe each string in the list to stdin, separated by newlines.  (Do\n+ * not inject your own newlines.)\n+ */\n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n+\n struct hook_cb_data {\n \t/* rc reflects the cumulative failure state */\n \tint rc;\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427712","messageId":"patch-19.27-7343be28ef-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 19/27] hook: convert 'post-rewrite' hook in sequencer.c to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:53Z","receivedAt":"2021-06-17T10:24:05Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using 'hook.h' for 'post-rewrite', we simplify hook invocations by\nnot needing to put together our own 'struct child_process'.\n\nThe signal handling that's being removed by this commit now takes\nplace in run-command.h:run_processes_parallel(), so it is OK to remove\nthem here.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n sequencer.c | 81 ++++++++++++++++++++++-------------------------------\n 1 file changed, 34 insertions(+), 47 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 8f46984ffb..ec2761e47d 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -35,6 +35,7 @@\n #include \"commit-reach.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"string-list.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -1147,33 +1148,28 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *argv[3];\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n-\tstruct strbuf sb = STRBUF_INIT;\n \n-\targv[0] = find_hook(\"post-rewrite\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tstrvec_push(&opt.args, \"amend\");\n \n-\targv[1] = \"amend\";\n-\targv[2] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"post-rewrite\";\n-\n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n-\tstrbuf_addf(&sb, \"%s %s\\n\", oid_to_hex(oldoid), oid_to_hex(newoid));\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\twrite_in_full(proc.in, sb.buf, sb.len);\n-\tclose(proc.in);\n-\tstrbuf_release(&sb);\n-\tsigchain_pop(SIGPIPE);\n-\treturn finish_command(&proc);\n+\tstrbuf_addf(&tmp,\n+\t\t    \"%s %s\",\n+\t\t    oid_to_hex(oldoid),\n+\t\t    oid_to_hex(newoid));\n+\tstring_list_append(&to_stdin, tmp.buf);\n+\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tcode = run_hooks(\"post-rewrite\", &opt);\n+\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n+\treturn code;\n }\n \n void commit_post_rewrite(struct repository *r,\n@@ -4527,30 +4523,21 @@ static int pick_commits(struct repository *r,\n \t\tflush_rewritten_pending();\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n-\t\t\tstruct child_process child = CHILD_PROCESS_INIT;\n-\t\t\tconst char *post_rewrite_hook =\n-\t\t\t\tfind_hook(\"post-rewrite\");\n-\n-\t\t\tchild.in = open(rebase_path_rewritten_list(), O_RDONLY);\n-\t\t\tchild.git_cmd = 1;\n-\t\t\tstrvec_push(&child.args, \"notes\");\n-\t\t\tstrvec_push(&child.args, \"copy\");\n-\t\t\tstrvec_push(&child.args, \"--for-rewrite=rebase\");\n+\t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n+\t\t\tnotes_cp.git_cmd = 1;\n+\t\t\tstrvec_push(&notes_cp.args, \"notes\");\n+\t\t\tstrvec_push(&notes_cp.args, \"copy\");\n+\t\t\tstrvec_push(&notes_cp.args, \"--for-rewrite=rebase\");\n \t\t\t/* we don't care if this copying failed */\n-\t\t\trun_command(&child);\n-\n-\t\t\tif (post_rewrite_hook) {\n-\t\t\t\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\n-\t\t\t\thook.in = open(rebase_path_rewritten_list(),\n-\t\t\t\t\tO_RDONLY);\n-\t\t\t\thook.stdout_to_stderr = 1;\n-\t\t\t\thook.trace2_hook_name = \"post-rewrite\";\n-\t\t\t\tstrvec_push(&hook.args, post_rewrite_hook);\n-\t\t\t\tstrvec_push(&hook.args, \"rebase\");\n-\t\t\t\t/* we don't care if this hook failed */\n-\t\t\t\trun_command(&hook);\n-\t\t\t}\n+\t\t\trun_command(&notes_cp);\n+\n+\t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n+\t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n+\t\t\trun_hooks(\"post-rewrite\", &hook_opt);\n+\t\t\trun_hooks_opt_clear(&hook_opt);\n \t\t}\n \t\tapply_autostash(rebase_path_autostash());\n \n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427713","messageId":"patch-20.27-85bf13a083-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 20/27] transport: convert pre-push hook to use config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:54Z","receivedAt":"2021-06-17T10:24:06Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using the hook.h:run_hooks API, pre-push hooks can be specified in\nthe config as well as in the hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n transport.c | 57 ++++++++++++++---------------------------------------\n 1 file changed, 15 insertions(+), 42 deletions(-)\n\ndiff --git a/transport.c b/transport.c\nindex 2ed270171f..9969ed2cdd 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1199,31 +1199,14 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)\n static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n-\tint ret = 0, x;\n+\tint ret = 0;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct strbuf tmp = STRBUF_INIT;\n \tstruct ref *r;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct strbuf buf;\n-\tconst char *argv[4];\n-\n-\tif (!(argv[0] = find_hook(\"pre-push\")))\n-\t\treturn 0;\n-\n-\targv[1] = transport->remote->name;\n-\targv[2] = transport->url;\n-\targv[3] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.trace2_hook_name = \"pre-push\";\n-\n-\tif (start_command(&proc)) {\n-\t\tfinish_command(&proc);\n-\t\treturn -1;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \n-\tstrbuf_init(&buf, 256);\n+\tstrvec_push(&opt.args, transport->remote->name);\n+\tstrvec_push(&opt.args, transport->url);\n \n \tfor (r = remote_refs; r; r = r->next) {\n \t\tif (!r->peer_ref) continue;\n@@ -1232,30 +1215,20 @@ static int run_pre_push_hook(struct transport *transport,\n \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n \n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n+\t\tstrbuf_reset(&tmp);\n+\t\tstrbuf_addf(&tmp, \"%s %s %s %s\",\n \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n \t\t\t r->name, oid_to_hex(&r->old_oid));\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\t/* We do not mind if a hook does not read all refs. */\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstring_list_append(&to_stdin, tmp.buf);\n \t}\n \n-\tstrbuf_release(&buf);\n-\n-\tx = close(proc.in);\n-\tif (!ret)\n-\t\tret = x;\n-\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n \n-\tx = finish_command(&proc);\n-\tif (!ret)\n-\t\tret = x;\n+\tret = run_hooks(\"pre-push\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n \n \treturn ret;\n }\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427714","messageId":"patch-22.27-f7f56d0a3b-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 22/27] run-command: allow capturing of collated output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:56Z","receivedAt":"2021-06-17T10:24:07Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome callers, for example server-side hooks which wish to relay hook\noutput to clients across a transport, want to capture what would\nnormally print to stderr and do something else with it. Allow that via a\ncallback.\n\nBy calling the callback regardless of whether there's output available,\nwe allow clients to send e.g. a keepalive if necessary.\n\nBecause we expose a strbuf, not a fd or FILE*, there's no need to create\na temporary pipe or similar - we can just skip the print to stderr and\ninstead hand it to the caller.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 33 +++++++++++++++++++++++++--------\n run-command.h               | 18 +++++++++++++++++-\n submodule.c                 |  2 +-\n t/helper/test-run-command.c | 25 ++++++++++++++++++++-----\n t/t0061-run-command.sh      |  7 +++++++\n 8 files changed, 73 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 3d8f04b392..0595b88b7c 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,7 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n-\t\t\t\t\t\t    NULL,\n+\t\t\t\t\t\t    NULL, NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 818494dd18..69782f6204 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2295,7 +2295,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure, NULL,\n+\t\t\t\t   update_clone_start_failure, NULL, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex c4df787f13..2dca969f9e 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -192,6 +192,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 3392640f17..4a1a7a1082 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1494,6 +1494,7 @@ struct parallel_processes {\n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n \tfeed_pipe_fn feed_pipe;\n+\tconsume_sideband_fn consume_sideband;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1559,6 +1560,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n \t\t    feed_pipe_fn feed_pipe,\n+\t\t    consume_sideband_fn consume_sideband,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1579,6 +1581,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n \tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n+\tpp->consume_sideband = consume_sideband;\n \n \tpp->nr_processes = 0;\n \tpp->output_owner = 0;\n@@ -1615,7 +1618,10 @@ static void pp_cleanup(struct parallel_processes *pp)\n \t * When get_next_task added messages to the buffer in its last\n \t * iteration, the buffered output is non empty.\n \t */\n-\tstrbuf_write(&pp->buffered_output, stderr);\n+\tif (pp->consume_sideband)\n+\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\telse\n+\t\tstrbuf_write(&pp->buffered_output, stderr);\n \tstrbuf_release(&pp->buffered_output);\n \n \tsigchain_pop_common();\n@@ -1736,9 +1742,13 @@ static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n static void pp_output(struct parallel_processes *pp)\n {\n \tint i = pp->output_owner;\n+\n \tif (pp->children[i].state == GIT_CP_WORKING &&\n \t    pp->children[i].err.len) {\n-\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\tif (pp->consume_sideband)\n+\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\telse\n+\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n \t\tstrbuf_reset(&pp->children[i].err);\n \t}\n }\n@@ -1777,11 +1787,15 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\t\tstrbuf_addbuf(&pp->buffered_output, &pp->children[i].err);\n \t\t\tstrbuf_reset(&pp->children[i].err);\n \t\t} else {\n-\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t/* Output errors, then all other finished child processes */\n+\t\t\tif (pp->consume_sideband) {\n+\t\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\t\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\t\t\t} else {\n+\t\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n+\t\t\t}\n \t\t\tstrbuf_reset(&pp->children[i].err);\n-\n-\t\t\t/* Output all other finished child processes */\n-\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n \t\t\tstrbuf_reset(&pp->buffered_output);\n \n \t\t\t/*\n@@ -1805,6 +1819,7 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n \t\t\t   feed_pipe_fn feed_pipe,\n+\t\t\t   consume_sideband_fn consume_sideband,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1815,7 +1830,7 @@ int run_processes_parallel(int n,\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n \n-\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, consume_sideband, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1853,6 +1868,7 @@ int run_processes_parallel(int n,\n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n \t\t\t       feed_pipe_fn feed_pipe,\n+\t\t\t       consume_sideband_fn consume_sideband,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1862,7 +1878,8 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, consume_sideband,\n+\t\t\t\t\ttask_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 41e36d26cb..7150da851a 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -433,6 +433,20 @@ typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n \t\t\t    void *pp_cb,\n \t\t\t    void *pp_task_cb);\n \n+/**\n+ * If this callback is provided, instead of collating process output to stderr,\n+ * they will be collated into a new pipe. consume_sideband_fn will be called\n+ * repeatedly. When output is available on that pipe, it will be contained in\n+ * 'output'. But it will be called with an empty 'output' too, to allow for\n+ * keepalives or similar operations if necessary.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel.\n+ *\n+ * Since this callback is provided with the collated output, no task cookie is\n+ * provided.\n+ */\n+typedef void (*consume_sideband_fn)(struct strbuf *output, void *pp_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -468,10 +482,12 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n \t\t\t   feed_pipe_fn,\n+\t\t\t   consume_sideband_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, consume_sideband_fn,\n+\t\t\t       task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n #endif\ndiff --git a/submodule.c b/submodule.c\nindex ea026a8195..7fe0c8f7c9 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1645,7 +1645,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   NULL, NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 9348184d30..d53db6d11c 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -51,6 +51,16 @@ static int no_job(struct child_process *cp,\n \treturn 0;\n }\n \n+static void test_consume_sideband(struct strbuf *output, void *cb)\n+{\n+\tFILE *sideband;\n+\n+\tsideband = fopen(\"./sideband\", \"a\");\n+\n+\tstrbuf_write(output, sideband);\n+\tfclose(sideband);\n+}\n+\n static int task_finished(int result,\n \t\t\t struct strbuf *err,\n \t\t\t void *pp_cb,\n@@ -201,7 +211,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_stdin, test_finished, &suite);\n+\t\t\t\t     test_stdin, NULL, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -429,23 +439,28 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-stdin\")) {\n \t\tproc.in = -1;\n \t\tproc.no_stdin = 0;\n \t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n-\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t\t\t\t\t     test_stdin, NULL, NULL, &proc));\n \t}\n \n+\tif (!strcmp(argv[1], \"run-command-sideband\"))\n+\t\texit(run_processes_parallel(jobs, parallel_next, NULL, NULL,\n+\t\t\t\t\t    test_consume_sideband, NULL,\n+\t\t\t\t\t    &proc));\n+\n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\n }\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 87759482ad..e99f6c7f44 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,13 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'run_command can divert output' '\n+\ttest_when_finished rm sideband &&\n+\ttest-tool run-command run-command-sideband 3 sh -c \"printf \\\"%s\\n%s\\n\\\" Hello World\" 2>actual &&\n+\ttest_must_be_empty actual &&\n+\ttest_cmp expect sideband\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n listening for stdin:\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427715","messageId":"patch-21.27-331014bad1-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 21/27] reference-transaction: use hook.h to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:55Z","receivedAt":"2021-06-17T10:24:08Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n refs.c | 42 +++++++++++++++---------------------------\n 1 file changed, 15 insertions(+), 27 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 59be29cf08..1149e7e7dc 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2062,47 +2062,35 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n \tstruct strbuf buf = STRBUF_INIT;\n-\tconst char *hook;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint ret = 0, i;\n+\tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n \n-\thook = find_hook(\"reference-transaction\");\n-\tif (!hook)\n+\tif (!hook_exists(\"reference-transaction\"))\n \t\treturn ret;\n \n-\tstrvec_pushl(&proc.args, hook, state, NULL);\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"reference-transaction\";\n-\n-\tret = start_command(&proc);\n-\tif (ret)\n-\t\treturn ret;\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstrvec_push(&opt.args, state);\n \n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n+\t\toid_to_hex_r(o, &update->old_oid);\n+\t\toid_to_hex_r(n, &update->new_oid);\n \n \t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n-\t\t\t    oid_to_hex(&update->old_oid),\n-\t\t\t    oid_to_hex(&update->new_oid),\n-\t\t\t    update->refname);\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstrbuf_addf(&buf, \"%s %s %s\", o, n, update->refname);\n+\t\tstring_list_append(&to_stdin, buf.buf);\n \t}\n \n-\tclose(proc.in);\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tret = run_hooks(\"reference-transaction\", &opt);\n+\trun_hooks_opt_clear(&opt);\n \tstrbuf_release(&buf);\n+\tstring_list_clear(&to_stdin, 0);\n \n-\tret |= finish_command(&proc);\n \treturn ret;\n }\n \n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427716","messageId":"patch-23.27-7f7fcc0688-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 23/27] hooks: allow callers to capture output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:57Z","receivedAt":"2021-06-17T10:24:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome server-side hooks will require capturing output to send over\nsideband instead of printing directly to stderr. Expose that capability.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 2 +-\n hook.h | 8 ++++++++\n 2 files changed, 9 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 2dca969f9e..e7f3b468ea 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -192,7 +192,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->consume_sideband,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/hook.h b/hook.h\nindex b78a8e4805..1302efa590 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -66,6 +66,14 @@ struct run_hooks_opt\n \t */\n \tfeed_pipe_fn feed_pipe;\n \tvoid *feed_pipe_ctx;\n+\n+\t/*\n+\t * Populate this to capture output and prevent it from being printed to\n+\t * stderr. This will be passed directly through to\n+\t * run_command:run_parallel_processes(). See t/helper/test-run-command.c\n+\t * for an example.\n+\t */\n+\tconsume_sideband_fn consume_sideband;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427717","messageId":"patch-24.27-e74d49e559-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 24/27] receive-pack: convert 'update' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:58Z","receivedAt":"2021-06-17T10:24:11Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nThis makes use of the new sideband API in hook.h added in the\npreceding commit.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 65 ++++++++++++++++++++++++++++--------------\n 1 file changed, 44 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 5248228ebf..378f8f6b5d 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -938,33 +938,56 @@ static int run_receive_hook(struct command *commands,\n \treturn status;\n }\n \n-static int run_update_hook(struct command *cmd)\n+static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n {\n-\tconst char *argv[5];\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tint code;\n+\tint keepalive_active = 0;\n \n-\targv[0] = find_hook(\"update\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tif (keepalive_in_sec <= 0)\n+\t\tuse_keepalive = KEEPALIVE_NEVER;\n+\tif (use_keepalive == KEEPALIVE_ALWAYS)\n+\t\tkeepalive_active = 1;\n \n-\targv[1] = cmd->ref_name;\n-\targv[2] = oid_to_hex(&cmd->old_oid);\n-\targv[3] = oid_to_hex(&cmd->new_oid);\n-\targv[4] = NULL;\n+\t/* send a keepalive if there is no data to write */\n+\tif (keepalive_active && !output->len) {\n+\t\tstatic const char buf[] = \"0005\\1\";\n+\t\twrite_or_die(1, buf, sizeof(buf) - 1);\n+\t\treturn;\n+\t}\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.argv = argv;\n-\tproc.trace2_hook_name = \"update\";\n+\tif (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {\n+\t\tconst char *first_null = memchr(output->buf, '\\0', output->len);\n+\t\tif (first_null) {\n+\t\t\t/* The null bit is excluded. */\n+\t\t\tsize_t before_null = first_null - output->buf;\n+\t\t\tsize_t after_null = output->len - (before_null + 1);\n+\t\t\tkeepalive_active = 1;\n+\t\t\tsend_sideband(1, 2, output->buf, before_null, use_sideband);\n+\t\t\tsend_sideband(1, 2, first_null + 1, after_null, use_sideband);\n+\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n+}\n+\n+static int run_update_hook(struct command *cmd)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint code;\n+\n+\tstrvec_pushl(&opt.args,\n+\t\t     cmd->ref_name,\n+\t\t     oid_to_hex(&cmd->old_oid),\n+\t\t     oid_to_hex(&cmd->new_oid),\n+\t\t     NULL);\n \n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n \tif (use_sideband)\n-\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\treturn finish_command(&proc);\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\tcode = run_hooks(\"update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn code;\n }\n \n static struct command *find_command_by_refname(struct command *list,\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427718","messageId":"patch-25.27-0bdc4878ac-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 25/27] post-update: use hook.h library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:22:59Z","receivedAt":"2021-06-17T10:24:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 26 +++++++-------------------\n 1 file changed, 7 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 378f8f6b5d..b2ccdb66da 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1657,33 +1657,21 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *hook;\n-\n-\thook = find_hook(\"post-update\");\n-\tif (!hook)\n-\t\treturn;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\n \t\t\tcontinue;\n-\t\tif (!proc.args.nr)\n-\t\t\tstrvec_push(&proc.args, hook);\n-\t\tstrvec_push(&proc.args, cmd->ref_name);\n+\t\tstrvec_push(&opt.args, cmd->ref_name);\n \t}\n-\tif (!proc.args.nr)\n+\tif (!opt.args.nr)\n \t\treturn;\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.trace2_hook_name = \"post-update\";\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n \n-\tif (!start_command(&proc)) {\n-\t\tif (use_sideband)\n-\t\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\t\tfinish_command(&proc);\n-\t}\n+\trun_hooks(\"post-update\", &opt);\n+\trun_hooks_opt_clear(&opt);\n }\n \n static void check_aliased_update_internal(struct command *cmd,\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427719","messageId":"patch-26.27-db70b59b3b-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 26/27] receive-pack: convert receive hooks to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:23:00Z","receivedAt":"2021-06-17T10:24:13Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 197 +++++++++++++++++++----------------------\n 1 file changed, 90 insertions(+), 107 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex b2ccdb66da..ec90e10477 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -748,7 +748,7 @@ static int check_cert_push_options(const struct string_list *push_options)\n \treturn retval;\n }\n \n-static void prepare_push_cert_sha1(struct child_process *proc)\n+static void prepare_push_cert_sha1(struct run_hooks_opt *opt)\n {\n \tstatic int already_done;\n \n@@ -772,110 +772,42 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n \t}\n \tif (!is_null_oid(&push_cert_oid)) {\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT=%s\",\n \t\t\t     oid_to_hex(&push_cert_oid));\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_SIGNER=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_SIGNER=%s\",\n \t\t\t     sigcheck.signer ? sigcheck.signer : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_KEY=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_KEY=%s\",\n \t\t\t     sigcheck.key ? sigcheck.key : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_STATUS=%c\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_STATUS=%c\",\n \t\t\t     sigcheck.result);\n \t\tif (push_cert_nonce) {\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE=%s\",\n \t\t\t\t     push_cert_nonce);\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE_STATUS=%s\",\n \t\t\t\t     nonce_status);\n \t\t\tif (nonce_status == NONCE_SLOP)\n-\t\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t\t     \"GIT_PUSH_CERT_NONCE_SLOP=%ld\",\n \t\t\t\t\t     nonce_stamp_slop);\n \t\t}\n \t}\n }\n \n+struct receive_hook_feed_context {\n+\tstruct command *cmd;\n+\tint skip_broken;\n+};\n+\n struct receive_hook_feed_state {\n \tstruct command *cmd;\n \tstruct ref_push_report *report;\n \tint skip_broken;\n \tstruct strbuf buf;\n-\tconst struct string_list *push_options;\n };\n \n-typedef int (*feed_fn)(void *, const char **, size_t *);\n-static int run_and_feed_hook(const char *hook_name, feed_fn feed,\n-\t\t\t     struct receive_hook_feed_state *feed_state)\n-{\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct async muxer;\n-\tconst char *argv[2];\n-\tint code;\n-\n-\targv[0] = find_hook(hook_name);\n-\tif (!argv[0])\n-\t\treturn 0;\n-\n-\targv[1] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = hook_name;\n-\n-\tif (feed_state->push_options) {\n-\t\tint i;\n-\t\tfor (i = 0; i < feed_state->push_options->nr; i++)\n-\t\t\tstrvec_pushf(&proc.env_array,\n-\t\t\t\t     \"GIT_PUSH_OPTION_%d=%s\", i,\n-\t\t\t\t     feed_state->push_options->items[i].string);\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT=%d\",\n-\t\t\t     feed_state->push_options->nr);\n-\t} else\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT\");\n-\n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&proc.env_array, tmp_objdir_env(tmp_objdir));\n-\n-\tif (use_sideband) {\n-\t\tmemset(&muxer, 0, sizeof(muxer));\n-\t\tmuxer.proc = copy_to_sideband;\n-\t\tmuxer.in = -1;\n-\t\tcode = start_async(&muxer);\n-\t\tif (code)\n-\t\t\treturn code;\n-\t\tproc.err = muxer.in;\n-\t}\n-\n-\tprepare_push_cert_sha1(&proc);\n-\n-\tcode = start_command(&proc);\n-\tif (code) {\n-\t\tif (use_sideband)\n-\t\t\tfinish_async(&muxer);\n-\t\treturn code;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\n-\twhile (1) {\n-\t\tconst char *buf;\n-\t\tsize_t n;\n-\t\tif (feed(feed_state, &buf, &n))\n-\t\t\tbreak;\n-\t\tif (write_in_full(proc.in, buf, n) < 0)\n-\t\t\tbreak;\n-\t}\n-\tclose(proc.in);\n-\tif (use_sideband)\n-\t\tfinish_async(&muxer);\n-\n-\tsigchain_pop(SIGPIPE);\n-\n-\treturn finish_command(&proc);\n-}\n-\n-static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n+static int feed_receive_hook(void *state_)\n {\n \tstruct receive_hook_feed_state *state = state_;\n \tstruct command *cmd = state->cmd;\n@@ -884,9 +816,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t       state->skip_broken && (cmd->error_string || cmd->did_not_exist))\n \t\tcmd = cmd->next;\n \tif (!cmd)\n-\t\treturn -1; /* EOF */\n-\tif (!bufp)\n-\t\treturn 0; /* OK, can feed something. */\n+\t\treturn 1; /* EOF - close the pipe*/\n \tstrbuf_reset(&state->buf);\n \tif (!state->report)\n \t\tstate->report = cmd->report;\n@@ -910,32 +840,36 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t\t\t    cmd->ref_name);\n \t\tstate->cmd = cmd->next;\n \t}\n-\tif (bufp) {\n-\t\t*bufp = state->buf.buf;\n-\t\t*sizep = state->buf.len;\n-\t}\n \treturn 0;\n }\n \n-static int run_receive_hook(struct command *commands,\n-\t\t\t    const char *hook_name,\n-\t\t\t    int skip_broken,\n-\t\t\t    const struct string_list *push_options)\n+static int feed_receive_hook_cb(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n {\n-\tstruct receive_hook_feed_state state;\n-\tint status;\n-\n-\tstrbuf_init(&state.buf, 0);\n-\tstate.cmd = commands;\n-\tstate.skip_broken = skip_broken;\n-\tstate.report = NULL;\n-\tif (feed_receive_hook(&state, NULL, NULL))\n-\t\treturn 0;\n-\tstate.cmd = commands;\n-\tstate.push_options = push_options;\n-\tstatus = run_and_feed_hook(hook_name, feed_receive_hook, &state);\n-\tstrbuf_release(&state.buf);\n-\treturn status;\n+\tstruct hook *hook = pp_task_cb;\n+\tstruct receive_hook_feed_state *feed_state = hook->feed_pipe_cb_data;\n+\tint rc;\n+\n+\t/* first-time setup */\n+\tif (!feed_state) {\n+\t\tstruct hook_cb_data *hook_cb = pp_cb;\n+\t\tstruct run_hooks_opt *opt = hook_cb->options;\n+\t\tstruct receive_hook_feed_context *ctx = opt->feed_pipe_ctx;\n+\t\tif (!ctx)\n+\t\t\tBUG(\"run_hooks_opt.feed_pipe_ctx required for receive hook\");\n+\n+\t\tfeed_state = xmalloc(sizeof(struct receive_hook_feed_state));\n+\t\tstrbuf_init(&feed_state->buf, 0);\n+\t\tfeed_state->cmd = ctx->cmd;\n+\t\tfeed_state->skip_broken = ctx->skip_broken;\n+\t\tfeed_state->report = NULL;\n+\n+\t\thook->feed_pipe_cb_data = feed_state;\n+\t}\n+\n+\trc = feed_receive_hook(feed_state);\n+\tif (!rc)\n+\t\tstrbuf_addbuf(pipe, &feed_state->buf);\n+\treturn rc;\n }\n \n static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n@@ -971,6 +905,55 @@ static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n \tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n }\n \n+static int run_receive_hook(struct command *commands,\n+\t\t\t    const char *hook_name,\n+\t\t\t    int skip_broken,\n+\t\t\t    const struct string_list *push_options)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct receive_hook_feed_context ctx;\n+\tint rc;\n+\tstruct command *iter = commands;\n+\n+\t/* if there are no valid commands, don't invoke the hook at all. */\n+\twhile (iter && skip_broken && (iter->error_string || iter->did_not_exist))\n+\t\titer = iter->next;\n+\tif (!iter)\n+\t\treturn 0;\n+\n+\t/* pre-receive hooks should run in series as the hook updates refs */\n+\tif (!strcmp(hook_name, \"pre-receive\"))\n+\t\topt.jobs = 1;\n+\n+\tif (push_options) {\n+\t\tint i;\n+\t\tfor (i = 0; i < push_options->nr; i++)\n+\t\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_%d=%s\", i,\n+\t\t\t\t     push_options->items[i].string);\n+\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_COUNT=%d\", push_options->nr);\n+\t} else\n+\t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n+\n+\tif (tmp_objdir)\n+\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\n+\tprepare_push_cert_sha1(&opt);\n+\n+\t/* set up sideband printer */\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\t/* set up stdin callback */\n+\tctx.cmd = commands;\n+\tctx.skip_broken = skip_broken;\n+\topt.feed_pipe = feed_receive_hook_cb;\n+\topt.feed_pipe_ctx = &ctx;\n+\n+\trc = run_hooks(hook_name, &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn rc;\n+}\n+\n static int run_update_hook(struct command *cmd)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427720","messageId":"patch-27.27-d86fedf041-20210617T101217Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 27/27] hooks: fix a TOCTOU in \"did we run a hook?\" heuristic","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-17T10:23:01Z","receivedAt":"2021-06-17T10:24:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a Time-of-check to time-of-use (TOCTOU) race in code added in\n680ee550d72 (commit: skip discarding the index if there is no\npre-commit hook, 2017-08-14).\n\nWe can fix the race passing around information about whether or not we\nran the hook in question, instead of running hook_exists() after the\nfact to check if the hook in question exists. This problem has been\nnoted on-list when 680ee550d72 was discussed[1], but had not been\nfixed.\n\nIn addition to fixing this for the pre-commit hook as suggested there\nI'm also fixing this for the pre-merge-commit hook. See\n6098817fd7f (git-merge: honor pre-merge-commit hook, 2019-08-07) for\nthe introduction of its previous behavior.\n\nLet's also change this for the push-to-checkout hook. Now instead of\nchecking if the hook exists and either doing a push to checkout or a\npush to deploy we'll always attempt a push to checkout. If the hook\ndoesn't exist we'll fall back on push to deploy. The same behavior as\nbefore, without the TOCTOU race. See 0855331941b (receive-pack:\nsupport push-to-checkout hook, 2014-12-01) for the introduction of the\nprevious behavior.\n\nThis leaves uses of hook_exists() in two places that matter. The\n\"reference-transaction\" check in refs.c, see 67541597670 (refs:\nimplement reference transaction hook, 2020-06-19), and the\nprepare-commit-msg hook, see 66618a50f9c (sequencer: run\n'prepare-commit-msg' hook, 2018-01-24).\n\nIn both of those cases we're saving ourselves CPU time by not\npreparing data for the hook that we'll then do nothing with if we\ndon't have the hook. So using this \"invoked_hook\" pattern doesn't make\nsense in those cases.\n\nMore importantly, in those cases the worst we'll do is miss that we\n\"should\" run the hook because a new hook appeared, whereas in the\npre-commit and pre-merge-commit cases we'll skip an important\ndiscard_cache() on the bases of our faulty guess.\n\nI do think none of these races really matter in practice. It would be\nsome one-off issue as a hook was added or removed. I did think it was\nstupid that we didn't pass a \"did this run?\" flag instead of doing\nthis guessing at a distance though, so now we're not guessing anymore.\n\n1. https://lore.kernel.org/git/20170810191613.kpmhzg4seyxy3cpq@sigill.intra.peff.net/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c       | 18 +++++++++++-------\n builtin/merge.c        | 16 ++++++++++------\n builtin/receive-pack.c |  8 +++++---\n commit.c               |  1 +\n commit.h               |  3 ++-\n hook.c                 |  4 ++++\n hook.h                 | 10 ++++++++++\n sequencer.c            |  4 ++--\n 8 files changed, 45 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex dad4e56544..a66727a612 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -725,11 +725,13 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \tint clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);\n \tint old_display_comment_prefix;\n \tint merge_contains_scissors = 0;\n+\tint invoked_hook = 0;\n \n \t/* This checks and barfs if author is badly specified */\n \tdetermine_author_info(author_ident);\n \n-\tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(use_editor, index_file, &invoked_hook,\n+\t\t\t\t\t  \"pre-commit\", NULL))\n \t\treturn 0;\n \n \tif (squash_message) {\n@@ -1045,10 +1047,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && hook_exists(\"pre-commit\")) {\n+\tif (!no_verify && invoked_hook) {\n \t\t/*\n-\t\t * Re-read the index as pre-commit hook could have updated it,\n-\t\t * and write it out as a tree.  We must do this before we invoke\n+\t\t * Re-read the index as the pre-commit-commit hook was invoked\n+\t\t * and could have updated it. We must do this before we invoke\n \t\t * the editor and after we invoke run_status above.\n \t\t */\n \t\tdiscard_cache();\n@@ -1060,7 +1062,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n+\tif (run_commit_hook(use_editor, index_file, NULL, \"prepare-commit-msg\",\n \t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n \t\treturn 0;\n \n@@ -1077,7 +1079,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t}\n \n \tif (!no_verify &&\n-\t    run_commit_hook(use_editor, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n+\t    run_commit_hook(use_editor, index_file, NULL, \"commit-msg\",\n+\t\t\t    git_path_commit_editmsg(), NULL)) {\n \t\treturn 0;\n \t}\n \n@@ -1830,7 +1833,8 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \n \trepo_rerere(the_repository, 0);\n \trun_auto_maintenance(quiet);\n-\trun_commit_hook(use_editor, get_index_file(), \"post-commit\", NULL);\n+\trun_commit_hook(use_editor, get_index_file(), NULL, \"post-commit\",\n+\t\t\tNULL);\n \tif (amend && !no_post_rewrite) {\n \t\tcommit_post_rewrite(the_repository, current_head, &oid);\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 6128b60942..0425c9bf2b 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -844,15 +844,18 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n {\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *index_file = get_index_file();\n+\tint invoked_hook = 0;\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, index_file, \"pre-merge-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(0 < option_edit, index_file,\n+\t\t\t\t\t  &invoked_hook, \"pre-merge-commit\",\n+\t\t\t\t\t  NULL))\n \t\tabort_commit(remoteheads, NULL);\n \t/*\n-\t * Re-read the index as pre-merge-commit hook could have updated it,\n-\t * and write it out as a tree.  We must do this before we invoke\n+\t * Re-read the index as the pre-merge-commit hook was invoked\n+\t * and could have updated it. We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (hook_exists(\"pre-merge-commit\"))\n+\tif (invoked_hook)\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n@@ -873,7 +876,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tappend_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);\n \twrite_merge_heads(remoteheads);\n \twrite_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);\n-\tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n+\tif (run_commit_hook(0 < option_edit, get_index_file(), NULL,\n+\t\t\t    \"prepare-commit-msg\",\n \t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \tif (0 < option_edit) {\n@@ -882,7 +886,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t}\n \n \tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n-\t\t\t\t\t  \"commit-msg\",\n+\t\t\t\t\t  NULL, \"commit-msg\",\n \t\t\t\t\t  git_path_merge_msg(the_repository), NULL))\n \t\tabort_commit(remoteheads, NULL);\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ec90e10477..cd658f41d5 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1439,10 +1439,12 @@ static const char *push_to_deploy(unsigned char *sha1,\n static const char *push_to_checkout_hook = \"push-to-checkout\";\n \n static const char *push_to_checkout(unsigned char *hash,\n+\t\t\t\t    int *invoked_hook,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\topt.invoked_hook = invoked_hook;\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n \tstrvec_pushv(&opt.env, env->v);\n@@ -1460,6 +1462,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n {\n \tconst char *retval, *work_tree, *git_dir = NULL;\n \tstruct strvec env = STRVEC_INIT;\n+\tint invoked_hook = 0;\n \n \tif (worktree && worktree->path)\n \t\twork_tree = worktree->path;\n@@ -1477,10 +1480,9 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!hook_exists(push_to_checkout_hook))\n+\tretval = push_to_checkout(sha1, &invoked_hook, &env, work_tree);\n+\tif (!invoked_hook)\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n-\telse\n-\t\tretval = push_to_checkout(sha1, &env, work_tree);\n \n \tstrvec_clear(&env);\n \treturn retval;\ndiff --git a/commit.c b/commit.c\nindex e8147a88fc..cf62ebceae 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1697,6 +1697,7 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n }\n \n int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook,\n \t\t    const char *name, ...)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\ndiff --git a/commit.h b/commit.h\nindex df42eb434f..b5a542993c 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -363,7 +363,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\n \n LAST_ARG_MUST_BE_NULL\n-int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);\n+int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook, const char *name, ...);\n \n /* Sign a commit or tag buffer, storing the result in a header. */\n int sign_with_header(struct strbuf *buf, const char *keyid);\ndiff --git a/hook.c b/hook.c\nindex e7f3b468ea..31e822bf51 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -164,6 +164,9 @@ static int notify_hook_finished(int result,\n \n \thook_cb->rc |= result;\n \n+\tif (hook_cb->invoked_hook)\n+\t\t*hook_cb->invoked_hook = 1;\n+\n \treturn 1;\n }\n \n@@ -178,6 +181,7 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t.rc = 0,\n \t\t.hook_name = hook_name,\n \t\t.options = options,\n+\t\t.invoked_hook = options->invoked_hook,\n \t};\n \tif (options->absolute_path) {\n \t\tstrbuf_add_absolute_path(&abs_path, hook_path);\ndiff --git a/hook.h b/hook.h\nindex 1302efa590..9d9171672d 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -74,6 +74,15 @@ struct run_hooks_opt\n \t * for an example.\n \t */\n \tconsume_sideband_fn consume_sideband;\n+\n+\t/*\n+\t * A pointer which if provided will be set to 1 or 0 depending\n+\t * on if a hook was invoked (i.e. existed), regardless of\n+\t * whether or not that was successful. Used for avoiding\n+\t * TOCTOU races in code that would otherwise call hook_exist()\n+\t * after a \"maybe hook run\" to see if a hook was invoked.\n+\t */\n+\tint *invoked_hook;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -96,6 +105,7 @@ struct hook_cb_data {\n \tconst char *hook_name;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n+\tint *invoked_hook;\n };\n \n void run_hooks_opt_clear(struct run_hooks_opt *o);\ndiff --git a/sequencer.c b/sequencer.c\nindex ec2761e47d..2440b9dccd 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1204,7 +1204,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n \t} else {\n \t\targ1 = \"message\";\n \t}\n-\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n+\tif (run_commit_hook(0, r->index_file, NULL, \"prepare-commit-msg\", name,\n \t\t\t    arg1, arg2, NULL))\n \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n \n@@ -1534,7 +1534,7 @@ static int try_to_commit(struct repository *r,\n \t\tgoto out;\n \t}\n \n-\trun_commit_hook(0, r->index_file, \"post-commit\", NULL);\n+\trun_commit_hook(0, r->index_file, NULL, \"post-commit\", NULL);\n \tif (flags & AMEND_MSG)\n \t\tcommit_post_rewrite(r, current_head, oid);\n \n-- \n2.32.0.576.g59759b6ca7d\n\n"},{"id":"427919","messageId":"YM0Zr1uiI0+ShZa5@google.com","threadId":"55286","inReplyTo":"patch-27.27-d86fedf041-20210617T101217Z-avarab@gmail.com","subject":"Re: [PATCH 27/27] hooks: fix a TOCTOU in \"did we run a hook?\" heuristic","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-06-18T22:09:51Z","receivedAt":"2021-06-18T22:10:02Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Jun 17, 2021 at 12:23:01PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> Fix a Time-of-check to time-of-use (TOCTOU) race in code added in\n> 680ee550d72 (commit: skip discarding the index if there is no\n> pre-commit hook, 2017-08-14).\n> \n> We can fix the race passing around information about whether or not we\n> ran the hook in question, instead of running hook_exists() after the\n> fact to check if the hook in question exists. This problem has been\n> noted on-list when 680ee550d72 was discussed[1], but had not been\n> fixed.\n> \n> In addition to fixing this for the pre-commit hook as suggested there\n> I'm also fixing this for the pre-merge-commit hook. See\n> 6098817fd7f (git-merge: honor pre-merge-commit hook, 2019-08-07) for\n> the introduction of its previous behavior.\n> \n> Let's also change this for the push-to-checkout hook. Now instead of\n> checking if the hook exists and either doing a push to checkout or a\n> push to deploy we'll always attempt a push to checkout. If the hook\n> doesn't exist we'll fall back on push to deploy. The same behavior as\n> before, without the TOCTOU race. See 0855331941b (receive-pack:\n> support push-to-checkout hook, 2014-12-01) for the introduction of the\n> previous behavior.\n> \n> This leaves uses of hook_exists() in two places that matter. The\n> \"reference-transaction\" check in refs.c, see 67541597670 (refs:\n> implement reference transaction hook, 2020-06-19), and the\n> prepare-commit-msg hook, see 66618a50f9c (sequencer: run\n> 'prepare-commit-msg' hook, 2018-01-24).\n> \n> In both of those cases we're saving ourselves CPU time by not\n> preparing data for the hook that we'll then do nothing with if we\n> don't have the hook. So using this \"invoked_hook\" pattern doesn't make\n> sense in those cases.\n> \n> More importantly, in those cases the worst we'll do is miss that we\n> \"should\" run the hook because a new hook appeared, whereas in the\n> pre-commit and pre-merge-commit cases we'll skip an important\n> discard_cache() on the bases of our faulty guess.\n> \n> I do think none of these races really matter in practice. It would be\n> some one-off issue as a hook was added or removed. I did think it was\n> stupid that we didn't pass a \"did this run?\" flag instead of doing\n> this guessing at a distance though, so now we're not guessing anymore.\n\nYeah, I think your solution is very neat. I like this patch.\n\n - Emily\n"},{"id":"427921","messageId":"YM0alLP5U81D+ZI0@google.com","threadId":"55286","inReplyTo":"87lf7bzbrk.fsf@evledraar.gmail.com","subject":"Re: [PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-06-18T22:13:40Z","receivedAt":"2021-06-18T22:13:50Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Jun 15, 2021 at 11:36:26AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > Overall, I think I like the direction your reroll is going - I've needed\n> > some time to process it. Hopefully I'll be able to get through all or\n> > most of the series this week, but there's a lot going on here, too. I'll\n> > do what I can. Thanks for the help.\n> \n> Yeah, will reply to any qusetions etc; and as noted above my initial\n> goal here was \"hey, what about this approach\", so if you wanted to pick\n> this up & run with it...\n> \n> This particular version of the series is at github.com/avar/git.git's\n> es-avar/config-based-hooks-3 b.t.w.\n\nHave finished scanning through the rest of the series, and I think I\nunderstand your goal a little better - you are not saying \"let me take\nover and drive this part of the feature set\", which is what I thought\ninitially. Instead, you seem to be saying \"let's chop it up this way\ninstead\".\n\nI don't dislike the reorganization, but I do still wonder whether it's\na setback to the progress the original series had made. I guess it is\nhard to know - I had thought the original series was pretty much ready\nto go in, therefore making \"what if we ordered it this way\" moot. But it\nseems that you disagree.\n\nAnyway, I do hear also that you don't have interest in driving this\nsubset to completion, and that's fine. Correct me if I'm wrong.\n\nI'll keep thinking on this over the weekend. Thanks for the suggestion.\n\n - Emily\n"},{"id":"427994","messageId":"87im2848pn.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YM0alLP5U81D+ZI0@google.com","subject":"Re: [PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-06-20T19:30:59Z","receivedAt":"2021-06-20T19:50:02Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Fri, Jun 18 2021, Emily Shaffer wrote:\n\n> On Tue, Jun 15, 2021 at 11:36:26AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> > Overall, I think I like the direction your reroll is going - I've needed\n>> > some time to process it. Hopefully I'll be able to get through all or\n>> > most of the series this week, but there's a lot going on here, too. I'll\n>> > do what I can. Thanks for the help.\n>> \n>> Yeah, will reply to any qusetions etc; and as noted above my initial\n>> goal here was \"hey, what about this approach\", so if you wanted to pick\n>> this up & run with it...\n>> \n>> This particular version of the series is at github.com/avar/git.git's\n>> es-avar/config-based-hooks-3 b.t.w.\n>\n> Have finished scanning through the rest of the series, and I think I\n> understand your goal a little better - you are not saying \"let me take\n> over and drive this part of the feature set\", which is what I thought\n> initially. Instead, you seem to be saying \"let's chop it up this way\n> instead\".\n\nYes, 30-some patches that both refactor and introduce new behavior are\nharder to reason about.\n\nI've also had suggestions about the end-state, but I think whatever we\narrive at doing the scaffolding first without behavior changes makes\nsense.\n\n> I don't dislike the reorganization, but I do still wonder whether it's\n> a setback to the progress the original series had made. I guess it is\n> hard to know - I had thought the original series was pretty much ready\n> to go in, therefore making \"what if we ordered it this way\" moot. But it\n> seems that you disagree.\n\nI'm still not sure if I disagree, well, I'm 95% sure I disagree with\nsome of the end-state, but you never replied to my questions about that:\nhttps://lore.kernel.org/git/87mtv8fww3.fsf@evledaraar.gmail.com/ &\nhttps://lore.kernel.org/git/87lf80l1m6.fsf@evledraar.gmail.com/; So I\ndon't know for sure, maybe there's things I missed there.\n\nI think since Junio picked up the \"base\" version of this and it looks\nlike we're going that way first that's not something we need to discuss\nnow if you'd like to punt it, but I'd really like to get that cleared up\npost-base topic.\n\nIn brief summary:\n\nI'm 100% with you on hooks being driven by config, that they aren't is\nin hindsight a historical wart. Ditto the parallel execution etc. (which\nI'd suggested in an earlier iteration). That's all great.\n\nWhere you lose me is the need for having \"git hook\" be an administrative\ninterface for it, particularly (as noted in the linked E-Mail) since the\nneed for that over simply using \"git config\", or a trivial \"git config\"\nwrapper seems to be fallout from other arbitrary design choices.\n\nI.e. that all the config for a hook needing to be discovered by a\ntwo-pass iteration over the config space (or keeping state), as opposed\nto a \"hookcfg.<name>.*\" (or whatever) prefix.\n\nMaybe that makes sense in the eventual end-state, your series has the\nequivalent of \"WIP, more will be added later\" around that \"git hook\"\ncommand; but not having the full overview of that I think we can make\nsimpler inroads into getting us all of the practical featureset we want,\nwithout regretting our choices in command & config interfaces later.\n\n> Anyway, I do hear also that you don't have interest in driving this\n> subset to completion, and that's fine. Correct me if I'm wrong.\n\nI submitted a v3 of this (which I forgot to label as such in the\nsubject) at\nhttps://lore.kernel.org/git/cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com/;\ngiven the timing our E-Mails may have crossed.\n\nBut no, I will drive this subset to completion. What I meant with the\n\"run with it\" comment and the earlier reply on v1 of my \"base\" version\nhere: https://lore.kernel.org/git/87y2bs7gyc.fsf@evledraar.gmail.com/\n\n... is that I'd be happier if I managed to just convince you that the\nmore piecemeal approach is better, and something you'd want to pick up &\ndrive going forward.\n\nI.e. it's still >95% your code, just re-arranged and split into subsets\nof your patches. I really did not mean to \"steal\" it, it's just\nsomething I hacked up one day to see if the more incremental approach\nI'd been suggesting (and felt you were either ignoring or were too busy\nto address) was something that could work.\n"},{"id":"428015","messageId":"xmqqk0mnj30d.fsf@gitster.g","threadId":"55286","inReplyTo":"87im2848pn.fsf@evledraar.gmail.com","subject":"Re: [PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-06-21T03:44:02Z","receivedAt":"2021-06-21T03:44:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:\n\n> I think since Junio picked up the \"base\" version of this and it looks\n> like we're going that way first that's not something we need to discuss\n> now if you'd like to punt it, but I'd really like to get that cleared up\n> post-base topic.\n\nI queued this 'base' thing only because (1) Emily promised to review\nit and I wanted to give her a version of \"seen\" she can try to build\non the part beyond \"refactoring\", (2) I wanted to see how the 'base'\nlooks myself and (3) I wanted to see what possible interactions with\nother topics in flight I should anticipate, if we were to decide to\ngo that route.  Please do not read more than that into what is and\nwhat is not in 'seen'.\n\n"},{"id":"428146","messageId":"YNEoMZ1QhZwmS0v2@google.com","threadId":"55286","inReplyTo":"87im2848pn.fsf@evledraar.gmail.com","subject":"Re: [PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-06-22T00:00:49Z","receivedAt":"2021-06-22T00:01:00Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Sun, Jun 20, 2021 at 09:30:59PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> On Fri, Jun 18 2021, Emily Shaffer wrote:\n> Yes, 30-some patches that both refactor and introduce new behavior are\n> harder to reason about.\n> \n> I've also had suggestions about the end-state, but I think whatever we\n> arrive at doing the scaffolding first without behavior changes makes\n> sense.\n> \n> > I don't dislike the reorganization, but I do still wonder whether it's\n> > a setback to the progress the original series had made. I guess it is\n> > hard to know - I had thought the original series was pretty much ready\n> > to go in, therefore making \"what if we ordered it this way\" moot. But it\n> > seems that you disagree.\n> \n> I'm still not sure if I disagree, well, I'm 95% sure I disagree with\n> some of the end-state, but you never replied to my questions about that:\n> https://lore.kernel.org/git/87mtv8fww3.fsf@evledaraar.gmail.com/ &\n> https://lore.kernel.org/git/87lf80l1m6.fsf@evledraar.gmail.com/; So I\n> don't know for sure, maybe there's things I missed there.\n> \n> I think since Junio picked up the \"base\" version of this and it looks\n> like we're going that way first that's not something we need to discuss\n> now if you'd like to punt it, but I'd really like to get that cleared up\n> post-base topic.\n> \n> In brief summary:\n> \n> I'm 100% with you on hooks being driven by config, that they aren't is\n> in hindsight a historical wart. Ditto the parallel execution etc. (which\n> I'd suggested in an earlier iteration). That's all great.\n> \n> Where you lose me is the need for having \"git hook\" be an administrative\n> interface for it, particularly (as noted in the linked E-Mail) since the\n> need for that over simply using \"git config\", or a trivial \"git config\"\n> wrapper seems to be fallout from other arbitrary design choices.\n> \n> I.e. that all the config for a hook needing to be discovered by a\n> two-pass iteration over the config space (or keeping state), as opposed\n> to a \"hookcfg.<name>.*\" (or whatever) prefix.\n> \n> Maybe that makes sense in the eventual end-state, your series has the\n> equivalent of \"WIP, more will be added later\" around that \"git hook\"\n> command; but not having the full overview of that I think we can make\n> simpler inroads into getting us all of the practical featureset we want,\n> without regretting our choices in command & config interfaces later.\n> \n> > Anyway, I do hear also that you don't have interest in driving this\n> > subset to completion, and that's fine. Correct me if I'm wrong.\n> \n> I submitted a v3 of this (which I forgot to label as such in the\n> subject) at\n> https://lore.kernel.org/git/cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com/;\n> given the timing our E-Mails may have crossed.\n> \n> But no, I will drive this subset to completion. What I meant with the\n> \"run with it\" comment and the earlier reply on v1 of my \"base\" version\n> here: https://lore.kernel.org/git/87y2bs7gyc.fsf@evledraar.gmail.com/\n> \n> ... is that I'd be happier if I managed to just convince you that the\n> more piecemeal approach is better, and something you'd want to pick up &\n> drive going forward.\n> \n> I.e. it's still >95% your code, just re-arranged and split into subsets\n> of your patches. I really did not mean to \"steal\" it, it's just\n> something I hacked up one day to see if the more incremental approach\n> I'd been suggesting (and felt you were either ignoring or were too busy\n> to address) was something that could work.\n\nOk. Thanks for clarifying.\n\nYes, I do like this direction, and I'm pleased you were able to chop it\nup in a way where partial submission made sense - I struggled with that,\nmyself. Yes, I am excited that you want to drive this series :) :) and\nwill be happy to rebase on top of it.\n\nI'll have a look at the range-diff for v3 this week. Thanks.\n\n - Emily\n"},{"id":"428491","messageId":"60d61d1e44307_cc8d20878@natae.notmuch","threadId":"55286","inReplyTo":"87y2bs7gyc.fsf@evledraar.gmail.com","subject":"Re: [PATCH 00/31] minimal restart of \"config-based-hooks\"","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-25T18:14:54Z","receivedAt":"2021-06-25T18:14:58Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n\n> I'm a bit surprised at what seems to be some hostility or annoyance that\n> I submitted this as a set of patches. That's ultimately something that\n> saves everyone involved time (well, except me by coming up with said\n> patches). To borrow some words:\n> \n>     \"Talk is cheap. Show me the code.\" ― Linus Torvalds.\n\nWords I live by.\n\n> If I give you feedback suggesting that maybe we should reorganize this\n> thing to split out refactorings from behavior changes I'm asking you to\n> do extra work. Ultimately neither I, you nor anyone else can really know\n> if such a proposed effort is going to be better until it happens.\n\nIndeed. That's why a lot of time instead of simply replying to a mail\nwith an idea, I actually attempt to code the idea, and only then send te\nreply.\n\nI would say 90% of the time what I originally thought changes once I\nactually try to implement it.\n\n> It's can be really hard to see how/where to split things when you're the\n> author of the code. It's really hard in the \"theory of mind\" sense of\n> things to explain an idea to someone who doesn't have the information\n> you have.\n\nMore like impossible.\n\nNo comic simply writes an act and goes to an auditorium like Carnegie\nHall to simply present it knowing full well how people are going to\nreact to it.\n\nYou never really know how other people are going to react, so it's\nbetter to not make assumptions, just try and find out.\n\n> I think I've made a good argument above for why this takes you a step\n> forward, not backwards, I'm hoping despite this initial reply that\n> you'll come to agree on that.\n\nNot to mention that the goal is not to land Emily's patches, the goal is\nto improve the code while minimizing the potential breakage. For that we\nneed as many eyes as possible, and in my opinion your reorganization\npatches totally help in that regard.\n\nThe fact that this makes it easier to land Emily's patches is an added\nbenefit.\n\n> In any case, writing code is hard, but splitting it up like I've done\n> here is rather easily done. It took me about a day with waiting for\n> \"rebase -i --exec='make test'\" equivalent, and that's from being mostly\n> unfamiliar with the code in question beforehand.\n\nIt's easy if you are an expert at rebasing, which not many people are.\n\nBut you can't become an expert if you don't do it, over and over. So\nit's usually better to not think too much about it and simply do it.\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"428492","messageId":"60d61ed6a45dd_cc8d20891@natae.notmuch","threadId":"55286","inReplyTo":"cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com","subject":"RE: [PATCH v2 00/30] Minimal restart of \"config-based-hooks\"","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-25T18:22:14Z","receivedAt":"2021-06-25T18:22:20Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> I proposed splitting Emily's \"hook config\" topic[1] into at least a\n> topic that retains all current behavior of the codebase, and merely\n> refactors existing behavior to new APIs, and then doing behavior\n> changes later.\n\nThis is good but can you add an introduction that is more than \"Emily's\nhook config topic\"? What is this patch series doing? (assume that\nEmily's patches don't exist)\n\n-- \nFelipe Contreras"},{"id":"428493","messageId":"60d62147d2661_cc8d2089d@natae.notmuch","threadId":"55286","inReplyTo":"patch-13.30-0300607a9b4-20210614T101920Z-avarab@gmail.com","subject":"RE: [PATCH v2 13/30] read-cache: convert post-index-change hook to use config","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-25T18:32:39Z","receivedAt":"2021-06-25T18:32:46Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> From: Emily Shaffer <emilyshaffer@google.com>\n> \n> By using hook.h instead of run-command.h to run, post-index-change hooks\n> can now be specified in the config in addition to the hookdir.\n> post-index-change is not run anywhere besides in read-cache.c.\n> \n> This removes the last direct user of run_hook_ve(), so we can make the\n> function static now. It'll be removed entirely soon.\n\nThis is mixing two logically independent changes. For the same reason I\nsuggested to split the run_hook_le() removal I think this should be\nsplit too.\n\nNot a big deal though, especially since the splitted patch would be very\nsmall.\n\n-- \nFelipe Contreras"},{"id":"428494","messageId":"60d621b2c36d3_cc8d208c6@natae.notmuch","threadId":"55286","inReplyTo":"patch-15.30-477eb2245c3-20210614T101920Z-avarab@gmail.com","subject":"RE: [PATCH v2 15/30] run-command: remove old run_hook_{le,ve}() hook API","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-25T18:34:26Z","receivedAt":"2021-06-25T18:34:34Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Ævar Arnfjörð Bjarmason wrote:\n> From: Emily Shaffer <emilyshaffer@google.com>\n> \n> The new hook.h library has replaced all run-command.h hook-related\n> functionality. So let's delete this dead code.\n\nAhhh, much less mental load of the reviewer ;)\n\n-- \nFelipe Contreras"},{"id":"428495","messageId":"60d6282e246e5_cc8d2085c@natae.notmuch","threadId":"55286","inReplyTo":"YM0alLP5U81D+ZI0@google.com","subject":"Re: [PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-25T19:02:06Z","receivedAt":"2021-06-25T19:02:10Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Emily Shaffer wrote:\n> On Tue, Jun 15, 2021 at 11:36:26AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > > Overall, I think I like the direction your reroll is going - I've needed\n> > > some time to process it. Hopefully I'll be able to get through all or\n> > > most of the series this week, but there's a lot going on here, too. I'll\n> > > do what I can. Thanks for the help.\n> > \n> > Yeah, will reply to any qusetions etc; and as noted above my initial\n> > goal here was \"hey, what about this approach\", so if you wanted to pick\n> > this up & run with it...\n> > \n> > This particular version of the series is at github.com/avar/git.git's\n> > es-avar/config-based-hooks-3 b.t.w.\n> \n> Have finished scanning through the rest of the series, and I think I\n> understand your goal a little better - you are not saying \"let me take\n> over and drive this part of the feature set\", which is what I thought\n> initially. Instead, you seem to be saying \"let's chop it up this way\n> instead\".\n\nIndeed. In particular Ævar's chopping allowed me to visualize what the\npatches were trying to do and it was much easier to review. Step by\nstep. I don't know about others, but I think it's similar.\n\n> I don't dislike the reorganization, but I do still wonder whether it's\n> a setback to the progress the original series had made. I guess it is\n> hard to know - I had thought the original series was pretty much ready\n> to go in, therefore making \"what if we ordered it this way\" moot. But it\n> seems that you disagree.\n> \n> Anyway, I do hear also that you don't have interest in driving this\n> subset to completion, and that's fine. Correct me if I'm wrong.\n\nIn an open source project nobody \"owns\" a set of patches, we can all\nwork on them collaboratively.\n\n> I'll keep thinking on this over the weekend. Thanks for the suggestion.\n\nMy suggestion is to not think about it too much. Just find what would be\nthe next logical step to do on top of Ævar's base and simply do it\n(cherry-pick or rebase).\n\nBy simply trying it out you would get a much better idea of how the\nseries could progress to the end-goal you have in mind.\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"428496","messageId":"60d629b28a7ce_cc8d20882@natae.notmuch","threadId":"55286","inReplyTo":"YMfLO9CT+iIDR3OA@google.com","subject":"Re: [PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-06-25T19:08:34Z","receivedAt":"2021-06-25T19:08:44Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Emily Shaffer wrote:\n> On Mon, Jun 14, 2021 at 12:32:50PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > \n> > In order to enable hooks to be run as an external process, by a\n> > standalone Git command, or by tools which wrap Git, provide an external\n> > means to run all configured hook commands for a given hook event.\n> \n> From what it says on the box, I'm slightly worried about this patch\n> doing too much at once, but let's see... (I think this is also a common\n> thing you and I disagree on - how much work to do per commit - so feel\n> free to ignore me ;) )\n\nFrom my cursory look this is the only big patch, and I trust the reason\nÆvar made it so big is that he couldn't figure out a way to chop it into\nsmaller pieces that would somehow work.\n\n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> \n> Thanks for including attribution - I appreciate it.\n\nIt's not really an attribution, since he used your code he is pretty\nmuch obligated to put your s-o-b, see Developer Certificate of Origin\n\nhttps://en.wikipedia.org/wiki/Developer_Certificate_of_Origin\n\n-- \nFelipe Contreras"},{"id":"428657","messageId":"xmqqzgv93244.fsf@gitster.g","threadId":"55286","inReplyTo":"YNEoMZ1QhZwmS0v2@google.com","subject":"Re: [PATCH v2 01/30] hook: add 'run' subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-06-29T01:12:43Z","receivedAt":"2021-06-29T01:12:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> Ok. Thanks for clarifying.\n>\n> Yes, I do like this direction, and I'm pleased you were able to chop it\n> up in a way where partial submission made sense - I struggled with that,\n> myself. Yes, I am excited that you want to drive this series :) :) and\n> will be happy to rebase on top of it.\n>\n> I'll have a look at the range-diff for v3 this week. Thanks.\n\nThanks for working together so well.  Very pleased to see\ncontributors and reviewers working together constructively.\n\n"},{"id":"429090","messageId":"YN+ljLU7b8o7AF3J@google.com","threadId":"55286","inReplyTo":"patch-07.27-12347d901b-20210617T101217Z-avarab@gmail.com","subject":"Re: [PATCH 07/27] git hook run: add an --ignore-missing flag","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-02T23:47:24Z","receivedAt":"2021-07-02T23:47:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Jun 17, 2021 at 12:22:41PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> For certain one-shot hooks we'd like to optimistically run them, and\n> not complain if they don't exist. This will be used by send-email in a\n> subsequent commit.\n> \n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  Documentation/git-hook.txt | 10 +++++++++-\n>  builtin/hook.c             |  8 +++++++-\n>  t/t1800-hook.sh            |  7 ++++++-\n>  3 files changed, 22 insertions(+), 3 deletions(-)\n> \n> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> index 660d6a992a..097fb9de63 100644\n> --- a/Documentation/git-hook.txt\n> +++ b/Documentation/git-hook.txt\n> @@ -8,7 +8,7 @@ git-hook - run git hooks\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -'git hook' run <hook-name> [-- <hook-args>]\n> +'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n>  \n>  DESCRIPTION\n>  -----------\n> @@ -29,6 +29,14 @@ optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n>  arguments (if any) differ by hook name, see linkgit:githooks[5] for\n>  what those are.\n>  \n> +OPTIONS\n> +-------\n> +\n> +--ignore-missing::\n> +\tIgnore any missing hook by quietly returning zero. Used for\n> +\ttools that want to do a blind one-shot run of a hook that may\n> +\tor may not be present.\n> +\n>  SEE ALSO\n>  --------\n>  linkgit:githooks[5]\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index 7714d31ef1..47e0de7bbc 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -22,10 +22,13 @@ static int run(int argc, const char **argv, const char *prefix)\n>  \tint i;\n>  \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>  \tint rc = 0;\n> +\tint ignore_missing = 0;\n>  \tconst char *hook_name;\n>  \tconst char *hook_path;\n>  \n>  \tstruct option run_options[] = {\n> +\t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n> +\t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n>  \t\tOPT_END(),\n>  \t};\n>  \n> @@ -49,11 +52,14 @@ static int run(int argc, const char **argv, const char *prefix)\n>  \t/*\n>  \t * We are not using run_hooks() because we'd like to detect\n>  \t * missing hooks. Let's find it ourselves and call\n> -\t * run_found_hooks() instead.\n> +\t * run_found_hooks() instead...\n>  \t */\n>  \thook_name = argv[0];\n>  \thook_path = find_hook(hook_name);\n>  \tif (!hook_path) {\n> +\t\t/* ... act like run_hooks() under --ignore-missing */\n> +\t\tif (ignore_missing)\n> +\t\t\treturn 0;\n>  \t\terror(\"cannot find a hook named %s\", hook_name);\n>  \t\treturn 1;\n>  \t}\n\nHave started to rebase the configgy stuff on top of this series, and the\nfinal implementation here is kind of confusing to me. The difference\nbetween run_hooks() and run_found_hooks() is the behavior when there's\nno hook to run; but you're checking for missing hooks before you even\nlet them try to run here in builtin/hook.c. So is it really necessary to\nhave the two methods? I think I'm missing something?\n\n> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> index ecd517b162..542e551628 100755\n> --- a/t/t1800-hook.sh\n> +++ b/t/t1800-hook.sh\n> @@ -23,7 +23,12 @@ test_expect_success 'git hook run: nonexistent hook' '\n>  \ttest_cmp stderr.expect stderr.actual\n>  '\n>  \n> -test_expect_success 'git hook run: basic' '\n> +test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '\n> +\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n> +\ttest_must_be_empty stderr.actual\n> +'\n> +\n> +test_expect_success 'git hook run -- basic' '\n>  \twrite_script .git/hooks/test-hook <<-EOF &&\n>  \techo Test hook\n>  \tEOF\n> -- \n> 2.32.0.576.g59759b6ca7d\n> \n"},{"id":"430276","messageId":"20210715232603.3415111-1-emilyshaffer@google.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH 0/9] config-based hooks restarted","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:25:54Z","receivedAt":"2021-07-15T23:26:13Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"The work formerly found at\nhttps://lore.kernel.org/git/20210527000856.695702-1-emilyshaffer@google.com\nhas been rebased on top of\nhttps://lore.kernel.org/git/cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com\n(ab/config-based-hooks-base).\n\nSome features have been dropped from v9 of the larger config-based-hooks\nseries, and many patches have been replaced by Ævar's new base.\n\nThis series no longer introduces the 'hook.runHooksDir' config - I think\nit is OK to introduce that later and quibble about it in isolation from\nthe rest of this work.\n\nI also would encourage folks interested in reviewing this to not spend\ntoo much time looking through it without first leaving a review on\nÆvar's series which this is based on; this series won't go anywhere\nwithout that one. :) (And yes, I owe a review on the more recent version\nof that series, too.)\n\nNote: I am worried that somewhere around patch 6 a flaky test may have\nbeen added - I saw flaky failures on the osx-clang and linux-musl tests\nwhen I ran CI on GitHub. I *think* this was due to missing a field when\ninitializing 'struct hook' for addition to the hook linked list, and I\n*think* I have fixed that, but if something jumps out to anyone in the\ncode or in the test, I'd really appreciate the hint. A passing CI run\nfor the tip of this series is at\nhttps://github.com/nasamuffin/git/actions/runs/1035559898.\n\n - Emily\n\nEmily Shaffer (9):\n  hook: run a list of hooks instead\n  hook: allow parallel hook execution\n  hook: introduce \"git hook list\"\n  hook: treat hookdir hook specially\n  hook: allow running non-native hooks\n  hook: include hooks from the config\n  hook: allow out-of-repo 'git hook' invocations\n  hook: teach 'hookcmd' config to alias hook scripts\n  hook: implement hookcmd.<name>.skip\n\n Documentation/config/hook.txt |  22 +++\n Documentation/git-hook.txt    |  97 ++++++++++++-\n builtin/am.c                  |  12 +-\n builtin/checkout.c            |   3 +-\n builtin/clone.c               |   3 +-\n builtin/gc.c                  |   3 +-\n builtin/hook.c                |  58 +++++++-\n builtin/merge.c               |   3 +-\n builtin/rebase.c              |   3 +-\n builtin/receive-pack.c        |  14 +-\n builtin/worktree.c            |   3 +-\n commit.c                      |   4 +-\n git.c                         |   2 +-\n hook.c                        | 251 +++++++++++++++++++++++++++++++---\n hook.h                        |  44 ++++--\n read-cache.c                  |   3 +-\n refs.c                        |   3 +-\n reset.c                       |   4 +-\n sequencer.c                   |   7 +-\n t/t1360-config-based-hooks.sh | 227 ++++++++++++++++++++++++++++++\n transport.c                   |   4 +-\n 21 files changed, 711 insertions(+), 59 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n create mode 100755 t/t1360-config-based-hooks.sh\n\n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430277","messageId":"20210715232603.3415111-2-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210715232603.3415111-1-emilyshaffer@google.com","subject":"[PATCH 1/9] hook: run a list of hooks instead","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:25:55Z","receivedAt":"2021-07-15T23:26:27Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"To prepare for multihook support, teach hook.[hc] to take a list of\nhooks at run_hooks and run_found_hooks. Right now the list is always one\nentry, but in the future we will allow users to supply more than one\nexecutable for a single hook event.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/hook.c |  9 +++---\n hook.c         | 85 ++++++++++++++++++++++++++++++++++++++++----------\n hook.h         | 15 ++++++++-\n 3 files changed, 87 insertions(+), 22 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 169a8dd08f..a41ff36da9 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -25,7 +25,7 @@ static int run(int argc, const char **argv, const char *prefix)\n \tint rc = 0;\n \tint ignore_missing = 0;\n \tconst char *hook_name;\n-\tconst char *hook_path;\n+\tstruct list_head *hooks;\n \n \tstruct option run_options[] = {\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n@@ -58,15 +58,16 @@ static int run(int argc, const char **argv, const char *prefix)\n \t * run_found_hooks() instead...\n \t */\n \thook_name = argv[0];\n-\thook_path = find_hook(hook_name);\n-\tif (!hook_path) {\n+\thooks = hook_list(hook_name);\n+\tif (list_empty(hooks)) {\n \t\t/* ... act like run_hooks() under --ignore-missing */\n \t\tif (ignore_missing)\n \t\t\treturn 0;\n \t\terror(\"cannot find a hook named %s\", hook_name);\n \t\treturn 1;\n \t}\n-\trc = run_found_hooks(hook_name, hook_path, &opt);\n+\n+\trc = run_found_hooks(hook_name, hooks, &opt);\n \n \trun_hooks_opt_clear(&opt);\n \ndiff --git a/hook.c b/hook.c\nindex 31e822bf51..c1dac6982f 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -4,6 +4,28 @@\n #include \"hook-list.h\"\n #include \"config.h\"\n \n+static void free_hook(struct hook *ptr)\n+{\n+\tif (ptr) {\n+\t\tfree(ptr->feed_pipe_cb_data);\n+\t}\n+\tfree(ptr);\n+}\n+\n+static void remove_hook(struct list_head *to_remove)\n+{\n+\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n+\tlist_del(to_remove);\n+\tfree_hook(hook_to_remove);\n+}\n+\n+void clear_hook_list(struct list_head *head)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, head)\n+\t\tremove_hook(pos);\n+}\n+\n static int known_hook(const char *name)\n {\n \tconst char **p;\n@@ -71,6 +93,30 @@ int hook_exists(const char *name)\n \treturn !!find_hook(name);\n }\n \n+struct list_head* hook_list(const char* hookname)\n+{\n+\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\n+\tINIT_LIST_HEAD(hook_head);\n+\n+\tif (!hookname)\n+\t\treturn NULL;\n+\n+\tif (have_git_dir()) {\n+\t\tconst char *hook_path = find_hook(hookname);\n+\n+\t\t/* Add the hook from the hookdir */\n+\t\tif (hook_path) {\n+\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n+\t\t\tto_add->hook_path = hook_path;\n+\t\t\tto_add->feed_pipe_cb_data = NULL;\n+\t\t\tlist_add_tail(&to_add->list, hook_head);\n+\t\t}\n+\t}\n+\n+\treturn hook_head;\n+}\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o)\n {\n \tstrvec_clear(&o->env);\n@@ -108,6 +154,8 @@ static int pick_next_hook(struct child_process *cp,\n \tstruct hook_cb_data *hook_cb = pp_cb;\n \tstruct hook *run_me = hook_cb->run_me;\n \n+\tif (!run_me)\n+\t\treturn 0;\n \n \t/* reopen the file for stdin; run_command closes it. */\n \tif (hook_cb->options->path_to_stdin) {\n@@ -126,7 +174,10 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n-\tstrvec_push(&cp->args, run_me->hook_path);\n+\tif (hook_cb->options->absolute_path)\n+\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n+\telse\n+\t\tstrvec_push(&cp->args, run_me->hook_path);\n \n \t/*\n \t * add passed-in argv, without expanding - let the user get back\n@@ -137,6 +188,13 @@ static int pick_next_hook(struct child_process *cp,\n \t/* Provide context for errors if necessary */\n \t*pp_task_cb = run_me;\n \n+\t/* Get the next entry ready */\n+\tif (hook_cb->run_me->list.next == hook_cb->head)\n+\t\thook_cb->run_me = NULL;\n+\telse\n+\t\thook_cb->run_me = list_entry(hook_cb->run_me->list.next,\n+\t\t\t\t\t     struct hook, list);\n+\n \treturn 1;\n }\n \n@@ -170,24 +228,17 @@ static int notify_hook_finished(int result,\n \treturn 1;\n }\n \n-int run_found_hooks(const char *hook_name, const char *hook_path,\n+int run_found_hooks(const char *hook_name, struct list_head *hooks,\n \t\t    struct run_hooks_opt *options)\n {\n-\tstruct strbuf abs_path = STRBUF_INIT;\n-\tstruct hook my_hook = {\n-\t\t.hook_path = hook_path,\n-\t};\n \tstruct hook_cb_data cb_data = {\n \t\t.rc = 0,\n \t\t.hook_name = hook_name,\n \t\t.options = options,\n \t\t.invoked_hook = options->invoked_hook,\n \t};\n-\tif (options->absolute_path) {\n-\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n-\t\tmy_hook.hook_path = abs_path.buf;\n-\t}\n-\tcb_data.run_me = &my_hook;\n+\n+\tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n \n \tif (options->jobs != 1)\n \t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n@@ -201,15 +252,13 @@ int run_found_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n-\tif (options->absolute_path)\n-\t\tstrbuf_release(&abs_path);\n \n \treturn cb_data.rc;\n }\n \n int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n {\n-\tconst char *hook_path;\n+\tstruct list_head *hooks;\n \tint ret;\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n@@ -217,15 +266,17 @@ int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n \tif (options->path_to_stdin && options->feed_pipe)\n \t\tBUG(\"choose only one method to populate stdin\");\n \n-\thook_path = find_hook(hook_name);\n+\thooks = hook_list(hook_name);\n \n \t/*\n \t * If you need to act on a missing hook, use run_found_hooks()\n \t * instead\n \t */\n-\tif (!hook_path)\n+\tif (list_empty(hooks))\n \t\treturn 0;\n \n-\tret = run_found_hooks(hook_name, hook_path, options);\n+\tret = run_found_hooks(hook_name, hooks, options);\n+\n+\tclear_hook_list(hooks);\n \treturn ret;\n }\ndiff --git a/hook.h b/hook.h\nindex 9d9171672d..b97237931b 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -3,6 +3,7 @@\n #include \"strbuf.h\"\n #include \"strvec.h\"\n #include \"run-command.h\"\n+#include \"list.h\"\n \n /*\n  * Returns the path to the hook file, or NULL if the hook is missing\n@@ -17,6 +18,7 @@ const char *find_hook(const char *name);\n int hook_exists(const char *hookname);\n \n struct hook {\n+\tstruct list_head list;\n \t/* The path to the hook */\n \tconst char *hook_path;\n \n@@ -27,6 +29,12 @@ struct hook {\n \tvoid *feed_pipe_cb_data;\n };\n \n+/*\n+ * Provides a linked list of 'struct hook' detailing commands which should run\n+ * in response to the 'hookname' event, in execution order.\n+ */\n+struct list_head* hook_list(const char *hookname);\n+\n struct run_hooks_opt\n {\n \t/* Environment vars to be set for each hook */\n@@ -103,6 +111,7 @@ struct hook_cb_data {\n \t/* rc reflects the cumulative failure state */\n \tint rc;\n \tconst char *hook_name;\n+\tstruct list_head *head;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n \tint *invoked_hook;\n@@ -120,6 +129,10 @@ int run_hooks(const char *hook_name, struct run_hooks_opt *options);\n  * Takes an already resolved hook and runs it. Internally the simpler\n  * run_hooks() will call this.\n  */\n-int run_found_hooks(const char *hookname, const char *hook_path,\n+int run_found_hooks(const char *hookname, struct list_head *hooks,\n \t\t    struct run_hooks_opt *options);\n+\n+/* Empties the list at 'head', calling 'free_hook()' on each entry */\n+void clear_hook_list(struct list_head *head);\n+\n #endif\n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430278","messageId":"20210715232603.3415111-3-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210715232603.3415111-1-emilyshaffer@google.com","subject":"[PATCH 2/9] hook: allow parallel hook execution","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:25:56Z","receivedAt":"2021-07-15T23:26:27Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"In many cases, there's no reason not to allow hooks to execute in\nparallel. run_processes_parallel() is well-suited - it's a task queue\nthat runs its housekeeping in series, which means users don't\nneed to worry about thread safety on their callback data. True\nmultithreaded execution with the async_* functions isn't necessary here.\nSynchronous hook execution can be achieved by only allowing 1 job to run\nat a time.\n\nTeach run_hooks() to use that function for simple hooks which don't\nrequire stdin or capture of stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/config/hook.txt |  4 ++++\n Documentation/git-hook.txt    | 17 ++++++++++++++++-\n builtin/am.c                  | 12 ++++++++----\n builtin/checkout.c            |  3 ++-\n builtin/clone.c               |  3 ++-\n builtin/gc.c                  |  3 ++-\n builtin/hook.c                |  6 +++++-\n builtin/merge.c               |  3 ++-\n builtin/rebase.c              |  3 ++-\n builtin/receive-pack.c        | 14 ++++++++++----\n builtin/worktree.c            |  3 ++-\n commit.c                      |  4 +++-\n hook.c                        | 32 +++++++++++++++++++++++++++++---\n hook.h                        | 11 +++++------\n read-cache.c                  |  3 ++-\n refs.c                        |  3 ++-\n reset.c                       |  4 +++-\n sequencer.c                   |  7 +++++--\n transport.c                   |  4 +++-\n 19 files changed, 107 insertions(+), 32 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nnew file mode 100644\nindex 0000000000..96d3d6572c\n--- /dev/null\n+++ b/Documentation/config/hook.txt\n@@ -0,0 +1,4 @@\n+hook.jobs::\n+\tSpecifies how many hooks can be run simultaneously during parallelized\n+\thook execution. If unspecified, defaults to the number of processors on\n+\tthe current system.\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex fa68c1f391..8bf82b5dd4 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,8 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n+\t<hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -42,6 +43,20 @@ OPTIONS\n \ttools that want to do a blind one-shot run of a hook that may\n \tor may not be present.\n \n+-j::\n+--jobs::\n+\tOnly valid for `run`.\n++\n+Specify how many hooks to run simultaneously. If this flag is not specified, use\n+the value of the `hook.jobs` config. If the config is not specified, use the\n+number of CPUs on the current system. Some hooks may be ineligible for\n+parallelization: for example, 'commit-msg' intends hooks modify the commit\n+message body and cannot be parallelized.\n+\n+CONFIGURATION\n+-------------\n+include::config/hook.txt[]\n+\n SEE ALSO\n --------\n linkgit:githooks[5]\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 6e4f9c8036..bdad38142a 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -445,8 +445,9 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \n+\trun_hooks_opt_init_sync(&opt);\n \tassert(state->msg);\n \tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n \tret = run_hooks(\"applypatch-msg\", &opt);\n@@ -467,9 +468,10 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tint ret;\n \n+\trun_hooks_opt_init_async(&opt);\n \tstrvec_push(&opt.args, \"rebase\");\n \topt.path_to_stdin = am_path(state, \"rewritten\");\n \n@@ -1602,9 +1604,10 @@ static void do_commit(const struct am_state *state)\n \tstruct commit_list *parents = NULL;\n \tconst char *reflog_msg, *author, *committer = NULL;\n \tstruct strbuf sb = STRBUF_INIT;\n-\tstruct run_hooks_opt hook_opt_pre = RUN_HOOKS_OPT_INIT;\n-\tstruct run_hooks_opt hook_opt_post = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt_pre;\n+\tstruct run_hooks_opt hook_opt_post;\n \n+\trun_hooks_opt_init_async(&hook_opt_pre);\n \tif (run_hooks(\"pre-applypatch\", &hook_opt_pre)) {\n \t\trun_hooks_opt_clear(&hook_opt_pre);\n \t\texit(1);\n@@ -1659,6 +1662,7 @@ static void do_commit(const struct am_state *state)\n \t\tfclose(fp);\n \t}\n \n+\trun_hooks_opt_init_async(&hook_opt_post);\n \trun_hooks(\"post-applypatch\", &hook_opt_post);\n \n \trun_hooks_opt_clear(&hook_opt_pre);\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 6205ace09f..be4450a433 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -107,9 +107,10 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tint rc;\n \n+\trun_hooks_opt_init_sync(&opt);\n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\n \tstrvec_pushl(&opt.args,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex de57a3119b..87cfbf60e5 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -776,7 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt;\n \n \tif (option_no_checkout)\n \t\treturn 0;\n@@ -822,6 +822,7 @@ static int checkout(int submodule_progress)\n \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n \t\tdie(_(\"unable to write new index file\"));\n \n+\trun_hooks_opt_init_sync(&hook_opt);\n \tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n \terr |= run_hooks(\"post-checkout\", &hook_opt);\n \trun_hooks_opt_clear(&hook_opt);\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex a12641a691..16890b097c 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -349,7 +349,7 @@ static void add_repack_incremental_option(void)\n \n static int need_to_gc(void)\n {\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt;\n \n \t/*\n \t * Setting gc.auto to 0 or negative can disable the\n@@ -397,6 +397,7 @@ static int need_to_gc(void)\n \telse\n \t\treturn 0;\n \n+\trun_hooks_opt_init_async(&hook_opt);\n \tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n \t\trun_hooks_opt_clear(&hook_opt);\n \t\treturn 0;\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex a41ff36da9..d196d8498c 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -21,7 +21,7 @@ static const char * const builtin_hook_run_usage[] = {\n static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tint rc = 0;\n \tint ignore_missing = 0;\n \tconst char *hook_name;\n@@ -32,9 +32,13 @@ static int run(int argc, const char **argv, const char *prefix)\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n \t\t\t   N_(\"file to read into hooks' stdin\")),\n+\t\tOPT_INTEGER('j', \"jobs\", &opt.jobs,\n+\t\t\t    N_(\"run up to <n> hooks simultaneously\")),\n \t\tOPT_END(),\n \t};\n \n+\trun_hooks_opt_init_async(&opt);\n+\n \targc = parse_options(argc, argv, prefix, run_options,\n \t\t\t     builtin_hook_run_usage,\n \t\t\t     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 0425c9bf2b..67c2eba053 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,7 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\n@@ -490,6 +490,7 @@ static void finish(struct commit *head_commit,\n \t}\n \n \t/* Run a post-merge hook */\n+\trun_hooks_opt_init_async(&opt);\n \tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n \trun_hooks(\"post-merge\", &opt);\n \trun_hooks_opt_clear(&opt);\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 2081f6fa8d..fe9f144cad 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1314,7 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\n@@ -2024,6 +2024,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t}\n \n \t/* If a hook exists, give it a chance to interrupt*/\n+\trun_hooks_opt_init_async(&hook_opt);\n \tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n \tif (!ok_to_skip_pre_rebase &&\n \t    run_hooks(\"pre-rebase\", &hook_opt)) {\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex cd658f41d5..e4726eb211 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -910,7 +910,7 @@ static int run_receive_hook(struct command *commands,\n \t\t\t    int skip_broken,\n \t\t\t    const struct string_list *push_options)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tstruct receive_hook_feed_context ctx;\n \tint rc;\n \tstruct command *iter = commands;\n@@ -922,6 +922,7 @@ static int run_receive_hook(struct command *commands,\n \t\treturn 0;\n \n \t/* pre-receive hooks should run in series as the hook updates refs */\n+\trun_hooks_opt_init_async(&opt);\n \tif (!strcmp(hook_name, \"pre-receive\"))\n \t\topt.jobs = 1;\n \n@@ -956,9 +957,10 @@ static int run_receive_hook(struct command *commands,\n \n static int run_update_hook(struct command *cmd)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tint code;\n \n+\trun_hooks_opt_init_async(&opt);\n \tstrvec_pushl(&opt.args,\n \t\t     cmd->ref_name,\n \t\t     oid_to_hex(&cmd->old_oid),\n@@ -1443,7 +1445,10 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n+\n+\n+\trun_hooks_opt_init_sync(&opt);\n \topt.invoked_hook = invoked_hook;\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n@@ -1642,8 +1647,9 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \n+\trun_hooks_opt_init_async(&opt);\n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\n \t\t\tcontinue;\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 2ad26a76f4..5a2c9d1039 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -382,8 +382,9 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt;\n \n+\t\trun_hooks_opt_init_sync(&opt);\n \t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n \t\tstrvec_pushl(&opt.args,\n \t\t\t     oid_to_hex(null_oid()),\ndiff --git a/commit.c b/commit.c\nindex cf62ebceae..58ee2c81bb 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1700,10 +1700,12 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook,\n \t\t    const char *name, ...)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tva_list args;\n \tconst char *arg;\n \tint ret;\n+\n+\trun_hooks_opt_init_sync(&opt);\n \tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\ndiff --git a/hook.c b/hook.c\nindex c1dac6982f..935751fa6c 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -88,6 +88,14 @@ const char *find_hook(const char *name)\n \treturn path.buf;\n }\n \n+int configured_hook_jobs(void)\n+{\n+\tint n = online_cpus();\n+\tgit_config_get_int(\"hook.jobs\", &n);\n+\n+\treturn n;\n+}\n+\n int hook_exists(const char *name)\n {\n \treturn !!find_hook(name);\n@@ -117,6 +125,26 @@ struct list_head* hook_list(const char* hookname)\n \treturn hook_head;\n }\n \n+void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n+{\n+\tstrvec_init(&o->env);\n+\tstrvec_init(&o->args);\n+\to->path_to_stdin = NULL;\n+\to->jobs = 1;\n+\to->dir = NULL;\n+\to->feed_pipe = NULL;\n+\to->feed_pipe_ctx = NULL;\n+\to->consume_sideband = NULL;\n+\to->invoked_hook = NULL;\n+\to->absolute_path = 0;\n+}\n+\n+void run_hooks_opt_init_async(struct run_hooks_opt *o)\n+{\n+\trun_hooks_opt_init_sync(o);\n+\to->jobs = configured_hook_jobs();\n+}\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o)\n {\n \tstrvec_clear(&o->env);\n@@ -238,11 +266,9 @@ int run_found_hooks(const char *hook_name, struct list_head *hooks,\n \t\t.invoked_hook = options->invoked_hook,\n \t};\n \n+\tcb_data.head = hooks;\n \tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n \n-\tif (options->jobs != 1)\n-\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n-\n \trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\ndiff --git a/hook.h b/hook.h\nindex b97237931b..586ddf40bb 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -35,6 +35,9 @@ struct hook {\n  */\n struct list_head* hook_list(const char *hookname);\n \n+/* Provides the number of threads to use for parallel hook execution. */\n+int configured_hook_jobs(void);\n+\n struct run_hooks_opt\n {\n \t/* Environment vars to be set for each hook */\n@@ -93,12 +96,6 @@ struct run_hooks_opt\n \tint *invoked_hook;\n };\n \n-#define RUN_HOOKS_OPT_INIT { \\\n-\t.jobs = 1, \\\n-\t.env = STRVEC_INIT, \\\n-\t.args = STRVEC_INIT, \\\n-}\n-\n /*\n  * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n  * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n@@ -117,6 +114,8 @@ struct hook_cb_data {\n \tint *invoked_hook;\n };\n \n+void run_hooks_opt_init_sync(struct run_hooks_opt *o);\n+void run_hooks_opt_init_async(struct run_hooks_opt *o);\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /*\ndiff --git a/read-cache.c b/read-cache.c\nindex f801313cc9..e8cbbc6ef2 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3063,7 +3063,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt;\n \n \tret = convert_to_sparse(istate);\n \n@@ -3092,6 +3092,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \telse\n \t\tret = close_lock_file_gently(lock);\n \n+\trun_hooks_opt_init_async(&hook_opt);\n \tstrvec_pushl(&hook_opt.args,\n \t\t     istate->updated_workdir ? \"1\" : \"0\",\n \t\t     istate->updated_skipworktree ? \"1\" : \"0\",\ndiff --git a/refs.c b/refs.c\nindex 1149e7e7dc..61d0bb2579 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2063,7 +2063,7 @@ static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint ret = 0, i;\n \tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n@@ -2071,6 +2071,7 @@ static int run_transaction_hook(struct ref_transaction *transaction,\n \tif (!hook_exists(\"reference-transaction\"))\n \t\treturn ret;\n \n+\trun_hooks_opt_init_async(&opt);\n \tstrvec_push(&opt.args, state);\n \n \tfor (i = 0; i < transaction->nr; i++) {\ndiff --git a/reset.c b/reset.c\nindex e6af33b901..48d45f5b79 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -128,7 +128,9 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\t\t\t    reflog_head);\n \t}\n \tif (run_hook) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt;\n+\n+\t\trun_hooks_opt_init_sync(&opt);\n \t\tstrvec_pushl(&opt.args,\n \t\t\t     oid_to_hex(orig ? orig : null_oid()),\n \t\t\t     oid_to_hex(oid),\ndiff --git a/sequencer.c b/sequencer.c\nindex 2440b9dccd..17b93242a7 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1148,11 +1148,13 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tstruct strbuf tmp = STRBUF_INIT;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n \n+\trun_hooks_opt_init_async(&opt);\n+\n \tstrvec_push(&opt.args, \"amend\");\n \n \tstrbuf_addf(&tmp,\n@@ -4524,7 +4526,7 @@ static int pick_commits(struct repository *r,\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n \t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n-\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt;\n \n \t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n \t\t\tnotes_cp.git_cmd = 1;\n@@ -4534,6 +4536,7 @@ static int pick_commits(struct repository *r,\n \t\t\t/* we don't care if this copying failed */\n \t\t\trun_command(&notes_cp);\n \n+\t\t\trun_hooks_opt_init_async(&hook_opt);\n \t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n \t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n \t\t\trun_hooks(\"post-rewrite\", &hook_opt);\ndiff --git a/transport.c b/transport.c\nindex 9969ed2cdd..3381d24225 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1200,11 +1200,13 @@ static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n \tint ret = 0;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt;\n \tstruct strbuf tmp = STRBUF_INIT;\n \tstruct ref *r;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \n+\trun_hooks_opt_init_async(&opt);\n+\n \tstrvec_push(&opt.args, transport->remote->name);\n \tstrvec_push(&opt.args, transport->url);\n \n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430279","messageId":"20210715232603.3415111-6-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210715232603.3415111-1-emilyshaffer@google.com","subject":"[PATCH 5/9] hook: allow running non-native hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:25:59Z","receivedAt":"2021-07-15T23:26:32Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"As the hook architecture and 'git hook run' become more featureful, we\nmay find wrappers wanting to use the hook architecture to run their own\nhooks, thereby getting nice things like parallelism and idiomatic Git\nconfiguration for free. Enable this by letting 'git hook run' bypass the\nknown_hooks() check.\n\nWe do still want to keep known_hooks() around, though - by die()ing when\nan internal Git call asks for run_hooks(\"my-new-hook\"), we can remind\nGit developers to update Documentation/githooks.txt with their new hook,\nwhich in turn helps Git users discover this new hook.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/hook.c |  4 ++--\n hook.c         | 32 ++++++++++++++++++++++++++++----\n hook.h         | 16 +++++++++++++++-\n 3 files changed, 45 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 8340c8c9a8..b08f9c9c4f 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -39,7 +39,7 @@ static int list(int argc, const char **argv, const char *prefix)\n \n \thookname = argv[0];\n \n-\thead = hook_list(hookname);\n+\thead = hook_list(hookname, 1);\n \n \tif (list_empty(head)) {\n \t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n@@ -103,7 +103,7 @@ static int run(int argc, const char **argv, const char *prefix)\n \t * run_found_hooks() instead...\n \t */\n \thook_name = argv[0];\n-\thooks = hook_list(hook_name);\n+\thooks = hook_list(hook_name, 1);\n \tif (list_empty(hooks)) {\n \t\t/* ... act like run_hooks() under --ignore-missing */\n \t\tif (ignore_missing)\ndiff --git a/hook.c b/hook.c\nindex 3a588cb055..e1cf035022 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -52,12 +52,21 @@ static int known_hook(const char *name)\n \n const char *find_hook(const char *name)\n {\n-\tstatic struct strbuf path = STRBUF_INIT;\n+\tconst char *hook_path;\n \n \tif (!known_hook(name))\n \t\tdie(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n \t\t    name);\n \n+\thook_path = find_hook_gently(name);\n+\n+\treturn hook_path;\n+}\n+\n+const char *find_hook_gently(const char *name)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\n \tstrbuf_reset(&path);\n \tstrbuf_git_path(&path, \"hooks/%s\", name);\n \tif (access(path.buf, X_OK) < 0) {\n@@ -101,10 +110,16 @@ int hook_exists(const char *name)\n \treturn !!find_hook(name);\n }\n \n-struct list_head* hook_list(const char* hookname)\n+struct hook_config_cb\n+{\n+\tstruct strbuf *hook_key;\n+\tstruct list_head *list;\n+};\n+\n+struct list_head* hook_list(const char* hookname, int allow_unknown)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n-\tconst char *hook_path = find_hook(hookname);\n+\tconst char *hook_path;\n \n \n \tINIT_LIST_HEAD(hook_head);\n@@ -112,6 +127,11 @@ struct list_head* hook_list(const char* hookname)\n \tif (!hookname)\n \t\treturn NULL;\n \n+\tif (allow_unknown)\n+\t\thook_path = find_hook_gently(hookname);\n+\telse\n+\t\thook_path = find_hook(hookname);\n+\n \t/* Add the hook from the hookdir */\n \tif (hook_path) {\n \t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n@@ -291,7 +311,11 @@ int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n \tif (options->path_to_stdin && options->feed_pipe)\n \t\tBUG(\"choose only one method to populate stdin\");\n \n-\thooks = hook_list(hook_name);\n+\t/*\n+\t * 'git hooks run <hookname>' uses run_found_hooks, so we don't need to\n+\t * allow unknown hooknames here.\n+\t */\n+\thooks = hook_list(hook_name, 0);\n \n \t/*\n \t * If you need to act on a missing hook, use run_found_hooks()\ndiff --git a/hook.h b/hook.h\nindex 60389cd8cd..2559232880 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -9,8 +9,16 @@\n  * Returns the path to the hook file, or NULL if the hook is missing\n  * or disabled. Note that this points to static storage that will be\n  * overwritten by further calls to find_hook and run_hook_*.\n+ *\n+ * If the hook is not a native hook (e.g. present in Documentation/githooks.txt)\n+ * find_hook() will die(). find_hook_gently() does not consult the native hook\n+ * list and will check for any hook name in the hooks directory regardless of\n+ * whether it is known. find_hook() should be used by internal calls to hooks,\n+ * and find_hook_gently() should only be used when the hookname was provided by\n+ * the user, such as by 'git hook run my-wrapper-hook'.\n  */\n const char *find_hook(const char *name);\n+const char *find_hook_gently(const char *name);\n \n /*\n  * A boolean version of find_hook()\n@@ -34,8 +42,14 @@ struct hook {\n /*\n  * Provides a linked list of 'struct hook' detailing commands which should run\n  * in response to the 'hookname' event, in execution order.\n+ *\n+ * If allow_unknown is unset, hooks will be checked against the hook list\n+ * generated from Documentation/githooks.txt. Otherwise, any hook name will be\n+ * allowed. allow_unknown should only be set when the hook name is provided by\n+ * the user; internal calls to hook_list should make sure the hook they are\n+ * invoking is present in Documentation/githooks.txt.\n  */\n-struct list_head* hook_list(const char *hookname);\n+struct list_head* hook_list(const char *hookname, int allow_unknown);\n \n /* Provides the number of threads to use for parallel hook execution. */\n int configured_hook_jobs(void);\n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430280","messageId":"20210715232603.3415111-5-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210715232603.3415111-1-emilyshaffer@google.com","subject":"[PATCH 4/9] hook: treat hookdir hook specially","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:25:58Z","receivedAt":"2021-07-15T23:26:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Soon, we will allow users to specify hooks using the config. These\nconfig-specified hooks may require different child_process options than\nhook executables in the gitdir. So, let's differentiate between hooks\ncoming from the gitdir and hooks coming from the config.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n hook.c | 3 ++-\n hook.h | 2 ++\n 2 files changed, 4 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 19138a8290..3a588cb055 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -117,6 +117,7 @@ struct list_head* hook_list(const char* hookname)\n \t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n \t\tto_add->hook_path = hook_path;\n \t\tto_add->feed_pipe_cb_data = NULL;\n+\t\tto_add->from_hookdir = 1;\n \t\tlist_add_tail(&to_add->list, hook_head);\n \t}\n \n@@ -200,7 +201,7 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n-\tif (hook_cb->options->absolute_path)\n+\tif (run_me->from_hookdir && hook_cb->options->absolute_path)\n \t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n \telse\n \t\tstrvec_push(&cp->args, run_me->hook_path);\ndiff --git a/hook.h b/hook.h\nindex 586ddf40bb..60389cd8cd 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -22,6 +22,8 @@ struct hook {\n \t/* The path to the hook */\n \tconst char *hook_path;\n \n+\tunsigned from_hookdir : 1;\n+\n \t/*\n \t * Use this to keep state for your feed_pipe_fn if you are using\n \t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430281","messageId":"20210715232603.3415111-4-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210715232603.3415111-1-emilyshaffer@google.com","subject":"[PATCH 3/9] hook: introduce \"git hook list\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:25:57Z","receivedAt":"2021-07-15T23:26:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"If more than one hook will be run, it may be useful to see a list of\nwhich hooks should be run. At very least, it will be useful for us to\ntest the semantics of multihooks ourselves.\n\nFor now, only list the hooks which will run in the order they will run\nin; later, it might be useful to include more information like where the\nhooks were configured and whether or not they will run.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/hook.c | 43 +++++++++++++++++++++++++++++++++++++++++++\n hook.c         | 18 ++++++++----------\n 2 files changed, 51 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex d196d8498c..8340c8c9a8 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -8,6 +8,7 @@\n \n static const char * const builtin_hook_usage[] = {\n \tN_(\"git hook <command> [...]\"),\n+\tN_(\"git hook list <hookname>\"),\n \tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n \tNULL\n };\n@@ -18,6 +19,46 @@ static const char * const builtin_hook_run_usage[] = {\n \tNULL\n };\n \n+static int list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct list_head *head, *pos;\n+\tconst char *hookname = NULL;\n+\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n+\n+\tstruct option list_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, list_options,\n+\t\t\t     builtin_hook_usage, 0);\n+\n+\tif (argc < 1) {\n+\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\t\t      builtin_hook_usage, list_options);\n+\t}\n+\n+\thookname = argv[0];\n+\n+\thead = hook_list(hookname);\n+\n+\tif (list_empty(head)) {\n+\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n+\t\t       hookname);\n+\t\treturn 0;\n+\t}\n+\n+\tlist_for_each(pos, head) {\n+\t\tstruct hook *item = list_entry(pos, struct hook, list);\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item)\n+\t\t\tprintf(\"%s\\n\", item->hook_path);\n+\t}\n+\n+\tclear_hook_list(head);\n+\tstrbuf_release(&hookdir_annotation);\n+\n+\treturn 0;\n+}\n static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n@@ -88,6 +129,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \tif (!argc)\n \t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n \n+\tif (!strcmp(argv[0], \"list\"))\n+\t\treturn list(argc, argv, prefix);\n \tif (!strcmp(argv[0], \"run\"))\n \t\treturn run(argc, argv, prefix);\n \telse\ndiff --git a/hook.c b/hook.c\nindex 935751fa6c..19138a8290 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -104,22 +104,20 @@ int hook_exists(const char *name)\n struct list_head* hook_list(const char* hookname)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\tconst char *hook_path = find_hook(hookname);\n+\n \n \tINIT_LIST_HEAD(hook_head);\n \n \tif (!hookname)\n \t\treturn NULL;\n \n-\tif (have_git_dir()) {\n-\t\tconst char *hook_path = find_hook(hookname);\n-\n-\t\t/* Add the hook from the hookdir */\n-\t\tif (hook_path) {\n-\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n-\t\t\tto_add->hook_path = hook_path;\n-\t\t\tto_add->feed_pipe_cb_data = NULL;\n-\t\t\tlist_add_tail(&to_add->list, hook_head);\n-\t\t}\n+\t/* Add the hook from the hookdir */\n+\tif (hook_path) {\n+\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n+\t\tto_add->hook_path = hook_path;\n+\t\tto_add->feed_pipe_cb_data = NULL;\n+\t\tlist_add_tail(&to_add->list, hook_head);\n \t}\n \n \treturn hook_head;\n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430282","messageId":"20210715232603.3415111-7-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210715232603.3415111-1-emilyshaffer@google.com","subject":"[PATCH 6/9] hook: include hooks from the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:26:00Z","receivedAt":"2021-07-15T23:26:37Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach the hook.[hc] library to parse configs to populare the list of\nhooks to run for a given event.\n\nMultiple commands can be specified for a given hook by providing\nmultiple \"hook.<hookname>.command = <path-to-hook>\" lines. Hooks will be\nrun in config order.\n\nFor example:\n\n  $ git config --list | grep ^hook\n  hook.pre-commit.command=~/bar.sh\n\n  $ git hook run\n  # Runs ~/bar.sh\n  # Runs .git/hooks/pre-commit\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/config/hook.txt |   5 ++\n Documentation/git-hook.txt    |  16 +++-\n builtin/hook.c                |   2 +-\n hook.c                        | 109 ++++++++++++++++++++---\n hook.h                        |   2 +-\n t/t1360-config-based-hooks.sh | 159 ++++++++++++++++++++++++++++++++++\n 6 files changed, 278 insertions(+), 15 deletions(-)\n create mode 100755 t/t1360-config-based-hooks.sh\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 96d3d6572c..a97b980cca 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -1,3 +1,8 @@\n+hook.<command>.command::\n+\tA command to execute during the <command> hook event. This can be an\n+\texecutable on your device, a oneliner for your shell, or the name of a\n+\thookcmd. See linkgit:git-hook[1].\n+\n hook.jobs::\n \tSpecifies how many hooks can be run simultaneously during parallelized\n \thook execution. If unspecified, defaults to the number of processors on\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 8bf82b5dd4..8e2ab724e8 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -18,12 +18,24 @@ This command is an interface to git hooks (see linkgit:githooks[5]).\n Currently it only provides a convenience wrapper for running hooks for\n use by git itself. In the future it might gain other functionality.\n \n+This command parses the default configuration files for sections like `hook\n+\"<hook name>\"`. `hook` is used to describe the commands which will be run during\n+a particular hook event; commands are run in the order Git encounters them\n+during the configuration parse (see linkgit:git-config[1]).\n+\n+In general, when instructions suggest adding a script to\n+`.git/hooks/<something>`, you can specify it in the config instead by running\n+`git config --add hook.<something>.command <path-to-script>` - this way you can\n+share the script between multiple repos. That is, `cp ~/my-script.sh\n+~/project/.git/hooks/pre-commit` would become `git config --add\n+hook.pre-commit.command ~/my-script.sh`.\n+\n SUBCOMMANDS\n -----------\n \n run::\n-\tRun the `<hook-name>` hook. See linkgit:githooks[5] for\n-\tthe hook names we support.\n+\tRuns hooks configured for `<hook-name>`, in the order they are\n+\tdiscovered during the config parse.\n +\n Any positional arguments to the hook should be passed after an\n optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex b08f9c9c4f..c54b0a4d13 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -51,7 +51,7 @@ static int list(int argc, const char **argv, const char *prefix)\n \t\tstruct hook *item = list_entry(pos, struct hook, list);\n \t\titem = list_entry(pos, struct hook, list);\n \t\tif (item)\n-\t\t\tprintf(\"%s\\n\", item->hook_path);\n+\t\t\tprintf(\"%s\\n\", item->command);\n \t}\n \n \tclear_hook_list(head);\ndiff --git a/hook.c b/hook.c\nindex e1cf035022..ed90edcad7 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -12,6 +12,51 @@ static void free_hook(struct hook *ptr)\n \tfree(ptr);\n }\n \n+/*\n+ * Walks the linked list at 'head' to check if any hook running 'command'\n+ * already exists. Returns a pointer to that hook if so, otherwise returns NULL.\n+ */\n+static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n+{\n+\tstruct list_head *pos = NULL, *tmp = NULL;\n+\tstruct hook *found = NULL;\n+\n+\tlist_for_each_safe(pos, tmp, head) {\n+\t\tstruct hook *it = list_entry(pos, struct hook, list);\n+\t\tif (!strcmp(it->command, command)) {\n+\t\t    list_del(pos);\n+\t\t    found = it;\n+\t\t    break;\n+\t\t}\n+\t}\n+\treturn found;\n+}\n+\n+/*\n+ * Adds a hook if it's not already in the list, or moves it to the tail of the\n+ * list if it was already there.\n+ * Returns a handle to the hook in case more modification is needed. Do not free\n+ * the returned handle.\n+ */\n+static struct hook * append_or_move_hook(struct list_head *head, const char *command)\n+{\n+\t/* check if the hook is already in the list */\n+\tstruct hook *to_add = find_hook_by_command(head, command);\n+\n+\tif (!to_add) {\n+\t\t/* adding a new hook, not moving an old one */\n+\t\tto_add = xmalloc(sizeof(*to_add));\n+\t\tto_add->command = command;\n+\t\tto_add->feed_pipe_cb_data = NULL;\n+\t\t/* This gets overwritten in hook_list() for hookdir hooks. */\n+\t\tto_add->from_hookdir = 0;\n+\t}\n+\n+\tlist_add_tail(&to_add->list, head);\n+\n+\treturn to_add;\n+}\n+\n static void remove_hook(struct list_head *to_remove)\n {\n \tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n@@ -116,30 +161,69 @@ struct hook_config_cb\n \tstruct list_head *list;\n };\n \n+/*\n+ * Callback for git_config which adds configured hooks to a hook list.\n+ * Hooks can be configured by specifying hook.<name>.command, for example,\n+ * hook.pre-commit.command = echo \"pre-commit hook!\"\n+ */\n+static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n+{\n+\tstruct hook_config_cb *data = cb_data;\n+\tconst char *hook_key = data->hook_key->buf;\n+\tstruct list_head *head = data->list;\n+\n+\tif (!strcmp(key, hook_key)) {\n+\t\tconst char *command = value;\n+\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\n+\n+\t\tif (!command) {\n+\t\t\tstrbuf_release(&hookcmd_name);\n+\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n+\t\t}\n+\n+\t\t/* TODO: implement skipping hooks */\n+\n+\t\t/* TODO: immplement hook aliases */\n+\n+\t\t/*\n+\t\t * TODO: implement an option-getting callback, e.g.\n+\t\t *   get configs by pattern hookcmd.$value.*\n+\t\t *   for each key+value, do_callback(key, value, cb_data)\n+\t\t */\n+\t\tappend_or_move_hook(head, command);\n+\n+\t\tstrbuf_release(&hookcmd_name);\n+\t}\n+\n+\treturn 0;\n+}\n+\n struct list_head* hook_list(const char* hookname, int allow_unknown)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n \tconst char *hook_path;\n-\n+\tstruct strbuf hook_key = STRBUF_INIT;\n+\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n \n \tINIT_LIST_HEAD(hook_head);\n \n \tif (!hookname)\n \t\treturn NULL;\n \n+\t/* Add the hooks from the config, e.g. hook.pre-commit.command */\n+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n+\tgit_config(hook_config_lookup, &cb_data);\n+\n+\n \tif (allow_unknown)\n \t\thook_path = find_hook_gently(hookname);\n \telse\n \t\thook_path = find_hook(hookname);\n \n \t/* Add the hook from the hookdir */\n-\tif (hook_path) {\n-\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n-\t\tto_add->hook_path = hook_path;\n-\t\tto_add->feed_pipe_cb_data = NULL;\n-\t\tto_add->from_hookdir = 1;\n-\t\tlist_add_tail(&to_add->list, hook_head);\n-\t}\n+\tif (hook_path)\n+\t\tappend_or_move_hook(hook_head, hook_path)->from_hookdir = 1;\n \n \treturn hook_head;\n }\n@@ -220,11 +304,14 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->trace2_hook_name = hook_cb->hook_name;\n \tcp->dir = hook_cb->options->dir;\n \n+\t/* to enable oneliners, let config-specified hooks run in shell */\n+\tcp->use_shell = !run_me->from_hookdir;\n+\n \t/* add command */\n \tif (run_me->from_hookdir && hook_cb->options->absolute_path)\n-\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n+\t\tstrvec_push(&cp->args, absolute_path(run_me->command));\n \telse\n-\t\tstrvec_push(&cp->args, run_me->hook_path);\n+\t\tstrvec_push(&cp->args, run_me->command);\n \n \t/*\n \t * add passed-in argv, without expanding - let the user get back\n@@ -255,7 +342,7 @@ static int notify_start_failure(struct strbuf *out,\n \thook_cb->rc |= 1;\n \n \tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n-\t\t    attempted->hook_path);\n+\t\t    attempted->command);\n \n \treturn 1;\n }\ndiff --git a/hook.h b/hook.h\nindex 2559232880..e8cd6b7c67 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -28,7 +28,7 @@ int hook_exists(const char *hookname);\n struct hook {\n \tstruct list_head list;\n \t/* The path to the hook */\n-\tconst char *hook_path;\n+\tconst char *command;\n \n \tunsigned from_hookdir : 1;\n \ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nnew file mode 100755\nindex 0000000000..12fca516ec\n--- /dev/null\n+++ b/t/t1360-config-based-hooks.sh\n@@ -0,0 +1,159 @@\n+#!/bin/bash\n+\n+test_description='config-managed multihooks, including git-hook command'\n+\n+. ./test-lib.sh\n+\n+ROOT=\n+if test_have_prereq MINGW\n+then\n+\t# In Git for Windows, Unix-like paths work only in shell scripts;\n+\t# `git.exe`, however, will prefix them with the pseudo root directory\n+\t# (of the Unix shell). Let's accommodate for that.\n+\tROOT=\"$(cd / && pwd)\"\n+fi\n+\n+setup_hooks () {\n+\ttest_config hook.pre-commit.command \"/path/ghi\" --add\n+\ttest_config_global hook.pre-commit.command \"/path/def\" --add\n+}\n+\n+setup_hookdir () {\n+\tmkdir .git/hooks\n+\twrite_script .git/hooks/pre-commit <<-EOF\n+\techo \\\"Legacy Hook\\\"\n+\tEOF\n+\ttest_when_finished rm -rf .git/hooks\n+}\n+\n+test_expect_success 'git hook rejects commands without a mode' '\n+\ttest_must_fail git hook pre-commit\n+'\n+\n+test_expect_success 'git hook rejects commands without a hookname' '\n+\ttest_must_fail git hook list\n+'\n+\n+test_expect_success 'git hook list orders by config order' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\t$ROOT/path/def\n+\t$ROOT/path/ghi\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list reorders on duplicate commands' '\n+\tsetup_hooks &&\n+\n+\ttest_config hook.pre-commit.command \"/path/def\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\t$ROOT/path/ghi\n+\t$ROOT/path/def\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list shows hooks from the hookdir' '\n+\tsetup_hookdir &&\n+\n+\tcat >expected <<-EOF &&\n+\t.git/hooks/pre-commit\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+\n+test_expect_success 'inline hook definitions execute oneliners' '\n+\ttest_config hook.pre-commit.command \"echo \\\"Hello World\\\"\" &&\n+\n+\techo \"Hello World\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions resolve paths' '\n+\twrite_script sample-hook.sh <<-EOF &&\n+\techo \\\"Sample Hook\\\"\n+\tEOF\n+\n+\ttest_when_finished \"rm sample-hook.sh\" &&\n+\n+\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\techo \\\"Sample Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook run can pass args' '\n+\twrite_script sample-hook.sh <<-\\EOF &&\n+\techo $1\n+\techo $2\n+\tEOF\n+\n+\ttest_config hook.pre-commit.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\tcat >expected <<-EOF &&\n+\targ1\n+\targ2\n+\tEOF\n+\n+\tgit hook run pre-commit -- arg1 arg2 2>actual &&\n+\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hookdir hook included in git hook run' '\n+\tsetup_hookdir &&\n+\n+\techo \\\"Legacy Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'out-of-repo runs excluded' '\n+\tsetup_hooks &&\n+\n+\tnongit test_must_fail git hook run pre-commit\n+'\n+\n+test_expect_success 'stdin to multiple hooks' '\n+\tgit config --add hook.test.command \"xargs -P1 -I% echo a%\" &&\n+\tgit config --add hook.test.command \"xargs -P1 -I% echo b%\" &&\n+\ttest_when_finished \"test_unconfig hook.test.command\" &&\n+\n+\tcat >input <<-EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tcat >expected <<-EOF &&\n+\ta1\n+\ta2\n+\ta3\n+\tb1\n+\tb2\n+\tb3\n+\tEOF\n+\n+\tgit hook run --to-stdin=input test 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_done\n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430283","messageId":"20210715232603.3415111-9-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210715232603.3415111-1-emilyshaffer@google.com","subject":"[PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:26:02Z","receivedAt":"2021-07-15T23:26:37Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"To enable fine-grained options which apply to a single hook executable,\nand to make it easier for a single executable to be run on multiple hook\nevents, teach \"hookcmd.<alias>.config\". These can be configured as\nfollows:\n\n  [hookcmd.my-linter]\n    command = ~/my-linter.sh\n  [hook.pre-commit]\n    command = my-linter\n\nDuring the config parse, we can attempt to dereference the\n'hook.pre-commit.command' string 'my-linter' and check if it matches any\nhookcmd names; if so, we can run the command associated with that\nhookcmd alias instead.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/config/hook.txt |  5 +++++\n Documentation/git-hook.txt    | 42 +++++++++++++++++++++++++++++++----\n hook.c                        |  9 +++++++-\n t/t1360-config-based-hooks.sh | 19 ++++++++++++++++\n 4 files changed, 70 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex a97b980cca..5b35170664 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -3,6 +3,11 @@ hook.<command>.command::\n \texecutable on your device, a oneliner for your shell, or the name of a\n \thookcmd. See linkgit:git-hook[1].\n \n+hookcmd.<name>.command::\n+\tA command to execute during a hook for which <name> has been specified\n+\tas a command. This can be an executable on your device or a oneliner for\n+\tyour shell. See linkgit:git-hook[1].\n+\n hook.jobs::\n \tSpecifies how many hooks can be run simultaneously during parallelized\n \thook execution. If unspecified, defaults to the number of processors on\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 8e2ab724e8..1a4d22fd90 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -18,10 +18,44 @@ This command is an interface to git hooks (see linkgit:githooks[5]).\n Currently it only provides a convenience wrapper for running hooks for\n use by git itself. In the future it might gain other functionality.\n \n-This command parses the default configuration files for sections like `hook\n-\"<hook name>\"`. `hook` is used to describe the commands which will be run during\n-a particular hook event; commands are run in the order Git encounters them\n-during the configuration parse (see linkgit:git-config[1]).\n+This command parses the default configuration files for sections `hook` and\n+`hookcmd`. `hook` is used to describe the commands which will be run during a\n+particular hook event; commands are run in the order Git encounters them during\n+the configuration parse (see linkgit:git-config[1]). `hookcmd` is used to\n+describe attributes of a specific command. If additional attributes don't need\n+to be specified, a command to run can be specified directly in the `hook`\n+section; if a `hookcmd` by that name isn't found, Git will attempt to run the\n+provided value directly. For example:\n+\n+Global config\n+----\n+  [hook \"post-commit\"]\n+    command = \"linter\"\n+    command = \"~/typocheck.sh\"\n+\n+  [hookcmd \"linter\"]\n+    command = \"/bin/linter --c\"\n+----\n+\n+Local config\n+----\n+  [hook \"prepare-commit-msg\"]\n+    command = \"linter\"\n+  [hook \"post-commit\"]\n+    command = \"python ~/run-test-suite.py\"\n+----\n+\n+With these configs, you'd then run post-commit hooks in this order:\n+\n+  /bin/linter --c\n+  ~/typocheck.sh\n+  python ~/run-test-suite.py\n+  .git/hooks/post-commit (if present)\n+\n+and prepare-commit-msg hooks in this order:\n+\n+  /bin/linter --c\n+  .git/hooks/prepare-commit-msg (if present)\n \n In general, when instructions suggest adding a script to\n `.git/hooks/<something>`, you can specify it in the config instead by running\ndiff --git a/hook.c b/hook.c\nindex b08b876d5d..21904d90f6 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -184,7 +184,14 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n \n \t\t/* TODO: implement skipping hooks */\n \n-\t\t/* TODO: immplement hook aliases */\n+\t\t/*\n+\t\t * Check if a hookcmd with that name exists. If it doesn't,\n+\t\t * 'git_config_get_value()' is documented not to touch &command,\n+\t\t * so we don't need to do anything.\n+\t\t */\n+\t\tstrbuf_reset(&hookcmd_name);\n+\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n+\t\tgit_config_get_value(hookcmd_name.buf, &command);\n \n \t\t/*\n \t\t * TODO: implement an option-getting callback, e.g.\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex e4a7b06ad1..50ee824f05 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -18,6 +18,11 @@ setup_hooks () {\n \ttest_config_global hook.pre-commit.command \"/path/def\" --add\n }\n \n+setup_hookcmd () {\n+\ttest_config hook.pre-commit.command \"abc\" --add\n+\ttest_config_global hookcmd.abc.command \"/path/abc\" --add\n+}\n+\n setup_hookdir () {\n \tmkdir .git/hooks\n \twrite_script .git/hooks/pre-commit <<-EOF\n@@ -59,6 +64,20 @@ test_expect_success 'git hook list orders by config order' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list dereferences a hookcmd' '\n+\tsetup_hooks &&\n+\tsetup_hookcmd &&\n+\n+\tcat >expected <<-EOF &&\n+\t$ROOT/path/def\n+\t$ROOT/path/ghi\n+\t$ROOT/path/abc\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_expect_success 'git hook list reorders on duplicate commands' '\n \tsetup_hooks &&\n \n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430285","messageId":"20210715232603.3415111-8-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210715232603.3415111-1-emilyshaffer@google.com","subject":"[PATCH 7/9] hook: allow out-of-repo 'git hook' invocations","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:26:01Z","receivedAt":"2021-07-15T23:26:39Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Since hooks can now be supplied via the config, and a config can be\npresent without a gitdir via the global and system configs, we can start\nto allow 'git hook run' to occur without a gitdir. This enables us to do\nthings like run sendemail-validate hooks when running 'git send-email'\nfrom a nongit directory.\n\nIt still doesn't make sense to look for hooks in the hookdir in nongit\nrepos, though, as there is no hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    For hookdir hooks, do we want to run them in nongit dir when core.hooksPath\n    is set? For example, if someone set core.hooksPath in their global config and\n    then ran 'git hook run sendemail-validate' in a nongit dir?\n\n git.c                         |  2 +-\n hook.c                        | 18 ++++++++++--------\n t/t1360-config-based-hooks.sh | 13 +++++++++++++\n 3 files changed, 24 insertions(+), 9 deletions(-)\n\ndiff --git a/git.c b/git.c\nindex 540909c391..39988ee3b0 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,7 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n-\t{ \"hook\", cmd_hook, RUN_SETUP },\n+\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nindex ed90edcad7..b08b876d5d 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -202,7 +202,6 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n struct list_head* hook_list(const char* hookname, int allow_unknown)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n-\tconst char *hook_path;\n \tstruct strbuf hook_key = STRBUF_INIT;\n \tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n \n@@ -216,14 +215,17 @@ struct list_head* hook_list(const char* hookname, int allow_unknown)\n \tgit_config(hook_config_lookup, &cb_data);\n \n \n-\tif (allow_unknown)\n-\t\thook_path = find_hook_gently(hookname);\n-\telse\n-\t\thook_path = find_hook(hookname);\n+\tif (have_git_dir()) {\n+\t\tconst char *hook_path;\n+\t\tif (allow_unknown)\n+\t\t\thook_path = find_hook_gently(hookname);\n+\t\telse\n+\t\t\thook_path = find_hook(hookname);\n \n-\t/* Add the hook from the hookdir */\n-\tif (hook_path)\n-\t\tappend_or_move_hook(hook_head, hook_path)->from_hookdir = 1;\n+\t\t/* Add the hook from the hookdir */\n+\t\tif (hook_path)\n+\t\t\tappend_or_move_hook(hook_head, hook_path)->from_hookdir = 1;\n+\t}\n \n \treturn hook_head;\n }\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 12fca516ec..e4a7b06ad1 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -34,6 +34,19 @@ test_expect_success 'git hook rejects commands without a hookname' '\n \ttest_must_fail git hook list\n '\n \n+test_expect_success 'git hook runs outside of a repo' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\t$ROOT/path/def\n+\tEOF\n+\n+\tnongit git config --list --global &&\n+\n+\tnongit git hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_expect_success 'git hook list orders by config order' '\n \tsetup_hooks &&\n \n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430284","messageId":"20210715232603.3415111-10-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210715232603.3415111-1-emilyshaffer@google.com","subject":"[PATCH 9/9] hook: implement hookcmd.<name>.skip","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-15T23:26:03Z","receivedAt":"2021-07-15T23:26:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"If a user wants a specific repo to skip execution of a hook which is set\nat a global or system level, they will be able to do so by specifying\n'skip' in their repo config:\n\n~/.gitconfig\n  [hook.pre-commit]\n    command = skippable-oneliner\n    command = skippable-hookcmd\n\n  [hookcmd.skippable-hookcmd]\n    command = foo.sh\n\n$GIT_DIR/.git/config\n  [hookcmd.skippable-oneliner]\n    skip = true\n  [hookcmd.skippable-hookcmd]\n    skip = true\n\nLater it may make sense to add an option like\n\"hookcmd.<name>.<hook-event>-skip\" - but for simplicity, let's start\nwith a universal skip setting like this.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/config/hook.txt |  8 ++++++++\n Documentation/git-hook.txt    | 30 +++++++++++++++++++++++++++++\n hook.c                        | 31 +++++++++++++++++++++---------\n t/t1360-config-based-hooks.sh | 36 +++++++++++++++++++++++++++++++++++\n 4 files changed, 96 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 5b35170664..6b3776f06f 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -8,6 +8,14 @@ hookcmd.<name>.command::\n \tas a command. This can be an executable on your device or a oneliner for\n \tyour shell. See linkgit:git-hook[1].\n \n+hookcmd.<name>.skip::\n+\tSpecify this boolean to remove a command from earlier in the execution\n+\torder. Useful if you want to make a single repo an exception to hook\n+\tconfigured at the system or global scope. If there is no hookcmd\n+\tspecified for the command you want to skip, you can use the value of\n+\t`hook.<command>.command` as <name> as a shortcut. The \"skip\" setting\n+\tmust be specified after the \"hook.<command>.command\" to have an effect.\n+\n hook.jobs::\n \tSpecifies how many hooks can be run simultaneously during parallelized\n \thook execution. If unspecified, defaults to the number of processors on\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 1a4d22fd90..fcd13da4ff 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -57,6 +57,36 @@ and prepare-commit-msg hooks in this order:\n   /bin/linter --c\n   .git/hooks/prepare-commit-msg (if present)\n \n+If there is a command you wish to run in most cases but have one or two\n+exceptional repos where it should be skipped, you can specify\n+`hookcmd.<name>.skip`, for example:\n+\n+System config\n+----\n+  [hook \"post-commit\"]\n+    command = check-for-secrets\n+\n+  [hookcmd \"check-for-secrets\"]\n+    command = /bin/secret-checker --aggressive\n+----\n+\n+Local config\n+----\n+  [hookcmd \"check-for-secrets\"]\n+    skip = true\n+  # This works for inlined hook commands, too:\n+  [hookcmd \"~/typocheck.sh\"]\n+    skip = true\n+----\n+\n+After these configs are added, and including the earlier example configs, the\n+hook list becomes:\n+\n+post-commit:\n+  /bin/linter --c\n+  python ~/run-test-suite.py\n+  .git/hooks/post-commit (if present)\n+\n In general, when instructions suggest adding a script to\n `.git/hooks/<something>`, you can specify it in the config instead by running\n `git config --add hook.<something>.command <path-to-script>` - this way you can\ndiff --git a/hook.c b/hook.c\nindex 21904d90f6..5faa1690e4 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -18,6 +18,7 @@ static void free_hook(struct hook *ptr)\n  */\n static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n {\n+\t/* check if the hook is already in the list */\n \tstruct list_head *pos = NULL, *tmp = NULL;\n \tstruct hook *found = NULL;\n \n@@ -40,7 +41,6 @@ static struct hook * find_hook_by_command(struct list_head *head, const char *co\n  */\n static struct hook * append_or_move_hook(struct list_head *head, const char *command)\n {\n-\t/* check if the hook is already in the list */\n \tstruct hook *to_add = find_hook_by_command(head, command);\n \n \tif (!to_add) {\n@@ -175,14 +175,15 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n \tif (!strcmp(key, hook_key)) {\n \t\tconst char *command = value;\n \t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\t\tint skip = 0;\n \n-\n-\t\tif (!command) {\n-\t\t\tstrbuf_release(&hookcmd_name);\n-\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n-\t\t}\n-\n-\t\t/* TODO: implement skipping hooks */\n+\t\t/*\n+\t\t * Check if we're removing that hook instead. Hookcmds are\n+\t\t * removed by name, and inlined hooks are removed by command\n+\t\t * content.\n+\t\t */\n+\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.skip\", command);\n+\t\tgit_config_get_bool(hookcmd_name.buf, &skip);\n \n \t\t/*\n \t\t * Check if a hookcmd with that name exists. If it doesn't,\n@@ -193,12 +194,24 @@ static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n \t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n \t\tgit_config_get_value(hookcmd_name.buf, &command);\n \n+\t\tif (!command) {\n+\t\t\tstrbuf_release(&hookcmd_name);\n+\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n+\t\t}\n+\n \t\t/*\n \t\t * TODO: implement an option-getting callback, e.g.\n \t\t *   get configs by pattern hookcmd.$value.*\n \t\t *   for each key+value, do_callback(key, value, cb_data)\n \t\t */\n-\t\tappend_or_move_hook(head, command);\n+\n+\t\tif (skip) {\n+\t\t\tstruct hook *to_remove = find_hook_by_command(head, command);\n+\t\t\tif (to_remove)\n+\t\t\t\tremove_hook(&(to_remove->list));\n+\t\t} else {\n+\t\t\tappend_or_move_hook(head, command);\n+\t\t}\n \n \t\tstrbuf_release(&hookcmd_name);\n \t}\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 50ee824f05..30dc7b6054 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -103,6 +103,42 @@ test_expect_success 'git hook list shows hooks from the hookdir' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list removes skipped hookcmd' '\n+\tsetup_hookcmd &&\n+\ttest_config hookcmd.abc.skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tno commands configured for hook '\\''pre-commit'\\''\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list ignores skip referring to unused hookcmd' '\n+\ttest_config hookcmd.abc.command \"/path/abc\" --add &&\n+\ttest_config hookcmd.abc.skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tno commands configured for hook '\\''pre-commit'\\''\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list removes skipped inlined hook' '\n+\tsetup_hooks &&\n+\ttest_config hookcmd.\"$ROOT/path/ghi\".skip \"true\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\t$ROOT/path/def\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n \n test_expect_success 'inline hook definitions execute oneliners' '\n \ttest_config hook.pre-commit.command \"echo \\\"Hello World\\\"\" &&\n-- \n2.32.0.402.g57bb445576-goog\n\n"},{"id":"430309","messageId":"87tukuy7vc.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210715232603.3415111-8-emilyshaffer@google.com","subject":"Re: [PATCH 7/9] hook: allow out-of-repo 'git hook' invocations","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-16T08:33:25Z","receivedAt":"2021-07-16T08:34:45Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 15 2021, Emily Shaffer wrote:\n\n> Since hooks can now be supplied via the config, and a config can be\n> present without a gitdir via the global and system configs, we can start\n> to allow 'git hook run' to occur without a gitdir. This enables us to do\n> things like run sendemail-validate hooks when running 'git send-email'\n> from a nongit directory.\n>\n> It still doesn't make sense to look for hooks in the hookdir in nongit\n> repos, though, as there is no hookdir.\n\nHrm, I haven't tested but re the discussion we had about\nRUN_SETUP_GENTLY on my re-rolled base topic is this really just a\nregression in my changes there?\n\nI.e. I assumed we could do RUN_SETUP for the bug-for-bug compatibility\nstep, but send-email runs out of repo hooks and we just didn't have\ntests for it, or am I missing something?\n"},{"id":"430310","messageId":"87r1fyy728.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210715232603.3415111-3-emilyshaffer@google.com","subject":"Re: [PATCH 2/9] hook: allow parallel hook execution","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-16T08:36:10Z","receivedAt":"2021-07-16T08:52:11Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 15 2021, Emily Shaffer wrote:\n\n> In many cases, there's no reason not to allow hooks to execute in\n> parallel. run_processes_parallel() is well-suited - it's a task queue\n> that runs its housekeeping in series, which means users don't\n> need to worry about thread safety on their callback data. True\n> multithreaded execution with the async_* functions isn't necessary here.\n> Synchronous hook execution can be achieved by only allowing 1 job to run\n> at a time.\n>\n> Teach run_hooks() to use that function for simple hooks which don't\n> require stdin or capture of stderr.\n\nThis doesn't mention...\n\n>  \tint ret;\n> -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n> +\tstruct run_hooks_opt opt;\n>  \n> +\trun_hooks_opt_init_sync(&opt);\n\n\n...why we need to bring the s/macro/func/ init pattern, back, but looking ahead...\n\n> +int configured_hook_jobs(void)a\n> +{\n> +\tint n = online_cpus();\n> +\tgit_config_get_int(\"hook.jobs\", &n);\n> +\n> +\treturn n;\n> +}\n> +\n>  int hook_exists(const char *name)\n>  {\n>  \treturn !!find_hook(name);\n> @@ -117,6 +125,26 @@ struct list_head* hook_list(const char* hookname)\n>  \treturn hook_head;\n>  }\n>  \n> +void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n> +{\n> +\tstrvec_init(&o->env);\n> +\tstrvec_init(&o->args);\n> +\to->path_to_stdin = NULL;\n> +\to->jobs = 1;\n> +\to->dir = NULL;\n> +\to->feed_pipe = NULL;\n> +\to->feed_pipe_ctx = NULL;\n> +\to->consume_sideband = NULL;\n> +\to->invoked_hook = NULL;\n> +\to->absolute_path = 0;\n> +}\n> +\n> +void run_hooks_opt_init_async(struct run_hooks_opt *o)\n> +{\n> +\trun_hooks_opt_init_sync(o);\n> +\to->jobs = configured_hook_jobs();\n> +}\n\n...okey, so it's because you brought back the \"call jobs function\" in\none of the init functions.\n\nI had a comment in a previous round, I found\nhttps://lore.kernel.org/git/87lf7bzbrk.fsf@evledraar.gmail.com/, but I\nthink there was a later one where I commented on the \"jobs\" field\nspecifically.\n\nAnyway, it seems much easier to me to just keep the simpler macro init\nand then:\n\n> -\tif (options->jobs != 1)\n> -\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n> -\n>  \trun_processes_parallel_tr2(options->jobs,\n>  \t\t\t\t   pick_next_hook,\n>  \t\t\t\t   notify_start_failure,\n\nThere's this one place where we use the \"jobs\" parameter, just do\nsomething like this there:\n        \n        int configured_hook_jobs(void)\n        {\n                static int jobs;\n                if (!jobs)\n                    return jobs;\n                if (git_config_get_int(\"hook.jobs\", &jobs))\n                    jobs = online_cpus();\n                return jobs;\n        }\n\nI.e. you also needlessly call online_cpus() when we're about to override\nit in the config. The git_config_get_int()'s return value indicates\nwhether you need to do that. Then just:\n\n    int jobs = options->jobs ? options->jobs : configured_hook_jobs();\n    run_processes_parallel_tr2(jobs, [...]);\n\nOr some such, i.e. we can defer getting the job number away from startup\nto when we actually need to start those jobs, and your whole use of a\nfunction init pattern came down to doing that really early.\n\nAs an aside if you /do/ need to do init-via-function my 5726a6b4012 (*.c\n*_init(): define in terms of corresponding *_INIT macro, 2021-07-01) in\n\"next\" shows a much nicer way to do that. I.e. you'd just do:\n\n    void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n    {\n         struct run_hooks_opt blank = RUN_HOOKS_OPT_INIT;\n         memcpy(o, &blank, sizeof(*o));\n    }\n\n    void run_hooks_opt_init_async(struct run_hooks_opt *o)\n    {\n        run_hooks_opt_init_sync(o);\n        o->jobs = configured_hook_jobs();\n    }\n\nIn some cases we do actually need to do init via functions, but can init\na large option via the macro, which IMO is nicer to read, but here I\nthink we don't need the functions at all per the above.\n"},{"id":"430311","messageId":"87o8b2y6u1.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210715232603.3415111-4-emilyshaffer@google.com","subject":"Re: [PATCH 3/9] hook: introduce \"git hook list\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-16T08:52:27Z","receivedAt":"2021-07-16T08:57:08Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 15 2021, Emily Shaffer wrote:\n\n>  static const char * const builtin_hook_usage[] = {\n>  \tN_(\"git hook <command> [...]\"),\n> +\tN_(\"git hook list <hookname>\"),\n>  \tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n>  \tNULL\n\nUses <hook-name> already, let's use that too. I can't remember if it's\nsomething I changed myself, or if your original version used both and I\npicked one for consistency, or...\n\nAnyway, I can re-roll the base topic or whatever, but let's have the end\nresult use one or the other.\n\n> +\tif (argc < 1) {\n> +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n> +\t\t\t      builtin_hook_usage, list_options);\n> +\t}\n\n{} braces not needed.\n\n\n> +\tif (!strcmp(argv[0], \"list\"))\n> +\t\treturn list(argc, argv, prefix);\n>  \tif (!strcmp(argv[0], \"run\"))\n\nThis should be \"else if\" now.\n\n(Doesn't matter for code execution, just IMO readability, but I'll leave\nthat to you ... :)\n\n>  \t\treturn run(argc, argv, prefix);\n>  \telse\n> diff --git a/hook.c b/hook.c\n> index 935751fa6c..19138a8290 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -104,22 +104,20 @@ int hook_exists(const char *name)\n>  struct list_head* hook_list(const char* hookname)\n>  {\n>  \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> +\tconst char *hook_path = find_hook(hookname);\n> +\n>  \n>  \tINIT_LIST_HEAD(hook_head);\n>  \n>  \tif (!hookname)\n>  \t\treturn NULL;\n>  \n> -\tif (have_git_dir()) {\n> -\t\tconst char *hook_path = find_hook(hookname);\n> -\n> -\t\t/* Add the hook from the hookdir */\n> -\t\tif (hook_path) {\n> -\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> -\t\t\tto_add->hook_path = hook_path;\n> -\t\t\tto_add->feed_pipe_cb_data = NULL;\n> -\t\t\tlist_add_tail(&to_add->list, hook_head);\n> -\t\t}\n> +\t/* Add the hook from the hookdir */\n> +\tif (hook_path) {\n> +\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> +\t\tto_add->hook_path = hook_path;\n> +\t\tto_add->feed_pipe_cb_data = NULL;\n> +\t\tlist_add_tail(&to_add->list, hook_head);\n\nMaybe we should have a INIT for \"struct hook\" too? This also needlessly\nleaves behind an un-free'd hook struct in a way that it wouldn't if we\njust had this on the stack, no?\n"},{"id":"430312","messageId":"87lf66y6pd.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210715232603.3415111-5-emilyshaffer@google.com","subject":"Re: [PATCH 4/9] hook: treat hookdir hook specially","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-16T08:58:34Z","receivedAt":"2021-07-16T08:59:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 15 2021, Emily Shaffer wrote:\n\n> Soon, we will allow users to specify hooks using the config. These\n> config-specified hooks may require different child_process options than\n> hook executables in the gitdir. So, let's differentiate between hooks\n> coming from the gitdir and hooks coming from the config.\n>\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  hook.c | 3 ++-\n>  hook.h | 2 ++\n>  2 files changed, 4 insertions(+), 1 deletion(-)\n>\n> diff --git a/hook.c b/hook.c\n> index 19138a8290..3a588cb055 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -117,6 +117,7 @@ struct list_head* hook_list(const char* hookname)\n>  \t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n>  \t\tto_add->hook_path = hook_path;\n>  \t\tto_add->feed_pipe_cb_data = NULL;\n> +\t\tto_add->from_hookdir = 1;\n>  \t\tlist_add_tail(&to_add->list, hook_head);\n>  \t}\n>  \n> @@ -200,7 +201,7 @@ static int pick_next_hook(struct child_process *cp,\n>  \tcp->dir = hook_cb->options->dir;\n>  \n>  \t/* add command */\n> -\tif (hook_cb->options->absolute_path)\n> +\tif (run_me->from_hookdir && hook_cb->options->absolute_path)\n>  \t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n>  \telse\n>  \t\tstrvec_push(&cp->args, run_me->hook_path);\n> diff --git a/hook.h b/hook.h\n> index 586ddf40bb..60389cd8cd 100644\n> --- a/hook.h\n> +++ b/hook.h\n> @@ -22,6 +22,8 @@ struct hook {\n>  \t/* The path to the hook */\n>  \tconst char *hook_path;\n>  \n> +\tunsigned from_hookdir : 1;\n> +\n>  \t/*\n>  \t * Use this to keep state for your feed_pipe_fn if you are using\n>  \t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n\nThe \"from_hookdir\" looks like it isn't used until 6/9, and maybe the\nabsolute_path change too? In any case this seems like a carried-forward\nrebase of\nhttps://lore.kernel.org/git/20210311021037.3001235-5-emilyshaffer@google.com/\nor some version thereof.\n\nAt this point I tihnk it would be way better to squash this and other\nsuch changes that basically add a field to a struct that isn't used yet\ninto whatever commit use/need them.\n"},{"id":"430313","messageId":"87im1ay67y.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210715232603.3415111-7-emilyshaffer@google.com","subject":"Re: [PATCH 6/9] hook: include hooks from the config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-16T09:01:24Z","receivedAt":"2021-07-16T09:10:21Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 15 2021, Emily Shaffer wrote:\n\n> +static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n\nnit: \"*find[...]\" not \"* find[...]\", also let's wrap the long line.\n\n> +{\n> +\tstruct list_head *pos = NULL, *tmp = NULL;\n> +\tstruct hook *found = NULL;\n> +\n> +\tlist_for_each_safe(pos, tmp, head) {\n> +\t\tstruct hook *it = list_entry(pos, struct hook, list);\n> +\t\tif (!strcmp(it->command, command)) {\n> +\t\t    list_del(pos);\n> +\t\t    found = it;\n> +\t\t    break;\n\nIndented with spaces.\n\nAlso is there some subtlety in the list macro here or can we just\n\"s/break/return it/\" and skip the break/return pattern?\n\n> +static struct hook * append_or_move_hook(struct list_head *head, const char *command)\n\nSame whitespace nits.\n\n> +\tif (!to_add) {\n> +\t\t/* adding a new hook, not moving an old one */\n> +\t\tto_add = xmalloc(sizeof(*to_add));\n> +\t\tto_add->command = command;\n> +\t\tto_add->feed_pipe_cb_data = NULL;\n> +\t\t/* This gets overwritten in hook_list() for hookdir hooks. */\n> +\t\tto_add->from_hookdir = 0;\n\nI commented on init verbosity elsewhere, i.e. we could do some things\nvia macros, but in this case just having an \"init\" helper make sense,\nbut we have at least two places copying the same init of all fields,\nshould just be hook_init_hook() or whatever it'll be called. Maybe with\na second \"from hookdir\" param?\n\n> +\tif (!strcmp(key, hook_key)) {\n> +\t\tconst char *command = value;\n> +\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n> +\n> +\n\nNit: 3x\\n, not 2x\\n\n\n> +\t\tif (!command) {\n> +\t\t\tstrbuf_release(&hookcmd_name);\n\nYou don't need to strbuf_release() things that you haven't done anything\nexcept init'd, but also...\n\n> +\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n\n...even if that were the case and it called malloc() memory diligence\nwhen we're calling BUG() is probably going overboard, and I say that as\nsomeone who'll tend to go overboard with it :)\n\n> +\t\t}\n> +\n> +\t\t/* TODO: implement skipping hooks */\n> +\n> +\t\t/* TODO: immplement hook aliases */\n> +\n> +\t\t/*\n> +\t\t * TODO: implement an option-getting callback, e.g.\n> +\t\t *   get configs by pattern hookcmd.$value.*\n> +\t\t *   for each key+value, do_callback(key, value, cb_data)\n> +\t\t */\n\nI think we should drop the TODO and just let the commit message /\ncomments speak to what we actually implement, and subsequent patches can\nadd more features.\n\n> -\n> +\tstruct strbuf hook_key = STRBUF_INIT;\n> +\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n\nLet's use designated initializers.\n\n>  \n>  \tINIT_LIST_HEAD(hook_head);\n>  \n>  \tif (!hookname)\n>  \t\treturn NULL;\n>  \n> +\t/* Add the hooks from the config, e.g. hook.pre-commit.command */\n> +\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n> +\tgit_config(hook_config_lookup, &cb_data);\n> +\n> +\n\nAnother 3x\\n\n\n> +\t/* to enable oneliners, let config-specified hooks run in shell */\n> +\tcp->use_shell = !run_me->from_hookdir;\n\nI've lost track at this point, but doesn't that mean we're going to use\na shell when we run our own do-not-need-a-shell hooks ourselves?\n\nIsn't isatty() more appropriate here, or actually even interactively why\nis the shell needed (maybe this is answered elswhere...).\n"},{"id":"430314","messageId":"87fswey5wd.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210715232603.3415111-9-emilyshaffer@google.com","subject":"Re: [PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-16T09:13:42Z","receivedAt":"2021-07-16T09:17:19Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 15 2021, Emily Shaffer wrote:\n\n> To enable fine-grained options which apply to a single hook executable,\n> and to make it easier for a single executable to be run on multiple hook\n> events, teach \"hookcmd.<alias>.config\". These can be configured as\n> follows:\n> [...]\n> diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> index a97b980cca..5b35170664 100644\n> --- a/Documentation/config/hook.txt\n> +++ b/Documentation/config/hook.txt\n> @@ -3,6 +3,11 @@ hook.<command>.command::\n>  \texecutable on your device, a oneliner for your shell, or the name of a\n>  \thookcmd. See linkgit:git-hook[1].\n>  \n> +hookcmd.<name>.command::\n> +\tA command to execute during a hook for which <name> has been specified\n> +\tas a command. This can be an executable on your device or a oneliner for\n> +\tyour shell. See linkgit:git-hook[1].\n> +\n> [...]\n> +Global config\n> +----\n> +  [hook \"post-commit\"]\n> +    command = \"linter\"\n> +    command = \"~/typocheck.sh\"\n> +\n> +  [hookcmd \"linter\"]\n> +    command = \"/bin/linter --c\"\n> +----\n> +\n> +Local config\n> +----\n> +  [hook \"prepare-commit-msg\"]\n> +    command = \"linter\"\n> +  [hook \"post-commit\"]\n> +    command = \"python ~/run-test-suite.py\"\n> +----\n> +\n> +With these configs, you'd then run post-commit hooks in this order:\n> +\n> +  /bin/linter --c\n> +  ~/typocheck.sh\n> +  python ~/run-test-suite.py\n> +  .git/hooks/post-commit (if present)\n> +\n> +and prepare-commit-msg hooks in this order:\n> +\n> +  /bin/linter --c\n> +  .git/hooks/prepare-commit-msg (if present)\n>  \n\nI still have outstanding feedback on the fundamental design\nhere. I.e. why is this not:\n\n    hook.<name>.event = post-commit\n    hook.<name>.command = <path>\n\nSee:\n\nhttps://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n\nAs noted there I don't see why not, and more complexity results from the\ndesign choice of doing it the way you're proposing. I.e. we can't\ndiscover hooks based on config prefixes, and we end up sticking full FS\npaths in config keys.\n"},{"id":"430949","messageId":"YPnfWfHMWTWvJJkH@google.com","threadId":"55286","inReplyTo":"87r1fyy728.fsf@evledraar.gmail.com","subject":"Re: [PATCH 2/9] hook: allow parallel hook execution","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-22T21:12:57Z","receivedAt":"2021-07-22T21:13:07Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Jul 16, 2021 at 10:36:10AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Jul 15 2021, Emily Shaffer wrote:\n> \n> > In many cases, there's no reason not to allow hooks to execute in\n> > parallel. run_processes_parallel() is well-suited - it's a task queue\n> > that runs its housekeeping in series, which means users don't\n> > need to worry about thread safety on their callback data. True\n> > multithreaded execution with the async_* functions isn't necessary here.\n> > Synchronous hook execution can be achieved by only allowing 1 job to run\n> > at a time.\n> >\n> > Teach run_hooks() to use that function for simple hooks which don't\n> > require stdin or capture of stderr.\n> \n> This doesn't mention...\n> \n> >  \tint ret;\n> > -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n> > +\tstruct run_hooks_opt opt;\n> >  \n> > +\trun_hooks_opt_init_sync(&opt);\n> \n> \n> ...why we need to bring the s/macro/func/ init pattern, back, but looking ahead...\n> \n> > +int configured_hook_jobs(void)a\n> > +{\n> > +\tint n = online_cpus();\n> > +\tgit_config_get_int(\"hook.jobs\", &n);\n> > +\n> > +\treturn n;\n> > +}\n> > +\n> >  int hook_exists(const char *name)\n> >  {\n> >  \treturn !!find_hook(name);\n> > @@ -117,6 +125,26 @@ struct list_head* hook_list(const char* hookname)\n> >  \treturn hook_head;\n> >  }\n> >  \n> > +void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n> > +{\n> > +\tstrvec_init(&o->env);\n> > +\tstrvec_init(&o->args);\n> > +\to->path_to_stdin = NULL;\n> > +\to->jobs = 1;\n> > +\to->dir = NULL;\n> > +\to->feed_pipe = NULL;\n> > +\to->feed_pipe_ctx = NULL;\n> > +\to->consume_sideband = NULL;\n> > +\to->invoked_hook = NULL;\n> > +\to->absolute_path = 0;\n> > +}\n> > +\n> > +void run_hooks_opt_init_async(struct run_hooks_opt *o)\n> > +{\n> > +\trun_hooks_opt_init_sync(o);\n> > +\to->jobs = configured_hook_jobs();\n> > +}\n> \n> ...okey, so it's because you brought back the \"call jobs function\" in\n> one of the init functions.\n> \n> I had a comment in a previous round, I found\n> https://lore.kernel.org/git/87lf7bzbrk.fsf@evledraar.gmail.com/, but I\n> think there was a later one where I commented on the \"jobs\" field\n> specifically.\n> \n> Anyway, it seems much easier to me to just keep the simpler macro init\n> and then:\n> \n> > -\tif (options->jobs != 1)\n> > -\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n> > -\n> >  \trun_processes_parallel_tr2(options->jobs,\n> >  \t\t\t\t   pick_next_hook,\n> >  \t\t\t\t   notify_start_failure,\n> \n> There's this one place where we use the \"jobs\" parameter, just do\n> something like this there:\n>         \n>         int configured_hook_jobs(void)\n>         {\n>                 static int jobs;\n>                 if (!jobs)\n>                     return jobs;\n>                 if (git_config_get_int(\"hook.jobs\", &jobs))\n>                     jobs = online_cpus();\n>                 return jobs;\n>         }\n> \n> I.e. you also needlessly call online_cpus() when we're about to override\n> it in the config. The git_config_get_int()'s return value indicates\n> whether you need to do that. Then just:\n> \n>     int jobs = options->jobs ? options->jobs : configured_hook_jobs();\n>     run_processes_parallel_tr2(jobs, [...]);\n\nAhh, and then let RUN_HOOKS_OPT_INIT_ASYNC set jobs to 0 (\"go look it\nup\"). Yeah, that makes sense.\n\nShout if somehow you meant to leave just one initializer macro;\notherwise, I'll do it this way - with RUN_HOOKS_OPT_INIT_ASYNC and\nRUN_HOOKS_OPT_INIT_SYNC. I think it's valuable for hook callers to make\nit very plain at the callsite whether they're parallelizable or not, and\nI think\n\n struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n opt.jobs = 0;\n\ndoesn't make that as obvious.\n\n> \n> Or some such, i.e. we can defer getting the job number away from startup\n> to when we actually need to start those jobs, and your whole use of a\n> function init pattern came down to doing that really early.\n> \n> As an aside if you /do/ need to do init-via-function my 5726a6b4012 (*.c\n> *_init(): define in terms of corresponding *_INIT macro, 2021-07-01) in\n> \"next\" shows a much nicer way to do that. I.e. you'd just do:\n> \n>     void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n>     {\n>          struct run_hooks_opt blank = RUN_HOOKS_OPT_INIT;\n>          memcpy(o, &blank, sizeof(*o));\n>     }\n> \n>     void run_hooks_opt_init_async(struct run_hooks_opt *o)\n>     {\n>         run_hooks_opt_init_sync(o);\n>         o->jobs = configured_hook_jobs();\n>     }\n> \n> In some cases we do actually need to do init via functions, but can init\n> a large option via the macro, which IMO is nicer to read, but here I\n> think we don't need the functions at all per the above.\n"},{"id":"430966","messageId":"YPnp6akn9bfxGlnG@google.com","threadId":"55286","inReplyTo":"patch-02.27-7209f73f28-20210617T101217Z-avarab@gmail.com","subject":"Re: [PATCH 02/27] gc: use hook library for pre-auto-gc hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-22T21:58:01Z","receivedAt":"2021-07-22T21:58:14Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Jun 17, 2021 at 12:22:36PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> Using the hook.h library instead of the run-command.h library to run\n> pre-auto-gc means that those hooks can be set up in config files, as\n> well as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n\nThis one is a good example of an issue Junio pointed out to me in an\nearlier iteration - since the strvecs in run_hooks_opt need to be\ncleared, you need to be careful with exiting without running\nrun_hooks_opt_clear(). need_to_gc() can exit before running the hook in\nsome cases.\n\n - Emily\n\n> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  builtin/gc.c | 8 +++++++-\n>  1 file changed, 7 insertions(+), 1 deletion(-)\n> \n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index f05d2f0a1a..a12641a691 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -32,6 +32,7 @@\n>  #include \"remote.h\"\n>  #include \"object-store.h\"\n>  #include \"exec-cmd.h\"\n> +#include \"hook.h\"\n>  \n>  #define FAILED_RUN \"failed to run %s\"\n>  \n> @@ -348,6 +349,8 @@ static void add_repack_incremental_option(void)\n>  \n>  static int need_to_gc(void)\n>  {\n> +\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n> +\n>  \t/*\n>  \t * Setting gc.auto to 0 or negative can disable the\n>  \t * automatic gc.\n> @@ -394,8 +397,11 @@ static int need_to_gc(void)\n>  \telse\n>  \t\treturn 0;\n>  \n> -\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n> +\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n> +\t\trun_hooks_opt_clear(&hook_opt);\n>  \t\treturn 0;\n> +\t}\n> +\trun_hooks_opt_clear(&hook_opt);\n>  \treturn 1;\n>  }\n>  \n> -- \n> 2.32.0.576.g59759b6ca7d\n> \n"},{"id":"430968","messageId":"YPnuvRQLzKa6h9kk@google.com","threadId":"55286","inReplyTo":"87o8b2y6u1.fsf@evledraar.gmail.com","subject":"Re: [PATCH 3/9] hook: introduce \"git hook list\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-22T22:18:37Z","receivedAt":"2021-07-22T22:18:46Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Jul 16, 2021 at 10:52:27AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Jul 15 2021, Emily Shaffer wrote:\n> \n> >  static const char * const builtin_hook_usage[] = {\n> >  \tN_(\"git hook <command> [...]\"),\n> > +\tN_(\"git hook list <hookname>\"),\n> >  \tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n> >  \tNULL\n> \n> Uses <hook-name> already, let's use that too. I can't remember if it's\n> something I changed myself, or if your original version used both and I\n> picked one for consistency, or...\n> \n> Anyway, I can re-roll the base topic or whatever, but let's have the end\n> result use one or the other.\n\n'hook-name' is fine, I'll use that. Thanks for pointing it out.\n\n> \n> > +\tif (argc < 1) {\n> > +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n> > +\t\t\t      builtin_hook_usage, list_options);\n> > +\t}\n> \n> {} braces not needed.\nACK\n> \n> \n> > +\tif (!strcmp(argv[0], \"list\"))\n> > +\t\treturn list(argc, argv, prefix);\n> >  \tif (!strcmp(argv[0], \"run\"))\n> \n> This should be \"else if\" now.\n> \n> (Doesn't matter for code execution, just IMO readability, but I'll leave\n> that to you ... :)\n\nEh, I think it's easier to read in the strcmps line up, so I will leave\nit this way ;)\n\n> \n> >  \t\treturn run(argc, argv, prefix);\n> >  \telse\n> > diff --git a/hook.c b/hook.c\n> > index 935751fa6c..19138a8290 100644\n> > --- a/hook.c\n> > +++ b/hook.c\n> > @@ -104,22 +104,20 @@ int hook_exists(const char *name)\n> >  struct list_head* hook_list(const char* hookname)\n> >  {\n> >  \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> > +\tconst char *hook_path = find_hook(hookname);\n> > +\n> >  \n> >  \tINIT_LIST_HEAD(hook_head);\n> >  \n> >  \tif (!hookname)\n> >  \t\treturn NULL;\n> >  \n> > -\tif (have_git_dir()) {\n> > -\t\tconst char *hook_path = find_hook(hookname);\n> > -\n> > -\t\t/* Add the hook from the hookdir */\n> > -\t\tif (hook_path) {\n> > -\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> > -\t\t\tto_add->hook_path = hook_path;\n> > -\t\t\tto_add->feed_pipe_cb_data = NULL;\n> > -\t\t\tlist_add_tail(&to_add->list, hook_head);\n> > -\t\t}\n> > +\t/* Add the hook from the hookdir */\n> > +\tif (hook_path) {\n> > +\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> > +\t\tto_add->hook_path = hook_path;\n> > +\t\tto_add->feed_pipe_cb_data = NULL;\n> > +\t\tlist_add_tail(&to_add->list, hook_head);\n> \n> Maybe we should have a INIT for \"struct hook\" too? This also needlessly\n> leaves behind an un-free'd hook struct in a way that it wouldn't if we\n> just had this on the stack, no?\n\nI can clean it up here, but I don't think we need an initializer for\nstruct hook. This code chunk gets moved into one of the list\nmanipulators (append_or_move() or something) after the config is\nintroduced.\n\nI don't think it leaves an unfreed hook laying around, does it?\nlist_add_tail() uses the malloc'd pointer, and we free() in\nclear_hook_list(). What am I missing?\n\n - Emily\n"},{"id":"430969","messageId":"YPnwBu4oMA9K445J@google.com","threadId":"55286","inReplyTo":"87lf66y6pd.fsf@evledraar.gmail.com","subject":"Re: [PATCH 4/9] hook: treat hookdir hook specially","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-22T22:24:06Z","receivedAt":"2021-07-22T22:24:17Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Jul 16, 2021 at 10:58:34AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Jul 15 2021, Emily Shaffer wrote:\n> \n> > Soon, we will allow users to specify hooks using the config. These\n> > config-specified hooks may require different child_process options than\n> > hook executables in the gitdir. So, let's differentiate between hooks\n> > coming from the gitdir and hooks coming from the config.\n> >\n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >  hook.c | 3 ++-\n> >  hook.h | 2 ++\n> >  2 files changed, 4 insertions(+), 1 deletion(-)\n> >\n> > diff --git a/hook.c b/hook.c\n> > index 19138a8290..3a588cb055 100644\n> > --- a/hook.c\n> > +++ b/hook.c\n> > @@ -117,6 +117,7 @@ struct list_head* hook_list(const char* hookname)\n> >  \t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> >  \t\tto_add->hook_path = hook_path;\n> >  \t\tto_add->feed_pipe_cb_data = NULL;\n> > +\t\tto_add->from_hookdir = 1;\n> >  \t\tlist_add_tail(&to_add->list, hook_head);\n> >  \t}\n> >  \n> > @@ -200,7 +201,7 @@ static int pick_next_hook(struct child_process *cp,\n> >  \tcp->dir = hook_cb->options->dir;\n> >  \n> >  \t/* add command */\n> > -\tif (hook_cb->options->absolute_path)\n> > +\tif (run_me->from_hookdir && hook_cb->options->absolute_path)\n> >  \t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n> >  \telse\n> >  \t\tstrvec_push(&cp->args, run_me->hook_path);\n> > diff --git a/hook.h b/hook.h\n> > index 586ddf40bb..60389cd8cd 100644\n> > --- a/hook.h\n> > +++ b/hook.h\n> > @@ -22,6 +22,8 @@ struct hook {\n> >  \t/* The path to the hook */\n> >  \tconst char *hook_path;\n> >  \n> > +\tunsigned from_hookdir : 1;\n> > +\n> >  \t/*\n> >  \t * Use this to keep state for your feed_pipe_fn if you are using\n> >  \t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n> \n> The \"from_hookdir\" looks like it isn't used until 6/9, and maybe the\n> absolute_path change too? In any case this seems like a carried-forward\n> rebase of\n> https://lore.kernel.org/git/20210311021037.3001235-5-emilyshaffer@google.com/\n> or some version thereof.\n> \n> At this point I tihnk it would be way better to squash this and other\n> such changes that basically add a field to a struct that isn't used yet\n> into whatever commit use/need them.\n\nI think at this point we run into you and me having different\npatch-storytelling styles - which probably is what led to the big topic\nrestart in the first place ;)\n\nI'd prefer to see the \"start using config too\" patch stay as small as it\ncan; that's why I ended up with a handful of minor setup commits and\nthen one \"and now here's config\" commit.\n\nEven if it's different from how you would tell it - is it wrong? (And if\nit is, that's fine, and I'll change it, but I don't think it is - that's\nwhy I structured the series this way.)\n\n - Emily\n"},{"id":"430973","messageId":"YPn2dfjzFLovIYIX@google.com","threadId":"55286","inReplyTo":"87im1ay67y.fsf@evledraar.gmail.com","subject":"Re: [PATCH 6/9] hook: include hooks from the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-22T22:51:33Z","receivedAt":"2021-07-22T22:51:42Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Jul 16, 2021 at 11:01:24AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Jul 15 2021, Emily Shaffer wrote:\n> \n> > +static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n> \n> nit: \"*find[...]\" not \"* find[...]\", also let's wrap the long line.\nACK\n> \n> > +{\n> > +\tstruct list_head *pos = NULL, *tmp = NULL;\n> > +\tstruct hook *found = NULL;\n> > +\n> > +\tlist_for_each_safe(pos, tmp, head) {\n> > +\t\tstruct hook *it = list_entry(pos, struct hook, list);\n> > +\t\tif (!strcmp(it->command, command)) {\n> > +\t\t    list_del(pos);\n> > +\t\t    found = it;\n> > +\t\t    break;\n> \n> Indented with spaces.\n\nI don't know how I even did this. *facepalm*\n\n> \n> Also is there some subtlety in the list macro here or can we just\n> \"s/break/return it/\" and skip the break/return pattern?\n\nI guess it's probably fine, but we'd need the final return anyway\n(\"otherwise returns NULL\"). IMO one return is more readable than two\nreturns, so I'd rather leave this.\n> \n> > +static struct hook * append_or_move_hook(struct list_head *head, const char *command)\n> \n> Same whitespace nits.\nACK\n> \n> > +\tif (!to_add) {\n> > +\t\t/* adding a new hook, not moving an old one */\n> > +\t\tto_add = xmalloc(sizeof(*to_add));\n> > +\t\tto_add->command = command;\n> > +\t\tto_add->feed_pipe_cb_data = NULL;\n> > +\t\t/* This gets overwritten in hook_list() for hookdir hooks. */\n> > +\t\tto_add->from_hookdir = 0;\n> \n> I commented on init verbosity elsewhere, i.e. we could do some things\n> via macros, but in this case just having an \"init\" helper make sense,\n> but we have at least two places copying the same init of all fields,\n> should just be hook_init_hook() or whatever it'll be called. Maybe with\n> a second \"from hookdir\" param?\n\nHm, where is the second place where we init everything? I think with\nthis commit we remove anywhere we're putting together a 'struct hook' manually\nexcept during this helper? Hooks from hookdir are initted by\n'append_or_move_hook()'ing them to the end of the list and modifying the\nfrom_hookdir field, and builtin/hook.c just calls hook_list() (and some\nlist.h helpers to find an entry).\n\n> \n> > +\tif (!strcmp(key, hook_key)) {\n> > +\t\tconst char *command = value;\n> > +\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n> > +\n> > +\n> \n> Nit: 3x\\n, not 2x\\n\n> \n> > +\t\tif (!command) {\n> > +\t\t\tstrbuf_release(&hookcmd_name);\n> \n> You don't need to strbuf_release() things that you haven't done anything\n> except init'd, but also...\n> \n> > +\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\n> \n> ...even if that were the case and it called malloc() memory diligence\n> when we're calling BUG() is probably going overboard, and I say that as\n> someone who'll tend to go overboard with it :)\n\n:) ok.\n\n> \n> > +\t\t}\n> > +\n> > +\t\t/* TODO: implement skipping hooks */\n> > +\n> > +\t\t/* TODO: immplement hook aliases */\n> > +\n> > +\t\t/*\n> > +\t\t * TODO: implement an option-getting callback, e.g.\n> > +\t\t *   get configs by pattern hookcmd.$value.*\n> > +\t\t *   for each key+value, do_callback(key, value, cb_data)\n> > +\t\t */\n> \n> I think we should drop the TODO and just let the commit message /\n> comments speak to what we actually implement, and subsequent patches can\n> add more features.\n\nOk, sure.\n\n> \n> > -\n> > +\tstruct strbuf hook_key = STRBUF_INIT;\n> > +\tstruct hook_config_cb cb_data = { &hook_key, hook_head };\n> \n> Let's use designated initializers.\nACK\n> \n> >  \n> >  \tINIT_LIST_HEAD(hook_head);\n> >  \n> >  \tif (!hookname)\n> >  \t\treturn NULL;\n> >  \n> > +\t/* Add the hooks from the config, e.g. hook.pre-commit.command */\n> > +\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n> > +\tgit_config(hook_config_lookup, &cb_data);\n> > +\n> > +\n> \n> Another 3x\\n\nACK\n> \n> > +\t/* to enable oneliners, let config-specified hooks run in shell */\n> > +\tcp->use_shell = !run_me->from_hookdir;\n> \n> I've lost track at this point, but doesn't that mean we're going to use\n> a shell when we run our own do-not-need-a-shell hooks ourselves?\n> \n> Isn't isatty() more appropriate here, or actually even interactively why\n> is the shell needed (maybe this is answered elswhere...).\n\nuse_shell means \"conditionally guess whether I need to wrap this thing\nin `sh -c`\" - it doesn't have anything to do with TTY or not. So we need\nthis for something like `hook.post-commit.command = echo \"made a\ncommit\"`. In this case the entire argv[0] will be the oneliner, which\nyou will need use_shell set for. If we *do* just do something simple,\nlike `hook.post-commit.command = /bin/mail`, even though\nuse_shell is marked, the child_process runner will notice that there's\nno reason to wrap in 'sh -c' and so will just run the /bin/mail\nexecutable directly.\n\n - Emily\n"},{"id":"430976","messageId":"YPn6LInRNmpfffuC@google.com","threadId":"55286","inReplyTo":"87tukuy7vc.fsf@evledraar.gmail.com","subject":"Re: [PATCH 7/9] hook: allow out-of-repo 'git hook' invocations","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-22T23:07:24Z","receivedAt":"2021-07-22T23:07:33Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Jul 16, 2021 at 10:33:25AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Jul 15 2021, Emily Shaffer wrote:\n> \n> > Since hooks can now be supplied via the config, and a config can be\n> > present without a gitdir via the global and system configs, we can start\n> > to allow 'git hook run' to occur without a gitdir. This enables us to do\n> > things like run sendemail-validate hooks when running 'git send-email'\n> > from a nongit directory.\n> >\n> > It still doesn't make sense to look for hooks in the hookdir in nongit\n> > repos, though, as there is no hookdir.\n> \n> Hrm, I haven't tested but re the discussion we had about\n> RUN_SETUP_GENTLY on my re-rolled base topic is this really just a\n> regression in my changes there?\n> \n> I.e. I assumed we could do RUN_SETUP for the bug-for-bug compatibility\n> step, but send-email runs out of repo hooks and we just didn't have\n> tests for it, or am I missing something?\n\nI'm not sure. I could see a case for you including RUN_SETUP_GENTLY on\nyour series and adding a test for sendemail-validate + core.hooksPath in\nglobal config. I think I also don't have support for that case here,\nactually....\n\nAnyway, it looks like right now git-send-email.perl:validate_patch()\ndoesn't bother if it's out-of-repo, so this wouldn't have worked before (and\nstill won't work even after this change). So either I can add a patch to\nmy series to allow that, or you can modify your patch converting\nsendemail-validate to 'git hook run' to drop the 'if $repo' line and\nteach your series to behave correctly with nongit+hooksPath. It looks\nlike in my earlier attempt at the series, I did drop that check and run\n'git hook run' no matter what kind of directory we're in.\n\n - Emily\n"},{"id":"430978","messageId":"YPn/5GLrAb17KqyB@google.com","threadId":"55286","inReplyTo":"87fswey5wd.fsf@evledraar.gmail.com","subject":"Re: [PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-22T23:31:48Z","receivedAt":"2021-07-22T23:31:57Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Jul 16, 2021 at 11:13:42AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Jul 15 2021, Emily Shaffer wrote:\n> \n> > To enable fine-grained options which apply to a single hook executable,\n> > and to make it easier for a single executable to be run on multiple hook\n> > events, teach \"hookcmd.<alias>.config\". These can be configured as\n> > follows:\n> > [...]\n> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> > index a97b980cca..5b35170664 100644\n> > --- a/Documentation/config/hook.txt\n> > +++ b/Documentation/config/hook.txt\n> > @@ -3,6 +3,11 @@ hook.<command>.command::\n> >  \texecutable on your device, a oneliner for your shell, or the name of a\n> >  \thookcmd. See linkgit:git-hook[1].\n> >  \n> > +hookcmd.<name>.command::\n> > +\tA command to execute during a hook for which <name> has been specified\n> > +\tas a command. This can be an executable on your device or a oneliner for\n> > +\tyour shell. See linkgit:git-hook[1].\n> > +\n> > [...]\n> > +Global config\n> > +----\n> > +  [hook \"post-commit\"]\n> > +    command = \"linter\"\n> > +    command = \"~/typocheck.sh\"\n> > +\n> > +  [hookcmd \"linter\"]\n> > +    command = \"/bin/linter --c\"\n> > +----\n> > +\n> > +Local config\n> > +----\n> > +  [hook \"prepare-commit-msg\"]\n> > +    command = \"linter\"\n> > +  [hook \"post-commit\"]\n> > +    command = \"python ~/run-test-suite.py\"\n> > +----\n> > +\n> > +With these configs, you'd then run post-commit hooks in this order:\n> > +\n> > +  /bin/linter --c\n> > +  ~/typocheck.sh\n> > +  python ~/run-test-suite.py\n> > +  .git/hooks/post-commit (if present)\n> > +\n> > +and prepare-commit-msg hooks in this order:\n> > +\n> > +  /bin/linter --c\n> > +  .git/hooks/prepare-commit-msg (if present)\n> >  \n> \n> I still have outstanding feedback on the fundamental design\n> here. I.e. why is this not:\n> \n>     hook.<name>.event = post-commit\n>     hook.<name>.command = <path>\n> \n> See:\n> \n> https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n> \n> As noted there I don't see why not, and more complexity results from the\n> design choice of doing it the way you're proposing. I.e. we can't\n> discover hooks based on config prefixes, and we end up sticking full FS\n> paths in config keys.\n\nInteresting. My gut says that it would make it harder to neatly write a\nconfig with the same hook running at the very beginning of one event and\nthe very end of another, but I'm not sure whether that's a case to\noptimize for.\n\nI would also need twice as many lines to run a script I wrote as a hook\n- that is, the base case which is probably all most people will need. So\nwith your proposal I *must* name every hook I want to add. Instead of\n\"hook.pre-commit.command = ~/check-for-debug-strings\" I need to\nconfigure \"hook.check-debug-strings.event = pre-commit\" and\n\"hook.check-debug-strings.command = ~/check-for-debug-strings\". That's a\nlittle frustrating, and if I never want to skip that check or move it\nlater or something, it will be extra overhead for no gain for me.\n\nI do agree that your approach bans the regrettably awkward\n\"hookcmd.~/check-for-debug-strings.skip = true\", but I'm not sure\nwhether or not it's worth it.\n\nTo help us visualize the change, here's some common and complicated\ntasks and how they look with either schema (let mine be A and yours be\nB):\n\n#1 - Add a oneliner (not executing a script)\nA:\n[hook \"post-commit\"]\n\tcommand = echo post commit\nB:\n[hook \"oneliner\"]\n\tevent = post-commit\n\tcommand = echo post commit\n\n#2 - Execute a script\nA:\n[hook \"post-commit\"]\n\tcommand = ~/my-post-commit-hook\nB:\n[hook \"script\"]\n\tevent = post-commit\n\tcommand = ~/my-post-commit-hook\n\n#3 - Run a handful of scripts in a specific order on one event\nA:\n[hook \"post-commit\"]\n\tcommand = ~/my-post-commit-1\n\tcommand = ~/my-post-commit-2\n\tcommand = ~/my-post-commit-3\nB:\n[hook \"script 1\"]\n\tevent = post-commit\n\tcommand = ~/my-post-commit-1\n[hook \"script 2\"]\n\tevent = post-commit\n\tcommand = ~/my-post-commit-2\n[hook \"script 3\"]\n\tevent = post-commit\n\tcommand = ~/my-post-commit-3\n\n#4 - Skip a script configured more globally\nA:\n(original config)\n[hook \"post-commit\"]\n\tcommand = /corp/stuff/corp-post-commit\n(local config)\n[hookcmd \"/corp/stuff/corp-post-commit\"]\n\tskip = true\nOR,\n(original config)\n[hookcmd \"corp-pc\"]\n\tcommand = /corp/stuff/corp-post-commit\n[hook \"post-commit\"]\n\tcommand = corp-pc\n(local config)\n[hookcmd \"corp-pc\"]\n\tskip = true\nB:\n(original config)\n[hook \"corp-pc\"]\n\tevent = post-commit\n\tcommand = /corp/stuff/corp-post-commit\n(local config)\n[hook \"corp-pc\"]\n\tskip = true\n\n#5 - Execute one script for multiple events\nA:\n[hookcmd \"reusable-hook\"]\n\tcommand = /long/path/reusable-hook\n[hook \"post-commit\"]\n\tcommand = reusable-hook\n[hook \"pre-commit\"]\n\tcommand = reusable-hook\n[hook \"prepare-commit-msg\"]\n\tcommand = reusable-hook\nB:\n[hook \"reusable-hook\"]\n\tcommand = /long/path/reusable-hook\n\tevent = post-commit\n\tevent = pre-commit\n\tevent = prepare-commit-msg\n\n#6 - Execute the same script early for one event and late for another\nevent\nA:\n(global)\n[hookcmd \"reusable-hook\"]\n\tcommand = /long/path/reusable-hook\n[hook \"pre-commit\"]\n\tcommand = reusable-hook\n(local)\n[hook \"post-commit\"]\n\tcommand = other\n\tcommand = hooks\n\tcommand = reusable-hook\n\nB:\n(global)\n[hook \"reusable-hook\"]\n\tcommand = /long/path/reusable-hook\n\tevent = pre-commit\n(local)\n[hook \"other\"]\n\tevent = post-commit\n\tcommand = other\n[hook \"hooks\"]\n\tevent = post-commit\n\tcommand = hooks\n[hook \"reusable-hook\"]\n\tevent = reusable-hook\n\n\nI'm not going to comment on which one I like more yet - I think I will\nstudy this for a while and let others weigh in on their preferences too.\nI tried to order the cases from most common to less common. Please feel\nfree to chime in with more use cases that you think would be handy which\nI forgot :)\n\n - Emily\n"},{"id":"431004","messageId":"87bl6ttmlv.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YPn/5GLrAb17KqyB@google.com","subject":"Re: [PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-23T07:41:35Z","receivedAt":"2021-07-23T09:17:48Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 22 2021, Emily Shaffer wrote:\n\n> On Fri, Jul 16, 2021 at 11:13:42AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> \n>> On Thu, Jul 15 2021, Emily Shaffer wrote:\n>> \n>> > To enable fine-grained options which apply to a single hook executable,\n>> > and to make it easier for a single executable to be run on multiple hook\n>> > events, teach \"hookcmd.<alias>.config\". These can be configured as\n>> > follows:\n>> > [...]\n>> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n>> > index a97b980cca..5b35170664 100644\n>> > --- a/Documentation/config/hook.txt\n>> > +++ b/Documentation/config/hook.txt\n>> > @@ -3,6 +3,11 @@ hook.<command>.command::\n>> >  \texecutable on your device, a oneliner for your shell, or the name of a\n>> >  \thookcmd. See linkgit:git-hook[1].\n>> >  \n>> > +hookcmd.<name>.command::\n>> > +\tA command to execute during a hook for which <name> has been specified\n>> > +\tas a command. This can be an executable on your device or a oneliner for\n>> > +\tyour shell. See linkgit:git-hook[1].\n>> > +\n>> > [...]\n>> > +Global config\n>> > +----\n>> > +  [hook \"post-commit\"]\n>> > +    command = \"linter\"\n>> > +    command = \"~/typocheck.sh\"\n>> > +\n>> > +  [hookcmd \"linter\"]\n>> > +    command = \"/bin/linter --c\"\n>> > +----\n>> > +\n>> > +Local config\n>> > +----\n>> > +  [hook \"prepare-commit-msg\"]\n>> > +    command = \"linter\"\n>> > +  [hook \"post-commit\"]\n>> > +    command = \"python ~/run-test-suite.py\"\n>> > +----\n>> > +\n>> > +With these configs, you'd then run post-commit hooks in this order:\n>> > +\n>> > +  /bin/linter --c\n>> > +  ~/typocheck.sh\n>> > +  python ~/run-test-suite.py\n>> > +  .git/hooks/post-commit (if present)\n>> > +\n>> > +and prepare-commit-msg hooks in this order:\n>> > +\n>> > +  /bin/linter --c\n>> > +  .git/hooks/prepare-commit-msg (if present)\n>> >  \n>> \n>> I still have outstanding feedback on the fundamental design\n>> here. I.e. why is this not:\n>> \n>>     hook.<name>.event = post-commit\n>>     hook.<name>.command = <path>\n>> \n>> See:\n>> \n>> https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n>> \n>> As noted there I don't see why not, and more complexity results from the\n>> design choice of doing it the way you're proposing. I.e. we can't\n>> discover hooks based on config prefixes, and we end up sticking full FS\n>> paths in config keys.\n>\n> Interesting. My gut says that it would make it harder to neatly write a\n> config with the same hook running at the very beginning of one event and\n> the very end of another, but I'm not sure whether that's a case to\n> optimize for.\n>\n> I would also need twice as many lines to run a script I wrote as a hook\n> - that is, the base case which is probably all most people will need. So\n> with your proposal I *must* name every hook I want to add. Instead of\n> \"hook.pre-commit.command = ~/check-for-debug-strings\" I need to\n> configure \"hook.check-debug-strings.event = pre-commit\" and\n> \"hook.check-debug-strings.command = ~/check-for-debug-strings\". That's a\n> little frustrating, and if I never want to skip that check or move it\n> later or something, it will be extra overhead for no gain for me.\n\nThe gain is that \"git hook list\" becomes a trivial \"git config\n-show-origin --show-scope --get-regexp\" wrapper.\n\nSo the series either doesn't need \"git hook list\" or such a thing\nbecomes much less complex, especially given the proposed addition of\nother features in the area like \"git hook edit\", i.e. (quoting the\nlinked E-Mail):\n\n    As just one example; surely \"git config edit <name>\" would need to\n    run around and find config files to edit, then open them in a loop\n    for you, no?\n\n    Which we'd eventually want for \"git config\" in general with an\n    --edit-regexp option or whatever, which brings us (well, at least\n    me) back to \"then let's just add it to git-config?\".\n\n> I do agree that your approach bans the regrettably awkward\n> \"hookcmd.~/check-for-debug-strings.skip = true\", but I'm not sure\n> whether or not it's worth it.\n\nThat design choice also means that you can't expand the path using \"git\nconfig --get --type=path.\n\nWe do have that with the \"includeIf\" construct, but if we can avoid it\nwe should, it makes it play nicer with other assumptions and features of\nthe config system.\n\nAs noted in the follow-up reply while we don't case normalize the LeVeL\"\npart of \"ThReE.LeVeL.KeY\" that's tolower(), which we know isn't a 1=1\nmapping on some\nFS's. https://lore.kernel.org/git/87y2ebo16v.fsf@evledraar.gmail.com/\n\n> To help us visualize the change, here's some common and complicated\n> tasks and how they look with either schema (let mine be A and yours be\n> B):\n\nBefore diving into that, I'll just say I don't care about the trivial\nspecifics of how this is done, i.e. the bikeshedding of what the config\nkeys etc. are named.\n\nJust in (as noted above) design choices here forcing avoidable\ncomplexity in other areas.\n\n> #1 - Add a oneliner (not executing a script)\n> A:\n> [hook \"post-commit\"]\n> \tcommand = echo post commit\n> B:\n> [hook \"oneliner\"]\n> \tevent = post-commit\n> \tcommand = echo post commit\n> #2 - Execute a script\n> A:\n> [hook \"post-commit\"]\n> \tcommand = ~/my-post-commit-hook\n> B:\n> [hook \"script\"]\n> \tevent = post-commit\n> \tcommand = ~/my-post-commit-hook\n\n...\n\n> #3 - Run a handful of scripts in a specific order on one event\n> A:\n> [hook \"post-commit\"]\n> \tcommand = ~/my-post-commit-1\n> \tcommand = ~/my-post-commit-2\n> \tcommand = ~/my-post-commit-3\n> B:\n> [hook \"script 1\"]\n> \tevent = post-commit\n> \tcommand = ~/my-post-commit-1\n> [hook \"script 2\"]\n> \tevent = post-commit\n> \tcommand = ~/my-post-commit-2\n> [hook \"script 3\"]\n> \tevent = post-commit\n> \tcommand = ~/my-post-commit-3\n\nTo reply to all the above, yes, your suggestion comes out ahead in being\nless verbose.\n\nBut isn't the real difference not in the differing prefixes, i.e. hook.*\nand hookcmd.* (A) v.s. always hook.* (B, which is what I'm mainly\nfocusing on, i.e. what requires the added complexity.\n\nBut that in that in your proposed way of doing it it's:\n\n    hook.<event-or-name>.*\n\nV.s. my suggestion of:\n\n    hook.<name>.*\n\nAnd thus whenever you have a <event-or-name> that just happens to be a\nbuilt-in hook listed in githooks(5) we in (A) implicitly expand config\nlike:\n\n    hook.post-commit.command = echo foo\n\nTo:\n\n    hook.post-commit.command = echo hi\n    hook.post-commit.type    = post-commit\n\nBut not knowing about \"foo\" we don't expand:\n\n    hook.foo.command = echo foo\n\nTo:\n\n    hook.foo.command = echo hi\n    hook.foo.type    = foo # This would be an error, or ignored.\n\nBut rather leave \"dangling\" for the user to later supply the \"*.event\"\nthemselves, i.e.:\n\n    hook.foo.command = echo hi\n    hook.foo.event = post-commit\n\nAnd means that you presumably need to detect this case and error about\nit, but my proposed model does not:\n\n    hook.post-commit.command = echo hi\n    # User error: \"*.type\" and <event-or-name>\" does not match for\n    # \"hook.*.command\"\n    hook.post-commit.type    = pre-commit\n\nAnd furthermore, that if someone in your model were to do:\n\n    hook.verify-commit.command = echo hi\n\nIt's \"dangling\" today, but if a later version of git learns about a\n\"verify-commit\" hook we'll start running it unexpectedly.\n\nBecause your design conflates the slot for known hook type names and\nuser-supplied names.\n\nOn balance I think it's better just to always supply two lines per-hook,\nbut whether we have this proposed shorthand or not is mostly orthogonal\nto everything I mentioned in\nhttps://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n\nI.e. my proposed version could also have it, but thinking about it I\nthink it's not worth it, we should always use <name>, not\n<event-or-name> for the reasons noted if you'll read ahead...\n\n> #4 - Skip a script configured more globally\n> A:\n> (original config)\n> [hook \"post-commit\"]\n> \tcommand = /corp/stuff/corp-post-commit\n> (local config)\n> [hookcmd \"/corp/stuff/corp-post-commit\"]\n> \tskip = true\n> OR,\n> (original config)\n> [hookcmd \"corp-pc\"]\n> \tcommand = /corp/stuff/corp-post-commit\n> [hook \"post-commit\"]\n> \tcommand = corp-pc\n> (local config)\n> [hookcmd \"corp-pc\"]\n> \tskip = true\n> B:\n> (original config)\n> [hook \"corp-pc\"]\n> \tevent = post-commit\n> \tcommand = /corp/stuff/corp-post-commit\n> (local config)\n> [hook \"corp-pc\"]\n> \tskip = true\n\n...which are among other things that no, my proposed version doesn't\nhave \"skip\" at all, see point #3 at\nhttps://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n\nI.e. I think the \"skip\" is a thing that falls out of a complexity of\nyour design that I'm proposing to do away with.\n\nThat complexity being that you use <event-or-name> and I use <name>, and\nyou want to in turn support N number of \"*.command\" for any\n\"hook.<event-or-name>\".\n\nI'm suggesting we use the typical \"last key wins\" semantics, if you want\nmultiple commands for a given hook type you'll just supply multiple\n\"hook.<name>\" sections with the same \"hook.*.event = type\" in all.\n\nThe way to skip hooks in my proposal is:\n\n    hook.<name>.command = true\n\nOr whatever noop command you want on the RHS. In practice we wouldn't\ninvoke \"true\", just like we don't invoke \"cat\" for \"PAGER=cat\".\n\nBut unlike \"*.skip\" that doesn't require complexity in implementation or\nuser understanding, it just falls naturally out of the rest of the\nmodel.\n\n> #5 - Execute one script for multiple events\n> A:\n> [hookcmd \"reusable-hook\"]\n> \tcommand = /long/path/reusable-hook\n> [hook \"post-commit\"]\n> \tcommand = reusable-hook\n> [hook \"pre-commit\"]\n> \tcommand = reusable-hook\n> [hook \"prepare-commit-msg\"]\n> \tcommand = reusable-hook\n> B:\n> [hook \"reusable-hook\"]\n> \tcommand = /long/path/reusable-hook\n> \tevent = post-commit\n> \tevent = pre-commit\n> \tevent = prepare-commit-msg\n\nIt's been so long since I wrote the original E-Mail that I'm having to\nskim my own summary there & come up with things as I go along, so maybe\nthis conflicts with something I said earlier.\n\nBut no, I don't think I meant that *.event should be multi-value, or\nthat that magic is worthwhile. I.e. I think we just want:\n\n    [hook \"not-a-reusable-section-1\"]\n        command = /long/path/reusable-hook\n        event = post-commit\n    [hook \"not-a-reusable-section-2\"]\n        command = /long/path/reusable-hook\n        event = pre-commit\n    [hook \"not-a-reusable-section-3\"]\n        command = /long/path/reusable-hook\n        event = prepare-commit-msg\n\nI.e. is wanting to use the same command for N different hooks really\nthat common a use-case to avoid a little verbosity, and quite a lot of\ncomplexity?\n\nHow would such a hook even be implemented?\n\nWe don't have anything in your hook execution model (and I don't\nremember you adding this) which passes down a \"you are this type of\nhook\" to hooks.\n\nIt's now implicit in that the hook invoked at .git/hooks/post-commit or\n.git/hooks/pre-commit can check its own basename, but it won't be with\nconfigurable hooks.\n\nWe could start passing down a GIT_HOOK_TYPE in the environment or\nwhatever, but I think the simpler case of just having the user do it is\nbetter.\n\nI'm assuming that mainly because people have wanted a\n\"/long/path/reusable-hook\" router script because we have not supported\nexecuting N hooks, or supported concurrency. Once we do that the\ncomplexity of such routing scripts (mostly, not everyone's needs will be\nmet) will be replaced by a little bit of config.\n\nI don't see why it's worth it to micro-optimize for lines in that config\nat the cost of increased config complexity.\n\nFor example, how do you \"skip\" an \"event\" type? You have it for\n\"*.command\"? So let's say in your model (A) I have system config like:\n\n    [hook \"my-reusable\"]\n    command = /some/commit-msg-hook\n    event = prepare-commit-msg\n\nNow I want to skip that, easy enough, in my local config:\n\n    [hook \"my-reusable\"]\n    skip = true\n\nOr if I wanted to also invoke it for commit-msg events, in my local\nconfig (which piggy-backs on the system one):\n\n    # Now runs for both prepare-commit-msg and commit-message\n    [hook \"my-reusable\"]\n    event = commit-msg\n\nOr, if I want to skip the command name and substitute my own:\n\n    [hook \"my-reusable\"]\n    skip = true\n    command = /my/commit-msg-hook\n\nI'm just assuming you mean that to work, as discussed above I think all\nthis is leading us down the road of too much complexity.\n\nBut anyway, now I want to instead not run it as a prepare-commit-msg\nhook, but a commit-msg hook.\n\nAs far as I can tell you haven't specified that, but I think something\nconsistent with your model would be\n\n    [hook \"my-reusable\"]\n    event = skip\n    event = commit-msg\n\nOr rather, because that conflates \"skip\" with a hook event type name:\n\n    [hook \"my-reusable\"]\n    skipEvent = true\n    event = commit-msg\n\nI.e. the \"skip\" you have now really means \"skipCommand\", so we'll need a\nskipSomeName to skip other \"hook.*.someName\".\n\nAs noted above I think all of this is too complex, let's just do the\nsystem config like this (exact same as the above):\n\n    [hook \"my-reusable\"]\n    command = /some/commit-msg-hook\n    event = prepare-commit-msg\n\nTo skip it, in my local config:\n\n    [hook \"my-reusable\"]\n    command = true\n\nAnd to re-use /some/commit-msg-hook without having to re-include it in\nyour local config the answer is that we don't support that level of\ncleverness. Just do this:\n\n    # First skip the system hook\n    [hook \"my-reusable\"]\n    command = true\n\n    # Set up another hook, just copy/pasting the /some/commit-msg-hook command\n    [hook \"my-local\"]\n    command = /some/commit-msg-hook\n    event = commit-msg\n\nI.e. I think these cases of someone having say a /etc/gitconfig hook on\ntheir system not under their control that they want to not run, *but*\nnot as the \"event\" there, but as another event type is too specific a\nuse-case for us to worry about.\n\n> #6 - Execute the same script early for one event and late for another\n> event\n> A:\n> (global)\n> [hookcmd \"reusable-hook\"]\n> \tcommand = /long/path/reusable-hook\n> [hook \"pre-commit\"]\n> \tcommand = reusable-hook\n> (local)\n> [hook \"post-commit\"]\n> \tcommand = other\n> \tcommand = hooks\n> \tcommand = reusable-hook\n\nEven with I think it's fair to say deep knowledge of your proposal at\nthis point I still needed to read this a few times to see if that:\n\n    command = reusable-hook\n\nIs referring to:\n\n    [hookcmd \"reusable-hook\"]\n\nI.e. is it going to run:\n\n    command = /long/path/reusable-hook\n\nOr is it just re-specifying /long/path/reusable-hook but relying on a\n$PATH lookup?\n\nHaving reasoned through that I think the answer is the former. But that\nalso means that in your model:\n\n    [hookcmd \"rm -rf /\"]\n    command = echo this will not actually destroy your data\n    [hook \"pre-commit\"]\n    command = rm -rf /\n\nIs going to run that friendly \"echo\" command, since \"command = rm -rf /\"\njust refers to the \"rm -rf /\" <name>, not <command>, right the \"hookcmd\"\nline is removed, at which point we'll stop treating it as a <name> and\nrun it as a <command>?\n\nIn practice I think users are unlikely to use actively harmful names\nlike that.\n\nI'm just making the point that I should not need to know about previous\nconfig to see if a \"hook.pre-commit.command = rm -rf /\" is harmless or\nnot, or need to carefully squint to see if the \"reusable-hook\" is\nreferring to a section name or command name.\n\nOr am I just confused at this point?\n\n> B:\n> (global)\n> [hook \"reusable-hook\"]\n> \tcommand = /long/path/reusable-hook\n> \tevent = pre-commit\n> (local)\n> [hook \"other\"]\n> \tevent = post-commit\n> \tcommand = other\n> [hook \"hooks\"]\n> \tevent = post-commit\n> \tcommand = hooks\n> [hook \"reusable-hook\"]\n> \tevent = reusable-hook\n\nNo, that's not what I'm proposing. I.e. \"*.event\" is reserved for\nbuilt-in events listed in githooks(5) that git itself knows about.\n\nIf you say \"event = reusable-hook\" that's going to be one of ignored,\nwarned or errored about.\n\n(Probably \"ignored\" is the best thing to support multi-version setups,\nbut that's not hook-specific, just how we treat unknown config in\ngeneral)\n\nMy version of this would be the same as noted with \"we don't support\nthat level of cleverness[...]\" above.\n\nI.e. you'd not re-use that \"/long/path/reusable-hook\", you can skip it\nthrough \"command = true\", then just copy the relevnt part into your new\nconfig. So:\n\n        (global)\n\t[hook \"reusable-hook\"]\n\t\tcommand = /long/path/reusable-hook\n\t\tevent = pre-commit\n\n\t(local)\n\t[hook \"reusable-hook\"]\n\t\tcommand = true # skip it\n\n        # The \"hooks\" name is arbitrary, \"my-hooks\" or whatever would be\n        # clearer, but just going with your example...\n\n\t[hook \"hooks\"]\n\t\tevent = post-commit\n\t\tcommand = hooks\n        # Not very reusable then...   \n\t[hook \"reusable-hook\"]\n                command = /long/path/reusable-hook\n\t\tevent = pre-commit\n\n\n> [...]Please feel free to chime in with more use cases that you think would\n> be handy which I forgot :)\n\nI couldn't find this at a quick glance but I think we also had a\ndisussion at some point about hooks controlling parallelism. AFAICT your\ncurrent implementation just has global:\n\n    hook.jobs=N\n\nAnd we then blacklist certain hooks to always have hook.jobs=1, e.g. the\ncommit-msg hook that needs an implicit \"lock\" on that file (or rather,\nwe think that's the most common use-case).\n\nI think my version of always having hook.<name>.{event,command} be one\nvalue is also better in that case, i.e. we'd then:\n\n    [hook \"myhook\"]\n    command = some-command\n    event = pre-receive\n    parallel = true # the default\n\n    [hook \"myhook2\"]\n    command = some-command2\n    event = pre-receive\n    parallel = true # the default\n\n    [hook \"myhook3\"]\n    command = some-unconcurrent-command\n    event = pre-receive\n    parallel = false # I'm not OK with concurrency\n\nTo go along with something like:\n\n    hook\n\tjobs = 8\n\nOr:\n\n    [hookEvent \"pre-receive\"]\n    jobs = 4\n\nBut if you have N numer of \"command\" in a section it gets murky, does\n\"parallel = false\" then apply to the whole section, but sections can\nhave one or more values. So we'd need both a\n\"hookEvent.pre-receive.jobs=N\" and per-section config to\ncontrol/suppress parallelism?\n\nI haven't thought about it deeply, but have a hunch that having sections\nbe a 1=1 mapping with a specific command instead of either 1=1 or 1=many\nis going to make that easier to implement and understand.\n"},{"id":"431005","messageId":"878s1xtmgz.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YPn6LInRNmpfffuC@google.com","subject":"Re: [PATCH 7/9] hook: allow out-of-repo 'git hook' invocations","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-23T09:18:22Z","receivedAt":"2021-07-23T09:20:43Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 22 2021, Emily Shaffer wrote:\n\n> On Fri, Jul 16, 2021 at 10:33:25AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> \n>> On Thu, Jul 15 2021, Emily Shaffer wrote:\n>> \n>> > Since hooks can now be supplied via the config, and a config can be\n>> > present without a gitdir via the global and system configs, we can start\n>> > to allow 'git hook run' to occur without a gitdir. This enables us to do\n>> > things like run sendemail-validate hooks when running 'git send-email'\n>> > from a nongit directory.\n>> >\n>> > It still doesn't make sense to look for hooks in the hookdir in nongit\n>> > repos, though, as there is no hookdir.\n>> \n>> Hrm, I haven't tested but re the discussion we had about\n>> RUN_SETUP_GENTLY on my re-rolled base topic is this really just a\n>> regression in my changes there?\n>> \n>> I.e. I assumed we could do RUN_SETUP for the bug-for-bug compatibility\n>> step, but send-email runs out of repo hooks and we just didn't have\n>> tests for it, or am I missing something?\n>\n> I'm not sure. I could see a case for you including RUN_SETUP_GENTLY on\n> your series and adding a test for sendemail-validate + core.hooksPath in\n> global config. I think I also don't have support for that case here,\n> actually....\n\nThe narrow goal of the base topic is to be a bug-for-bug compatible\nversion of what we have now on \"master\", just via a dispatch\ncommand/API.\n\nSo yeah, that git-send-email.perl behavior looks bizarre, but let's fix\nit in a separate set of behavior changing patches on top, no?\n\n> Anyway, it looks like right now git-send-email.perl:validate_patch()\n> doesn't bother if it's out-of-repo, so this wouldn't have worked before (and\n> still won't work even after this change). So either I can add a patch to\n> my series to allow that, or you can modify your patch converting\n> sendemail-validate to 'git hook run' to drop the 'if $repo' line and\n> teach your series to behave correctly with nongit+hooksPath. It looks\n> like in my earlier attempt at the series, I did drop that check and run\n> 'git hook run' no matter what kind of directory we're in.\n\nI think this was one of the things that either needed a test change in\nyour series, or I saw tests for changes to existing but untested\nbehavior, I think this was the latter. \n\nI completely agree that the behavior is weird, probably undesired and\nshould be changed. I'm just saying that we should split up refactorings\n& behavior changes. \n"},{"id":"431006","messageId":"875yx1tm7p.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YPn2dfjzFLovIYIX@google.com","subject":"Re: [PATCH 6/9] hook: include hooks from the config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-23T09:22:07Z","receivedAt":"2021-07-23T09:26:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 22 2021, Emily Shaffer wrote:\n\n> On Fri, Jul 16, 2021 at 11:01:24AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> \n>> On Thu, Jul 15 2021, Emily Shaffer wrote:\n>> \n>> > +static struct hook * find_hook_by_command(struct list_head *head, const char *command)\n>> \n>> nit: \"*find[...]\" not \"* find[...]\", also let's wrap the long line.\n> ACK\n>> \n>> > +{\n>> > +\tstruct list_head *pos = NULL, *tmp = NULL;\n>> > +\tstruct hook *found = NULL;\n>> > +\n>> > +\tlist_for_each_safe(pos, tmp, head) {\n>> > +\t\tstruct hook *it = list_entry(pos, struct hook, list);\n>> > +\t\tif (!strcmp(it->command, command)) {\n>> > +\t\t    list_del(pos);\n>> > +\t\t    found = it;\n>> > +\t\t    break;\n>> \n>> Indented with spaces.\n>\n> I don't know how I even did this. *facepalm*\n>\n>> \n>> Also is there some subtlety in the list macro here or can we just\n>> \"s/break/return it/\" and skip the break/return pattern?\n>\n> I guess it's probably fine, but we'd need the final return anyway\n> (\"otherwise returns NULL\"). IMO one return is more readable than two\n> returns, so I'd rather leave this.\n\nSure makes sense. I'd tend to go for two returns, but let's not split\nhairs on personal style.\n\n>> \n>> > +static struct hook * append_or_move_hook(struct list_head *head, const char *command)\n>> \n>> Same whitespace nits.\n> ACK\n>> \n>> > +\tif (!to_add) {\n>> > +\t\t/* adding a new hook, not moving an old one */\n>> > +\t\tto_add = xmalloc(sizeof(*to_add));\n>> > +\t\tto_add->command = command;\n>> > +\t\tto_add->feed_pipe_cb_data = NULL;\n>> > +\t\t/* This gets overwritten in hook_list() for hookdir hooks. */\n>> > +\t\tto_add->from_hookdir = 0;\n>> \n>> I commented on init verbosity elsewhere, i.e. we could do some things\n>> via macros, but in this case just having an \"init\" helper make sense,\n>> but we have at least two places copying the same init of all fields,\n>> should just be hook_init_hook() or whatever it'll be called. Maybe with\n>> a second \"from hookdir\" param?\n>\n> Hm, where is the second place where we init everything? I think with\n> this commit we remove anywhere we're putting together a 'struct hook' manually\n> except during this helper? Hooks from hookdir are initted by\n> 'append_or_move_hook()'ing them to the end of the list and modifying the\n> from_hookdir field, and builtin/hook.c just calls hook_list() (and some\n> list.h helpers to find an entry).\n\nLooking again I think I just misread this then, thanks.\n\n>> > +\t/* to enable oneliners, let config-specified hooks run in shell */\n>> > +\tcp->use_shell = !run_me->from_hookdir;\n>> \n>> I've lost track at this point, but doesn't that mean we're going to use\n>> a shell when we run our own do-not-need-a-shell hooks ourselves?\n>> \n>> Isn't isatty() more appropriate here, or actually even interactively why\n>> is the shell needed (maybe this is answered elswhere...).\n>\n> use_shell means \"conditionally guess whether I need to wrap this thing\n> in `sh -c`\" - it doesn't have anything to do with TTY or not. So we need\n> this for something like `hook.post-commit.command = echo \"made a\n> commit\"`. In this case the entire argv[0] will be the oneliner, which\n> you will need use_shell set for. If we *do* just do something simple,\n> like `hook.post-commit.command = /bin/mail`, even though\n> use_shell is marked, the child_process runner will notice that there's\n> no reason to wrap in 'sh -c' and so will just run the /bin/mail\n> executable directly.\n\nAh, I missed that. Makes sense.\n"},{"id":"431007","messageId":"8735s5tm3t.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YPnwBu4oMA9K445J@google.com","subject":"Re: [PATCH 4/9] hook: treat hookdir hook specially","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-23T09:26:56Z","receivedAt":"2021-07-23T09:28:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 22 2021, Emily Shaffer wrote:\n\n> On Fri, Jul 16, 2021 at 10:58:34AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> \n>> On Thu, Jul 15 2021, Emily Shaffer wrote:\n>> \n>> > Soon, we will allow users to specify hooks using the config. These\n>> > config-specified hooks may require different child_process options than\n>> > hook executables in the gitdir. So, let's differentiate between hooks\n>> > coming from the gitdir and hooks coming from the config.\n>> >\n>> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n>> > ---\n>> >  hook.c | 3 ++-\n>> >  hook.h | 2 ++\n>> >  2 files changed, 4 insertions(+), 1 deletion(-)\n>> >\n>> > diff --git a/hook.c b/hook.c\n>> > index 19138a8290..3a588cb055 100644\n>> > --- a/hook.c\n>> > +++ b/hook.c\n>> > @@ -117,6 +117,7 @@ struct list_head* hook_list(const char* hookname)\n>> >  \t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n>> >  \t\tto_add->hook_path = hook_path;\n>> >  \t\tto_add->feed_pipe_cb_data = NULL;\n>> > +\t\tto_add->from_hookdir = 1;\n>> >  \t\tlist_add_tail(&to_add->list, hook_head);\n>> >  \t}\n>> >  \n>> > @@ -200,7 +201,7 @@ static int pick_next_hook(struct child_process *cp,\n>> >  \tcp->dir = hook_cb->options->dir;\n>> >  \n>> >  \t/* add command */\n>> > -\tif (hook_cb->options->absolute_path)\n>> > +\tif (run_me->from_hookdir && hook_cb->options->absolute_path)\n>> >  \t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n>> >  \telse\n>> >  \t\tstrvec_push(&cp->args, run_me->hook_path);\n>> > diff --git a/hook.h b/hook.h\n>> > index 586ddf40bb..60389cd8cd 100644\n>> > --- a/hook.h\n>> > +++ b/hook.h\n>> > @@ -22,6 +22,8 @@ struct hook {\n>> >  \t/* The path to the hook */\n>> >  \tconst char *hook_path;\n>> >  \n>> > +\tunsigned from_hookdir : 1;\n>> > +\n>> >  \t/*\n>> >  \t * Use this to keep state for your feed_pipe_fn if you are using\n>> >  \t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n>> \n>> The \"from_hookdir\" looks like it isn't used until 6/9, and maybe the\n>> absolute_path change too? In any case this seems like a carried-forward\n>> rebase of\n>> https://lore.kernel.org/git/20210311021037.3001235-5-emilyshaffer@google.com/\n>> or some version thereof.\n>> \n>> At this point I tihnk it would be way better to squash this and other\n>> such changes that basically add a field to a struct that isn't used yet\n>> into whatever commit use/need them.\n>\n> I think at this point we run into you and me having different\n> patch-storytelling styles - which probably is what led to the big topic\n> restart in the first place ;)\n>\n> I'd prefer to see the \"start using config too\" patch stay as small as it\n> can; that's why I ended up with a handful of minor setup commits and\n> then one \"and now here's config\" commit.\n>\n> Even if it's different from how you would tell it - is it wrong? (And if\n> it is, that's fine, and I'll change it, but I don't think it is - that's\n> why I structured the series this way.)\n\nIt's not wrong, if you'd like it that way sure.\n\nThe only reason I nudged you about it was that I assumed you'd perhaps\nmostly rebased these one-at-a-time on top of the base topic, before the\nbase topic the patches were much larger.\n\nThere is some arbitrary cut-off-limit where it makes sense to split the\naddition of code to be used later with the actual use, and that limit is\na matter of taste.\n\nI thought perhaps given the base topic some of these patches should be\nsqushed given their new size, but if you've looked at them and think\nit's fine as-is let's leave it at that.\n"},{"id":"431009","messageId":"87zguds7ho.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YPnuvRQLzKa6h9kk@google.com","subject":"Re: [PATCH 3/9] hook: introduce \"git hook list\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-23T09:29:04Z","receivedAt":"2021-07-23T09:29:48Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 22 2021, Emily Shaffer wrote:\n\n> On Fri, Jul 16, 2021 at 10:52:27AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> \n>> On Thu, Jul 15 2021, Emily Shaffer wrote:\n>> \n>> >  static const char * const builtin_hook_usage[] = {\n>> >  \tN_(\"git hook <command> [...]\"),\n>> > +\tN_(\"git hook list <hookname>\"),\n>> >  \tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n>> >  \tNULL\n>> \n>> Uses <hook-name> already, let's use that too. I can't remember if it's\n>> something I changed myself, or if your original version used both and I\n>> picked one for consistency, or...\n>> \n>> Anyway, I can re-roll the base topic or whatever, but let's have the end\n>> result use one or the other.\n>\n> 'hook-name' is fine, I'll use that. Thanks for pointing it out.\n>\n>> \n>> > +\tif (argc < 1) {\n>> > +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n>> > +\t\t\t      builtin_hook_usage, list_options);\n>> > +\t}\n>> \n>> {} braces not needed.\n> ACK\n>> \n>> \n>> > +\tif (!strcmp(argv[0], \"list\"))\n>> > +\t\treturn list(argc, argv, prefix);\n>> >  \tif (!strcmp(argv[0], \"run\"))\n>> \n>> This should be \"else if\" now.\n>> \n>> (Doesn't matter for code execution, just IMO readability, but I'll leave\n>> that to you ... :)\n>\n> Eh, I think it's easier to read in the strcmps line up, so I will leave\n> it this way ;)\n\n*nod*\n\n>> \n>> >  \t\treturn run(argc, argv, prefix);\n>> >  \telse\n>> > diff --git a/hook.c b/hook.c\n>> > index 935751fa6c..19138a8290 100644\n>> > --- a/hook.c\n>> > +++ b/hook.c\n>> > @@ -104,22 +104,20 @@ int hook_exists(const char *name)\n>> >  struct list_head* hook_list(const char* hookname)\n>> >  {\n>> >  \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n>> > +\tconst char *hook_path = find_hook(hookname);\n>> > +\n>> >  \n>> >  \tINIT_LIST_HEAD(hook_head);\n>> >  \n>> >  \tif (!hookname)\n>> >  \t\treturn NULL;\n>> >  \n>> > -\tif (have_git_dir()) {\n>> > -\t\tconst char *hook_path = find_hook(hookname);\n>> > -\n>> > -\t\t/* Add the hook from the hookdir */\n>> > -\t\tif (hook_path) {\n>> > -\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n>> > -\t\t\tto_add->hook_path = hook_path;\n>> > -\t\t\tto_add->feed_pipe_cb_data = NULL;\n>> > -\t\t\tlist_add_tail(&to_add->list, hook_head);\n>> > -\t\t}\n>> > +\t/* Add the hook from the hookdir */\n>> > +\tif (hook_path) {\n>> > +\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n>> > +\t\tto_add->hook_path = hook_path;\n>> > +\t\tto_add->feed_pipe_cb_data = NULL;\n>> > +\t\tlist_add_tail(&to_add->list, hook_head);\n>> \n>> Maybe we should have a INIT for \"struct hook\" too? This also needlessly\n>> leaves behind an un-free'd hook struct in a way that it wouldn't if we\n>> just had this on the stack, no?\n>\n> I can clean it up here, but I don't think we need an initializer for\n> struct hook. This code chunk gets moved into one of the list\n> manipulators (append_or_move() or something) after the config is\n> introduced.\n>\n> I don't think it leaves an unfreed hook laying around, does it?\n> list_add_tail() uses the malloc'd pointer, and we free() in\n> clear_hook_list(). What am I missing?\n\nI don't think you're missing anything. I replied on that \"struct hook\"\nin another E-Mail, i.e. I think I just misread parts of this. Thanks.\n"},{"id":"431010","messageId":"87wnphs7de.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YPnfWfHMWTWvJJkH@google.com","subject":"Re: [PATCH 2/9] hook: allow parallel hook execution","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-07-23T09:30:47Z","receivedAt":"2021-07-23T09:32:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Jul 22 2021, Emily Shaffer wrote:\n\n> On Fri, Jul 16, 2021 at 10:36:10AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> \n>> On Thu, Jul 15 2021, Emily Shaffer wrote:\n>> \n>> > In many cases, there's no reason not to allow hooks to execute in\n>> > parallel. run_processes_parallel() is well-suited - it's a task queue\n>> > that runs its housekeeping in series, which means users don't\n>> > need to worry about thread safety on their callback data. True\n>> > multithreaded execution with the async_* functions isn't necessary here.\n>> > Synchronous hook execution can be achieved by only allowing 1 job to run\n>> > at a time.\n>> >\n>> > Teach run_hooks() to use that function for simple hooks which don't\n>> > require stdin or capture of stderr.\n>> \n>> This doesn't mention...\n>> \n>> >  \tint ret;\n>> > -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>> > +\tstruct run_hooks_opt opt;\n>> >  \n>> > +\trun_hooks_opt_init_sync(&opt);\n>> \n>> \n>> ...why we need to bring the s/macro/func/ init pattern, back, but looking ahead...\n>> \n>> > +int configured_hook_jobs(void)a\n>> > +{\n>> > +\tint n = online_cpus();\n>> > +\tgit_config_get_int(\"hook.jobs\", &n);\n>> > +\n>> > +\treturn n;\n>> > +}\n>> > +\n>> >  int hook_exists(const char *name)\n>> >  {\n>> >  \treturn !!find_hook(name);\n>> > @@ -117,6 +125,26 @@ struct list_head* hook_list(const char* hookname)\n>> >  \treturn hook_head;\n>> >  }\n>> >  \n>> > +void run_hooks_opt_init_sync(struct run_hooks_opt *o)\n>> > +{\n>> > +\tstrvec_init(&o->env);\n>> > +\tstrvec_init(&o->args);\n>> > +\to->path_to_stdin = NULL;\n>> > +\to->jobs = 1;\n>> > +\to->dir = NULL;\n>> > +\to->feed_pipe = NULL;\n>> > +\to->feed_pipe_ctx = NULL;\n>> > +\to->consume_sideband = NULL;\n>> > +\to->invoked_hook = NULL;\n>> > +\to->absolute_path = 0;\n>> > +}\n>> > +\n>> > +void run_hooks_opt_init_async(struct run_hooks_opt *o)\n>> > +{\n>> > +\trun_hooks_opt_init_sync(o);\n>> > +\to->jobs = configured_hook_jobs();\n>> > +}\n>> \n>> ...okey, so it's because you brought back the \"call jobs function\" in\n>> one of the init functions.\n>> \n>> I had a comment in a previous round, I found\n>> https://lore.kernel.org/git/87lf7bzbrk.fsf@evledraar.gmail.com/, but I\n>> think there was a later one where I commented on the \"jobs\" field\n>> specifically.\n>> \n>> Anyway, it seems much easier to me to just keep the simpler macro init\n>> and then:\n>> \n>> > -\tif (options->jobs != 1)\n>> > -\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n>> > -\n>> >  \trun_processes_parallel_tr2(options->jobs,\n>> >  \t\t\t\t   pick_next_hook,\n>> >  \t\t\t\t   notify_start_failure,\n>> \n>> There's this one place where we use the \"jobs\" parameter, just do\n>> something like this there:\n>>         \n>>         int configured_hook_jobs(void)\n>>         {\n>>                 static int jobs;\n>>                 if (!jobs)\n>>                     return jobs;\n>>                 if (git_config_get_int(\"hook.jobs\", &jobs))\n>>                     jobs = online_cpus();\n>>                 return jobs;\n>>         }\n>> \n>> I.e. you also needlessly call online_cpus() when we're about to override\n>> it in the config. The git_config_get_int()'s return value indicates\n>> whether you need to do that. Then just:\n>> \n>>     int jobs = options->jobs ? options->jobs : configured_hook_jobs();\n>>     run_processes_parallel_tr2(jobs, [...]);\n>\n> Ahh, and then let RUN_HOOKS_OPT_INIT_ASYNC set jobs to 0 (\"go look it\n> up\"). Yeah, that makes sense.\n>\n> Shout if somehow you meant to leave just one initializer macro;\n> otherwise, I'll do it this way - with RUN_HOOKS_OPT_INIT_ASYNC and\n> RUN_HOOKS_OPT_INIT_SYNC. I think it's valuable for hook callers to make\n> it very plain at the callsite whether they're parallelizable or not, and\n> I think\n>\n>  struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>  opt.jobs = 0;\n>\n> doesn't make that as obvious.\n\nYes agreed, sorry about the ambiguity, I meant we should have two init\nmacros, just like e.g. STRING_LIST_INIT_NODUP and STRING_LIST_INIT_DUP.\n"},{"id":"431065","messageId":"60fafd8295996_defb208dc@natae.notmuch","threadId":"55286","inReplyTo":"YPnwBu4oMA9K445J@google.com","subject":"Re: [PATCH 4/9] hook: treat hookdir hook specially","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-07-23T17:33:54Z","receivedAt":"2021-07-23T17:34:00Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Hello Emily,\n\nEmily Shaffer wrote:\n> On Fri, Jul 16, 2021 at 10:58:34AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > On Thu, Jul 15 2021, Emily Shaffer wrote:\n> > \n> > > Soon, we will allow users to specify hooks using the config. These\n> > > config-specified hooks may require different child_process options than\n> > > hook executables in the gitdir. So, let's differentiate between hooks\n> > > coming from the gitdir and hooks coming from the config.\n> > >\n> > > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > > ---\n> > >  hook.c | 3 ++-\n> > >  hook.h | 2 ++\n> > >  2 files changed, 4 insertions(+), 1 deletion(-)\n> > >\n> > > diff --git a/hook.c b/hook.c\n> > > index 19138a8290..3a588cb055 100644\n> > > --- a/hook.c\n> > > +++ b/hook.c\n> > > @@ -117,6 +117,7 @@ struct list_head* hook_list(const char* hookname)\n> > >  \t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> > >  \t\tto_add->hook_path = hook_path;\n> > >  \t\tto_add->feed_pipe_cb_data = NULL;\n> > > +\t\tto_add->from_hookdir = 1;\n> > >  \t\tlist_add_tail(&to_add->list, hook_head);\n> > >  \t}\n> > >  \n> > > @@ -200,7 +201,7 @@ static int pick_next_hook(struct child_process *cp,\n> > >  \tcp->dir = hook_cb->options->dir;\n> > >  \n> > >  \t/* add command */\n> > > -\tif (hook_cb->options->absolute_path)\n> > > +\tif (run_me->from_hookdir && hook_cb->options->absolute_path)\n> > >  \t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n> > >  \telse\n> > >  \t\tstrvec_push(&cp->args, run_me->hook_path);\n> > > diff --git a/hook.h b/hook.h\n> > > index 586ddf40bb..60389cd8cd 100644\n> > > --- a/hook.h\n> > > +++ b/hook.h\n> > > @@ -22,6 +22,8 @@ struct hook {\n> > >  \t/* The path to the hook */\n> > >  \tconst char *hook_path;\n> > >  \n> > > +\tunsigned from_hookdir : 1;\n> > > +\n> > >  \t/*\n> > >  \t * Use this to keep state for your feed_pipe_fn if you are using\n> > >  \t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n> > \n> > The \"from_hookdir\" looks like it isn't used until 6/9, and maybe the\n> > absolute_path change too? In any case this seems like a carried-forward\n> > rebase of\n> > https://lore.kernel.org/git/20210311021037.3001235-5-emilyshaffer@google.com/\n> > or some version thereof.\n> > \n> > At this point I tihnk it would be way better to squash this and other\n> > such changes that basically add a field to a struct that isn't used yet\n> > into whatever commit use/need them.\n> \n> I think at this point we run into you and me having different\n> patch-storytelling styles - which probably is what led to the big topic\n> restart in the first place ;)\n\nYes, but as a reader of the story I prefer not to have to read the\nentire thing in order to understand it. I prefer each page to tell a\nsmall story.\n\n> I'd prefer to see the \"start using config too\" patch stay as small as it\n> can; that's why I ended up with a handful of minor setup commits and\n> then one \"and now here's config\" commit.\n\nI'm in favor of small patches too, but not to the point where the patch\nis not useful by itself.\n\nThis is where where patch storytelling and actual storytelling differ:\nwe don't need Chekhov's guns.\n\n> Even if it's different from how you would tell it - is it wrong? (And if\n> it is, that's fine, and I'll change it, but I don't think it is - that's\n> why I structured the series this way.)\n\nIt's not wrong.\n\nBut one of the strongest advantages of open source is that you can have\nmany more reviewers than in closed source, and following Linus's law:\n\"given enough eyeballs, all bugs are shallow\", that's a great thing.\n\nBut in order to fully take advantage of that you need to write patches\nin a way that armchair reviewers can simply take a cursory look at the\npatch and figure out that's obviously correct.\n\nPutting my armchair reviewer hat I cannot do that for this particular\npatch, I would need to do more work to make sense of it, and while I'm\nwriting this message to explains that, others will simply skip it, and\nthat's a lost opportunity.\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"431078","messageId":"CAPig+cS6EmxZeO5Ad3oUMn2tkSwE1CdLTaA7-Pp2CbmO--EW3g@mail.gmail.com","threadId":"55286","inReplyTo":"60fafd8295996_defb208dc@natae.notmuch","subject":"Re: [PATCH 4/9] hook: treat hookdir hook specially","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-07-23T18:22:38Z","receivedAt":"2021-07-23T18:22:52Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Fri, Jul 23, 2021 at 1:34 PM Felipe Contreras\n<felipe.contreras@gmail.com> wrote:\n> Emily Shaffer wrote:\n> > On Fri, Jul 16, 2021 at 10:58:34AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > > At this point I tihnk it would be way better to squash this and other\n> > > such changes that basically add a field to a struct that isn't used yet\n> > > into whatever commit use/need them.\n> >\n> > I think at this point we run into you and me having different\n> > patch-storytelling styles - which probably is what led to the big topic\n> > restart in the first place ;)\n>\n> Yes, but as a reader of the story I prefer not to have to read the\n> entire thing in order to understand it. I prefer each page to tell a\n> small story.\n>\n> Putting my armchair reviewer hat I cannot do that for this particular\n> patch, I would need to do more work to make sense of it, and while I'm\n> writing this message to explains that, others will simply skip it, and\n> that's a lost opportunity.\n\nImplicit in what Felipe and Ævar are saying is that a well-structured\npatch series asks the reviewer to keep only one or two details in mind\nafter reading a patch in order to understand the next patch in the\nseries, and that the reviewer shouldn't be expected to keep a large\nset of details in mind over several patches. Unlike the author of the\npatches who can keep all the details in mind at once and understands\nthe series in its entirety, reviewers (usually) don't have such\nluxury[1]. So, it's important to hand-hold reviewers as much as\npossible by not asking them to remember a lot of details between\npatches and by ensuring that the details which they must remember only\nneed to be remembered for a very short time. This is why it is\nhelpful, for instance, to bundle documentation and test updates in the\npatch with changes to code, so the reviewer can see at a glance that\nthe changes to documentation and tests match the changes to the code,\nrather than delaying documentation and test updates until later in the\nseries.\n\n[1]: If you've ever read a novel in which the author has multiple\nstory lines running and switches between story lines infrequently,\nsuch that when the author switches back to story-line \"A\", which you\nlast saw 100 pages ago, and you can't remember what was going in \"A\"\nor even who the minor characters are anymore, so that you have to go\nback and reread 10 or 20 pages from the previous time you saw \"A\",\nthen that's representative of the difficulty reviewers can experience\nwhen reading a patch series, except with a patch series, the cognitive\nload is already quite high. (Very nice run-on sentence I just wrote.)\n"},{"id":"431092","messageId":"60fb203f641a9_1073e208e9@natae.notmuch","threadId":"55286","inReplyTo":"CAPig+cS6EmxZeO5Ad3oUMn2tkSwE1CdLTaA7-Pp2CbmO--EW3g@mail.gmail.com","subject":"Re: [PATCH 4/9] hook: treat hookdir hook specially","fromName":"Felipe Contreras","fromEmail":"felipe.contreras@gmail.com","sentAt":"2021-07-23T20:02:07Z","receivedAt":"2021-07-23T20:02:11Z","isPatch":true,"sender":{"key":"felipe.contreras@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8358?v=4"},"body":"Eric Sunshine wrote:\n> On Fri, Jul 23, 2021 at 1:34 PM Felipe Contreras\n> <felipe.contreras@gmail.com> wrote:\n> > Emily Shaffer wrote:\n> > > On Fri, Jul 16, 2021 at 10:58:34AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> > > > At this point I tihnk it would be way better to squash this and other\n> > > > such changes that basically add a field to a struct that isn't used yet\n> > > > into whatever commit use/need them.\n> > >\n> > > I think at this point we run into you and me having different\n> > > patch-storytelling styles - which probably is what led to the big topic\n> > > restart in the first place ;)\n> >\n> > Yes, but as a reader of the story I prefer not to have to read the\n> > entire thing in order to understand it. I prefer each page to tell a\n> > small story.\n> >\n> > Putting my armchair reviewer hat I cannot do that for this particular\n> > patch, I would need to do more work to make sense of it, and while I'm\n> > writing this message to explains that, others will simply skip it, and\n> > that's a lost opportunity.\n> \n> Implicit in what Felipe and Ævar are saying is that a well-structured\n> patch series asks the reviewer to keep only one or two details in mind\n> after reading a patch in order to understand the next patch in the\n> series, and that the reviewer shouldn't be expected to keep a large\n> set of details in mind over several patches. Unlike the author of the\n> patches who can keep all the details in mind at once and understands\n> the series in its entirety, reviewers (usually) don't have such\n> luxury[1]. So, it's important to hand-hold reviewers as much as\n> possible by not asking them to remember a lot of details between\n> patches and by ensuring that the details which they must remember only\n> need to be remembered for a very short time. This is why it is\n> helpful, for instance, to bundle documentation and test updates in the\n> patch with changes to code, so the reviewer can see at a glance that\n> the changes to documentation and tests match the changes to the code,\n> rather than delaying documentation and test updates until later in the\n> series.\n\nAnother important point is that while as a patch author it's natural to\npush back against possibly unnecessary changes because it would require\nconsiderably more work, there's a reason why writers sometimes chose to\nrewrite entire chapters, and it's because every effort to improve the\ntext as an author would be translated into less effort for the\npotentially millions of readers.\n\nIt's a multiplicative effect.\n\nSure, programmers don't have millions of readers, but I think it makes\nsense to do 2x the effort as a patch author to receive 4x the review\n(at least).\n\nCheers.\n\n-- \nFelipe Contreras"},{"id":"431434","messageId":"YQHAasrmcbdiCDQF@google.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"Re: [PATCH 00/27] Base for \"config-based-hooks\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-07-28T20:39:06Z","receivedAt":"2021-07-28T20:39:15Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Jun 17, 2021 at 12:22:34PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> This v3 of the \"Base for 'config-based-hooks'\" topic is >95% a\n> slimmed-down versio nof Emily Schaffer's work to introduce a mechanism\n(Tiniest of nits - I only have a 'c' in my middle name ;) )\n> to drive hooks via config.\n> \n> This topic doesn't do that, but moves all hook execution (C libary and\n> Perl|Python script) to either the new hook.[ch] library code, or the\n> \"git hook run\" utility.\n> \n> See previous iterations for more details:\n> \n>  v0 (Emily's): http://lore.kernel.org/git/20210527000856.695702-1-emilyshaffer@google.com\n>  v1: https://lore.kernel.org/git/cover-00.31-00000000000-20210528T110515Z-avarab@gmail.com/\n>  v2: http://lore.kernel.org/git/cover-00.30-00000000000-20210614T101920Z-avarab@gmail.com\n> \n> This series gained two new dependencies since v2, my just-submitted\n> preparatory topics:\n> \n>     https://lore.kernel.org/git/cover-0.3-0000000000-20210617T095827Z-avarab@gmail.com/\n>     https://lore.kernel.org/git/cover-0.3-0000000000-20210617T100239Z-avarab@gmail.com/\n> \n> And hopefully addreses all the feedback on v2, mainly/entirely from\n> Emily at https://lore.kernel.org/git/YMfLO9CT+iIDR3OA@google.com\n> \n> I'd normally have waited longer for a v3, but as discussed in the\n> small hook.[ch] topic that precedes this one building the new\n> hook-list.h had an error on Windows CI due to a missing CMake change,\n> that's now fixed.\n> \n> Other changes can be seen in the range-diff, all rather trivial fixes\n> like comment fixes, trimming off \"argc/argv\" before we pass things to\n> \"git hook run\", removing a redundant test setup etc.\n> \n> I also added a GIT_TEST_FAKE_HOOKS=true for use in the test suite to\n> make us support a \"test-hook\" and a \"does-not-exist\" hook, we'd\n> previously accept those outside the test environment.\n\nI initially wasn't sure this is necessary, and I'm still not entirely\nsure - it makes more sense to me to see test-hook and does-not-exist use\nessentially\nhttps://lore.kernel.org/git/20210715232603.3415111-8-emilyshaffer@google.com.\n\nWould it make sense for you to absorb that much - \"when we're 'git hook\nrun'ing, don't validate hookname\" - into your series to avoid this\ntest-fake-hooks thing, and also teach the test-helper to use (or not\nuse) the allow_unknown flag?\n\n[...]\n\n> Range-diff:\n>  1:  447d349c73 !  1:  cf4b06bfdf hook: add 'run' subcommand\n>     @@ Documentation/git-hook.txt (new)\n>     @@ Documentation/githooks.txt: and \"0\" meaning they were not.\n>     -@@ Makefile: LIB_OBJS += hash-lookup.o\n>     - LIB_OBJS += hashmap.o\n>     - LIB_OBJS += help.o\n>     - LIB_OBJS += hex.o\n>     -+LIB_OBJS += hook.o\n>     - LIB_OBJS += ident.o\n>     - LIB_OBJS += json-writer.o\n>     - LIB_OBJS += kwset.o\n\nAh, no longer adding hook.o to the Makefile because it's been added in\none of your splintered-off dependency topics. Ok.\n\n>     @@ builtin/hook.c (new)\n>      +#include \"strvec.h\"\n>      +\n>      +static const char * const builtin_hook_usage[] = {\n>     ++\tN_(\"git hook <command> [...]\"),\n>     ++\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>     ++\tNULL\n>     ++};\n>     ++\n>     ++static const char * const builtin_hook_run_usage[] = {\n>      +\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>      +\tNULL\n>      +};\nCool, so now we have a separate 'git hook<enter>' usage and 'git hook\nrun<enter>' usage.\n\nI was going to complain about the strings being duplicated, but it\nappears that other commands that use this pattern - like 'git remote' -\ndo it the same way as you're doing here. So seems like that's a problem\nto ponder later, unrelated to this topic ;)\n\n>     @@ builtin/hook.c (new)\n>      +\t};\n>      +\n>      +\targc = parse_options(argc, argv, prefix, run_options,\n>     -+\t\t\t     builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n>     ++\t\t\t     builtin_hook_run_usage,\n>     ++\t\t\t     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n>      +\n>     -+\tif (argc > 2) {\n>     -+\t\tif (strcmp(argv[2], \"--\") &&\n>     -+\t\t    strcmp(argv[2], \"--end-of-options\"))\n>     ++\tif (argc > 1) {\n>     ++\t\tif (strcmp(argv[1], \"--\") &&\n>     ++\t\t    strcmp(argv[1], \"--end-of-options\"))\n>      +\t\t\t/* Having a -- for \"run\" is mandatory */\n>      +\t\t\tusage_with_options(builtin_hook_usage, run_options);\n>      +\t\t/* Add our arguments, start after -- */\n>     -+\t\tfor (i = 3 ; i < argc; i++)\n>     ++\t\tfor (i = 2 ; i < argc; i++)\n\nNice, so the implementation of run_hooks() has one less stray arg to\nworry about.\n\n>      +\t\t\tstrvec_push(&opt.args, argv[i]);\n>      +\t}\n>      +\n>      +\t/* Need to take into account core.hooksPath */\n>      +\tgit_config(git_default_config, NULL);\n>      +\n>     -+\thook_name = argv[1];\n>     ++\t/*\n>     ++\t * We are not using run_hooks() because we'd like to detect\n>     ++\t * missing hooks. Let's find it ourselves and call\n>     ++\t * run_found_hooks() instead.\n>     ++\t */\n>     ++\thook_name = argv[0];\n\nAnd a more explicit comment. Thanks.\n\n>      +\thook_path = find_hook(hook_name);\n>      +\tif (!hook_path) {\n>      +\t\terror(\"cannot find a hook named %s\", hook_name);\n>     @@ builtin/hook.c (new)\n>      +\tstruct option builtin_hook_options[] = {\n>      +\t\tOPT_END(),\n>      +\t};\n>     ++\targc = parse_options(argc, argv, NULL, builtin_hook_options,\n>     ++\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n>     ++\tif (!argc)\n>     ++\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n\nNice - parse_options() removes the 'hook' part of the invocation for us,\nso we don't need to carry it around into the subcommands. Thanks.\n\n>      +\n>     -+\tif (!strcmp(argv[1], \"run\"))\n>     ++\tif (!strcmp(argv[0], \"run\"))\n>      +\t\treturn run(argc, argv, prefix);\n>     -+\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n>     -+\treturn 1;\n>     ++\telse\n>     ++\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n>      +}\n\nAh, and here we lose the awkward \"return something even though\nusage_with_options() exits\". Ok.\n\n>     + ## hook.c ##\n>     + static int known_hook(const char *name)\n>     + {\n>     + \tconst char **p;\n>     + \tsize_t len = strlen(name);\n>     ++\tstatic int test_hooks_ok = -1;\n>     ++\n\nSetting aside whether or not I think it's a good idea, this seems\ncorrect - we either allow test hooks for the entire lifetime of the\nprocess (e.g. from the test runner) or enforce hookname validation for\nthe entire lifetime of the process (e.g. 'git hook run' or normal Git\noperation). So it makes sense to cache it in a static. Ok.\n\n>     + \tfor (p = hook_name_list; *p; p++) {\n>     + \t\tconst char *hook = *p;\n>     + \n>     +@@ hook.c: static int known_hook(const char *name)\n>     + \t\t\treturn 1;\n>     + \t}\n>     + \n>     ++\tif (test_hooks_ok == -1)\n>     ++\t\ttest_hooks_ok = git_env_bool(\"GIT_TEST_FAKE_HOOKS\", 0);\n>     ++\n>     ++\tif (test_hooks_ok &&\n>     ++\t    (!strcmp(name, \"test-hook\") ||\n>     ++\t     !strcmp(name, \"does-not-exist\")))\n>     ++\t\treturn 1;\n>     ++\n\nAs I mentioned above, this still seems too restrictive to me, but I\ndon't need to rehash it here.\n\n>     @@ hook.c (new)\n>      +\tstruct hook_cb_data *hook_cb = pp_cb;\n>      +\tstruct hook *run_me = hook_cb->run_me;\n>      +\n>     -+\tif (!run_me)\n>     -+\t\tBUG(\"did we not return 1 in notify_hook_finished?\");\n>     -+\n\nI think the logic here is not quite correct, and I patched it in my\nseries on top when I enabled multiple hooks.\n\nMy understanding of the contract with run_processes_parallel() is this:\n - When you're done assigning tasks, return '0' from pick_next_hook()\n - If you want to stop all task processing because of one task's exit\n   state, return '1' from notify_hook_finished()\n\nBut you instead are unconditionally returning '1' from\n'notify_hook_finished()', and not handling \"no more tasks to assign\" in\npick_next_hook(). I think it is more correct to return 0 from\n'notify_hook_finished()' - \"this hook ran correctly, or if it ran\npoorly, at least we don't want to prevent anybody else from running, as\nthat's not our problem\". And then in your pick_next_hook(), since you\nknow you only want to run one hook and you aren't using a list yet, at\nthe end after you prepare the child_process, you can set hook_cb->run_me\n= NULL - \"there is no next hook to pick\" - and then use \"if (!run_me)\"\nto return 0 to tell run_processes_parallel() that there are no more\ntasks to assign.\n\nI  mention it because without noticing your changed flow in this patch,\nI wrote a fairly pernicious bug in my rebase on top. By leaving\n'notify_hook_finished()' to return 1, but enabling parallelism,\nmultihooks *appeared* to work - because all the tasks were assigned and\nstarted when running in parallel, even though the first one to finish\nsaid \"wait don't start any more tasks please\". But when running multiple\nhooks with j=1, only the first hook would run, even though the list was\npopulated correctly - and stepping through pick_next_hook() didn't\nindicate why.\n\nI *think* you did it this way to avoid calling pick_next_hook() again with\nnothing to iterate over, but since run_processes_parallel() is written\nfor lists of tasks, I don't think that kludge is the \"correct\" approach.\n\n>     +@@ hook.h: const char *find_hook(const char *name);\n>     -+\t/* Number of threads to parallelize across */\n>     ++\t/*\n>     ++\t * Number of threads to parallelize across, currently a stub,\n>     ++\t * we use the parallel API for future-proofing, but we always\n>     ++\t * have one hook of a given name, so this is always an\n>     ++\t * implicit 1 for now.\n>     ++\t */\n>      +\tint jobs;\n\nMakes me wonder whether you really even want to leave it in here. I\nguess it makes your call to run_processes_parallel() cleaner. But it'd\nalso be handy to swap '1' for 'options->jobs' in\nhttps://lore.kernel.org/git/20210715232603.3415111-3-emilyshaffer@google.com\ninstead, if you'd prefer.\n\n>      +};\n>      +\n>     @@ hook.h (new)\n>      +\t.args = STRVEC_INIT, \\\n>      +}\n>      +\n>     -+/*\n>     -+ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n>     -+ */\n\nWhy delete the comment? What was it hurting? ;)\n\n>     @@ t/t1800-hook.sh (new)\n>     ++test_expect_success 'git hook run: nonexistent hook' '\n\nAh, thanks for '%s/--/:/'. I know it was a nit, but that was bugging me :)\n\n>      +test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n>      +\tmkdir my-hooks &&\n>     -+\twrite_script my-hooks/test-hook <<-EOF &&\n>     -+\techo Hook ran >>actual\n>     ++\twrite_script my-hooks/test-hook <<-\\EOF &&\n>     ++\techo Hook ran $1 >>actual\n>      +\tEOF\n>      +\n>      +\tcat >expect <<-\\EOF &&\n>      +\tTest hook\n>     -+\tHook ran\n>     -+\tHook ran\n>     -+\tHook ran\n>     -+\tHook ran\n>     ++\tHook ran one\n>     ++\tHook ran two\n>     ++\tHook ran three\n>     ++\tHook ran four\n>      +\tEOF\n>      +\n>      +\t# Test various ways of specifying the path. See also\n>      +\t# t1350-config-hooks-path.sh\n>      +\t>actual &&\n>     -+\tgit hook run test-hook 2>>actual &&\n>     -+\tgit -c core.hooksPath=my-hooks hook run test-hook 2>>actual &&\n>     -+\tgit -c core.hooksPath=my-hooks/ hook run test-hook 2>>actual &&\n>     -+\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook 2>>actual &&\n>     -+\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook 2>>actual &&\n>     ++\tgit hook run test-hook -- ignored 2>>actual &&\n>     ++\tgit -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&\n>     ++\tgit -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&\n>     ++\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook -- three 2>>actual &&\n>     ++\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook -- four 2>>actual &&\n\nThanks - this will be much easier to debug when someone (me, probably)\nbreaks one of these by accident ;)\n\n>      +\ttest_cmp expect actual\n>      +'\n>      +\n>     -+test_expect_success 'set up a pre-commit hook in core.hooksPath' '\n>     -+\t>actual &&\n>     -+\tmkdir -p .git/custom-hooks .git/hooks &&\n>     -+\twrite_script .git/custom-hooks/pre-commit <<-\\EOF &&\n>     -+\techo CUSTOM >>actual\n>     -+\tEOF\n>     -+\twrite_script .git/hooks/pre-commit <<-\\EOF\n>     -+\techo NORMAL >>actual\n>     -+\tEOF\n>     -+'\n\nHm, is this removed because we feel like the test-hook test above gives\nus enough coverage? Ok.\n\n>  9:  8d8b2d2645 !  7:  12347d901b git hook run: add an --ignore-missing flag\n>       ## t/t1800-hook.sh ##\n>     +-test_expect_success 'git hook run: basic' '\n>     ++test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '\n>      +\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n>      +\ttest_must_be_empty stderr.actual\n>      +'\n>      +\n>     - test_expect_success 'git hook run -- basic' '\n>     ++test_expect_success 'git hook run -- basic' '\n\nAh, I see the range-diff caught s/--/:/ earlier but that step was missed\nin patch 9, which made this bit of the diff a little confused.\n\n> 11:  aa970a8175 !  9:  246a82b55b git-p4: use 'git hook' to run hooks\n>     @@ Commit message\n>          Python, we can directly call 'git hook run'. We emulate the existence\n>          check with the --ignore-missing flag.\n>      \n>     +    As this is the last hook execution in git.git to not go through \"git\n>     +    hook run\" or the hook.[ch] library we can now be absolutely sure that\n>     +    our assertion in hook.c that only hooks known by the generated (from\n>     +    githooks(5)) hook-list.h are permitted.\n>     +\n\n\\o/\n\n> 17:  c4f60db606 ! 15:  b7c0ee9719 hook: support passing stdin to hooks\n>       ## builtin/hook.c ##\n>       static const char * const builtin_hook_usage[] = {\n>     + \tN_(\"git hook <command> [...]\"),\n>      -\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>     ++\tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n>     + \tNULL\n>     + };\n>     + \n>     + static const char * const builtin_hook_run_usage[] = {\n>     + \tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>      +\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n\nNice, and here the strings in builtin_hook_usage and\nbuiltin_hook_run_usage start to differ, which makes my earlier concern\nentirely moot. Thanks.\n\n--\n\nAside from the questions about a) this test hook envvar and b) specific\nmechanics of terminating run_processes_parallel(), this series looks\ngood to me.\n\n - Emily\n"},{"id":"431842","messageId":"patch-v4-01.36-81fe1ed90d5-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 01/36] Makefile: mark \"check\" target as .PHONY","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:27Z","receivedAt":"2021-08-03T19:39:15Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a bug in 44c9e8594e (Fix up header file dependencies and add\nsparse checking rules, 2005-07-03), we never marked the phony \"check\"\ntarget as such.\n\nPerhaps we should just remove it, since as of a combination of\n912f9980d2 (Makefile: help people who run 'make check' by mistake,\n2008-11-11) 0bcd9ae85d (sparse: Fix errors due to missing\ntarget-specific variables, 2011-04-21) we've been suggesting the user\nrun \"make sparse\" directly.\n\nBut under that mode it still does something, as well as directing the\nuser to run \"make test\" under non-sparse. So let's punt that and\nnarrowly fix the PHONY bug.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/Makefile b/Makefile\nindex c6f6246bf63..2ff038069e8 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2931,6 +2931,7 @@ hdr-check: $(HCO)\n style:\n \tgit clang-format --style file --diff --extensions c,h\n \n+.PHONY: check\n check: config-list.h command-list.h\n \t@if sparse; \\\n \tthen \\\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431843","messageId":"patch-v4-02.36-0f749530777-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 02/36] Makefile: stop hardcoding {command,config}-list.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:28Z","receivedAt":"2021-08-03T19:39:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Change various places that hardcode the names of these two files to\nrefer to either $(GENERATED_H), or to a new generated-hdrs\ntarget. That target is consistent with the *-objs targets I recently\nadded in 029bac01a8 (Makefile: add {program,xdiff,test,git,fuzz}-objs\n& objects targets, 2021-02-23).\n\nA subsequent commit will add a new generated hook-list.h. By doing\nthis refactoring we'll only need to add the new file to the\nGENERATED_H variable, not EXCEPT_HDRS, the vcbuild/README etc.\n\nI have not tested the Windows-specific change in config.mak.uname\nbeing made here, but we use other variables from the Makefile in the\nsame block, and the GENERATED_H is fully defined before we include\nconfig.mak.uname.\n\nHardcoding command-list.h there seems to have been a case of\ncopy/paste programming in 976aaedca0 (msvc: add a Makefile target to\npre-generate the Visual Studio solution, 2019-07-29). The\nconfig-list.h was added later in 709df95b78 (help: move\nlist_config_help to builtin/help, 2020-04-16).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile              | 6 ++++--\n compat/vcbuild/README | 2 +-\n config.mak.uname      | 6 +++---\n 3 files changed, 8 insertions(+), 6 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 2ff038069e8..89bf0dd7332 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -823,6 +823,8 @@ XDIFF_LIB = xdiff/lib.a\n \n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n+.PHONY: generated-hdrs\n+generated-hdrs: $(GENERATED_H)\n \n LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentation/' 2>/dev/null || \\\n \t$(FIND) . \\\n@@ -2909,7 +2911,7 @@ $(SP_OBJ): %.sp: %.c GIT-CFLAGS FORCE\n .PHONY: sparse $(SP_OBJ)\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := command-list.h config-list.h unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n ifndef GCRYPT_SHA256\n \tEXCEPT_HDRS += sha256/gcrypt.h\n endif\n@@ -2932,7 +2934,7 @@ style:\n \tgit clang-format --style file --diff --extensions c,h\n \n .PHONY: check\n-check: config-list.h command-list.h\n+check: $(GENERATED_H)\n \t@if sparse; \\\n \tthen \\\n \t\techo >&2 \"Use 'make sparse' instead\"; \\\ndiff --git a/compat/vcbuild/README b/compat/vcbuild/README\nindex 51fb083dbbe..29ec1d0f104 100644\n--- a/compat/vcbuild/README\n+++ b/compat/vcbuild/README\n@@ -92,7 +92,7 @@ The Steps of Build Git with VS2008\n    the git operations.\n \n 3. Inside Git's directory run the command:\n-       make command-list.h config-list.h\n+       make generated-hdrs\n    to generate the header file needed to compile git.\n \n 4. Then either build Git with the GNU Make Makefile in the Git projects\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 69413fb3dc0..9988378160b 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -732,9 +732,9 @@ vcxproj:\n \t echo '</Project>') >git-remote-http/LinkOrCopyRemoteHttp.targets\n \tgit add -f git/LinkOrCopyBuiltins.targets git-remote-http/LinkOrCopyRemoteHttp.targets\n \n-\t# Add command-list.h and config-list.h\n-\t$(MAKE) MSVC=1 SKIP_VCPKG=1 prefix=/mingw64 config-list.h command-list.h\n-\tgit add -f config-list.h command-list.h\n+\t# Add generated headers\n+\t$(MAKE) MSVC=1 SKIP_VCPKG=1 prefix=/mingw64 $(GENERATED_H)\n+\tgit add -f $(GENERATED_H)\n \n \t# Add scripts\n \trm -f perl/perl.mak\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431844","messageId":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-00.27-0000000000-20210617T101216Z-avarab@gmail.com","subject":"[PATCH v4 00/36] Run hooks via \"git run hook\" & hook library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:26Z","receivedAt":"2021-08-03T19:39:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This is a v4 re-roll of the \"Base for \"config-based-hooks\" topic.\n\nIt's grown from 27 to 36 patches mainly because I re-folded the two\ntopics it depended on into it, i.e.:\n\n    https://lore.kernel.org/git/cover-0.3-0000000000-20210629T190137Z-avarab@gmail.com/\n    https://lore.kernel.org/git/cover-0.3-0000000000-20210629T183325Z-avarab@gmail.com/\n\nI think those have long since reached a level of stability suitable\nfor being merged down, but since Junio didn't pick them up\nseparately[1][2] there wasn't much point in keeping them split-up.\n\nThis re-roll mostly addresses Emily's comments on the v3:\nhttps://lore.kernel.org/git/YQHAasrmcbdiCDQF@google.com/\n\nI.e.:\n\n * I did not change the part where I die on hooks that aren't known to\n   git itself. I think it makes sense to keep this topic purely for\n   bug-for-bug compatibility with existing behavior, and to flip the\n   RUN_SETUP_GENTLY flag in her follow-up feature topic.\n\n * The (ab)use of the run_processes_parallel() API should be gone. I\n   just misunderstood how it worked.\n\n * The v3 would segfault on a plain \"git hook run\". I've changed\n   builtin/hook.c to use the same pattern for subcommands as\n   builtin/commit-graph.c et al. This also makes the usage output more\n   consistent.\n\n * The stub \"jobs\" member of the struct is now gone, and it's\n   hardcoded to 1. Emily's feature topic can add it back (per her\n   suggestion).\n\nOther updates:\n\n * In the base topic the s/Signed-off-by/Reviewed-by/g from René\n   change that Junio applied locally has been folded in.\n\n * Almost all the callers were just \"one-shot\" callers, I introduced a\n   new run_hooks_oneshot() function for those, which gets rid of the\n   verbosity around memory management, see e.g. the \"builtin/gc.c\" in\n   the range-diff below. That run_hooks_oneshot() can also take a NULL\n   set of options.\n\n * We'd leak memory from \"my_hook.feed_pipe_cb_data\" in hook.c, we now\n   free() it.\n\n * Some s/STRING_LIST_INIT_DUP/STRING_LIST_INIT_NODUP &\n   strbuf_detach()/ changes in transport.c and reference-transaction\n   to avoid needless verbosity around memory management.\n\n * A new 'hook.c users: use \"hook_exists()\" insted of \"find_hook()\"'\n   patch in what was previously one of the base topics, makes\n   subsequent changes smaller.\n\n1. https://lore.kernel.org/git/87sg00qfbp.fsf@evledraar.gmail.com/\n2. https://lore.kernel.org/git/87a6mevkrx.fsf@evledraar.gmail.com/\n\nEmily Shaffer (26):\n  hook.c: add a hook_exists() wrapper and use it in bugreport.c\n  hook: add 'run' subcommand\n  gc: use hook library for pre-auto-gc hook\n  rebase: convert pre-rebase to use hook.h\n  am: convert applypatch to use hook.h\n  hooks: convert 'post-checkout' hook to hook library\n  merge: convert post-merge to use hook.h\n  send-email: use 'git hook run' for 'sendemail-validate'\n  git-p4: use 'git hook' to run hooks\n  commit: convert {pre-commit,prepare-commit-msg} hook to hook.h\n  read-cache: convert post-index-change to use hook.h\n  receive-pack: convert push-to-checkout hook to hook.h\n  run-command: remove old run_hook_{le,ve}() hook API\n  run-command: allow stdin for run_processes_parallel\n  hook: support passing stdin to hooks\n  am: convert 'post-rewrite' hook to hook.h\n  run-command: add stdin callback for parallelization\n  hook: provide stdin by string_list or callback\n  hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n  transport: convert pre-push hook to hook.h\n  reference-transaction: use hook.h to run hooks\n  run-command: allow capturing of collated output\n  hooks: allow callers to capture output\n  receive-pack: convert 'update' hook to hook.h\n  post-update: use hook.h library\n  receive-pack: convert receive hooks to hook.h\n\nÆvar Arnfjörð Bjarmason (10):\n  Makefile: mark \"check\" target as .PHONY\n  Makefile: stop hardcoding {command,config}-list.h\n  Makefile: remove an out-of-date comment\n  hook.[ch]: move find_hook() to this new library\n  hook.c users: use \"hook_exists()\" insted of \"find_hook()\"\n  hook-list.h: add a generated list of hooks, like config-list.h\n  git hook run: add an --ignore-missing flag\n  hook tests: test for exact \"pre-push\" hook input\n  hook tests: use a modern style for \"pre-push\" tests\n  hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n\n .gitignore                          |   2 +\n Documentation/git-hook.txt          |  51 +++++\n Documentation/githooks.txt          |   4 +\n Makefile                            |  26 ++-\n builtin.h                           |   1 +\n builtin/am.c                        |  29 +--\n builtin/bugreport.c                 |  46 +----\n builtin/checkout.c                  |  14 +-\n builtin/clone.c                     |   6 +-\n builtin/commit.c                    |  19 +-\n builtin/fetch.c                     |   1 +\n builtin/gc.c                        |   3 +-\n builtin/hook.c                      |  92 +++++++++\n builtin/merge.c                     |  21 +-\n builtin/rebase.c                    |   6 +-\n builtin/receive-pack.c              | 285 +++++++++++++---------------\n builtin/submodule--helper.c         |   2 +-\n builtin/worktree.c                  |  29 ++-\n command-list.txt                    |   1 +\n commit.c                            |  16 +-\n commit.h                            |   3 +-\n compat/vcbuild/README               |   2 +-\n config.mak.uname                    |   6 +-\n contrib/buildsystems/CMakeLists.txt |   7 +\n generate-hooklist.sh                |  18 ++\n git-p4.py                           |  72 +------\n git-send-email.perl                 |  20 +-\n git.c                               |   1 +\n hook.c                              | 246 ++++++++++++++++++++++++\n hook.h                              | 126 ++++++++++++\n read-cache.c                        |  11 +-\n refs.c                              |  41 ++--\n reset.c                             |  14 +-\n run-command.c                       | 157 +++++++--------\n run-command.h                       |  55 +++---\n sequencer.c                         |  86 ++++-----\n submodule.c                         |   1 +\n t/helper/test-run-command.c         |  46 ++++-\n t/t0061-run-command.sh              |  37 ++++\n t/t1800-hook.sh                     | 156 +++++++++++++++\n t/t5571-pre-push-hook.sh            |  94 +++++----\n t/t9001-send-email.sh               |   4 +-\n transport.c                         |  57 ++----\n 43 files changed, 1303 insertions(+), 611 deletions(-)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100755 generate-hooklist.sh\n create mode 100644 hook.c\n create mode 100644 hook.h\n create mode 100755 t/t1800-hook.sh\n\nRange-diff against v3:\n 1:  27c94247f87 =  1:  81fe1ed90d5 Makefile: mark \"check\" target as .PHONY\n 2:  6e164edb0b0 !  2:  0f749530777 Makefile: stop hardcoding {command,config}-list.h\n    @@ Commit message\n         added in 029bac01a8 (Makefile: add {program,xdiff,test,git,fuzz}-objs\n         & objects targets, 2021-02-23).\n     \n    +    A subsequent commit will add a new generated hook-list.h. By doing\n    +    this refactoring we'll only need to add the new file to the\n    +    GENERATED_H variable, not EXCEPT_HDRS, the vcbuild/README etc.\n    +\n         I have not tested the Windows-specific change in config.mak.uname\n         being made here, but we use other variables from the Makefile in the\n         same block, and the GENERATED_H is fully defined before we include\n         config.mak.uname.\n     \n         Hardcoding command-list.h there seems to have been a case of\n    -    copy/paste programming in dce7d29551 (msvc: support building Git using\n    -    MS Visual C++, 2019-06-25). The config-list.h was added later in\n    -    709df95b78 (help: move list_config_help to builtin/help, 2020-04-16).\n    +    copy/paste programming in 976aaedca0 (msvc: add a Makefile target to\n    +    pre-generate the Visual Studio solution, 2019-07-29). The\n    +    config-list.h was added later in 709df95b78 (help: move\n    +    list_config_help to builtin/help, 2020-04-16).\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n 3:  ddae86802e2 !  3:  644b31fe281 Makefile: remove an out-of-date comment\n    @@ Commit message\n         The rest of it was also somewhere between inaccurate and outdated,\n         since as of b8ba629264 (Makefile: fold MISC_H into LIB_H, 2012-06-20)\n         it's not followed by a list of header files, that got moved earlier in\n    -    the file into LIB_H in b8ba629264 (Makefile: fold MISC_H into LIB_H,\n    -    2012-06-20).\n    +    the file into LIB_H in 60d24dd255 (Makefile: fold XDIFF_H and VCSSVN_H\n    +    into LIB_H, 2012-07-06).\n     \n         Let's just remove it entirely, to the extent that we have anything\n         useful to say here the comment on the\n    @@ Makefile: ifneq ($(dep_files_present),)\n     -# Dependencies on automatically generated headers such as command-list.h\n     -# should _not_ be included here, since they are necessary even when\n     -# building an object for the first time.\n    - \n    +-\n      $(OBJECTS): $(LIB_H) $(GENERATED_H)\n      endif\n    + \n 4:  58c37e4f06e =  4:  89c4d44b0c3 hook.[ch]: move find_hook() to this new library\n 5:  0cf7e078ef4 =  5:  3514e0c0251 hook.c: add a hook_exists() wrapper and use it in bugreport.c\n -:  ----------- >  6:  d5ef40f77dc hook.c users: use \"hook_exists()\" insted of \"find_hook()\"\n 6:  f343fc7ae66 !  7:  4cfd72722c1 hook-list.h: add a generated list of hooks, like config-list.h\n    @@ Commit message\n          - 976aaedca0 (msvc: add a Makefile target to pre-generate the Visual\n            Studio solution, 2019-07-29)\n     \n    +    The LC_ALL=C is needed because at least in my locale the dash (\"-\") is\n    +    ignored for the purposes of sorting, which results in a different\n    +    order. I'm not aware of anything in git that has a hard dependency on\n    +    the order, but e.g. the bugreport output would end up using whatever\n    +    locale was in effect when git was compiled.\n    +\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n    +    Helped-by: René Scharfe <l.s.r@web.de>\n     \n      ## .gitignore ##\n     @@\n    @@ Makefile: command-list.h: $(wildcard Documentation/git*.txt)\n      \t\t$(patsubst %,--exclude-program %,$(EXCLUDED_PROGRAMS)) \\\n      \t\tcommand-list.txt >$@+ && mv $@+ $@\n      \n    -+hook-list.h: generate-hooklist.sh\n    -+hook-list.h: Documentation/githooks.txt\n    ++hook-list.h: generate-hooklist.sh Documentation/githooks.txt\n     +\t$(QUIET_GEN)$(SHELL_PATH) ./generate-hooklist.sh \\\n     +\t\t>$@+ && mv $@+ $@\n     +\n    @@ contrib/buildsystems/CMakeLists.txt: if(NOT EXISTS ${CMAKE_BINARY_DIR}/config-li\n      ## generate-hooklist.sh (new) ##\n     @@\n     +#!/bin/sh\n    ++#\n    ++# Usage: ./generate-hooklist.sh >hook-list.h\n     +\n    -+echo \"/* Automatically generated by generate-hooklist.sh */\"\n    ++cat <<EOF\n    ++/* Automatically generated by generate-hooklist.sh */\n     +\n    -+print_hook_list () {\n    -+\tcat <<EOF\n     +static const char *hook_name_list[] = {\n     +EOF\n    -+\tperl -ne '\n    -+\t\tchomp;\n    -+\t\t@l[$.] = $_;\n    -+\t\tpush @h => $l[$. - 1] if /^~~~+$/s;\n    -+\t\tEND {\n    -+\t\t\tprint qq[\\t\"$_\",\\n] for sort @h;\n    -+\t\t}\n    -+\t' <Documentation/githooks.txt\n    -+\tcat <<EOF\n    ++\n    ++sed -n -e '/^~~~~*$/ {x; s/^.*$/\t\"&\",/; p;}; x' \\\n    ++\t<Documentation/githooks.txt |\n    ++\tLC_ALL=C sort\n    ++\n    ++cat <<EOF\n     +\tNULL,\n     +};\n     +EOF\n    -+}\n    -+\n    -+echo\n    -+print_hook_list\n     \n      ## hook.c ##\n     @@\n 7:  cf4b06bfdf8 !  8:  7cb4a4cb69e hook: add 'run' subcommand\n    @@ Commit message\n         let's start with the bare minimum required to support our simplest\n         hooks.\n     \n    +    In terms of implementation the usage_with_options() and \"goto usage\"\n    +    pattern here mirrors that of\n    +    builtin/{commit-graph,multi-pack-index}.c.\n    +\n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n    @@ builtin/hook.c (new)\n     +#include \"strbuf.h\"\n     +#include \"strvec.h\"\n     +\n    ++#define BUILTIN_HOOK_RUN_USAGE \\\n    ++\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n    ++\n     +static const char * const builtin_hook_usage[] = {\n    -+\tN_(\"git hook <command> [...]\"),\n    -+\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n    ++\tBUILTIN_HOOK_RUN_USAGE,\n     +\tNULL\n     +};\n     +\n     +static const char * const builtin_hook_run_usage[] = {\n    -+\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n    ++\tBUILTIN_HOOK_RUN_USAGE,\n     +\tNULL\n     +};\n     +\n    @@ builtin/hook.c (new)\n     +{\n     +\tint i;\n     +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tint rc = 0;\n     +\tconst char *hook_name;\n     +\tconst char *hook_path;\n    -+\n     +\tstruct option run_options[] = {\n     +\t\tOPT_END(),\n     +\t};\n    ++\tint ret;\n     +\n     +\targc = parse_options(argc, argv, prefix, run_options,\n     +\t\t\t     builtin_hook_run_usage,\n    -+\t\t\t     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n    -+\n    -+\tif (argc > 1) {\n    -+\t\tif (strcmp(argv[1], \"--\") &&\n    -+\t\t    strcmp(argv[1], \"--end-of-options\"))\n    -+\t\t\t/* Having a -- for \"run\" is mandatory */\n    -+\t\t\tusage_with_options(builtin_hook_usage, run_options);\n    -+\t\t/* Add our arguments, start after -- */\n    -+\t\tfor (i = 2 ; i < argc; i++)\n    -+\t\t\tstrvec_push(&opt.args, argv[i]);\n    -+\t}\n    ++\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n     +\n    -+\t/* Need to take into account core.hooksPath */\n    -+\tgit_config(git_default_config, NULL);\n    ++\tif (!argc)\n    ++\t\tgoto usage;\n     +\n     +\t/*\n    -+\t * We are not using run_hooks() because we'd like to detect\n    -+\t * missing hooks. Let's find it ourselves and call\n    -+\t * run_found_hooks() instead.\n    ++\t * Having a -- for \"run\" when providing <hook-args> is\n    ++\t * mandatory.\n     +\t */\n    ++\tif (argc > 1 && strcmp(argv[1], \"--\") &&\n    ++\t    strcmp(argv[1], \"--end-of-options\"))\n    ++\t\tgoto usage;\n    ++\n    ++\t/* Add our arguments, start after -- */\n    ++\tfor (i = 2 ; i < argc; i++)\n    ++\t\tstrvec_push(&opt.args, argv[i]);\n    ++\n    ++\t/* Need to take into account core.hooksPath */\n    ++\tgit_config(git_default_config, NULL);\n    ++\n     +\thook_name = argv[0];\n     +\thook_path = find_hook(hook_name);\n     +\tif (!hook_path) {\n     +\t\terror(\"cannot find a hook named %s\", hook_name);\n     +\t\treturn 1;\n     +\t}\n    -+\trc = run_found_hooks(hook_name, hook_path, &opt);\n     +\n    ++\tret = run_hooks(hook_name, hook_path, &opt);\n     +\trun_hooks_opt_clear(&opt);\n    -+\n    -+\treturn rc;\n    ++\treturn ret;\n    ++usage:\n    ++\tusage_with_options(builtin_hook_run_usage, run_options);\n     +}\n     +\n     +int cmd_hook(int argc, const char **argv, const char *prefix)\n    @@ builtin/hook.c (new)\n     +\tstruct option builtin_hook_options[] = {\n     +\t\tOPT_END(),\n     +\t};\n    ++\n     +\targc = parse_options(argc, argv, NULL, builtin_hook_options,\n     +\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n     +\tif (!argc)\n    -+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n    ++\t\tgoto usage;\n     +\n     +\tif (!strcmp(argv[0], \"run\"))\n     +\t\treturn run(argc, argv, prefix);\n    -+\telse\n    -+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n    ++\n    ++usage:\n    ++\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n     +}\n     \n      ## command-list.txt ##\n    @@ hook.c: int hook_exists(const char *name)\n     +\tstruct hook_cb_data *hook_cb = pp_cb;\n     +\tstruct hook *run_me = hook_cb->run_me;\n     +\n    ++\tif (!run_me)\n    ++\t\treturn 0;\n    ++\n     +\tcp->no_stdin = 1;\n     +\tcp->env = hook_cb->options->env.v;\n     +\tcp->stdout_to_stderr = 1;\n    @@ hook.c: int hook_exists(const char *name)\n     +\t/* Provide context for errors if necessary */\n     +\t*pp_task_cb = run_me;\n     +\n    ++\t/*\n    ++\t * This pick_next_hook() will be called again, we're only\n    ++\t * running one hook, so indicate that no more work will be\n    ++\t * done.\n    ++\t */\n    ++\thook_cb->run_me = NULL;\n    ++\n     +\treturn 1;\n     +}\n     +\n    @@ hook.c: int hook_exists(const char *name)\n     +\n     +\thook_cb->rc |= result;\n     +\n    -+\treturn 1;\n    ++\treturn 0;\n     +}\n     +\n    -+int run_found_hooks(const char *hook_name, const char *hook_path,\n    -+\t\t    struct run_hooks_opt *options)\n    ++int run_hooks(const char *hook_name, const char *hook_path,\n    ++\t      struct run_hooks_opt *options)\n     +{\n     +\tstruct hook my_hook = {\n     +\t\t.hook_path = hook_path,\n    @@ hook.c: int hook_exists(const char *name)\n     +\t\t.hook_name = hook_name,\n     +\t\t.options = options,\n     +\t};\n    -+\tcb_data.run_me = &my_hook;\n    ++\tint jobs = 1;\n    ++\n    ++\tif (!options)\n    ++\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n     +\n    -+\tif (options->jobs != 1)\n    -+\t\tBUG(\"we do not handle %d or any other != 1 job number yet\", options->jobs);\n    ++\tcb_data.run_me = &my_hook;\n     +\n    -+\trun_processes_parallel_tr2(options->jobs,\n    ++\trun_processes_parallel_tr2(jobs,\n     +\t\t\t\t   pick_next_hook,\n     +\t\t\t\t   notify_start_failure,\n     +\t\t\t\t   notify_hook_finished,\n    @@ hook.c: int hook_exists(const char *name)\n     +\t\t\t\t   hook_name);\n     +\n     +\treturn cb_data.rc;\n    -+}\n    -+\n    -+int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n    -+{\n    -+\tconst char *hook_path;\n    -+\tint ret;\n    -+\tif (!options)\n    -+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n    -+\n    -+\thook_path = find_hook(hook_name);\n    -+\n    -+\t/*\n    -+\t * If you need to act on a missing hook, use run_found_hooks()\n    -+\t * instead\n    -+\t */\n    -+\tif (!hook_path)\n    -+\t\treturn 0;\n    -+\n    -+\tret = run_found_hooks(hook_name, hook_path, options);\n    -+\treturn ret;\n     +}\n     \n      ## hook.h ##\n    @@ hook.h: const char *find_hook(const char *name);\n     +\n     +\t/* Args to be passed to each hook */\n     +\tstruct strvec args;\n    -+\n    -+\t/*\n    -+\t * Number of threads to parallelize across, currently a stub,\n    -+\t * we use the parallel API for future-proofing, but we always\n    -+\t * have one hook of a given name, so this is always an\n    -+\t * implicit 1 for now.\n    -+\t */\n    -+\tint jobs;\n     +};\n     +\n     +#define RUN_HOOKS_OPT_INIT { \\\n    -+\t.jobs = 1, \\\n     +\t.env = STRVEC_INIT, \\\n     +\t.args = STRVEC_INIT, \\\n     +}\n     +\n    ++/*\n    ++ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n    ++ */\n     +struct hook_cb_data {\n     +\t/* rc reflects the cumulative failure state */\n     +\tint rc;\n    @@ hook.h: const char *find_hook(const char *name);\n     +\n     +void run_hooks_opt_clear(struct run_hooks_opt *o);\n     +\n    -+/*\n    -+ * Calls find_hook(hookname) and runs the hooks (if any) with\n    -+ * run_found_hooks().\n    -+ */\n    -+int run_hooks(const char *hook_name, struct run_hooks_opt *options);\n    -+\n    -+/*\n    -+ * Takes an already resolved hook and runs it. Internally the simpler\n    -+ * run_hooks() will call this.\n    ++/**\n    ++ * Takes an already resolved hook found via find_hook() and runs\n    ++ * it. Does not call run_hooks_opt_clear() for you.\n     + */\n    -+int run_found_hooks(const char *hookname, const char *hook_path,\n    -+\t\t    struct run_hooks_opt *options);\n    ++int run_hooks(const char *hookname, const char *hook_path,\n    ++\t      struct run_hooks_opt *options);\n      #endif\n     \n      ## t/t1800-hook.sh (new) ##\n    @@ t/t1800-hook.sh (new)\n     +\n     +test_expect_success 'git hook usage' '\n     +\ttest_expect_code 129 git hook &&\n    -+\ttest_expect_code 129 git hook -h &&\n    -+\ttest_expect_code 129 git hook run -h\n    ++\ttest_expect_code 129 git hook run &&\n    ++\ttest_expect_code 129 git hook run -h &&\n    ++\ttest_expect_code 129 git hook run --unknown 2>err &&\n    ++\tgrep \"unknown option\" err\n     +'\n     +\n     +test_expect_success 'setup GIT_TEST_FAKE_HOOKS=true to permit \"test-hook\" and \"does-not-exist\" names\"' '\n 8:  7209f73f281 !  9:  2b8500aa675 gc: use hook library for pre-auto-gc hook\n    @@ Metadata\n      ## Commit message ##\n         gc: use hook library for pre-auto-gc hook\n     \n    -    Using the hook.h library instead of the run-command.h library to run\n    -    pre-auto-gc means that those hooks can be set up in config files, as\n    -    well as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n    +    Move the pre-auto-gc hook away from run-command.h to and over to the\n    +    new hook.h library.\n    +\n    +    To do this introduce a simple run_hooks_oneshot() wrapper, we'll be\n    +    using it extensively for these simple cases of wanting to run a single\n    +    hook under a given name, and having it free the memory we allocate for\n    +    us.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n    @@ builtin/gc.c\n      \n      #define FAILED_RUN \"failed to run %s\"\n      \n    -@@ builtin/gc.c: static void add_repack_incremental_option(void)\n    - \n    - static int need_to_gc(void)\n    - {\n    -+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n    -+\n    - \t/*\n    - \t * Setting gc.auto to 0 or negative can disable the\n    - \t * automatic gc.\n     @@ builtin/gc.c: static int need_to_gc(void)\n      \telse\n      \t\treturn 0;\n      \n     -\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n    -+\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n    -+\t\trun_hooks_opt_clear(&hook_opt);\n    ++\tif (run_hooks_oneshot(\"pre-auto-gc\", NULL))\n      \t\treturn 0;\n    -+\t}\n    -+\trun_hooks_opt_clear(&hook_opt);\n      \treturn 1;\n      }\n    +\n    + ## hook.c ##\n    +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n      \n    + \treturn cb_data.rc;\n    + }\n    ++\n    ++int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n    ++{\n    ++\tconst char *hook_path;\n    ++\tint ret;\n    ++\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n    ++\n    ++\tif (!options)\n    ++\t\toptions = &hook_opt_scratch;\n    ++\n    ++\thook_path = find_hook(hook_name);\n    ++\tif (!hook_path) {\n    ++\t\tret = 0;\n    ++\t\tgoto cleanup;\n    ++\t}\n    ++\n    ++\tret = run_hooks(hook_name, hook_path, options);\n    ++cleanup:\n    ++\trun_hooks_opt_clear(options);\n    ++\treturn ret;\n    ++}\n    +\n    + ## hook.h ##\n    +@@ hook.h: void run_hooks_opt_clear(struct run_hooks_opt *o);\n    + /**\n    +  * Takes an already resolved hook found via find_hook() and runs\n    +  * it. Does not call run_hooks_opt_clear() for you.\n    ++ *\n    ++ * See run_hooks_oneshot() for the simpler one-shot API.\n    +  */\n    + int run_hooks(const char *hookname, const char *hook_path,\n    + \t      struct run_hooks_opt *options);\n    ++\n    ++/**\n    ++ * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n    ++ * with run_hooks().\n    ++ *\n    ++ * If \"options\" is provided calls run_hooks_opt_clear() on it for\n    ++ * you. If \"options\" is NULL a scratch one will be provided for you\n    ++ * before calling run_hooks().\n    ++ */\n    ++int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n    ++\n    + #endif\n 9:  e9a1e7cf61e ! 10:  3ee55d2c10f rebase: teach pre-rebase to use hook.h\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    rebase: teach pre-rebase to use hook.h\n    +    rebase: convert pre-rebase to use hook.h\n     \n         Move the pre-rebase hook away from run-command.h to and over to the\n         new hook.h library.\n    @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix\n      \tif (!ok_to_skip_pre_rebase &&\n     -\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n     -\t\t\targc ? argv[0] : NULL, NULL))\n    -+\t    run_hooks(\"pre-rebase\", &hook_opt)) {\n    -+\t\trun_hooks_opt_clear(&hook_opt);\n    ++\t    run_hooks_oneshot(\"pre-rebase\", &hook_opt))\n      \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n    -+\t}\n    -+\trun_hooks_opt_clear(&hook_opt);\n      \n      \tif (options.flags & REBASE_DIFFSTAT) {\n    - \t\tstruct diff_options opts;\n10:  1d087269303 ! 11:  050f20d14f0 am: convert applypatch hooks to use config\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    am: convert applypatch hooks to use config\n    +    am: convert applypatch to use hook.h\n     \n         Teach pre-applypatch, post-applypatch, and applypatch-msg to use the\n         hook.h library instead of the run-command.h library.\n    @@ builtin/am.c: static void am_destroy(const struct am_state *state)\n      \tassert(state->msg);\n     -\tret = run_hook_le(NULL, \"applypatch-msg\", am_path(state, \"final-commit\"), NULL);\n     +\tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n    -+\tret = run_hooks(\"applypatch-msg\", &opt);\n    -+\trun_hooks_opt_clear(&opt);\n    ++\tret = run_hooks_oneshot(\"applypatch-msg\", &opt);\n      \n      \tif (!ret) {\n      \t\tFREE_AND_NULL(state->msg);\n     @@ builtin/am.c: static void do_commit(const struct am_state *state)\n    - \tstruct commit_list *parents = NULL;\n      \tconst char *reflog_msg, *author, *committer = NULL;\n      \tstruct strbuf sb = STRBUF_INIT;\n    -+\tstruct run_hooks_opt hook_opt_pre = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt hook_opt_post = RUN_HOOKS_OPT_INIT;\n      \n     -\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n    -+\tif (run_hooks(\"pre-applypatch\", &hook_opt_pre)) {\n    -+\t\trun_hooks_opt_clear(&hook_opt_pre);\n    ++\tif (run_hooks_oneshot(\"pre-applypatch\", NULL))\n      \t\texit(1);\n    -+\t}\n      \n      \tif (write_cache_as_tree(&tree, 0, NULL))\n    - \t\tdie(_(\"git write-tree failed to write a tree\"));\n     @@ builtin/am.c: static void do_commit(const struct am_state *state)\n      \t\tfclose(fp);\n      \t}\n      \n     -\trun_hook_le(NULL, \"post-applypatch\", NULL);\n    -+\trun_hooks(\"post-applypatch\", &hook_opt_post);\n    ++\trun_hooks_oneshot(\"post-applypatch\", NULL);\n      \n    -+\trun_hooks_opt_clear(&hook_opt_pre);\n    -+\trun_hooks_opt_clear(&hook_opt_post);\n      \tstrbuf_release(&sb);\n      }\n    - \n11:  32eec5dc2f0 ! 12:  ac875d284da hooks: convert 'post-checkout' hook to hook library\n    @@ builtin/checkout.c: struct branch_info {\n     -\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n     -\t\t\t   changed ? \"1\" : \"0\", NULL);\n     +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tint rc;\n     +\n      \t/* \"new_commit\" can be NULL when checking out from the index before\n      \t   a commit exists. */\n    @@ builtin/checkout.c: struct branch_info {\n     +\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n     +\t\t     changed ? \"1\" : \"0\",\n     +\t\t     NULL);\n    -+\trc = run_hooks(\"post-checkout\", &opt);\n    -+\trun_hooks_opt_clear(&opt);\n    -+\treturn rc;\n    ++\treturn run_hooks_oneshot(\"post-checkout\", &opt);\n      }\n      \n      static int update_some(const struct object_id *oid, struct strbuf *base,\n    @@ builtin/clone.c: static int checkout(int submodule_progress)\n     -\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(null_oid()),\n     -\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n     +\tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n    -+\terr |= run_hooks(\"post-checkout\", &hook_opt);\n    -+\trun_hooks_opt_clear(&hook_opt);\n    ++\terr |= run_hooks_oneshot(\"post-checkout\", &hook_opt);\n      \n      \tif (!err && (option_recurse_submodules.nr > 0)) {\n      \t\tstruct strvec args = STRVEC_INIT;\n    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam\n     +\t\topt.dir = path;\n     +\t\topt.absolute_path = 1;\n     +\n    -+\t\tret = run_hooks(\"post-checkout\", &opt);\n    -+\n    -+\t\trun_hooks_opt_clear(&opt);\n    ++\t\tret = run_hooks_oneshot(\"post-checkout\", &opt);\n      \t}\n      \n      \tstrvec_clear(&child_env);\n    @@ hook.c: static int pick_next_hook(struct child_process *cp,\n      \t/* add command */\n      \tstrvec_push(&cp->args, run_me->hook_path);\n     @@ hook.c: static int notify_hook_finished(int result,\n    - int run_found_hooks(const char *hook_name, const char *hook_path,\n    - \t\t    struct run_hooks_opt *options)\n    + int run_hooks(const char *hook_name, const char *hook_path,\n    + \t      struct run_hooks_opt *options)\n      {\n     +\tstruct strbuf abs_path = STRBUF_INIT;\n      \tstruct hook my_hook = {\n      \t\t.hook_path = hook_path,\n      \t};\n    -@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    - \t\t.hook_name = hook_name,\n    - \t\t.options = options,\n    - \t};\n    +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n    + \tif (!options)\n    + \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n    + \n     +\tif (options->absolute_path) {\n     +\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n     +\t\tmy_hook.hook_path = abs_path.buf;\n     +\t}\n      \tcb_data.run_me = &my_hook;\n      \n    - \tif (options->jobs != 1)\n    -@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    - \t\t\t\t   &cb_data,\n    + \trun_processes_parallel_tr2(jobs,\n    +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n      \t\t\t\t   \"hook\",\n      \t\t\t\t   hook_name);\n    + \n     +\tif (options->absolute_path)\n     +\t\tstrbuf_release(&abs_path);\n    - \n    ++\n      \treturn cb_data.rc;\n      }\n    + \n     \n      ## hook.h ##\n     @@ hook.h: struct run_hooks_opt\n    - \t * implicit 1 for now.\n    - \t */\n    - \tint jobs;\n    + \n    + \t/* Args to be passed to each hook */\n    + \tstruct strvec args;\n     +\n     +\t/* Resolve and run the \"absolute_path(hook)\" instead of\n     +\t * \"hook\". Used for \"git worktree\" hooks\n    @@ hook.h: struct run_hooks_opt\n     +\n     +\t/* Path to initial working directory for subprocess */\n     +\tconst char *dir;\n    -+\n      };\n      \n      #define RUN_HOOKS_OPT_INIT { \\\n     \n      ## read-cache.c ##\n     @@\n    - #include \"progress.h\"\n      #include \"sparse-index.h\"\n      #include \"csum-file.h\"\n    + #include \"promisor-remote.h\"\n     +#include \"hook.h\"\n      \n      /* Mask for the name length in ce_flags in the on-disk index */\n    @@ reset.c: int reset_head(struct repository *r, struct object_id *oid, const char\n     +\t\t\t     oid_to_hex(oid),\n     +\t\t\t     \"1\",\n     +\t\t\t     NULL);\n    -+\t\trun_hooks(\"post-checkout\", &opt);\n    -+\t\trun_hooks_opt_clear(&opt);\n    ++\t\trun_hooks_oneshot(\"post-checkout\", &opt);\n     +\t}\n      \n      leave_reset_head:\n12:  e9fa3f67593 ! 13:  69763bc2255 merge: use config-based hooks for post-merge hook\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    merge: use config-based hooks for post-merge hook\n    +    merge: convert post-merge to use hook.h\n     \n    -    Teach post-merge to use the hook.h library instead of the run-command.h\n    -    library to run hooks. This means that post-merge hooks can come from the\n    -    config as well as from the hookdir. post-merge is invoked only from\n    -    builtin/merge.c.\n    +    Teach post-merge to use the hook.h library instead of the\n    +    run-command.h library to run hooks.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n    @@ builtin/merge.c: static void finish(struct commit *head_commit,\n      \t/* Run a post-merge hook */\n     -\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n     +\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n    -+\trun_hooks(\"post-merge\", &opt);\n    -+\trun_hooks_opt_clear(&opt);\n    ++\trun_hooks_oneshot(\"post-merge\", &opt);\n      \n      \tapply_autostash(git_path_merge_autostash(the_repository));\n      \tstrbuf_release(&reflog_message);\n    -@@ builtin/merge.c: static void prepare_to_commit(struct commit_list *remoteheads)\n    - \t * and write it out as a tree.  We must do this before we invoke\n    - \t * the editor and after we invoke run_status above.\n    - \t */\n    --\tif (find_hook(\"pre-merge-commit\"))\n    -+\tif (hook_exists(\"pre-merge-commit\"))\n    - \t\tdiscard_cache();\n    - \tread_cache_from(index_file);\n    - \tstrbuf_addbuf(&msg, &merge_msg);\n13:  12347d901bb ! 14:  2ca1ca1b8e4 git hook run: add an --ignore-missing flag\n    @@ Documentation/git-hook.txt: optional `--` (or `--end-of-options`, see linkgit:gi\n      linkgit:githooks[5]\n     \n      ## builtin/hook.c ##\n    +@@\n    + #include \"strvec.h\"\n    + \n    + #define BUILTIN_HOOK_RUN_USAGE \\\n    +-\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n    ++\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n    + \n    + static const char * const builtin_hook_usage[] = {\n    + \tBUILTIN_HOOK_RUN_USAGE,\n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n    + {\n      \tint i;\n      \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    - \tint rc = 0;\n     +\tint ignore_missing = 0;\n      \tconst char *hook_name;\n      \tconst char *hook_path;\n    - \n      \tstruct option run_options[] = {\n     +\t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n     +\t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n      \t\tOPT_END(),\n      \t};\n    - \n    + \tint ret;\n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n    - \t/*\n    - \t * We are not using run_hooks() because we'd like to detect\n    - \t * missing hooks. Let's find it ourselves and call\n    --\t * run_found_hooks() instead.\n    -+\t * run_found_hooks() instead...\n    - \t */\n    + \tgit_config(git_default_config, NULL);\n    + \n      \thook_name = argv[0];\n    ++\tif (ignore_missing)\n    ++\t\treturn run_hooks_oneshot(hook_name, &opt);\n      \thook_path = find_hook(hook_name);\n      \tif (!hook_path) {\n    -+\t\t/* ... act like run_hooks() under --ignore-missing */\n    -+\t\tif (ignore_missing)\n    -+\t\t\treturn 0;\n      \t\terror(\"cannot find a hook named %s\", hook_name);\n    - \t\treturn 1;\n    - \t}\n     \n      ## t/t1800-hook.sh ##\n     @@ t/t1800-hook.sh: test_expect_success 'git hook run: nonexistent hook' '\n      \ttest_cmp stderr.expect stderr.actual\n      '\n      \n    --test_expect_success 'git hook run: basic' '\n     +test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '\n     +\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n     +\ttest_must_be_empty stderr.actual\n     +'\n     +\n    -+test_expect_success 'git hook run -- basic' '\n    + test_expect_success 'git hook run: basic' '\n      \twrite_script .git/hooks/test-hook <<-EOF &&\n      \techo Test hook\n    - \tEOF\n14:  71d209b4077 ! 15:  5b66b04bec7 send-email: use 'git hook run' for 'sendemail-validate'\n    @@ git-send-email.perl: sub validate_patch {\n      \tif ($repo) {\n     +\t\tmy $hook_name = 'sendemail-validate';\n      \t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n    --\t\tmy $validate_hook = catfile($hooks_path,\n    + \t\trequire File::Spec;\n    +-\t\tmy $validate_hook = File::Spec->catfile($hooks_path,\n     -\t\t\t\t\t    'sendemail-validate');\n    -+\t\tmy $validate_hook = catfile($hooks_path, $hook_name);\n    ++\t\tmy $validate_hook = File::Spec->catfile($hooks_path, $hook_name);\n      \t\tmy $hook_error;\n      \t\tif (-x $validate_hook) {\n    - \t\t\tmy $target = abs_path($fn);\n    + \t\t\trequire Cwd;\n     @@ git-send-email.perl: sub validate_patch {\n      \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n      \t\t\t\tor die(\"chdir: $!\");\n15:  246a82b55b2 = 16:  14a37a43db2 git-p4: use 'git hook' to run hooks\n16:  e3f8482d803 ! 17:  ad5d0e0e7de commit: use hook.h to execute hooks\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    commit: use hook.h to execute hooks\n    +    commit: convert {pre-commit,prepare-commit-msg} hook to hook.h\n     \n    -    Teach run_commit_hook() to call hook.h instead of run-command.h. This\n    -    covers 'pre-commit', 'commit-msg', and\n    -    'prepare-commit-msg'.\n    -\n    -    Additionally, ask the hook library - not run-command - whether any\n    -    hooks will be run, as it's possible hooks may exist in the config but\n    -    not the hookdir.\n    -\n    -    Because all but 'post-commit' hooks are expected to make some state\n    -    change, force all but 'post-commit' hook to run in series. 'post-commit'\n    -    \"is meant primarily for notification, and cannot affect the outcome of\n    -    `git commit`,\" so it is fine to run in parallel.\n    +    Move these hooks hook away from run-command.h to and over to the new\n    +    hook.h library.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n    - ## builtin/commit.c ##\n    -@@ builtin/commit.c: static int prepare_to_commit(const char *index_file, const char *prefix,\n    - \t\treturn 0;\n    - \t}\n    - \n    --\tif (!no_verify && find_hook(\"pre-commit\")) {\n    -+\tif (!no_verify && hook_exists(\"pre-commit\")) {\n    - \t\t/*\n    - \t\t * Re-read the index as pre-commit hook could have updated it,\n    - \t\t * and write it out as a tree.  We must do this before we invoke\n    -\n      ## commit.c ##\n     @@\n      #include \"commit-reach.h\"\n    @@ commit.c: size_t ignore_non_trailer(const char *buf, size_t len)\n     -\tstruct strvec hook_env = STRVEC_INIT;\n     +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n      \tva_list args;\n    +-\tint ret;\n     +\tconst char *arg;\n    - \tint ret;\n    --\n    + \n     -\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n     +\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n      \n    @@ commit.c: size_t ignore_non_trailer(const char *buf, size_t len)\n     +\t\tstrvec_push(&opt.args, arg);\n      \tva_end(args);\n     -\tstrvec_clear(&hook_env);\n    -+\n    -+\tret = run_hooks(name, &opt);\n    -+\trun_hooks_opt_clear(&opt);\n      \n    - \treturn ret;\n    +-\treturn ret;\n    ++\treturn run_hooks_oneshot(name, &opt);\n      }\n    -\n    - ## sequencer.c ##\n    -@@ sequencer.c: static int try_to_commit(struct repository *r,\n    - \t\t}\n    - \t}\n    - \n    --\tif (find_hook(\"prepare-commit-msg\")) {\n    -+\tif (hook_exists(\"prepare-commit-msg\")) {\n    - \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n    - \t\tif (res)\n    - \t\t\tgoto out;\n17:  6ed61071c5e ! 18:  3d3a33e2674 read-cache: convert post-index-change hook to use config\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    read-cache: convert post-index-change hook to use config\n    +    read-cache: convert post-index-change to use hook.h\n     \n    -    By using hook.h instead of run-command.h to run, post-index-change hooks\n    -    can now be specified in the config in addition to the hookdir.\n    -    post-index-change is not run anywhere besides in read-cache.c.\n    +    Move the post-index-change hook away from run-command.h to and over to\n    +    the new hook.h library.\n     \n         This removes the last direct user of run_hook_ve(), so we can make the\n         function static now. It'll be removed entirely soon.\n    @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struc\n     +\t\t     istate->updated_workdir ? \"1\" : \"0\",\n     +\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n     +\t\t     NULL);\n    -+\trun_hooks(\"post-index-change\", &hook_opt);\n    -+\trun_hooks_opt_clear(&hook_opt);\n    ++\trun_hooks_oneshot(\"post-index-change\", &hook_opt);\n     +\n      \tistate->updated_workdir = 0;\n      \tistate->updated_skipworktree = 0;\n18:  e4ef3f4548a ! 19:  893f8666301 receive-pack: convert push-to-checkout hook to hook.h\n    @@ Metadata\n      ## Commit message ##\n         receive-pack: convert push-to-checkout hook to hook.h\n     \n    -    By using hook.h instead of run-command.h to invoke push-to-checkout,\n    -    hooks can now be specified in the config as well as in the hookdir.\n    -    push-to-checkout is not called anywhere but in builtin/receive-pack.c.\n    +    Move the push-to-checkout hook away from run-command.h to and over to\n    +    the new hook.h library.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n    @@ builtin/receive-pack.c: static const char *push_to_checkout(unsigned char *hash,\n     -\t\t\thash_to_hex(hash), NULL))\n     +\tstrvec_pushv(&opt.env, env->v);\n     +\tstrvec_push(&opt.args, hash_to_hex(hash));\n    -+\tif (run_hooks(push_to_checkout_hook, &opt)) {\n    -+\t\trun_hooks_opt_clear(&opt);\n    ++\tif (run_hooks_oneshot(push_to_checkout_hook, &opt))\n      \t\treturn \"push-to-checkout hook declined\";\n    --\telse\n    -+\t} else {\n    -+\t\trun_hooks_opt_clear(&opt);\n    - \t\treturn NULL;\n    -+\t}\n    - }\n    - \n    - static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree)\n    -@@ builtin/receive-pack.c: static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n    - \n    - \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n    - \n    --\tif (!find_hook(push_to_checkout_hook))\n    -+\tif (!hook_exists(push_to_checkout_hook))\n    - \t\tretval = push_to_deploy(sha1, &env, work_tree);\n      \telse\n    - \t\tretval = push_to_checkout(sha1, &env, work_tree);\n    + \t\treturn NULL;\n19:  e3dda367ec9 = 20:  070433deba5 run-command: remove old run_hook_{le,ve}() hook API\n20:  477d75bf579 = 21:  1028e0c1667 run-command: allow stdin for run_processes_parallel\n21:  b7c0ee9719a ! 22:  639e59e9ed0 hook: support passing stdin to hooks\n    @@ Documentation/git-hook.txt: what those are.\n     \n      ## builtin/hook.c ##\n     @@\n    + #include \"strvec.h\"\n      \n    - static const char * const builtin_hook_usage[] = {\n    - \tN_(\"git hook <command> [...]\"),\n    --\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n    -+\tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n    - \tNULL\n    - };\n    - \n    - static const char * const builtin_hook_run_usage[] = {\n    - \tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n    -+\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n    - \tNULL\n    - };\n    + #define BUILTIN_HOOK_RUN_USAGE \\\n    +-\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n    ++\tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n      \n    + static const char * const builtin_hook_usage[] = {\n    + \tBUILTIN_HOOK_RUN_USAGE,\n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \tstruct option run_options[] = {\n      \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n     +\t\t\t   N_(\"file to read into hooks' stdin\")),\n      \t\tOPT_END(),\n      \t};\n    - \n    + \tint ret;\n     \n      ## hook.c ##\n     @@ hook.c: static int pick_next_hook(struct child_process *cp,\n    - \tstruct hook_cb_data *hook_cb = pp_cb;\n    - \tstruct hook *run_me = hook_cb->run_me;\n    + \tif (!run_me)\n    + \t\treturn 0;\n      \n     -\tcp->no_stdin = 1;\n    -+\n     +\t/* reopen the file for stdin; run_command closes it. */\n     +\tif (hook_cb->options->path_to_stdin) {\n     +\t\tcp->no_stdin = 0;\n    @@ hook.c: static int pick_next_hook(struct child_process *cp,\n     \n      ## hook.h ##\n     @@ hook.h: struct run_hooks_opt\n    + \n      \t/* Path to initial working directory for subprocess */\n      \tconst char *dir;\n    - \n    ++\n     +\t/* Path to file which should be piped to stdin for each hook */\n     +\tconst char *path_to_stdin;\n      };\n22:  4035069a98c ! 23:  7d1925cca48 am: convert 'post-rewrite' hook to hook.h\n    @@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)\n      {\n     -\tstruct child_process cp = CHILD_PROCESS_INIT;\n     -\tconst char *hook = find_hook(\"post-rewrite\");\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    - \tint ret;\n    - \n    +-\tint ret;\n    +-\n     -\tif (!hook)\n     -\t\treturn 0;\n     -\n    @@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)\n     -\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n     -\tcp.stdout_to_stderr = 1;\n     -\tcp.trace2_hook_name = \"post-rewrite\";\n    -+\tstrvec_push(&opt.args, \"rebase\");\n    -+\topt.path_to_stdin = am_path(state, \"rewritten\");\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n      \n     -\tret = run_command(&cp);\n    -+\tret = run_hooks(\"post-rewrite\", &opt);\n    ++\tstrvec_push(&opt.args, \"rebase\");\n    ++\topt.path_to_stdin = am_path(state, \"rewritten\");\n      \n     -\tclose(cp.in);\n    -+\trun_hooks_opt_clear(&opt);\n    - \treturn ret;\n    +-\treturn ret;\n    ++\treturn run_hooks_oneshot(\"post-rewrite\", &opt);\n      }\n      \n    + /**\n23:  c847a19581a ! 24:  0c24221b522 run-command: add stdin callback for parallelization\n    @@ builtin/submodule--helper.c: static int update_submodules(struct submodule_updat\n      \n     \n      ## hook.c ##\n    -@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    - \trun_processes_parallel_tr2(options->jobs,\n    +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n    + \trun_processes_parallel_tr2(jobs,\n      \t\t\t\t   pick_next_hook,\n      \t\t\t\t   notify_start_failure,\n     +\t\t\t\t   NULL,\n    @@ run-command.h: typedef int (*task_finished_fn)(int result,\n     +\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n      \t\t\t       const char *tr2_category, const char *tr2_label);\n      \n    - #endif\n    + /**\n     \n      ## submodule.c ##\n     @@ submodule.c: int fetch_populated_submodules(struct repository *r,\n24:  da46c859c1c ! 25:  05d1085f7eb hook: provide stdin by string_list or callback\n    @@ hook.c: static int pick_next_hook(struct child_process *cp,\n      \t} else {\n      \t\tcp->no_stdin = 1;\n      \t}\n    -@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    - \trun_processes_parallel_tr2(options->jobs,\n    +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n    + \trun_processes_parallel_tr2(jobs,\n      \t\t\t\t   pick_next_hook,\n      \t\t\t\t   notify_start_failure,\n     -\t\t\t\t   NULL,\n    @@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n      \t\t\t\t   notify_hook_finished,\n      \t\t\t\t   &cb_data,\n      \t\t\t\t   \"hook\",\n    -@@ hook.c: int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n    +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n    + \n    + \tif (options->absolute_path)\n    + \t\tstrbuf_release(&abs_path);\n    ++\tfree(my_hook.feed_pipe_cb_data);\n    + \n    + \treturn cb_data.rc;\n    + }\n    +@@ hook.c: int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n      \tif (!options)\n    - \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n    + \t\toptions = &hook_opt_scratch;\n      \n     +\tif (options->path_to_stdin && options->feed_pipe)\n     +\t\tBUG(\"choose only one method to populate stdin\");\n     +\n      \thook_path = find_hook(hook_name);\n    - \n    - \t/*\n    + \tif (!hook_path) {\n    + \t\tret = 0;\n     \n      ## hook.h ##\n     @@ hook.h: int hook_exists(const char *hookname);\n    @@ hook.h: struct run_hooks_opt\n     + */\n     +int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n     +\n    - struct hook_cb_data {\n    - \t/* rc reflects the cumulative failure state */\n    - \tint rc;\n    + /*\n    +  * Callback provided to feed_pipe_fn and consume_sideband_fn.\n    +  */\n25:  7343be28ef4 ! 26:  4b7175af2e5 hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n    @@ sequencer.c: int update_head_with_reflog(const struct commit *old_head,\n     +\topt.feed_pipe = pipe_from_string_list;\n     +\topt.feed_pipe_ctx = &to_stdin;\n     +\n    -+\tcode = run_hooks(\"post-rewrite\", &opt);\n    ++\tcode = run_hooks_oneshot(\"post-rewrite\", &opt);\n     +\n    -+\trun_hooks_opt_clear(&opt);\n     +\tstrbuf_release(&tmp);\n     +\tstring_list_clear(&to_stdin, 0);\n     +\treturn code;\n    @@ sequencer.c: static int pick_commits(struct repository *r,\n     +\n     +\t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n     +\t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n    -+\t\t\trun_hooks(\"post-rewrite\", &hook_opt);\n    -+\t\t\trun_hooks_opt_clear(&hook_opt);\n    ++\t\t\trun_hooks_oneshot(\"post-rewrite\", &hook_opt);\n      \t\t}\n      \t\tapply_autostash(rebase_path_autostash());\n      \n26:  85bf13a0835 ! 27:  3f24e056410 transport: convert pre-push hook to use config\n    @@ Metadata\n     Author: Emily Shaffer <emilyshaffer@google.com>\n     \n      ## Commit message ##\n    -    transport: convert pre-push hook to use config\n    +    transport: convert pre-push hook to hook.h\n     \n    -    By using the hook.h:run_hooks API, pre-push hooks can be specified in\n    -    the config as well as in the hookdir.\n    +    Move the pre-push hook away from run-command.h to and over to the new\n    +    hook.h library.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n    @@ transport.c: static void die_with_unpushed_submodules(struct string_list *needs_\n     -\tint ret = 0, x;\n     +\tint ret = 0;\n     +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct strbuf tmp = STRBUF_INIT;\n      \tstruct ref *r;\n     -\tstruct child_process proc = CHILD_PROCESS_INIT;\n     -\tstruct strbuf buf;\n    @@ transport.c: static void die_with_unpushed_submodules(struct string_list *needs_\n     -\t\tfinish_command(&proc);\n     -\t\treturn -1;\n     -\t}\n    --\n    --\tsigchain_push(SIGPIPE, SIG_IGN);\n    -+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n    ++\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n      \n    +-\tsigchain_push(SIGPIPE, SIG_IGN);\n    +-\n     -\tstrbuf_init(&buf, 256);\n     +\tstrvec_push(&opt.args, transport->remote->name);\n     +\tstrvec_push(&opt.args, transport->url);\n      \n      \tfor (r = remote_refs; r; r = r->next) {\n    ++\t\tstruct strbuf buf = STRBUF_INIT;\n    ++\n      \t\tif (!r->peer_ref) continue;\n    -@@ transport.c: static int run_pre_push_hook(struct transport *transport,\n    + \t\tif (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;\n    + \t\tif (r->status == REF_STATUS_REJECT_STALE) continue;\n      \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n      \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n      \n     -\t\tstrbuf_reset(&buf);\n     -\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n    -+\t\tstrbuf_reset(&tmp);\n    -+\t\tstrbuf_addf(&tmp, \"%s %s %s %s\",\n    ++\t\tstrbuf_addf(&buf, \"%s %s %s %s\",\n      \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n      \t\t\t r->name, oid_to_hex(&r->old_oid));\n     -\n    @@ transport.c: static int run_pre_push_hook(struct transport *transport,\n     -\t\t\t\tret = -1;\n     -\t\t\tbreak;\n     -\t\t}\n    -+\t\tstring_list_append(&to_stdin, tmp.buf);\n    ++\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n      \t}\n      \n     -\tstrbuf_release(&buf);\n    @@ transport.c: static int run_pre_push_hook(struct transport *transport,\n     -\tx = finish_command(&proc);\n     -\tif (!ret)\n     -\t\tret = x;\n    -+\tret = run_hooks(\"pre-push\", &opt);\n    -+\trun_hooks_opt_clear(&opt);\n    -+\tstrbuf_release(&tmp);\n    ++\tret = run_hooks_oneshot(\"pre-push\", &opt);\n    ++\tto_stdin.strdup_strings = 1;\n     +\tstring_list_clear(&to_stdin, 0);\n      \n      \treturn ret;\n -:  ----------- > 28:  ecf75f33233 hook tests: test for exact \"pre-push\" hook input\n -:  ----------- > 29:  2c961be94b4 hook tests: use a modern style for \"pre-push\" tests\n27:  331014bad17 ! 30:  1ce456f9d9d reference-transaction: use hook.h to run hooks\n    @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n      \t\t\t\tconst char *state)\n      {\n     -\tstruct child_process proc = CHILD_PROCESS_INIT;\n    - \tstruct strbuf buf = STRBUF_INIT;\n    +-\tstruct strbuf buf = STRBUF_INIT;\n     -\tconst char *hook;\n     +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n    ++\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n      \tint ret = 0, i;\n    -+\tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n      \n     -\thook = find_hook(\"reference-transaction\");\n     -\tif (!hook)\n    -+\tif (!hook_exists(\"reference-transaction\"))\n    - \t\treturn ret;\n    - \n    +-\t\treturn ret;\n    +-\n     -\tstrvec_pushl(&proc.args, hook, state, NULL);\n     -\tproc.in = -1;\n     -\tproc.stdout_to_stderr = 1;\n    @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n     -\n     -\tret = start_command(&proc);\n     -\tif (ret)\n    --\t\treturn ret;\n    --\n    ++\tif (!hook_exists(\"reference-transaction\"))\n    + \t\treturn ret;\n    + \n     -\tsigchain_push(SIGPIPE, SIG_IGN);\n     +\tstrvec_push(&opt.args, state);\n      \n      \tfor (i = 0; i < transaction->nr; i++) {\n      \t\tstruct ref_update *update = transaction->updates[i];\n    -+\t\toid_to_hex_r(o, &update->old_oid);\n    -+\t\toid_to_hex_r(n, &update->new_oid);\n    ++\t\tstruct strbuf buf = STRBUF_INIT;\n      \n    - \t\tstrbuf_reset(&buf);\n    +-\t\tstrbuf_reset(&buf);\n     -\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n    --\t\t\t    oid_to_hex(&update->old_oid),\n    --\t\t\t    oid_to_hex(&update->new_oid),\n    --\t\t\t    update->refname);\n    ++\t\tstrbuf_addf(&buf, \"%s %s %s\",\n    + \t\t\t    oid_to_hex(&update->old_oid),\n    + \t\t\t    oid_to_hex(&update->new_oid),\n    + \t\t\t    update->refname);\n     -\n     -\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n     -\t\t\tif (errno != EPIPE)\n     -\t\t\t\tret = -1;\n     -\t\t\tbreak;\n     -\t\t}\n    -+\t\tstrbuf_addf(&buf, \"%s %s %s\", o, n, update->refname);\n    -+\t\tstring_list_append(&to_stdin, buf.buf);\n    ++\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n      \t}\n      \n     -\tclose(proc.in);\n     -\tsigchain_pop(SIGPIPE);\n    +-\tstrbuf_release(&buf);\n     +\topt.feed_pipe = pipe_from_string_list;\n     +\topt.feed_pipe_ctx = &to_stdin;\n     +\n    -+\tret = run_hooks(\"reference-transaction\", &opt);\n    -+\trun_hooks_opt_clear(&opt);\n    - \tstrbuf_release(&buf);\n    ++\tret = run_hooks_oneshot(\"reference-transaction\", &opt);\n    ++\tto_stdin.strdup_strings = 1;\n     +\tstring_list_clear(&to_stdin, 0);\n      \n     -\tret |= finish_command(&proc);\n28:  f7f56d0a3bb ! 31:  6e5f1f5bd3a run-command: allow capturing of collated output\n    @@ builtin/submodule--helper.c: static int update_submodules(struct submodule_updat\n      \n     \n      ## hook.c ##\n    -@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n      \t\t\t\t   pick_next_hook,\n      \t\t\t\t   notify_start_failure,\n      \t\t\t\t   options->feed_pipe,\n    @@ run-command.h: int run_processes_parallel(int n,\n     +\t\t\t       task_finished_fn, void *pp_cb,\n      \t\t\t       const char *tr2_category, const char *tr2_label);\n      \n    - #endif\n    + /**\n     \n      ## submodule.c ##\n     @@ submodule.c: int fetch_populated_submodules(struct repository *r,\n29:  7f7fcc06885 ! 32:  0b6e9c6d07a hooks: allow callers to capture output\n    @@ Commit message\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## hook.c ##\n    -@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n      \t\t\t\t   pick_next_hook,\n      \t\t\t\t   notify_start_failure,\n      \t\t\t\t   options->feed_pipe,\n    @@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n      \t\t\t\t   notify_hook_finished,\n      \t\t\t\t   &cb_data,\n      \t\t\t\t   \"hook\",\n    + \t\t\t\t   hook_name);\n    + \n    ++\n    + \tif (options->absolute_path)\n    + \t\tstrbuf_release(&abs_path);\n    + \tfree(my_hook.feed_pipe_cb_data);\n     \n      ## hook.h ##\n     @@ hook.h: struct run_hooks_opt\n30:  e74d49e5593 ! 33:  dcf63634338 receive-pack: convert 'update' hook to hook.h\n    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n     +static int run_update_hook(struct command *cmd)\n     +{\n     +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tint code;\n     +\n     +\tstrvec_pushl(&opt.args,\n     +\t\t     cmd->ref_name,\n    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n     -\treturn finish_command(&proc);\n     +\t\topt.consume_sideband = hook_output_to_sideband;\n     +\n    -+\tcode = run_hooks(\"update\", &opt);\n    -+\trun_hooks_opt_clear(&opt);\n    -+\treturn code;\n    ++\treturn run_hooks_oneshot(\"update\", &opt);\n      }\n      \n      static struct command *find_command_by_refname(struct command *list,\n31:  0bdc4878ac8 ! 34:  f352a485e59 post-update: use hook.h library\n    @@ builtin/receive-pack.c: static const char *update(struct command *cmd, struct sh\n     -\t\t\tcopy_to_sideband(proc.err, -1, NULL);\n     -\t\tfinish_command(&proc);\n     -\t}\n    -+\trun_hooks(\"post-update\", &opt);\n    -+\trun_hooks_opt_clear(&opt);\n    ++\trun_hooks_oneshot(\"post-update\", &opt);\n      }\n      \n      static void check_aliased_update_internal(struct command *cmd,\n32:  db70b59b3bd ! 35:  ceef2f3e804 receive-pack: convert receive hooks to hook.h\n    @@ builtin/receive-pack.c: static void hook_output_to_sideband(struct strbuf *outpu\n     +{\n     +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n     +\tstruct receive_hook_feed_context ctx;\n    -+\tint rc;\n     +\tstruct command *iter = commands;\n     +\n     +\t/* if there are no valid commands, don't invoke the hook at all. */\n    @@ builtin/receive-pack.c: static void hook_output_to_sideband(struct strbuf *outpu\n     +\tif (!iter)\n     +\t\treturn 0;\n     +\n    -+\t/* pre-receive hooks should run in series as the hook updates refs */\n    -+\tif (!strcmp(hook_name, \"pre-receive\"))\n    -+\t\topt.jobs = 1;\n    -+\n     +\tif (push_options) {\n     +\t\tint i;\n     +\t\tfor (i = 0; i < push_options->nr; i++)\n    @@ builtin/receive-pack.c: static void hook_output_to_sideband(struct strbuf *outpu\n     +\topt.feed_pipe = feed_receive_hook_cb;\n     +\topt.feed_pipe_ctx = &ctx;\n     +\n    -+\trc = run_hooks(hook_name, &opt);\n    -+\trun_hooks_opt_clear(&opt);\n    -+\treturn rc;\n    ++\treturn run_hooks_oneshot(hook_name, &opt);\n     +}\n     +\n      static int run_update_hook(struct command *cmd)\n33:  d86fedf041c ! 36:  b71d7628b40 hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n    @@ hook.c: static int notify_hook_finished(int result,\n     +\tif (hook_cb->invoked_hook)\n     +\t\t*hook_cb->invoked_hook = 1;\n     +\n    - \treturn 1;\n    + \treturn 0;\n      }\n      \n    -@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n    +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n      \t\t.rc = 0,\n      \t\t.hook_name = hook_name,\n      \t\t.options = options,\n     +\t\t.invoked_hook = options->invoked_hook,\n      \t};\n    - \tif (options->absolute_path) {\n    - \t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n    + \tint jobs = 1;\n    + \n     \n      ## hook.h ##\n     @@ hook.h: struct run_hooks_opt\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431845","messageId":"patch-v4-03.36-644b31fe281-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 03/36] Makefile: remove an out-of-date comment","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:29Z","receivedAt":"2021-08-03T19:39:22Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This comment added in dfea575017 (Makefile: lazily compute header\ndependencies, 2010-01-26) has been out of date since\n92b88eba9f (Makefile: use `git ls-files` to list header files, if\npossible, 2019-03-04), when we did exactly what it tells us not to do\nand added $(GENERATED_H) to $(OBJECTS) dependencies.\n\nThe rest of it was also somewhere between inaccurate and outdated,\nsince as of b8ba629264 (Makefile: fold MISC_H into LIB_H, 2012-06-20)\nit's not followed by a list of header files, that got moved earlier in\nthe file into LIB_H in 60d24dd255 (Makefile: fold XDIFF_H and VCSSVN_H\ninto LIB_H, 2012-07-06).\n\nLet's just remove it entirely, to the extent that we have anything\nuseful to say here the comment on the\n\"USE_COMPUTED_HEADER_DEPENDENCIES\" variable a few lines above this\nchange does the job for us.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile | 7 -------\n 1 file changed, 7 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 89bf0dd7332..0a540dcd34e 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2519,13 +2519,6 @@ ifneq ($(dep_files_present),)\n include $(dep_files_present)\n endif\n else\n-# Dependencies on header files, for platforms that do not support\n-# the gcc -MMD option.\n-#\n-# Dependencies on automatically generated headers such as command-list.h\n-# should _not_ be included here, since they are necessary even when\n-# building an object for the first time.\n-\n $(OBJECTS): $(LIB_H) $(GENERATED_H)\n endif\n \n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431846","messageId":"patch-v4-04.36-89c4d44b0c3-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 04/36] hook.[ch]: move find_hook() to this new library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:30Z","receivedAt":"2021-08-03T19:39:22Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Move the find_hook() function from run-command.c to a new hook.c\nlibrary. This change establishes a stub library that's pretty\npointless right now, but will see much wider use with Emily Shaffer's\nupcoming \"configuration-based hooks\" series.\n\nEventually all the hook related code will live in hook.[ch]. Let's\nstart that process by moving the simple find_hook() function over\nas-is.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile               |  1 +\n builtin/am.c           |  1 +\n builtin/bugreport.c    |  2 +-\n builtin/commit.c       |  1 +\n builtin/merge.c        |  1 +\n builtin/receive-pack.c |  1 +\n builtin/worktree.c     |  1 +\n hook.c                 | 37 +++++++++++++++++++++++++++++++++++++\n hook.h                 | 11 +++++++++++\n refs.c                 |  1 +\n run-command.c          | 35 +----------------------------------\n run-command.h          |  7 -------\n sequencer.c            |  1 +\n transport.c            |  1 +\n 14 files changed, 59 insertions(+), 42 deletions(-)\n create mode 100644 hook.c\n create mode 100644 hook.h\n\ndiff --git a/Makefile b/Makefile\nindex 0a540dcd34e..bc258886904 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -910,6 +910,7 @@ LIB_OBJS += hash-lookup.o\n LIB_OBJS += hashmap.o\n LIB_OBJS += help.o\n LIB_OBJS += hex.o\n+LIB_OBJS += hook.o\n LIB_OBJS += ident.o\n LIB_OBJS += json-writer.o\n LIB_OBJS += kwset.o\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 0c2ad96b70e..c603f3cebdf 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -11,6 +11,7 @@\n #include \"parse-options.h\"\n #include \"dir.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"quote.h\"\n #include \"tempfile.h\"\n #include \"lockfile.h\"\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 9915a5841de..596f079a7f9 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -3,7 +3,7 @@\n #include \"strbuf.h\"\n #include \"help.h\"\n #include \"compat/compiler.h\"\n-#include \"run-command.h\"\n+#include \"hook.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 7436262aae2..51b07ee02ea 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -19,6 +19,7 @@\n #include \"revision.h\"\n #include \"wt-status.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"refs.h\"\n #include \"log-tree.h\"\n #include \"strbuf.h\"\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex a8a843b1f54..be98d66b0a8 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -13,6 +13,7 @@\n #include \"builtin.h\"\n #include \"lockfile.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"diff.h\"\n #include \"diff-merges.h\"\n #include \"refs.h\"\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 2d1f97e1ca7..97aebdc15bd 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -7,6 +7,7 @@\n #include \"pkt-line.h\"\n #include \"sideband.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"commit.h\"\n #include \"object.h\"\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 0d0a80da61f..d22ece93e1a 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -8,6 +8,7 @@\n #include \"branch.h\"\n #include \"refs.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"sigchain.h\"\n #include \"submodule.h\"\n #include \"utf8.h\"\ndiff --git a/hook.c b/hook.c\nnew file mode 100644\nindex 00000000000..c4dbef1d0ef\n--- /dev/null\n+++ b/hook.c\n@@ -0,0 +1,37 @@\n+#include \"cache.h\"\n+#include \"hook.h\"\n+#include \"run-command.h\"\n+\n+const char *find_hook(const char *name)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\n+\tstrbuf_reset(&path);\n+\tstrbuf_git_path(&path, \"hooks/%s\", name);\n+\tif (access(path.buf, X_OK) < 0) {\n+\t\tint err = errno;\n+\n+#ifdef STRIP_EXTENSION\n+\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n+\t\tif (access(path.buf, X_OK) >= 0)\n+\t\t\treturn path.buf;\n+\t\tif (errno == EACCES)\n+\t\t\terr = errno;\n+#endif\n+\n+\t\tif (err == EACCES && advice_ignored_hook) {\n+\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n+\n+\t\t\tif (!string_list_lookup(&advise_given, name)) {\n+\t\t\t\tstring_list_insert(&advise_given, name);\n+\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n+\t\t\t\t\t \"it's not set as executable.\\n\"\n+\t\t\t\t\t \"You can disable this warning with \"\n+\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n+\t\t\t\t       path.buf);\n+\t\t\t}\n+\t\t}\n+\t\treturn NULL;\n+\t}\n+\treturn path.buf;\n+}\ndiff --git a/hook.h b/hook.h\nnew file mode 100644\nindex 00000000000..68624f16059\n--- /dev/null\n+++ b/hook.h\n@@ -0,0 +1,11 @@\n+#ifndef HOOK_H\n+#define HOOK_H\n+\n+/*\n+ * Returns the path to the hook file, or NULL if the hook is missing\n+ * or disabled. Note that this points to static storage that will be\n+ * overwritten by further calls to find_hook and run_hook_*.\n+ */\n+const char *find_hook(const char *name);\n+\n+#endif\ndiff --git a/refs.c b/refs.c\nindex 8b9f7c3a80a..6211692eaae 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -10,6 +10,7 @@\n #include \"refs.h\"\n #include \"refs/refs-internal.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"object-store.h\"\n #include \"object.h\"\n #include \"tag.h\"\ndiff --git a/run-command.c b/run-command.c\nindex f72e72cce73..352f5be1646 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -8,6 +8,7 @@\n #include \"string-list.h\"\n #include \"quote.h\"\n #include \"config.h\"\n+#include \"hook.h\"\n \n void child_process_init(struct child_process *child)\n {\n@@ -1319,40 +1320,6 @@ int async_with_fork(void)\n #endif\n }\n \n-const char *find_hook(const char *name)\n-{\n-\tstatic struct strbuf path = STRBUF_INIT;\n-\n-\tstrbuf_reset(&path);\n-\tstrbuf_git_path(&path, \"hooks/%s\", name);\n-\tif (access(path.buf, X_OK) < 0) {\n-\t\tint err = errno;\n-\n-#ifdef STRIP_EXTENSION\n-\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n-\t\tif (access(path.buf, X_OK) >= 0)\n-\t\t\treturn path.buf;\n-\t\tif (errno == EACCES)\n-\t\t\terr = errno;\n-#endif\n-\n-\t\tif (err == EACCES && advice_ignored_hook) {\n-\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n-\n-\t\t\tif (!string_list_lookup(&advise_given, name)) {\n-\t\t\t\tstring_list_insert(&advise_given, name);\n-\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n-\t\t\t\t\t \"it's not set as executable.\\n\"\n-\t\t\t\t\t \"You can disable this warning with \"\n-\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n-\t\t\t\t       path.buf);\n-\t\t\t}\n-\t\t}\n-\t\treturn NULL;\n-\t}\n-\treturn path.buf;\n-}\n-\n int run_hook_ve(const char *const *env, const char *name, va_list args)\n {\n \tstruct child_process hook = CHILD_PROCESS_INIT;\ndiff --git a/run-command.h b/run-command.h\nindex af1296769f9..f76b740f927 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -204,13 +204,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/*\n- * Returns the path to the hook file, or NULL if the hook is missing\n- * or disabled. Note that this points to static storage that will be\n- * overwritten by further calls to find_hook and run_hook_*.\n- */\n-const char *find_hook(const char *name);\n-\n /**\n  * Run a hook.\n  * The first argument is a pathname to an index file, or NULL\ndiff --git a/sequencer.c b/sequencer.c\nindex 7f07cd00f3f..ea4199d65a4 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -8,6 +8,7 @@\n #include \"sequencer.h\"\n #include \"tag.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"utf8.h\"\n #include \"cache-tree.h\"\ndiff --git a/transport.c b/transport.c\nindex 17e9629710a..77e196f75f5 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -2,6 +2,7 @@\n #include \"config.h\"\n #include \"transport.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"pkt-line.h\"\n #include \"fetch-pack.h\"\n #include \"remote.h\"\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431847","messageId":"patch-v4-05.36-3514e0c0251-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 05/36] hook.c: add a hook_exists() wrapper and use it in bugreport.c","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:31Z","receivedAt":"2021-08-03T19:39:26Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nAdd a boolean version of the find_hook() function for those callers\nwho are only interested in checking whether the hook exists, not what\nthe path to it is.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/bugreport.c | 2 +-\n hook.c              | 5 +++++\n hook.h              | 5 +++++\n 3 files changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 596f079a7f9..941c8d5e270 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -82,7 +82,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t}\n \n \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (find_hook(hook[i]))\n+\t\tif (hook_exists(hook[i]))\n \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n }\n \ndiff --git a/hook.c b/hook.c\nindex c4dbef1d0ef..97cd799a320 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -35,3 +35,8 @@ const char *find_hook(const char *name)\n \t}\n \treturn path.buf;\n }\n+\n+int hook_exists(const char *name)\n+{\n+\treturn !!find_hook(name);\n+}\ndiff --git a/hook.h b/hook.h\nindex 68624f16059..4c547ac15e5 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -8,4 +8,9 @@\n  */\n const char *find_hook(const char *name);\n \n+/*\n+ * A boolean version of find_hook()\n+ */\n+int hook_exists(const char *hookname);\n+\n #endif\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431848","messageId":"patch-v4-07.36-4cfd72722c1-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 07/36] hook-list.h: add a generated list of hooks, like config-list.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:33Z","receivedAt":"2021-08-03T19:39:27Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Make githooks(5) the source of truth for what hooks git supports, and\ndie hooks we don't know about in find_hook(). This ensures that the\ndocumentation and the C code's idea about existing hooks doesn't\ndiverge.\n\nWe still have Perl and Python code running its own hooks, but that'll\nbe addressed by Emily Shaffer's upcoming \"git hook run\" command.\n\nThis resolves a long-standing TODO item in bugreport.c of there being\nno centralized listing of hooks, and fixes a bug with the bugreport\nlisting only knowing about 1/4 of the p4 hooks. It didn't know about\nthe recent \"reference-transaction\" hook either.\n\nI have not been able to directly test the CMake change being made\nhere. Since 4c2c38e800 (ci: modification of main.yml to use cmake for\nvs-build job, 2020-06-26) some of the Windows CI has a hard dependency\non CMake, this change works there, and is to my eyes an obviously\ncorrect use of a pattern established in previous CMake changes,\nnamely:\n\n - 061c2240b1 (Introduce CMake support for configuring Git,\n    2020-06-12)\n - 709df95b78 (help: move list_config_help to builtin/help,\n    2020-04-16)\n - 976aaedca0 (msvc: add a Makefile target to pre-generate the Visual\n   Studio solution, 2019-07-29)\n\nThe LC_ALL=C is needed because at least in my locale the dash (\"-\") is\nignored for the purposes of sorting, which results in a different\norder. I'm not aware of anything in git that has a hard dependency on\nthe order, but e.g. the bugreport output would end up using whatever\nlocale was in effect when git was compiled.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nHelped-by: René Scharfe <l.s.r@web.de>\n---\n .gitignore                          |  1 +\n Makefile                            | 10 ++++++-\n builtin/bugreport.c                 | 44 ++++++-----------------------\n contrib/buildsystems/CMakeLists.txt |  7 +++++\n generate-hooklist.sh                | 18 ++++++++++++\n hook.c                              | 19 +++++++++++++\n 6 files changed, 62 insertions(+), 37 deletions(-)\n create mode 100755 generate-hooklist.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 311841f9bed..6be9de41ae8 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -190,6 +190,7 @@\n /gitweb/static/gitweb.min.*\n /config-list.h\n /command-list.h\n+/hook-list.h\n *.tar.gz\n *.dsc\n *.deb\ndiff --git a/Makefile b/Makefile\nindex bc258886904..1740b99db00 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -823,6 +823,8 @@ XDIFF_LIB = xdiff/lib.a\n \n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n+GENERATED_H += hook-list.h\n+\n .PHONY: generated-hdrs\n generated-hdrs: $(GENERATED_H)\n \n@@ -2226,7 +2228,9 @@ git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS) $(GITLIBS)\n \n help.sp help.s help.o: command-list.h\n \n-builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX\n+hook.sp hook.s hook.o: hook-list.h\n+\n+builtin/help.sp builtin/help.s builtin/help.o: config-list.h hook-list.h GIT-PREFIX\n builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \\\n \t'-DGIT_HTML_PATH=\"$(htmldir_relative_SQ)\"' \\\n \t'-DGIT_MAN_PATH=\"$(mandir_relative_SQ)\"' \\\n@@ -2259,6 +2263,10 @@ command-list.h: $(wildcard Documentation/git*.txt)\n \t\t$(patsubst %,--exclude-program %,$(EXCLUDED_PROGRAMS)) \\\n \t\tcommand-list.txt >$@+ && mv $@+ $@\n \n+hook-list.h: generate-hooklist.sh Documentation/githooks.txt\n+\t$(QUIET_GEN)$(SHELL_PATH) ./generate-hooklist.sh \\\n+\t\t>$@+ && mv $@+ $@\n+\n SCRIPT_DEFINES = $(SHELL_PATH_SQ):$(DIFF_SQ):$(GIT_VERSION):\\\n \t$(localedir_SQ):$(NO_CURL):$(USE_GETTEXT_SCHEME):$(SANE_TOOL_PATH_SQ):\\\n \t$(gitwebdir_SQ):$(PERL_PATH_SQ):$(SANE_TEXT_GREP):$(PAGER_ENV):\\\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 941c8d5e270..a7a1fcb8a7a 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -4,6 +4,7 @@\n #include \"help.h\"\n #include \"compat/compiler.h\"\n #include \"hook.h\"\n+#include \"hook-list.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\n@@ -41,39 +42,7 @@ static void get_system_info(struct strbuf *sys_info)\n \n static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n {\n-\t/*\n-\t * NEEDSWORK: Doesn't look like there is a list of all possible hooks;\n-\t * so below is a transcription of `git help hooks`. Later, this should\n-\t * be replaced with some programmatically generated list (generated from\n-\t * doc or else taken from some library which tells us about all the\n-\t * hooks)\n-\t */\n-\tstatic const char *hook[] = {\n-\t\t\"applypatch-msg\",\n-\t\t\"pre-applypatch\",\n-\t\t\"post-applypatch\",\n-\t\t\"pre-commit\",\n-\t\t\"pre-merge-commit\",\n-\t\t\"prepare-commit-msg\",\n-\t\t\"commit-msg\",\n-\t\t\"post-commit\",\n-\t\t\"pre-rebase\",\n-\t\t\"post-checkout\",\n-\t\t\"post-merge\",\n-\t\t\"pre-push\",\n-\t\t\"pre-receive\",\n-\t\t\"update\",\n-\t\t\"post-receive\",\n-\t\t\"post-update\",\n-\t\t\"push-to-checkout\",\n-\t\t\"pre-auto-gc\",\n-\t\t\"post-rewrite\",\n-\t\t\"sendemail-validate\",\n-\t\t\"fsmonitor-watchman\",\n-\t\t\"p4-pre-submit\",\n-\t\t\"post-index-change\",\n-\t};\n-\tint i;\n+\tconst char **p;\n \n \tif (nongit) {\n \t\tstrbuf_addstr(hook_info,\n@@ -81,9 +50,12 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t\treturn;\n \t}\n \n-\tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (hook_exists(hook[i]))\n-\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n+\tfor (p = hook_name_list; *p; p++) {\n+\t\tconst char *hook = *p;\n+\n+\t\tif (hook_exists(hook))\n+\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook);\n+\t}\n }\n \n static const char * const bugreport_usage[] = {\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 171b4124afe..fd1399c440f 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -624,6 +624,13 @@ if(NOT EXISTS ${CMAKE_BINARY_DIR}/config-list.h)\n \t\t\tOUTPUT_FILE ${CMAKE_BINARY_DIR}/config-list.h)\n endif()\n \n+if(NOT EXISTS ${CMAKE_BINARY_DIR}/hook-list.h)\n+\tmessage(\"Generating hook-list.h\")\n+\texecute_process(COMMAND ${SH_EXE} ${CMAKE_SOURCE_DIR}/generate-hooklist.sh\n+\t\t\tWORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\n+\t\t\tOUTPUT_FILE ${CMAKE_BINARY_DIR}/hook-list.h)\n+endif()\n+\n include_directories(${CMAKE_BINARY_DIR})\n \n #build\ndiff --git a/generate-hooklist.sh b/generate-hooklist.sh\nnew file mode 100755\nindex 00000000000..6d4e56d1a31\n--- /dev/null\n+++ b/generate-hooklist.sh\n@@ -0,0 +1,18 @@\n+#!/bin/sh\n+#\n+# Usage: ./generate-hooklist.sh >hook-list.h\n+\n+cat <<EOF\n+/* Automatically generated by generate-hooklist.sh */\n+\n+static const char *hook_name_list[] = {\n+EOF\n+\n+sed -n -e '/^~~~~*$/ {x; s/^.*$/\t\"&\",/; p;}; x' \\\n+\t<Documentation/githooks.txt |\n+\tLC_ALL=C sort\n+\n+cat <<EOF\n+\tNULL,\n+};\n+EOF\ndiff --git a/hook.c b/hook.c\nindex 97cd799a320..1f1db1ec9bf 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -1,11 +1,30 @@\n #include \"cache.h\"\n #include \"hook.h\"\n #include \"run-command.h\"\n+#include \"hook-list.h\"\n+\n+static int known_hook(const char *name)\n+{\n+\tconst char **p;\n+\tsize_t len = strlen(name);\n+\tfor (p = hook_name_list; *p; p++) {\n+\t\tconst char *hook = *p;\n+\n+\t\tif (!strncmp(name, hook, len) && hook[len] == '\\0')\n+\t\t\treturn 1;\n+\t}\n+\n+\treturn 0;\n+}\n \n const char *find_hook(const char *name)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \n+\tif (!known_hook(name))\n+\t\tdie(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n+\t\t    name);\n+\n \tstrbuf_reset(&path);\n \tstrbuf_git_path(&path, \"hooks/%s\", name);\n \tif (access(path.buf, X_OK) < 0) {\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431849","messageId":"patch-v4-06.36-d5ef40f77dc-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 06/36] hook.c users: use \"hook_exists()\" insted of \"find_hook()\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:32Z","receivedAt":"2021-08-03T19:39:28Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Use the new hook_exists() function instead of find_hook() where the\nlatter was called in boolean contexts. This make subsequent changes in\na series where we further refactor the hook API clearer, as we won't\nconflate wanting to get the path of the hook with checking for its\nexistence.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c       | 2 +-\n builtin/merge.c        | 2 +-\n builtin/receive-pack.c | 2 +-\n sequencer.c            | 2 +-\n 4 files changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 51b07ee02ea..aa3c741efa9 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1052,7 +1052,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && find_hook(\"pre-commit\")) {\n+\tif (!no_verify && hook_exists(\"pre-commit\")) {\n \t\t/*\n \t\t * Re-read the index as pre-commit hook could have updated it,\n \t\t * and write it out as a tree.  We must do this before we invoke\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex be98d66b0a8..03f244dd5a0 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -849,7 +849,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t * and write it out as a tree.  We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (find_hook(\"pre-merge-commit\"))\n+\tif (hook_exists(\"pre-merge-commit\"))\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 97aebdc15bd..91fa799b66e 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1464,7 +1464,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!find_hook(push_to_checkout_hook))\n+\tif (!hook_exists(push_to_checkout_hook))\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n \telse\n \t\tretval = push_to_checkout(sha1, &env, work_tree);\ndiff --git a/sequencer.c b/sequencer.c\nindex ea4199d65a4..9aac08c1545 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1446,7 +1446,7 @@ static int try_to_commit(struct repository *r,\n \t\t}\n \t}\n \n-\tif (find_hook(\"prepare-commit-msg\")) {\n+\tif (hook_exists(\"prepare-commit-msg\")) {\n \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n \t\tif (res)\n \t\t\tgoto out;\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431850","messageId":"patch-v4-08.36-7cb4a4cb69e-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 08/36] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:34Z","receivedAt":"2021-08-03T19:39:30Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn order to enable hooks to be run as an external process, by a\nstandalone Git command, or by tools which wrap Git, provide an external\nmeans to run all configured hook commands for a given hook event.\n\nMost of our hooks require more complex functionality than this, but\nlet's start with the bare minimum required to support our simplest\nhooks.\n\nIn terms of implementation the usage_with_options() and \"goto usage\"\npattern here mirrors that of\nbuiltin/{commit-graph,multi-pack-index}.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n .gitignore                 |   1 +\n Documentation/git-hook.txt |  38 +++++++++++\n Documentation/githooks.txt |   4 ++\n Makefile                   |   1 +\n builtin.h                  |   1 +\n builtin/hook.c             |  85 ++++++++++++++++++++++++\n command-list.txt           |   1 +\n git.c                      |   1 +\n hook.c                     | 111 +++++++++++++++++++++++++++++++\n hook.h                     |  41 ++++++++++++\n t/t1800-hook.sh            | 133 +++++++++++++++++++++++++++++++++++++\n 11 files changed, 417 insertions(+)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100755 t/t1800-hook.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 6be9de41ae8..66189ca3cdc 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-hook\n /git-http-backend\n /git-http-fetch\n /git-http-push\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nnew file mode 100644\nindex 00000000000..660d6a992a0\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,38 @@\n+git-hook(1)\n+===========\n+\n+NAME\n+----\n+git-hook - run git hooks\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git hook' run <hook-name> [-- <hook-args>]\n+\n+DESCRIPTION\n+-----------\n+\n+This command is an interface to git hooks (see linkgit:githooks[5]).\n+Currently it only provides a convenience wrapper for running hooks for\n+use by git itself. In the future it might gain other functionality.\n+\n+SUBCOMMANDS\n+-----------\n+\n+run::\n+\tRun the `<hook-name>` hook. See linkgit:githooks[5] for\n+\tthe hook names we support.\n++\n+Any positional arguments to the hook should be passed after an\n+optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n+arguments (if any) differ by hook name, see linkgit:githooks[5] for\n+what those are.\n+\n+SEE ALSO\n+--------\n+linkgit:githooks[5]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex b51959ff941..a16e62bc8c8 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -698,6 +698,10 @@ and \"0\" meaning they were not.\n Only one parameter should be set to \"1\" when the hook runs.  The hook\n running passing \"1\", \"1\" should not be possible.\n \n+SEE ALSO\n+--------\n+linkgit:git-hook[1]\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex 1740b99db00..e68ca41507f 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1114,6 +1114,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\ndiff --git a/builtin.h b/builtin.h\nindex 16ecd5586f0..91740c15149 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);\n int cmd_grep(int argc, const char **argv, const char *prefix);\n int cmd_hash_object(int argc, const char **argv, const char *prefix);\n int cmd_help(int argc, const char **argv, const char *prefix);\n+int cmd_hook(int argc, const char **argv, const char *prefix);\n int cmd_index_pack(int argc, const char **argv, const char *prefix);\n int cmd_init_db(int argc, const char **argv, const char *prefix);\n int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/hook.c b/builtin/hook.c\nnew file mode 100644\nindex 00000000000..41dd15550cf\n--- /dev/null\n+++ b/builtin/hook.c\n@@ -0,0 +1,85 @@\n+#include \"cache.h\"\n+#include \"builtin.h\"\n+#include \"config.h\"\n+#include \"hook.h\"\n+#include \"parse-options.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+#define BUILTIN_HOOK_RUN_USAGE \\\n+\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n+\n+static const char * const builtin_hook_usage[] = {\n+\tBUILTIN_HOOK_RUN_USAGE,\n+\tNULL\n+};\n+\n+static const char * const builtin_hook_run_usage[] = {\n+\tBUILTIN_HOOK_RUN_USAGE,\n+\tNULL\n+};\n+\n+static int run(int argc, const char **argv, const char *prefix)\n+{\n+\tint i;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tconst char *hook_name;\n+\tconst char *hook_path;\n+\tstruct option run_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, run_options,\n+\t\t\t     builtin_hook_run_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (!argc)\n+\t\tgoto usage;\n+\n+\t/*\n+\t * Having a -- for \"run\" when providing <hook-args> is\n+\t * mandatory.\n+\t */\n+\tif (argc > 1 && strcmp(argv[1], \"--\") &&\n+\t    strcmp(argv[1], \"--end-of-options\"))\n+\t\tgoto usage;\n+\n+\t/* Add our arguments, start after -- */\n+\tfor (i = 2 ; i < argc; i++)\n+\t\tstrvec_push(&opt.args, argv[i]);\n+\n+\t/* Need to take into account core.hooksPath */\n+\tgit_config(git_default_config, NULL);\n+\n+\thook_name = argv[0];\n+\thook_path = find_hook(hook_name);\n+\tif (!hook_path) {\n+\t\terror(\"cannot find a hook named %s\", hook_name);\n+\t\treturn 1;\n+\t}\n+\n+\tret = run_hooks(hook_name, hook_path, &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn ret;\n+usage:\n+\tusage_with_options(builtin_hook_run_usage, run_options);\n+}\n+\n+int cmd_hook(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option builtin_hook_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, builtin_hook_options,\n+\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n+\tif (!argc)\n+\t\tgoto usage;\n+\n+\tif (!strcmp(argv[0], \"run\"))\n+\t\treturn run(argc, argv, prefix);\n+\n+usage:\n+\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex a289f09ed6f..9ccd8e5aebe 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-hook                                mainporcelain\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\n git-http-push                           synchelpers\ndiff --git a/git.c b/git.c\nindex 18bed9a9964..540909c391f 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nindex 1f1db1ec9bf..c8ff3975043 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -2,11 +2,14 @@\n #include \"hook.h\"\n #include \"run-command.h\"\n #include \"hook-list.h\"\n+#include \"config.h\"\n \n static int known_hook(const char *name)\n {\n \tconst char **p;\n \tsize_t len = strlen(name);\n+\tstatic int test_hooks_ok = -1;\n+\n \tfor (p = hook_name_list; *p; p++) {\n \t\tconst char *hook = *p;\n \n@@ -14,6 +17,14 @@ static int known_hook(const char *name)\n \t\t\treturn 1;\n \t}\n \n+\tif (test_hooks_ok == -1)\n+\t\ttest_hooks_ok = git_env_bool(\"GIT_TEST_FAKE_HOOKS\", 0);\n+\n+\tif (test_hooks_ok &&\n+\t    (!strcmp(name, \"test-hook\") ||\n+\t     !strcmp(name, \"does-not-exist\")))\n+\t\treturn 1;\n+\n \treturn 0;\n }\n \n@@ -59,3 +70,103 @@ int hook_exists(const char *name)\n {\n \treturn !!find_hook(name);\n }\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o)\n+{\n+\tstrvec_clear(&o->env);\n+\tstrvec_clear(&o->args);\n+}\n+\n+static int pick_next_hook(struct child_process *cp,\n+\t\t\t  struct strbuf *out,\n+\t\t\t  void *pp_cb,\n+\t\t\t  void **pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *run_me = hook_cb->run_me;\n+\n+\tif (!run_me)\n+\t\treturn 0;\n+\n+\tcp->no_stdin = 1;\n+\tcp->env = hook_cb->options->env.v;\n+\tcp->stdout_to_stderr = 1;\n+\tcp->trace2_hook_name = hook_cb->hook_name;\n+\n+\t/* add command */\n+\tstrvec_push(&cp->args, run_me->hook_path);\n+\n+\t/*\n+\t * add passed-in argv, without expanding - let the user get back\n+\t * exactly what they put in\n+\t */\n+\tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n+\n+\t/* Provide context for errors if necessary */\n+\t*pp_task_cb = run_me;\n+\n+\t/*\n+\t * This pick_next_hook() will be called again, we're only\n+\t * running one hook, so indicate that no more work will be\n+\t * done.\n+\t */\n+\thook_cb->run_me = NULL;\n+\n+\treturn 1;\n+}\n+\n+static int notify_start_failure(struct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cp)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *attempted = pp_task_cp;\n+\n+\thook_cb->rc |= 1;\n+\n+\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n+\t\t    attempted->hook_path);\n+\n+\treturn 1;\n+}\n+\n+static int notify_hook_finished(int result,\n+\t\t\t\tstruct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\n+\thook_cb->rc |= result;\n+\n+\treturn 0;\n+}\n+\n+int run_hooks(const char *hook_name, const char *hook_path,\n+\t      struct run_hooks_opt *options)\n+{\n+\tstruct hook my_hook = {\n+\t\t.hook_path = hook_path,\n+\t};\n+\tstruct hook_cb_data cb_data = {\n+\t\t.rc = 0,\n+\t\t.hook_name = hook_name,\n+\t\t.options = options,\n+\t};\n+\tint jobs = 1;\n+\n+\tif (!options)\n+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n+\n+\tcb_data.run_me = &my_hook;\n+\n+\trun_processes_parallel_tr2(jobs,\n+\t\t\t\t   pick_next_hook,\n+\t\t\t\t   notify_start_failure,\n+\t\t\t\t   notify_hook_finished,\n+\t\t\t\t   &cb_data,\n+\t\t\t\t   \"hook\",\n+\t\t\t\t   hook_name);\n+\n+\treturn cb_data.rc;\n+}\ndiff --git a/hook.h b/hook.h\nindex 4c547ac15e5..361984c69d4 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,5 +1,8 @@\n #ifndef HOOK_H\n #define HOOK_H\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+#include \"run-command.h\"\n \n /*\n  * Returns the path to the hook file, or NULL if the hook is missing\n@@ -13,4 +16,42 @@ const char *find_hook(const char *name);\n  */\n int hook_exists(const char *hookname);\n \n+struct hook {\n+\t/* The path to the hook */\n+\tconst char *hook_path;\n+};\n+\n+struct run_hooks_opt\n+{\n+\t/* Environment vars to be set for each hook */\n+\tstruct strvec env;\n+\n+\t/* Args to be passed to each hook */\n+\tstruct strvec args;\n+};\n+\n+#define RUN_HOOKS_OPT_INIT { \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n+/*\n+ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n+ */\n+struct hook_cb_data {\n+\t/* rc reflects the cumulative failure state */\n+\tint rc;\n+\tconst char *hook_name;\n+\tstruct hook *run_me;\n+\tstruct run_hooks_opt *options;\n+};\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o);\n+\n+/**\n+ * Takes an already resolved hook found via find_hook() and runs\n+ * it. Does not call run_hooks_opt_clear() for you.\n+ */\n+int run_hooks(const char *hookname, const char *hook_path,\n+\t      struct run_hooks_opt *options);\n #endif\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nnew file mode 100755\nindex 00000000000..644df0a583c\n--- /dev/null\n+++ b/t/t1800-hook.sh\n@@ -0,0 +1,133 @@\n+#!/bin/bash\n+\n+test_description='git-hook command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'git hook usage' '\n+\ttest_expect_code 129 git hook &&\n+\ttest_expect_code 129 git hook run &&\n+\ttest_expect_code 129 git hook run -h &&\n+\ttest_expect_code 129 git hook run --unknown 2>err &&\n+\tgrep \"unknown option\" err\n+'\n+\n+test_expect_success 'setup GIT_TEST_FAKE_HOOKS=true to permit \"test-hook\" and \"does-not-exist\" names\"' '\n+\tGIT_TEST_FAKE_HOOKS=true &&\n+\texport GIT_TEST_FAKE_HOOKS\n+'\n+\n+test_expect_success 'git hook run: nonexistent hook' '\n+\tcat >stderr.expect <<-\\EOF &&\n+\terror: cannot find a hook named test-hook\n+\tEOF\n+\ttest_expect_code 1 git hook run test-hook 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual\n+'\n+\n+test_expect_success 'git hook run: basic' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tEOF\n+\tgit hook run test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run: stdout and stderr both write to our stderr' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo >&1 Will end up on stderr\n+\techo >&2 Will end up on stderr\n+\tEOF\n+\n+\tcat >stderr.expect <<-\\EOF &&\n+\tWill end up on stderr\n+\tWill end up on stderr\n+\tEOF\n+\tgit hook run test-hook >stdout.actual 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual &&\n+\ttest_must_be_empty stdout.actual\n+'\n+\n+test_expect_success 'git hook run: exit codes are passed along' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 1\n+\tEOF\n+\n+\ttest_expect_code 1 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 2\n+\tEOF\n+\n+\ttest_expect_code 2 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 128\n+\tEOF\n+\n+\ttest_expect_code 128 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 129\n+\tEOF\n+\n+\ttest_expect_code 129 git hook run test-hook\n+'\n+\n+test_expect_success 'git hook run arg u ments without -- is not allowed' '\n+\ttest_expect_code 129 git hook run test-hook arg u ments\n+'\n+\n+test_expect_success 'git hook run -- pass arguments' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo $1\n+\techo $2\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\targ\n+\tu ments\n+\tEOF\n+\n+\tgit hook run test-hook -- arg \"u ments\" 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run -- out-of-repo runs excluded' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tnongit test_must_fail git hook run test-hook\n+'\n+\n+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n+\tmkdir my-hooks &&\n+\twrite_script my-hooks/test-hook <<-\\EOF &&\n+\techo Hook ran $1 >>actual\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tHook ran one\n+\tHook ran two\n+\tHook ran three\n+\tHook ran four\n+\tEOF\n+\n+\t# Test various ways of specifying the path. See also\n+\t# t1350-config-hooks-path.sh\n+\t>actual &&\n+\tgit hook run test-hook -- ignored 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook -- three 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook -- four 2>>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431851","messageId":"patch-v4-12.36-ac875d284da-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 12/36] hooks: convert 'post-checkout' hook to hook library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:38Z","receivedAt":"2021-08-03T19:39:34Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the running of the 'post-checkout' hook away from run-command.h\nto the new hook.h library. For \"worktree\" this requires a change to it\nto run the hooks from a given directory.\n\nWe could strictly speaking skip the \"absolute_path\" flag and just\ncheck if \"dir\" is specified, but let's split them up for clarity, as\nwell as for any future user who'd like to set \"dir\" but not implicitly\nchange the argument to an absolute path.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/checkout.c | 14 +++++++++-----\n builtin/clone.c    |  6 ++++--\n builtin/worktree.c | 28 ++++++++++++----------------\n hook.c             |  9 +++++++++\n hook.h             |  8 ++++++++\n read-cache.c       |  1 +\n reset.c            | 14 ++++++++++----\n 7 files changed, 53 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f4cd7747d35..6d69b4c0113 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"diff.h\"\n #include \"dir.h\"\n+#include \"hook.h\"\n #include \"ll-merge.h\"\n #include \"lockfile.h\"\n #include \"merge-recursive.h\"\n@@ -106,13 +107,16 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\treturn run_hook_le(NULL, \"post-checkout\",\n-\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n-\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n-\t\t\t   changed ? \"1\" : \"0\", NULL);\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\n-\n+\tstrvec_pushl(&opt.args,\n+\t\t     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n+\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n+\t\t     changed ? \"1\" : \"0\",\n+\t\t     NULL);\n+\treturn run_hooks_oneshot(\"post-checkout\", &opt);\n }\n \n static int update_some(const struct object_id *oid, struct strbuf *base,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 66fe66679c8..27fc05ee511 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -32,6 +32,7 @@\n #include \"connected.h\"\n #include \"packfile.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"hook.h\"\n \n /*\n  * Overall FIXMEs:\n@@ -775,6 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tif (option_no_checkout)\n \t\treturn 0;\n@@ -820,8 +822,8 @@ static int checkout(int submodule_progress)\n \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n \t\tdie(_(\"unable to write new index file\"));\n \n-\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(null_oid()),\n-\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n+\tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n+\terr |= run_hooks_oneshot(\"post-checkout\", &hook_opt);\n \n \tif (!err && (option_recurse_submodules.nr > 0)) {\n \t\tstruct strvec args = STRVEC_INIT;\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex d22ece93e1a..330867c19bf 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -382,22 +382,18 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tconst char *hook = find_hook(\"post-checkout\");\n-\t\tif (hook) {\n-\t\t\tconst char *env[] = { \"GIT_DIR\", \"GIT_WORK_TREE\", NULL };\n-\t\t\tcp.git_cmd = 0;\n-\t\t\tcp.no_stdin = 1;\n-\t\t\tcp.stdout_to_stderr = 1;\n-\t\t\tcp.dir = path;\n-\t\t\tcp.env = env;\n-\t\t\tcp.argv = NULL;\n-\t\t\tcp.trace2_hook_name = \"post-checkout\";\n-\t\t\tstrvec_pushl(&cp.args, absolute_path(hook),\n-\t\t\t\t     oid_to_hex(null_oid()),\n-\t\t\t\t     oid_to_hex(&commit->object.oid),\n-\t\t\t\t     \"1\", NULL);\n-\t\t\tret = run_command(&cp);\n-\t\t}\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(null_oid()),\n+\t\t\t     oid_to_hex(&commit->object.oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\topt.dir = path;\n+\t\topt.absolute_path = 1;\n+\n+\t\tret = run_hooks_oneshot(\"post-checkout\", &opt);\n \t}\n \n \tstrvec_clear(&child_env);\ndiff --git a/hook.c b/hook.c\nindex 981a9bf46e6..5ecf79c8397 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -92,6 +92,7 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\n+\tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n \tstrvec_push(&cp->args, run_me->hook_path);\n@@ -145,6 +146,7 @@ static int notify_hook_finished(int result,\n int run_hooks(const char *hook_name, const char *hook_path,\n \t      struct run_hooks_opt *options)\n {\n+\tstruct strbuf abs_path = STRBUF_INIT;\n \tstruct hook my_hook = {\n \t\t.hook_path = hook_path,\n \t};\n@@ -158,6 +160,10 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n+\tif (options->absolute_path) {\n+\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n+\t\tmy_hook.hook_path = abs_path.buf;\n+\t}\n \tcb_data.run_me = &my_hook;\n \n \trun_processes_parallel_tr2(jobs,\n@@ -168,6 +174,9 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n \n+\tif (options->absolute_path)\n+\t\tstrbuf_release(&abs_path);\n+\n \treturn cb_data.rc;\n }\n \ndiff --git a/hook.h b/hook.h\nindex 2201ab9d568..53ea3a9649e 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -28,6 +28,14 @@ struct run_hooks_opt\n \n \t/* Args to be passed to each hook */\n \tstruct strvec args;\n+\n+\t/* Resolve and run the \"absolute_path(hook)\" instead of\n+\t * \"hook\". Used for \"git worktree\" hooks\n+\t */\n+\tint absolute_path;\n+\n+\t/* Path to initial working directory for subprocess */\n+\tconst char *dir;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/read-cache.c b/read-cache.c\nindex 99a174b91e6..c9e2b013972 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -28,6 +28,7 @@\n #include \"sparse-index.h\"\n #include \"csum-file.h\"\n #include \"promisor-remote.h\"\n+#include \"hook.h\"\n \n /* Mask for the name length in ce_flags in the on-disk index */\n \ndiff --git a/reset.c b/reset.c\nindex 4bea758053b..6499bc5127d 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -7,6 +7,7 @@\n #include \"tree-walk.h\"\n #include \"tree.h\"\n #include \"unpack-trees.h\"\n+#include \"hook.h\"\n \n int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t       const char *switch_to_branch, unsigned flags,\n@@ -126,10 +127,15 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\tret = create_symref(\"HEAD\", switch_to_branch,\n \t\t\t\t\t    reflog_head);\n \t}\n-\tif (run_hook)\n-\t\trun_hook_le(NULL, \"post-checkout\",\n-\t\t\t    oid_to_hex(orig ? orig : null_oid()),\n-\t\t\t    oid_to_hex(oid), \"1\", NULL);\n+\tif (run_hook) {\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(orig ? orig : null_oid()),\n+\t\t\t     oid_to_hex(oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\trun_hooks_oneshot(\"post-checkout\", &opt);\n+\t}\n \n leave_reset_head:\n \tstrbuf_release(&msg);\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431852","messageId":"patch-v4-09.36-2b8500aa675-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 09/36] gc: use hook library for pre-auto-gc hook","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:35Z","receivedAt":"2021-08-03T19:39:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the pre-auto-gc hook away from run-command.h to and over to the\nnew hook.h library.\n\nTo do this introduce a simple run_hooks_oneshot() wrapper, we'll be\nusing it extensively for these simple cases of wanting to run a single\nhook under a given name, and having it free the memory we allocate for\nus.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c |  3 ++-\n hook.c       | 21 +++++++++++++++++++++\n hook.h       | 13 +++++++++++++\n 3 files changed, 36 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex f05d2f0a1ac..2f74cf394d3 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -32,6 +32,7 @@\n #include \"remote.h\"\n #include \"object-store.h\"\n #include \"exec-cmd.h\"\n+#include \"hook.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -394,7 +395,7 @@ static int need_to_gc(void)\n \telse\n \t\treturn 0;\n \n-\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n+\tif (run_hooks_oneshot(\"pre-auto-gc\", NULL))\n \t\treturn 0;\n \treturn 1;\n }\ndiff --git a/hook.c b/hook.c\nindex c8ff3975043..981a9bf46e6 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -170,3 +170,24 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \n \treturn cb_data.rc;\n }\n+\n+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n+{\n+\tconst char *hook_path;\n+\tint ret;\n+\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n+\n+\tif (!options)\n+\t\toptions = &hook_opt_scratch;\n+\n+\thook_path = find_hook(hook_name);\n+\tif (!hook_path) {\n+\t\tret = 0;\n+\t\tgoto cleanup;\n+\t}\n+\n+\tret = run_hooks(hook_name, hook_path, options);\n+cleanup:\n+\trun_hooks_opt_clear(options);\n+\treturn ret;\n+}\ndiff --git a/hook.h b/hook.h\nindex 361984c69d4..2201ab9d568 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -51,7 +51,20 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);\n /**\n  * Takes an already resolved hook found via find_hook() and runs\n  * it. Does not call run_hooks_opt_clear() for you.\n+ *\n+ * See run_hooks_oneshot() for the simpler one-shot API.\n  */\n int run_hooks(const char *hookname, const char *hook_path,\n \t      struct run_hooks_opt *options);\n+\n+/**\n+ * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n+ * with run_hooks().\n+ *\n+ * If \"options\" is provided calls run_hooks_opt_clear() on it for\n+ * you. If \"options\" is NULL a scratch one will be provided for you\n+ * before calling run_hooks().\n+ */\n+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n+\n #endif\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431853","messageId":"patch-v4-10.36-3ee55d2c10f-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 10/36] rebase: convert pre-rebase to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:36Z","receivedAt":"2021-08-03T19:39:36Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the pre-rebase hook away from run-command.h to and over to the\nnew hook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/rebase.c | 6 ++++--\n 1 file changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 12f093121d9..e7c668c99b1 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -28,6 +28,7 @@\n #include \"sequencer.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_REFLOG_ACTION \"rebase\"\n \n@@ -1313,6 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\n@@ -2022,9 +2024,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t}\n \n \t/* If a hook exists, give it a chance to interrupt*/\n+\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n \tif (!ok_to_skip_pre_rebase &&\n-\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n-\t\t\targc ? argv[0] : NULL, NULL))\n+\t    run_hooks_oneshot(\"pre-rebase\", &hook_opt))\n \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n \n \tif (options.flags & REBASE_DIFFSTAT) {\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431854","messageId":"patch-v4-11.36-050f20d14f0-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 11/36] am: convert applypatch to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:37Z","receivedAt":"2021-08-03T19:39:39Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach pre-applypatch, post-applypatch, and applypatch-msg to use the\nhook.h library instead of the run-command.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 8 +++++---\n 1 file changed, 5 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex c603f3cebdf..e444b18b64a 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -446,9 +446,11 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tassert(state->msg);\n-\tret = run_hook_le(NULL, \"applypatch-msg\", am_path(state, \"final-commit\"), NULL);\n+\tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n+\tret = run_hooks_oneshot(\"applypatch-msg\", &opt);\n \n \tif (!ret) {\n \t\tFREE_AND_NULL(state->msg);\n@@ -1609,7 +1611,7 @@ static void do_commit(const struct am_state *state)\n \tconst char *reflog_msg, *author, *committer = NULL;\n \tstruct strbuf sb = STRBUF_INIT;\n \n-\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n+\tif (run_hooks_oneshot(\"pre-applypatch\", NULL))\n \t\texit(1);\n \n \tif (write_cache_as_tree(&tree, 0, NULL))\n@@ -1661,7 +1663,7 @@ static void do_commit(const struct am_state *state)\n \t\tfclose(fp);\n \t}\n \n-\trun_hook_le(NULL, \"post-applypatch\", NULL);\n+\trun_hooks_oneshot(\"post-applypatch\", NULL);\n \n \tstrbuf_release(&sb);\n }\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431855","messageId":"patch-v4-13.36-69763bc2255-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 13/36] merge: convert post-merge to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:39Z","receivedAt":"2021-08-03T19:39:40Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach post-merge to use the hook.h library instead of the\nrun-command.h library to run hooks.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/merge.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 03f244dd5a0..4965df2ac29 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,6 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\n@@ -489,7 +490,8 @@ static void finish(struct commit *head_commit,\n \t}\n \n \t/* Run a post-merge hook */\n-\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n+\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n+\trun_hooks_oneshot(\"post-merge\", &opt);\n \n \tapply_autostash(git_path_merge_autostash(the_repository));\n \tstrbuf_release(&reflog_message);\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431856","messageId":"patch-v4-14.36-2ca1ca1b8e4-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 14/36] git hook run: add an --ignore-missing flag","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:40Z","receivedAt":"2021-08-03T19:39:42Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"For certain one-shot hooks we'd like to optimistically run them, and\nnot complain if they don't exist. This will be used by send-email in a\nsubsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt | 10 +++++++++-\n builtin/hook.c             |  7 ++++++-\n t/t1800-hook.sh            |  5 +++++\n 3 files changed, 20 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 660d6a992a0..097fb9de63b 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run <hook-name> [-- <hook-args>]\n+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -29,6 +29,14 @@ optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n arguments (if any) differ by hook name, see linkgit:githooks[5] for\n what those are.\n \n+OPTIONS\n+-------\n+\n+--ignore-missing::\n+\tIgnore any missing hook by quietly returning zero. Used for\n+\ttools that want to do a blind one-shot run of a hook that may\n+\tor may not be present.\n+\n SEE ALSO\n --------\n linkgit:githooks[5]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 41dd15550cf..f33db9953c7 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -7,7 +7,7 @@\n #include \"strvec.h\"\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n-\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n+\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n \n static const char * const builtin_hook_usage[] = {\n \tBUILTIN_HOOK_RUN_USAGE,\n@@ -23,9 +23,12 @@ static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint ignore_missing = 0;\n \tconst char *hook_name;\n \tconst char *hook_path;\n \tstruct option run_options[] = {\n+\t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n+\t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n \t\tOPT_END(),\n \t};\n \tint ret;\n@@ -53,6 +56,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \tgit_config(git_default_config, NULL);\n \n \thook_name = argv[0];\n+\tif (ignore_missing)\n+\t\treturn run_hooks_oneshot(hook_name, &opt);\n \thook_path = find_hook(hook_name);\n \tif (!hook_path) {\n \t\terror(\"cannot find a hook named %s\", hook_name);\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 644df0a583c..49df5a2cdfb 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -25,6 +25,11 @@ test_expect_success 'git hook run: nonexistent hook' '\n \ttest_cmp stderr.expect stderr.actual\n '\n \n+test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '\n+\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n+\ttest_must_be_empty stderr.actual\n+'\n+\n test_expect_success 'git hook run: basic' '\n \twrite_script .git/hooks/test-hook <<-EOF &&\n \techo Test hook\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431857","messageId":"patch-v4-15.36-5b66b04bec7-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 15/36] send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:41Z","receivedAt":"2021-08-03T19:39:45Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nChange the \"sendmail-validate\" hook to be run via the \"git hook run\"\nwrapper instead of via a direct invocation.\n\nThis is the smallest possibly change to get \"send-email\" using \"git\nhook run\". We still check the hook itself with \"-x\", and set a\n\"GIT_DIR\" variable, both of which are asserted by our tests. We'll\nneed to get rid of this special behavior if we start running N hooks,\nbut for now let's be as close to bug-for-bug compatible as possible.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl   | 20 ++++++++++++--------\n t/t9001-send-email.sh |  4 ++--\n 2 files changed, 14 insertions(+), 10 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex e65d969d0bb..126850d974b 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -195,13 +195,13 @@ sub format_2822_time {\n my $editor;\n \n sub system_or_msg {\n-\tmy ($args, $msg) = @_;\n+\tmy ($args, $msg, $cmd_name) = @_;\n \tsystem(@$args);\n \tmy $signalled = $? & 127;\n \tmy $exit_code = $? >> 8;\n \treturn unless $signalled or $exit_code;\n \n-\tmy @sprintf_args = ($args->[0], $exit_code);\n+\tmy @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);\n \tif (defined $msg) {\n \t\t# Quiet the 'redundant' warning category, except we\n \t\t# need to support down to Perl 5.8, so we can't do a\n@@ -2031,10 +2031,10 @@ sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n \tif ($repo) {\n+\t\tmy $hook_name = 'sendemail-validate';\n \t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n \t\trequire File::Spec;\n-\t\tmy $validate_hook = File::Spec->catfile($hooks_path,\n-\t\t\t\t\t    'sendemail-validate');\n+\t\tmy $validate_hook = File::Spec->catfile($hooks_path, $hook_name);\n \t\tmy $hook_error;\n \t\tif (-x $validate_hook) {\n \t\t\trequire Cwd;\n@@ -2044,13 +2044,17 @@ sub validate_patch {\n \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n \t\t\t\tor die(\"chdir: $!\");\n \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n+\t\t\tmy @validate_hook = (\"git\", \"hook\", \"run\", \"--ignore-missing\", $hook_name, \"--\", $target);\n+\t\t\t$hook_error = system_or_msg(\\@validate_hook, undef,\n+\t\t\t\t\t\t       \"git hook run $hook_name -- <patch>\");\n \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n \t\t}\n \t\tif ($hook_error) {\n-\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n-\t\t\t\t       \"%s\\n\" .\n-\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n+\t\t\t$hook_error = sprintf(__(\"fatal: %s: rejected by %s hook\\n\" .\n+\t\t\t\t\t\t $hook_error . \"\\n\" .\n+\t\t\t\t\t\t \"warning: no patches were sent\\n\"),\n+\t\t\t\t\t      $fn, $hook_name);\n+\t\t\tdie $hook_error;\n \t\t}\n \t}\n \ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 57fc10e7f82..9ec7d75f0ff 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -539,7 +539,7 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'my-hooks/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -558,7 +558,7 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'$hooks_path/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431858","messageId":"patch-v4-16.36-14a37a43db2-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 16/36] git-p4: use 'git hook' to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:42Z","receivedAt":"2021-08-03T19:39:46Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nInstead of duplicating the behavior of run-command.h:run_hook_le() in\nPython, we can directly call 'git hook run'. We emulate the existence\ncheck with the --ignore-missing flag.\n\nAs this is the last hook execution in git.git to not go through \"git\nhook run\" or the hook.[ch] library we can now be absolutely sure that\nour assertion in hook.c that only hooks known by the generated (from\ngithooks(5)) hook-list.h are permitted.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-p4.py | 72 ++++++-------------------------------------------------\n 1 file changed, 7 insertions(+), 65 deletions(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex 2b4500226aa..1f24cbf0bca 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -207,71 +207,13 @@ def decode_path(path):\n         return path\n \n def run_git_hook(cmd, param=[]):\n-    \"\"\"Execute a hook if the hook exists.\"\"\"\n-    if verbose:\n-        sys.stderr.write(\"Looking for hook: %s\\n\" % cmd)\n-        sys.stderr.flush()\n-\n-    hooks_path = gitConfig(\"core.hooksPath\")\n-    if len(hooks_path) <= 0:\n-        hooks_path = os.path.join(os.environ[\"GIT_DIR\"], \"hooks\")\n-\n-    if not isinstance(param, list):\n-        param=[param]\n-\n-    # resolve hook file name, OS depdenent\n-    hook_file = os.path.join(hooks_path, cmd)\n-    if platform.system() == 'Windows':\n-        if not os.path.isfile(hook_file):\n-            # look for the file with an extension\n-            files = glob.glob(hook_file + \".*\")\n-            if not files:\n-                return True\n-            files.sort()\n-            hook_file = files.pop()\n-            while hook_file.upper().endswith(\".SAMPLE\"):\n-                # The file is a sample hook. We don't want it\n-                if len(files) > 0:\n-                    hook_file = files.pop()\n-                else:\n-                    return True\n-\n-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):\n-        return True\n-\n-    return run_hook_command(hook_file, param) == 0\n-\n-def run_hook_command(cmd, param):\n-    \"\"\"Executes a git hook command\n-       cmd = the command line file to be executed. This can be\n-       a file that is run by OS association.\n-\n-       param = a list of parameters to pass to the cmd command\n-\n-       On windows, the extension is checked to see if it should\n-       be run with the Git for Windows Bash shell.  If there\n-       is no file extension, the file is deemed a bash shell\n-       and will be handed off to sh.exe. Otherwise, Windows\n-       will be called with the shell to handle the file assocation.\n-\n-       For non Windows operating systems, the file is called\n-       as an executable.\n-    \"\"\"\n-    cli = [cmd] + param\n-    use_shell = False\n-    if platform.system() == 'Windows':\n-        (root,ext) = os.path.splitext(cmd)\n-        if ext == \"\":\n-            exe_path = os.environ.get(\"EXEPATH\")\n-            if exe_path is None:\n-                exe_path = \"\"\n-            else:\n-                exe_path = os.path.join(exe_path, \"bin\")\n-            cli = [os.path.join(exe_path, \"SH.EXE\")] + cli\n-        else:\n-            use_shell = True\n-    return subprocess.call(cli, shell=use_shell)\n-\n+    \"\"\"args are specified with -a <arg> -a <arg> -a <arg>\"\"\"\n+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]\n+    if param:\n+        args.append(\"--\")\n+        for p in param:\n+            args.append(p)\n+    return subprocess.call(args) == 0\n \n def write_pipe(c, stdin):\n     if verbose:\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431859","messageId":"patch-v4-17.36-ad5d0e0e7de-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 17/36] commit: convert {pre-commit,prepare-commit-msg} hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:43Z","receivedAt":"2021-08-03T19:39:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove these hooks hook away from run-command.h to and over to the new\nhook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n commit.c | 15 ++++++++-------\n 1 file changed, 8 insertions(+), 7 deletions(-)\n\ndiff --git a/commit.c b/commit.c\nindex 143f472c0f2..63d7943a86d 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -21,6 +21,7 @@\n #include \"commit-reach.h\"\n #include \"run-command.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);\n \n@@ -1698,22 +1699,22 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    const char *name, ...)\n {\n-\tstruct strvec hook_env = STRVEC_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tva_list args;\n-\tint ret;\n+\tconst char *arg;\n \n-\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n+\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\n \t * Let the hook know that no editor will be launched.\n \t */\n \tif (!editor_is_used)\n-\t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n+\t\tstrvec_push(&opt.env, \"GIT_EDITOR=:\");\n \n \tva_start(args, name);\n-\tret = run_hook_ve(hook_env.v, name, args);\n+\twhile ((arg = va_arg(args, const char *)))\n+\t\tstrvec_push(&opt.args, arg);\n \tva_end(args);\n-\tstrvec_clear(&hook_env);\n \n-\treturn ret;\n+\treturn run_hooks_oneshot(name, &opt);\n }\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431860","messageId":"patch-v4-18.36-3d3a33e2674-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 18/36] read-cache: convert post-index-change to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:44Z","receivedAt":"2021-08-03T19:39:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the post-index-change hook away from run-command.h to and over to\nthe new hook.h library.\n\nThis removes the last direct user of run_hook_ve(), so we can make the\nfunction static now. It'll be removed entirely soon.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n read-cache.c  | 10 +++++++---\n run-command.c |  2 +-\n run-command.h |  1 -\n 3 files changed, 8 insertions(+), 5 deletions(-)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex c9e2b013972..90099ca14df 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3068,6 +3068,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tret = convert_to_sparse(istate);\n \n@@ -3096,9 +3097,12 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \telse\n \t\tret = close_lock_file_gently(lock);\n \n-\trun_hook_le(NULL, \"post-index-change\",\n-\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n-\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n+\tstrvec_pushl(&hook_opt.args,\n+\t\t     istate->updated_workdir ? \"1\" : \"0\",\n+\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trun_hooks_oneshot(\"post-index-change\", &hook_opt);\n+\n \tistate->updated_workdir = 0;\n \tistate->updated_skipworktree = 0;\n \ndiff --git a/run-command.c b/run-command.c\nindex 352f5be1646..b4341ba1c7b 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1320,7 +1320,7 @@ int async_with_fork(void)\n #endif\n }\n \n-int run_hook_ve(const char *const *env, const char *name, va_list args)\n+static int run_hook_ve(const char *const *env, const char *name, va_list args)\n {\n \tstruct child_process hook = CHILD_PROCESS_INIT;\n \tconst char *p;\ndiff --git a/run-command.h b/run-command.h\nindex f76b740f927..7a867d41217 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -219,7 +219,6 @@ int run_command(struct child_process *);\n  */\n LAST_ARG_MUST_BE_NULL\n int run_hook_le(const char *const *env, const char *name, ...);\n-int run_hook_ve(const char *const *env, const char *name, va_list args);\n \n /*\n  * Trigger an auto-gc\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431861","messageId":"patch-v4-19.36-893f8666301-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 19/36] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:45Z","receivedAt":"2021-08-03T19:39:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the push-to-checkout hook away from run-command.h to and over to\nthe new hook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 7 +++++--\n 1 file changed, 5 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 91fa799b66e..a7d03bbc7d3 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1435,9 +1435,12 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n-\tif (run_hook_le(env->v, push_to_checkout_hook,\n-\t\t\thash_to_hex(hash), NULL))\n+\tstrvec_pushv(&opt.env, env->v);\n+\tstrvec_push(&opt.args, hash_to_hex(hash));\n+\tif (run_hooks_oneshot(push_to_checkout_hook, &opt))\n \t\treturn \"push-to-checkout hook declined\";\n \telse\n \t\treturn NULL;\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431862","messageId":"patch-v4-20.36-070433deba5-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 20/36] run-command: remove old run_hook_{le,ve}() hook API","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:46Z","receivedAt":"2021-08-03T19:39:51Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nThe new hook.h library has replaced all run-command.h hook-related\nfunctionality. So let's delete this dead code.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n run-command.c | 32 --------------------------------\n run-command.h | 16 ----------------\n 2 files changed, 48 deletions(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex b4341ba1c7b..1399243de8a 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1320,38 +1320,6 @@ int async_with_fork(void)\n #endif\n }\n \n-static int run_hook_ve(const char *const *env, const char *name, va_list args)\n-{\n-\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\tconst char *p;\n-\n-\tp = find_hook(name);\n-\tif (!p)\n-\t\treturn 0;\n-\n-\tstrvec_push(&hook.args, p);\n-\twhile ((p = va_arg(args, const char *)))\n-\t\tstrvec_push(&hook.args, p);\n-\thook.env = env;\n-\thook.no_stdin = 1;\n-\thook.stdout_to_stderr = 1;\n-\thook.trace2_hook_name = name;\n-\n-\treturn run_command(&hook);\n-}\n-\n-int run_hook_le(const char *const *env, const char *name, ...)\n-{\n-\tva_list args;\n-\tint ret;\n-\n-\tva_start(args, name);\n-\tret = run_hook_ve(env, name, args);\n-\tva_end(args);\n-\n-\treturn ret;\n-}\n-\n struct io_pump {\n \t/* initialized by caller */\n \tint fd;\ndiff --git a/run-command.h b/run-command.h\nindex 7a867d41217..cfb6887e4ae 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -204,22 +204,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/**\n- * Run a hook.\n- * The first argument is a pathname to an index file, or NULL\n- * if the hook uses the default index file or no index is needed.\n- * The second argument is the name of the hook.\n- * The further arguments correspond to the hook arguments.\n- * The last argument has to be NULL to terminate the arguments list.\n- * If the hook does not exist or is not executable, the return\n- * value will be zero.\n- * If it is executable, the hook will be executed and the exit\n- * status of the hook is returned.\n- * On execution, .stdout_to_stderr and .no_stdin will be set.\n- */\n-LAST_ARG_MUST_BE_NULL\n-int run_hook_le(const char *const *env, const char *name, ...);\n-\n /*\n  * Trigger an auto-gc\n  */\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431863","messageId":"patch-v4-21.36-1028e0c1667-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 21/36] run-command: allow stdin for run_processes_parallel","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:47Z","receivedAt":"2021-08-03T19:39:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nWhile it makes sense not to inherit stdin from the parent process to\navoid deadlocking, it's not necessary to completely ban stdin to\nchildren. An informed user should be able to configure stdin safely. By\nsetting `some_child.process.no_stdin=1` before calling `get_next_task()`\nwe provide a reasonable default behavior but enable users to set up\nstdin streaming for themselves during the callback.\n\n`some_child.process.stdout_to_stderr`, however, remains unmodifiable by\n`get_next_task()` - the rest of the run_processes_parallel() API depends\non child output in stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n run-command.c | 9 ++++++++-\n 1 file changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex 1399243de8a..482ee2d76c6 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1627,6 +1627,14 @@ static int pp_start_one(struct parallel_processes *pp)\n \tif (i == pp->max_processes)\n \t\tBUG(\"bookkeeping is hard\");\n \n+\t/*\n+\t * By default, do not inherit stdin from the parent process - otherwise,\n+\t * all children would share stdin! Users may overwrite this to provide\n+\t * something to the child's stdin by having their 'get_next_task'\n+\t * callback assign 0 to .no_stdin and an appropriate integer to .in.\n+\t */\n+\tpp->children[i].process.no_stdin = 1;\n+\n \tcode = pp->get_next_task(&pp->children[i].process,\n \t\t\t\t &pp->children[i].err,\n \t\t\t\t pp->data,\n@@ -1638,7 +1646,6 @@ static int pp_start_one(struct parallel_processes *pp)\n \t}\n \tpp->children[i].process.err = -1;\n \tpp->children[i].process.stdout_to_stderr = 1;\n-\tpp->children[i].process.no_stdin = 1;\n \n \tif (start_command(&pp->children[i].process)) {\n \t\tcode = pp->start_failure(&pp->children[i].err,\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431864","messageId":"patch-v4-22.36-639e59e9ed0-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 22/36] hook: support passing stdin to hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:48Z","receivedAt":"2021-08-03T19:39:52Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome hooks (such as post-rewrite) need to take input via stdin.\nPreviously, callers provided stdin to hooks by setting\nrun-command.h:child_process.in, which takes a FD. Callers would open the\nfile in question themselves before calling run-command(). However, since\nwe will now need to seek to the front of the file and read it again for\nevery hook which runs, hook.h:run_command() takes a path and handles FD\nmanagement itself. Since this file is opened for read only, it should\nnot prevent later parallel execution support.\n\nOn the frontend, this is supported by asking for a file path, rather\nthan by reading stdin. Reading directly from stdin would involve caching\nthe entire stdin (to memory or to disk) and reading it back from the\nbeginning to each hook. We'd want to support cases like insufficient\nmemory or storage for the file. While this may prove useful later, for\nnow the path of least resistance is to just ask the user to make this\ninterim file themselves.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt |  7 ++++++-\n builtin/hook.c             |  4 +++-\n hook.c                     |  8 +++++++-\n hook.h                     |  3 +++\n t/t1800-hook.sh            | 18 ++++++++++++++++++\n 5 files changed, 37 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 097fb9de63b..fa68c1f3912 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -32,6 +32,11 @@ what those are.\n OPTIONS\n -------\n \n+--to-stdin::\n+\tFor \"run\"; Specify a file which will be streamed into the\n+\thook's stdin. The hook will receive the entire file from\n+\tbeginning to EOF.\n+\n --ignore-missing::\n \tIgnore any missing hook by quietly returning zero. Used for\n \ttools that want to do a blind one-shot run of a hook that may\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex f33db9953c7..27dce6a2f0e 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -7,7 +7,7 @@\n #include \"strvec.h\"\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n-\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n+\tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n \n static const char * const builtin_hook_usage[] = {\n \tBUILTIN_HOOK_RUN_USAGE,\n@@ -29,6 +29,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \tstruct option run_options[] = {\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n+\t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n+\t\t\t   N_(\"file to read into hooks' stdin\")),\n \t\tOPT_END(),\n \t};\n \tint ret;\ndiff --git a/hook.c b/hook.c\nindex 5ecf79c8397..63c9a60921f 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -88,7 +88,13 @@ static int pick_next_hook(struct child_process *cp,\n \tif (!run_me)\n \t\treturn 0;\n \n-\tcp->no_stdin = 1;\n+\t/* reopen the file for stdin; run_command closes it. */\n+\tif (hook_cb->options->path_to_stdin) {\n+\t\tcp->no_stdin = 0;\n+\t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else {\n+\t\tcp->no_stdin = 1;\n+\t}\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\ndiff --git a/hook.h b/hook.h\nindex 53ea3a9649e..cd6a68a3b50 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -36,6 +36,9 @@ struct run_hooks_opt\n \n \t/* Path to initial working directory for subprocess */\n \tconst char *dir;\n+\n+\t/* Path to file which should be piped to stdin for each hook */\n+\tconst char *path_to_stdin;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 49df5a2cdfb..217db848b3f 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -135,4 +135,22 @@ test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'stdin to hooks' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo BEGIN stdin\n+\tcat\n+\techo END stdin\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\tBEGIN stdin\n+\thello\n+\tEND stdin\n+\tEOF\n+\n+\techo hello >input &&\n+\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431865","messageId":"patch-v4-24.36-0c24221b522-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 24/36] run-command: add stdin callback for parallelization","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:50Z","receivedAt":"2021-08-03T19:39:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIf a user of the run_processes_parallel() API wants to pipe a large\namount of information to stdin of each parallel command, that\ninformation could exceed the buffer of the pipe allocated for that\nprocess's stdin.  Generally this is solved by repeatedly writing to\nchild_process.in between calls to start_command() and finish_command();\nrun_processes_parallel() did not provide users an opportunity to access\nchild_process at that time.\n\nBecause the data might be extremely large (for example, a list of all\nrefs received during a push from a client) simply taking a string_list\nor strbuf is not as scalable as using a callback; the rest of the\nrun_processes_parallel() API also uses callbacks, so making this feature\nmatch the rest of the API reduces mental load on the user.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  1 +\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 54 +++++++++++++++++++++++++++++++++++--\n run-command.h               | 17 +++++++++++-\n submodule.c                 |  1 +\n t/helper/test-run-command.c | 31 ++++++++++++++++++---\n t/t0061-run-command.sh      | 30 +++++++++++++++++++++\n 8 files changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 25740c13df1..fef6e85d003 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,6 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n+\t\t\t\t\t\t    NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex f73963ad67d..f42ded548bf 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2294,7 +2294,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure,\n+\t\t\t\t   update_clone_start_failure, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex 63c9a60921f..a7462639d97 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -175,6 +175,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 482ee2d76c6..f1616858d18 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1492,6 +1492,7 @@ struct parallel_processes {\n \n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n+\tfeed_pipe_fn feed_pipe;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1519,6 +1520,13 @@ static int default_start_failure(struct strbuf *out,\n \treturn 0;\n }\n \n+static int default_feed_pipe(struct strbuf *pipe,\n+\t\t\t     void *pp_cb,\n+\t\t\t     void *pp_task_cb)\n+{\n+\treturn 1;\n+}\n+\n static int default_task_finished(int result,\n \t\t\t\t struct strbuf *out,\n \t\t\t\t void *pp_cb,\n@@ -1549,6 +1557,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    int n,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n+\t\t    feed_pipe_fn feed_pipe,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1567,6 +1576,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->get_next_task = get_next_task;\n \n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n+\tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n \n \tpp->nr_processes = 0;\n@@ -1664,6 +1674,37 @@ static int pp_start_one(struct parallel_processes *pp)\n \treturn 0;\n }\n \n+static void pp_buffer_stdin(struct parallel_processes *pp)\n+{\n+\tint i;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t/* Buffer stdin for each pipe. */\n+\tfor (i = 0; i < pp->max_processes; i++) {\n+\t\tif (pp->children[i].state == GIT_CP_WORKING &&\n+\t\t    pp->children[i].process.in > 0) {\n+\t\t\tint done;\n+\t\t\tstrbuf_reset(&sb);\n+\t\t\tdone = pp->feed_pipe(&sb, pp->data,\n+\t\t\t\t\t      pp->children[i].data);\n+\t\t\tif (sb.len) {\n+\t\t\t\tif (write_in_full(pp->children[i].process.in,\n+\t\t\t\t\t      sb.buf, sb.len) < 0) {\n+\t\t\t\t\tif (errno != EPIPE)\n+\t\t\t\t\t\tdie_errno(\"write\");\n+\t\t\t\t\tdone = 1;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif (done) {\n+\t\t\t\tclose(pp->children[i].process.in);\n+\t\t\t\tpp->children[i].process.in = 0;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&sb);\n+}\n+\n static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n {\n \tint i;\n@@ -1728,6 +1769,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\tpp->nr_processes--;\n \t\tpp->children[i].state = GIT_CP_FREE;\n \t\tpp->pfd[i].fd = -1;\n+\t\tpp->children[i].process.in = 0;\n \t\tchild_process_init(&pp->children[i].process);\n \n \t\tif (i != pp->output_owner) {\n@@ -1761,6 +1803,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n+\t\t\t   feed_pipe_fn feed_pipe,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1769,7 +1812,9 @@ int run_processes_parallel(int n,\n \tint spawn_cap = 4;\n \tstruct parallel_processes pp;\n \n-\tpp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb);\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1786,6 +1831,7 @@ int run_processes_parallel(int n,\n \t\t}\n \t\tif (!pp.nr_processes)\n \t\t\tbreak;\n+\t\tpp_buffer_stdin(&pp);\n \t\tpp_buffer_stderr(&pp, output_timeout);\n \t\tpp_output(&pp);\n \t\tcode = pp_collect_finished(&pp);\n@@ -1797,11 +1843,15 @@ int run_processes_parallel(int n,\n \t}\n \n \tpp_cleanup(&pp);\n+\n+\tsigchain_pop(SIGPIPE);\n+\n \treturn 0;\n }\n \n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n+\t\t\t       feed_pipe_fn feed_pipe,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1811,7 +1861,7 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\ttask_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex cfb6887e4ae..80d394664ae 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -422,6 +422,20 @@ typedef int (*start_failure_fn)(struct strbuf *out,\n \t\t\t\tvoid *pp_cb,\n \t\t\t\tvoid *pp_task_cb);\n \n+/**\n+ * This callback is called repeatedly on every child process who requests\n+ * start_command() to create a pipe by setting child_process.in < 0.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel, and\n+ * pp_task_cb is the callback cookie as passed into get_next_task_fn.\n+ * The contents of 'send' will be read into the pipe and passed to the pipe.\n+ *\n+ * Return nonzero to close the pipe.\n+ */\n+typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n+\t\t\t    void *pp_cb,\n+\t\t\t    void *pp_task_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -456,10 +470,11 @@ typedef int (*task_finished_fn)(int result,\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n+\t\t\t   feed_pipe_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n /**\ndiff --git a/submodule.c b/submodule.c\nindex 8e611fe1dbf..db1700a502d 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1632,6 +1632,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 7ae03dc7123..9348184d303 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,\n \t\treturn 0;\n \n \tstrvec_pushv(&cp->args, d->argv);\n+\tcp->in = d->in;\n+\tcp->no_stdin = d->no_stdin;\n \tstrbuf_addstr(err, \"preloaded output of a child\\n\");\n \tnumber_callbacks++;\n+\n+\t*task_cb = xmalloc(sizeof(int));\n+\t*(int*)(*task_cb) = 2;\n \treturn 1;\n }\n \n@@ -55,6 +60,17 @@ static int task_finished(int result,\n \treturn 1;\n }\n \n+static int test_stdin(struct strbuf *pipe, void *cb, void *task_cb)\n+{\n+\tint *lines_remaining = task_cb;\n+\n+\tif (*lines_remaining)\n+\t\tstrbuf_addf(pipe, \"sample stdin %d\\n\", --(*lines_remaining));\n+\n+\treturn !(*lines_remaining);\n+}\n+\n+\n struct testsuite {\n \tstruct string_list tests, failed;\n \tint next;\n@@ -185,7 +201,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_finished, &suite);\n+\t\t\t\t     test_stdin, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -413,15 +429,22 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\n+\tif (!strcmp(argv[1], \"run-command-stdin\")) {\n+\t\tproc.in = -1;\n+\t\tproc.no_stdin = 0;\n+\t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n+\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t}\n \n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 7d599675e35..87759482ad1 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,36 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+cat >expect <<-EOF\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+EOF\n+\n+test_expect_success 'run_command listens to stdin' '\n+\twrite_script stdin-script <<-\\EOF &&\n+\techo \"listening for stdin:\"\n+\twhile read line; do\n+\t\techo \"$line\"\n+\tdone\n+\tEOF\n+\ttest-tool run-command run-command-stdin 2 ./stdin-script 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n asking for a quick stop\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431866","messageId":"patch-v4-23.36-7d1925cca48-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 23/36] am: convert 'post-rewrite' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:49Z","receivedAt":"2021-08-03T19:39:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 20 ++++----------------\n 1 file changed, 4 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e444b18b64a..9e3d4d9ab44 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -467,24 +467,12 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tconst char *hook = find_hook(\"post-rewrite\");\n-\tint ret;\n-\n-\tif (!hook)\n-\t\treturn 0;\n-\n-\tstrvec_push(&cp.args, hook);\n-\tstrvec_push(&cp.args, \"rebase\");\n-\n-\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n-\tcp.stdout_to_stderr = 1;\n-\tcp.trace2_hook_name = \"post-rewrite\";\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n-\tret = run_command(&cp);\n+\tstrvec_push(&opt.args, \"rebase\");\n+\topt.path_to_stdin = am_path(state, \"rewritten\");\n \n-\tclose(cp.in);\n-\treturn ret;\n+\treturn run_hooks_oneshot(\"post-rewrite\", &opt);\n }\n \n /**\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431867","messageId":"patch-v4-26.36-4b7175af2e5-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 26/36] hook: convert 'post-rewrite' hook in sequencer.c to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:52Z","receivedAt":"2021-08-03T19:39:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using 'hook.h' for 'post-rewrite', we simplify hook invocations by\nnot needing to put together our own 'struct child_process'.\n\nThe signal handling that's being removed by this commit now takes\nplace in run-command.h:run_processes_parallel(), so it is OK to remove\nthem here.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n sequencer.c | 79 ++++++++++++++++++++++-------------------------------\n 1 file changed, 32 insertions(+), 47 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 9aac08c1545..77f809c00e4 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -35,6 +35,7 @@\n #include \"commit-reach.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"string-list.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -1147,33 +1148,27 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *argv[3];\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n-\tstruct strbuf sb = STRBUF_INIT;\n \n-\targv[0] = find_hook(\"post-rewrite\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tstrvec_push(&opt.args, \"amend\");\n \n-\targv[1] = \"amend\";\n-\targv[2] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"post-rewrite\";\n-\n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n-\tstrbuf_addf(&sb, \"%s %s\\n\", oid_to_hex(oldoid), oid_to_hex(newoid));\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\twrite_in_full(proc.in, sb.buf, sb.len);\n-\tclose(proc.in);\n-\tstrbuf_release(&sb);\n-\tsigchain_pop(SIGPIPE);\n-\treturn finish_command(&proc);\n+\tstrbuf_addf(&tmp,\n+\t\t    \"%s %s\",\n+\t\t    oid_to_hex(oldoid),\n+\t\t    oid_to_hex(newoid));\n+\tstring_list_append(&to_stdin, tmp.buf);\n+\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tcode = run_hooks_oneshot(\"post-rewrite\", &opt);\n+\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n+\treturn code;\n }\n \n void commit_post_rewrite(struct repository *r,\n@@ -4526,30 +4521,20 @@ static int pick_commits(struct repository *r,\n \t\tflush_rewritten_pending();\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n-\t\t\tstruct child_process child = CHILD_PROCESS_INIT;\n-\t\t\tconst char *post_rewrite_hook =\n-\t\t\t\tfind_hook(\"post-rewrite\");\n-\n-\t\t\tchild.in = open(rebase_path_rewritten_list(), O_RDONLY);\n-\t\t\tchild.git_cmd = 1;\n-\t\t\tstrvec_push(&child.args, \"notes\");\n-\t\t\tstrvec_push(&child.args, \"copy\");\n-\t\t\tstrvec_push(&child.args, \"--for-rewrite=rebase\");\n+\t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n+\t\t\tnotes_cp.git_cmd = 1;\n+\t\t\tstrvec_push(&notes_cp.args, \"notes\");\n+\t\t\tstrvec_push(&notes_cp.args, \"copy\");\n+\t\t\tstrvec_push(&notes_cp.args, \"--for-rewrite=rebase\");\n \t\t\t/* we don't care if this copying failed */\n-\t\t\trun_command(&child);\n-\n-\t\t\tif (post_rewrite_hook) {\n-\t\t\t\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\n-\t\t\t\thook.in = open(rebase_path_rewritten_list(),\n-\t\t\t\t\tO_RDONLY);\n-\t\t\t\thook.stdout_to_stderr = 1;\n-\t\t\t\thook.trace2_hook_name = \"post-rewrite\";\n-\t\t\t\tstrvec_push(&hook.args, post_rewrite_hook);\n-\t\t\t\tstrvec_push(&hook.args, \"rebase\");\n-\t\t\t\t/* we don't care if this hook failed */\n-\t\t\t\trun_command(&hook);\n-\t\t\t}\n+\t\t\trun_command(&notes_cp);\n+\n+\t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n+\t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n+\t\t\trun_hooks_oneshot(\"post-rewrite\", &hook_opt);\n \t\t}\n \t\tapply_autostash(rebase_path_autostash());\n \n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431868","messageId":"patch-v4-25.36-05d1085f7eb-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 25/36] hook: provide stdin by string_list or callback","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:51Z","receivedAt":"2021-08-03T19:39:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn cases where a hook requires only a small amount of information via\nstdin, it should be simple for users to provide a string_list alone. But\nin more complicated cases where the stdin is too large to hold in\nmemory, let's instead provide a callback the users can populate line\nafter line.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 33 ++++++++++++++++++++++++++++++++-\n hook.h | 27 +++++++++++++++++++++++++++\n 2 files changed, 59 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex a7462639d97..44e73827800 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -77,6 +77,29 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)\n \tstrvec_clear(&o->args);\n }\n \n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n+{\n+\tint *item_idx;\n+\tstruct hook *ctx = pp_task_cb;\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct string_list *to_pipe = hook_cb->options->feed_pipe_ctx;\n+\n+\t/* Bootstrap the state manager if necessary. */\n+\tif (!ctx->feed_pipe_cb_data) {\n+\t\tctx->feed_pipe_cb_data = xmalloc(sizeof(unsigned int));\n+\t\t*(int*)ctx->feed_pipe_cb_data = 0;\n+\t}\n+\n+\titem_idx = ctx->feed_pipe_cb_data;\n+\n+\tif (*item_idx < to_pipe->nr) {\n+\t\tstrbuf_addf(pipe, \"%s\\n\", to_pipe->items[*item_idx].string);\n+\t\t(*item_idx)++;\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n static int pick_next_hook(struct child_process *cp,\n \t\t\t  struct strbuf *out,\n \t\t\t  void *pp_cb,\n@@ -92,6 +115,10 @@ static int pick_next_hook(struct child_process *cp,\n \tif (hook_cb->options->path_to_stdin) {\n \t\tcp->no_stdin = 0;\n \t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else if (hook_cb->options->feed_pipe) {\n+\t\t/* ask for start_command() to make a pipe for us */\n+\t\tcp->in = -1;\n+\t\tcp->no_stdin = 0;\n \t} else {\n \t\tcp->no_stdin = 1;\n \t}\n@@ -175,7 +202,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->feed_pipe,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n@@ -183,6 +210,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \n \tif (options->absolute_path)\n \t\tstrbuf_release(&abs_path);\n+\tfree(my_hook.feed_pipe_cb_data);\n \n \treturn cb_data.rc;\n }\n@@ -196,6 +224,9 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n \tif (!options)\n \t\toptions = &hook_opt_scratch;\n \n+\tif (options->path_to_stdin && options->feed_pipe)\n+\t\tBUG(\"choose only one method to populate stdin\");\n+\n \thook_path = find_hook(hook_name);\n \tif (!hook_path) {\n \t\tret = 0;\ndiff --git a/hook.h b/hook.h\nindex cd6a68a3b50..b55f283f90b 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -19,6 +19,12 @@ int hook_exists(const char *hookname);\n struct hook {\n \t/* The path to the hook */\n \tconst char *hook_path;\n+\n+\t/*\n+\t * Use this to keep state for your feed_pipe_fn if you are using\n+\t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n+\t */\n+\tvoid *feed_pipe_cb_data;\n };\n \n struct run_hooks_opt\n@@ -39,6 +45,19 @@ struct run_hooks_opt\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n+\n+\t/*\n+\t * Callback and state pointer to ask for more content to pipe to stdin.\n+\t * Will be called repeatedly, for each hook. See\n+\t * hook.c:pipe_from_stdin() for an example. Keep per-hook state in\n+\t * hook.feed_pipe_cb_data (per process). Keep initialization context in\n+\t * feed_pipe_ctx (shared by all processes).\n+\t *\n+\t * See 'pipe_from_string_list()' for info about how to specify a\n+\t * string_list as the stdin input instead of writing your own handler.\n+\t */\n+\tfeed_pipe_fn feed_pipe;\n+\tvoid *feed_pipe_ctx;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -46,6 +65,14 @@ struct run_hooks_opt\n \t.args = STRVEC_INIT, \\\n }\n \n+/*\n+ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n+ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n+ * This will pipe each string in the list to stdin, separated by newlines.  (Do\n+ * not inject your own newlines.)\n+ */\n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n+\n /*\n  * Callback provided to feed_pipe_fn and consume_sideband_fn.\n  */\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431869","messageId":"patch-v4-27.36-3f24e056410-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 27/36] transport: convert pre-push hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:53Z","receivedAt":"2021-08-03T19:40:00Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the pre-push hook away from run-command.h to and over to the new\nhook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n transport.c | 56 ++++++++++++++---------------------------------------\n 1 file changed, 14 insertions(+), 42 deletions(-)\n\ndiff --git a/transport.c b/transport.c\nindex 77e196f75f5..4ca8fc0391d 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1203,63 +1203,35 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)\n static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n-\tint ret = 0, x;\n+\tint ret = 0;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tstruct ref *r;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct strbuf buf;\n-\tconst char *argv[4];\n-\n-\tif (!(argv[0] = find_hook(\"pre-push\")))\n-\t\treturn 0;\n-\n-\targv[1] = transport->remote->name;\n-\targv[2] = transport->url;\n-\targv[3] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.trace2_hook_name = \"pre-push\";\n-\n-\tif (start_command(&proc)) {\n-\t\tfinish_command(&proc);\n-\t\treturn -1;\n-\t}\n+\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\n-\tstrbuf_init(&buf, 256);\n+\tstrvec_push(&opt.args, transport->remote->name);\n+\tstrvec_push(&opt.args, transport->url);\n \n \tfor (r = remote_refs; r; r = r->next) {\n+\t\tstruct strbuf buf = STRBUF_INIT;\n+\n \t\tif (!r->peer_ref) continue;\n \t\tif (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;\n \t\tif (r->status == REF_STATUS_REJECT_STALE) continue;\n \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n \n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n+\t\tstrbuf_addf(&buf, \"%s %s %s %s\",\n \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n \t\t\t r->name, oid_to_hex(&r->old_oid));\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\t/* We do not mind if a hook does not read all refs. */\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n \t}\n \n-\tstrbuf_release(&buf);\n-\n-\tx = close(proc.in);\n-\tif (!ret)\n-\t\tret = x;\n-\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n \n-\tx = finish_command(&proc);\n-\tif (!ret)\n-\t\tret = x;\n+\tret = run_hooks_oneshot(\"pre-push\", &opt);\n+\tto_stdin.strdup_strings = 1;\n+\tstring_list_clear(&to_stdin, 0);\n \n \treturn ret;\n }\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431870","messageId":"patch-v4-28.36-ecf75f33233-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 28/36] hook tests: test for exact \"pre-push\" hook input","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:54Z","receivedAt":"2021-08-03T19:40:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Extend the tests added in ec55559f937 (push: Add support for pre-push\nhooks, 2013-01-13) to exhaustively test for the exact input we're\nexpecting. This helps a parallel series that's refactoring how the\nhook is called, to e.g. make sure that we don't miss a trailing\nnewline.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t5571-pre-push-hook.sh | 23 ++++++++++++++++++-----\n 1 file changed, 18 insertions(+), 5 deletions(-)\n\ndiff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh\nindex ad8d5804f7b..d2857a6fbc0 100755\n--- a/t/t5571-pre-push-hook.sh\n+++ b/t/t5571-pre-push-hook.sh\n@@ -11,7 +11,7 @@ HOOKDIR=\"$(git rev-parse --git-dir)/hooks\"\n HOOK=\"$HOOKDIR/pre-push\"\n mkdir -p \"$HOOKDIR\"\n write_script \"$HOOK\" <<EOF\n-cat >/dev/null\n+cat >actual\n exit 0\n EOF\n \n@@ -20,10 +20,16 @@ test_expect_success 'setup' '\n \tgit init --bare repo1 &&\n \tgit remote add parent1 repo1 &&\n \ttest_commit one &&\n-\tgit push parent1 HEAD:foreign\n+\tcat >expect <<-EOF &&\n+\tHEAD $(git rev-parse HEAD) refs/heads/foreign $(test_oid zero)\n+\tEOF\n+\n+\ttest_when_finished \"rm actual\" &&\n+\tgit push parent1 HEAD:foreign &&\n+\ttest_cmp expect actual\n '\n write_script \"$HOOK\" <<EOF\n-cat >/dev/null\n+cat >actual\n exit 1\n EOF\n \n@@ -32,11 +38,18 @@ export COMMIT1\n \n test_expect_success 'push with failing hook' '\n \ttest_commit two &&\n-\ttest_must_fail git push parent1 HEAD\n+\tcat >expect <<-EOF &&\n+\tHEAD $(git rev-parse HEAD) refs/heads/main $(test_oid zero)\n+\tEOF\n+\n+\ttest_when_finished \"rm actual\" &&\n+\ttest_must_fail git push parent1 HEAD &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success '--no-verify bypasses hook' '\n-\tgit push --no-verify parent1 HEAD\n+\tgit push --no-verify parent1 HEAD &&\n+\ttest_path_is_missing actual\n '\n \n COMMIT2=\"$(git rev-parse HEAD)\"\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431871","messageId":"patch-v4-29.36-2c961be94b4-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 29/36] hook tests: use a modern style for \"pre-push\" tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:55Z","receivedAt":"2021-08-03T19:40:02Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Indent the here-docs and use \"test_cmp\" instead of \"diff\" in tests\nadded in ec55559f937 (push: Add support for pre-push hooks,\n2013-01-13). Let's also use the more typical \"expect\" instead of\n\"expected\" to be consistent with the rest of the test file.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t5571-pre-push-hook.sh | 71 ++++++++++++++++++++--------------------\n 1 file changed, 35 insertions(+), 36 deletions(-)\n\ndiff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh\nindex d2857a6fbc0..6d0d5b854ea 100755\n--- a/t/t5571-pre-push-hook.sh\n+++ b/t/t5571-pre-push-hook.sh\n@@ -61,15 +61,15 @@ echo \"$2\" >>actual\n cat >>actual\n EOF\n \n-cat >expected <<EOF\n-parent1\n-repo1\n-refs/heads/main $COMMIT2 refs/heads/foreign $COMMIT1\n-EOF\n-\n test_expect_success 'push with hook' '\n+\tcat >expected <<-EOF &&\n+\tparent1\n+\trepo1\n+\trefs/heads/main $COMMIT2 refs/heads/foreign $COMMIT1\n+\tEOF\n+\n \tgit push parent1 main:foreign &&\n-\tdiff expected actual\n+\ttest_cmp expected actual\n '\n \n test_expect_success 'add a branch' '\n@@ -80,49 +80,48 @@ test_expect_success 'add a branch' '\n COMMIT3=\"$(git rev-parse HEAD)\"\n export COMMIT3\n \n-cat >expected <<EOF\n-parent1\n-repo1\n-refs/heads/other $COMMIT3 refs/heads/foreign $COMMIT2\n-EOF\n-\n test_expect_success 'push to default' '\n+\tcat >expect <<-EOF &&\n+\tparent1\n+\trepo1\n+\trefs/heads/other $COMMIT3 refs/heads/foreign $COMMIT2\n+\tEOF\n \tgit push &&\n-\tdiff expected actual\n+\ttest_cmp expect actual\n '\n \n-cat >expected <<EOF\n-parent1\n-repo1\n-refs/tags/one $COMMIT1 refs/tags/tag1 $ZERO_OID\n-HEAD~ $COMMIT2 refs/heads/prev $ZERO_OID\n-EOF\n-\n test_expect_success 'push non-branches' '\n+\tcat >expect <<-EOF &&\n+\tparent1\n+\trepo1\n+\trefs/tags/one $COMMIT1 refs/tags/tag1 $ZERO_OID\n+\tHEAD~ $COMMIT2 refs/heads/prev $ZERO_OID\n+\tEOF\n+\n \tgit push parent1 one:tag1 HEAD~:refs/heads/prev &&\n-\tdiff expected actual\n+\ttest_cmp expect actual\n '\n \n-cat >expected <<EOF\n-parent1\n-repo1\n-(delete) $ZERO_OID refs/heads/prev $COMMIT2\n-EOF\n-\n test_expect_success 'push delete' '\n+\tcat >expect <<-EOF &&\n+\tparent1\n+\trepo1\n+\t(delete) $ZERO_OID refs/heads/prev $COMMIT2\n+\tEOF\n+\n \tgit push parent1 :prev &&\n-\tdiff expected actual\n+\ttest_cmp expect actual\n '\n \n-cat >expected <<EOF\n-repo1\n-repo1\n-HEAD $COMMIT3 refs/heads/other $ZERO_OID\n-EOF\n-\n test_expect_success 'push to URL' '\n+\tcat >expect <<-EOF &&\n+\trepo1\n+\trepo1\n+\tHEAD $COMMIT3 refs/heads/other $ZERO_OID\n+\tEOF\n+\n \tgit push repo1 HEAD &&\n-\tdiff expected actual\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'set up many-ref tests' '\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431872","messageId":"patch-v4-30.36-1ce456f9d9d-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 30/36] reference-transaction: use hook.h to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:56Z","receivedAt":"2021-08-03T19:40:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n refs.c | 40 +++++++++++++---------------------------\n 1 file changed, 13 insertions(+), 27 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 6211692eaae..73d4a939267 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2062,47 +2062,33 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct strbuf buf = STRBUF_INIT;\n-\tconst char *hook;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \tint ret = 0, i;\n \n-\thook = find_hook(\"reference-transaction\");\n-\tif (!hook)\n-\t\treturn ret;\n-\n-\tstrvec_pushl(&proc.args, hook, state, NULL);\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"reference-transaction\";\n-\n-\tret = start_command(&proc);\n-\tif (ret)\n+\tif (!hook_exists(\"reference-transaction\"))\n \t\treturn ret;\n \n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstrvec_push(&opt.args, state);\n \n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n+\t\tstruct strbuf buf = STRBUF_INIT;\n \n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n+\t\tstrbuf_addf(&buf, \"%s %s %s\",\n \t\t\t    oid_to_hex(&update->old_oid),\n \t\t\t    oid_to_hex(&update->new_oid),\n \t\t\t    update->refname);\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n \t}\n \n-\tclose(proc.in);\n-\tsigchain_pop(SIGPIPE);\n-\tstrbuf_release(&buf);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tret = run_hooks_oneshot(\"reference-transaction\", &opt);\n+\tto_stdin.strdup_strings = 1;\n+\tstring_list_clear(&to_stdin, 0);\n \n-\tret |= finish_command(&proc);\n \treturn ret;\n }\n \n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431873","messageId":"patch-v4-32.36-0b6e9c6d07a-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 32/36] hooks: allow callers to capture output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:58Z","receivedAt":"2021-08-03T19:40:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome server-side hooks will require capturing output to send over\nsideband instead of printing directly to stderr. Expose that capability.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 3 ++-\n hook.h | 8 ++++++++\n 2 files changed, 10 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 9ae3007cdd3..efed5f73b4d 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -203,12 +203,13 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->consume_sideband,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n \n+\n \tif (options->absolute_path)\n \t\tstrbuf_release(&abs_path);\n \tfree(my_hook.feed_pipe_cb_data);\ndiff --git a/hook.h b/hook.h\nindex b55f283f90b..37a9690c2ca 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -58,6 +58,14 @@ struct run_hooks_opt\n \t */\n \tfeed_pipe_fn feed_pipe;\n \tvoid *feed_pipe_ctx;\n+\n+\t/*\n+\t * Populate this to capture output and prevent it from being printed to\n+\t * stderr. This will be passed directly through to\n+\t * run_command:run_parallel_processes(). See t/helper/test-run-command.c\n+\t * for an example.\n+\t */\n+\tconsume_sideband_fn consume_sideband;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431874","messageId":"patch-v4-31.36-6e5f1f5bd3a-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 31/36] run-command: allow capturing of collated output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:57Z","receivedAt":"2021-08-03T19:40:05Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome callers, for example server-side hooks which wish to relay hook\noutput to clients across a transport, want to capture what would\nnormally print to stderr and do something else with it. Allow that via a\ncallback.\n\nBy calling the callback regardless of whether there's output available,\nwe allow clients to send e.g. a keepalive if necessary.\n\nBecause we expose a strbuf, not a fd or FILE*, there's no need to create\na temporary pipe or similar - we can just skip the print to stderr and\ninstead hand it to the caller.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 33 +++++++++++++++++++++++++--------\n run-command.h               | 18 +++++++++++++++++-\n submodule.c                 |  2 +-\n t/helper/test-run-command.c | 25 ++++++++++++++++++++-----\n t/t0061-run-command.sh      |  7 +++++++\n 8 files changed, 73 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex fef6e85d003..de14df5085c 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1817,7 +1817,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n-\t\t\t\t\t\t    NULL,\n+\t\t\t\t\t\t    NULL, NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex f42ded548bf..e5e5a8bcfb5 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2294,7 +2294,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure, NULL,\n+\t\t\t\t   update_clone_start_failure, NULL, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex 44e73827800..9ae3007cdd3 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -203,6 +203,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex f1616858d18..aacc336f951 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1493,6 +1493,7 @@ struct parallel_processes {\n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n \tfeed_pipe_fn feed_pipe;\n+\tconsume_sideband_fn consume_sideband;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1558,6 +1559,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n \t\t    feed_pipe_fn feed_pipe,\n+\t\t    consume_sideband_fn consume_sideband,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1578,6 +1580,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n \tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n+\tpp->consume_sideband = consume_sideband;\n \n \tpp->nr_processes = 0;\n \tpp->output_owner = 0;\n@@ -1614,7 +1617,10 @@ static void pp_cleanup(struct parallel_processes *pp)\n \t * When get_next_task added messages to the buffer in its last\n \t * iteration, the buffered output is non empty.\n \t */\n-\tstrbuf_write(&pp->buffered_output, stderr);\n+\tif (pp->consume_sideband)\n+\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\telse\n+\t\tstrbuf_write(&pp->buffered_output, stderr);\n \tstrbuf_release(&pp->buffered_output);\n \n \tsigchain_pop_common();\n@@ -1735,9 +1741,13 @@ static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n static void pp_output(struct parallel_processes *pp)\n {\n \tint i = pp->output_owner;\n+\n \tif (pp->children[i].state == GIT_CP_WORKING &&\n \t    pp->children[i].err.len) {\n-\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\tif (pp->consume_sideband)\n+\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\telse\n+\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n \t\tstrbuf_reset(&pp->children[i].err);\n \t}\n }\n@@ -1776,11 +1786,15 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\t\tstrbuf_addbuf(&pp->buffered_output, &pp->children[i].err);\n \t\t\tstrbuf_reset(&pp->children[i].err);\n \t\t} else {\n-\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t/* Output errors, then all other finished child processes */\n+\t\t\tif (pp->consume_sideband) {\n+\t\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\t\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\t\t\t} else {\n+\t\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n+\t\t\t}\n \t\t\tstrbuf_reset(&pp->children[i].err);\n-\n-\t\t\t/* Output all other finished child processes */\n-\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n \t\t\tstrbuf_reset(&pp->buffered_output);\n \n \t\t\t/*\n@@ -1804,6 +1818,7 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n \t\t\t   feed_pipe_fn feed_pipe,\n+\t\t\t   consume_sideband_fn consume_sideband,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1814,7 +1829,7 @@ int run_processes_parallel(int n,\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n \n-\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, consume_sideband, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1852,6 +1867,7 @@ int run_processes_parallel(int n,\n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n \t\t\t       feed_pipe_fn feed_pipe,\n+\t\t\t       consume_sideband_fn consume_sideband,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1861,7 +1877,8 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, consume_sideband,\n+\t\t\t\t\ttask_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 80d394664ae..e321d23bbd2 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -436,6 +436,20 @@ typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n \t\t\t    void *pp_cb,\n \t\t\t    void *pp_task_cb);\n \n+/**\n+ * If this callback is provided, instead of collating process output to stderr,\n+ * they will be collated into a new pipe. consume_sideband_fn will be called\n+ * repeatedly. When output is available on that pipe, it will be contained in\n+ * 'output'. But it will be called with an empty 'output' too, to allow for\n+ * keepalives or similar operations if necessary.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel.\n+ *\n+ * Since this callback is provided with the collated output, no task cookie is\n+ * provided.\n+ */\n+typedef void (*consume_sideband_fn)(struct strbuf *output, void *pp_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -471,10 +485,12 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n \t\t\t   feed_pipe_fn,\n+\t\t\t   consume_sideband_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, consume_sideband_fn,\n+\t\t\t       task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n /**\ndiff --git a/submodule.c b/submodule.c\nindex db1700a502d..32364d8bd56 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1632,7 +1632,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   NULL, NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 9348184d303..d53db6d11c4 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -51,6 +51,16 @@ static int no_job(struct child_process *cp,\n \treturn 0;\n }\n \n+static void test_consume_sideband(struct strbuf *output, void *cb)\n+{\n+\tFILE *sideband;\n+\n+\tsideband = fopen(\"./sideband\", \"a\");\n+\n+\tstrbuf_write(output, sideband);\n+\tfclose(sideband);\n+}\n+\n static int task_finished(int result,\n \t\t\t struct strbuf *err,\n \t\t\t void *pp_cb,\n@@ -201,7 +211,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_stdin, test_finished, &suite);\n+\t\t\t\t     test_stdin, NULL, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -429,23 +439,28 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-stdin\")) {\n \t\tproc.in = -1;\n \t\tproc.no_stdin = 0;\n \t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n-\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t\t\t\t\t     test_stdin, NULL, NULL, &proc));\n \t}\n \n+\tif (!strcmp(argv[1], \"run-command-sideband\"))\n+\t\texit(run_processes_parallel(jobs, parallel_next, NULL, NULL,\n+\t\t\t\t\t    test_consume_sideband, NULL,\n+\t\t\t\t\t    &proc));\n+\n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\n }\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 87759482ad1..e99f6c7f445 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,13 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'run_command can divert output' '\n+\ttest_when_finished rm sideband &&\n+\ttest-tool run-command run-command-sideband 3 sh -c \"printf \\\"%s\\n%s\\n\\\" Hello World\" 2>actual &&\n+\ttest_must_be_empty actual &&\n+\ttest_cmp expect sideband\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n listening for stdin:\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431875","messageId":"patch-v4-33.36-dcf63634338-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 33/36] receive-pack: convert 'update' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:38:59Z","receivedAt":"2021-08-03T19:40:06Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nThis makes use of the new sideband API in hook.h added in the\npreceding commit.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 62 ++++++++++++++++++++++++++++--------------\n 1 file changed, 41 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a7d03bbc7d3..31ce4ece4e7 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -937,33 +937,53 @@ static int run_receive_hook(struct command *commands,\n \treturn status;\n }\n \n-static int run_update_hook(struct command *cmd)\n+static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n {\n-\tconst char *argv[5];\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tint code;\n+\tint keepalive_active = 0;\n \n-\targv[0] = find_hook(\"update\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tif (keepalive_in_sec <= 0)\n+\t\tuse_keepalive = KEEPALIVE_NEVER;\n+\tif (use_keepalive == KEEPALIVE_ALWAYS)\n+\t\tkeepalive_active = 1;\n \n-\targv[1] = cmd->ref_name;\n-\targv[2] = oid_to_hex(&cmd->old_oid);\n-\targv[3] = oid_to_hex(&cmd->new_oid);\n-\targv[4] = NULL;\n+\t/* send a keepalive if there is no data to write */\n+\tif (keepalive_active && !output->len) {\n+\t\tstatic const char buf[] = \"0005\\1\";\n+\t\twrite_or_die(1, buf, sizeof(buf) - 1);\n+\t\treturn;\n+\t}\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.argv = argv;\n-\tproc.trace2_hook_name = \"update\";\n+\tif (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {\n+\t\tconst char *first_null = memchr(output->buf, '\\0', output->len);\n+\t\tif (first_null) {\n+\t\t\t/* The null bit is excluded. */\n+\t\t\tsize_t before_null = first_null - output->buf;\n+\t\t\tsize_t after_null = output->len - (before_null + 1);\n+\t\t\tkeepalive_active = 1;\n+\t\t\tsend_sideband(1, 2, output->buf, before_null, use_sideband);\n+\t\t\tsend_sideband(1, 2, first_null + 1, after_null, use_sideband);\n+\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n+}\n+\n+static int run_update_hook(struct command *cmd)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n+\tstrvec_pushl(&opt.args,\n+\t\t     cmd->ref_name,\n+\t\t     oid_to_hex(&cmd->old_oid),\n+\t\t     oid_to_hex(&cmd->new_oid),\n+\t\t     NULL);\n \n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n \tif (use_sideband)\n-\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\treturn finish_command(&proc);\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\treturn run_hooks_oneshot(\"update\", &opt);\n }\n \n static struct command *find_command_by_refname(struct command *list,\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431876","messageId":"patch-v4-36.36-b71d7628b40-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 36/36] hooks: fix a TOCTOU in \"did we run a hook?\" heuristic","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:39:02Z","receivedAt":"2021-08-03T19:40:08Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a Time-of-check to time-of-use (TOCTOU) race in code added in\n680ee550d72 (commit: skip discarding the index if there is no\npre-commit hook, 2017-08-14).\n\nWe can fix the race passing around information about whether or not we\nran the hook in question, instead of running hook_exists() after the\nfact to check if the hook in question exists. This problem has been\nnoted on-list when 680ee550d72 was discussed[1], but had not been\nfixed.\n\nIn addition to fixing this for the pre-commit hook as suggested there\nI'm also fixing this for the pre-merge-commit hook. See\n6098817fd7f (git-merge: honor pre-merge-commit hook, 2019-08-07) for\nthe introduction of its previous behavior.\n\nLet's also change this for the push-to-checkout hook. Now instead of\nchecking if the hook exists and either doing a push to checkout or a\npush to deploy we'll always attempt a push to checkout. If the hook\ndoesn't exist we'll fall back on push to deploy. The same behavior as\nbefore, without the TOCTOU race. See 0855331941b (receive-pack:\nsupport push-to-checkout hook, 2014-12-01) for the introduction of the\nprevious behavior.\n\nThis leaves uses of hook_exists() in two places that matter. The\n\"reference-transaction\" check in refs.c, see 67541597670 (refs:\nimplement reference transaction hook, 2020-06-19), and the\nprepare-commit-msg hook, see 66618a50f9c (sequencer: run\n'prepare-commit-msg' hook, 2018-01-24).\n\nIn both of those cases we're saving ourselves CPU time by not\npreparing data for the hook that we'll then do nothing with if we\ndon't have the hook. So using this \"invoked_hook\" pattern doesn't make\nsense in those cases.\n\nMore importantly, in those cases the worst we'll do is miss that we\n\"should\" run the hook because a new hook appeared, whereas in the\npre-commit and pre-merge-commit cases we'll skip an important\ndiscard_cache() on the bases of our faulty guess.\n\nI do think none of these races really matter in practice. It would be\nsome one-off issue as a hook was added or removed. I did think it was\nstupid that we didn't pass a \"did this run?\" flag instead of doing\nthis guessing at a distance though, so now we're not guessing anymore.\n\n1. https://lore.kernel.org/git/20170810191613.kpmhzg4seyxy3cpq@sigill.intra.peff.net/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c       | 18 +++++++++++-------\n builtin/merge.c        | 16 ++++++++++------\n builtin/receive-pack.c |  8 +++++---\n commit.c               |  1 +\n commit.h               |  3 ++-\n hook.c                 |  4 ++++\n hook.h                 | 10 ++++++++++\n sequencer.c            |  4 ++--\n 8 files changed, 45 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex aa3c741efa9..0b7642e8324 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -725,11 +725,13 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \tint clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);\n \tint old_display_comment_prefix;\n \tint merge_contains_scissors = 0;\n+\tint invoked_hook = 0;\n \n \t/* This checks and barfs if author is badly specified */\n \tdetermine_author_info(author_ident);\n \n-\tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(use_editor, index_file, &invoked_hook,\n+\t\t\t\t\t  \"pre-commit\", NULL))\n \t\treturn 0;\n \n \tif (squash_message) {\n@@ -1052,10 +1054,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && hook_exists(\"pre-commit\")) {\n+\tif (!no_verify && invoked_hook) {\n \t\t/*\n-\t\t * Re-read the index as pre-commit hook could have updated it,\n-\t\t * and write it out as a tree.  We must do this before we invoke\n+\t\t * Re-read the index as the pre-commit-commit hook was invoked\n+\t\t * and could have updated it. We must do this before we invoke\n \t\t * the editor and after we invoke run_status above.\n \t\t */\n \t\tdiscard_cache();\n@@ -1067,7 +1069,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n+\tif (run_commit_hook(use_editor, index_file, NULL, \"prepare-commit-msg\",\n \t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n \t\treturn 0;\n \n@@ -1084,7 +1086,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t}\n \n \tif (!no_verify &&\n-\t    run_commit_hook(use_editor, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n+\t    run_commit_hook(use_editor, index_file, NULL, \"commit-msg\",\n+\t\t\t    git_path_commit_editmsg(), NULL)) {\n \t\treturn 0;\n \t}\n \n@@ -1840,7 +1843,8 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \n \trepo_rerere(the_repository, 0);\n \trun_auto_maintenance(quiet);\n-\trun_commit_hook(use_editor, get_index_file(), \"post-commit\", NULL);\n+\trun_commit_hook(use_editor, get_index_file(), NULL, \"post-commit\",\n+\t\t\tNULL);\n \tif (amend && !no_post_rewrite) {\n \t\tcommit_post_rewrite(the_repository, current_head, &oid);\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 4965df2ac29..9bd4a2532c3 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -843,15 +843,18 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n {\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *index_file = get_index_file();\n+\tint invoked_hook = 0;\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, index_file, \"pre-merge-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(0 < option_edit, index_file,\n+\t\t\t\t\t  &invoked_hook, \"pre-merge-commit\",\n+\t\t\t\t\t  NULL))\n \t\tabort_commit(remoteheads, NULL);\n \t/*\n-\t * Re-read the index as pre-merge-commit hook could have updated it,\n-\t * and write it out as a tree.  We must do this before we invoke\n+\t * Re-read the index as the pre-merge-commit hook was invoked\n+\t * and could have updated it. We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (hook_exists(\"pre-merge-commit\"))\n+\tif (invoked_hook)\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n@@ -872,7 +875,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tappend_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);\n \twrite_merge_heads(remoteheads);\n \twrite_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);\n-\tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n+\tif (run_commit_hook(0 < option_edit, get_index_file(), NULL,\n+\t\t\t    \"prepare-commit-msg\",\n \t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \tif (0 < option_edit) {\n@@ -881,7 +885,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t}\n \n \tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n-\t\t\t\t\t  \"commit-msg\",\n+\t\t\t\t\t  NULL, \"commit-msg\",\n \t\t\t\t\t  git_path_merge_msg(the_repository), NULL))\n \t\tabort_commit(remoteheads, NULL);\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex c3984680d7f..ebec6f3bb10 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1428,10 +1428,12 @@ static const char *push_to_deploy(unsigned char *sha1,\n static const char *push_to_checkout_hook = \"push-to-checkout\";\n \n static const char *push_to_checkout(unsigned char *hash,\n+\t\t\t\t    int *invoked_hook,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\topt.invoked_hook = invoked_hook;\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n \tstrvec_pushv(&opt.env, env->v);\n@@ -1446,6 +1448,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n {\n \tconst char *retval, *work_tree, *git_dir = NULL;\n \tstruct strvec env = STRVEC_INIT;\n+\tint invoked_hook = 0;\n \n \tif (worktree && worktree->path)\n \t\twork_tree = worktree->path;\n@@ -1463,10 +1466,9 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!hook_exists(push_to_checkout_hook))\n+\tretval = push_to_checkout(sha1, &invoked_hook, &env, work_tree);\n+\tif (!invoked_hook)\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n-\telse\n-\t\tretval = push_to_checkout(sha1, &env, work_tree);\n \n \tstrvec_clear(&env);\n \treturn retval;\ndiff --git a/commit.c b/commit.c\nindex 63d7943a86d..842e47beae2 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1697,6 +1697,7 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n }\n \n int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook,\n \t\t    const char *name, ...)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\ndiff --git a/commit.h b/commit.h\nindex df42eb434f3..b5a542993c6 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -363,7 +363,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\n \n LAST_ARG_MUST_BE_NULL\n-int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);\n+int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook, const char *name, ...);\n \n /* Sign a commit or tag buffer, storing the result in a header. */\n int sign_with_header(struct strbuf *buf, const char *keyid);\ndiff --git a/hook.c b/hook.c\nindex efed5f73b4d..ee20b2e3658 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -173,6 +173,9 @@ static int notify_hook_finished(int result,\n \n \thook_cb->rc |= result;\n \n+\tif (hook_cb->invoked_hook)\n+\t\t*hook_cb->invoked_hook = 1;\n+\n \treturn 0;\n }\n \n@@ -187,6 +190,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t.rc = 0,\n \t\t.hook_name = hook_name,\n \t\t.options = options,\n+\t\t.invoked_hook = options->invoked_hook,\n \t};\n \tint jobs = 1;\n \ndiff --git a/hook.h b/hook.h\nindex 37a9690c2ca..58dfbf474c9 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -66,6 +66,15 @@ struct run_hooks_opt\n \t * for an example.\n \t */\n \tconsume_sideband_fn consume_sideband;\n+\n+\t/*\n+\t * A pointer which if provided will be set to 1 or 0 depending\n+\t * on if a hook was invoked (i.e. existed), regardless of\n+\t * whether or not that was successful. Used for avoiding\n+\t * TOCTOU races in code that would otherwise call hook_exist()\n+\t * after a \"maybe hook run\" to see if a hook was invoked.\n+\t */\n+\tint *invoked_hook;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -90,6 +99,7 @@ struct hook_cb_data {\n \tconst char *hook_name;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n+\tint *invoked_hook;\n };\n \n void run_hooks_opt_clear(struct run_hooks_opt *o);\ndiff --git a/sequencer.c b/sequencer.c\nindex 77f809c00e4..f451e23d0c1 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1203,7 +1203,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n \t} else {\n \t\targ1 = \"message\";\n \t}\n-\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n+\tif (run_commit_hook(0, r->index_file, NULL, \"prepare-commit-msg\", name,\n \t\t\t    arg1, arg2, NULL))\n \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n \n@@ -1533,7 +1533,7 @@ static int try_to_commit(struct repository *r,\n \t\tgoto out;\n \t}\n \n-\trun_commit_hook(0, r->index_file, \"post-commit\", NULL);\n+\trun_commit_hook(0, r->index_file, NULL, \"post-commit\", NULL);\n \tif (flags & AMEND_MSG)\n \t\tcommit_post_rewrite(r, current_head, oid);\n \n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431877","messageId":"patch-v4-34.36-f352a485e59-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 34/36] post-update: use hook.h library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:39:00Z","receivedAt":"2021-08-03T19:40:09Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 25 ++++++-------------------\n 1 file changed, 6 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 31ce4ece4e7..26e302aab85 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1650,33 +1650,20 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *hook;\n-\n-\thook = find_hook(\"post-update\");\n-\tif (!hook)\n-\t\treturn;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\n \t\t\tcontinue;\n-\t\tif (!proc.args.nr)\n-\t\t\tstrvec_push(&proc.args, hook);\n-\t\tstrvec_push(&proc.args, cmd->ref_name);\n+\t\tstrvec_push(&opt.args, cmd->ref_name);\n \t}\n-\tif (!proc.args.nr)\n+\tif (!opt.args.nr)\n \t\treturn;\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.trace2_hook_name = \"post-update\";\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n \n-\tif (!start_command(&proc)) {\n-\t\tif (use_sideband)\n-\t\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\t\tfinish_command(&proc);\n-\t}\n+\trun_hooks_oneshot(\"post-update\", &opt);\n }\n \n static void check_aliased_update_internal(struct command *cmd,\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431878","messageId":"patch-v4-35.36-ceef2f3e804-20210803T191505Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v4 35/36] receive-pack: convert receive hooks to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-03T19:39:01Z","receivedAt":"2021-08-03T19:40:11Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 190 ++++++++++++++++++-----------------------\n 1 file changed, 83 insertions(+), 107 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 26e302aab85..c3984680d7f 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -747,7 +747,7 @@ static int check_cert_push_options(const struct string_list *push_options)\n \treturn retval;\n }\n \n-static void prepare_push_cert_sha1(struct child_process *proc)\n+static void prepare_push_cert_sha1(struct run_hooks_opt *opt)\n {\n \tstatic int already_done;\n \n@@ -771,110 +771,42 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n \t}\n \tif (!is_null_oid(&push_cert_oid)) {\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT=%s\",\n \t\t\t     oid_to_hex(&push_cert_oid));\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_SIGNER=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_SIGNER=%s\",\n \t\t\t     sigcheck.signer ? sigcheck.signer : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_KEY=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_KEY=%s\",\n \t\t\t     sigcheck.key ? sigcheck.key : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_STATUS=%c\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_STATUS=%c\",\n \t\t\t     sigcheck.result);\n \t\tif (push_cert_nonce) {\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE=%s\",\n \t\t\t\t     push_cert_nonce);\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE_STATUS=%s\",\n \t\t\t\t     nonce_status);\n \t\t\tif (nonce_status == NONCE_SLOP)\n-\t\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t\t     \"GIT_PUSH_CERT_NONCE_SLOP=%ld\",\n \t\t\t\t\t     nonce_stamp_slop);\n \t\t}\n \t}\n }\n \n+struct receive_hook_feed_context {\n+\tstruct command *cmd;\n+\tint skip_broken;\n+};\n+\n struct receive_hook_feed_state {\n \tstruct command *cmd;\n \tstruct ref_push_report *report;\n \tint skip_broken;\n \tstruct strbuf buf;\n-\tconst struct string_list *push_options;\n };\n \n-typedef int (*feed_fn)(void *, const char **, size_t *);\n-static int run_and_feed_hook(const char *hook_name, feed_fn feed,\n-\t\t\t     struct receive_hook_feed_state *feed_state)\n-{\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct async muxer;\n-\tconst char *argv[2];\n-\tint code;\n-\n-\targv[0] = find_hook(hook_name);\n-\tif (!argv[0])\n-\t\treturn 0;\n-\n-\targv[1] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = hook_name;\n-\n-\tif (feed_state->push_options) {\n-\t\tint i;\n-\t\tfor (i = 0; i < feed_state->push_options->nr; i++)\n-\t\t\tstrvec_pushf(&proc.env_array,\n-\t\t\t\t     \"GIT_PUSH_OPTION_%d=%s\", i,\n-\t\t\t\t     feed_state->push_options->items[i].string);\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT=%d\",\n-\t\t\t     feed_state->push_options->nr);\n-\t} else\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT\");\n-\n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&proc.env_array, tmp_objdir_env(tmp_objdir));\n-\n-\tif (use_sideband) {\n-\t\tmemset(&muxer, 0, sizeof(muxer));\n-\t\tmuxer.proc = copy_to_sideband;\n-\t\tmuxer.in = -1;\n-\t\tcode = start_async(&muxer);\n-\t\tif (code)\n-\t\t\treturn code;\n-\t\tproc.err = muxer.in;\n-\t}\n-\n-\tprepare_push_cert_sha1(&proc);\n-\n-\tcode = start_command(&proc);\n-\tif (code) {\n-\t\tif (use_sideband)\n-\t\t\tfinish_async(&muxer);\n-\t\treturn code;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\n-\twhile (1) {\n-\t\tconst char *buf;\n-\t\tsize_t n;\n-\t\tif (feed(feed_state, &buf, &n))\n-\t\t\tbreak;\n-\t\tif (write_in_full(proc.in, buf, n) < 0)\n-\t\t\tbreak;\n-\t}\n-\tclose(proc.in);\n-\tif (use_sideband)\n-\t\tfinish_async(&muxer);\n-\n-\tsigchain_pop(SIGPIPE);\n-\n-\treturn finish_command(&proc);\n-}\n-\n-static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n+static int feed_receive_hook(void *state_)\n {\n \tstruct receive_hook_feed_state *state = state_;\n \tstruct command *cmd = state->cmd;\n@@ -883,9 +815,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t       state->skip_broken && (cmd->error_string || cmd->did_not_exist))\n \t\tcmd = cmd->next;\n \tif (!cmd)\n-\t\treturn -1; /* EOF */\n-\tif (!bufp)\n-\t\treturn 0; /* OK, can feed something. */\n+\t\treturn 1; /* EOF - close the pipe*/\n \tstrbuf_reset(&state->buf);\n \tif (!state->report)\n \t\tstate->report = cmd->report;\n@@ -909,32 +839,36 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t\t\t    cmd->ref_name);\n \t\tstate->cmd = cmd->next;\n \t}\n-\tif (bufp) {\n-\t\t*bufp = state->buf.buf;\n-\t\t*sizep = state->buf.len;\n-\t}\n \treturn 0;\n }\n \n-static int run_receive_hook(struct command *commands,\n-\t\t\t    const char *hook_name,\n-\t\t\t    int skip_broken,\n-\t\t\t    const struct string_list *push_options)\n+static int feed_receive_hook_cb(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n {\n-\tstruct receive_hook_feed_state state;\n-\tint status;\n-\n-\tstrbuf_init(&state.buf, 0);\n-\tstate.cmd = commands;\n-\tstate.skip_broken = skip_broken;\n-\tstate.report = NULL;\n-\tif (feed_receive_hook(&state, NULL, NULL))\n-\t\treturn 0;\n-\tstate.cmd = commands;\n-\tstate.push_options = push_options;\n-\tstatus = run_and_feed_hook(hook_name, feed_receive_hook, &state);\n-\tstrbuf_release(&state.buf);\n-\treturn status;\n+\tstruct hook *hook = pp_task_cb;\n+\tstruct receive_hook_feed_state *feed_state = hook->feed_pipe_cb_data;\n+\tint rc;\n+\n+\t/* first-time setup */\n+\tif (!feed_state) {\n+\t\tstruct hook_cb_data *hook_cb = pp_cb;\n+\t\tstruct run_hooks_opt *opt = hook_cb->options;\n+\t\tstruct receive_hook_feed_context *ctx = opt->feed_pipe_ctx;\n+\t\tif (!ctx)\n+\t\t\tBUG(\"run_hooks_opt.feed_pipe_ctx required for receive hook\");\n+\n+\t\tfeed_state = xmalloc(sizeof(struct receive_hook_feed_state));\n+\t\tstrbuf_init(&feed_state->buf, 0);\n+\t\tfeed_state->cmd = ctx->cmd;\n+\t\tfeed_state->skip_broken = ctx->skip_broken;\n+\t\tfeed_state->report = NULL;\n+\n+\t\thook->feed_pipe_cb_data = feed_state;\n+\t}\n+\n+\trc = feed_receive_hook(feed_state);\n+\tif (!rc)\n+\t\tstrbuf_addbuf(pipe, &feed_state->buf);\n+\treturn rc;\n }\n \n static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n@@ -970,6 +904,48 @@ static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n \tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n }\n \n+static int run_receive_hook(struct command *commands,\n+\t\t\t    const char *hook_name,\n+\t\t\t    int skip_broken,\n+\t\t\t    const struct string_list *push_options)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct receive_hook_feed_context ctx;\n+\tstruct command *iter = commands;\n+\n+\t/* if there are no valid commands, don't invoke the hook at all. */\n+\twhile (iter && skip_broken && (iter->error_string || iter->did_not_exist))\n+\t\titer = iter->next;\n+\tif (!iter)\n+\t\treturn 0;\n+\n+\tif (push_options) {\n+\t\tint i;\n+\t\tfor (i = 0; i < push_options->nr; i++)\n+\t\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_%d=%s\", i,\n+\t\t\t\t     push_options->items[i].string);\n+\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_COUNT=%d\", push_options->nr);\n+\t} else\n+\t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n+\n+\tif (tmp_objdir)\n+\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\n+\tprepare_push_cert_sha1(&opt);\n+\n+\t/* set up sideband printer */\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\t/* set up stdin callback */\n+\tctx.cmd = commands;\n+\tctx.skip_broken = skip_broken;\n+\topt.feed_pipe = feed_receive_hook_cb;\n+\topt.feed_pipe_ctx = &ctx;\n+\n+\treturn run_hooks_oneshot(hook_name, &opt);\n+}\n+\n static int run_update_hook(struct command *cmd)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n-- \n2.33.0.rc0.595.ge31e012651d\n\n"},{"id":"431948","messageId":"87zgtxms4s.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"patch-v4-08.36-7cb4a4cb69e-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 08/36] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-04T10:15:43Z","receivedAt":"2021-08-04T10:17:04Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Aug 03 2021, Ævar Arnfjörð Bjarmason wrote:\n\n> From: Emily Shaffer <emilyshaffer@google.com>\n> [...]\n> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> new file mode 100755\n> index 00000000000..644df0a583c\n> --- /dev/null\n> +++ b/t/t1800-hook.sh\n> @@ -0,0 +1,133 @@\n> +#!/bin/bash\n> +\n\nIt appears that since at least May 2020 and across N re-rolls of this\ntopic nobody (including me) has noticed that this should be #!/bin/sh,\nnot #!/bin/bash.\n\nI've got it fixed up locally, but thought it was not worth another\n36-patch re-roll pending further feedback.\n"},{"id":"431977","messageId":"YQr6xojzxk3xJrTw@google.com","threadId":"55286","inReplyTo":"87bl6ttmlv.fsf@evledraar.gmail.com","subject":"Re: [PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-04T20:38:30Z","receivedAt":"2021-08-04T20:38:41Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Jul 23, 2021 at 09:41:35AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Jul 22 2021, Emily Shaffer wrote:\n> \n> > On Fri, Jul 16, 2021 at 11:13:42AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> >> \n> >> \n> >> On Thu, Jul 15 2021, Emily Shaffer wrote:\n> >> \n> >> > To enable fine-grained options which apply to a single hook executable,\n> >> > and to make it easier for a single executable to be run on multiple hook\n> >> > events, teach \"hookcmd.<alias>.config\". These can be configured as\n> >> > follows:\n> >> > [...]\n> >> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> >> > index a97b980cca..5b35170664 100644\n> >> > --- a/Documentation/config/hook.txt\n> >> > +++ b/Documentation/config/hook.txt\n> >> > @@ -3,6 +3,11 @@ hook.<command>.command::\n> >> >  \texecutable on your device, a oneliner for your shell, or the name of a\n> >> >  \thookcmd. See linkgit:git-hook[1].\n> >> >  \n> >> > +hookcmd.<name>.command::\n> >> > +\tA command to execute during a hook for which <name> has been specified\n> >> > +\tas a command. This can be an executable on your device or a oneliner for\n> >> > +\tyour shell. See linkgit:git-hook[1].\n> >> > +\n> >> > [...]\n> >> > +Global config\n> >> > +----\n> >> > +  [hook \"post-commit\"]\n> >> > +    command = \"linter\"\n> >> > +    command = \"~/typocheck.sh\"\n> >> > +\n> >> > +  [hookcmd \"linter\"]\n> >> > +    command = \"/bin/linter --c\"\n> >> > +----\n> >> > +\n> >> > +Local config\n> >> > +----\n> >> > +  [hook \"prepare-commit-msg\"]\n> >> > +    command = \"linter\"\n> >> > +  [hook \"post-commit\"]\n> >> > +    command = \"python ~/run-test-suite.py\"\n> >> > +----\n> >> > +\n> >> > +With these configs, you'd then run post-commit hooks in this order:\n> >> > +\n> >> > +  /bin/linter --c\n> >> > +  ~/typocheck.sh\n> >> > +  python ~/run-test-suite.py\n> >> > +  .git/hooks/post-commit (if present)\n> >> > +\n> >> > +and prepare-commit-msg hooks in this order:\n> >> > +\n> >> > +  /bin/linter --c\n> >> > +  .git/hooks/prepare-commit-msg (if present)\n> >> >  \n> >> \n> >> I still have outstanding feedback on the fundamental design\n> >> here. I.e. why is this not:\n> >> \n> >>     hook.<name>.event = post-commit\n> >>     hook.<name>.command = <path>\n> >> \n> >> See:\n> >> \n> >> https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n> >> \n> >> As noted there I don't see why not, and more complexity results from the\n> >> design choice of doing it the way you're proposing. I.e. we can't\n> >> discover hooks based on config prefixes, and we end up sticking full FS\n> >> paths in config keys.\n> >\n> > Interesting. My gut says that it would make it harder to neatly write a\n> > config with the same hook running at the very beginning of one event and\n> > the very end of another, but I'm not sure whether that's a case to\n> > optimize for.\n> >\n> > I would also need twice as many lines to run a script I wrote as a hook\n> > - that is, the base case which is probably all most people will need. So\n> > with your proposal I *must* name every hook I want to add. Instead of\n> > \"hook.pre-commit.command = ~/check-for-debug-strings\" I need to\n> > configure \"hook.check-debug-strings.event = pre-commit\" and\n> > \"hook.check-debug-strings.command = ~/check-for-debug-strings\". That's a\n> > little frustrating, and if I never want to skip that check or move it\n> > later or something, it will be extra overhead for no gain for me.\n> \n> The gain is that \"git hook list\" becomes a trivial \"git config\n> -show-origin --show-scope --get-regexp\" wrapper.\n\nThis isn't a very compelling reason to me, if it makes the user\nexperience worse. (I'm not convinced that it does - just saying I\ndisagree with this reasoning.)\n\n> \n> So the series either doesn't need \"git hook list\" or such a thing\n> becomes much less complex, especially given the proposed addition of\n> other features in the area like \"git hook edit\", i.e. (quoting the\n> linked E-Mail):\n\nI still think \"git hook list\" is useful to have for end-users, compared\nto remembering the appropriate \"git config\" invocation. It futureproofs\nus if we do want to change the execution ordering to something besides\nconfig order, it's easier to remember/more discoverable, etc.\n\n>     As just one example; surely \"git config edit <name>\" would need to\n>     run around and find config files to edit, then open them in a loop\n>     for you, no?\n> \n>     Which we'd eventually want for \"git config\" in general with an\n>     --edit-regexp option or whatever, which brings us (well, at least\n>     me) back to \"then let's just add it to git-config?\".\n> \n> > I do agree that your approach bans the regrettably awkward\n> > \"hookcmd.~/check-for-debug-strings.skip = true\", but I'm not sure\n> > whether or not it's worth it.\n> \n> That design choice also means that you can't expand the path using \"git\n> config --get --type=path.\n\nYeah, this is a pretty handy point.\n\n> \n> We do have that with the \"includeIf\" construct, but if we can avoid it\n> we should, it makes it play nicer with other assumptions and features of\n> the config system.\n> \n> As noted in the follow-up reply while we don't case normalize the LeVeL\"\n> part of \"ThReE.LeVeL.KeY\" that's tolower(), which we know isn't a 1=1\n> mapping on some\n> FS's. https://lore.kernel.org/git/87y2ebo16v.fsf@evledraar.gmail.com/\n\nThis, too.\n\n> \n> > To help us visualize the change, here's some common and complicated\n> > tasks and how they look with either schema (let mine be A and yours be\n> > B):\n> \n> Before diving into that, I'll just say I don't care about the trivial\n> specifics of how this is done, i.e. the bikeshedding of what the config\n> keys etc. are named.\n> \n> Just in (as noted above) design choices here forcing avoidable\n> complexity in other areas.\n> \n> > #1 - Add a oneliner (not executing a script)\n> > A:\n> > [hook \"post-commit\"]\n> > \tcommand = echo post commit\n> > B:\n> > [hook \"oneliner\"]\n> > \tevent = post-commit\n> > \tcommand = echo post commit\n> > #2 - Execute a script\n> > A:\n> > [hook \"post-commit\"]\n> > \tcommand = ~/my-post-commit-hook\n> > B:\n> > [hook \"script\"]\n> > \tevent = post-commit\n> > \tcommand = ~/my-post-commit-hook\n> \n> ...\n> \n> > #3 - Run a handful of scripts in a specific order on one event\n> > A:\n> > [hook \"post-commit\"]\n> > \tcommand = ~/my-post-commit-1\n> > \tcommand = ~/my-post-commit-2\n> > \tcommand = ~/my-post-commit-3\n> > B:\n> > [hook \"script 1\"]\n> > \tevent = post-commit\n> > \tcommand = ~/my-post-commit-1\n> > [hook \"script 2\"]\n> > \tevent = post-commit\n> > \tcommand = ~/my-post-commit-2\n> > [hook \"script 3\"]\n> > \tevent = post-commit\n> > \tcommand = ~/my-post-commit-3\n> \n> To reply to all the above, yes, your suggestion comes out ahead in being\n> less verbose.\n> \n> But isn't the real difference not in the differing prefixes, i.e. hook.*\n> and hookcmd.* (A) v.s. always hook.* (B, which is what I'm mainly\n> focusing on, i.e. what requires the added complexity.\n> \n> But that in that in your proposed way of doing it it's:\n> \n>     hook.<event-or-name>.*\n> \n> V.s. my suggestion of:\n> \n>     hook.<name>.*\n> \n\nNo, I think that's a misunderstanding of proposal A. My proposal A is\nalways hook.<event>.*, and hookcmd.<name>.*, and the way that I read\nyour proposal B is that it's always hook.<name>.*.\n\n> And thus whenever you have a <event-or-name> that just happens to be a\n> built-in hook listed in githooks(5) we in (A) implicitly expand config\n> like:\n> \n>     hook.post-commit.command = echo foo\n> \n> \n>     hook.post-commit.command = echo hi\n>     hook.post-commit.type    = post-commit\n\nThis is where I'm becoming confused. I *think* you're saying that with\nyour proposal B, if the subsection happens to be the name of a built-in\nhook, we can skip the .event field. But it also reads like you're saying\nthis is how my proposal A works, which isn't the case, because the\nsubsection is always assumed to be the name of the hook event we're\ntrying to run.\n\n> \n> But not knowing about \"foo\" we don't expand:\n> \n>     hook.foo.command = echo foo\n> \n> \n>     hook.foo.command = echo hi\n>     hook.foo.type    = foo # This would be an error, or ignored.\n> \n> But rather leave \"dangling\" for the user to later supply the \"*.event\"\n> themselves, i.e.:\n> \n>     hook.foo.command = echo hi\n>     hook.foo.event = post-commit\n\nIf you're talking about proposal B, then no, this isn't how it works.\nThis config would imply that someone else (like a wrapper) can run \"echo\nhi\" by asking for \"git hook run foo\". But I'm not sure exactly that\nthat's what you mean...\n\n> \n> And means that you presumably need to detect this case and error about\n> it, but my proposed model does not:\n> \n>     hook.post-commit.command = echo hi\n>     # User error: \"*.type\" and <event-or-name>\" does not match for\n>     # \"hook.*.command\"\n>     hook.post-commit.type    = pre-commit\n> \n> And furthermore, that if someone in your model were to do:\n> \n>     hook.verify-commit.command = echo hi\n> \n> It's \"dangling\" today, but if a later version of git learns about a\n> \"verify-commit\" hook we'll start running it unexpectedly.\n\nNo, that's in fact as designed, with my model B. The user configured\n\"echo hi\" to run on \"verify-commit\" events; if those events are\ninitially used by some wrapper, but later we decide they're a great idea\nand absorb the verify-commit event into native Git, then this is working\nas intended. I think your argument is based on a misunderstanding of the\ndesign.\n\nCould it be that the way I provided the examples (my schema after A: and\nyour schema after B:) made it hard to parse? Sorry about that if so.\n\n> \n> Because your design conflates the slot for known hook type names and\n> user-supplied names.\n> \n> On balance I think it's better just to always supply two lines per-hook,\n> but whether we have this proposed shorthand or not is mostly orthogonal\n> to everything I mentioned in\n> https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n> \n> I.e. my proposed version could also have it, but thinking about it I\n> think it's not worth it, we should always use <name>, not\n> <event-or-name> for the reasons noted if you'll read ahead...\n> \n> > #4 - Skip a script configured more globally\n> > A:\n> > (original config)\n> > [hook \"post-commit\"]\n> > \tcommand = /corp/stuff/corp-post-commit\n> > (local config)\n> > [hookcmd \"/corp/stuff/corp-post-commit\"]\n> > \tskip = true\n> > OR,\n> > (original config)\n> > [hookcmd \"corp-pc\"]\n> > \tcommand = /corp/stuff/corp-post-commit\n> > [hook \"post-commit\"]\n> > \tcommand = corp-pc\n> > (local config)\n> > [hookcmd \"corp-pc\"]\n> > \tskip = true\n> > B:\n> > (original config)\n> > [hook \"corp-pc\"]\n> > \tevent = post-commit\n> > \tcommand = /corp/stuff/corp-post-commit\n> > (local config)\n> > [hook \"corp-pc\"]\n> > \tskip = true\n> \n> ...which are among other things that no, my proposed version doesn't\n> have \"skip\" at all, see point #3 at\n> https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n> \n> I.e. I think the \"skip\" is a thing that falls out of a complexity of\n> your design that I'm proposing to do away with.\n> \n> That complexity being that you use <event-or-name> and I use <name>, and\n> you want to in turn support N number of \"*.command\" for any\n> \"hook.<event-or-name>\".\n> \n> I'm suggesting we use the typical \"last key wins\" semantics, if you want\n> multiple commands for a given hook type you'll just supply multiple\n> \"hook.<name>\" sections with the same \"hook.*.event = type\" in all.\n> \n> The way to skip hooks in my proposal is:\n> \n>     hook.<name>.command = true\n\nI see - instead of fiddly reorganizing the list, you're just changing\nwhat the specific hook name is redirecting to.\n\nThen, instead of storing the resolved command in the 'struct hook', we\ncan store the name, and always dereference the name right before\nexecution, using last-one-wins semantics.\n\nThe only thing I don't like about this is that it's a little bit\nconfusing to the user. I personally would rather have more complicated\ncode and less complicated user experience, so I like 'skip' more because\nit's readable and because you don't need to guess \"oh, it'll just\nexecute /bin/true which is a noop\". That's easy to understand for\nsomeone familiar with Unix scripting (and therefore for we Git\ncontributors), but confusing for everybody else.\n\nMaybe we can take the essential spirit of this implementation but make\nit a little bit simpler. In pseudocode:\n\npopulate_list(hookname):\n  # look for everybody who says \"hook.*.event = $hookname\"\n  hooks = get_configs_matching(hook.*.event, hookname)\n  # the \"*\" bit is the bit we dereference later\n  for hook in hooks:\n    hook_list.add({.name = hook.key.subsection})\n\nrun_hooks(hook_list):\n  for hook in hook_list:\n    # should we even run?\n    if (get_last_config(\"hook.\" + hook.name + \".skip\"))\n      continue\n\n    # dereference the hook name and find out the command\n    cmd = get_last_config(\"hook.\" + hook.name + \".command\")\n    run(cmd)\n\nThe price for this improved readability is an extra config lookup in\neach case. But the code is still readable, definitely moreso than\nrewalking the config list over and over as the implementation stands\nnow. I personally think it's worth it.\n\n(One could also imagine a more readable placeholder than /bin/true in\nthe \"hook.<name>.command\" field, but then we get to decide just how hard\nwe want to avoid colliding with legitimate user hook names. Is .command\n= skip enough? is .command = RESERVED_NAME_SKIP_THIS_HOOK enough? Is\nsomething unrunnable like .command = \"skip me\" enough, although it could\nconceivably collide with a determined user?)\n\n> \n> Or whatever noop command you want on the RHS. In practice we wouldn't\n> invoke \"true\", just like we don't invoke \"cat\" for \"PAGER=cat\".\n> \n> But unlike \"*.skip\" that doesn't require complexity in implementation or\n> user understanding, it just falls naturally out of the rest of the\n> model.\n> \n> > #5 - Execute one script for multiple events\n> > A:\n> > [hookcmd \"reusable-hook\"]\n> > \tcommand = /long/path/reusable-hook\n> > [hook \"post-commit\"]\n> > \tcommand = reusable-hook\n> > [hook \"pre-commit\"]\n> > \tcommand = reusable-hook\n> > [hook \"prepare-commit-msg\"]\n> > \tcommand = reusable-hook\n> > B:\n> > [hook \"reusable-hook\"]\n> > \tcommand = /long/path/reusable-hook\n> > \tevent = post-commit\n> > \tevent = pre-commit\n> > \tevent = prepare-commit-msg\n> \n> It's been so long since I wrote the original E-Mail that I'm having to\n> skim my own summary there & come up with things as I go along, so maybe\n> this conflicts with something I said earlier.\n> \n> But no, I don't think I meant that *.event should be multi-value, or\n> that that magic is worthwhile. I.e. I think we just want:\n> \n>     [hook \"not-a-reusable-section-1\"]\n>         command = /long/path/reusable-hook\n>         event = post-commit\n>     [hook \"not-a-reusable-section-2\"]\n>         command = /long/path/reusable-hook\n>         event = pre-commit\n>     [hook \"not-a-reusable-section-3\"]\n>         command = /long/path/reusable-hook\n>         event = prepare-commit-msg\n> \n> I.e. is wanting to use the same command for N different hooks really\n> that common a use-case to avoid a little verbosity, and quite a lot of\n> complexity?\n> \n> How would such a hook even be implemented?\n> \n> We don't have anything in your hook execution model (and I don't\n> remember you adding this) which passes down a \"you are this type of\n> hook\" to hooks.\n\nNo, but it's something I'm interested in passing as an environment\nvariable. I didn't add it to this series because it seemed to me to\ndistract from the core feature. We would like to add it to simplify our\ninvocations of https://github.com/awslabs/git-secrets, so it's on my\nradar.\n\nAnd even without this, just today I wanted to configure a hook to really\nmake sure I had config.mak set up in all my Git worktrees (since you\npointed out I pushed code that fails -Wall.... ;) ) and set it up to run\non both pre-commit and post-checkout. So yes, it is feasible now, for\nvery stupid hooks.\n\n> \n> It's now implicit in that the hook invoked at .git/hooks/post-commit or\n> .git/hooks/pre-commit can check its own basename, but it won't be with\n> configurable hooks.\n> \n> We could start passing down a GIT_HOOK_TYPE in the environment or\n> whatever, but I think the simpler case of just having the user do it is\n> better.\n> \n> I'm assuming that mainly because people have wanted a\n> \"/long/path/reusable-hook\" router script because we have not supported\n> executing N hooks, or supported concurrency. Once we do that the\n> complexity of such routing scripts (mostly, not everyone's needs will be\n> met) will be replaced by a little bit of config.\n\nThe above linked git-secrets is an example; the config is irritating and\ndoesn't really need to be there, when we could just teach the script\nitself how to check GIT_HOOK_TYPE envvar.\n\n> \n> I don't see why it's worth it to micro-optimize for lines in that config\n> at the cost of increased config complexity.\n> \n> For example, how do you \"skip\" an \"event\" type? You have it for\n> \"*.command\"? So let's say in your model (A) I have system config like:\n(The following isn't how my model A works, because it refers always to\n\"hook.<event>\", as I mentioned above. But I also covered this same topic\nin detail earlier in this email, so I'm going to snip this section.)\n\n> > #6 - Execute the same script early for one event and late for another\n> > event\n> > A:\n> > (global)\n> > [hookcmd \"reusable-hook\"]\n> > \tcommand = /long/path/reusable-hook\n> > [hook \"pre-commit\"]\n> > \tcommand = reusable-hook\n> > (local)\n> > [hook \"post-commit\"]\n> > \tcommand = other\n> > \tcommand = hooks\n> > \tcommand = reusable-hook\n> \n> Even with I think it's fair to say deep knowledge of your proposal at\n> this point I still needed to read this a few times to see if that:\n> \n>     command = reusable-hook\n> \n> Is referring to:\n> \n>     [hookcmd \"reusable-hook\"]\n> \n> I.e. is it going to run:\n> \n>     command = /long/path/reusable-hook\n> \n> Or is it just re-specifying /long/path/reusable-hook but relying on a\n> $PATH lookup?\n> \n> Having reasoned through that I think the answer is the former. But that\n> also means that in your model:\n> \n>     [hookcmd \"rm -rf /\"]\n>     command = echo this will not actually destroy your data\n>     [hook \"pre-commit\"]\n>     command = rm -rf /\n> \n> Is going to run that friendly \"echo\" command, since \"command = rm -rf /\"\n> just refers to the \"rm -rf /\" <name>, not <command>, right the \"hookcmd\"\n> line is removed, at which point we'll stop treating it as a <name> and\n> run it as a <command>?\n> \n> In practice I think users are unlikely to use actively harmful names\n> like that.\n> \n> I'm just making the point that I should not need to know about previous\n> config to see if a \"hook.pre-commit.command = rm -rf /\" is harmless or\n> not, or need to carefully squint to see if the \"reusable-hook\" is\n> referring to a section name or command name.\n> \n> Or am I just confused at this point?\n\nNah, that's an accurate understanding of how it would work with the\ncurrent implementation. Sure, it's a good point.\n\n> \n> > B:\n> > (global)\n> > [hook \"reusable-hook\"]\n> > \tcommand = /long/path/reusable-hook\n> > \tevent = pre-commit\n> > (local)\n> > [hook \"other\"]\n> > \tevent = post-commit\n> > \tcommand = other\n> > [hook \"hooks\"]\n> > \tevent = post-commit\n> > \tcommand = hooks\n> > [hook \"reusable-hook\"]\n> > \tevent = reusable-hook\n\nHm, I think I mistyped when I wrote \"event = reusable-hook\" here, and\nmeant to write \"event = post-commit\" to indicate that we would move\n\"reusable-hook\" to the bottom of the execution list for \"post-commit\".\n\nI'll snip most of the rebuttal about my typo.\n\n> \n>         (global)\n> \t[hook \"reusable-hook\"]\n> \t\tcommand = /long/path/reusable-hook\n> \t\tevent = pre-commit\n> \n> \t(local)\n> \t[hook \"reusable-hook\"]\n> \t\tcommand = true # skip it\n> \n>         # The \"hooks\" name is arbitrary, \"my-hooks\" or whatever would be\n>         # clearer, but just going with your example...\n> \n> \t[hook \"hooks\"]\n> \t\tevent = post-commit\n> \t\tcommand = hooks\n>         # Not very reusable then...   \n> \t[hook \"reusable-hook\"]\n>                 command = /long/path/reusable-hook\n> \t\tevent = pre-commit\n> \n> \n> > [...]Please feel free to chime in with more use cases that you think would\n> > be handy which I forgot :)\n> \n> I couldn't find this at a quick glance but I think we also had a\n> disussion at some point about hooks controlling parallelism. AFAICT your\n> current implementation just has global:\n> \n>     hook.jobs=N\n> \n> And we then blacklist certain hooks to always have hook.jobs=1, e.g. the\n> commit-msg hook that needs an implicit \"lock\" on that file (or rather,\n> we think that's the most common use-case).\n> \n> I think my version of always having hook.<name>.{event,command} be one\n> value is also better in that case, i.e. we'd then:\n> \n>     [hook \"myhook\"]\n>     command = some-command\n>     event = pre-receive\n>     parallel = true # the default\n> \n>     [hook \"myhook2\"]\n>     command = some-command2\n>     event = pre-receive\n>     parallel = true # the default\n> \n>     [hook \"myhook3\"]\n>     command = some-unconcurrent-command\n>     event = pre-receive\n>     parallel = false # I'm not OK with concurrency\n\nI am not sure what it means for a single executable to write \"parallel =\ntrue\" - it is a single executable.\n\nOk, that is me being facetious - I think you are saying we can AND\ntogether all of the 'hook.<thing-with-event-we-care-about>.parallel' to\ndecide whether or not to run in parallel.\n\nI would rather not discuss this now, for this series, because regardless\nof which config schema we use today, we can figure out \"parallel unless\nwe really don't want it\" later on. It is too complex to discuss in the\ncontext of \"hey, we should also configure hooks somewhere else\". Let's\nleave it for future work.\n\n> I haven't thought about it deeply, but have a hunch that having sections\n> be a 1=1 mapping with a specific command instead of either 1=1 or 1=many\n> is going to make that easier to implement and understand.\n\nSure, probably. But it can be added later to either schema... ;)\n\n\nAnyway, I did a quick strawpoll with my colleagues (Jonathan Nieder,\nJonathan Tan, and Josh Steadmon) and they all like your syntax more. I\nam ambivalent between the two, personally.\n\nSo what I will do, hopefully today, maybe not, is the following:\n\n - no more hookcmd. (and there was much rejoicing)\n - Hooks are specified with \"[hook \"name-of-hook\"]\"\n - I do see value in having an explicit .skip field rather than mapping\n   .command to a noop, so \"hook.name-of-hook.skip\" as described above.\n   Of course the method you described will work regardless, since its\n   mechanism is based on the inherent result of executing /bin/true.\n - I do see value in allowing \"hook.name-of-hook.event\" to be defined\n   repeatably, as described above, so I will include that.\n\nAnd left for later work:\n - teaching HOOK_EVENT_NAME=post-commit (etc)\n - figuring out what the heck we want to do with allowing hooks to\n   describe whether they allow parallelism\n\nUnfortunately I've dropped the design doc from this series, since nobody\nseemed interested in having it checked in, but I'll try to rework the\nhelp doc to make the schema more clear.\n\nThanks, I think it was useful to hash this out.\n\n - Emily\n"},{"id":"431991","messageId":"20210804214928.3689322-1-jonathantanmy@google.com","threadId":"55286","inReplyTo":"87bl6ttmlv.fsf@evledraar.gmail.com","subject":"Re: [PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2021-08-04T21:49:28Z","receivedAt":"2021-08-04T21:49:33Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"> > I would also need twice as many lines to run a script I wrote as a hook\n> > - that is, the base case which is probably all most people will need. So\n> > with your proposal I *must* name every hook I want to add. Instead of\n> > \"hook.pre-commit.command = ~/check-for-debug-strings\" I need to\n> > configure \"hook.check-debug-strings.event = pre-commit\" and\n> > \"hook.check-debug-strings.command = ~/check-for-debug-strings\". That's a\n> > little frustrating, and if I never want to skip that check or move it\n> > later or something, it will be extra overhead for no gain for me.\n> \n> The gain is that \"git hook list\" becomes a trivial \"git config\n> -show-origin --show-scope --get-regexp\" wrapper.\n\nI think that in both schemes, \"git hook list\" is not difficult to\nimplement.\n\n> > I do agree that your approach bans the regrettably awkward\n> > \"hookcmd.~/check-for-debug-strings.skip = true\", but I'm not sure\n> > whether or not it's worth it.\n> \n> That design choice also means that you can't expand the path using \"git\n> config --get --type=path.\n\nI'm not sure what the use of this is, especially when the more important\nposition is in \"hook.pre-commit.command\" (which can be expanded, I\nbelieve).\n\n> As noted in the follow-up reply while we don't case normalize the LeVeL\"\n> part of \"ThReE.LeVeL.KeY\" that's tolower(), which we know isn't a 1=1\n> mapping on some\n> FS's. https://lore.kernel.org/git/87y2ebo16v.fsf@evledraar.gmail.com/\n\nWe're comparing strings in the config file, though, not strings against\nwhat's on the filesystem.\n\n> To reply to all the above, yes, your suggestion comes out ahead in being\n> less verbose.\n> \n> But isn't the real difference not in the differing prefixes, i.e. hook.*\n> and hookcmd.* (A) v.s. always hook.* (B, which is what I'm mainly\n> focusing on, i.e. what requires the added complexity.\n\nOK, this is a good point - it may be confusing for the user to remember\nthe difference between \"hook\" and \"hookcmd\", and your scheme eliminates\nthat.\n\n> But that in that in your proposed way of doing it it's:\n> \n>     hook.<event-or-name>.*\n> \n> V.s. my suggestion of:\n> \n>     hook.<name>.*\n\n[snip discussion of how hook.<event-or-name> is bad]\n\nI think you were thinking that someone would say \"let's unify 'hook' and\n'hookcmd' then\" in response to you saying \"'hook' vs 'hookcmd' is bad\",\nand you're arguing against this new point. I don't think anyone is\nsaying that, though.\n\n> That complexity being that you use <event-or-name> and I use <name>, and\n> you want to in turn support N number of \"*.command\" for any\n> \"hook.<event-or-name>\".\n\nI don't know if anyone is wanting to support this.\n\n> The way to skip hooks in my proposal is:\n> \n>     hook.<name>.command = true\n> \n> Or whatever noop command you want on the RHS. In practice we wouldn't\n> invoke \"true\", just like we don't invoke \"cat\" for \"PAGER=cat\".\n> \n> But unlike \"*.skip\" that doesn't require complexity in implementation or\n> user understanding, it just falls naturally out of the rest of the\n> model.\n\nThis is a good point.\n\n> Even with I think it's fair to say deep knowledge of your proposal at\n> this point I still needed to read this a few times to see if that:\n> \n>     command = reusable-hook\n> \n> Is referring to:\n> \n>     [hookcmd \"reusable-hook\"]\n> \n> I.e. is it going to run:\n> \n>     command = /long/path/reusable-hook\n> \n> Or is it just re-specifying /long/path/reusable-hook but relying on a\n> $PATH lookup?\n\nThis is true - seeing \"hook.post-commit.command = reusable-hook\", we\nneed to look at the rest of the config to see how it is interpreted.\n\nGoing back to the central idea, though, I think that the main advantage\nof the scheme Emily proposed is the ability to write, more concisely:\n\n  [hook.pre-commit]\n  command = /path/to/command-1\n  command = /path/to/command-2\n\ninstead of\n\n  [hook.command-1]\n  event = pre-commit\n  command = /path/to/command-1\n  [hook.command-2]\n  event = pre-commit\n  command = /path/to/command-2\n\nBut Ævar's scheme gives us the advantage that if need to do anything\nmore complicated (even merely slightly more complicated - for example,\nskipping a hook or overriding the command of a hook), it would be hard\nto write (and even to just figure it out) in Emily's scheme. In Ævar's\nscheme, just following the standard \"last config wins\" rule (and knowing\nabout /usr/bin/true) can already do a lot. For this reason, I think it's\nworth considering Ævar's scheme - writing 3 lines instead of 2 is not\nmuch more difficult to teach and to do.\n"},{"id":"432027","messageId":"877dh0n1b3.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YQr6xojzxk3xJrTw@google.com","subject":"Re: [PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-05T00:17:29Z","receivedAt":"2021-08-05T01:11:02Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 04 2021, Emily Shaffer wrote:\n\n> On Fri, Jul 23, 2021 at 09:41:35AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> \n>> On Thu, Jul 22 2021, Emily Shaffer wrote:\n>> \n>> > On Fri, Jul 16, 2021 at 11:13:42AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> >> \n>> >> \n>> >> On Thu, Jul 15 2021, Emily Shaffer wrote:\n>> >> \n>> >> > To enable fine-grained options which apply to a single hook executable,\n>> >> > and to make it easier for a single executable to be run on multiple hook\n>> >> > events, teach \"hookcmd.<alias>.config\". These can be configured as\n>> >> > follows:\n>> >> > [...]\n>> >> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n>> >> > index a97b980cca..5b35170664 100644\n>> >> > --- a/Documentation/config/hook.txt\n>> >> > +++ b/Documentation/config/hook.txt\n>> >> > @@ -3,6 +3,11 @@ hook.<command>.command::\n>> >> >  \texecutable on your device, a oneliner for your shell, or the name of a\n>> >> >  \thookcmd. See linkgit:git-hook[1].\n>> >> >  \n>> >> > +hookcmd.<name>.command::\n>> >> > +\tA command to execute during a hook for which <name> has been specified\n>> >> > +\tas a command. This can be an executable on your device or a oneliner for\n>> >> > +\tyour shell. See linkgit:git-hook[1].\n>> >> > +\n>> >> > [...]\n>> >> > +Global config\n>> >> > +----\n>> >> > +  [hook \"post-commit\"]\n>> >> > +    command = \"linter\"\n>> >> > +    command = \"~/typocheck.sh\"\n>> >> > +\n>> >> > +  [hookcmd \"linter\"]\n>> >> > +    command = \"/bin/linter --c\"\n>> >> > +----\n>> >> > +\n>> >> > +Local config\n>> >> > +----\n>> >> > +  [hook \"prepare-commit-msg\"]\n>> >> > +    command = \"linter\"\n>> >> > +  [hook \"post-commit\"]\n>> >> > +    command = \"python ~/run-test-suite.py\"\n>> >> > +----\n>> >> > +\n>> >> > +With these configs, you'd then run post-commit hooks in this order:\n>> >> > +\n>> >> > +  /bin/linter --c\n>> >> > +  ~/typocheck.sh\n>> >> > +  python ~/run-test-suite.py\n>> >> > +  .git/hooks/post-commit (if present)\n>> >> > +\n>> >> > +and prepare-commit-msg hooks in this order:\n>> >> > +\n>> >> > +  /bin/linter --c\n>> >> > +  .git/hooks/prepare-commit-msg (if present)\n>> >> >  \n>> >> \n>> >> I still have outstanding feedback on the fundamental design\n>> >> here. I.e. why is this not:\n>> >> \n>> >>     hook.<name>.event = post-commit\n>> >>     hook.<name>.command = <path>\n>> >> \n>> >> See:\n>> >> \n>> >> https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n>> >> \n>> >> As noted there I don't see why not, and more complexity results from the\n>> >> design choice of doing it the way you're proposing. I.e. we can't\n>> >> discover hooks based on config prefixes, and we end up sticking full FS\n>> >> paths in config keys.\n>> >\n>> > Interesting. My gut says that it would make it harder to neatly write a\n>> > config with the same hook running at the very beginning of one event and\n>> > the very end of another, but I'm not sure whether that's a case to\n>> > optimize for.\n>> >\n>> > I would also need twice as many lines to run a script I wrote as a hook\n>> > - that is, the base case which is probably all most people will need. So\n>> > with your proposal I *must* name every hook I want to add. Instead of\n>> > \"hook.pre-commit.command = ~/check-for-debug-strings\" I need to\n>> > configure \"hook.check-debug-strings.event = pre-commit\" and\n>> > \"hook.check-debug-strings.command = ~/check-for-debug-strings\". That's a\n>> > little frustrating, and if I never want to skip that check or move it\n>> > later or something, it will be extra overhead for no gain for me.\n>> \n>> The gain is that \"git hook list\" becomes a trivial \"git config\n>> -show-origin --show-scope --get-regexp\" wrapper.\n>\n> This isn't a very compelling reason to me, if it makes the user\n> experience worse. (I'm not convinced that it does - just saying I\n> disagree with this reasoning.)\n\n...(see below)...\n\n>> \n>> So the series either doesn't need \"git hook list\" or such a thing\n>> becomes much less complex, especially given the proposed addition of\n>> other features in the area like \"git hook edit\", i.e. (quoting the\n>> linked E-Mail):\n>\n> I still think \"git hook list\" is useful to have for end-users, compared\n> to remembering the appropriate \"git config\" invocation. It futureproofs\n> us if we do want to change the execution ordering to something besides\n> config order, it's easier to remember/more discoverable, etc.\n\nTo clarify: I'm not against there being a \"git hook list\", I think we\nshould have it.\n\nIt's useful UI. I.e. we have \"run\", it makes sense to have the same\nmechanism spew out its idea of what hooks are where, especially since\nthat's not entirely contained in the config, but a union of config and\nGIT_DIR/hooks (and then there's core.hooksPath...).\n\nSo I'm very much for it being there.\n\nI've just been commenting on the relative complexity of the config\nschema, which in one way surfaces as not being able to do the config\npart of the \"git hook list\" as a simple shell-out (or equivalent library\ninvocation) of \"git config\" under the hood, but also means that users\nneed to mentally track a more complex format when e.g. manually editing\nconfig...\n\n>> [...]\n>> But isn't the real difference not in the differing prefixes, i.e. hook.*\n>> and hookcmd.* (A) v.s. always hook.* (B, which is what I'm mainly\n>> focusing on, i.e. what requires the added complexity.\n>> \n>> But that in that in your proposed way of doing it it's:\n>> \n>>     hook.<event-or-name>.*\n>> \n>> V.s. my suggestion of:\n>> \n>>     hook.<name>.*\n>> \n>\n> No, I think that's a misunderstanding of proposal A. My proposal A is\n> always hook.<event>.*, and hookcmd.<name>.*, and the way that I read\n> your proposal B is that it's always hook.<name>.*.\n\nAh, thanks.\n\n>> And thus whenever you have a <event-or-name> that just happens to be a\n>> built-in hook listed in githooks(5) we in (A) implicitly expand config\n>> like:\n>> \n>>     hook.post-commit.command = echo foo\n>> \n>> \n>>     hook.post-commit.command = echo hi\n>>     hook.post-commit.type    = post-commit\n>\n> This is where I'm becoming confused. I *think* you're saying that with\n> your proposal B, if the subsection happens to be the name of a built-in\n> hook, we can skip the .event field. But it also reads like you're saying\n> this is how my proposal A works, which isn't the case, because the\n> subsection is always assumed to be the name of the hook event we're\n> trying to run.\n\nI *think* this is best addressed below, moving on...\n\n>> \n>> But not knowing about \"foo\" we don't expand:\n>> \n>>     hook.foo.command = echo foo\n>> \n>> \n>>     hook.foo.command = echo hi\n>>     hook.foo.type    = foo # This would be an error, or ignored.\n>> \n>> But rather leave \"dangling\" for the user to later supply the \"*.event\"\n>> themselves, i.e.:\n>> \n>>     hook.foo.command = echo hi\n>>     hook.foo.event = post-commit\n>\n> If you're talking about proposal B, then no, this isn't how it works.\n> This config would imply that someone else (like a wrapper) can run \"echo\n> hi\" by asking for \"git hook run foo\". But I'm not sure exactly that\n> that's what you mean...\n\n...and on (maybe)....\n\n>> \n>> And means that you presumably need to detect this case and error about\n>> it, but my proposed model does not:\n>> \n>>     hook.post-commit.command = echo hi\n>>     # User error: \"*.type\" and <event-or-name>\" does not match for\n>>     # \"hook.*.command\"\n>>     hook.post-commit.type    = pre-commit\n>> \n>> And furthermore, that if someone in your model were to do:\n>> \n>>     hook.verify-commit.command = echo hi\n>> \n>> It's \"dangling\" today, but if a later version of git learns about a\n>> \"verify-commit\" hook we'll start running it unexpectedly.\n>\n> No, that's in fact as designed, with my model B. The user configured\n> \"echo hi\" to run on \"verify-commit\" events; if those events are\n> initially used by some wrapper, but later we decide they're a great idea\n> and absorb the verify-commit event into native Git, then this is working\n> as intended. I think your argument is based on a misunderstanding of the\n> design.\n>\n> Could it be that the way I provided the examples (my schema after A: and\n> your schema after B:) made it hard to parse? Sorry about that if so.\n\nAren't you assuming that users who specify a verify-commit will be happy\nbecause git's usurping of that will 1=1 match what they were using\n\"verify-commit\" for.\n\nI'm pointing out that we can't know that, and since you want to make\n\"git hook run\" a general thing that runs any <name> of script you've\nconfigured, and not just what's in githooks(5) that it becomes very\nlikely that if we add a new hook with some obvious name that we'll\neither break things for users, or subtly change behavior.\n\nWhich isn't just theoretical, e.g. I tend to run something like a \"git\nlog --check @{u}..\" before I run git-send-email, with this configurable\nhook mechanism having a \"git hook run sendemail-check\" would be a way I\nmight expose that in my own ~/.gitconfig.\n\nBut if git-send-email learns a \"sendemail-check\" and the behavior\ndoesn't exactly match mine; E.g. maybe it similar to pre-auto-gc expects\nme to return a status code to ask me if I want to abort on a failed\n--check, but mine expects a revision range to run \"log --check\".\n\nIn practice that's a non-issue with the current hook mechanism,\ni.e. nobody's sticking a script into .git/hooks/my-custom-name and\nexpecting it to do anything useful (and if they are, they have only\nthemselves to blame).\n\nWhereas we'd now be actively inviting users to squat on the same\nnamespace we ourselves will need for future hooks.\n\n>> \n>> Because your design conflates the slot for known hook type names and\n>> user-supplied names.\n>> \n>> On balance I think it's better just to always supply two lines per-hook,\n>> but whether we have this proposed shorthand or not is mostly orthogonal\n>> to everything I mentioned in\n>> https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n>> \n>> I.e. my proposed version could also have it, but thinking about it I\n>> think it's not worth it, we should always use <name>, not\n>> <event-or-name> for the reasons noted if you'll read ahead...\n>> \n>> > #4 - Skip a script configured more globally\n>> > A:\n>> > (original config)\n>> > [hook \"post-commit\"]\n>> > \tcommand = /corp/stuff/corp-post-commit\n>> > (local config)\n>> > [hookcmd \"/corp/stuff/corp-post-commit\"]\n>> > \tskip = true\n>> > OR,\n>> > (original config)\n>> > [hookcmd \"corp-pc\"]\n>> > \tcommand = /corp/stuff/corp-post-commit\n>> > [hook \"post-commit\"]\n>> > \tcommand = corp-pc\n>> > (local config)\n>> > [hookcmd \"corp-pc\"]\n>> > \tskip = true\n>> > B:\n>> > (original config)\n>> > [hook \"corp-pc\"]\n>> > \tevent = post-commit\n>> > \tcommand = /corp/stuff/corp-post-commit\n>> > (local config)\n>> > [hook \"corp-pc\"]\n>> > \tskip = true\n>> \n>> ...which are among other things that no, my proposed version doesn't\n>> have \"skip\" at all, see point #3 at\n>> https://lore.kernel.org/git/87mtv8fww3.fsf@evledraar.gmail.com/\n>> \n>> I.e. I think the \"skip\" is a thing that falls out of a complexity of\n>> your design that I'm proposing to do away with.\n>> \n>> That complexity being that you use <event-or-name> and I use <name>, and\n>> you want to in turn support N number of \"*.command\" for any\n>> \"hook.<event-or-name>\".\n>> \n>> I'm suggesting we use the typical \"last key wins\" semantics, if you want\n>> multiple commands for a given hook type you'll just supply multiple\n>> \"hook.<name>\" sections with the same \"hook.*.event = type\" in all.\n>> \n>> The way to skip hooks in my proposal is:\n>> \n>>     hook.<name>.command = true\n>\n> I see - instead of fiddly reorganizing the list, you're just changing\n> what the specific hook name is redirecting to.\n>\n> Then, instead of storing the resolved command in the 'struct hook', we\n> can store the name, and always dereference the name right before\n> execution, using last-one-wins semantics.\n>\n> The only thing I don't like about this is that it's a little bit\n> confusing to the user. I personally would rather have more complicated\n> code and less complicated user experience, so I like 'skip' more because\n> it's readable and because you don't need to guess \"oh, it'll just\n> execute /bin/true which is a noop\". That's easy to understand for\n> someone familiar with Unix scripting (and therefore for we Git\n> contributors), but confusing for everybody else.\n>\n> Maybe we can take the essential spirit of this implementation but make\n> it a little bit simpler. In pseudocode:\n>\n> populate_list(hookname):\n>   # look for everybody who says \"hook.*.event = $hookname\"\n>   hooks = get_configs_matching(hook.*.event, hookname)\n>   # the \"*\" bit is the bit we dereference later\n>   for hook in hooks:\n>     hook_list.add({.name = hook.key.subsection})\n>\n> run_hooks(hook_list):\n>   for hook in hook_list:\n>     # should we even run?\n>     if (get_last_config(\"hook.\" + hook.name + \".skip\"))\n>       continue\n>\n>     # dereference the hook name and find out the command\n>     cmd = get_last_config(\"hook.\" + hook.name + \".command\")\n>     run(cmd)\n>\n> The price for this improved readability is an extra config lookup in\n> each case. But the code is still readable, definitely moreso than\n> rewalking the config list over and over as the implementation stands\n> now. I personally think it's worth it.\n\nI think the less complicated user experience is for our config system to\nwork as consistently as possible with other config. If we define special\nrules and what's effectively special syntax with \"skip\" etc. we're\nasking users to read how that works, and keep that all in their head\njust to deal with that one area of our config.\n\n> (One could also imagine a more readable placeholder than /bin/true in\n> the \"hook.<name>.command\" field, but then we get to decide just how hard\n> we want to avoid colliding with legitimate user hook names. Is .command\n> = skip enough? is .command = RESERVED_NAME_SKIP_THIS_HOOK enough? Is\n> something unrunnable like .command = \"skip me\" enough, although it could\n> conceivably collide with a determined user?)\n\nAll of that's something you'll need to explain in detail to users, which\nall seems way more complex than a simple:\n\n    To skip a previously defined hook insert a noop-command, any will\n    do, but setting it to \"true\" (usually /bin/true) is a handy\n    convention for doing nothing.\n\nI.e. by keeping the config field as doing one thing only you avoid any\nsuch collisions etc.\n\n>> \n>> Or whatever noop command you want on the RHS. In practice we wouldn't\n>> invoke \"true\", just like we don't invoke \"cat\" for \"PAGER=cat\".\n>> \n>> But unlike \"*.skip\" that doesn't require complexity in implementation or\n>> user understanding, it just falls naturally out of the rest of the\n>> model.\n>> \n>> > #5 - Execute one script for multiple events\n>> > A:\n>> > [hookcmd \"reusable-hook\"]\n>> > \tcommand = /long/path/reusable-hook\n>> > [hook \"post-commit\"]\n>> > \tcommand = reusable-hook\n>> > [hook \"pre-commit\"]\n>> > \tcommand = reusable-hook\n>> > [hook \"prepare-commit-msg\"]\n>> > \tcommand = reusable-hook\n>> > B:\n>> > [hook \"reusable-hook\"]\n>> > \tcommand = /long/path/reusable-hook\n>> > \tevent = post-commit\n>> > \tevent = pre-commit\n>> > \tevent = prepare-commit-msg\n>> \n>> It's been so long since I wrote the original E-Mail that I'm having to\n>> skim my own summary there & come up with things as I go along, so maybe\n>> this conflicts with something I said earlier.\n>> \n>> But no, I don't think I meant that *.event should be multi-value, or\n>> that that magic is worthwhile. I.e. I think we just want:\n>> \n>>     [hook \"not-a-reusable-section-1\"]\n>>         command = /long/path/reusable-hook\n>>         event = post-commit\n>>     [hook \"not-a-reusable-section-2\"]\n>>         command = /long/path/reusable-hook\n>>         event = pre-commit\n>>     [hook \"not-a-reusable-section-3\"]\n>>         command = /long/path/reusable-hook\n>>         event = prepare-commit-msg\n>> \n>> I.e. is wanting to use the same command for N different hooks really\n>> that common a use-case to avoid a little verbosity, and quite a lot of\n>> complexity?\n>> \n>> How would such a hook even be implemented?\n>> \n>> We don't have anything in your hook execution model (and I don't\n>> remember you adding this) which passes down a \"you are this type of\n>> hook\" to hooks.\n>\n> No, but it's something I'm interested in passing as an environment\n> variable. I didn't add it to this series because it seemed to me to\n> distract from the core feature. We would like to add it to simplify our\n> invocations of https://github.com/awslabs/git-secrets, so it's on my\n> radar.\n\nHaving such an env var as part of the initial series seems like a\nsensible thing to have.\n\n> And even without this, just today I wanted to configure a hook to really\n> make sure I had config.mak set up in all my Git worktrees (since you\n> pointed out I pushed code that fails -Wall.... ;) ) and set it up to run\n> on both pre-commit and post-checkout. So yes, it is feasible now, for\n> very stupid hooks.\n\nAside: It's nice to use DEVELOPER=1 in config.mak (or as make param)\nwhen hacking on git, it'll set -Wall -Werror and other nice warning\nflags.\n\n> [...]\n>> > [...]Please feel free to chime in with more use cases that you think would\n>> > be handy which I forgot :)\n>> \n>> I couldn't find this at a quick glance but I think we also had a\n>> disussion at some point about hooks controlling parallelism. AFAICT your\n>> current implementation just has global:\n>> \n>>     hook.jobs=N\n>> \n>> And we then blacklist certain hooks to always have hook.jobs=1, e.g. the\n>> commit-msg hook that needs an implicit \"lock\" on that file (or rather,\n>> we think that's the most common use-case).\n>> \n>> I think my version of always having hook.<name>.{event,command} be one\n>> value is also better in that case, i.e. we'd then:\n>> \n>>     [hook \"myhook\"]\n>>     command = some-command\n>>     event = pre-receive\n>>     parallel = true # the default\n>> \n>>     [hook \"myhook2\"]\n>>     command = some-command2\n>>     event = pre-receive\n>>     parallel = true # the default\n>> \n>>     [hook \"myhook3\"]\n>>     command = some-unconcurrent-command\n>>     event = pre-receive\n>>     parallel = false # I'm not OK with concurrency\n>\n> I am not sure what it means for a single executable to write \"parallel =\n> true\" - it is a single executable.\n>\n> Ok, that is me being facetious - I think you are saying we can AND\n> together all of the 'hook.<thing-with-event-we-care-about>.parallel' to\n> decide whether or not to run in parallel.\n\nRight, the case (whatever the config mechanism) wanting to use several\noff-the-shelf hooks and accomplish through git some version of this:\n\n    parallel -j8 pre-receive-parallel-*.sh &&\n    parallel -j1 pre-receive-non-parallel-*.sh\n\nI.e. since we have N scripts for the \"pre-receive\" type, and we're\nexpecting to say whether on not parallelism is OK or not, it seems like\na natural thing we'll want to declare that differently for some of those\nthan for others.\n\n> I would rather not discuss this now, for this series, because regardless\n> of which config schema we use today, we can figure out \"parallel unless\n> we really don't want it\" later on. It is too complex to discuss in the\n> context of \"hey, we should also configure hooks somewhere else\". Let's\n> leave it for future work.\n\nThe point is that no, we really can't figure it out as easily later on\nregardless of the config schema.\n\nBecause with 1=many you can't have 1=many.someAttribute=XYZ without that\n*.someAttribute=XYZ declaring something for all of 1=many, whereas if\nit's 1=1 then 1=1.someAttribute.XYZ obviously applies only to that 1=1.\n\n>> I haven't thought about it deeply, but have a hunch that having sections\n>> be a 1=1 mapping with a specific command instead of either 1=1 or 1=many\n>> is going to make that easier to implement and understand.\n>\n> Sure, probably. But it can be added later to either schema... ;)\n>\n>\n> Anyway, I did a quick strawpoll with my colleagues (Jonathan Nieder,\n> Jonathan Tan, and Josh Steadmon) and they all like your syntax more. I\n> am ambivalent between the two, personally.\n>\n> So what I will do, hopefully today, maybe not, is the following:\n>\n>  - no more hookcmd. (and there was much rejoicing)\n>  - Hooks are specified with \"[hook \"name-of-hook\"]\"\n\nThanks, I look forward to checking that out. As should be obvious from\nmy misunderstanding of some of your config proposal (I snippet out some\nof those from the reply) I'm still not entirely clear on even what the\ncurrent proposed behavior is, hopefully something simpler will be easier\nto grok ... :)\n\n>  - I do see value in having an explicit .skip field rather than mapping\n>    .command to a noop, so \"hook.name-of-hook.skip\" as described above.\n>    Of course the method you described will work regardless, since its\n>    mechanism is based on the inherent result of executing /bin/true.\n\nI think we've mainly focused on the theoretical aspect of this, but FWIW\nI'm still entirely unclear on what this feature is even aimed for.\n\nAll of the rest of our config does not have an explicit \"skip\" for\nanything, just last-set-wins. In terms of a real-world use-case wouldn't\na user just edit or comment out the config earlier in ~/.gitconfig, and\nnot \"skip\" it at the end with \"git config [...] --add\"?\n\nI suspect that the use-case is some Googly centrally managed\n/etc/gitconfig, but that's just speculation...\n\n>  - I do see value in allowing \"hook.name-of-hook.event\" to be defined\n>    repeatably, as described above, so I will include that.\n> [...]\n>  - figuring out what the heck we want to do with allowing hooks to\n>    describe whether they allow parallelism\n\nWhich just to check if we're on the same page, needs to be figured out\nbecause of the complexity of that \"defined repeatably\", no?\n\n> Unfortunately I've dropped the design doc from this series, since nobody\n> seemed interested in having it checked in, but I'll try to rework the\n> help doc to make the schema more clear.\n\nFWIW I wasn't opposed to it per-se, but remembered commenting on it in\nearlier rounds, and it being some combination of docs that should really\nbe in manpages (i.e. describing current actual behavior), more general\nrational and \"why is it like this\" (which correctly belongs in design\ndocs), and musings about hypothetical future features (e.g. \"git hook\nedit\" and the like)'.\n"},{"id":"432138","messageId":"YQxcDZSEvrRKIN+L@google.com","threadId":"55286","inReplyTo":"877dh0n1b3.fsf@evledraar.gmail.com","subject":"Re: [PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-05T21:45:49Z","receivedAt":"2021-08-05T21:45:58Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Aug 05, 2021 at 02:17:29AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> >> The gain is that \"git hook list\" becomes a trivial \"git config\n> >> -show-origin --show-scope --get-regexp\" wrapper.\n> >\n> > This isn't a very compelling reason to me, if it makes the user\n> > experience worse. (I'm not convinced that it does - just saying I\n> > disagree with this reasoning.)\n> \n> ...(see below)...\n> \n> >> \n> >> So the series either doesn't need \"git hook list\" or such a thing\n> >> becomes much less complex, especially given the proposed addition of\n> >> other features in the area like \"git hook edit\", i.e. (quoting the\n> >> linked E-Mail):\n> >\n> > I still think \"git hook list\" is useful to have for end-users, compared\n> > to remembering the appropriate \"git config\" invocation. It futureproofs\n> > us if we do want to change the execution ordering to something besides\n> > config order, it's easier to remember/more discoverable, etc.\n> \n> To clarify: I'm not against there being a \"git hook list\", I think we\n> should have it.\n> \n> It's useful UI. I.e. we have \"run\", it makes sense to have the same\n> mechanism spew out its idea of what hooks are where, especially since\n> that's not entirely contained in the config, but a union of config and\n> GIT_DIR/hooks (and then there's core.hooksPath...).\n> \n> So I'm very much for it being there.\n> \n> I've just been commenting on the relative complexity of the config\n> schema, which in one way surfaces as not being able to do the config\n> part of the \"git hook list\" as a simple shell-out (or equivalent library\n> invocation) of \"git config\" under the hood, but also means that users\n> need to mentally track a more complex format when e.g. manually editing\n> config...\n\nSure, makes sense.\n\n> >> \n> >> And means that you presumably need to detect this case and error about\n> >> it, but my proposed model does not:\n> >> \n> >>     hook.post-commit.command = echo hi\n> >>     # User error: \"*.type\" and <event-or-name>\" does not match for\n> >>     # \"hook.*.command\"\n> >>     hook.post-commit.type    = pre-commit\n> >> \n> >> And furthermore, that if someone in your model were to do:\n> >> \n> >>     hook.verify-commit.command = echo hi\n> >> \n> >> It's \"dangling\" today, but if a later version of git learns about a\n> >> \"verify-commit\" hook we'll start running it unexpectedly.\n> >\n> > No, that's in fact as designed, with my model B. The user configured\n> > \"echo hi\" to run on \"verify-commit\" events; if those events are\n> > initially used by some wrapper, but later we decide they're a great idea\n> > and absorb the verify-commit event into native Git, then this is working\n> > as intended. I think your argument is based on a misunderstanding of the\n> > design.\n> >\n> > Could it be that the way I provided the examples (my schema after A: and\n> > your schema after B:) made it hard to parse? Sorry about that if so.\n> \n> Aren't you assuming that users who specify a verify-commit will be happy\n> because git's usurping of that will 1=1 match what they were using\n> \"verify-commit\" for.\n> \n> I'm pointing out that we can't know that, and since you want to make\n> \"git hook run\" a general thing that runs any <name> of script you've\n> configured, and not just what's in githooks(5) that it becomes very\n> likely that if we add a new hook with some obvious name that we'll\n> either break things for users, or subtly change behavior.\n> \n> Which isn't just theoretical, e.g. I tend to run something like a \"git\n> log --check @{u}..\" before I run git-send-email, with this configurable\n> hook mechanism having a \"git hook run sendemail-check\" would be a way I\n> might expose that in my own ~/.gitconfig.\n> \n> But if git-send-email learns a \"sendemail-check\" and the behavior\n> doesn't exactly match mine; E.g. maybe it similar to pre-auto-gc expects\n> me to return a status code to ask me if I want to abort on a failed\n> --check, but mine expects a revision range to run \"log --check\".\n> \n> In practice that's a non-issue with the current hook mechanism,\n> i.e. nobody's sticking a script into .git/hooks/my-custom-name and\n> expecting it to do anything useful (and if they are, they have only\n> themselves to blame).\n> \n> Whereas we'd now be actively inviting users to squat on the same\n> namespace we ourselves will need for future hooks.\n\nYeah, this is a good point. Seems worth a note in the 'git hook run'\ndoc, making a point that \"you can use this for your wrapper to run\nspecific hooks, but be careful about namespace collisions\". We're a lot\nless likely to add a hook named \"repotool-verify-commit\" than we are to\nadd a hook named \"verify-commit\", for example.\n\nI think it's enough to warn about future namespace collisions and make\nan \"at your own risk\" note.\n\n> > Maybe we can take the essential spirit of this implementation but make\n> > it a little bit simpler. In pseudocode:\n> >\n> > populate_list(hookname):\n> >   # look for everybody who says \"hook.*.event = $hookname\"\n> >   hooks = get_configs_matching(hook.*.event, hookname)\n> >   # the \"*\" bit is the bit we dereference later\n> >   for hook in hooks:\n> >     hook_list.add({.name = hook.key.subsection})\n> >\n> > run_hooks(hook_list):\n> >   for hook in hook_list:\n> >     # should we even run?\n> >     if (get_last_config(\"hook.\" + hook.name + \".skip\"))\n> >       continue\n> >\n> >     # dereference the hook name and find out the command\n> >     cmd = get_last_config(\"hook.\" + hook.name + \".command\")\n> >     run(cmd)\n> >\n> > The price for this improved readability is an extra config lookup in\n> > each case. But the code is still readable, definitely moreso than\n> > rewalking the config list over and over as the implementation stands\n> > now. I personally think it's worth it.\n> \n> I think the less complicated user experience is for our config system to\n> work as consistently as possible with other config. If we define special\n> rules and what's effectively special syntax with \"skip\" etc. we're\n> asking users to read how that works, and keep that all in their head\n> just to deal with that one area of our config.\n> \n> > (One could also imagine a more readable placeholder than /bin/true in\n> > the \"hook.<name>.command\" field, but then we get to decide just how hard\n> > we want to avoid colliding with legitimate user hook names. Is .command\n> > = skip enough? is .command = RESERVED_NAME_SKIP_THIS_HOOK enough? Is\n> > something unrunnable like .command = \"skip me\" enough, although it could\n> > conceivably collide with a determined user?)\n> \n> All of that's something you'll need to explain in detail to users, which\n> all seems way more complex than a simple:\n> \n>     To skip a previously defined hook insert a noop-command, any will\n>     do, but setting it to \"true\" (usually /bin/true) is a handy\n>     convention for doing nothing.\n> \n> I.e. by keeping the config field as doing one thing only you avoid any\n> such collisions etc.\n\nI disagree fundamentally that \"find and run a noop command like\n/bin/true\" is simpler to average users than \"skip it by setting a\nconfig\". Like I said below, by including \"skip\" both approaches will\nwork.\n\n> > No, but it's something I'm interested in passing as an environment\n> > variable. I didn't add it to this series because it seemed to me to\n> > distract from the core feature. We would like to add it to simplify our\n> > invocations of https://github.com/awslabs/git-secrets, so it's on my\n> > radar.\n> \n> Having such an env var as part of the initial series seems like a\n> sensible thing to have.\n\nEh. To me, it feels like feature creep. It also is something we could\nadd today to the existing hook mechanism (even if it's a little\npointless since you can basename, like you say), so it feels orthogonal.\nI would prefer not to add it in this series.\n\n> \n> > And even without this, just today I wanted to configure a hook to really\n> > make sure I had config.mak set up in all my Git worktrees (since you\n> > pointed out I pushed code that fails -Wall.... ;) ) and set it up to run\n> > on both pre-commit and post-checkout. So yes, it is feasible now, for\n> > very stupid hooks.\n> \n> Aside: It's nice to use DEVELOPER=1 in config.mak (or as make param)\n> when hacking on git, it'll set -Wall -Werror and other nice warning\n> flags.\n\nThis is literally what I was saying I wrote the hook to do, and is also\na tip I included when I wrote MyFirstContribution.txt.\n\n> > I am not sure what it means for a single executable to write \"parallel =\n> > true\" - it is a single executable.\n> >\n> > Ok, that is me being facetious - I think you are saying we can AND\n> > together all of the 'hook.<thing-with-event-we-care-about>.parallel' to\n> > decide whether or not to run in parallel.\n> \n> Right, the case (whatever the config mechanism) wanting to use several\n> off-the-shelf hooks and accomplish through git some version of this:\n> \n>     parallel -j8 pre-receive-parallel-*.sh &&\n>     parallel -j1 pre-receive-non-parallel-*.sh\n> \n> I.e. since we have N scripts for the \"pre-receive\" type, and we're\n> expecting to say whether on not parallelism is OK or not, it seems like\n> a natural thing we'll want to declare that differently for some of those\n> than for others.\n> \n> > I would rather not discuss this now, for this series, because regardless\n> > of which config schema we use today, we can figure out \"parallel unless\n> > we really don't want it\" later on. It is too complex to discuss in the\n> > context of \"hey, we should also configure hooks somewhere else\". Let's\n> > leave it for future work.\n> \n> The point is that no, we really can't figure it out as easily later on\n> regardless of the config schema.\n> \n> Because with 1=many you can't have 1=many.someAttribute=XYZ without that\n> *.someAttribute=XYZ declaring something for all of 1=many, whereas if\n> it's 1=1 then 1=1.someAttribute.XYZ obviously applies only to that 1=1.\n\nI think this is moot, since we are moving to \"all config hooks have a\nname\", but my plan previously was to let this be set on a hookcmd.\nEssentially, your suggestion is to make every hook a hookcmd. My point\nwas that it's easy to extend [object which represents an executable] in\nthe config to include \"always run me in series\" or \"run me in series for\nthis specific event\" regardless. That is, one could imagine, discarding\nentirely the hookcmd junk and going with the schema I sketched in my\nlast email (which lands somewhere between yours and mine):\n\n[hook \"linter\"]\n  command = ~/linter.sh\n  event = pre-commit\n  parallel = false\n\nor...\n\n[hook \"linter\"]\n  command = ~/linter.sh\n  event = pre-commit\n  event = commit-msg\n\n[hook \"linter.commit-msg\"]\n  parallel = false\n\nOr even...\n\n[hook \"linter\"]\n  command = ~/linter.sh\n  event = pre-commit\n  event = commit-msg\n  parallel = commit-msg\n\nThe possibilities go on, as far as configuration goes.\n\nTo me, the harder part of this problem is actually implementing the\nexecution. We had some discussions at length early on in the\nconfig-based hook series about ways to do this kind of complex \"some\nstuff needs synchronous execution and some stuff doesn't, in the same\nevent\" and decided that it mostly resolved to \"you ain't gonna need it\"\nprinciple. So I would prefer to discuss this when we find out we do\nactually need it.\n\n> \n> >> I haven't thought about it deeply, but have a hunch that having sections\n> >> be a 1=1 mapping with a specific command instead of either 1=1 or 1=many\n> >> is going to make that easier to implement and understand.\n> >\n> > Sure, probably. But it can be added later to either schema... ;)\n> >\n> >\n> > Anyway, I did a quick strawpoll with my colleagues (Jonathan Nieder,\n> > Jonathan Tan, and Josh Steadmon) and they all like your syntax more. I\n> > am ambivalent between the two, personally.\n> >\n> > So what I will do, hopefully today, maybe not, is the following:\n> >\n> >  - no more hookcmd. (and there was much rejoicing)\n> >  - Hooks are specified with \"[hook \"name-of-hook\"]\"\n> \n> Thanks, I look forward to checking that out. As should be obvious from\n> my misunderstanding of some of your config proposal (I snippet out some\n> of those from the reply) I'm still not entirely clear on even what the\n> current proposed behavior is, hopefully something simpler will be easier\n> to grok ... :)\n\nFingers crossed ;)\n\n> \n> >  - I do see value in having an explicit .skip field rather than mapping\n> >    .command to a noop, so \"hook.name-of-hook.skip\" as described above.\n> >    Of course the method you described will work regardless, since its\n> >    mechanism is based on the inherent result of executing /bin/true.\n> \n> I think we've mainly focused on the theoretical aspect of this, but FWIW\n> I'm still entirely unclear on what this feature is even aimed for.\n> \n> All of the rest of our config does not have an explicit \"skip\" for\n> anything, just last-set-wins. In terms of a real-world use-case wouldn't\n> a user just edit or comment out the config earlier in ~/.gitconfig, and\n> not \"skip\" it at the end with \"git config [...] --add\"?\n> \n> I suspect that the use-case is some Googly centrally managed\n> /etc/gitconfig, but that's just speculation...\n\nYep, this is exactly why. We've talked often on-list about how we ship\nand configure Git for Googlers, but the upshot is \"we pack up 'next' and\nalso ship an '/etc/gitconfig'\".\n\nBut I can also think of one really basic scenario when I'd want to skip\na hook in one repo without just commenting out my ~/.gitconfig: the\nGerrit Change-Id hook.\n\nGerrit requires all commit messages to contain this Change-Id: abc123\nfooter. It adds the footer by way of a commit-msg hook. That hook works\nthe same regardless of what your Gerrit remote is, so you can run the\nsame script on any project that uses Gerrit for code review. If, as I have in\nthe past, the vast majority of my projects use Gerrit, but I have one\nproject which does not, then I would love to configure the Gerrit\nChange-Id hook globally and un-configure it for my one non-Gerrit\nproject.\n\n(At that time, I maintained a subsystem in a project based on Yocto, so\nI needed to regularly contribute to 5-10 projects, all but one of which\nused Gerrit. The one non-Gerrit one used a mailing list. I also had a\nhobby project and my dotfiles, neither of which used Gerrit. This is not\nan uncommon use case.)\n\n> \n> >  - I do see value in allowing \"hook.name-of-hook.event\" to be defined\n> >    repeatably, as described above, so I will include that.\n> > [...]\n> >  - figuring out what the heck we want to do with allowing hooks to\n> >    describe whether they allow parallelism\n> \n> Which just to check if we're on the same page, needs to be figured out\n> because of the complexity of that \"defined repeatably\", no?\n\nAs I wrote above, I think this falls under YAGNI, to be honest, and I\ndon't think we've painted ourselves into a corner if we do end up\nneeding it. I think the semantical complexity is a harder problem than\nthe syntactical complexity, and I'd rather just punt it ;)\n\n> \n> > Unfortunately I've dropped the design doc from this series, since nobody\n> > seemed interested in having it checked in, but I'll try to rework the\n> > help doc to make the schema more clear.\n> \n> FWIW I wasn't opposed to it per-se, but remembered commenting on it in\n> earlier rounds, and it being some combination of docs that should really\n> be in manpages (i.e. describing current actual behavior), more general\n> rational and \"why is it like this\" (which correctly belongs in design\n> docs), and musings about hypothetical future features (e.g. \"git hook\n> edit\" and the like)'.\n\nYeah, that's pretty typical of design docs at places I've worked now and\nin the past. I don't have a big problem with not checking it in - those\nkinds of docs are most valuable during the early design stage, and so I\nthink that doc has now served its purpose.\n\n - Emily\n"},{"id":"432145","messageId":"87sfznzd7v.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YQxcDZSEvrRKIN+L@google.com","subject":"Re: [PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-05T22:26:02Z","receivedAt":"2021-08-05T23:25:13Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Aug 05 2021, Emily Shaffer wrote:\n\n> On Thu, Aug 05, 2021 at 02:17:29AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> [...]\n>> > No, that's in fact as designed, with my model B. The user configured\n>> > \"echo hi\" to run on \"verify-commit\" events; if those events are\n>> > initially used by some wrapper, but later we decide they're a great idea\n>> > and absorb the verify-commit event into native Git, then this is working\n>> > as intended. I think your argument is based on a misunderstanding of the\n>> > design.\n>> >\n>> > Could it be that the way I provided the examples (my schema after A: and\n>> > your schema after B:) made it hard to parse? Sorry about that if so.\n>> \n>> Aren't you assuming that users who specify a verify-commit will be happy\n>> because git's usurping of that will 1=1 match what they were using\n>> \"verify-commit\" for.\n>> \n>> I'm pointing out that we can't know that, and since you want to make\n>> \"git hook run\" a general thing that runs any <name> of script you've\n>> configured, and not just what's in githooks(5) that it becomes very\n>> likely that if we add a new hook with some obvious name that we'll\n>> either break things for users, or subtly change behavior.\n>> \n>> Which isn't just theoretical, e.g. I tend to run something like a \"git\n>> log --check @{u}..\" before I run git-send-email, with this configurable\n>> hook mechanism having a \"git hook run sendemail-check\" would be a way I\n>> might expose that in my own ~/.gitconfig.\n>> \n>> But if git-send-email learns a \"sendemail-check\" and the behavior\n>> doesn't exactly match mine; E.g. maybe it similar to pre-auto-gc expects\n>> me to return a status code to ask me if I want to abort on a failed\n>> --check, but mine expects a revision range to run \"log --check\".\n>> \n>> In practice that's a non-issue with the current hook mechanism,\n>> i.e. nobody's sticking a script into .git/hooks/my-custom-name and\n>> expecting it to do anything useful (and if they are, they have only\n>> themselves to blame).\n>> \n>> Whereas we'd now be actively inviting users to squat on the same\n>> namespace we ourselves will need for future hooks.\n>\n> Yeah, this is a good point. Seems worth a note in the 'git hook run'\n> doc, making a point that \"you can use this for your wrapper to run\n> specific hooks, but be careful about namespace collisions\". We're a lot\n> less likely to add a hook named \"repotool-verify-commit\" than we are to\n> add a hook named \"verify-commit\", for example.\n>\n> I think it's enough to warn about future namespace collisions and make\n> an \"at your own risk\" note.\n\nI might have lost track at this point, but later examples in this E-Mail\nyou show don't seem to require such a note.\n\nI.e. it's only an issue if we conflate a semantically meaningful slot\nlike \"pre-commit\" in the config with one that can also have the meaning\nof simply defining an arbitrary user-decided name.\n\nThere's no such collision if the config uses\ne.g. hook.mycheck.event=pre-commit & hook.mycheck.command=mycmd, as\nopposed to hook.pre-commit.command=mycmd.\n\nOn the specifics of that example: I don't really care about the\nbikeshedding of the config key naming specifics, just the semantics of\nnot putting user defined names and hook type names in the same slot if\nwe can avoid it.\n\n>> > No, but it's something I'm interested in passing as an environment\n>> > variable. I didn't add it to this series because it seemed to me to\n>> > distract from the core feature. We would like to add it to simplify our\n>> > invocations of https://github.com/awslabs/git-secrets, so it's on my\n>> > radar.\n>> \n>> Having such an env var as part of the initial series seems like a\n>> sensible thing to have.\n>\n> Eh. To me, it feels like feature creep. It also is something we could\n> add today to the existing hook mechanism (even if it's a little\n> pointless since you can basename, like you say), so it feels orthogonal.\n> I would prefer not to add it in this series.\n\nSure, I guess you can add two hook sections to replace e.g. your\n{pre,post}-receive hook (which are commonly routed to the same script\nwith file-based hooks). Having a single setenv() seems easy enough, and\nI'd bet a way more common use-case than wanting to skip earlier defined\nhooks...\n\n>> > I am not sure what it means for a single executable to write \"parallel =\n>> > true\" - it is a single executable.\n>> >\n>> > Ok, that is me being facetious - I think you are saying we can AND\n>> > together all of the 'hook.<thing-with-event-we-care-about>.parallel' to\n>> > decide whether or not to run in parallel.\n>> \n>> Right, the case (whatever the config mechanism) wanting to use several\n>> off-the-shelf hooks and accomplish through git some version of this:\n>> \n>>     parallel -j8 pre-receive-parallel-*.sh &&\n>>     parallel -j1 pre-receive-non-parallel-*.sh\n>> \n>> I.e. since we have N scripts for the \"pre-receive\" type, and we're\n>> expecting to say whether on not parallelism is OK or not, it seems like\n>> a natural thing we'll want to declare that differently for some of those\n>> than for others.\n>> \n>> > I would rather not discuss this now, for this series, because regardless\n>> > of which config schema we use today, we can figure out \"parallel unless\n>> > we really don't want it\" later on. It is too complex to discuss in the\n>> > context of \"hey, we should also configure hooks somewhere else\". Let's\n>> > leave it for future work.\n>> \n>> The point is that no, we really can't figure it out as easily later on\n>> regardless of the config schema.\n>> \n>> Because with 1=many you can't have 1=many.someAttribute=XYZ without that\n>> *.someAttribute=XYZ declaring something for all of 1=many, whereas if\n>> it's 1=1 then 1=1.someAttribute.XYZ obviously applies only to that 1=1.\n>\n> I think this is moot, since we are moving to \"all config hooks have a\n> name\", but my plan previously was to let this be set on a hookcmd.\n> Essentially, your suggestion is to make every hook a hookcmd. My point\n> was that it's easy to extend [object which represents an executable] in\n> the config to include \"always run me in series\" or \"run me in series for\n> this specific event\" regardless. That is, one could imagine, discarding\n> entirely the hookcmd junk and going with the schema I sketched in my\n> last email (which lands somewhere between yours and mine):\n\nJust to be clear, I don't have any concrete suggestion in mind right now\n(actually as I write this I can only vaguely recall what I suggested\nbefore).\n\nWhat I have been suggesting is not any specific implementation, but that\nwe have a bias for the simple over the complex for an initial\nimplementation.\n\nComplexity can always be added later, whereas coming up with a config\nschema that's irregular compared to other existing config in git is\nsomething we might regret sooner than later.\n\n> [hook \"linter\"]\n>   command = ~/linter.sh\n>   event = pre-commit\n>   parallel = false\n>\n> or...\n>\n> [hook \"linter\"]\n>   command = ~/linter.sh\n>   event = pre-commit\n>   event = commit-msg\n>\n> [hook \"linter.commit-msg\"]\n>   parallel = false\n>\n> Or even...\n>\n> [hook \"linter\"]\n>   command = ~/linter.sh\n>   event = pre-commit\n>   event = commit-msg\n>   parallel = commit-msg\n>\n> The possibilities go on, as far as configuration goes.\n>\n> To me, the harder part of this problem is actually implementing the\n> execution. We had some discussions at length early on in the\n> config-based hook series about ways to do this kind of complex \"some\n> stuff needs synchronous execution and some stuff doesn't, in the same\n> event\" and decided that it mostly resolved to \"you ain't gonna need it\"\n> principle. So I would prefer to discuss this when we find out we do\n> actually need it.\n\nWhat I was mainly going for with \"we really can't figure it out as\neasily later\" above was not that this tweaking of \"jobs\" or parallelism\nwas essential per-hook.\n\nBut that it was a handy shorthand for a config attribute you might want\nto define for hooks, and having what are effectively groups of hooks,\nwith N \"command\" or \"event\" in one section might make things more\ncomplex once you'd want to define optional attributes for one of those\ncommands or events.\n\n>> [...]\n>> >  - I do see value in having an explicit .skip field rather than mapping\n>> >    .command to a noop, so \"hook.name-of-hook.skip\" as described above.\n>> >    Of course the method you described will work regardless, since its\n>> >    mechanism is based on the inherent result of executing /bin/true.\n>> \n>> I think we've mainly focused on the theoretical aspect of this, but FWIW\n>> I'm still entirely unclear on what this feature is even aimed for.\n>> \n>> All of the rest of our config does not have an explicit \"skip\" for\n>> anything, just last-set-wins. In terms of a real-world use-case wouldn't\n>> a user just edit or comment out the config earlier in ~/.gitconfig, and\n>> not \"skip\" it at the end with \"git config [...] --add\"?\n>> \n>> I suspect that the use-case is some Googly centrally managed\n>> /etc/gitconfig, but that's just speculation...\n>\n> Yep, this is exactly why. We've talked often on-list about how we ship\n> and configure Git for Googlers, but the upshot is \"we pack up 'next' and\n> also ship an '/etc/gitconfig'\".\n>\n> But I can also think of one really basic scenario when I'd want to skip\n> a hook in one repo without just commenting out my ~/.gitconfig: the\n> Gerrit Change-Id hook.\n>\n> Gerrit requires all commit messages to contain this Change-Id: abc123\n> footer. It adds the footer by way of a commit-msg hook. That hook works\n> the same regardless of what your Gerrit remote is, so you can run the\n> same script on any project that uses Gerrit for code review. If, as I have in\n> the past, the vast majority of my projects use Gerrit, but I have one\n> project which does not, then I would love to configure the Gerrit\n> Change-Id hook globally and un-configure it for my one non-Gerrit\n> project.\n>\n> (At that time, I maintained a subsystem in a project based on Yocto, so\n> I needed to regularly contribute to 5-10 projects, all but one of which\n> used Gerrit. The one non-Gerrit one used a mailing list. I also had a\n> hobby project and my dotfiles, neither of which used Gerrit. This is not\n> an uncommon use case.)\n\n> I disagree fundamentally that \"find and run a noop command like\n> /bin/true\" is simpler to average users than \"skip it by setting a\n> config\". Like I said below, by including \"skip\" both approaches will\n> work.\n\nIn reply to this, and moving things around a bit in the reply:\n\n>> All of that's something you'll need to explain in detail to users, which\n>> all seems way more complex than a simple:\n>> \n>>     To skip a previously defined hook insert a noop-command, any will\n>>     do, but setting it to \"true\" (usually /bin/true) is a handy\n>>     convention for doing nothing.\n>> \n>> I.e. by keeping the config field as doing one thing only you avoid any\n>> such collisions etc.\n>\n> I disagree fundamentally that \"find and run a noop command like\n> /bin/true\" is simpler to average users than \"skip it by setting a\n> config\". Like I said below, by including \"skip\" both approaches will\n> work.\n\nTo clarify, I haven't been advocating for that \"skip = true\" convention\nbecause I think it's a sensible thing per-se, but that I think this\nuse-case is something that an individual configurable feature in git\ndoesn't need stateful syntax to deal with.\n\nWe have any number of multi-value and single-value config within git. I\njust don't see why on balance hooks need a special syntax to skip\nearlier set config for hooks specifically.\n\nE.g. this gerrit example would also be true of someone in a corporate\nsetting using git-send-email, and wanting a list of sendemail.cc on all\nbut their dotfiles project, or one other non-work project.\n\nDoes that mean we need a sendemail.skipCC and special handling for it in\ngit-send-email.perl? No, I think we'd generally advice users to just put\nthose projects under ~/work or whatever, and then use config includes to\nset config for that group of projects based on the path:\n\n    [includeIf \"gitdir:~/work/\"]\n        path = ~/.gitconfig.d/work\n\nOr, if a hook is really so special that it's needed everywhere define it\nin /etc/gitconfig, and then just make the hook itself do:\n\n    if test \"$(git config --bool googleHooks.disableOurGlobalHook)\" = \"true\"\n    then\n        exit 0\n    fi\n\nWhich is pretty much (with the hook.* config prefix) how we've adviced\nusers to do this since approximately forever with the sample hooks we\nship.\n\nThe advantage of using includes in that way is e.g. that you can easily\nsee how your hook came to be configured with:\n\n    git config --list --show-origin\n\nI.e. that (by convention) it comes via a conditionally included\n~/.gitconfig.d/gerrit file. If it's a multi-value like sendemail.cc the\nsemantics are also clear, e.g. you can get all values we'll use with\n\"git config --get-all\".\n\nWhereas choosing to implement this with something that *looks* like a\nconfig keyword, but really isn't is just confusing.\n\nWe need to explain in one way how users might arrange for the likes of\nsendemail.cc to be defined for some, but not all of their repos, and\nexplain it differently when it comes to hooks. There's inherent value in\nthat explanation being the same for both.\n"},{"id":"432204","messageId":"YQ2ZBw4vZswlSkYq@google.com","threadId":"55286","inReplyTo":"87sfznzd7v.fsf@evledraar.gmail.com","subject":"Re: [PATCH 8/9] hook: teach 'hookcmd' config to alias hook scripts","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-06T20:18:15Z","receivedAt":"2021-08-06T20:18:24Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Aug 06, 2021 at 12:26:02AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Thu, Aug 05 2021, Emily Shaffer wrote:\n> \n> > On Thu, Aug 05, 2021 at 02:17:29AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> >> [...]\n> >> > No, that's in fact as designed, with my model B. The user configured\n> >> > \"echo hi\" to run on \"verify-commit\" events; if those events are\n> >> > initially used by some wrapper, but later we decide they're a great idea\n> >> > and absorb the verify-commit event into native Git, then this is working\n> >> > as intended. I think your argument is based on a misunderstanding of the\n> >> > design.\n> >> >\n> >> > Could it be that the way I provided the examples (my schema after A: and\n> >> > your schema after B:) made it hard to parse? Sorry about that if so.\n> >> \n> >> Aren't you assuming that users who specify a verify-commit will be happy\n> >> because git's usurping of that will 1=1 match what they were using\n> >> \"verify-commit\" for.\n> >> \n> >> I'm pointing out that we can't know that, and since you want to make\n> >> \"git hook run\" a general thing that runs any <name> of script you've\n> >> configured, and not just what's in githooks(5) that it becomes very\n> >> likely that if we add a new hook with some obvious name that we'll\n> >> either break things for users, or subtly change behavior.\n> >> \n> >> Which isn't just theoretical, e.g. I tend to run something like a \"git\n> >> log --check @{u}..\" before I run git-send-email, with this configurable\n> >> hook mechanism having a \"git hook run sendemail-check\" would be a way I\n> >> might expose that in my own ~/.gitconfig.\n> >> \n> >> But if git-send-email learns a \"sendemail-check\" and the behavior\n> >> doesn't exactly match mine; E.g. maybe it similar to pre-auto-gc expects\n> >> me to return a status code to ask me if I want to abort on a failed\n> >> --check, but mine expects a revision range to run \"log --check\".\n> >> \n> >> In practice that's a non-issue with the current hook mechanism,\n> >> i.e. nobody's sticking a script into .git/hooks/my-custom-name and\n> >> expecting it to do anything useful (and if they are, they have only\n> >> themselves to blame).\n> >> \n> >> Whereas we'd now be actively inviting users to squat on the same\n> >> namespace we ourselves will need for future hooks.\n> >\n> > Yeah, this is a good point. Seems worth a note in the 'git hook run'\n> > doc, making a point that \"you can use this for your wrapper to run\n> > specific hooks, but be careful about namespace collisions\". We're a lot\n> > less likely to add a hook named \"repotool-verify-commit\" than we are to\n> > add a hook named \"verify-commit\", for example.\n> >\n> > I think it's enough to warn about future namespace collisions and make\n> > an \"at your own risk\" note.\n> \n> I might have lost track at this point, but later examples in this E-Mail\n> you show don't seem to require such a note.\n> \n> I.e. it's only an issue if we conflate a semantically meaningful slot\n> like \"pre-commit\" in the config with one that can also have the meaning\n> of simply defining an arbitrary user-decided name.\n\nNo, I don't think that's the case. The examples I used later in the mail\ndon't have anything to do with this \"wrapper wants to use 'git hooks\nrun'\" case.\n\nLet's imagine a current commonly-used wrapper, 'repo'\n(https://gerrit.googlesource.com/git-repo/+/refs/heads/master/README.md).\nThis wrapper is used to manage multiple Git repos into something\nsemantically similar to submodules.\n\nLet's say that 'repo' decides that instead of rolling its own hook\nsystem, it would like to use Git's native hook system. Let's say that it\nwants to call user-defined hooks any time someone has just completed\nrunning 'repo sync'. It could ask users to define a config like so:\n\n  [hook \"myrepohook\"]\n    event = post-sync\n    command = ~/some/downloaded/myrepohook --with --args\n\nThen 'repo' itself can, at the end of its 'repo sync' implementation,\ncall 'git hook run post-sync' (instead of using some other hook\nspecification schema).\n\nBut Git itself has a 'git submodule sync' command, and let's imagine\nthat we decided we want to run some user-defined hook at the end of 'git\nsubmodule sync'. Naturally, we can call it 'post-sync', and then we will\naccidentally invoke the hooks which someone configured for 'repo' - as\nyou pointed out would be a concern with my design, this will *also* be\na concern with your design.\n\nThis specific use case is what I think we can get around with some \"at\nyour own risk\" documentation, because we are much less likely to collide\nwith someone asking their users to configure like so:\n\n  [hook \"myrepohook\"]\n    event = repo-tool-post-sync\n    command = ~/some/downloaded/myrepohook --with-args\n\n> >> > No, but it's something I'm interested in passing as an environment\n> >> > variable. I didn't add it to this series because it seemed to me to\n> >> > distract from the core feature. We would like to add it to simplify our\n> >> > invocations of https://github.com/awslabs/git-secrets, so it's on my\n> >> > radar.\n> >> \n> >> Having such an env var as part of the initial series seems like a\n> >> sensible thing to have.\n> >\n> > Eh. To me, it feels like feature creep. It also is something we could\n> > add today to the existing hook mechanism (even if it's a little\n> > pointless since you can basename, like you say), so it feels orthogonal.\n> > I would prefer not to add it in this series.\n> \n> Sure, I guess you can add two hook sections to replace e.g. your\n> {pre,post}-receive hook (which are commonly routed to the same script\n> with file-based hooks). Having a single setenv() seems easy enough, and\n> I'd bet a way more common use-case than wanting to skip earlier defined\n> hooks...\n\nMore than anything else, I think this comment convinced me that the skip\nconfig also falls under YAGNI and I can drop it (and we can add it later\nif \"hook.myhook.command = true\" isn't clear enough).\n\nI snipped the rest because I don't disagree or have anything further to\nsay. Look for a reroll this weekend, we hope.\n\n - Emily\n"},{"id":"432550","messageId":"20210812004258.74318-1-emilyshaffer@google.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v2 0/6] config-based hooks restarted","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-12T00:42:52Z","receivedAt":"2021-08-12T00:43:14Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"This is the config-based hooks topic rebased onto v4 of Ævar's\nbranch[1]. There is a happy CI build of it on GitHub[2].\n\nThe topic overall adds the ability to set up hooks by modifying the\nconfig, in addition to placing specially named hooks into the hookdir.\nThis enables users to specify multiple hooks for a given event, and so\nthis topic also fleshes out the use of the run_processes_parallel() API\nwhich is now introduced in Ævar's reordering of prior patches.\n\nPatches 1-4 make some minor changes to prepare Ævar's series to handle\nmore than one hook at a time. With the exception of patch 4, there\nshould be no behavior change for existing hooks.\n\nPatch 2 is opinionated about which hooks should and shouldn't be allowed\nto run in parallel; if you care about a specific hook, please take a\nlook there.\n\nPatch 5 is the motivating feature - it begins to parse the config\nlooking for hooks.\n\nPatch 6 takes advantage of the decoupling of hooks and GITDIR to allow\nout-of-repo hook runs, which would only run hooks specified in the\nsystem or global config. This mainly targets 'sendemail-validate', but\n'git-sendemail.perl' still explicitly disallows out-of-repo hook\nexecution on that hook for now. (Maybe that change should be added in\nthis series? Or maybe patch 6 belongs with that kind of change?)\n\nSince v1:\n\nThe largest change is that the config schema is different, following the\ndiscussion from [3] and on. 'hookcmd' has gone away entirely, and all\nconfig-specified hooks need a user-provided name associated with them.\n\nI have also dropped the 'skip' config from this series (also discussed\nin [3]) as users can reset their 'hook.myhook.command' to 'true' or some\nother noop in a later config to effectively disable any hook. The 'skip'\nfeature may be added later or may not, depending on if users ask us for\nit.\n\nThe 'from_hookdir' field has been removed entirely. When storing hooks\nby name instead of by command, it makes sense enough to store the one\nfrom the hookdir in a special way - NULL name - because it does not have\na name to specify. This way we can use that field as an indicator that\nthe hook came from the hookdir, and we also do not need to reserve a\nname to use for hookdir hooks (as opposed to setting hookdir hooks to\nhave name = \"FROM_HOOKDIR\" or something).\n\nThe tests have been moved into t1800-hook.sh and simplified.\n\nThe 'struct run_hooks_opt' initializer has been moved to macros (as\nopposed to functions), and .jobs=0 indicates that we should look up the\nconfigured job count or nproc at hook run time. Ævar asked me to change\nthese macros to RUN_HOOKS_OPT_INIT and RUN_HOOKS_OPT_INIT_SYNC or\nsomething to simplify the merge, but I did not do that - I removed all\ninstances of RUN_HOOKS_OPT_INIT and now everybody uses\nRUN_HOOKS_OPT_INIT_SYNC or RUN_HOOKS_OPT_INIT_ASYNC. I think it's\nimportant for contributors to be able to tell at a glance whether the\nhook event expects resource contention that would prevent parallelism. I\nalso think it makes the review easier (if longer) for folks who are\nreviewing trying to decide whether the parallelism is appropriate for\neach hook event. I do think that run_hooks_oneshot() also is a step\nbackwards in this regard, but I didn't get a chance to say so on the\ncommit that introduces it yet.\n\nThanks in advance, all.\n\n - Emily\n\n1: https://lore.kernel.org/git/cover-v4-00.36-00000000000-20210803T191505Z-avarab%40gmail.com\n2: https://github.com/nasamuffin/git/actions/runs/1122126800 (which\npoints to an earlier successul run before I messed with the commit\nmessages)\n3: https://lore.kernel.org/git/87fswey5wd.fsf%40evledraar.gmail.com\n\nEmily Shaffer (6):\n  hook: run a list of hooks instead\n  hook: allow parallel hook execution\n  hook: introduce \"git hook list\"\n  hook: allow running non-native hooks\n  hook: include hooks from the config\n  hook: allow out-of-repo 'git hook' invocations\n\n Documentation/config/hook.txt |   9 ++\n Documentation/git-hook.txt    |  48 +++++-\n builtin/am.c                  |   4 +-\n builtin/checkout.c            |   2 +-\n builtin/clone.c               |   2 +-\n builtin/hook.c                |  68 +++++++-\n builtin/merge.c               |   2 +-\n builtin/rebase.c              |   2 +-\n builtin/receive-pack.c        |   9 +-\n builtin/worktree.c            |   2 +-\n commit.c                      |   2 +-\n git.c                         |   2 +-\n hook.c                        | 289 +++++++++++++++++++++++++++++-----\n hook.h                        |  61 +++++--\n read-cache.c                  |   2 +-\n refs.c                        |   2 +-\n reset.c                       |   3 +-\n sequencer.c                   |   4 +-\n t/t1800-hook.sh               | 161 ++++++++++++++++++-\n transport.c                   |   2 +-\n 20 files changed, 596 insertions(+), 80 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n\nRange-diff against v1:\n1:  c4b95fb08a < -:  ---------- hook: treat hookdir hook specially\n-:  ---------- > 1:  5177e8ba2c hook: run a list of hooks instead\n-:  ---------- > 2:  eda439cd57 hook: allow parallel hook execution\n-:  ---------- > 3:  cdfe3b6e16 hook: introduce \"git hook list\"\n2:  e6a56ac674 = 4:  eb4e03e22b hook: allow running non-native hooks\n3:  32ad49ea9b ! 5:  2c8e874158 hook: include hooks from the config\n    @@ Commit message\n         hooks to run for a given event.\n     \n         Multiple commands can be specified for a given hook by providing\n    -    multiple \"hook.<hookname>.command = <path-to-hook>\" lines. Hooks will be\n    -    run in config order.\n    +    multiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n    +    \"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be run in\n    +    config order of the \"hook.<name>.event\" lines.\n     \n         For example:\n     \n           $ git config --list | grep ^hook\n    -      hook.pre-commit.command=~/bar.sh\n    +      hook.bar.command=~/bar.sh\n    +      hook.bar.event=pre-commit\n     \n           $ git hook run\n           # Runs ~/bar.sh\n    @@ hook.c: const char *find_hook_gently(const char *name)\n     -\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n     -\t\tto_add->hook_path = hook_path;\n     -\t\tto_add->feed_pipe_cb_data = NULL;\n    --\t\tto_add->from_hookdir = 1;\n     -\t\tlist_add_tail(&to_add->list, hook_head);\n     -\t}\n     +\t/* Add the hook from the hookdir. The placeholder makes it easier to\n    @@ hook.c: static int pick_next_hook(struct child_process *cp,\n     +\tcp->use_shell = !!run_me->name;\n     +\n      \t/* add command */\n    --\tif (run_me->from_hookdir && hook_cb->options->absolute_path)\n    +-\tif (hook_cb->options->absolute_path)\n     -\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n     -\telse\n     -\t\tstrvec_push(&cp->args, run_me->hook_path);\n    @@ hook.h: int hook_exists(const char *hookname);\n      \tstruct list_head list;\n     -\t/* The path to the hook */\n     -\tconst char *hook_path;\n    --\n    --\tunsigned from_hookdir : 1;\n     +\t/*\n     +\t * The friendly name of the hook. NULL indicates the hook is from the\n     +\t * hookdir.\n4:  91e54185b3 = 6:  3216e51b6b hook: allow out-of-repo 'git hook' invocations\n-- \n2.32.0.605.g8dce9f2422-goog\n\n"},{"id":"432551","messageId":"20210812004258.74318-2-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210812004258.74318-1-emilyshaffer@google.com","subject":"[PATCH v2 1/6] hook: run a list of hooks instead","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-12T00:42:53Z","receivedAt":"2021-08-12T00:43:17Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"To prepare for multihook support, teach hook.[hc] to take a list of\nhooks at run_hooks and run_found_hooks. Right now the list is always one\nentry, but in the future we will allow users to supply more than one\nexecutable for a single hook event.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/hook.c |  14 ++++---\n hook.c         | 103 +++++++++++++++++++++++++++++++++++--------------\n hook.h         |  16 +++++++-\n 3 files changed, 96 insertions(+), 37 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 5eb7cf73a4..4d39c9e75e 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -25,7 +25,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint ignore_missing = 0;\n \tconst char *hook_name;\n-\tconst char *hook_path;\n+\tstruct list_head *hooks;\n+\n \tstruct option run_options[] = {\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n@@ -58,15 +59,16 @@ static int run(int argc, const char **argv, const char *prefix)\n \tgit_config(git_default_config, NULL);\n \n \thook_name = argv[0];\n-\tif (ignore_missing)\n-\t\treturn run_hooks_oneshot(hook_name, &opt);\n-\thook_path = find_hook(hook_name);\n-\tif (!hook_path) {\n+\thooks = hook_list(hook_name);\n+\tif (list_empty(hooks)) {\n+\t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n+\t\tif (ignore_missing)\n+\t\t\treturn 0;\n \t\terror(\"cannot find a hook named %s\", hook_name);\n \t\treturn 1;\n \t}\n \n-\tret = run_hooks(hook_name, hook_path, &opt);\n+\tret = run_hooks(hook_name, hooks, &opt);\n \trun_hooks_opt_clear(&opt);\n \treturn ret;\n usage:\ndiff --git a/hook.c b/hook.c\nindex ee20b2e365..80e150548c 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -4,6 +4,28 @@\n #include \"hook-list.h\"\n #include \"config.h\"\n \n+static void free_hook(struct hook *ptr)\n+{\n+\tif (ptr) {\n+\t\tfree(ptr->feed_pipe_cb_data);\n+\t}\n+\tfree(ptr);\n+}\n+\n+static void remove_hook(struct list_head *to_remove)\n+{\n+\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n+\tlist_del(to_remove);\n+\tfree_hook(hook_to_remove);\n+}\n+\n+void clear_hook_list(struct list_head *head)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, head)\n+\t\tremove_hook(pos);\n+}\n+\n static int known_hook(const char *name)\n {\n \tconst char **p;\n@@ -71,6 +93,30 @@ int hook_exists(const char *name)\n \treturn !!find_hook(name);\n }\n \n+struct list_head* hook_list(const char* hookname)\n+{\n+\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\n+\tINIT_LIST_HEAD(hook_head);\n+\n+\tif (!hookname)\n+\t\treturn NULL;\n+\n+\tif (have_git_dir()) {\n+\t\tconst char *hook_path = find_hook(hookname);\n+\n+\t\t/* Add the hook from the hookdir */\n+\t\tif (hook_path) {\n+\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n+\t\t\tto_add->hook_path = hook_path;\n+\t\t\tto_add->feed_pipe_cb_data = NULL;\n+\t\t\tlist_add_tail(&to_add->list, hook_head);\n+\t\t}\n+\t}\n+\n+\treturn hook_head;\n+}\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o)\n {\n \tstrvec_clear(&o->env);\n@@ -128,7 +174,10 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n-\tstrvec_push(&cp->args, run_me->hook_path);\n+\tif (hook_cb->options->absolute_path)\n+\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n+\telse\n+\t\tstrvec_push(&cp->args, run_me->hook_path);\n \n \t/*\n \t * add passed-in argv, without expanding - let the user get back\n@@ -139,12 +188,12 @@ static int pick_next_hook(struct child_process *cp,\n \t/* Provide context for errors if necessary */\n \t*pp_task_cb = run_me;\n \n-\t/*\n-\t * This pick_next_hook() will be called again, we're only\n-\t * running one hook, so indicate that no more work will be\n-\t * done.\n-\t */\n-\thook_cb->run_me = NULL;\n+\t/* Get the next entry ready */\n+\tif (hook_cb->run_me->list.next == hook_cb->head)\n+\t\thook_cb->run_me = NULL;\n+\telse\n+\t\thook_cb->run_me = list_entry(hook_cb->run_me->list.next,\n+\t\t\t\t\t     struct hook, list);\n \n \treturn 1;\n }\n@@ -179,13 +228,9 @@ static int notify_hook_finished(int result,\n \treturn 0;\n }\n \n-int run_hooks(const char *hook_name, const char *hook_path,\n-\t      struct run_hooks_opt *options)\n+int run_hooks(const char *hook_name, struct list_head *hooks,\n+\t\t    struct run_hooks_opt *options)\n {\n-\tstruct strbuf abs_path = STRBUF_INIT;\n-\tstruct hook my_hook = {\n-\t\t.hook_path = hook_path,\n-\t};\n \tstruct hook_cb_data cb_data = {\n \t\t.rc = 0,\n \t\t.hook_name = hook_name,\n@@ -197,11 +242,9 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n-\tif (options->absolute_path) {\n-\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n-\t\tmy_hook.hook_path = abs_path.buf;\n-\t}\n-\tcb_data.run_me = &my_hook;\n+\n+\tcb_data.head = hooks;\n+\tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n \n \trun_processes_parallel_tr2(jobs,\n \t\t\t\t   pick_next_hook,\n@@ -213,18 +256,15 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n \n-\n-\tif (options->absolute_path)\n-\t\tstrbuf_release(&abs_path);\n-\tfree(my_hook.feed_pipe_cb_data);\n+\tclear_hook_list(hooks);\n \n \treturn cb_data.rc;\n }\n \n int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n {\n-\tconst char *hook_path;\n-\tint ret;\n+\tstruct list_head *hooks;\n+\tint ret = 0;\n \tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n \n \tif (!options)\n@@ -233,14 +273,19 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n \tif (options->path_to_stdin && options->feed_pipe)\n \t\tBUG(\"choose only one method to populate stdin\");\n \n-\thook_path = find_hook(hook_name);\n-\tif (!hook_path) {\n-\t\tret = 0;\n+\thooks = hook_list(hook_name);\n+\n+\t/*\n+\t * If you need to act on a missing hook, use run_found_hooks()\n+\t * instead\n+\t */\n+\tif (list_empty(hooks))\n \t\tgoto cleanup;\n-\t}\n \n-\tret = run_hooks(hook_name, hook_path, options);\n+\tret = run_hooks(hook_name, hooks, options);\n+\n cleanup:\n \trun_hooks_opt_clear(options);\n+\tclear_hook_list(hooks);\n \treturn ret;\n }\ndiff --git a/hook.h b/hook.h\nindex 58dfbf474c..7705e6a529 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -3,6 +3,7 @@\n #include \"strbuf.h\"\n #include \"strvec.h\"\n #include \"run-command.h\"\n+#include \"list.h\"\n \n /*\n  * Returns the path to the hook file, or NULL if the hook is missing\n@@ -17,6 +18,7 @@ const char *find_hook(const char *name);\n int hook_exists(const char *hookname);\n \n struct hook {\n+\tstruct list_head list;\n \t/* The path to the hook */\n \tconst char *hook_path;\n \n@@ -27,6 +29,12 @@ struct hook {\n \tvoid *feed_pipe_cb_data;\n };\n \n+/*\n+ * Provides a linked list of 'struct hook' detailing commands which should run\n+ * in response to the 'hookname' event, in execution order.\n+ */\n+struct list_head* hook_list(const char *hookname);\n+\n struct run_hooks_opt\n {\n \t/* Environment vars to be set for each hook */\n@@ -97,6 +105,7 @@ struct hook_cb_data {\n \t/* rc reflects the cumulative failure state */\n \tint rc;\n \tconst char *hook_name;\n+\tstruct list_head *head;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n \tint *invoked_hook;\n@@ -110,8 +119,8 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);\n  *\n  * See run_hooks_oneshot() for the simpler one-shot API.\n  */\n-int run_hooks(const char *hookname, const char *hook_path,\n-\t      struct run_hooks_opt *options);\n+int run_hooks(const char *hookname, struct list_head *hooks,\n+\t\t    struct run_hooks_opt *options);\n \n /**\n  * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n@@ -123,4 +132,7 @@ int run_hooks(const char *hookname, const char *hook_path,\n  */\n int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n \n+/* Empties the list at 'head', calling 'free_hook()' on each entry */\n+void clear_hook_list(struct list_head *head);\n+\n #endif\n-- \n2.32.0.605.g8dce9f2422-goog\n\n"},{"id":"432552","messageId":"20210812004258.74318-3-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210812004258.74318-1-emilyshaffer@google.com","subject":"[PATCH v2 2/6] hook: allow parallel hook execution","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-12T00:42:54Z","receivedAt":"2021-08-12T00:43:18Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"In many cases, there's no reason not to allow hooks to execute in\nparallel. run_processes_parallel() is well-suited - it's a task queue\nthat runs its housekeeping in series, which means users don't\nneed to worry about thread safety on their callback data. True\nmultithreaded execution with the async_* functions isn't necessary here.\nSynchronous hook execution can be achieved by only allowing 1 job to run\nat a time.\n\nTeach run_hooks() to use that function for simple hooks which don't\nrequire stdin or capture of stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nHelped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/config/hook.txt |  4 ++++\n Documentation/git-hook.txt    | 17 ++++++++++++++++-\n builtin/am.c                  |  4 ++--\n builtin/checkout.c            |  2 +-\n builtin/clone.c               |  2 +-\n builtin/hook.c                |  4 +++-\n builtin/merge.c               |  2 +-\n builtin/rebase.c              |  2 +-\n builtin/receive-pack.c        |  9 +++++----\n builtin/worktree.c            |  2 +-\n commit.c                      |  2 +-\n hook.c                        | 36 +++++++++++++++++++++++++++++++----\n hook.h                        | 24 ++++++++++++++++++-----\n read-cache.c                  |  2 +-\n refs.c                        |  2 +-\n reset.c                       |  3 ++-\n sequencer.c                   |  4 ++--\n transport.c                   |  2 +-\n 18 files changed, 94 insertions(+), 29 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nnew file mode 100644\nindex 0000000000..96d3d6572c\n--- /dev/null\n+++ b/Documentation/config/hook.txt\n@@ -0,0 +1,4 @@\n+hook.jobs::\n+\tSpecifies how many hooks can be run simultaneously during parallelized\n+\thook execution. If unspecified, defaults to the number of processors on\n+\tthe current system.\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex fa68c1f391..8bf82b5dd4 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,8 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n+\t<hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -42,6 +43,20 @@ OPTIONS\n \ttools that want to do a blind one-shot run of a hook that may\n \tor may not be present.\n \n+-j::\n+--jobs::\n+\tOnly valid for `run`.\n++\n+Specify how many hooks to run simultaneously. If this flag is not specified, use\n+the value of the `hook.jobs` config. If the config is not specified, use the\n+number of CPUs on the current system. Some hooks may be ineligible for\n+parallelization: for example, 'commit-msg' intends hooks modify the commit\n+message body and cannot be parallelized.\n+\n+CONFIGURATION\n+-------------\n+include::config/hook.txt[]\n+\n SEE ALSO\n --------\n linkgit:githooks[5]\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 9e3d4d9ab4..c24a27d6a1 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -446,7 +446,7 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n \n \tassert(state->msg);\n \tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n@@ -467,7 +467,7 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \n \tstrvec_push(&opt.args, \"rebase\");\n \topt.path_to_stdin = am_path(state, \"rewritten\");\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 6d69b4c011..27166c0bb8 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -107,7 +107,7 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n \n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 27fc05ee51..599e7a7936 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -776,7 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_SYNC;\n \n \tif (option_no_checkout)\n \t\treturn 0;\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 4d39c9e75e..12c9126032 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -22,7 +22,7 @@ static const char * const builtin_hook_run_usage[] = {\n static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n \tint ignore_missing = 0;\n \tconst char *hook_name;\n \tstruct list_head *hooks;\n@@ -32,6 +32,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n \t\t\t   N_(\"file to read into hooks' stdin\")),\n+\t\tOPT_INTEGER('j', \"jobs\", &opt.jobs,\n+\t\t\t    N_(\"run up to <n> hooks simultaneously\")),\n \t\tOPT_END(),\n \t};\n \tint ret;\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 9bd4a2532c..c749c382c3 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,7 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex e7c668c99b..fecf248ed9 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1314,7 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ebec6f3bb1..b32dcc9000 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -909,7 +909,7 @@ static int run_receive_hook(struct command *commands,\n \t\t\t    int skip_broken,\n \t\t\t    const struct string_list *push_options)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \tstruct receive_hook_feed_context ctx;\n \tstruct command *iter = commands;\n \n@@ -948,7 +948,7 @@ static int run_receive_hook(struct command *commands,\n \n static int run_update_hook(struct command *cmd)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \n \tstrvec_pushl(&opt.args,\n \t\t     cmd->ref_name,\n@@ -1432,7 +1432,8 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n+\n \topt.invoked_hook = invoked_hook;\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n@@ -1628,7 +1629,7 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 330867c19b..efead564c1 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -382,7 +382,7 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n \n \t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n \t\tstrvec_pushl(&opt.args,\ndiff --git a/commit.c b/commit.c\nindex 842e47beae..0e6e5a5b27 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1700,7 +1700,7 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook,\n \t\t    const char *name, ...)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n \tva_list args;\n \tconst char *arg;\n \ndiff --git a/hook.c b/hook.c\nindex 80e150548c..37f682c6d8 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -228,6 +228,28 @@ static int notify_hook_finished(int result,\n \treturn 0;\n }\n \n+/*\n+ * Determines how many jobs to use after we know we want to parallelize. First\n+ * priority is the config 'hook.jobs' and second priority is the number of CPUs.\n+ */\n+static int configured_hook_jobs(void)\n+{\n+\t/*\n+\t * The config and the CPU count probably won't change during the process\n+\t * lifetime, so cache the result in case we invoke multiple hooks during\n+\t * one process.\n+\t */\n+\tstatic int jobs = 0;\n+\tif (jobs)\n+\t\treturn jobs;\n+\n+\tif (git_config_get_int(\"hook.jobs\", &jobs))\n+\t\t/* if the config isn't set, fall back to CPU count. */\n+\t\tjobs = online_cpus();\n+\n+\treturn jobs;\n+}\n+\n int run_hooks(const char *hook_name, struct list_head *hooks,\n \t\t    struct run_hooks_opt *options)\n {\n@@ -237,16 +259,18 @@ int run_hooks(const char *hook_name, struct list_head *hooks,\n \t\t.options = options,\n \t\t.invoked_hook = options->invoked_hook,\n \t};\n-\tint jobs = 1;\n \n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n-\n \tcb_data.head = hooks;\n \tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n \n-\trun_processes_parallel_tr2(jobs,\n+\t/* INIT_ASYNC sets jobs to 0, so go look up how many to use. */\n+\tif (!options->jobs)\n+\t\toptions->jobs = configured_hook_jobs();\n+\n+\trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n@@ -265,7 +289,11 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n {\n \tstruct list_head *hooks;\n \tint ret = 0;\n-\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n+\t/*\n+\t * Turn on parallelism by default. Hooks which don't want it should\n+\t * specify their options accordingly.\n+\t */\n+\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT_ASYNC;\n \n \tif (!options)\n \t\toptions = &hook_opt_scratch;\ndiff --git a/hook.h b/hook.h\nindex 7705e6a529..4f90228a0c 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -43,6 +43,13 @@ struct run_hooks_opt\n \t/* Args to be passed to each hook */\n \tstruct strvec args;\n \n+\t/*\n+\t * Number of threads to parallelize across. Set to 0 to use the\n+\t * 'hook.jobs' config or, if that config is unset, the number of cores\n+\t * on the system.\n+\t */\n+\tint jobs;\n+\n \t/* Resolve and run the \"absolute_path(hook)\" instead of\n \t * \"hook\". Used for \"git worktree\" hooks\n \t */\n@@ -85,11 +92,6 @@ struct run_hooks_opt\n \tint *invoked_hook;\n };\n \n-#define RUN_HOOKS_OPT_INIT { \\\n-\t.env = STRVEC_INIT, \\\n-\t.args = STRVEC_INIT, \\\n-}\n-\n /*\n  * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n  * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n@@ -111,6 +113,18 @@ struct hook_cb_data {\n \tint *invoked_hook;\n };\n \n+#define RUN_HOOKS_OPT_INIT_SYNC { \\\n+\t.jobs = 1, \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n+#define RUN_HOOKS_OPT_INIT_ASYNC { \\\n+\t.jobs = 0, \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /**\ndiff --git a/read-cache.c b/read-cache.c\nindex 90099ca14d..fd2bc67667 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3068,7 +3068,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \n \tret = convert_to_sparse(istate);\n \ndiff --git a/refs.c b/refs.c\nindex 73d4a93926..305a075746 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2062,7 +2062,7 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \tint ret = 0, i;\n \ndiff --git a/reset.c b/reset.c\nindex 6499bc5127..b93fe6a783 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -128,7 +128,8 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\t\t\t    reflog_head);\n \t}\n \tif (run_hook) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n+\n \t\tstrvec_pushl(&opt.args,\n \t\t\t     oid_to_hex(orig ? orig : null_oid()),\n \t\t\t     oid_to_hex(oid),\ndiff --git a/sequencer.c b/sequencer.c\nindex f451e23d0c..f13a0cbfbe 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1148,7 +1148,7 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \tstruct strbuf tmp = STRBUF_INIT;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n@@ -4522,7 +4522,7 @@ static int pick_commits(struct repository *r,\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n \t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n-\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \n \t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n \t\t\tnotes_cp.git_cmd = 1;\ndiff --git a/transport.c b/transport.c\nindex 4ca8fc0391..abf8785885 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1204,7 +1204,7 @@ static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n \tint ret = 0;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n \tstruct ref *r;\n \tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \n-- \n2.32.0.605.g8dce9f2422-goog\n\n"},{"id":"432553","messageId":"20210812004258.74318-4-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210812004258.74318-1-emilyshaffer@google.com","subject":"[PATCH v2 3/6] hook: introduce \"git hook list\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-12T00:42:55Z","receivedAt":"2021-08-12T00:43:22Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"If more than one hook will be run, it may be useful to see a list of\nwhich hooks should be run. At very least, it will be useful for us to\ntest the semantics of multihooks ourselves.\n\nFor now, only list the hooks which will run in the order they will run\nin; later, it might be useful to include more information like where the\nhooks were configured and whether or not they will run.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/hook.c | 49 +++++++++++++++++++++++++++++++++++++++++++++++++\n hook.c         | 18 ++++++++----------\n 2 files changed, 57 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 12c9126032..c36b05376c 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -8,8 +8,11 @@\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n \tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n+#define BUILTIN_HOOK_LIST_USAGE \\\n+\tN_(\"git hook list <hook-name>\")\n \n static const char * const builtin_hook_usage[] = {\n+\tBUILTIN_HOOK_LIST_USAGE,\n \tBUILTIN_HOOK_RUN_USAGE,\n \tNULL\n };\n@@ -19,6 +22,50 @@ static const char * const builtin_hook_run_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_hook_list_usage[] = {\n+\tBUILTIN_HOOK_LIST_USAGE,\n+\tNULL\n+};\n+\n+static int list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct list_head *head, *pos;\n+\tconst char *hookname = NULL;\n+\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n+\n+\tstruct option list_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, list_options,\n+\t\t\t     builtin_hook_list_usage, 0);\n+\n+\tif (argc < 1)\n+\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\t\t      builtin_hook_list_usage, list_options);\n+\n+\thookname = argv[0];\n+\n+\thead = hook_list(hookname);\n+\n+\tif (list_empty(head)) {\n+\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n+\t\t       hookname);\n+\t\treturn 0;\n+\t}\n+\n+\tlist_for_each(pos, head) {\n+\t\tstruct hook *item = list_entry(pos, struct hook, list);\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item)\n+\t\t\tprintf(\"%s\\n\", item->hook_path);\n+\t}\n+\n+\tclear_hook_list(head);\n+\tstrbuf_release(&hookdir_annotation);\n+\n+\treturn 0;\n+}\n static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n@@ -88,6 +135,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \tif (!argc)\n \t\tgoto usage;\n \n+\tif (!strcmp(argv[0], \"list\"))\n+\t\treturn list(argc, argv, prefix);\n \tif (!strcmp(argv[0], \"run\"))\n \t\treturn run(argc, argv, prefix);\n \ndiff --git a/hook.c b/hook.c\nindex 37f682c6d8..2714b63473 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -96,22 +96,20 @@ int hook_exists(const char *name)\n struct list_head* hook_list(const char* hookname)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\tconst char *hook_path = find_hook(hookname);\n+\n \n \tINIT_LIST_HEAD(hook_head);\n \n \tif (!hookname)\n \t\treturn NULL;\n \n-\tif (have_git_dir()) {\n-\t\tconst char *hook_path = find_hook(hookname);\n-\n-\t\t/* Add the hook from the hookdir */\n-\t\tif (hook_path) {\n-\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n-\t\t\tto_add->hook_path = hook_path;\n-\t\t\tto_add->feed_pipe_cb_data = NULL;\n-\t\t\tlist_add_tail(&to_add->list, hook_head);\n-\t\t}\n+\t/* Add the hook from the hookdir */\n+\tif (hook_path) {\n+\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n+\t\tto_add->hook_path = hook_path;\n+\t\tto_add->feed_pipe_cb_data = NULL;\n+\t\tlist_add_tail(&to_add->list, hook_head);\n \t}\n \n \treturn hook_head;\n-- \n2.32.0.605.g8dce9f2422-goog\n\n"},{"id":"432554","messageId":"20210812004258.74318-5-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210812004258.74318-1-emilyshaffer@google.com","subject":"[PATCH v2 4/6] hook: allow running non-native hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-12T00:42:56Z","receivedAt":"2021-08-12T00:43:23Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"As the hook architecture and 'git hook run' become more featureful, we\nmay find wrappers wanting to use the hook architecture to run their own\nhooks, thereby getting nice things like parallelism and idiomatic Git\nconfiguration for free. Enable this by letting 'git hook run' bypass the\nknown_hooks() check.\n\nWe do still want to keep known_hooks() around, though - by die()ing when\nan internal Git call asks for run_hooks(\"my-new-hook\"), we can remind\nGit developers to update Documentation/githooks.txt with their new hook,\nwhich in turn helps Git users discover this new hook.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt |  8 ++++++++\n builtin/hook.c             |  4 ++--\n hook.c                     | 32 ++++++++++++++++++++++++++++----\n hook.h                     | 16 +++++++++++++++-\n 4 files changed, 53 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 8bf82b5dd4..11a8b87c60 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -18,6 +18,14 @@ This command is an interface to git hooks (see linkgit:githooks[5]).\n Currently it only provides a convenience wrapper for running hooks for\n use by git itself. In the future it might gain other functionality.\n \n+It's possible to use this command to refer to hooks which are not native to Git,\n+for example if a wrapper around Git wishes to expose hooks into its own\n+operation in a way which is already familiar to Git users. However, wrappers\n+invoking such hooks should be careful to name their hook events something which\n+Git is unlikely to use for a native hook later on. For example, Git is much less\n+likely to create a `mytool-validate-commit` hook than it is to create a\n+`validate-commit` hook.\n+\n SUBCOMMANDS\n -----------\n \ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex c36b05376c..3aa65dd791 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -46,7 +46,7 @@ static int list(int argc, const char **argv, const char *prefix)\n \n \thookname = argv[0];\n \n-\thead = hook_list(hookname);\n+\thead = hook_list(hookname, 1);\n \n \tif (list_empty(head)) {\n \t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n@@ -108,7 +108,7 @@ static int run(int argc, const char **argv, const char *prefix)\n \tgit_config(git_default_config, NULL);\n \n \thook_name = argv[0];\n-\thooks = hook_list(hook_name);\n+\thooks = hook_list(hook_name, 1);\n \tif (list_empty(hooks)) {\n \t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n \t\tif (ignore_missing)\ndiff --git a/hook.c b/hook.c\nindex 2714b63473..e5acd02a50 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -52,12 +52,21 @@ static int known_hook(const char *name)\n \n const char *find_hook(const char *name)\n {\n-\tstatic struct strbuf path = STRBUF_INIT;\n+\tconst char *hook_path;\n \n \tif (!known_hook(name))\n \t\tdie(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n \t\t    name);\n \n+\thook_path = find_hook_gently(name);\n+\n+\treturn hook_path;\n+}\n+\n+const char *find_hook_gently(const char *name)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\n \tstrbuf_reset(&path);\n \tstrbuf_git_path(&path, \"hooks/%s\", name);\n \tif (access(path.buf, X_OK) < 0) {\n@@ -93,10 +102,16 @@ int hook_exists(const char *name)\n \treturn !!find_hook(name);\n }\n \n-struct list_head* hook_list(const char* hookname)\n+struct hook_config_cb\n+{\n+\tstruct strbuf *hook_key;\n+\tstruct list_head *list;\n+};\n+\n+struct list_head* hook_list(const char* hookname, int allow_unknown)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n-\tconst char *hook_path = find_hook(hookname);\n+\tconst char *hook_path;\n \n \n \tINIT_LIST_HEAD(hook_head);\n@@ -104,6 +119,11 @@ struct list_head* hook_list(const char* hookname)\n \tif (!hookname)\n \t\treturn NULL;\n \n+\tif (allow_unknown)\n+\t\thook_path = find_hook_gently(hookname);\n+\telse\n+\t\thook_path = find_hook(hookname);\n+\n \t/* Add the hook from the hookdir */\n \tif (hook_path) {\n \t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n@@ -299,7 +319,11 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n \tif (options->path_to_stdin && options->feed_pipe)\n \t\tBUG(\"choose only one method to populate stdin\");\n \n-\thooks = hook_list(hook_name);\n+\t/*\n+\t * 'git hooks run <hookname>' uses run_found_hooks, so we don't need to\n+\t * allow unknown hooknames here.\n+\t */\n+\thooks = hook_list(hook_name, 0);\n \n \t/*\n \t * If you need to act on a missing hook, use run_found_hooks()\ndiff --git a/hook.h b/hook.h\nindex 4f90228a0c..ffa96c6e4d 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -9,8 +9,16 @@\n  * Returns the path to the hook file, or NULL if the hook is missing\n  * or disabled. Note that this points to static storage that will be\n  * overwritten by further calls to find_hook and run_hook_*.\n+ *\n+ * If the hook is not a native hook (e.g. present in Documentation/githooks.txt)\n+ * find_hook() will die(). find_hook_gently() does not consult the native hook\n+ * list and will check for any hook name in the hooks directory regardless of\n+ * whether it is known. find_hook() should be used by internal calls to hooks,\n+ * and find_hook_gently() should only be used when the hookname was provided by\n+ * the user, such as by 'git hook run my-wrapper-hook'.\n  */\n const char *find_hook(const char *name);\n+const char *find_hook_gently(const char *name);\n \n /*\n  * A boolean version of find_hook()\n@@ -32,8 +40,14 @@ struct hook {\n /*\n  * Provides a linked list of 'struct hook' detailing commands which should run\n  * in response to the 'hookname' event, in execution order.\n+ *\n+ * If allow_unknown is unset, hooks will be checked against the hook list\n+ * generated from Documentation/githooks.txt. Otherwise, any hook name will be\n+ * allowed. allow_unknown should only be set when the hook name is provided by\n+ * the user; internal calls to hook_list should make sure the hook they are\n+ * invoking is present in Documentation/githooks.txt.\n  */\n-struct list_head* hook_list(const char *hookname);\n+struct list_head* hook_list(const char *hookname, int allow_unknown);\n \n struct run_hooks_opt\n {\n-- \n2.32.0.605.g8dce9f2422-goog\n\n"},{"id":"432556","messageId":"20210812004258.74318-6-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210812004258.74318-1-emilyshaffer@google.com","subject":"[PATCH v2 5/6] hook: include hooks from the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-12T00:42:57Z","receivedAt":"2021-08-12T00:43:27Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach the hook.[hc] library to parse configs to populare the list of\nhooks to run for a given event.\n\nMultiple commands can be specified for a given hook by providing\nmultiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n\"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be run in\nconfig order of the \"hook.<name>.event\" lines.\n\nFor example:\n\n  $ git config --list | grep ^hook\n  hook.bar.command=~/bar.sh\n  hook.bar.event=pre-commit\n\n  $ git hook run\n  # Runs ~/bar.sh\n  # Runs .git/hooks/pre-commit\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/config/hook.txt |   5 +\n Documentation/git-hook.txt    |  23 ++++-\n builtin/hook.c                |   5 +-\n hook.c                        | 166 +++++++++++++++++++++++++++++-----\n hook.h                        |   7 +-\n t/t1800-hook.sh               | 141 ++++++++++++++++++++++++++++-\n 6 files changed, 318 insertions(+), 29 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 96d3d6572c..a97b980cca 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -1,3 +1,8 @@\n+hook.<command>.command::\n+\tA command to execute during the <command> hook event. This can be an\n+\texecutable on your device, a oneliner for your shell, or the name of a\n+\thookcmd. See linkgit:git-hook[1].\n+\n hook.jobs::\n \tSpecifies how many hooks can be run simultaneously during parallelized\n \thook execution. If unspecified, defaults to the number of processors on\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 11a8b87c60..c610ed9583 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -26,12 +26,31 @@ Git is unlikely to use for a native hook later on. For example, Git is much less\n likely to create a `mytool-validate-commit` hook than it is to create a\n `validate-commit` hook.\n \n+This command parses the default configuration files for pairs of configs like\n+so:\n+\n+  [hook \"linter\"]\n+    event = pre-commit\n+    command = ~/bin/linter --c\n+\n+Conmmands are run in the order Git encounters their associated\n+`hook.<name>.event` configs during the configuration parse (see\n+linkgit:git-config[1]).\n+\n+In general, when instructions suggest adding a script to\n+`.git/hooks/<hook-event>`, you can specify it in the config instead by running\n+`git config --add hook.<some-name>.command <path-to-script> && git config --add\n+hook.<some-name>.event <hook-event>` - this way you can share the script between\n+multiple repos. That is, `cp ~/my-script.sh ~/project/.git/hooks/pre-commit`\n+would become `git config --add hook.my-script.command ~/my-script.sh && git\n+config --add hook.my-script.event pre-commit`.\n+\n SUBCOMMANDS\n -----------\n \n run::\n-\tRun the `<hook-name>` hook. See linkgit:githooks[5] for\n-\tthe hook names we support.\n+\tRuns hooks configured for `<hook-name>`, in the order they are\n+\tdiscovered during the config parse.\n +\n Any positional arguments to the hook should be passed after an\n optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 3aa65dd791..ea49dc4ef6 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -49,7 +49,7 @@ static int list(int argc, const char **argv, const char *prefix)\n \thead = hook_list(hookname, 1);\n \n \tif (list_empty(head)) {\n-\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n+\t\tprintf(_(\"no hooks configured for event '%s'\\n\"),\n \t\t       hookname);\n \t\treturn 0;\n \t}\n@@ -58,7 +58,8 @@ static int list(int argc, const char **argv, const char *prefix)\n \t\tstruct hook *item = list_entry(pos, struct hook, list);\n \t\titem = list_entry(pos, struct hook, list);\n \t\tif (item)\n-\t\t\tprintf(\"%s\\n\", item->hook_path);\n+\t\t\tprintf(\"%s\\n\", item->name ? item->name\n+\t\t\t\t\t\t  : _(\"hook from hookdir\"));\n \t}\n \n \tclear_hook_list(head);\ndiff --git a/hook.c b/hook.c\nindex e5acd02a50..51ada266bc 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -12,6 +12,50 @@ static void free_hook(struct hook *ptr)\n \tfree(ptr);\n }\n \n+/*\n+ * Walks the linked list at 'head' to check if any hook named 'name'\n+ * already exists. Returns a pointer to that hook if so, otherwise returns NULL.\n+ */\n+static struct hook *find_hook_by_name(struct list_head *head,\n+\t\t\t\t\t const char *name)\n+{\n+\tstruct list_head *pos = NULL, *tmp = NULL;\n+\tstruct hook *found = NULL;\n+\n+\tlist_for_each_safe(pos, tmp, head) {\n+\t\tstruct hook *it = list_entry(pos, struct hook, list);\n+\t\tif (!strcmp(it->name, name)) {\n+\t\t\tlist_del(pos);\n+\t\t\tfound = it;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\treturn found;\n+}\n+\n+/*\n+ * Adds a hook if it's not already in the list, or moves it to the tail of the\n+ * list if it was already there. name == NULL indicates it's from the hookdir;\n+ * just append it in this case.\n+ */\n+static void append_or_move_hook(struct list_head *head, const char *name)\n+{\n+\tstruct hook *to_add = NULL;\n+\n+\t/* if it's not from hookdir, check if the hook is already in the list */\n+\tif (name)\n+\t\tto_add = find_hook_by_name(head, name);\n+\n+\tif (!to_add) {\n+\t\t/* adding a new hook, not moving an old one */\n+\t\tto_add = xmalloc(sizeof(*to_add));\n+\t\tto_add->name = name;\n+\t\tto_add->feed_pipe_cb_data = NULL;\n+\t}\n+\n+\tlist_add_tail(&to_add->list, head);\n+}\n+\n static void remove_hook(struct list_head *to_remove)\n {\n \tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n@@ -99,38 +143,80 @@ const char *find_hook_gently(const char *name)\n \n int hook_exists(const char *name)\n {\n-\treturn !!find_hook(name);\n+\treturn !list_empty(hook_list(name, 0));\n }\n \n struct hook_config_cb\n {\n-\tstruct strbuf *hook_key;\n+\tconst char *hook_event;\n \tstruct list_head *list;\n };\n \n-struct list_head* hook_list(const char* hookname, int allow_unknown)\n+/*\n+ * Callback for git_config which adds configured hooks to a hook list.  Hooks\n+ * can be configured by specifying both hook.<friend-name>.command = <path> and\n+ * hook.<friendly-name>.event = <hook-event>.\n+ */\n+static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n+{\n+\tstruct hook_config_cb *data = cb_data;\n+\tconst char *subsection, *parsed_key;\n+\tsize_t subsection_len = 0;\n+\tstruct strbuf subsection_cpy = STRBUF_INIT;\n+\n+\t/*\n+\t * Don't bother doing the expensive parse if there's no\n+\t * chance that the config matches 'hook.myhook.event = hook_event'.\n+\t */\n+\tif (!value || strcmp(value, data->hook_event))\n+\t\treturn 0;\n+\n+\t/* Looking for \"hook.friendlyname.event = hook_event\" */\n+\tif (parse_config_key(key,\n+\t\t\t    \"hook\",\n+\t\t\t    &subsection,\n+\t\t\t    &subsection_len,\n+\t\t\t    &parsed_key) ||\n+\t    strcmp(parsed_key, \"event\"))\n+\t\treturn 0;\n+\n+\t/*\n+\t * 'subsection' is a pointer to the internals of 'key', which we don't\n+\t * own the memory for. Copy it away to the hook list.\n+\t */\n+\tstrbuf_add(&subsection_cpy, subsection, subsection_len);\n+\n+\tappend_or_move_hook(data->list, strbuf_detach(&subsection_cpy, NULL));\n+\n+\n+\treturn 0;\n+}\n+\n+struct list_head* hook_list(const char *hookname, int allow_unknown)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n-\tconst char *hook_path;\n+\tstruct hook_config_cb cb_data = {\n+\t\t.hook_event = hookname,\n+\t\t.list = hook_head,\n+\t};\n \n+\tif (!allow_unknown && !known_hook(hookname))\n+\t\tdie(_(\"Don't recognize hook event '%s'. \"\n+\t\t      \"Is it documented in 'githooks.txt'?\"),\n+\t\t      hookname);\n \n \tINIT_LIST_HEAD(hook_head);\n \n \tif (!hookname)\n \t\treturn NULL;\n \n-\tif (allow_unknown)\n-\t\thook_path = find_hook_gently(hookname);\n-\telse\n-\t\thook_path = find_hook(hookname);\n+\t/* Add the hooks from the config, e.g. hook.myhook.event = pre-commit */\n+\tgit_config(hook_config_lookup, &cb_data);\n \n-\t/* Add the hook from the hookdir */\n-\tif (hook_path) {\n-\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n-\t\tto_add->hook_path = hook_path;\n-\t\tto_add->feed_pipe_cb_data = NULL;\n-\t\tlist_add_tail(&to_add->list, hook_head);\n-\t}\n+\t/* Add the hook from the hookdir. The placeholder makes it easier to\n+\t * allocate work in pick_next_hook. */\n+\tif (find_hook_gently(hookname))\n+\t\tappend_or_move_hook(hook_head, NULL);\n \n \treturn hook_head;\n }\n@@ -191,11 +277,43 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->trace2_hook_name = hook_cb->hook_name;\n \tcp->dir = hook_cb->options->dir;\n \n+\t/* to enable oneliners, let config-specified hooks run in shell.\n+\t * config-specified hooks have a name. */\n+\tcp->use_shell = !!run_me->name;\n+\n \t/* add command */\n-\tif (hook_cb->options->absolute_path)\n-\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n-\telse\n-\t\tstrvec_push(&cp->args, run_me->hook_path);\n+\tif (run_me->name) {\n+\t\t/* ...from config */\n+\t\tstruct strbuf cmd_key = STRBUF_INIT;\n+\t\tchar *command = NULL;\n+\n+\t\tstrbuf_addf(&cmd_key, \"hook.%s.command\", run_me->name);\n+\t\tif (git_config_get_string(cmd_key.buf, &command)) {\n+\t\t\t/* TODO test me! */\n+\t\t\tdie(_(\"'hook.%s.command' must be configured \"\n+\t\t\t      \"or 'hook.%s.event' must be removed; aborting.\\n\"),\n+\t\t\t    run_me->name, run_me->name);\n+\t\t}\n+\n+\t\tstrvec_push(&cp->args, command);\n+\t} else {\n+\t\t/* ...from hookdir. */\n+\t\tconst char *hook_path = NULL;\n+\t\t/*\n+\t\t *\n+\t\t * At this point we are already running, so don't validate\n+\t\t * whether the hook name is known or not.\n+\t\t */\n+\t\thook_path = find_hook_gently(hook_cb->hook_name);\n+\t\tif (!hook_path)\n+\t\t\tBUG(\"hookdir hook in hook list but no hookdir hook present in filesystem\");\n+\n+\t\tif (hook_cb->options->absolute_path)\n+\t\t\thook_path = absolute_path(hook_path);\n+\n+\t\tstrvec_push(&cp->args, hook_path);\n+\t}\n+\n \n \t/*\n \t * add passed-in argv, without expanding - let the user get back\n@@ -225,8 +343,11 @@ static int notify_start_failure(struct strbuf *out,\n \n \thook_cb->rc |= 1;\n \n-\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n-\t\t    attempted->hook_path);\n+\tif (attempted->name)\n+\t\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n+\t\t    attempted->name);\n+\telse\n+\t\tstrbuf_addstr(out, _(\"Couldn't start hook from hooks directory\\n\"));\n \n \treturn 1;\n }\n@@ -320,7 +441,8 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n \t\tBUG(\"choose only one method to populate stdin\");\n \n \t/*\n-\t * 'git hooks run <hookname>' uses run_found_hooks, so we don't need to\n+\t * 'git hooks run <hookname>' uses run_found_hooks, and we want to make\n+\t * sure internal callers are using known hooks, so we don't need to\n \t * allow unknown hooknames here.\n \t */\n \thooks = hook_list(hook_name, 0);\ndiff --git a/hook.h b/hook.h\nindex ffa96c6e4d..a6263864b9 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -27,8 +27,11 @@ int hook_exists(const char *hookname);\n \n struct hook {\n \tstruct list_head list;\n-\t/* The path to the hook */\n-\tconst char *hook_path;\n+\t/*\n+\t * The friendly name of the hook. NULL indicates the hook is from the\n+\t * hookdir.\n+\t */\n+\tconst char *name;\n \n \t/*\n \t * Use this to keep state for your feed_pipe_fn if you are using\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 217db848b3..ef2432f53a 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -1,13 +1,29 @@\n #!/bin/bash\n \n-test_description='git-hook command'\n+test_description='git-hook command and config-managed multihooks'\n \n . ./test-lib.sh\n \n+setup_hooks () {\n+\ttest_config hook.ghi.event pre-commit --add\n+\ttest_config hook.ghi.command \"/path/ghi\" --add\n+\ttest_config_global hook.def.event pre-commit --add\n+\ttest_config_global hook.def.command \"/path/def\" --add\n+}\n+\n+setup_hookdir () {\n+\tmkdir .git/hooks\n+\twrite_script .git/hooks/pre-commit <<-EOF\n+\techo \\\"Legacy Hook\\\"\n+\tEOF\n+\ttest_when_finished rm -rf .git/hooks\n+}\n+\n test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook &&\n \ttest_expect_code 129 git hook run &&\n \ttest_expect_code 129 git hook run -h &&\n+\ttest_expect_code 129 git hook list -h &&\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \tgrep \"unknown option\" err\n '\n@@ -153,4 +169,127 @@ test_expect_success 'stdin to hooks' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'git hook list orders by config order' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tdef\n+\tghi\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list reorders on duplicate event declarations' '\n+\tsetup_hooks &&\n+\n+\t# 'def' is usually configured globally; move it to the end by\n+\t# configuring it locally.\n+\ttest_config hook.def.event \"pre-commit\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tghi\n+\tdef\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list shows hooks from the hookdir' '\n+\tsetup_hookdir &&\n+\n+\tcat >expected <<-EOF &&\n+\thook from hookdir\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions execute oneliners' '\n+\ttest_config hook.oneliner.event \"pre-commit\" &&\n+\ttest_config hook.oneliner.command \"echo \\\"Hello World\\\"\" &&\n+\n+\techo \"Hello World\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions resolve paths' '\n+\twrite_script sample-hook.sh <<-EOF &&\n+\techo \\\"Sample Hook\\\"\n+\tEOF\n+\n+\ttest_when_finished \"rm sample-hook.sh\" &&\n+\n+\ttest_config hook.sample-hook.event pre-commit &&\n+\ttest_config hook.sample-hook.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\techo \\\"Sample Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hookdir hook included in git hook run' '\n+\tsetup_hookdir &&\n+\n+\techo \\\"Legacy Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'stdin to multiple hooks' '\n+\ttest_config hook.stdin-a.event \"test-hook\" --add &&\n+\ttest_config hook.stdin-a.command \"xargs -P1 -I% echo a%\" --add &&\n+\ttest_config hook.stdin-b.event \"test-hook\" --add &&\n+\ttest_config hook.stdin-b.command \"xargs -P1 -I% echo b%\" --add &&\n+\n+\tcat >input <<-EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tcat >expected <<-EOF &&\n+\ta1\n+\ta2\n+\ta3\n+\tb1\n+\tb2\n+\tb3\n+\tEOF\n+\n+\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'multiple hooks in series' '\n+\ttest_config hook.series-1.event \"test-hook\" &&\n+\ttest_config hook.series-1.command \"echo 1\" --add &&\n+\ttest_config hook.series-2.event \"test-hook\" &&\n+\ttest_config hook.series-2.command \"echo 2\" --add &&\n+\tmkdir .git/hooks &&\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo 3\n+\tEOF\n+\n+\tcat >expected <<-EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tgit hook run -j1 test-hook 2>actual &&\n+\ttest_cmp expected actual &&\n+\n+\trm -rf .git/hooks\n+'\n test_done\n-- \n2.32.0.605.g8dce9f2422-goog\n\n"},{"id":"432555","messageId":"20210812004258.74318-7-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210812004258.74318-1-emilyshaffer@google.com","subject":"[PATCH v2 6/6] hook: allow out-of-repo 'git hook' invocations","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-12T00:42:58Z","receivedAt":"2021-08-12T00:43:28Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Since hooks can now be supplied via the config, and a config can be\npresent without a gitdir via the global and system configs, we can start\nto allow 'git hook run' to occur without a gitdir. This enables us to do\nthings like run sendemail-validate hooks when running 'git send-email'\nfrom a nongit directory.\n\nIt still doesn't make sense to look for hooks in the hookdir in nongit\nrepos, though, as there is no hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    For hookdir hooks, do we want to run them in nongit dir when core.hooksPath\n    is set? For example, if someone set core.hooksPath in their global config and\n    then ran 'git hook run sendemail-validate' in a nongit dir?\n\n git.c           |  2 +-\n hook.c          |  2 +-\n t/t1800-hook.sh | 20 +++++++++++++++-----\n 3 files changed, 17 insertions(+), 7 deletions(-)\n\ndiff --git a/git.c b/git.c\nindex 540909c391..39988ee3b0 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,7 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n-\t{ \"hook\", cmd_hook, RUN_SETUP },\n+\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nindex 51ada266bc..87d57f4118 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -215,7 +215,7 @@ struct list_head* hook_list(const char *hookname, int allow_unknown)\n \n \t/* Add the hook from the hookdir. The placeholder makes it easier to\n \t * allocate work in pick_next_hook. */\n-\tif (find_hook_gently(hookname))\n+\tif (have_git_dir() && find_hook_gently(hookname))\n \t\tappend_or_move_hook(hook_head, NULL);\n \n \treturn hook_head;\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex ef2432f53a..a7e45c0d16 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -118,15 +118,25 @@ test_expect_success 'git hook run -- pass arguments' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'git hook run -- out-of-repo runs excluded' '\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\techo Test hook\n-\tEOF\n+test_expect_success 'git hook run: out-of-repo runs execute global hooks' '\n+\ttest_config_global hook.global-hook.event test-hook --add &&\n+\ttest_config_global hook.global-hook.command \"echo no repo no problems\" --add &&\n+\n+\techo \"global-hook\" >expect &&\n+\tnongit git hook list test-hook >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"no repo no problems\" >expect &&\n \n-\tnongit test_must_fail git hook run test-hook\n+\tnongit git hook run test-hook 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n \tmkdir my-hooks &&\n \twrite_script my-hooks/test-hook <<-\\EOF &&\n \techo Hook ran $1 >>actual\n-- \n2.32.0.605.g8dce9f2422-goog\n\n"},{"id":"432559","messageId":"xmqqa6ln6zi0.fsf@gitster.g","threadId":"55286","inReplyTo":"20210812004258.74318-1-emilyshaffer@google.com","subject":"Re: [PATCH v2 0/6] config-based hooks restarted","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-12T04:47:03Z","receivedAt":"2021-08-12T04:47:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> This is the config-based hooks topic rebased onto v4 of Ævar's\n> branch[1].\n\nI have [1] in my tree, but these patches do not seem to apply\ncleanly; I see a failure in \"git hooks list\" step.\n\nShould I perhaps try merging [1] to one of the v2.33-rc and then\nqueue these on top?\n"},{"id":"432561","messageId":"xmqq5ywb6yrm.fsf@gitster.g","threadId":"55286","inReplyTo":"xmqqa6ln6zi0.fsf@gitster.g","subject":"Re: [PATCH v2 0/6] config-based hooks restarted","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-12T05:02:53Z","receivedAt":"2021-08-12T05:02:56Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Emily Shaffer <emilyshaffer@google.com> writes:\n>\n>> This is the config-based hooks topic rebased onto v4 of Ævar's\n>> branch[1].\n>\n> I have [1] in my tree, but these patches do not seem to apply\n> cleanly; I see a failure in \"git hooks list\" step.\n>\n> Should I perhaps try merging [1] to one of the v2.33-rc and then\n> queue these on top?\n\nAh, I figured it out.  Your 3/6 seems to be stale wrt 4787177b\n(hook: support passing stdin to hooks, 2021-08-03) that came from\nhttps://lore.kernel.org/git/patch-v4-22.36-639e59e9ed0-20210803T191505Z-avarab@gmail.com/\n\nThat patch did this to the early part of builtin/hook.c:\n\n        diff --git a/builtin/hook.c b/builtin/hook.c\n        index f33db9953c..27dce6a2f0 100644\n        --- a/builtin/hook.c\n        +++ b/builtin/hook.c\n        @@ -7,7 +7,7 @@\n         #include \"strvec.h\"\n\n         #define BUILTIN_HOOK_RUN_USAGE \\\n        -\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n        +\tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n\n         static const char * const builtin_hook_usage[] = {\n                BUILTIN_HOOK_RUN_USAGE,\n\n\nbut your [v2 3/6] here expects something entirely different.\n\n        diff --git a/builtin/hook.c b/builtin/hook.c\n        index 12c9126032..c36b05376c 100644\n        --- a/builtin/hook.c\n        +++ b/builtin/hook.c\n        @@ -8,8 +8,11 @@\n\n         #define BUILTIN_HOOK_RUN_USAGE \\\n                N_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n        +#define BUILTIN_HOOK_LIST_USAGE \\\n        +\tN_(\"git hook list <hook-name>\")\n\nI've wiggled the patch in, as there wasn't any other funny\ninconsistency like this one, but please double check the result\nafter I push it out perhaps tomorrow morning.\n\nThanks.\n"},{"id":"432585","messageId":"xmqqbl6260eo.fsf@gitster.g","threadId":"55286","inReplyTo":"20210812004258.74318-2-emilyshaffer@google.com","subject":"Re: [PATCH v2 1/6] hook: run a list of hooks instead","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-12T17:25:03Z","receivedAt":"2021-08-12T17:25:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> To prepare for multihook support, teach hook.[hc] to take a list of\n> hooks at run_hooks and run_found_hooks. Right now the list is always one\n> entry, but in the future we will allow users to supply more than one\n> executable for a single hook event.\n>\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  builtin/hook.c |  14 ++++---\n>  hook.c         | 103 +++++++++++++++++++++++++++++++++++--------------\n>  hook.h         |  16 +++++++-\n>  3 files changed, 96 insertions(+), 37 deletions(-)\n>\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index 5eb7cf73a4..4d39c9e75e 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -25,7 +25,8 @@ static int run(int argc, const char **argv, const char *prefix)\n>  \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>  \tint ignore_missing = 0;\n>  \tconst char *hook_name;\n> -\tconst char *hook_path;\n> +\tstruct list_head *hooks;\n> +\n\nNatural.  We used to use the path to the hook because we were\nexpecting only one. We now use the name to find a list of hooks.\n\nAll the caller sees is just list_head without any direct visibility\ninto it, which feels like a great abstraction.  Presumably everything\nwill go through the API functions taking this opaque \"list of hooks\"\nthing (or \"the first one in the list\" if the API function does not\niterate over it, perhaps?).\n\n>  \tstruct option run_options[] = {\n>  \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n>  \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n> @@ -58,15 +59,16 @@ static int run(int argc, const char **argv, const char *prefix)\n>  \tgit_config(git_default_config, NULL);\n>  \n>  \thook_name = argv[0];\n> -\tif (ignore_missing)\n> -\t\treturn run_hooks_oneshot(hook_name, &opt);\n> -\thook_path = find_hook(hook_name);\n> -\tif (!hook_path) {\n> +\thooks = hook_list(hook_name);\n\nOK.\n\n> +\tif (list_empty(hooks)) {\n> +\t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n> +\t\tif (ignore_missing)\n> +\t\t\treturn 0;\n\nOK.\n\n>  \t\terror(\"cannot find a hook named %s\", hook_name);\n>  \t\treturn 1;\n>  \t}\n\n> diff --git a/hook.c b/hook.c\n> index ee20b2e365..80e150548c 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -4,6 +4,28 @@\n>  #include \"hook-list.h\"\n>  #include \"config.h\"\n>  \n> +static void free_hook(struct hook *ptr)\n> +{\n> +\tif (ptr) {\n> +\t\tfree(ptr->feed_pipe_cb_data);\n> +\t}\n\nLose the extra {}, as we do not do more than the above free() even\nat the end of the series?\n\n> +\tfree(ptr);\n> +}\n> +\n> +static void remove_hook(struct list_head *to_remove)\n> +{\n> +\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n> +\tlist_del(to_remove);\n> +\tfree_hook(hook_to_remove);\n> +}\n> +\n> +void clear_hook_list(struct list_head *head)\n> +{\n> +\tstruct list_head *pos, *tmp;\n> +\tlist_for_each_safe(pos, tmp, head)\n> +\t\tremove_hook(pos);\n> +}\n\nOK.\n\n> +struct list_head* hook_list(const char* hookname)\n\nShift both of the asterisks; our asterisks do not stick to the type\nbut to the identifier.\n\n> +{\n> +\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> +\n> +\tINIT_LIST_HEAD(hook_head);\n> +\n> +\tif (!hookname)\n> +\t\treturn NULL;\n\nChecking for invalid hookname first would avoid leaking hook_head,\nno?  The caller of hook_list() we saw earlier immediately calls\nlist_empty() which will segfault.  The caller need to be tightened,\nbut I wonder if it would be a programmer's error to pass a NULL\nhookname to this function.  If so, this can simply be a BUG() and\nthe earlier allocation and initialization of hook_head can stay\nwhere they are.  Otherwise, the caller should see if this returns a\nNULL.\n\n> +\tif (have_git_dir()) {\n> +\t\tconst char *hook_path = find_hook(hookname);\n> +\n> +\t\t/* Add the hook from the hookdir */\n> +\t\tif (hook_path) {\n> +\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> +\t\t\tto_add->hook_path = hook_path;\n> +\t\t\tto_add->feed_pipe_cb_data = NULL;\n> +\t\t\tlist_add_tail(&to_add->list, hook_head);\n> +\t\t}\n> +\t}\n\nCalling this function to grab a list of hooks when we are not in any\nrepository is not an error but just we get \"there is nothing to\nrun\".  Does the design give us a more useful behaviour, compared to\nalternatives like \"you have to be in a repository or calling this\nfunction is an error\"?\n\nNot an objection wrapped in a rhetorical question, but a genuine\nquestion.  \"It would help this and that usecase\" would be an ideal\nanswer, \"We could do either way, but we happened to have written the\ncode this way first, and at the end of the series we did not see any\npractical downsides\" would also be a great answer.\n\n> +\treturn hook_head;\n> +}\n> +\n\n> +/*\n> + * Provides a linked list of 'struct hook' detailing commands which should run\n> + * in response to the 'hookname' event, in execution order.\n> + */\n> +struct list_head* hook_list(const char *hookname);\n\nstruct list_head *hook_list(const char *hookname);\n"},{"id":"432589","messageId":"xmqqmtpm4kmy.fsf@gitster.g","threadId":"55286","inReplyTo":"20210812004258.74318-3-emilyshaffer@google.com","subject":"Re: [PATCH v2 2/6] hook: allow parallel hook execution","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-12T17:51:01Z","receivedAt":"2021-08-12T17:51:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> In many cases, there's no reason not to allow hooks to execute in\n> parallel. run_processes_parallel() is well-suited - it's a task queue\n> that runs its housekeeping in series, which means users don't\n> need to worry about thread safety on their callback data. True\n> multithreaded execution with the async_* functions isn't necessary here.\n> Synchronous hook execution can be achieved by only allowing 1 job to run\n> at a time.\n>\n> Teach run_hooks() to use that function for simple hooks which don't\n> require stdin or capture of stderr.\n\nSince run_hooks() has been using run_processes_parallel_tr2()\nalready in the ab/config-based-hooks-base topic, the above\ndescription puzzled me quite a bit.  I think you meant to say with\nthis step you update the callers to pass either .jobs=1 or .jobs=0\nin the hooks_opt, so that hooks _can_ run in parallel (as opposed\nto using hardcoded jobs=1 in run_hooks() when calling the underlying\nrun_processes_parallel_tr2() function).\n\nI do not think SYNC and ASYNC are great adjectives for what you are\ndoing in this step.  Wouldn't ASYNC mean \"the caller tells the hooks\nto run, continues without waiting for them to finish\"?  In order to\nlet more than one hooks to run at the same time, the caller has to\ncontinue without waiting for the first one it started so that it can\nstart the second one before the first one finishes (otherwise these\ntwo hooks will not run at the same time), but ASYNC implys a lot\nmore than that.  After starting all of these hooks, the caller may\ncontinue doing things that are not even related to these spawned\nhooks if a hook is run asynchronously.\n\nBut I do not think that is what is happening here.  After all,\nrun_processes_parallel_tr2() will not return until all of the\nsubprocesses are culled.\n\nPerhaps the distinction you are trying to express is easier to\nconvey to readers if you instead used the verb TO RUN with the\nadverb SERIALLY vs IN PARALLEL (as opposed to SYNCHRONOUSLY vs\nASYNCHRONOUSLY)?\n\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index 4d39c9e75e..12c9126032 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -22,7 +22,7 @@ static const char * const builtin_hook_run_usage[] = {\n>  static int run(int argc, const char **argv, const char *prefix)\n>  {\n>  \tint i;\n> -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n> +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n>  \tint ignore_missing = 0;\n>  \tconst char *hook_name;\n>  \tstruct list_head *hooks;\n> @@ -32,6 +32,8 @@ static int run(int argc, const char **argv, const char *prefix)\n>  \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n>  \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n>  \t\t\t   N_(\"file to read into hooks' stdin\")),\n> +\t\tOPT_INTEGER('j', \"jobs\", &opt.jobs,\n> +\t\t\t    N_(\"run up to <n> hooks simultaneously\")),\n>  \t\tOPT_END(),\n>  \t};\n>  \tint ret;\n\nOK, so by default \"git hook\" runs 1 at a time but we can instruct to\nrun the optimal number of jobs via \"git hook -j0\" etc.\n\n> diff --git a/hook.c b/hook.c\n> index 80e150548c..37f682c6d8 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -228,6 +228,28 @@ static int notify_hook_finished(int result,\n>  \treturn 0;\n>  }\n>  \n> +/*\n> + * Determines how many jobs to use after we know we want to parallelize. First\n> + * priority is the config 'hook.jobs' and second priority is the number of CPUs.\n> + */\n> +static int configured_hook_jobs(void)\n> +{\n> +\t/*\n> +\t * The config and the CPU count probably won't change during the process\n> +\t * lifetime, so cache the result in case we invoke multiple hooks during\n> +\t * one process.\n> +\t */\n> +\tstatic int jobs = 0;\n> +\tif (jobs)\n> +\t\treturn jobs;\n> +\n> +\tif (git_config_get_int(\"hook.jobs\", &jobs))\n> +\t\t/* if the config isn't set, fall back to CPU count. */\n> +\t\tjobs = online_cpus();\n> +\n> +\treturn jobs;\n> +}\n\nNot a suggestion to make this improvement as a part of this series,\nbut there already are more than several codepaths doing essentially\nthe same thing as above, taking their own configuration or\nenvironment variables and falling back to online_cpus().\n\nIt may be worth introducing a shared helper function so that this\ncan become\n\n\tstatic int configured_hook_jobs(void)\n\t{\n\t\tstatic int jobs; /* no need for \"= 0\" */\n\n\t\tif (!jobs)\n\t\t\tjobs = default_parallelism(\"hook.jobs\", NULL);\n\t\treturn jobs;\n\t}\n\nwhere the new helper takes the configuration and environment\nvariable names.\n\nThanks.\n"},{"id":"432590","messageId":"xmqq5ywa4hg8.fsf@gitster.g","threadId":"55286","inReplyTo":"20210812004258.74318-4-emilyshaffer@google.com","subject":"Re: [PATCH v2 3/6] hook: introduce \"git hook list\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-12T18:59:51Z","receivedAt":"2021-08-12T18:59:57Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> +static int list(int argc, const char **argv, const char *prefix)\n> +{\n> +\tstruct list_head *head, *pos;\n> +\tconst char *hookname = NULL;\n> +\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n> +\n> +\tstruct option list_options[] = {\n> +\t\tOPT_END(),\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, prefix, list_options,\n> +\t\t\t     builtin_hook_list_usage, 0);\n> +\n> +\tif (argc < 1)\n> +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n> +\t\t\t      builtin_hook_list_usage, list_options);\n> +\n> +\thookname = argv[0];\n> +\n> +\thead = hook_list(hookname);\n> +\n> +\tif (list_empty(head)) {\n\nThe same \"can't hook_list() signal an error by returning NULL?\"\ncomment applies here.\n\n\thead = hook_list(hookname);\n\tif (!head)\n\t\tdie(_(\"no such hook '%s'\"), hookname);\n\nor something?\n\n> +\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> +\t\t       hookname);\n> +\t\treturn 0;\n\nIf it is a normally expected state that there is no hook for the\ngiven name, signalling success by returning 0 here may be sensible,\nbut then the message should at least go to the standard error stream\nto leave the standard output empty, so that a caller can reasonably\ndo something like\n\n\tfor path in $(git hooks list \"$1\")\n\tdo\n\t\tls -l \"$path\"\n\tdone\n\nIf we really want to show such a message, perhaps\n\n\tif (list_empty(head)) {\n        \tif (!quiet)\n\t\t\twarning(_(\"no commands configured\"));\n\t\treturn 0;\n\t}\n\nThe normal display just shows the path without saying \"command %s\nwill run for hook %s\"; the warning probably should do the same.\n\nHaving said that, if it truly is a normal and expected state that no\nhook is defined for the given name, I actually think there should be\nno message.\n\n> +\t}\n> +\n> +\tlist_for_each(pos, head) {\n> +\t\tstruct hook *item = list_entry(pos, struct hook, list);\n> +\t\titem = list_entry(pos, struct hook, list);\n> +\t\tif (item)\n> +\t\t\tprintf(\"%s\\n\", item->hook_path);\n> +\t}\n\n> diff --git a/hook.c b/hook.c\n> index 37f682c6d8..2714b63473 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -96,22 +96,20 @@ int hook_exists(const char *name)\n>  struct list_head* hook_list(const char* hookname)\n>  {\n>  \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> +\tconst char *hook_path = find_hook(hookname);\n> +\n>  \n>  \tINIT_LIST_HEAD(hook_head);\n>  \n>  \tif (!hookname)\n>  \t\treturn NULL;\n>  \n> -\tif (have_git_dir()) {\n> -\t\tconst char *hook_path = find_hook(hookname);\n> -\n> -\t\t/* Add the hook from the hookdir */\n> -\t\tif (hook_path) {\n> -\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> -\t\t\tto_add->hook_path = hook_path;\n> -\t\t\tto_add->feed_pipe_cb_data = NULL;\n> -\t\t\tlist_add_tail(&to_add->list, hook_head);\n> -\t\t}\n> +\t/* Add the hook from the hookdir */\n> +\tif (hook_path) {\n> +\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> +\t\tto_add->hook_path = hook_path;\n> +\t\tto_add->feed_pipe_cb_data = NULL;\n> +\t\tlist_add_tail(&to_add->list, hook_head);\n>  \t}\n\nI do not think this belongs to the step to add \"list\" command.  The\nlog message does not explain or justify why have-git-dir goes away,\neither.\n"},{"id":"432591","messageId":"xmqqy29632hx.fsf@gitster.g","threadId":"55286","inReplyTo":"20210812004258.74318-5-emilyshaffer@google.com","subject":"Re: [PATCH v2 4/6] hook: allow running non-native hooks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-12T19:08:10Z","receivedAt":"2021-08-12T19:08:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index c36b05376c..3aa65dd791 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -46,7 +46,7 @@ static int list(int argc, const char **argv, const char *prefix)\n>  \n>  \thookname = argv[0];\n>  \n> -\thead = hook_list(hookname);\n> +\thead = hook_list(hookname, 1);\n>  \n>  \tif (list_empty(head)) {\n>  \t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> @@ -108,7 +108,7 @@ static int run(int argc, const char **argv, const char *prefix)\n>  \tgit_config(git_default_config, NULL);\n>  \n>  \thook_name = argv[0];\n> -\thooks = hook_list(hook_name);\n> +\thooks = hook_list(hook_name, 1);\n>  \tif (list_empty(hooks)) {\n>  \t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n>  \t\tif (ignore_missing)\n\nThis is minor, as I expect that the callers of hook_list() will\nalways confined in builtin/hook.c, but it probably is easier to read\nif you gave two functions, just like you have the pair of helpers\nfind_hook() and find_hook_gently(), as the literal \"1\" forces the\nreaders to remember if that means \"die if not found\", or \"ok if it\nis a bogus name\".\n\nIn addition, it may make more sense to keep hook_list() signal\nfailure by returning NULL and leave the dying to the caller.\nIn-code callers (as opposed to \"git hook run\" that can throw any\nrandom string that came from the user at the API) will never throw a\nbogus name unless there is a bug, and they'll have to check for an\nerror return from hook_list() anyway and the error message they\nwould give may have to be different from the one that is given\nagainst a hook name randomly thrown at us by the user.\n"},{"id":"432604","messageId":"xmqqsfze1jb3.fsf@gitster.g","threadId":"55286","inReplyTo":"20210812004258.74318-6-emilyshaffer@google.com","subject":"Re: [PATCH v2 5/6] hook: include hooks from the config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-12T20:48:00Z","receivedAt":"2021-08-12T20:48:09Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> Teach the hook.[hc] library to parse configs to populare the list of\n> hooks to run for a given event.\n>\n> Multiple commands can be specified for a given hook by providing\n> multiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n> \"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be run in\n> config order of the \"hook.<name>.event\" lines.\n>\n> For example:\n>\n>   $ git config --list | grep ^hook\n>   hook.bar.command=~/bar.sh\n>   hook.bar.event=pre-commit\n\nYour answer might be \"read the design doc\", but it is unclear to me\nwhy \"bar\" (friendly-name) is needed in this picture at all.  Is it\nbecause you may want to fire more than one command for pre-commit\nevent?  IOW,\n\n\t[hook \"bar\"]\n\t\tcommand = bar1.sh\n\t\tcommand = bar2.sh\n\t\tevent = pre-commit\n\nis easier to manage with an extra level of redirection?  I doubt it\nas \n\n\t[hook \"pre-commit\"]\n\t\tcommand = bar1.sh\n\t\tcommand = bar2.sh\n\nwould be equally expressive and shorter.  Or would it help use case\nfor multiple \"friendly-name\" to refer to the same \"event\", e.g.\n\n\t[hook \"xyzzy\"]\n\t\tevent = pre-commit\n\t\tcommand = xyzzy1\n\n\t[hook \"frotz\"]\n\t\tevent = pre-commit\n                command = frotz1\n                command = frotz2\n\nor something?  I am not sure if this gives us useful extra\nflexibility, and if so, the extra flexibility helps us more than it\nconfuses us.\n\nAnd moving the \"event\" to the second level in the configuration\nhierarchy, getting rid of \"friendly-name\" from the design, would not\nmake this example unworkable, either:\n\n>   $ git hook run\n>   # Runs ~/bar.sh\n>   # Runs .git/hooks/pre-commit\n\nAgain, this is not an objection wrapped in a rhetorical question.\nIt just is that I do not see how the extra level of redirection\nhelps us.\n\n> diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> index 96d3d6572c..a97b980cca 100644\n> --- a/Documentation/config/hook.txt\n> +++ b/Documentation/config/hook.txt\n> @@ -1,3 +1,8 @@\n> +hook.<command>.command::\n> +\tA command to execute during the <command> hook event. This can be an\n> +\texecutable on your device, a oneliner for your shell, or the name of a\n> +\thookcmd. See linkgit:git-hook[1].\n\nPlease make sure you use the terminology consistently.  If the\nsecond level is \"friendly name\", hook.<name>.command should be\ndescribed, instead of hook.<command>.command.\n\nAlso, to help those who are familiar with the current Git from their\nuse in the past 10 years or so, giving an example name from the\ncurrent system may help, e.g. when describing hook.<name>.event,\nyou may want to say the values are things like \"pre-commit\",\n\"receive\", etc.\n\n> +This command parses the default configuration files for pairs of configs like\n> +so:\n> +\n> +  [hook \"linter\"]\n> +    event = pre-commit\n> +    command = ~/bin/linter --c\n\nThe above addition of .command should also have hook.<name>.event\nnext to it, no?\n\n> +Conmmands are run in the order Git encounters their associated\n\n\"Conmmands -> Commands\", I would think.\n\n> +`hook.<name>.event` configs during the configuration parse (see\n> +linkgit:git-config[1]).\n\nHere you use <name>, which should be matched by the description in\nthe first hunk of the patch to this file.\n\n> +In general, when instructions suggest adding a script to\n> +`.git/hooks/<hook-event>`, you can specify it in the config instead by running\n> +`git config --add hook.<some-name>.command <path-to-script> && git config --add\n> +hook.<some-name>.event <hook-event>` - this way you can share the script between\n> +multiple repos. That is, `cp ~/my-script.sh ~/project/.git/hooks/pre-commit`\n> +would become `git config --add hook.my-script.command ~/my-script.sh && git\n> +config --add hook.my-script.event pre-commit`.\n\nOne repository may use a friendly name \"xyzzy\" while the other may\nuse \"frotz\" to group the hooks that trigger upon \"pre-commit\" event,\nbut unless one of the repositories change the friendly name to\nmatch, they cannot share these configurations, no?  It seems that an\nextra level of indirection is hindering sharing, rather than\nhelping.\n\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index 3aa65dd791..ea49dc4ef6 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -49,7 +49,7 @@ static int list(int argc, const char **argv, const char *prefix)\n>  \thead = hook_list(hookname, 1);\n>  \n>  \tif (list_empty(head)) {\n> -\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> +\t\tprintf(_(\"no hooks configured for event '%s'\\n\"),\n>  \t\t       hookname);\n> ...\n> @@ -58,7 +58,8 @@ static int list(int argc, const char **argv, const char *prefix)\n>  \t\tstruct hook *item = list_entry(pos, struct hook, list);\n>  \t\titem = list_entry(pos, struct hook, list);\n>  \t\tif (item)\n> -\t\t\tprintf(\"%s\\n\", item->hook_path);\n> +\t\t\tprintf(\"%s\\n\", item->name ? item->name\n> +\t\t\t\t\t\t  : _(\"hook from hookdir\"));\n>  \t}\n\nI won't comment on this part as my comments on earlier patches would\nprobably have butchered the preimage already for this change to\nsurvive intact ;-)\n"},{"id":"432891","messageId":"YRrnRjJxcwyMQY7S@google.com","threadId":"55286","inReplyTo":"xmqq5ywb6yrm.fsf@gitster.g","subject":"Re: [PATCH v2 0/6] config-based hooks restarted","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-16T22:31:34Z","receivedAt":"2021-08-16T22:31:45Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Aug 11, 2021 at 10:02:53PM -0700, Junio C Hamano wrote:\n> \n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > Emily Shaffer <emilyshaffer@google.com> writes:\n> >\n> >> This is the config-based hooks topic rebased onto v4 of Ævar's\n> >> branch[1].\n> >\n> > I have [1] in my tree, but these patches do not seem to apply\n> > cleanly; I see a failure in \"git hooks list\" step.\n> >\n> > Should I perhaps try merging [1] to one of the v2.33-rc and then\n> > queue these on top?\n> \n> Ah, I figured it out.  Your 3/6 seems to be stale wrt 4787177b\n> (hook: support passing stdin to hooks, 2021-08-03) that came from\n> https://lore.kernel.org/git/patch-v4-22.36-639e59e9ed0-20210803T191505Z-avarab@gmail.com/\n\nAh, I am sorry!\n\n> That patch did this to the early part of builtin/hook.c:\n> \n>         diff --git a/builtin/hook.c b/builtin/hook.c\n>         index f33db9953c..27dce6a2f0 100644\n>         --- a/builtin/hook.c\n>         +++ b/builtin/hook.c\n>         @@ -7,7 +7,7 @@\n>          #include \"strvec.h\"\n> \n>          #define BUILTIN_HOOK_RUN_USAGE \\\n>         -\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n>         +\tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n> \n>          static const char * const builtin_hook_usage[] = {\n>                 BUILTIN_HOOK_RUN_USAGE,\n> \n> \n> but your [v2 3/6] here expects something entirely different.\n> \n>         diff --git a/builtin/hook.c b/builtin/hook.c\n>         index 12c9126032..c36b05376c 100644\n>         --- a/builtin/hook.c\n>         +++ b/builtin/hook.c\n>         @@ -8,8 +8,11 @@\n> \n>          #define BUILTIN_HOOK_RUN_USAGE \\\n>                 N_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n>         +#define BUILTIN_HOOK_LIST_USAGE \\\n>         +\tN_(\"git hook list <hook-name>\")\n> \n> I've wiggled the patch in, as there wasn't any other funny\n> inconsistency like this one, but please double check the result\n> after I push it out perhaps tomorrow morning.\n\nI'll take a look and update local accordingly. Sorry for the trouble.\n\n - Emily\n"},{"id":"432896","messageId":"YRr24PJHlyFg7n/W@google.com","threadId":"55286","inReplyTo":"xmqqbl6260eo.fsf@gitster.g","subject":"Re: [PATCH v2 1/6] hook: run a list of hooks instead","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-16T23:38:08Z","receivedAt":"2021-08-16T23:38:18Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Aug 12, 2021 at 10:25:03AM -0700, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > To prepare for multihook support, teach hook.[hc] to take a list of\n> > hooks at run_hooks and run_found_hooks. Right now the list is always one\n> > entry, but in the future we will allow users to supply more than one\n> > executable for a single hook event.\n> >\n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >  builtin/hook.c |  14 ++++---\n> >  hook.c         | 103 +++++++++++++++++++++++++++++++++++--------------\n> >  hook.h         |  16 +++++++-\n> >  3 files changed, 96 insertions(+), 37 deletions(-)\n> >\n> > diff --git a/builtin/hook.c b/builtin/hook.c\n> > index 5eb7cf73a4..4d39c9e75e 100644\n> > --- a/builtin/hook.c\n> > +++ b/builtin/hook.c\n> > @@ -25,7 +25,8 @@ static int run(int argc, const char **argv, const char *prefix)\n> >  \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n> >  \tint ignore_missing = 0;\n> >  \tconst char *hook_name;\n> > -\tconst char *hook_path;\n> > +\tstruct list_head *hooks;\n> > +\n> \n> Natural.  We used to use the path to the hook because we were\n> expecting only one. We now use the name to find a list of hooks.\n> \n> All the caller sees is just list_head without any direct visibility\n> into it, which feels like a great abstraction.  Presumably everything\n> will go through the API functions taking this opaque \"list of hooks\"\n> thing (or \"the first one in the list\" if the API function does not\n> iterate over it, perhaps?).\n\nHum, I guess that in a later patch builtin/hook.c does learn to take\napart the list_head into a 'struct hook' to print the output of 'git\nhook list'. I haven't read your review of that patch yet though.\n\n> > diff --git a/hook.c b/hook.c\n> > index ee20b2e365..80e150548c 100644\n> > --- a/hook.c\n> > +++ b/hook.c\n> > @@ -4,6 +4,28 @@\n> >  #include \"hook-list.h\"\n> >  #include \"config.h\"\n> >  \n> > +static void free_hook(struct hook *ptr)\n> > +{\n> > +\tif (ptr) {\n> > +\t\tfree(ptr->feed_pipe_cb_data);\n> > +\t}\n> \n> Lose the extra {}, as we do not do more than the above free() even\n> at the end of the series?\n\nACK\n\n> \n> > +struct list_head* hook_list(const char* hookname)\n> \n> Shift both of the asterisks; our asterisks do not stick to the type\n> but to the identifier.\n\nACK\n\n> \n> > +{\n> > +\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> > +\n> > +\tINIT_LIST_HEAD(hook_head);\n> > +\n> > +\tif (!hookname)\n> > +\t\treturn NULL;\n> \n> Checking for invalid hookname first would avoid leaking hook_head,\n> no?  The caller of hook_list() we saw earlier immediately calls\n> list_empty() which will segfault.  The caller need to be tightened,\n> but I wonder if it would be a programmer's error to pass a NULL\n> hookname to this function.  If so, this can simply be a BUG() and\n> the earlier allocation and initialization of hook_head can stay\n> where they are.  Otherwise, the caller should see if this returns a\n> NULL.\n\nAh, good point. I think this makes sense to BUG().\n\n> \n> > +\tif (have_git_dir()) {\n> > +\t\tconst char *hook_path = find_hook(hookname);\n> > +\n> > +\t\t/* Add the hook from the hookdir */\n> > +\t\tif (hook_path) {\n> > +\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> > +\t\t\tto_add->hook_path = hook_path;\n> > +\t\t\tto_add->feed_pipe_cb_data = NULL;\n> > +\t\t\tlist_add_tail(&to_add->list, hook_head);\n> > +\t\t}\n> > +\t}\n> \n> Calling this function to grab a list of hooks when we are not in any\n> repository is not an error but just we get \"there is nothing to\n> run\".  Does the design give us a more useful behaviour, compared to\n> alternatives like \"you have to be in a repository or calling this\n> function is an error\"?\n\nLater we enable calling hook_list without a gitdir, in patch 6\n(https://lore.kernel.org/git/20210812004258.74318-7-emilyshaffer%40google.com).\nSo maybe the behavior as it is now is premature?\n\nBut leaving the hook list empty means we can behave gracefully if\nanybody is calling a hook without necessarily being sure they have\ngitdir. I would need to audit callsites to check if they are checking\nwhether they have one or not. I think in most cases they probably are\nchecking that.\n\n> \n> Not an objection wrapped in a rhetorical question, but a genuine\n> question.  \"It would help this and that usecase\" would be an ideal\n> answer, \"We could do either way, but we happened to have written the\n> code this way first, and at the end of the series we did not see any\n> practical downsides\" would also be a great answer.\n\nThe main use case, today, for letting this work nicely even outside of\na gitdir, is sendemail-validate hook. But mostly, I was thinking that\nwhen we're freed from dependence on .git/hooks/, there's no reason to\ndisallow out-of-repo hooks in case someone wants to add a new one in the\nfuture - for example to 'git maintenance' daemon runs.\n\n> \n> > +\treturn hook_head;\n> > +}\n> > +\n> \n> > +/*\n> > + * Provides a linked list of 'struct hook' detailing commands which should run\n> > + * in response to the 'hookname' event, in execution order.\n> > + */\n> > +struct list_head* hook_list(const char *hookname);\n> \n> struct list_head *hook_list(const char *hookname);\nACK\n\nThanks for the thoughts.\n - Emily\n"},{"id":"432898","messageId":"YRr78f5YgGjBmBYp@google.com","threadId":"55286","inReplyTo":"xmqqmtpm4kmy.fsf@gitster.g","subject":"Re: [PATCH v2 2/6] hook: allow parallel hook execution","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-16T23:59:45Z","receivedAt":"2021-08-16T23:59:56Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Aug 12, 2021 at 10:51:01AM -0700, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > In many cases, there's no reason not to allow hooks to execute in\n> > parallel. run_processes_parallel() is well-suited - it's a task queue\n> > that runs its housekeeping in series, which means users don't\n> > need to worry about thread safety on their callback data. True\n> > multithreaded execution with the async_* functions isn't necessary here.\n> > Synchronous hook execution can be achieved by only allowing 1 job to run\n> > at a time.\n> >\n> > Teach run_hooks() to use that function for simple hooks which don't\n> > require stdin or capture of stderr.\n> \n> Since run_hooks() has been using run_processes_parallel_tr2()\n> already in the ab/config-based-hooks-base topic, the above\n> description puzzled me quite a bit.  I think you meant to say with\n> this step you update the callers to pass either .jobs=1 or .jobs=0\n> in the hooks_opt, so that hooks _can_ run in parallel (as opposed\n> to using hardcoded jobs=1 in run_hooks() when calling the underlying\n> run_processes_parallel_tr2() function).\n\nI think you are right, and this commit message is from a much older\nversion of the series. Ok; I will reword it. I think it can be much\nsimpler.\n\n> \n> I do not think SYNC and ASYNC are great adjectives for what you are\n> doing in this step.  Wouldn't ASYNC mean \"the caller tells the hooks\n> to run, continues without waiting for them to finish\"?  In order to\n> let more than one hooks to run at the same time, the caller has to\n> continue without waiting for the first one it started so that it can\n> start the second one before the first one finishes (otherwise these\n> two hooks will not run at the same time), but ASYNC implys a lot\n> more than that.  After starting all of these hooks, the caller may\n> continue doing things that are not even related to these spawned\n> hooks if a hook is run asynchronously.\n> \n> But I do not think that is what is happening here.  After all,\n> run_processes_parallel_tr2() will not return until all of the\n> subprocesses are culled.\n> \n> Perhaps the distinction you are trying to express is easier to\n> convey to readers if you instead used the verb TO RUN with the\n> adverb SERIALLY vs IN PARALLEL (as opposed to SYNCHRONOUSLY vs\n> ASYNCHRONOUSLY)?\n\nGood point. I think you are right and I'll change the init macros to\nSERIAL/PARALLEL instead.\n\n> \n> > diff --git a/builtin/hook.c b/builtin/hook.c\n> > index 4d39c9e75e..12c9126032 100644\n> > --- a/builtin/hook.c\n> > +++ b/builtin/hook.c\n> > @@ -22,7 +22,7 @@ static const char * const builtin_hook_run_usage[] = {\n> >  static int run(int argc, const char **argv, const char *prefix)\n> >  {\n> >  \tint i;\n> > -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n> > +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n> >  \tint ignore_missing = 0;\n> >  \tconst char *hook_name;\n> >  \tstruct list_head *hooks;\n> > @@ -32,6 +32,8 @@ static int run(int argc, const char **argv, const char *prefix)\n> >  \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n> >  \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n> >  \t\t\t   N_(\"file to read into hooks' stdin\")),\n> > +\t\tOPT_INTEGER('j', \"jobs\", &opt.jobs,\n> > +\t\t\t    N_(\"run up to <n> hooks simultaneously\")),\n> >  \t\tOPT_END(),\n> >  \t};\n> >  \tint ret;\n> \n> OK, so by default \"git hook\" runs 1 at a time but we can instruct to\n> run the optimal number of jobs via \"git hook -j0\" etc.\n\nIs it surprising for an explicitly provided \"-j0\" to still be overridden\nby a config if one was provided? That is, \"-j0\" doesn't mean\n\"-j$(nproc)\" as it does in many other Unixy tools, here. It means\n\"-j(hook.jobs OR $(nproc))\". I guess you could also say that \"if you\nconfigured hook.jobs, it is because you decided nproc was less optimal\nthan the value you put into hook.jobs\" - so maybe it is fine?\n\n> \n> > diff --git a/hook.c b/hook.c\n> > index 80e150548c..37f682c6d8 100644\n> > --- a/hook.c\n> > +++ b/hook.c\n> > @@ -228,6 +228,28 @@ static int notify_hook_finished(int result,\n> >  \treturn 0;\n> >  }\n> >  \n> > +/*\n> > + * Determines how many jobs to use after we know we want to parallelize. First\n> > + * priority is the config 'hook.jobs' and second priority is the number of CPUs.\n> > + */\n> > +static int configured_hook_jobs(void)\n> > +{\n> > +\t/*\n> > +\t * The config and the CPU count probably won't change during the process\n> > +\t * lifetime, so cache the result in case we invoke multiple hooks during\n> > +\t * one process.\n> > +\t */\n> > +\tstatic int jobs = 0;\n> > +\tif (jobs)\n> > +\t\treturn jobs;\n> > +\n> > +\tif (git_config_get_int(\"hook.jobs\", &jobs))\n> > +\t\t/* if the config isn't set, fall back to CPU count. */\n> > +\t\tjobs = online_cpus();\n> > +\n> > +\treturn jobs;\n> > +}\n> \n> Not a suggestion to make this improvement as a part of this series,\n> but there already are more than several codepaths doing essentially\n> the same thing as above, taking their own configuration or\n> environment variables and falling back to online_cpus().\n> \n> It may be worth introducing a shared helper function so that this\n> can become\n> \n> \tstatic int configured_hook_jobs(void)\n> \t{\n> \t\tstatic int jobs; /* no need for \"= 0\" */\n> \n> \t\tif (!jobs)\n> \t\t\tjobs = default_parallelism(\"hook.jobs\", NULL);\n> \t\treturn jobs;\n> \t}\n> \n> where the new helper takes the configuration and environment\n> variable names.\n\nSounds handy.\n\n - Emily\n"},{"id":"432899","messageId":"YRsEhidmf54bdEJE@google.com","threadId":"55286","inReplyTo":"xmqq5ywa4hg8.fsf@gitster.g","subject":"Re: [PATCH v2 3/6] hook: introduce \"git hook list\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-17T00:36:22Z","receivedAt":"2021-08-17T00:37:52Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Aug 12, 2021 at 11:59:51AM -0700, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > +static int list(int argc, const char **argv, const char *prefix)\n> > +{\n> > +\tstruct list_head *head, *pos;\n> > +\tconst char *hookname = NULL;\n> > +\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n> > +\n> > +\tstruct option list_options[] = {\n> > +\t\tOPT_END(),\n> > +\t};\n> > +\n> > +\targc = parse_options(argc, argv, prefix, list_options,\n> > +\t\t\t     builtin_hook_list_usage, 0);\n> > +\n> > +\tif (argc < 1)\n> > +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n> > +\t\t\t      builtin_hook_list_usage, list_options);\n> > +\n> > +\thookname = argv[0];\n> > +\n> > +\thead = hook_list(hookname);\n> > +\n> > +\tif (list_empty(head)) {\n> \n> The same \"can't hook_list() signal an error by returning NULL?\"\n> comment applies here.\n> \n> \thead = hook_list(hookname);\n> \tif (!head)\n> \t\tdie(_(\"no such hook '%s'\"), hookname);\n> \n> or something?\n> \n> > +\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> > +\t\t       hookname);\n> > +\t\treturn 0;\n> \n> If it is a normally expected state that there is no hook for the\n> given name, signalling success by returning 0 here may be sensible,\n> but then the message should at least go to the standard error stream\n> to leave the standard output empty, so that a caller can reasonably\n> do something like\n> \n> \tfor path in $(git hooks list \"$1\")\n> \tdo\n> \t\tls -l \"$path\"\n> \tdone\n> \n> If we really want to show such a message, perhaps\n> \n> \tif (list_empty(head)) {\n>         \tif (!quiet)\n> \t\t\twarning(_(\"no commands configured\"));\n> \t\treturn 0;\n> \t}\n> \n> The normal display just shows the path without saying \"command %s\n> will run for hook %s\"; the warning probably should do the same.\n> \n> Having said that, if it truly is a normal and expected state that no\n> hook is defined for the given name, I actually think there should be\n> no message.\n\nAh, I think you are saying \"either return an error code and be chatty if you\nwant, or return an empty list and a success code, but pick one\". Makes\nsense to me.\n\nNo message + well-defined return code sounds fine. I'll do that.\n\n> \n> > +\t}\n> > +\n> > +\tlist_for_each(pos, head) {\n> > +\t\tstruct hook *item = list_entry(pos, struct hook, list);\n> > +\t\titem = list_entry(pos, struct hook, list);\n> > +\t\tif (item)\n> > +\t\t\tprintf(\"%s\\n\", item->hook_path);\n> > +\t}\n> \n> > diff --git a/hook.c b/hook.c\n> > index 37f682c6d8..2714b63473 100644\n> > --- a/hook.c\n> > +++ b/hook.c\n> > @@ -96,22 +96,20 @@ int hook_exists(const char *name)\n> >  struct list_head* hook_list(const char* hookname)\n> >  {\n> >  \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> > +\tconst char *hook_path = find_hook(hookname);\n> > +\n> >  \n> >  \tINIT_LIST_HEAD(hook_head);\n> >  \n> >  \tif (!hookname)\n> >  \t\treturn NULL;\n> >  \n> > -\tif (have_git_dir()) {\n> > -\t\tconst char *hook_path = find_hook(hookname);\n> > -\n> > -\t\t/* Add the hook from the hookdir */\n> > -\t\tif (hook_path) {\n> > -\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> > -\t\t\tto_add->hook_path = hook_path;\n> > -\t\t\tto_add->feed_pipe_cb_data = NULL;\n> > -\t\t\tlist_add_tail(&to_add->list, hook_head);\n> > -\t\t}\n> > +\t/* Add the hook from the hookdir */\n> > +\tif (hook_path) {\n> > +\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> > +\t\tto_add->hook_path = hook_path;\n> > +\t\tto_add->feed_pipe_cb_data = NULL;\n> > +\t\tlist_add_tail(&to_add->list, hook_head);\n> >  \t}\n> \n> I do not think this belongs to the step to add \"list\" command.  The\n> log message does not explain or justify why have-git-dir goes away,\n> either.\n\nAh, sure.\n\n\nIt seems like I also didn't update the documentation for 'git hook'\ncommand during this commit. Will fix that as well.\n"},{"id":"433083","messageId":"YR1y7jxhGmnTJKyH@google.com","threadId":"55286","inReplyTo":"xmqqy29632hx.fsf@gitster.g","subject":"Re: [PATCH v2 4/6] hook: allow running non-native hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-18T20:51:58Z","receivedAt":"2021-08-18T20:52:11Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Aug 12, 2021 at 12:08:10PM -0700, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > diff --git a/builtin/hook.c b/builtin/hook.c\n> > index c36b05376c..3aa65dd791 100644\n> > --- a/builtin/hook.c\n> > +++ b/builtin/hook.c\n> > @@ -46,7 +46,7 @@ static int list(int argc, const char **argv, const char *prefix)\n> >  \n> >  \thookname = argv[0];\n> >  \n> > -\thead = hook_list(hookname);\n> > +\thead = hook_list(hookname, 1);\n> >  \n> >  \tif (list_empty(head)) {\n> >  \t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> > @@ -108,7 +108,7 @@ static int run(int argc, const char **argv, const char *prefix)\n> >  \tgit_config(git_default_config, NULL);\n> >  \n> >  \thook_name = argv[0];\n> > -\thooks = hook_list(hook_name);\n> > +\thooks = hook_list(hook_name, 1);\n> >  \tif (list_empty(hooks)) {\n> >  \t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n> >  \t\tif (ignore_missing)\n> \n> This is minor, as I expect that the callers of hook_list() will\n> always confined in builtin/hook.c, but it probably is easier to read\n> if you gave two functions, just like you have the pair of helpers\n> find_hook() and find_hook_gently(), as the literal \"1\" forces the\n> readers to remember if that means \"die if not found\", or \"ok if it\n> is a bogus name\".\n\nYes, I see what you mean. Ok. I have been wanting to change the naming\nanyways - most functions in hook.h are verb-y (\"find hook\", \"run hooks\",\nso on) but hook_list stands out as the only noun-y function.\n\nSo I considered changing it to \"list_hooks\" and \"list_hooks_gently\", to align\nwith find_hook(_gently)....\n\n> \n> In addition, it may make more sense to keep hook_list() signal\n> failure by returning NULL and leave the dying to the caller.\n> In-code callers (as opposed to \"git hook run\" that can throw any\n> random string that came from the user at the API) will never throw a\n> bogus name unless there is a bug, and they'll have to check for an\n> error return from hook_list() anyway and the error message they\n> would give may have to be different from the one that is given\n> against a hook name randomly thrown at us by the user.\n\nSure, that makes sense enough... but then I wonder if it would be better\nto let the caller check whether the name is allowed at all, first,\nseparately from the hook_list() call.\n\nOn the one hand, having hook_list() do the validation of the hook name\nmakes it harder for a hook doing something very unusual to neglect to\nadd documentation. (I'm thinking of, for example, a hook doing something\nequally weird to the proc-receive hook, which cannot use the hook\nlibrary because it needs to be able to do this weird two-way\ncommunication thing.\n(https://lore.kernel.org/git/20210527000856.695702-31-emilyshaffer%40google.com))\nIt would be pretty bad for a hook which is already complicated to also\nforget to include documentation.\n\nOn the other hand, as it is now - builtin/hook.c hardcodes \"I don't care\nif the hook is unknown\" and hook.c hardcodes \"reject if the hook is\nunknown\" and nobody else calls hook_list at all - it isn't so bad to\nbail early, before even calling hook_list() in the first place, if the\nhook is unknown.\n\nI also think that approach would make a callsite easier to understand\nthan checking for null from hook_list().\n\n  const char *hookname = \"my-new-hook\";\n\n  /* Here it's pretty clear what the reason for the error was... */\n  if (!known_hook(hookname))\n    BUG(\"is hook '%s' in Documentation/githooks.txt?\", hookname);\n\n  hooks = hook_list(hookname);\n  ...\n\nvs.\n\n  const char *hookname = \"my-new-hook\";\n  hooks = hook_list(hookname);\n  /*\n   * But here, I have to go and look at the hook_list() source to\n   * understand why null 'hooks' means I missed some doc step.\n   */\n  if (!hookname)\n    BUG(\"is hook '%s' in Documentation/githooks.txt?\", hookname);\n  ...\n\nMaybe others disagree with me, but I would guess the first example is\nmore easily understandable to someone unfamiliar with the hook code. So\nI think I will go with that approach, and include some notice in the doc\ncomment over hook_list().\n\n - Emily\n"},{"id":"433087","messageId":"YR14M7IJ9SoSwFDU@google.com","threadId":"55286","inReplyTo":"YR1y7jxhGmnTJKyH@google.com","subject":"Re: [PATCH v2 4/6] hook: allow running non-native hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-18T21:14:27Z","receivedAt":"2021-08-18T21:14:37Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Aug 18, 2021 at 01:51:58PM -0700, Emily Shaffer wrote:\n> \n> On Thu, Aug 12, 2021 at 12:08:10PM -0700, Junio C Hamano wrote:\n> > \n> > Emily Shaffer <emilyshaffer@google.com> writes:\n> > \n> > > diff --git a/builtin/hook.c b/builtin/hook.c\n> > > index c36b05376c..3aa65dd791 100644\n> > > --- a/builtin/hook.c\n> > > +++ b/builtin/hook.c\n> > > @@ -46,7 +46,7 @@ static int list(int argc, const char **argv, const char *prefix)\n> > >  \n> > >  \thookname = argv[0];\n> > >  \n> > > -\thead = hook_list(hookname);\n> > > +\thead = hook_list(hookname, 1);\n> > >  \n> > >  \tif (list_empty(head)) {\n> > >  \t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> > > @@ -108,7 +108,7 @@ static int run(int argc, const char **argv, const char *prefix)\n> > >  \tgit_config(git_default_config, NULL);\n> > >  \n> > >  \thook_name = argv[0];\n> > > -\thooks = hook_list(hook_name);\n> > > +\thooks = hook_list(hook_name, 1);\n> > >  \tif (list_empty(hooks)) {\n> > >  \t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n> > >  \t\tif (ignore_missing)\n> > \n> > This is minor, as I expect that the callers of hook_list() will\n> > always confined in builtin/hook.c, but it probably is easier to read\n> > if you gave two functions, just like you have the pair of helpers\n> > find_hook() and find_hook_gently(), as the literal \"1\" forces the\n> > readers to remember if that means \"die if not found\", or \"ok if it\n> > is a bogus name\".\n> \n> Yes, I see what you mean. Ok. I have been wanting to change the naming\n> anyways - most functions in hook.h are verb-y (\"find hook\", \"run hooks\",\n> so on) but hook_list stands out as the only noun-y function.\n> \n> So I considered changing it to \"list_hooks\" and \"list_hooks_gently\", to align\n> with find_hook(_gently)....\n> \n> > \n> > In addition, it may make more sense to keep hook_list() signal\n> > failure by returning NULL and leave the dying to the caller.\n> > In-code callers (as opposed to \"git hook run\" that can throw any\n> > random string that came from the user at the API) will never throw a\n> > bogus name unless there is a bug, and they'll have to check for an\n> > error return from hook_list() anyway and the error message they\n> > would give may have to be different from the one that is given\n> > against a hook name randomly thrown at us by the user.\n> \n> Sure, that makes sense enough... but then I wonder if it would be better\n> to let the caller check whether the name is allowed at all, first,\n> separately from the hook_list() call.\n> \n> On the one hand, having hook_list() do the validation of the hook name\n> makes it harder for a hook doing something very unusual to neglect to\n> add documentation. (I'm thinking of, for example, a hook doing something\n> equally weird to the proc-receive hook, which cannot use the hook\n> library because it needs to be able to do this weird two-way\n> communication thing.\n> (https://lore.kernel.org/git/20210527000856.695702-31-emilyshaffer%40google.com))\n> It would be pretty bad for a hook which is already complicated to also\n> forget to include documentation.\n> \n> On the other hand, as it is now - builtin/hook.c hardcodes \"I don't care\n> if the hook is unknown\" and hook.c hardcodes \"reject if the hook is\n> unknown\" and nobody else calls hook_list at all - it isn't so bad to\n> bail early, before even calling hook_list() in the first place, if the\n> hook is unknown.\n> \n> I also think that approach would make a callsite easier to understand\n> than checking for null from hook_list().\n> \n>   const char *hookname = \"my-new-hook\";\n> \n>   /* Here it's pretty clear what the reason for the error was... */\n>   if (!known_hook(hookname))\n>     BUG(\"is hook '%s' in Documentation/githooks.txt?\", hookname);\n> \n>   hooks = hook_list(hookname);\n>   ...\n> \n> vs.\n> \n>   const char *hookname = \"my-new-hook\";\n>   hooks = hook_list(hookname);\n>   /*\n>    * But here, I have to go and look at the hook_list() source to\n>    * understand why null 'hooks' means I missed some doc step.\n>    */\n>   if (!hookname)\n>     BUG(\"is hook '%s' in Documentation/githooks.txt?\", hookname);\n>   ...\n> \n> Maybe others disagree with me, but I would guess the first example is\n> more easily understandable to someone unfamiliar with the hook code. So\n> I think I will go with that approach, and include some notice in the doc\n> comment over hook_list().\n\nHm. Now that I sit to type it, I guess putting the onus on the\nstrange-new-hook caller to also type \"if known_hook()\" is about the same\nas just expecting the strange-new-hook caller to know they are supposed\nto document their hook. Plus, known_hook() is static right now.\n\nI think it still makes sense to BUG() instead of error() or die() in\n'list_hooks()' (non-gently) - the failure of that call is a developer\nerror, either in not having documented their hook correctly or in\ncalling 'list_hooks()' instead of 'list_hooks_gently()' when they meant\nthe latter. So I will not take the NULL return approach.\n\n - Emily\n"},{"id":"433088","messageId":"xmqqh7fm4faw.fsf@gitster.g","threadId":"55286","inReplyTo":"YR14M7IJ9SoSwFDU@google.com","subject":"Re: [PATCH v2 4/6] hook: allow running non-native hooks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-18T21:24:23Z","receivedAt":"2021-08-18T21:24:27Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n>> Yes, I see what you mean. Ok. I have been wanting to change the naming\n>> anyways - most functions in hook.h are verb-y (\"find hook\", \"run hooks\",\n>> so on) but hook_list stands out as the only noun-y function.\n>> \n>> So I considered changing it to \"list_hooks\" and \"list_hooks_gently\", to align\n>> with find_hook(_gently)....\n\nI do not claim I am goot at naming (or better than you at it\nanyway), but list-hooks sounds to me like it is calling printf()\nto show the hooks to the user, not computing a list of hooks and\nreturning it to the caller.\n\n>> I also think that approach would make a callsite easier to understand\n>> than checking for null from hook_list().\n>> \n>>   const char *hookname = \"my-new-hook\";\n>> \n>>   /* Here it's pretty clear what the reason for the error was... */\n>>   if (!known_hook(hookname))\n>>     BUG(\"is hook '%s' in Documentation/githooks.txt?\", hookname);\n\nYes.  The callsite becomes easier to understand, and it separates\nthe responsibility between the helper to respond to \"please give me\nlist of defined hooks\" and its caller that may react to the returned\nlist with \"ok, among these hooks, this does not look kosher for such\nand such reason, so I'd die/warn/error\" much cleaner.\n\n\n"},{"id":"433094","messageId":"YR2hQBtZTX5oHdQg@google.com","threadId":"55286","inReplyTo":"xmqqsfze1jb3.fsf@gitster.g","subject":"Re: [PATCH v2 5/6] hook: include hooks from the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T00:09:36Z","receivedAt":"2021-08-19T00:09:47Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Aug 12, 2021 at 01:48:00PM -0700, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > Teach the hook.[hc] library to parse configs to populare the list of\n> > hooks to run for a given event.\n> >\n> > Multiple commands can be specified for a given hook by providing\n> > multiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n> > \"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be run in\n> > config order of the \"hook.<name>.event\" lines.\n> >\n> > For example:\n> >\n> >   $ git config --list | grep ^hook\n> >   hook.bar.command=~/bar.sh\n> >   hook.bar.event=pre-commit\n> \n> Your answer might be \"read the design doc\", but it is unclear to me\n> why \"bar\" (friendly-name) is needed in this picture at all.  Is it\n> because you may want to fire more than one command for pre-commit\n> event?  IOW,\n> \n> \t[hook \"bar\"]\n> \t\tcommand = bar1.sh\n> \t\tcommand = bar2.sh\n> \t\tevent = pre-commit\n> \n> is easier to manage with an extra level of redirection?  I doubt it\n> as \n> \n> \t[hook \"pre-commit\"]\n> \t\tcommand = bar1.sh\n> \t\tcommand = bar2.sh\n> \n> would be equally expressive and shorter.  Or would it help use case\n> for multiple \"friendly-name\" to refer to the same \"event\", e.g.\n> \n> \t[hook \"xyzzy\"]\n> \t\tevent = pre-commit\n> \t\tcommand = xyzzy1\n> \n> \t[hook \"frotz\"]\n> \t\tevent = pre-commit\n>                 command = frotz1\n>                 command = frotz2\n> \n> or something?  I am not sure if this gives us useful extra\n> flexibility, and if so, the extra flexibility helps us more than it\n> confuses us.\n> \n> And moving the \"event\" to the second level in the configuration\n> hierarchy, getting rid of \"friendly-name\" from the design, would not\n> make this example unworkable, either:\n> \n> >   $ git hook run\n> >   # Runs ~/bar.sh\n> >   # Runs .git/hooks/pre-commit\n> \n> Again, this is not an objection wrapped in a rhetorical question.\n> It just is that I do not see how the extra level of redirection\n> helps us.\n\nPlease have a look at\nhttps://lore.kernel.org/git/87fswey5wd.fsf%40evledraar.gmail.com and\nreplies, where Ævar and I discussed the schema change at length. I know\nit is a lot of back and forth but I think it is useful to understand why\nI ended up changing the schema this way.\n\n> \n> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> > index 96d3d6572c..a97b980cca 100644\n> > --- a/Documentation/config/hook.txt\n> > +++ b/Documentation/config/hook.txt\n> > @@ -1,3 +1,8 @@\n> > +hook.<command>.command::\n> > +\tA command to execute during the <command> hook event. This can be an\n> > +\texecutable on your device, a oneliner for your shell, or the name of a\n> > +\thookcmd. See linkgit:git-hook[1].\n> \n> Please make sure you use the terminology consistently.  If the\n> second level is \"friendly name\", hook.<name>.command should be\n> described, instead of hook.<command>.command.\n\nThanks, this is an oversight. Will update the config/hook.txt doc in\nnext reroll.\n\n> \n> Also, to help those who are familiar with the current Git from their\n> use in the past 10 years or so, giving an example name from the\n> current system may help, e.g. when describing hook.<name>.event,\n> you may want to say the values are things like \"pre-commit\",\n> \"receive\", etc.\n\nSure.\n\n> \n> > +This command parses the default configuration files for pairs of configs like\n> > +so:\n> > +\n> > +  [hook \"linter\"]\n> > +    event = pre-commit\n> > +    command = ~/bin/linter --c\n> \n> The above addition of .command should also have hook.<name>.event\n> next to it, no?\n\nI don't understand the question. Doesn't this config snippet equate to\n\"\"\"\n  hook.linter.event=pre-commit\n  hook.linter.command=~/bin/linter --c\n\"\"\"\n? So in this case, '<name>' is 'linter', as that's not a native Git hook.\n\n> \n> > +Conmmands are run in the order Git encounters their associated\n> \n> \"Conmmands -> Commands\", I would think.\nACK\n> \n> > +`hook.<name>.event` configs during the configuration parse (see\n> > +linkgit:git-config[1]).\n> \n> Here you use <name>, which should be matched by the description in\n> the first hunk of the patch to this file.\nYep.\n> \n> > +In general, when instructions suggest adding a script to\n> > +`.git/hooks/<hook-event>`, you can specify it in the config instead by running\n> > +`git config --add hook.<some-name>.command <path-to-script> && git config --add\n> > +hook.<some-name>.event <hook-event>` - this way you can share the script between\n> > +multiple repos. That is, `cp ~/my-script.sh ~/project/.git/hooks/pre-commit`\n> > +would become `git config --add hook.my-script.command ~/my-script.sh && git\n> > +config --add hook.my-script.event pre-commit`.\n> \n> One repository may use a friendly name \"xyzzy\" while the other may\n> use \"frotz\" to group the hooks that trigger upon \"pre-commit\" event,\n> but unless one of the repositories change the friendly name to\n> match, they cannot share these configurations, no?  It seems that an\n> extra level of indirection is hindering sharing, rather than\n> helping.\n\nAh, I think this means the documentation isn't sufficient, if you are\nasking that. Instead of explaining it in ephemeral email, I think it is\nbetter for me to explain it in documentation reroll, and for you to then\ntell me how you interpret it. I expect to send the reroll before I go\nhome today, since I didn't receive comments from anybody besides you so\nfar.\n\nThanks very much for the feedback on the doc - this is very useful.\n\n - Emily\n"},{"id":"433096","messageId":"YR2jLdYQA5CVzX5h@google.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 00/36] Run hooks via \"git run hook\" & hook library","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T00:17:49Z","receivedAt":"2021-08-19T00:18:00Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:26PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> This is a v4 re-roll of the \"Base for \"config-based-hooks\" topic.\n\nI performed the rebase of my series on top of this one, and did not find\nvery much objectionable there, but do not consider this email a thorough\nreview of the interdiff. I expect to have time for that later in the\nweek.\n\n> Other updates:\n> \n>  * In the base topic the s/Signed-off-by/Reviewed-by/g from René\n>    change that Junio applied locally has been folded in.\n> \n>  * Almost all the callers were just \"one-shot\" callers, I introduced a\n>    new run_hooks_oneshot() function for those, which gets rid of the\n>    verbosity around memory management, see e.g. the \"builtin/gc.c\" in\n>    the range-diff below. That run_hooks_oneshot() can also take a NULL\n>    set of options.\n\nI am not so wild about this, to be honest, only because after my patch\n2, it is hard to understand whether or not a hook is running in parallel\nor in series. It is extra code-reader overhead to know that\nrun_hooks_oneshot() assumes that a hook can be parallelized, and I think\nthat is unfortunate.\n\nBut that is a pretty minor complaint, and I do like the lower\ncomplexity, and I agree the defaults are \"good enough\" for most hook\nevents. So I guess I'm asking for ideas on how to make it more obvious\nwhat this oneshot call does in terms of parallelism.\n\nThanks, and I hope to get to a fuller review of the interdiff soon.\n\n - Emily\n"},{"id":"433100","messageId":"20210819033450.3382652-1-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210812004258.74318-1-emilyshaffer@google.com","subject":"[PATCH v3 0/6] config-based hooks restarted","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T03:34:44Z","receivedAt":"2021-08-19T03:34:57Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"This is the config-based hooks topic rebased onto v4 of Ævar's\nbranch[1]. There is a happy CI build of it on GitHub[2].\n\nThe topic overall adds the ability to set up hooks by modifying the\nconfig, in addition to placing specially named hooks into the hookdir.\nThis enables users to specify multiple hooks for a given event, and so\nthis topic also fleshes out the use of the run_processes_parallel() API\nwhich is now introduced in Ævar's reordering of prior patches.\n\nPatches 1-4 make some minor changes to prepare Ævar's series to handle\nmore than one hook at a time. With the exception of patch 4, there\nshould be no behavior change for existing hooks.\n\nPatch 2 is opinionated about which hooks should and shouldn't be allowed\nto run in parallel; if you care about a specific hook, please take a\nlook there.\n\nPatch 5 is the motivating feature - it begins to parse the config\nlooking for hooks.\n\nPatch 6 takes advantage of the decoupling of hooks and GITDIR to allow\nout-of-repo hook runs, which would only run hooks specified in the\nsystem or global config. This mainly targets 'sendemail-validate', but\n'git-sendemail.perl' still explicitly disallows out-of-repo hook\nexecution on that hook for now. (Maybe that change should be added in\nthis series? Or maybe patch 6 belongs with that kind of change?)\n\nSince v2, I have addressed comments left by Junio - thanks for the\nreview. Mostly these were small nits, but two somewhat larger changes\ncame out:\n\n - 'hook_list' becomes 'list_hooks' and 'list_hooks_gently'. It might\n   still be better to name it something else, and it might still be\n   better to decouple the \"is this a known hook\" check from the \"give me\n   all the hooks\" function; comments welcome.\n - The documentation in patch 5, which starts using the config, has been\n   expanded to hopefully become more clear. I am especially interested\n   in feedback in this doc change, as it's the main place users should\n   be able to learn how to use the new feature.\n\nEverything else should be pretty minor.\n\nRight now I'm trying to focus on this series first and foremost, hence\nsending two rerolls based on the same version of Ævar's base restart.\nI'll try to perform a code review on Ævar's latest tomorrow.\n\nThanks!\n - Emily\n\n1: https://lore.kernel.org/git/cover-v4-00.36-00000000000-20210803T191505Z-avarab%40gmail.com\n2: https://github.com/nasamuffin/git/actions/runs/1145128560\n\nEmily Shaffer (6):\n  hook: run a list of hooks instead\n  hook: allow parallel hook execution\n  hook: introduce \"git hook list\"\n  hook: allow running non-native hooks\n  hook: include hooks from the config\n  hook: allow out-of-repo 'git hook' invocations\n\n Documentation/config/hook.txt |  22 +++\n Documentation/git-hook.txt    |  87 +++++++++-\n builtin/am.c                  |   4 +-\n builtin/checkout.c            |   2 +-\n builtin/clone.c               |   2 +-\n builtin/hook.c                |  65 +++++++-\n builtin/merge.c               |   2 +-\n builtin/rebase.c              |   2 +-\n builtin/receive-pack.c        |   9 +-\n builtin/worktree.c            |   2 +-\n commit.c                      |   2 +-\n git.c                         |   2 +-\n hook.c                        | 293 +++++++++++++++++++++++++++++-----\n hook.h                        |  61 +++++--\n read-cache.c                  |   2 +-\n refs.c                        |   2 +-\n reset.c                       |   3 +-\n sequencer.c                   |   4 +-\n t/t1800-hook.sh               | 161 ++++++++++++++++++-\n transport.c                   |   2 +-\n 20 files changed, 648 insertions(+), 81 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n\nRange-diff against v2:\n 1:  81fe1ed90d <  -:  ---------- Makefile: mark \"check\" target as .PHONY\n 2:  b32abc81fb <  -:  ---------- Makefile: stop hardcoding {command,config}-list.h\n 3:  fa46bd1154 <  -:  ---------- Makefile: remove an out-of-date comment\n 4:  715fd63089 <  -:  ---------- hook.[ch]: move find_hook() to this new library\n 5:  ac6c018d27 <  -:  ---------- hook.c: add a hook_exists() wrapper and use it in bugreport.c\n 6:  53619d87a2 <  -:  ---------- hook.c users: use \"hook_exists()\" insted of \"find_hook()\"\n 7:  39dbb89620 <  -:  ---------- hook-list.h: add a generated list of hooks, like config-list.h\n 8:  7d2c1cec74 <  -:  ---------- hook: add 'run' subcommand\n 9:  3fea1cdaf8 <  -:  ---------- gc: use hook library for pre-auto-gc hook\n10:  c86a9c6f95 <  -:  ---------- rebase: convert pre-rebase to use hook.h\n11:  31dff1f274 <  -:  ---------- am: convert applypatch to use hook.h\n12:  a2e27d745c <  -:  ---------- hooks: convert 'post-checkout' hook to hook library\n13:  1a71fa8b75 <  -:  ---------- merge: convert post-merge to use hook.h\n14:  95e19fa468 <  -:  ---------- git hook run: add an --ignore-missing flag\n15:  371f581e3b <  -:  ---------- send-email: use 'git hook run' for 'sendemail-validate'\n16:  99306a4bd3 <  -:  ---------- git-p4: use 'git hook' to run hooks\n17:  59d1a563ca <  -:  ---------- commit: convert {pre-commit,prepare-commit-msg} hook to hook.h\n18:  7c8de04707 <  -:  ---------- read-cache: convert post-index-change to use hook.h\n19:  a2e7176609 <  -:  ---------- receive-pack: convert push-to-checkout hook to hook.h\n20:  f03ef24df7 <  -:  ---------- run-command: remove old run_hook_{le,ve}() hook API\n21:  18c1dfe677 <  -:  ---------- run-command: allow stdin for run_processes_parallel\n22:  eff713fc19 <  -:  ---------- hook: support passing stdin to hooks\n23:  2cbec65834 <  -:  ---------- am: convert 'post-rewrite' hook to hook.h\n24:  5d34d18bee <  -:  ---------- run-command: add stdin callback for parallelization\n25:  b989d910ed <  -:  ---------- hook: provide stdin by string_list or callback\n26:  ce6929493b <  -:  ---------- hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n27:  13affda962 <  -:  ---------- transport: convert pre-push hook to hook.h\n28:  c74720029a <  -:  ---------- hook tests: test for exact \"pre-push\" hook input\n29:  1252a251bb <  -:  ---------- hook tests: use a modern style for \"pre-push\" tests\n30:  3f2488fea6 <  -:  ---------- reference-transaction: use hook.h to run hooks\n31:  2e78c47ead <  -:  ---------- run-command: allow capturing of collated output\n32:  95132acbb0 <  -:  ---------- hooks: allow callers to capture output\n33:  f2510ca910 <  -:  ---------- receive-pack: convert 'update' hook to hook.h\n34:  8686308864 <  -:  ---------- post-update: use hook.h library\n35:  b10f5142a0 <  -:  ---------- receive-pack: convert receive hooks to hook.h\n36:  5c29c932c3 <  -:  ---------- hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n37:  5177e8ba2c !  1:  6d6400329c hook: run a list of hooks instead\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n     -\t\treturn run_hooks_oneshot(hook_name, &opt);\n     -\thook_path = find_hook(hook_name);\n     -\tif (!hook_path) {\n    -+\thooks = hook_list(hook_name);\n    ++\thooks = list_hooks(hook_name);\n     +\tif (list_empty(hooks)) {\n     +\t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n     +\t\tif (ignore_missing)\n    @@ hook.c\n      \n     +static void free_hook(struct hook *ptr)\n     +{\n    -+\tif (ptr) {\n    ++\tif (ptr)\n     +\t\tfree(ptr->feed_pipe_cb_data);\n    -+\t}\n     +\tfree(ptr);\n     +}\n     +\n    @@ hook.c\n      static int known_hook(const char *name)\n      {\n      \tconst char **p;\n    -@@ hook.c: int hook_exists(const char *name)\n    - \treturn !!find_hook(name);\n    - }\n    +@@ hook.c: const char *find_hook(const char *name)\n      \n    -+struct list_head* hook_list(const char* hookname)\n    + int hook_exists(const char *name)\n    + {\n    +-\treturn !!find_hook(name);\n    ++\treturn !list_empty(list_hooks(name));\n    ++}\n    ++\n    ++struct list_head *list_hooks(const char *hookname)\n     +{\n     +\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n     +\n     +\tINIT_LIST_HEAD(hook_head);\n     +\n     +\tif (!hookname)\n    -+\t\treturn NULL;\n    ++\t\tBUG(\"null hookname was provided to hook_list()!\");\n     +\n     +\tif (have_git_dir()) {\n     +\t\tconst char *hook_path = find_hook(hookname);\n    @@ hook.c: int hook_exists(const char *name)\n     +\t}\n     +\n     +\treturn hook_head;\n    -+}\n    -+\n    + }\n    + \n      void run_hooks_opt_clear(struct run_hooks_opt *o)\n    - {\n    - \tstrvec_clear(&o->env);\n     @@ hook.c: static int pick_next_hook(struct child_process *cp,\n      \tcp->dir = hook_cb->options->dir;\n      \n    @@ hook.c: int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *optio\n     -\thook_path = find_hook(hook_name);\n     -\tif (!hook_path) {\n     -\t\tret = 0;\n    -+\thooks = hook_list(hook_name);\n    ++\thooks = list_hooks(hook_name);\n     +\n     +\t/*\n     +\t * If you need to act on a missing hook, use run_found_hooks()\n    @@ hook.h: struct hook {\n     + * Provides a linked list of 'struct hook' detailing commands which should run\n     + * in response to the 'hookname' event, in execution order.\n     + */\n    -+struct list_head* hook_list(const char *hookname);\n    ++struct list_head *list_hooks(const char *hookname);\n     +\n      struct run_hooks_opt\n      {\n38:  eda439cd57 !  2:  dfb995ce4d hook: allow parallel hook execution\n    @@ Commit message\n         hook: allow parallel hook execution\n     \n         In many cases, there's no reason not to allow hooks to execute in\n    -    parallel. run_processes_parallel() is well-suited - it's a task queue\n    -    that runs its housekeeping in series, which means users don't\n    -    need to worry about thread safety on their callback data. True\n    -    multithreaded execution with the async_* functions isn't necessary here.\n    -    Synchronous hook execution can be achieved by only allowing 1 job to run\n    -    at a time.\n    +    parallel, if more than one was provided. hook.c already calls\n    +    run_processes_parallel(), so all we need to do is allow the job count we\n    +    hand to run_processes_parallel() to be greater than 1.\n     \n    -    Teach run_hooks() to use that function for simple hooks which don't\n    -    require stdin or capture of stderr.\n    +    If users have specified no alternative, we can use the processor count\n    +    from online_cpus() to run an efficient number of tasks at once. However,\n    +    users can also override this number if they want by configuring\n    +    'hook.jobs'.\n    +\n    +    To avoid looking up 'hook.jobs' in cases where we don't end up with any\n    +    hooks to run anyways, teach the hook runner commands to notice if\n    +    .jobs==0 and do a config or online_cpus() lookup if so, when we already\n    +    know we have jobs to run.\n    +\n    +    Serial execution can still be performed by setting .jobs == 1.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n    @@ builtin/am.c: static void am_destroy(const struct am_state *state)\n      {\n      \tint ret;\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n      \n      \tassert(state->msg);\n      \tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n    @@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)\n      static int run_post_rewrite_hook(const struct am_state *state)\n      {\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \n      \tstrvec_push(&opt.args, \"rebase\");\n      \topt.path_to_stdin = am_path(state, \"rewritten\");\n    @@ builtin/checkout.c: struct branch_info {\n      \t\t\t      int changed)\n      {\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n      \n      \t/* \"new_commit\" can be NULL when checking out from the index before\n      \t   a commit exists. */\n    @@ builtin/clone.c: static int checkout(int submodule_progress)\n      \tstruct tree_desc t;\n      \tint err = 0;\n     -\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_SYNC;\n    ++\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_SERIAL;\n      \n      \tif (option_no_checkout)\n      \t\treturn 0;\n    @@ builtin/hook.c: static const char * const builtin_hook_run_usage[] = {\n      {\n      \tint i;\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n      \tint ignore_missing = 0;\n      \tconst char *hook_name;\n      \tstruct list_head *hooks;\n    @@ builtin/merge.c: static void finish(struct commit *head_commit,\n      {\n      \tstruct strbuf reflog_message = STRBUF_INIT;\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \tconst struct object_id *head = &head_commit->object.oid;\n      \n      \tif (!msg)\n    @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix\n      \tint reschedule_failed_exec = -1;\n      \tint allow_preemptive_ff = 1;\n     -\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \tstruct option builtin_rebase_options[] = {\n      \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n      \t\t\t   N_(\"revision\"),\n    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n      \t\t\t    const struct string_list *push_options)\n      {\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \tstruct receive_hook_feed_context ctx;\n      \tstruct command *iter = commands;\n      \n    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n      static int run_update_hook(struct command *cmd)\n      {\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \n      \tstrvec_pushl(&opt.args,\n      \t\t     cmd->ref_name,\n    @@ builtin/receive-pack.c: static const char *push_to_checkout(unsigned char *hash,\n      \t\t\t\t    const char *work_tree)\n      {\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n     +\n      \topt.invoked_hook = invoked_hook;\n      \n    @@ builtin/receive-pack.c: static const char *update(struct command *cmd, struct sh\n      {\n      \tstruct command *cmd;\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \n      \tfor (cmd = commands; cmd; cmd = cmd->next) {\n      \t\tif (cmd->error_string || cmd->did_not_exist)\n    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam\n      \t */\n      \tif (!ret && opts->checkout) {\n     -\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n    ++\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n      \n      \t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n      \t\tstrvec_pushl(&opt.args,\n    @@ commit.c: int run_commit_hook(int editor_is_used, const char *index_file,\n      \t\t    const char *name, ...)\n      {\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n      \tva_list args;\n      \tconst char *arg;\n      \n    @@ hook.c: int run_hooks(const char *hook_name, struct list_head *hooks,\n      \tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n      \n     -\trun_processes_parallel_tr2(jobs,\n    -+\t/* INIT_ASYNC sets jobs to 0, so go look up how many to use. */\n    ++\t/* INIT_PARALLEL sets jobs to 0, so go look up how many to use. */\n     +\tif (!options->jobs)\n     +\t\toptions->jobs = configured_hook_jobs();\n     +\n    @@ hook.c: int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *optio\n     +\t * Turn on parallelism by default. Hooks which don't want it should\n     +\t * specify their options accordingly.\n     +\t */\n    -+\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \n      \tif (!options)\n      \t\toptions = &hook_opt_scratch;\n    @@ hook.h: struct hook_cb_data {\n      \tint *invoked_hook;\n      };\n      \n    -+#define RUN_HOOKS_OPT_INIT_SYNC { \\\n    ++#define RUN_HOOKS_OPT_INIT_SERIAL { \\\n     +\t.jobs = 1, \\\n     +\t.env = STRVEC_INIT, \\\n     +\t.args = STRVEC_INIT, \\\n     +}\n     +\n    -+#define RUN_HOOKS_OPT_INIT_ASYNC { \\\n    ++#define RUN_HOOKS_OPT_INIT_PARALLEL { \\\n     +\t.jobs = 0, \\\n     +\t.env = STRVEC_INIT, \\\n     +\t.args = STRVEC_INIT, \\\n    @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struc\n      \tint ret;\n      \tint was_full = !istate->sparse_index;\n     -\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \n      \tret = convert_to_sparse(istate);\n      \n    @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n      \t\t\t\tconst char *state)\n      {\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n      \tint ret = 0, i;\n      \n    @@ reset.c: int reset_head(struct repository *r, struct object_id *oid, const char\n      \t}\n      \tif (run_hook) {\n     -\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n     +\n      \t\tstrvec_pushl(&opt.args,\n      \t\t\t     oid_to_hex(orig ? orig : null_oid()),\n    @@ sequencer.c: int update_head_with_reflog(const struct commit *old_head,\n      \t\t\t    const struct object_id *newoid)\n      {\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \tstruct strbuf tmp = STRBUF_INIT;\n      \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n      \tint code;\n    @@ sequencer.c: static int pick_commits(struct repository *r,\n      \t\t\t\tst.st_size > 0) {\n      \t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n     -\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n    -+\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \n      \t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n      \t\t\tnotes_cp.git_cmd = 1;\n    @@ transport.c: static int run_pre_push_hook(struct transport *transport,\n      {\n      \tint ret = 0;\n     -\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_ASYNC;\n    ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n      \tstruct ref *r;\n      \tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n      \n39:  cdfe3b6e16 !  3:  c8a04306e9 hook: introduce \"git hook list\"\n    @@ Commit message\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    + ## Documentation/git-hook.txt ##\n    +@@ Documentation/git-hook.txt: SYNOPSIS\n    + [verse]\n    + 'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n    + \t<hook-name> [-- <hook-args>]\n    ++'git hook' list <hook-name>\n    + \n    + DESCRIPTION\n    + -----------\n    +@@ Documentation/git-hook.txt: optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n    + arguments (if any) differ by hook name, see linkgit:githooks[5] for\n    + what those are.\n    + \n    ++list::\n    ++\tPrint a list of hooks which will be run on `<hook-name>` event. If no\n    ++\thooks are configured for that event, print nothing and return 1.\n    ++\n    + OPTIONS\n    + -------\n    + \n    +\n      ## builtin/hook.c ##\n     @@\n      \n      #define BUILTIN_HOOK_RUN_USAGE \\\n    - \tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n    + \tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n     +#define BUILTIN_HOOK_LIST_USAGE \\\n     +\tN_(\"git hook list <hook-name>\")\n      \n    @@ builtin/hook.c: static const char * const builtin_hook_run_usage[] = {\n     +\n     +\thead = hook_list(hookname);\n     +\n    -+\tif (list_empty(head)) {\n    -+\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n    -+\t\t       hookname);\n    -+\t\treturn 0;\n    -+\t}\n    ++\tif (list_empty(head))\n    ++\t\treturn 1;\n     +\n     +\tlist_for_each(pos, head) {\n     +\t\tstruct hook *item = list_entry(pos, struct hook, list);\n    @@ builtin/hook.c: int cmd_hook(int argc, const char **argv, const char *prefix)\n      \n     \n      ## hook.c ##\n    -@@ hook.c: int hook_exists(const char *name)\n    - struct list_head* hook_list(const char* hookname)\n    +@@ hook.c: struct list_head *list_hooks(const char *hookname)\n      {\n      \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n    -+\tconst char *hook_path = find_hook(hookname);\n    -+\n      \n    ++\n      \tINIT_LIST_HEAD(hook_head);\n      \n      \tif (!hookname)\n    - \t\treturn NULL;\n    +@@ hook.c: struct list_head *list_hooks(const char *hookname)\n      \n    --\tif (have_git_dir()) {\n    --\t\tconst char *hook_path = find_hook(hookname);\n    + \tif (have_git_dir()) {\n    + \t\tconst char *hook_path = find_hook(hookname);\n     -\n     -\t\t/* Add the hook from the hookdir */\n    --\t\tif (hook_path) {\n    --\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n    --\t\t\tto_add->hook_path = hook_path;\n    --\t\t\tto_add->feed_pipe_cb_data = NULL;\n    --\t\t\tlist_add_tail(&to_add->list, hook_head);\n    --\t\t}\n    -+\t/* Add the hook from the hookdir */\n    -+\tif (hook_path) {\n    -+\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n    -+\t\tto_add->hook_path = hook_path;\n    -+\t\tto_add->feed_pipe_cb_data = NULL;\n    -+\t\tlist_add_tail(&to_add->list, hook_head);\n    - \t}\n    - \n    - \treturn hook_head;\n    + \t\tif (hook_path) {\n    + \t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n    + \t\t\tto_add->hook_path = hook_path;\n40:  eb4e03e22b !  4:  af14116d0f hook: allow running non-native hooks\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n      \thookname = argv[0];\n      \n     -\thead = hook_list(hookname);\n    -+\thead = hook_list(hookname, 1);\n    ++\thead = list_hooks_gently(hookname);\n      \n    - \tif (list_empty(head)) {\n    - \t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n    + \tif (list_empty(head))\n    + \t\treturn 1;\n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \tgit_config(git_default_config, NULL);\n      \n      \thook_name = argv[0];\n    --\thooks = hook_list(hook_name);\n    -+\thooks = hook_list(hook_name, 1);\n    +-\thooks = list_hooks(hook_name);\n    ++\thooks = list_hooks_gently(hook_name);\n      \tif (list_empty(hooks)) {\n      \t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n      \t\tif (ignore_missing)\n    @@ hook.c: static int known_hook(const char *name)\n     +\tconst char *hook_path;\n      \n      \tif (!known_hook(name))\n    - \t\tdie(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n    +-\t\tdie(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n    ++\t\tBUG(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n      \t\t    name);\n      \n     +\thook_path = find_hook_gently(name);\n    @@ hook.c: static int known_hook(const char *name)\n      \tstrbuf_git_path(&path, \"hooks/%s\", name);\n      \tif (access(path.buf, X_OK) < 0) {\n     @@ hook.c: int hook_exists(const char *name)\n    - \treturn !!find_hook(name);\n    + \treturn !list_empty(list_hooks(name));\n      }\n      \n    --struct list_head* hook_list(const char* hookname)\n     +struct hook_config_cb\n     +{\n     +\tstruct strbuf *hook_key;\n     +\tstruct list_head *list;\n     +};\n     +\n    -+struct list_head* hook_list(const char* hookname, int allow_unknown)\n    + struct list_head *list_hooks(const char *hookname)\n      {\n    - \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n    --\tconst char *hook_path = find_hook(hookname);\n    -+\tconst char *hook_path;\n    +-\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n    ++\tif (!known_hook(hookname))\n    ++\t\tBUG(\"Don't recognize hook event '%s'! \"\n    ++\t\t    \"Is it documented in Documentation/githooks.txt?\",\n    ++\t\t    hookname);\n    ++\treturn list_hooks_gently(hookname);\n    ++}\n      \n    ++struct list_head *list_hooks_gently(const char *hookname)\n    ++{\n    ++\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n      \n      \tINIT_LIST_HEAD(hook_head);\n    -@@ hook.c: struct list_head* hook_list(const char* hookname)\n    - \tif (!hookname)\n    - \t\treturn NULL;\n    - \n    -+\tif (allow_unknown)\n    -+\t\thook_path = find_hook_gently(hookname);\n    -+\telse\n    -+\t\thook_path = find_hook(hookname);\n    -+\n    - \t/* Add the hook from the hookdir */\n    - \tif (hook_path) {\n    - \t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n    + \n    +@@ hook.c: struct list_head *list_hooks(const char *hookname)\n    + \t\tBUG(\"null hookname was provided to hook_list()!\");\n    + \n    + \tif (have_git_dir()) {\n    +-\t\tconst char *hook_path = find_hook(hookname);\n    ++\t\tconst char *hook_path = find_hook_gently(hookname);\n    + \t\tif (hook_path) {\n    + \t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n    + \t\t\tto_add->hook_path = hook_path;\n     @@ hook.c: int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n      \tif (options->path_to_stdin && options->feed_pipe)\n      \t\tBUG(\"choose only one method to populate stdin\");\n      \n    --\thooks = hook_list(hook_name);\n     +\t/*\n    -+\t * 'git hooks run <hookname>' uses run_found_hooks, so we don't need to\n    ++\t * 'git hooks run <hookname>' uses run_hooks, so we don't need to\n     +\t * allow unknown hooknames here.\n     +\t */\n    -+\thooks = hook_list(hook_name, 0);\n    + \thooks = list_hooks(hook_name);\n      \n      \t/*\n    - \t * If you need to act on a missing hook, use run_found_hooks()\n     \n      ## hook.h ##\n     @@\n    @@ hook.h\n       * overwritten by further calls to find_hook and run_hook_*.\n     + *\n     + * If the hook is not a native hook (e.g. present in Documentation/githooks.txt)\n    -+ * find_hook() will die(). find_hook_gently() does not consult the native hook\n    ++ * find_hook() will BUG(). find_hook_gently() does not consult the native hook\n     + * list and will check for any hook name in the hooks directory regardless of\n     + * whether it is known. find_hook() should be used by internal calls to hooks,\n     + * and find_hook_gently() should only be used when the hookname was provided by\n    @@ hook.h: struct hook {\n       * Provides a linked list of 'struct hook' detailing commands which should run\n       * in response to the 'hookname' event, in execution order.\n     + *\n    -+ * If allow_unknown is unset, hooks will be checked against the hook list\n    -+ * generated from Documentation/githooks.txt. Otherwise, any hook name will be\n    -+ * allowed. allow_unknown should only be set when the hook name is provided by\n    -+ * the user; internal calls to hook_list should make sure the hook they are\n    -+ * invoking is present in Documentation/githooks.txt.\n    ++ * list_hooks() checks the provided hookname against Documentation/githooks.txt\n    ++ * and BUG()s if it is not found.  list_hooks_gently() allows any hookname. The\n    ++ * latter should only be used when the hook name is provided by the user, and\n    ++ * the former should be used by internal callers.\n       */\n    --struct list_head* hook_list(const char *hookname);\n    -+struct list_head* hook_list(const char *hookname, int allow_unknown);\n    + struct list_head *list_hooks(const char *hookname);\n    ++struct list_head *list_hooks_gently(const char *hookname);\n      \n      struct run_hooks_opt\n      {\n41:  2c8e874158 !  5:  2bbb179962 hook: include hooks from the config\n    @@ Commit message\n     \n      ## Documentation/config/hook.txt ##\n     @@\n    -+hook.<command>.command::\n    -+\tA command to execute during the <command> hook event. This can be an\n    -+\texecutable on your device, a oneliner for your shell, or the name of a\n    -+\thookcmd. See linkgit:git-hook[1].\n    ++hook.<name>.command::\n    ++\tA command to execute whenever `hook.<name>` is invoked. `<name>` should\n    ++\tbe a unique \"friendly\" name which you can use to identify this hook\n    ++\tcommand. (You can specify when to invoke this command with\n    ++\t`hook.<name>.event`.) The value can be an executable on your device or a\n    ++\toneliner for your shell. If more than one value is specified for the\n    ++\tsame `<name>`, the last value parsed will be the only command executed.\n    ++\tSee linkgit:git-hook[1].\n    ++\n    ++hook.<name>.event::\n    ++\tThe hook events which should invoke `hook.<name>`. `<name>` should be a\n    ++\tunique \"friendly\" name which you can use to identify this hook. The\n    ++\tvalue should be the name of a hook event, like \"pre-commit\" or \"update\".\n    ++\t(See linkgit:githooks[5] for a complete list of hooks Git knows about.)\n    ++\tOn the specified event, the associated `hook.<name>.command` will be\n    ++\texecuted. More than one event can be specified if you wish for\n    ++\t`hook.<name>` to execute on multiple events. See linkgit:git-hook[1].\n     +\n      hook.jobs::\n      \tSpecifies how many hooks can be run simultaneously during parallelized\n    @@ Documentation/git-hook.txt: Git is unlikely to use for a native hook later on. F\n     +    event = pre-commit\n     +    command = ~/bin/linter --c\n     +\n    -+Conmmands are run in the order Git encounters their associated\n    ++In this example, `[hook \"linter\"]` represents one script - `~/bin/linter --c` -\n    ++which can be shared by many repos, and even by many hook events, if appropriate.\n    ++\n    ++Commands are run in the order Git encounters their associated\n     +`hook.<name>.event` configs during the configuration parse (see\n    -+linkgit:git-config[1]).\n    ++linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be\n    ++added, only one `hook.linter.command` event is valid - Git uses \"last-one-wins\"\n    ++to determine which command to run.\n    ++\n    ++So if you wanted your linter to run when you commit as well as when you push,\n    ++you would configure it like so:\n    ++\n    ++  [hook \"linter\"]\n    ++    event = pre-commit\n    ++    event = pre-push\n    ++    command = ~/bin/linter --c\n    ++\n    ++With this config, `~/bin/linter --c` would be run by Git before a commit is\n    ++generated (during `pre-commit`) as well as before a push is performed (during\n    ++`pre-push`).\n    ++\n    ++And if you wanted to run your linter as well as a secret-leak detector during\n    ++only the \"pre-commit\" hook event, you would configure it instead like so:\n    ++\n    ++  [hook \"linter\"]\n    ++    event = pre-commit\n    ++    command = ~/bin/linter --c\n    ++  [hook \"no-leaks\"]\n    ++    event = pre-commit\n    ++    command = ~/bin/leak-detector\n    ++\n    ++With this config, before a commit is generated (during `pre-commit`), Git would\n    ++first start `~/bin/linter --c` and second start `~/bin/leak-detector`. It would\n    ++evaluate the output of each when deciding whether to proceed with the commit.\n    ++\n    ++For a full list of hook events which you can set your `hook.<name>.event` to,\n    ++and how hooks are invoked during those events, see linkgit:githooks[5].\n     +\n     +In general, when instructions suggest adding a script to\n     +`.git/hooks/<hook-event>`, you can specify it in the config instead by running\n    @@ Documentation/git-hook.txt: Git is unlikely to use for a native hook later on. F\n      optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n     \n      ## builtin/hook.c ##\n    -@@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n    - \thead = hook_list(hookname, 1);\n    - \n    - \tif (list_empty(head)) {\n    --\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n    -+\t\tprintf(_(\"no hooks configured for event '%s'\\n\"),\n    - \t\t       hookname);\n    - \t\treturn 0;\n    - \t}\n     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n      \t\tstruct hook *item = list_entry(pos, struct hook, list);\n      \t\titem = list_entry(pos, struct hook, list);\n    @@ hook.c: static void free_hook(struct hook *ptr)\n      static void remove_hook(struct list_head *to_remove)\n      {\n      \tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n    -@@ hook.c: const char *find_hook_gently(const char *name)\n    - \n    - int hook_exists(const char *name)\n    - {\n    --\treturn !!find_hook(name);\n    -+\treturn !list_empty(hook_list(name, 0));\n    - }\n    +@@ hook.c: int hook_exists(const char *name)\n      \n      struct hook_config_cb\n      {\n    @@ hook.c: const char *find_hook_gently(const char *name)\n      \tstruct list_head *list;\n      };\n      \n    --struct list_head* hook_list(const char* hookname, int allow_unknown)\n     +/*\n     + * Callback for git_config which adds configured hooks to a hook list.  Hooks\n     + * can be configured by specifying both hook.<friend-name>.command = <path> and\n    @@ hook.c: const char *find_hook_gently(const char *name)\n     +\treturn 0;\n     +}\n     +\n    -+struct list_head* hook_list(const char *hookname, int allow_unknown)\n    + struct list_head *list_hooks(const char *hookname)\n    + {\n    + \tif (!known_hook(hookname))\n    +@@ hook.c: struct list_head *list_hooks(const char *hookname)\n    + struct list_head *list_hooks_gently(const char *hookname)\n      {\n      \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n    --\tconst char *hook_path;\n     +\tstruct hook_config_cb cb_data = {\n     +\t\t.hook_event = hookname,\n     +\t\t.list = hook_head,\n     +\t};\n      \n    -+\tif (!allow_unknown && !known_hook(hookname))\n    -+\t\tdie(_(\"Don't recognize hook event '%s'. \"\n    -+\t\t      \"Is it documented in 'githooks.txt'?\"),\n    -+\t\t      hookname);\n    - \n      \tINIT_LIST_HEAD(hook_head);\n      \n      \tif (!hookname)\n    - \t\treturn NULL;\n    + \t\tBUG(\"null hookname was provided to hook_list()!\");\n      \n    --\tif (allow_unknown)\n    --\t\thook_path = find_hook_gently(hookname);\n    --\telse\n    --\t\thook_path = find_hook(hookname);\n    +-\tif (have_git_dir()) {\n    +-\t\tconst char *hook_path = find_hook_gently(hookname);\n    +-\t\tif (hook_path) {\n    +-\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n    +-\t\t\tto_add->hook_path = hook_path;\n    +-\t\t\tto_add->feed_pipe_cb_data = NULL;\n    +-\t\t\tlist_add_tail(&to_add->list, hook_head);\n    +-\t\t}\n    +-\t}\n     +\t/* Add the hooks from the config, e.g. hook.myhook.event = pre-commit */\n     +\tgit_config(hook_config_lookup, &cb_data);\n    - \n    --\t/* Add the hook from the hookdir */\n    --\tif (hook_path) {\n    --\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n    --\t\tto_add->hook_path = hook_path;\n    --\t\tto_add->feed_pipe_cb_data = NULL;\n    --\t\tlist_add_tail(&to_add->list, hook_head);\n    --\t}\n    ++\n     +\t/* Add the hook from the hookdir. The placeholder makes it easier to\n     +\t * allocate work in pick_next_hook. */\n     +\tif (find_hook_gently(hookname))\n    @@ hook.c: static int notify_start_failure(struct strbuf *out,\n      \n      \treturn 1;\n      }\n    -@@ hook.c: int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n    - \t\tBUG(\"choose only one method to populate stdin\");\n    - \n    - \t/*\n    --\t * 'git hooks run <hookname>' uses run_found_hooks, so we don't need to\n    -+\t * 'git hooks run <hookname>' uses run_found_hooks, and we want to make\n    -+\t * sure internal callers are using known hooks, so we don't need to\n    - \t * allow unknown hooknames here.\n    - \t */\n    - \thooks = hook_list(hook_name, 0);\n     \n      ## hook.h ##\n     @@ hook.h: int hook_exists(const char *hookname);\n42:  3216e51b6b !  6:  30ffe98601 hook: allow out-of-repo 'git hook' invocations\n    @@ git.c: static struct cmd_struct commands[] = {\n      \t{ \"init-db\", cmd_init_db },\n     \n      ## hook.c ##\n    -@@ hook.c: struct list_head* hook_list(const char *hookname, int allow_unknown)\n    +@@ hook.c: struct list_head *list_hooks_gently(const char *hookname)\n      \n      \t/* Add the hook from the hookdir. The placeholder makes it easier to\n      \t * allocate work in pick_next_hook. */\n-- \n2.33.0.rc2.250.ged5fa647cd-goog\n\n"},{"id":"433101","messageId":"20210819033450.3382652-2-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210819033450.3382652-1-emilyshaffer@google.com","subject":"[PATCH v3 1/6] hook: run a list of hooks instead","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T03:34:45Z","receivedAt":"2021-08-19T03:35:00Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"To prepare for multihook support, teach hook.[hc] to take a list of\nhooks at run_hooks and run_found_hooks. Right now the list is always one\nentry, but in the future we will allow users to supply more than one\nexecutable for a single hook event.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/hook.c |  14 ++++---\n hook.c         | 104 +++++++++++++++++++++++++++++++++++--------------\n hook.h         |  16 +++++++-\n 3 files changed, 96 insertions(+), 38 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 27dce6a2f0..e18357ba1f 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -25,7 +25,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint ignore_missing = 0;\n \tconst char *hook_name;\n-\tconst char *hook_path;\n+\tstruct list_head *hooks;\n+\n \tstruct option run_options[] = {\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n@@ -58,15 +59,16 @@ static int run(int argc, const char **argv, const char *prefix)\n \tgit_config(git_default_config, NULL);\n \n \thook_name = argv[0];\n-\tif (ignore_missing)\n-\t\treturn run_hooks_oneshot(hook_name, &opt);\n-\thook_path = find_hook(hook_name);\n-\tif (!hook_path) {\n+\thooks = list_hooks(hook_name);\n+\tif (list_empty(hooks)) {\n+\t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n+\t\tif (ignore_missing)\n+\t\t\treturn 0;\n \t\terror(\"cannot find a hook named %s\", hook_name);\n \t\treturn 1;\n \t}\n \n-\tret = run_hooks(hook_name, hook_path, &opt);\n+\tret = run_hooks(hook_name, hooks, &opt);\n \trun_hooks_opt_clear(&opt);\n \treturn ret;\n usage:\ndiff --git a/hook.c b/hook.c\nindex ee20b2e365..333cfd633a 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -4,6 +4,27 @@\n #include \"hook-list.h\"\n #include \"config.h\"\n \n+static void free_hook(struct hook *ptr)\n+{\n+\tif (ptr)\n+\t\tfree(ptr->feed_pipe_cb_data);\n+\tfree(ptr);\n+}\n+\n+static void remove_hook(struct list_head *to_remove)\n+{\n+\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n+\tlist_del(to_remove);\n+\tfree_hook(hook_to_remove);\n+}\n+\n+void clear_hook_list(struct list_head *head)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, head)\n+\t\tremove_hook(pos);\n+}\n+\n static int known_hook(const char *name)\n {\n \tconst char **p;\n@@ -68,7 +89,31 @@ const char *find_hook(const char *name)\n \n int hook_exists(const char *name)\n {\n-\treturn !!find_hook(name);\n+\treturn !list_empty(list_hooks(name));\n+}\n+\n+struct list_head *list_hooks(const char *hookname)\n+{\n+\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\n+\tINIT_LIST_HEAD(hook_head);\n+\n+\tif (!hookname)\n+\t\tBUG(\"null hookname was provided to hook_list()!\");\n+\n+\tif (have_git_dir()) {\n+\t\tconst char *hook_path = find_hook(hookname);\n+\n+\t\t/* Add the hook from the hookdir */\n+\t\tif (hook_path) {\n+\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n+\t\t\tto_add->hook_path = hook_path;\n+\t\t\tto_add->feed_pipe_cb_data = NULL;\n+\t\t\tlist_add_tail(&to_add->list, hook_head);\n+\t\t}\n+\t}\n+\n+\treturn hook_head;\n }\n \n void run_hooks_opt_clear(struct run_hooks_opt *o)\n@@ -128,7 +173,10 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n-\tstrvec_push(&cp->args, run_me->hook_path);\n+\tif (hook_cb->options->absolute_path)\n+\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n+\telse\n+\t\tstrvec_push(&cp->args, run_me->hook_path);\n \n \t/*\n \t * add passed-in argv, without expanding - let the user get back\n@@ -139,12 +187,12 @@ static int pick_next_hook(struct child_process *cp,\n \t/* Provide context for errors if necessary */\n \t*pp_task_cb = run_me;\n \n-\t/*\n-\t * This pick_next_hook() will be called again, we're only\n-\t * running one hook, so indicate that no more work will be\n-\t * done.\n-\t */\n-\thook_cb->run_me = NULL;\n+\t/* Get the next entry ready */\n+\tif (hook_cb->run_me->list.next == hook_cb->head)\n+\t\thook_cb->run_me = NULL;\n+\telse\n+\t\thook_cb->run_me = list_entry(hook_cb->run_me->list.next,\n+\t\t\t\t\t     struct hook, list);\n \n \treturn 1;\n }\n@@ -179,13 +227,9 @@ static int notify_hook_finished(int result,\n \treturn 0;\n }\n \n-int run_hooks(const char *hook_name, const char *hook_path,\n-\t      struct run_hooks_opt *options)\n+int run_hooks(const char *hook_name, struct list_head *hooks,\n+\t\t    struct run_hooks_opt *options)\n {\n-\tstruct strbuf abs_path = STRBUF_INIT;\n-\tstruct hook my_hook = {\n-\t\t.hook_path = hook_path,\n-\t};\n \tstruct hook_cb_data cb_data = {\n \t\t.rc = 0,\n \t\t.hook_name = hook_name,\n@@ -197,11 +241,9 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n-\tif (options->absolute_path) {\n-\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n-\t\tmy_hook.hook_path = abs_path.buf;\n-\t}\n-\tcb_data.run_me = &my_hook;\n+\n+\tcb_data.head = hooks;\n+\tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n \n \trun_processes_parallel_tr2(jobs,\n \t\t\t\t   pick_next_hook,\n@@ -213,18 +255,15 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n \n-\n-\tif (options->absolute_path)\n-\t\tstrbuf_release(&abs_path);\n-\tfree(my_hook.feed_pipe_cb_data);\n+\tclear_hook_list(hooks);\n \n \treturn cb_data.rc;\n }\n \n int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n {\n-\tconst char *hook_path;\n-\tint ret;\n+\tstruct list_head *hooks;\n+\tint ret = 0;\n \tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n \n \tif (!options)\n@@ -233,14 +272,19 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n \tif (options->path_to_stdin && options->feed_pipe)\n \t\tBUG(\"choose only one method to populate stdin\");\n \n-\thook_path = find_hook(hook_name);\n-\tif (!hook_path) {\n-\t\tret = 0;\n+\thooks = list_hooks(hook_name);\n+\n+\t/*\n+\t * If you need to act on a missing hook, use run_found_hooks()\n+\t * instead\n+\t */\n+\tif (list_empty(hooks))\n \t\tgoto cleanup;\n-\t}\n \n-\tret = run_hooks(hook_name, hook_path, options);\n+\tret = run_hooks(hook_name, hooks, options);\n+\n cleanup:\n \trun_hooks_opt_clear(options);\n+\tclear_hook_list(hooks);\n \treturn ret;\n }\ndiff --git a/hook.h b/hook.h\nindex 58dfbf474c..12b56616bb 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -3,6 +3,7 @@\n #include \"strbuf.h\"\n #include \"strvec.h\"\n #include \"run-command.h\"\n+#include \"list.h\"\n \n /*\n  * Returns the path to the hook file, or NULL if the hook is missing\n@@ -17,6 +18,7 @@ const char *find_hook(const char *name);\n int hook_exists(const char *hookname);\n \n struct hook {\n+\tstruct list_head list;\n \t/* The path to the hook */\n \tconst char *hook_path;\n \n@@ -27,6 +29,12 @@ struct hook {\n \tvoid *feed_pipe_cb_data;\n };\n \n+/*\n+ * Provides a linked list of 'struct hook' detailing commands which should run\n+ * in response to the 'hookname' event, in execution order.\n+ */\n+struct list_head *list_hooks(const char *hookname);\n+\n struct run_hooks_opt\n {\n \t/* Environment vars to be set for each hook */\n@@ -97,6 +105,7 @@ struct hook_cb_data {\n \t/* rc reflects the cumulative failure state */\n \tint rc;\n \tconst char *hook_name;\n+\tstruct list_head *head;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n \tint *invoked_hook;\n@@ -110,8 +119,8 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);\n  *\n  * See run_hooks_oneshot() for the simpler one-shot API.\n  */\n-int run_hooks(const char *hookname, const char *hook_path,\n-\t      struct run_hooks_opt *options);\n+int run_hooks(const char *hookname, struct list_head *hooks,\n+\t\t    struct run_hooks_opt *options);\n \n /**\n  * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n@@ -123,4 +132,7 @@ int run_hooks(const char *hookname, const char *hook_path,\n  */\n int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n \n+/* Empties the list at 'head', calling 'free_hook()' on each entry */\n+void clear_hook_list(struct list_head *head);\n+\n #endif\n-- \n2.33.0.rc2.250.ged5fa647cd-goog\n\n"},{"id":"433102","messageId":"20210819033450.3382652-3-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210819033450.3382652-1-emilyshaffer@google.com","subject":"[PATCH v3 2/6] hook: allow parallel hook execution","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T03:34:46Z","receivedAt":"2021-08-19T03:35:03Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"In many cases, there's no reason not to allow hooks to execute in\nparallel, if more than one was provided. hook.c already calls\nrun_processes_parallel(), so all we need to do is allow the job count we\nhand to run_processes_parallel() to be greater than 1.\n\nIf users have specified no alternative, we can use the processor count\nfrom online_cpus() to run an efficient number of tasks at once. However,\nusers can also override this number if they want by configuring\n'hook.jobs'.\n\nTo avoid looking up 'hook.jobs' in cases where we don't end up with any\nhooks to run anyways, teach the hook runner commands to notice if\n.jobs==0 and do a config or online_cpus() lookup if so, when we already\nknow we have jobs to run.\n\nSerial execution can still be performed by setting .jobs == 1.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nHelped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/config/hook.txt |  4 ++++\n Documentation/git-hook.txt    | 17 ++++++++++++++++-\n builtin/am.c                  |  4 ++--\n builtin/checkout.c            |  2 +-\n builtin/clone.c               |  2 +-\n builtin/hook.c                |  4 +++-\n builtin/merge.c               |  2 +-\n builtin/rebase.c              |  2 +-\n builtin/receive-pack.c        |  9 +++++----\n builtin/worktree.c            |  2 +-\n commit.c                      |  2 +-\n hook.c                        | 36 +++++++++++++++++++++++++++++++----\n hook.h                        | 24 ++++++++++++++++++-----\n read-cache.c                  |  2 +-\n refs.c                        |  2 +-\n reset.c                       |  3 ++-\n sequencer.c                   |  4 ++--\n transport.c                   |  2 +-\n 18 files changed, 94 insertions(+), 29 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nnew file mode 100644\nindex 0000000000..96d3d6572c\n--- /dev/null\n+++ b/Documentation/config/hook.txt\n@@ -0,0 +1,4 @@\n+hook.jobs::\n+\tSpecifies how many hooks can be run simultaneously during parallelized\n+\thook execution. If unspecified, defaults to the number of processors on\n+\tthe current system.\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex fa68c1f391..8bf82b5dd4 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,8 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n+\t<hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -42,6 +43,20 @@ OPTIONS\n \ttools that want to do a blind one-shot run of a hook that may\n \tor may not be present.\n \n+-j::\n+--jobs::\n+\tOnly valid for `run`.\n++\n+Specify how many hooks to run simultaneously. If this flag is not specified, use\n+the value of the `hook.jobs` config. If the config is not specified, use the\n+number of CPUs on the current system. Some hooks may be ineligible for\n+parallelization: for example, 'commit-msg' intends hooks modify the commit\n+message body and cannot be parallelized.\n+\n+CONFIGURATION\n+-------------\n+include::config/hook.txt[]\n+\n SEE ALSO\n --------\n linkgit:githooks[5]\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 9e3d4d9ab4..c7ffc7eafc 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -446,7 +446,7 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \n \tassert(state->msg);\n \tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n@@ -467,7 +467,7 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tstrvec_push(&opt.args, \"rebase\");\n \topt.path_to_stdin = am_path(state, \"rewritten\");\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 6d69b4c011..2cb5c67f64 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -107,7 +107,7 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 27fc05ee51..986c3b1932 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -776,7 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \n \tif (option_no_checkout)\n \t\treturn 0;\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex e18357ba1f..4dd3617876 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -22,7 +22,7 @@ static const char * const builtin_hook_run_usage[] = {\n static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \tint ignore_missing = 0;\n \tconst char *hook_name;\n \tstruct list_head *hooks;\n@@ -32,6 +32,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n \t\t\t   N_(\"file to read into hooks' stdin\")),\n+\t\tOPT_INTEGER('j', \"jobs\", &opt.jobs,\n+\t\t\t    N_(\"run up to <n> hooks simultaneously\")),\n \t\tOPT_END(),\n \t};\n \tint ret;\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 9bd4a2532c..81d7ebbbf6 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,7 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex e7c668c99b..ad118c983c 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1314,7 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ebec6f3bb1..7460124b74 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -909,7 +909,7 @@ static int run_receive_hook(struct command *commands,\n \t\t\t    int skip_broken,\n \t\t\t    const struct string_list *push_options)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct receive_hook_feed_context ctx;\n \tstruct command *iter = commands;\n \n@@ -948,7 +948,7 @@ static int run_receive_hook(struct command *commands,\n \n static int run_update_hook(struct command *cmd)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tstrvec_pushl(&opt.args,\n \t\t     cmd->ref_name,\n@@ -1432,7 +1432,8 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n+\n \topt.invoked_hook = invoked_hook;\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n@@ -1628,7 +1629,7 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 330867c19b..3090506790 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -382,7 +382,7 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \n \t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n \t\tstrvec_pushl(&opt.args,\ndiff --git a/commit.c b/commit.c\nindex 842e47beae..a38bd04752 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1700,7 +1700,7 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook,\n \t\t    const char *name, ...)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \tva_list args;\n \tconst char *arg;\n \ndiff --git a/hook.c b/hook.c\nindex 333cfd633a..b8420353fa 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -227,6 +227,28 @@ static int notify_hook_finished(int result,\n \treturn 0;\n }\n \n+/*\n+ * Determines how many jobs to use after we know we want to parallelize. First\n+ * priority is the config 'hook.jobs' and second priority is the number of CPUs.\n+ */\n+static int configured_hook_jobs(void)\n+{\n+\t/*\n+\t * The config and the CPU count probably won't change during the process\n+\t * lifetime, so cache the result in case we invoke multiple hooks during\n+\t * one process.\n+\t */\n+\tstatic int jobs = 0;\n+\tif (jobs)\n+\t\treturn jobs;\n+\n+\tif (git_config_get_int(\"hook.jobs\", &jobs))\n+\t\t/* if the config isn't set, fall back to CPU count. */\n+\t\tjobs = online_cpus();\n+\n+\treturn jobs;\n+}\n+\n int run_hooks(const char *hook_name, struct list_head *hooks,\n \t\t    struct run_hooks_opt *options)\n {\n@@ -236,16 +258,18 @@ int run_hooks(const char *hook_name, struct list_head *hooks,\n \t\t.options = options,\n \t\t.invoked_hook = options->invoked_hook,\n \t};\n-\tint jobs = 1;\n \n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n-\n \tcb_data.head = hooks;\n \tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n \n-\trun_processes_parallel_tr2(jobs,\n+\t/* INIT_PARALLEL sets jobs to 0, so go look up how many to use. */\n+\tif (!options->jobs)\n+\t\toptions->jobs = configured_hook_jobs();\n+\n+\trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n@@ -264,7 +288,11 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n {\n \tstruct list_head *hooks;\n \tint ret = 0;\n-\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n+\t/*\n+\t * Turn on parallelism by default. Hooks which don't want it should\n+\t * specify their options accordingly.\n+\t */\n+\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tif (!options)\n \t\toptions = &hook_opt_scratch;\ndiff --git a/hook.h b/hook.h\nindex 12b56616bb..cd3123d290 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -43,6 +43,13 @@ struct run_hooks_opt\n \t/* Args to be passed to each hook */\n \tstruct strvec args;\n \n+\t/*\n+\t * Number of threads to parallelize across. Set to 0 to use the\n+\t * 'hook.jobs' config or, if that config is unset, the number of cores\n+\t * on the system.\n+\t */\n+\tint jobs;\n+\n \t/* Resolve and run the \"absolute_path(hook)\" instead of\n \t * \"hook\". Used for \"git worktree\" hooks\n \t */\n@@ -85,11 +92,6 @@ struct run_hooks_opt\n \tint *invoked_hook;\n };\n \n-#define RUN_HOOKS_OPT_INIT { \\\n-\t.env = STRVEC_INIT, \\\n-\t.args = STRVEC_INIT, \\\n-}\n-\n /*\n  * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n  * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n@@ -111,6 +113,18 @@ struct hook_cb_data {\n \tint *invoked_hook;\n };\n \n+#define RUN_HOOKS_OPT_INIT_SERIAL { \\\n+\t.jobs = 1, \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n+#define RUN_HOOKS_OPT_INIT_PARALLEL { \\\n+\t.jobs = 0, \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /**\ndiff --git a/read-cache.c b/read-cache.c\nindex 90099ca14d..f157e62531 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3068,7 +3068,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tret = convert_to_sparse(istate);\n \ndiff --git a/refs.c b/refs.c\nindex 73d4a93926..5543b8cdab 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2062,7 +2062,7 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \tint ret = 0, i;\n \ndiff --git a/reset.c b/reset.c\nindex 6499bc5127..1941adb771 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -128,7 +128,8 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\t\t\t    reflog_head);\n \t}\n \tif (run_hook) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n+\n \t\tstrvec_pushl(&opt.args,\n \t\t\t     oid_to_hex(orig ? orig : null_oid()),\n \t\t\t     oid_to_hex(oid),\ndiff --git a/sequencer.c b/sequencer.c\nindex f451e23d0c..30fbe79b8a 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1148,7 +1148,7 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct strbuf tmp = STRBUF_INIT;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n@@ -4522,7 +4522,7 @@ static int pick_commits(struct repository *r,\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n \t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n-\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n \t\t\tnotes_cp.git_cmd = 1;\ndiff --git a/transport.c b/transport.c\nindex 4ca8fc0391..33da71a108 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1204,7 +1204,7 @@ static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n \tint ret = 0;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct ref *r;\n \tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \n-- \n2.33.0.rc2.250.ged5fa647cd-goog\n\n"},{"id":"433103","messageId":"20210819033450.3382652-4-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210819033450.3382652-1-emilyshaffer@google.com","subject":"[PATCH v3 3/6] hook: introduce \"git hook list\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T03:34:47Z","receivedAt":"2021-08-19T03:35:06Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"If more than one hook will be run, it may be useful to see a list of\nwhich hooks should be run. At very least, it will be useful for us to\ntest the semantics of multihooks ourselves.\n\nFor now, only list the hooks which will run in the order they will run\nin; later, it might be useful to include more information like where the\nhooks were configured and whether or not they will run.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt |  5 +++++\n builtin/hook.c             | 46 ++++++++++++++++++++++++++++++++++++++\n hook.c                     |  3 +--\n 3 files changed, 52 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 8bf82b5dd4..701ada9fc0 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -10,6 +10,7 @@ SYNOPSIS\n [verse]\n 'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n \t<hook-name> [-- <hook-args>]\n+'git hook' list <hook-name>\n \n DESCRIPTION\n -----------\n@@ -30,6 +31,10 @@ optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n arguments (if any) differ by hook name, see linkgit:githooks[5] for\n what those are.\n \n+list::\n+\tPrint a list of hooks which will be run on `<hook-name>` event. If no\n+\thooks are configured for that event, print nothing and return 1.\n+\n OPTIONS\n -------\n \ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 4dd3617876..d21f303eca 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -8,8 +8,11 @@\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n \tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n+#define BUILTIN_HOOK_LIST_USAGE \\\n+\tN_(\"git hook list <hook-name>\")\n \n static const char * const builtin_hook_usage[] = {\n+\tBUILTIN_HOOK_LIST_USAGE,\n \tBUILTIN_HOOK_RUN_USAGE,\n \tNULL\n };\n@@ -19,6 +22,47 @@ static const char * const builtin_hook_run_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_hook_list_usage[] = {\n+\tBUILTIN_HOOK_LIST_USAGE,\n+\tNULL\n+};\n+\n+static int list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct list_head *head, *pos;\n+\tconst char *hookname = NULL;\n+\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n+\n+\tstruct option list_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, list_options,\n+\t\t\t     builtin_hook_list_usage, 0);\n+\n+\tif (argc < 1)\n+\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\t\t      builtin_hook_list_usage, list_options);\n+\n+\thookname = argv[0];\n+\n+\thead = hook_list(hookname);\n+\n+\tif (list_empty(head))\n+\t\treturn 1;\n+\n+\tlist_for_each(pos, head) {\n+\t\tstruct hook *item = list_entry(pos, struct hook, list);\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item)\n+\t\t\tprintf(\"%s\\n\", item->hook_path);\n+\t}\n+\n+\tclear_hook_list(head);\n+\tstrbuf_release(&hookdir_annotation);\n+\n+\treturn 0;\n+}\n static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n@@ -88,6 +132,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \tif (!argc)\n \t\tgoto usage;\n \n+\tif (!strcmp(argv[0], \"list\"))\n+\t\treturn list(argc, argv, prefix);\n \tif (!strcmp(argv[0], \"run\"))\n \t\treturn run(argc, argv, prefix);\n \ndiff --git a/hook.c b/hook.c\nindex b8420353fa..b1ea372e15 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -96,6 +96,7 @@ struct list_head *list_hooks(const char *hookname)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n \n+\n \tINIT_LIST_HEAD(hook_head);\n \n \tif (!hookname)\n@@ -103,8 +104,6 @@ struct list_head *list_hooks(const char *hookname)\n \n \tif (have_git_dir()) {\n \t\tconst char *hook_path = find_hook(hookname);\n-\n-\t\t/* Add the hook from the hookdir */\n \t\tif (hook_path) {\n \t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n \t\t\tto_add->hook_path = hook_path;\n-- \n2.33.0.rc2.250.ged5fa647cd-goog\n\n"},{"id":"433104","messageId":"20210819033450.3382652-5-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210819033450.3382652-1-emilyshaffer@google.com","subject":"[PATCH v3 4/6] hook: allow running non-native hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T03:34:48Z","receivedAt":"2021-08-19T03:35:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"As the hook architecture and 'git hook run' become more featureful, we\nmay find wrappers wanting to use the hook architecture to run their own\nhooks, thereby getting nice things like parallelism and idiomatic Git\nconfiguration for free. Enable this by letting 'git hook run' bypass the\nknown_hooks() check.\n\nWe do still want to keep known_hooks() around, though - by die()ing when\nan internal Git call asks for run_hooks(\"my-new-hook\"), we can remind\nGit developers to update Documentation/githooks.txt with their new hook,\nwhich in turn helps Git users discover this new hook.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt |  8 ++++++++\n builtin/hook.c             |  4 ++--\n hook.c                     | 35 +++++++++++++++++++++++++++++++----\n hook.h                     | 14 ++++++++++++++\n 4 files changed, 55 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 701ada9fc0..d1db084e4f 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -19,6 +19,14 @@ This command is an interface to git hooks (see linkgit:githooks[5]).\n Currently it only provides a convenience wrapper for running hooks for\n use by git itself. In the future it might gain other functionality.\n \n+It's possible to use this command to refer to hooks which are not native to Git,\n+for example if a wrapper around Git wishes to expose hooks into its own\n+operation in a way which is already familiar to Git users. However, wrappers\n+invoking such hooks should be careful to name their hook events something which\n+Git is unlikely to use for a native hook later on. For example, Git is much less\n+likely to create a `mytool-validate-commit` hook than it is to create a\n+`validate-commit` hook.\n+\n SUBCOMMANDS\n -----------\n \ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex d21f303eca..80397d39f5 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -46,7 +46,7 @@ static int list(int argc, const char **argv, const char *prefix)\n \n \thookname = argv[0];\n \n-\thead = hook_list(hookname);\n+\thead = list_hooks_gently(hookname);\n \n \tif (list_empty(head))\n \t\treturn 1;\n@@ -105,7 +105,7 @@ static int run(int argc, const char **argv, const char *prefix)\n \tgit_config(git_default_config, NULL);\n \n \thook_name = argv[0];\n-\thooks = list_hooks(hook_name);\n+\thooks = list_hooks_gently(hook_name);\n \tif (list_empty(hooks)) {\n \t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n \t\tif (ignore_missing)\ndiff --git a/hook.c b/hook.c\nindex b1ea372e15..ab1e86ddcf 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -51,12 +51,21 @@ static int known_hook(const char *name)\n \n const char *find_hook(const char *name)\n {\n-\tstatic struct strbuf path = STRBUF_INIT;\n+\tconst char *hook_path;\n \n \tif (!known_hook(name))\n-\t\tdie(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n+\t\tBUG(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n \t\t    name);\n \n+\thook_path = find_hook_gently(name);\n+\n+\treturn hook_path;\n+}\n+\n+const char *find_hook_gently(const char *name)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\n \tstrbuf_reset(&path);\n \tstrbuf_git_path(&path, \"hooks/%s\", name);\n \tif (access(path.buf, X_OK) < 0) {\n@@ -92,10 +101,24 @@ int hook_exists(const char *name)\n \treturn !list_empty(list_hooks(name));\n }\n \n+struct hook_config_cb\n+{\n+\tstruct strbuf *hook_key;\n+\tstruct list_head *list;\n+};\n+\n struct list_head *list_hooks(const char *hookname)\n {\n-\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\tif (!known_hook(hookname))\n+\t\tBUG(\"Don't recognize hook event '%s'! \"\n+\t\t    \"Is it documented in Documentation/githooks.txt?\",\n+\t\t    hookname);\n+\treturn list_hooks_gently(hookname);\n+}\n \n+struct list_head *list_hooks_gently(const char *hookname)\n+{\n+\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n \n \tINIT_LIST_HEAD(hook_head);\n \n@@ -103,7 +126,7 @@ struct list_head *list_hooks(const char *hookname)\n \t\tBUG(\"null hookname was provided to hook_list()!\");\n \n \tif (have_git_dir()) {\n-\t\tconst char *hook_path = find_hook(hookname);\n+\t\tconst char *hook_path = find_hook_gently(hookname);\n \t\tif (hook_path) {\n \t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n \t\t\tto_add->hook_path = hook_path;\n@@ -299,6 +322,10 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n \tif (options->path_to_stdin && options->feed_pipe)\n \t\tBUG(\"choose only one method to populate stdin\");\n \n+\t/*\n+\t * 'git hooks run <hookname>' uses run_hooks, so we don't need to\n+\t * allow unknown hooknames here.\n+\t */\n \thooks = list_hooks(hook_name);\n \n \t/*\ndiff --git a/hook.h b/hook.h\nindex cd3123d290..6b7b2d14d2 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -9,8 +9,16 @@\n  * Returns the path to the hook file, or NULL if the hook is missing\n  * or disabled. Note that this points to static storage that will be\n  * overwritten by further calls to find_hook and run_hook_*.\n+ *\n+ * If the hook is not a native hook (e.g. present in Documentation/githooks.txt)\n+ * find_hook() will BUG(). find_hook_gently() does not consult the native hook\n+ * list and will check for any hook name in the hooks directory regardless of\n+ * whether it is known. find_hook() should be used by internal calls to hooks,\n+ * and find_hook_gently() should only be used when the hookname was provided by\n+ * the user, such as by 'git hook run my-wrapper-hook'.\n  */\n const char *find_hook(const char *name);\n+const char *find_hook_gently(const char *name);\n \n /*\n  * A boolean version of find_hook()\n@@ -32,8 +40,14 @@ struct hook {\n /*\n  * Provides a linked list of 'struct hook' detailing commands which should run\n  * in response to the 'hookname' event, in execution order.\n+ *\n+ * list_hooks() checks the provided hookname against Documentation/githooks.txt\n+ * and BUG()s if it is not found.  list_hooks_gently() allows any hookname. The\n+ * latter should only be used when the hook name is provided by the user, and\n+ * the former should be used by internal callers.\n  */\n struct list_head *list_hooks(const char *hookname);\n+struct list_head *list_hooks_gently(const char *hookname);\n \n struct run_hooks_opt\n {\n-- \n2.33.0.rc2.250.ged5fa647cd-goog\n\n"},{"id":"433106","messageId":"20210819033450.3382652-6-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210819033450.3382652-1-emilyshaffer@google.com","subject":"[PATCH v3 5/6] hook: include hooks from the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T03:34:49Z","receivedAt":"2021-08-19T03:35:12Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach the hook.[hc] library to parse configs to populare the list of\nhooks to run for a given event.\n\nMultiple commands can be specified for a given hook by providing\nmultiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n\"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be run in\nconfig order of the \"hook.<name>.event\" lines.\n\nFor example:\n\n  $ git config --list | grep ^hook\n  hook.bar.command=~/bar.sh\n  hook.bar.event=pre-commit\n\n  $ git hook run\n  # Runs ~/bar.sh\n  # Runs .git/hooks/pre-commit\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/config/hook.txt |  18 ++++\n Documentation/git-hook.txt    |  57 ++++++++++++-\n builtin/hook.c                |   3 +-\n hook.c                        | 153 ++++++++++++++++++++++++++++++----\n hook.h                        |   7 +-\n t/t1800-hook.sh               | 141 ++++++++++++++++++++++++++++++-\n 6 files changed, 357 insertions(+), 22 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 96d3d6572c..c394756328 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -1,3 +1,21 @@\n+hook.<name>.command::\n+\tA command to execute whenever `hook.<name>` is invoked. `<name>` should\n+\tbe a unique \"friendly\" name which you can use to identify this hook\n+\tcommand. (You can specify when to invoke this command with\n+\t`hook.<name>.event`.) The value can be an executable on your device or a\n+\toneliner for your shell. If more than one value is specified for the\n+\tsame `<name>`, the last value parsed will be the only command executed.\n+\tSee linkgit:git-hook[1].\n+\n+hook.<name>.event::\n+\tThe hook events which should invoke `hook.<name>`. `<name>` should be a\n+\tunique \"friendly\" name which you can use to identify this hook. The\n+\tvalue should be the name of a hook event, like \"pre-commit\" or \"update\".\n+\t(See linkgit:githooks[5] for a complete list of hooks Git knows about.)\n+\tOn the specified event, the associated `hook.<name>.command` will be\n+\texecuted. More than one event can be specified if you wish for\n+\t`hook.<name>` to execute on multiple events. See linkgit:git-hook[1].\n+\n hook.jobs::\n \tSpecifies how many hooks can be run simultaneously during parallelized\n \thook execution. If unspecified, defaults to the number of processors on\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex d1db084e4f..9c6cbdc2eb 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -27,12 +27,65 @@ Git is unlikely to use for a native hook later on. For example, Git is much less\n likely to create a `mytool-validate-commit` hook than it is to create a\n `validate-commit` hook.\n \n+This command parses the default configuration files for pairs of configs like\n+so:\n+\n+  [hook \"linter\"]\n+    event = pre-commit\n+    command = ~/bin/linter --c\n+\n+In this example, `[hook \"linter\"]` represents one script - `~/bin/linter --c` -\n+which can be shared by many repos, and even by many hook events, if appropriate.\n+\n+Commands are run in the order Git encounters their associated\n+`hook.<name>.event` configs during the configuration parse (see\n+linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be\n+added, only one `hook.linter.command` event is valid - Git uses \"last-one-wins\"\n+to determine which command to run.\n+\n+So if you wanted your linter to run when you commit as well as when you push,\n+you would configure it like so:\n+\n+  [hook \"linter\"]\n+    event = pre-commit\n+    event = pre-push\n+    command = ~/bin/linter --c\n+\n+With this config, `~/bin/linter --c` would be run by Git before a commit is\n+generated (during `pre-commit`) as well as before a push is performed (during\n+`pre-push`).\n+\n+And if you wanted to run your linter as well as a secret-leak detector during\n+only the \"pre-commit\" hook event, you would configure it instead like so:\n+\n+  [hook \"linter\"]\n+    event = pre-commit\n+    command = ~/bin/linter --c\n+  [hook \"no-leaks\"]\n+    event = pre-commit\n+    command = ~/bin/leak-detector\n+\n+With this config, before a commit is generated (during `pre-commit`), Git would\n+first start `~/bin/linter --c` and second start `~/bin/leak-detector`. It would\n+evaluate the output of each when deciding whether to proceed with the commit.\n+\n+For a full list of hook events which you can set your `hook.<name>.event` to,\n+and how hooks are invoked during those events, see linkgit:githooks[5].\n+\n+In general, when instructions suggest adding a script to\n+`.git/hooks/<hook-event>`, you can specify it in the config instead by running\n+`git config --add hook.<some-name>.command <path-to-script> && git config --add\n+hook.<some-name>.event <hook-event>` - this way you can share the script between\n+multiple repos. That is, `cp ~/my-script.sh ~/project/.git/hooks/pre-commit`\n+would become `git config --add hook.my-script.command ~/my-script.sh && git\n+config --add hook.my-script.event pre-commit`.\n+\n SUBCOMMANDS\n -----------\n \n run::\n-\tRun the `<hook-name>` hook. See linkgit:githooks[5] for\n-\tthe hook names we support.\n+\tRuns hooks configured for `<hook-name>`, in the order they are\n+\tdiscovered during the config parse.\n +\n Any positional arguments to the hook should be passed after an\n optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 80397d39f5..50233366a8 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -55,7 +55,8 @@ static int list(int argc, const char **argv, const char *prefix)\n \t\tstruct hook *item = list_entry(pos, struct hook, list);\n \t\titem = list_entry(pos, struct hook, list);\n \t\tif (item)\n-\t\t\tprintf(\"%s\\n\", item->hook_path);\n+\t\t\tprintf(\"%s\\n\", item->name ? item->name\n+\t\t\t\t\t\t  : _(\"hook from hookdir\"));\n \t}\n \n \tclear_hook_list(head);\ndiff --git a/hook.c b/hook.c\nindex ab1e86ddcf..581d87cbbd 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -11,6 +11,50 @@ static void free_hook(struct hook *ptr)\n \tfree(ptr);\n }\n \n+/*\n+ * Walks the linked list at 'head' to check if any hook named 'name'\n+ * already exists. Returns a pointer to that hook if so, otherwise returns NULL.\n+ */\n+static struct hook *find_hook_by_name(struct list_head *head,\n+\t\t\t\t\t const char *name)\n+{\n+\tstruct list_head *pos = NULL, *tmp = NULL;\n+\tstruct hook *found = NULL;\n+\n+\tlist_for_each_safe(pos, tmp, head) {\n+\t\tstruct hook *it = list_entry(pos, struct hook, list);\n+\t\tif (!strcmp(it->name, name)) {\n+\t\t\tlist_del(pos);\n+\t\t\tfound = it;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\treturn found;\n+}\n+\n+/*\n+ * Adds a hook if it's not already in the list, or moves it to the tail of the\n+ * list if it was already there. name == NULL indicates it's from the hookdir;\n+ * just append it in this case.\n+ */\n+static void append_or_move_hook(struct list_head *head, const char *name)\n+{\n+\tstruct hook *to_add = NULL;\n+\n+\t/* if it's not from hookdir, check if the hook is already in the list */\n+\tif (name)\n+\t\tto_add = find_hook_by_name(head, name);\n+\n+\tif (!to_add) {\n+\t\t/* adding a new hook, not moving an old one */\n+\t\tto_add = xmalloc(sizeof(*to_add));\n+\t\tto_add->name = name;\n+\t\tto_add->feed_pipe_cb_data = NULL;\n+\t}\n+\n+\tlist_add_tail(&to_add->list, head);\n+}\n+\n static void remove_hook(struct list_head *to_remove)\n {\n \tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n@@ -103,10 +147,50 @@ int hook_exists(const char *name)\n \n struct hook_config_cb\n {\n-\tstruct strbuf *hook_key;\n+\tconst char *hook_event;\n \tstruct list_head *list;\n };\n \n+/*\n+ * Callback for git_config which adds configured hooks to a hook list.  Hooks\n+ * can be configured by specifying both hook.<friend-name>.command = <path> and\n+ * hook.<friendly-name>.event = <hook-event>.\n+ */\n+static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n+{\n+\tstruct hook_config_cb *data = cb_data;\n+\tconst char *subsection, *parsed_key;\n+\tsize_t subsection_len = 0;\n+\tstruct strbuf subsection_cpy = STRBUF_INIT;\n+\n+\t/*\n+\t * Don't bother doing the expensive parse if there's no\n+\t * chance that the config matches 'hook.myhook.event = hook_event'.\n+\t */\n+\tif (!value || strcmp(value, data->hook_event))\n+\t\treturn 0;\n+\n+\t/* Looking for \"hook.friendlyname.event = hook_event\" */\n+\tif (parse_config_key(key,\n+\t\t\t    \"hook\",\n+\t\t\t    &subsection,\n+\t\t\t    &subsection_len,\n+\t\t\t    &parsed_key) ||\n+\t    strcmp(parsed_key, \"event\"))\n+\t\treturn 0;\n+\n+\t/*\n+\t * 'subsection' is a pointer to the internals of 'key', which we don't\n+\t * own the memory for. Copy it away to the hook list.\n+\t */\n+\tstrbuf_add(&subsection_cpy, subsection, subsection_len);\n+\n+\tappend_or_move_hook(data->list, strbuf_detach(&subsection_cpy, NULL));\n+\n+\n+\treturn 0;\n+}\n+\n struct list_head *list_hooks(const char *hookname)\n {\n \tif (!known_hook(hookname))\n@@ -119,21 +203,23 @@ struct list_head *list_hooks(const char *hookname)\n struct list_head *list_hooks_gently(const char *hookname)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\tstruct hook_config_cb cb_data = {\n+\t\t.hook_event = hookname,\n+\t\t.list = hook_head,\n+\t};\n \n \tINIT_LIST_HEAD(hook_head);\n \n \tif (!hookname)\n \t\tBUG(\"null hookname was provided to hook_list()!\");\n \n-\tif (have_git_dir()) {\n-\t\tconst char *hook_path = find_hook_gently(hookname);\n-\t\tif (hook_path) {\n-\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n-\t\t\tto_add->hook_path = hook_path;\n-\t\t\tto_add->feed_pipe_cb_data = NULL;\n-\t\t\tlist_add_tail(&to_add->list, hook_head);\n-\t\t}\n-\t}\n+\t/* Add the hooks from the config, e.g. hook.myhook.event = pre-commit */\n+\tgit_config(hook_config_lookup, &cb_data);\n+\n+\t/* Add the hook from the hookdir. The placeholder makes it easier to\n+\t * allocate work in pick_next_hook. */\n+\tif (find_hook_gently(hookname))\n+\t\tappend_or_move_hook(hook_head, NULL);\n \n \treturn hook_head;\n }\n@@ -194,11 +280,43 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->trace2_hook_name = hook_cb->hook_name;\n \tcp->dir = hook_cb->options->dir;\n \n+\t/* to enable oneliners, let config-specified hooks run in shell.\n+\t * config-specified hooks have a name. */\n+\tcp->use_shell = !!run_me->name;\n+\n \t/* add command */\n-\tif (hook_cb->options->absolute_path)\n-\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n-\telse\n-\t\tstrvec_push(&cp->args, run_me->hook_path);\n+\tif (run_me->name) {\n+\t\t/* ...from config */\n+\t\tstruct strbuf cmd_key = STRBUF_INIT;\n+\t\tchar *command = NULL;\n+\n+\t\tstrbuf_addf(&cmd_key, \"hook.%s.command\", run_me->name);\n+\t\tif (git_config_get_string(cmd_key.buf, &command)) {\n+\t\t\t/* TODO test me! */\n+\t\t\tdie(_(\"'hook.%s.command' must be configured \"\n+\t\t\t      \"or 'hook.%s.event' must be removed; aborting.\\n\"),\n+\t\t\t    run_me->name, run_me->name);\n+\t\t}\n+\n+\t\tstrvec_push(&cp->args, command);\n+\t} else {\n+\t\t/* ...from hookdir. */\n+\t\tconst char *hook_path = NULL;\n+\t\t/*\n+\t\t *\n+\t\t * At this point we are already running, so don't validate\n+\t\t * whether the hook name is known or not.\n+\t\t */\n+\t\thook_path = find_hook_gently(hook_cb->hook_name);\n+\t\tif (!hook_path)\n+\t\t\tBUG(\"hookdir hook in hook list but no hookdir hook present in filesystem\");\n+\n+\t\tif (hook_cb->options->absolute_path)\n+\t\t\thook_path = absolute_path(hook_path);\n+\n+\t\tstrvec_push(&cp->args, hook_path);\n+\t}\n+\n \n \t/*\n \t * add passed-in argv, without expanding - let the user get back\n@@ -228,8 +346,11 @@ static int notify_start_failure(struct strbuf *out,\n \n \thook_cb->rc |= 1;\n \n-\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n-\t\t    attempted->hook_path);\n+\tif (attempted->name)\n+\t\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n+\t\t    attempted->name);\n+\telse\n+\t\tstrbuf_addstr(out, _(\"Couldn't start hook from hooks directory\\n\"));\n \n \treturn 1;\n }\ndiff --git a/hook.h b/hook.h\nindex 6b7b2d14d2..621bd2cde1 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -27,8 +27,11 @@ int hook_exists(const char *hookname);\n \n struct hook {\n \tstruct list_head list;\n-\t/* The path to the hook */\n-\tconst char *hook_path;\n+\t/*\n+\t * The friendly name of the hook. NULL indicates the hook is from the\n+\t * hookdir.\n+\t */\n+\tconst char *name;\n \n \t/*\n \t * Use this to keep state for your feed_pipe_fn if you are using\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 217db848b3..ef2432f53a 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -1,13 +1,29 @@\n #!/bin/bash\n \n-test_description='git-hook command'\n+test_description='git-hook command and config-managed multihooks'\n \n . ./test-lib.sh\n \n+setup_hooks () {\n+\ttest_config hook.ghi.event pre-commit --add\n+\ttest_config hook.ghi.command \"/path/ghi\" --add\n+\ttest_config_global hook.def.event pre-commit --add\n+\ttest_config_global hook.def.command \"/path/def\" --add\n+}\n+\n+setup_hookdir () {\n+\tmkdir .git/hooks\n+\twrite_script .git/hooks/pre-commit <<-EOF\n+\techo \\\"Legacy Hook\\\"\n+\tEOF\n+\ttest_when_finished rm -rf .git/hooks\n+}\n+\n test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook &&\n \ttest_expect_code 129 git hook run &&\n \ttest_expect_code 129 git hook run -h &&\n+\ttest_expect_code 129 git hook list -h &&\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \tgrep \"unknown option\" err\n '\n@@ -153,4 +169,127 @@ test_expect_success 'stdin to hooks' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'git hook list orders by config order' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tdef\n+\tghi\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list reorders on duplicate event declarations' '\n+\tsetup_hooks &&\n+\n+\t# 'def' is usually configured globally; move it to the end by\n+\t# configuring it locally.\n+\ttest_config hook.def.event \"pre-commit\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tghi\n+\tdef\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list shows hooks from the hookdir' '\n+\tsetup_hookdir &&\n+\n+\tcat >expected <<-EOF &&\n+\thook from hookdir\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions execute oneliners' '\n+\ttest_config hook.oneliner.event \"pre-commit\" &&\n+\ttest_config hook.oneliner.command \"echo \\\"Hello World\\\"\" &&\n+\n+\techo \"Hello World\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions resolve paths' '\n+\twrite_script sample-hook.sh <<-EOF &&\n+\techo \\\"Sample Hook\\\"\n+\tEOF\n+\n+\ttest_when_finished \"rm sample-hook.sh\" &&\n+\n+\ttest_config hook.sample-hook.event pre-commit &&\n+\ttest_config hook.sample-hook.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\techo \\\"Sample Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hookdir hook included in git hook run' '\n+\tsetup_hookdir &&\n+\n+\techo \\\"Legacy Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'stdin to multiple hooks' '\n+\ttest_config hook.stdin-a.event \"test-hook\" --add &&\n+\ttest_config hook.stdin-a.command \"xargs -P1 -I% echo a%\" --add &&\n+\ttest_config hook.stdin-b.event \"test-hook\" --add &&\n+\ttest_config hook.stdin-b.command \"xargs -P1 -I% echo b%\" --add &&\n+\n+\tcat >input <<-EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tcat >expected <<-EOF &&\n+\ta1\n+\ta2\n+\ta3\n+\tb1\n+\tb2\n+\tb3\n+\tEOF\n+\n+\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'multiple hooks in series' '\n+\ttest_config hook.series-1.event \"test-hook\" &&\n+\ttest_config hook.series-1.command \"echo 1\" --add &&\n+\ttest_config hook.series-2.event \"test-hook\" &&\n+\ttest_config hook.series-2.command \"echo 2\" --add &&\n+\tmkdir .git/hooks &&\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo 3\n+\tEOF\n+\n+\tcat >expected <<-EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tgit hook run -j1 test-hook 2>actual &&\n+\ttest_cmp expected actual &&\n+\n+\trm -rf .git/hooks\n+'\n test_done\n-- \n2.33.0.rc2.250.ged5fa647cd-goog\n\n"},{"id":"433105","messageId":"20210819033450.3382652-7-emilyshaffer@google.com","threadId":"55286","inReplyTo":"20210819033450.3382652-1-emilyshaffer@google.com","subject":"[PATCH v3 6/6] hook: allow out-of-repo 'git hook' invocations","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T03:34:50Z","receivedAt":"2021-08-19T03:35:15Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Since hooks can now be supplied via the config, and a config can be\npresent without a gitdir via the global and system configs, we can start\nto allow 'git hook run' to occur without a gitdir. This enables us to do\nthings like run sendemail-validate hooks when running 'git send-email'\nfrom a nongit directory.\n\nIt still doesn't make sense to look for hooks in the hookdir in nongit\nrepos, though, as there is no hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n\nNotes:\n    For hookdir hooks, do we want to run them in nongit dir when core.hooksPath\n    is set? For example, if someone set core.hooksPath in their global config and\n    then ran 'git hook run sendemail-validate' in a nongit dir?\n\n git.c           |  2 +-\n hook.c          |  2 +-\n t/t1800-hook.sh | 20 +++++++++++++++-----\n 3 files changed, 17 insertions(+), 7 deletions(-)\n\ndiff --git a/git.c b/git.c\nindex 540909c391..39988ee3b0 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,7 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n-\t{ \"hook\", cmd_hook, RUN_SETUP },\n+\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nindex 581d87cbbd..2e08156546 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -218,7 +218,7 @@ struct list_head *list_hooks_gently(const char *hookname)\n \n \t/* Add the hook from the hookdir. The placeholder makes it easier to\n \t * allocate work in pick_next_hook. */\n-\tif (find_hook_gently(hookname))\n+\tif (have_git_dir() && find_hook_gently(hookname))\n \t\tappend_or_move_hook(hook_head, NULL);\n \n \treturn hook_head;\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex ef2432f53a..a7e45c0d16 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -118,15 +118,25 @@ test_expect_success 'git hook run -- pass arguments' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'git hook run -- out-of-repo runs excluded' '\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\techo Test hook\n-\tEOF\n+test_expect_success 'git hook run: out-of-repo runs execute global hooks' '\n+\ttest_config_global hook.global-hook.event test-hook --add &&\n+\ttest_config_global hook.global-hook.command \"echo no repo no problems\" --add &&\n+\n+\techo \"global-hook\" >expect &&\n+\tnongit git hook list test-hook >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"no repo no problems\" >expect &&\n \n-\tnongit test_must_fail git hook run test-hook\n+\tnongit git hook run test-hook 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n \tmkdir my-hooks &&\n \twrite_script my-hooks/test-hook <<-\\EOF &&\n \techo Hook ran $1 >>actual\n-- \n2.33.0.rc2.250.ged5fa647cd-goog\n\n"},{"id":"433176","messageId":"xmqqo89txdo5.fsf@gitster.g","threadId":"55286","inReplyTo":"20210819033450.3382652-6-emilyshaffer@google.com","subject":"Re: [PATCH v3 5/6] hook: include hooks from the config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-19T22:39:06Z","receivedAt":"2021-08-19T22:39:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> index 96d3d6572c..c394756328 100644\n> --- a/Documentation/config/hook.txt\n> +++ b/Documentation/config/hook.txt\n> @@ -1,3 +1,21 @@\n> +hook.<name>.command::\n> +\tA command to execute whenever `hook.<name>` is invoked. `<name>` should\n> +\tbe a unique \"friendly\" name which you can use to identify this hook\n> +\tcommand. (You can specify when to invoke this command with\n> +\t`hook.<name>.event`.) The value can be an executable on your device or a\n> +\toneliner for your shell. If more than one value is specified for the\n> +\tsame `<name>`, the last value parsed will be the only command executed.\n> +\tSee linkgit:git-hook[1].\n> +\n> +hook.<name>.event::\n> +\tThe hook events which should invoke `hook.<name>`. `<name>` should be a\n> +\tunique \"friendly\" name which you can use to identify this hook. The\n> +\tvalue should be the name of a hook event, like \"pre-commit\" or \"update\".\n> +\t(See linkgit:githooks[5] for a complete list of hooks Git knows about.)\n> +\tOn the specified event, the associated `hook.<name>.command` will be\n> +\texecuted. More than one event can be specified if you wish for\n> +\t`hook.<name>` to execute on multiple events. See linkgit:git-hook[1].\n\nLooking much better.  It now gives enough information to readers to\nunderstand (if not enough to convince that it is a good idea) why an\nindirection with \"friendly name\" between the event and command is\nthere.  In short, <name> names the command to be run and without\nindirection, you'd end up having to write:\n\n    [hook \"check-whitespace && spellcheck-log-message\"]\n\tevent = pre-commit\n    [hook \"check-whitespace && spellcheck-log-message\"]\n\tevent = another-hookable-event\n\nwhich may give the same expressiveness (and may even be workable if\nthe configuration were machine generated) but it is typo-prone, and\na single typo, or even an insignificant whitespace change in the\ncommand, would destroy the grouping of \"this command fires upon\nthese events\".\n\nIt becomes much less typo prone with the indirection, i.e.\n\n    [hook \"logcheck\"]\n\tcommand = check-whitespace && spellcheck-log-message\n\n    [hook \"logcheck\"]\n\tevent = pre-commit\n\n    [hook \"logcheck\"]\n\tevent = another-hookable-event\n\nusing the \"friendly name\", especially if these entries are spread\nacross different configuration files.\n\nMy original question was primarily because I thought the\nsecond-level <name> corresponded to <event>.  If that were the case,\nit can trivially be made simpler without making it typo-prone, i.e.\n\n    [hook \"pre-commit\"]\n\tcommand = check-whitespace && spellcheck-log-message\n\n    [hook \"another-hookable-event\"]\n\tcommand = check-whitespace && spellcheck-log-message\n\nsince the name of the event would be much shorter than the command\nline.  But since we are not grouping per hookable event (to apply\nthe \"last one wins\" rule to determine which single command is the\none that gets to run).\n\nThanks.\n\n\n"},{"id":"433178","messageId":"YR7r3h1AG4Zyn7x7@google.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 00/36] Run hooks via \"git run hook\" & hook library","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T23:40:14Z","receivedAt":"2021-08-19T23:40:25Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:26PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> This is a v4 re-roll of the \"Base for \"config-based-hooks\" topic.\n>  * Almost all the callers were just \"one-shot\" callers, I introduced a\n>    new run_hooks_oneshot() function for those, which gets rid of the\n>    verbosity around memory management, see e.g. the \"builtin/gc.c\" in\n>    the range-diff below. That run_hooks_oneshot() can also take a NULL\n>    set of options.\n\nI discussed this elswhere, but just to make doubly sure that comment\ndoesn't get lost, here's a link to my mail:\nhttps://lore.kernel.org/git/YR2jLdYQA5CVzX5h%40google.com\n\n> Range-diff against v3:\n>  1:  27c94247f87 =  1:  81fe1ed90d5 Makefile: mark \"check\" target as .PHONY\n>  2:  6e164edb0b0 !  2:  0f749530777 Makefile: stop hardcoding {command,config}-list.h\n>  3:  ddae86802e2 !  3:  644b31fe281 Makefile: remove an out-of-date comment\n>  4:  58c37e4f06e =  4:  89c4d44b0c3 hook.[ch]: move find_hook() to this new library\n>  5:  0cf7e078ef4 =  5:  3514e0c0251 hook.c: add a hook_exists() wrapper and use it in bugreport.c\n>  -:  ----------- >  6:  d5ef40f77dc hook.c users: use \"hook_exists()\" insted of \"find_hook()\"\n>  6:  f343fc7ae66 !  7:  4cfd72722c1 hook-list.h: add a generated list of hooks, like config-list.h\n\nSince these 6 were broken out and then brought back into this series,\nI'll review them individually downthread.\n\n>  7:  cf4b06bfdf8 !  8:  7cb4a4cb69e hook: add 'run' subcommand\n>     @@ builtin/hook.c (new)\n>      +#include \"strbuf.h\"\n>      +#include \"strvec.h\"\n>      +\n>     ++#define BUILTIN_HOOK_RUN_USAGE \\\n>     ++\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n>     ++\n\nNice, now we avoid string duplication here...\n\n>      +static const char * const builtin_hook_usage[] = {\n>     -+\tN_(\"git hook <command> [...]\"),\n\n...and there is no point including this vague thing that has a more\nspecific description right after. Ok.\n\n>     -+\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>     ++\tBUILTIN_HOOK_RUN_USAGE,\n>      +\tNULL\n>      +};\n[...]\n>     @@ builtin/hook.c (new)\n>      +{\n>      +\tint i;\n>      +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>     -+\tint rc = 0;\n>      +\tconst char *hook_name;\n>      +\tconst char *hook_path;\n>     -+\n>      +\tstruct option run_options[] = {\n>      +\t\tOPT_END(),\n>      +\t};\n>     ++\tint ret;\n>      +\n>      +\targc = parse_options(argc, argv, prefix, run_options,\n>      +\t\t\t     builtin_hook_run_usage,\n>     -+\t\t\t     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n>     -+\n>     -+\tif (argc > 1) {\n>     -+\t\tif (strcmp(argv[1], \"--\") &&\n>     -+\t\t    strcmp(argv[1], \"--end-of-options\"))\n>     -+\t\t\t/* Having a -- for \"run\" is mandatory */\n>     -+\t\t\tusage_with_options(builtin_hook_usage, run_options);\n>     -+\t\t/* Add our arguments, start after -- */\n>     -+\t\tfor (i = 2 ; i < argc; i++)\n>     -+\t\t\tstrvec_push(&opt.args, argv[i]);\n>     -+\t}\n>     ++\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n\nNice - this code is being broken up and moved later.\n\n>      +\n>     -+\t/* Need to take into account core.hooksPath */\n>     -+\tgit_config(git_default_config, NULL);\n>     ++\tif (!argc)\n>     ++\t\tgoto usage;\n>      +\n>      +\t/*\n>     -+\t * We are not using run_hooks() because we'd like to detect\n>     -+\t * missing hooks. Let's find it ourselves and call\n>     -+\t * run_found_hooks() instead.\n\nThis comment disappears entirely. I am not too terribly upset about it,\nsince this behavior changes in my series anyway, but it seems like it\nmight have been an accident?\n\n>     ++\t * Having a -- for \"run\" when providing <hook-args> is\n>     ++\t * mandatory.\n>      +\t */\n>     ++\tif (argc > 1 && strcmp(argv[1], \"--\") &&\n>     ++\t    strcmp(argv[1], \"--end-of-options\"))\n>     ++\t\tgoto usage;\n>     ++\n>     ++\t/* Add our arguments, start after -- */\n>     ++\tfor (i = 2 ; i < argc; i++)\n>     ++\t\tstrvec_push(&opt.args, argv[i]);\n>     ++\n>     ++\t/* Need to take into account core.hooksPath */\n>     ++\tgit_config(git_default_config, NULL);\n>     ++\n>      +\thook_name = argv[0];\n>      +\thook_path = find_hook(hook_name);\n>      +\tif (!hook_path) {\n>      +\t\terror(\"cannot find a hook named %s\", hook_name);\n>      +\t\treturn 1;\n>      +\t}\n>     -+\trc = run_found_hooks(hook_name, hook_path, &opt);\n>      +\n>     ++\tret = run_hooks(hook_name, hook_path, &opt);\n>      +\trun_hooks_opt_clear(&opt);\n>     -+\n>     -+\treturn rc;\n>     ++\treturn ret;\n>     ++usage:\n>     ++\tusage_with_options(builtin_hook_run_usage, run_options);\n>      +}\n>      +\n>      +int cmd_hook(int argc, const char **argv, const char *prefix)\n>     @@ builtin/hook.c (new)\n>      +\tstruct option builtin_hook_options[] = {\n>      +\t\tOPT_END(),\n>      +\t};\n>     ++\n>      +\targc = parse_options(argc, argv, NULL, builtin_hook_options,\n>      +\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n>      +\tif (!argc)\n>     -+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n>     ++\t\tgoto usage;\n>      +\n>      +\tif (!strcmp(argv[0], \"run\"))\n>      +\t\treturn run(argc, argv, prefix);\n>     -+\telse\n>     -+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n>     ++\n>     ++usage:\n>     ++\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n>      +}\n\nThe goto pattern seems readable enough elsewhere, though.\n\n>       ## command-list.txt ##\n>     @@ hook.c: int hook_exists(const char *name)\n>      +\tstruct hook_cb_data *hook_cb = pp_cb;\n>      +\tstruct hook *run_me = hook_cb->run_me;\n>      +\n>     ++\tif (!run_me)\n>     ++\t\treturn 0;\n>     ++\nNow we protect ourselves from the repeated calls to pick_next_hook().\nGood.\n>      +\tcp->no_stdin = 1;\n>      +\tcp->env = hook_cb->options->env.v;\n>      +\tcp->stdout_to_stderr = 1;\n>     @@ hook.c: int hook_exists(const char *name)\n>      +\t/* Provide context for errors if necessary */\n>      +\t*pp_task_cb = run_me;\n>      +\n>     ++\t/*\n>     ++\t * This pick_next_hook() will be called again, we're only\n>     ++\t * running one hook, so indicate that no more work will be\n>     ++\t * done.\n>     ++\t */\n>     ++\thook_cb->run_me = NULL;\n>     ++\nAnd clearly explaining the caller's behavior here. Sure.\n>      +\treturn 1;\n>      +}\n>      +\n>     @@ hook.c: int hook_exists(const char *name)\n>      +\n>      +\thook_cb->rc |= result;\n>      +\n>     -+\treturn 1;\n>     ++\treturn 0;\nAnd finally, report \"everything is fine\" on task finished. Thanks.\n>      +}\n>      +\n>     -+int run_found_hooks(const char *hook_name, const char *hook_path,\n>     -+\t\t    struct run_hooks_opt *options)\n>     ++int run_hooks(const char *hook_name, const char *hook_path,\n>     ++\t      struct run_hooks_opt *options)\n\nOk. With the rename, we only have \"verbose way to call it\" and \"speedy\nway to call it\". No more \"run_found_hooks()\". Thanks, I think I like\nthis better - I found \"run_found_hooks()\" to be a little ambiguous.\n\n>     @@ hook.c: int hook_exists(const char *name)\n>      +\t\t\t\t   hook_name);\n>      +\n>      +\treturn cb_data.rc;\n>     -+}\n>     -+\n>     -+int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n>     -+{\n>     -+\tconst char *hook_path;\n>     -+\tint ret;\n>     -+\tif (!options)\n>     -+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n>     -+\n>     -+\thook_path = find_hook(hook_name);\n>     -+\n>     -+\t/*\n>     -+\t * If you need to act on a missing hook, use run_found_hooks()\n>     -+\t * instead\n>     -+\t */\n>     -+\tif (!hook_path)\n>     -+\t\treturn 0;\n>     -+\n>     -+\tret = run_found_hooks(hook_name, hook_path, options);\n>     -+\treturn ret;\n\nOk - so we wait for another patch to introduce run_hooks_oneshot. Fine.\n\n>      +}\n>      \n>       ## hook.h ##\n>     @@ hook.h: const char *find_hook(const char *name);\n>      +\n>      +\t/* Args to be passed to each hook */\n>      +\tstruct strvec args;\n>     -+\n>     -+\t/*\n>     -+\t * Number of threads to parallelize across, currently a stub,\n>     -+\t * we use the parallel API for future-proofing, but we always\n>     -+\t * have one hook of a given name, so this is always an\n>     -+\t * implicit 1 for now.\n>     -+\t */\n>     -+\tint jobs;\n\nThanks. I think this makes more sense than the weird \"futureproofing\"\nstate it was in before.\n\n>       ## t/t1800-hook.sh (new) ##\n>     @@ t/t1800-hook.sh (new)\n>      +\n>      +test_expect_success 'git hook usage' '\n>      +\ttest_expect_code 129 git hook &&\n>     -+\ttest_expect_code 129 git hook -h &&\n>     -+\ttest_expect_code 129 git hook run -h\n>     ++\ttest_expect_code 129 git hook run &&\n>     ++\ttest_expect_code 129 git hook run -h &&\n>     ++\ttest_expect_code 129 git hook run --unknown 2>err &&\n>     ++\tgrep \"unknown option\" err\n>      +'\n\nNice. I was able to use this test myself when I added 'git hook list',\ntoo.\n\nThe changes to this patch look good to me.\n\n>  8:  7209f73f281 !  9:  2b8500aa675 gc: use hook library for pre-auto-gc hook\n>     @@ Metadata\n>       ## Commit message ##\n>          gc: use hook library for pre-auto-gc hook\n>      \n>     -    Using the hook.h library instead of the run-command.h library to run\n>     -    pre-auto-gc means that those hooks can be set up in config files, as\n>     -    well as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n>     +    Move the pre-auto-gc hook away from run-command.h to and over to the\n>     +    new hook.h library.\n>     +\n>     +    To do this introduce a simple run_hooks_oneshot() wrapper, we'll be\n>     +    using it extensively for these simple cases of wanting to run a single\n>     +    hook under a given name, and having it free the memory we allocate for\n>     +    us.\n\nCool, so we have an excuse for introducing the oneshot wrapper right\naway. Thanks.\n\n>     -@@ builtin/gc.c: static void add_repack_incremental_option(void)\n>     - \n>     - static int need_to_gc(void)\n>     - {\n>     -+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n>     -+\n>     - \t/*\n>     - \t * Setting gc.auto to 0 or negative can disable the\n>     - \t * automatic gc.\n>      @@ builtin/gc.c: static int need_to_gc(void)\n>       \telse\n>       \t\treturn 0;\n>       \n>      -\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n>     -+\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n>     -+\t\trun_hooks_opt_clear(&hook_opt);\n>     ++\tif (run_hooks_oneshot(\"pre-auto-gc\", NULL))\n>       \t\treturn 0;\n>     -+\t}\n>     -+\trun_hooks_opt_clear(&hook_opt);\n>       \treturn 1;\n>       }\n\nNice - the callsite looks much tidier now.\n\n>     +\n>     + ## hook.c ##\n>     +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n>       \n>     + \treturn cb_data.rc;\n>     + }\n>     ++\n>     ++int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n>     ++{\n>     ++\tconst char *hook_path;\n>     ++\tint ret;\n>     ++\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n>     ++\n>     ++\tif (!options)\n>     ++\t\toptions = &hook_opt_scratch;\n\nAh, it still takes an options (optionally, ha ha) because run_hooks()\nwould require the caller to provide the hook list/hook path. Ok. Seems\nfine, and I like that the '_oneshot' naming change makes it less\nsurprising that run_hooks_opt_clear() will get called for you.\n\n>     ++\n>     ++\thook_path = find_hook(hook_name);\n>     ++\tif (!hook_path) {\n>     ++\t\tret = 0;\n>     ++\t\tgoto cleanup;\n>     ++\t}\n>     ++\n>     ++\tret = run_hooks(hook_name, hook_path, options);\n>     ++cleanup:\n>     ++\trun_hooks_opt_clear(options);\n>     ++\treturn ret;\n>     ++}\n>     +\n>     + ## hook.h ##\n>     +@@ hook.h: void run_hooks_opt_clear(struct run_hooks_opt *o);\n>     + /**\n>     +  * Takes an already resolved hook found via find_hook() and runs\n>     +  * it. Does not call run_hooks_opt_clear() for you.\n>     ++ *\n>     ++ * See run_hooks_oneshot() for the simpler one-shot API.\n>     +  */\n>     + int run_hooks(const char *hookname, const char *hook_path,\n>     + \t      struct run_hooks_opt *options);\n>     ++\n>     ++/**\n>     ++ * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n>     ++ * with run_hooks().\n>     ++ *\n>     ++ * If \"options\" is provided calls run_hooks_opt_clear() on it for\n>     ++ * you. If \"options\" is NULL a scratch one will be provided for you\n>     ++ * before calling run_hooks().\n\n\"A scratch one will be provided for you\" doesn't sound quite right -\nit's not exposed to the caller at all, but the comment sounds like\nyou're handing the caller this scratch struct. Maybe it's better to say\n\"the default options from RUN_HOOKS_OPT_INIT will be used\"?\n\nAs a bonus, if you directly reference the initter macro, then it will\nautomatically become clear to folks reading the documentation what the\nexpected parallelism is on their hook, as the parallelism (_SERIES or\n_PARALLEL) is included on the name of the macro in my change later.\n\n>     ++ */\n>     ++int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n>     ++\n>     + #endif\n\n>  9:  e9a1e7cf61e ! 10:  3ee55d2c10f rebase: teach pre-rebase to use hook.h\n>     @@ Metadata\n>      Author: Emily Shaffer <emilyshaffer@google.com>\n>      \n>       ## Commit message ##\n>     -    rebase: teach pre-rebase to use hook.h\n>     +    rebase: convert pre-rebase to use hook.h\n>      \n>          Move the pre-rebase hook away from run-command.h to and over to the\n>          new hook.h library.\n>     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix\n>       \tif (!ok_to_skip_pre_rebase &&\n>      -\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n>      -\t\t\targc ? argv[0] : NULL, NULL))\n>     -+\t    run_hooks(\"pre-rebase\", &hook_opt)) {\n>     -+\t\trun_hooks_opt_clear(&hook_opt);\n>     ++\t    run_hooks_oneshot(\"pre-rebase\", &hook_opt))\n\nOk, it just uses the oneshot call instead. Looks good.\n> 10:  1d087269303 ! 11:  050f20d14f0 am: convert applypatch hooks to use config\n> 11:  32eec5dc2f0 ! 12:  ac875d284da hooks: convert 'post-checkout' hook to hook library\n> 12:  e9fa3f67593 ! 13:  69763bc2255 merge: use config-based hooks for post-merge hook\n\nThe above all just use the oneshot instead, and have some minor tweaks\nto commit messages to lose references to the config. Looks good.\n\n> 13:  12347d901bb ! 14:  2ca1ca1b8e4 git hook run: add an --ignore-missing flag\n>     @@ Documentation/git-hook.txt: optional `--` (or `--end-of-options`, see linkgit:gi\n>       linkgit:githooks[5]\n>      \n>       ## builtin/hook.c ##\n>     +@@\n>     + #include \"strvec.h\"\n>     + \n>     + #define BUILTIN_HOOK_RUN_USAGE \\\n>     +-\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n>     ++\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n\nOk. We update the usage in the macro here instead. Nice.\n\n>      @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n>     - \t/*\n>     - \t * We are not using run_hooks() because we'd like to detect\n>     - \t * missing hooks. Let's find it ourselves and call\n>     --\t * run_found_hooks() instead.\n>     -+\t * run_found_hooks() instead...\n>     - \t */\n>     + \tgit_config(git_default_config, NULL);\n>     + \n>       \thook_name = argv[0];\n>     ++\tif (ignore_missing)\n>     ++\t\treturn run_hooks_oneshot(hook_name, &opt);\n>       \thook_path = find_hook(hook_name);\n>       \tif (!hook_path) {\n>     -+\t\t/* ... act like run_hooks() under --ignore-missing */\n>     -+\t\tif (ignore_missing)\n>     -+\t\t\treturn 0;\n>       \t\terror(\"cannot find a hook named %s\", hook_name);\n>     - \t\treturn 1;\n>     - \t}\n\nHm. I actually think this was clearer before. I *think* that is because\nyou and I have different opinions on the clarity of multiple returns :)\n\nBut I think it is a little confusing to say \"ok, we will call an\nentirely different entry point in case of this one thing which we will\nhave to check anyways\".\n\n> 14:  71d209b4077 ! 15:  5b66b04bec7 send-email: use 'git hook run' for 'sendemail-validate'\n>     @@ git-send-email.perl: sub validate_patch {\n>       \tif ($repo) {\n>      +\t\tmy $hook_name = 'sendemail-validate';\n>       \t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n>     --\t\tmy $validate_hook = catfile($hooks_path,\n>     + \t\trequire File::Spec;\n>     +-\t\tmy $validate_hook = File::Spec->catfile($hooks_path,\n>      -\t\t\t\t\t    'sendemail-validate');\n>     -+\t\tmy $validate_hook = catfile($hooks_path, $hook_name);\n>     ++\t\tmy $validate_hook = File::Spec->catfile($hooks_path, $hook_name);\n\nOk, we are adapting to some other change around File::Spec that happened\nto git-send-email.perl elsewhere. The change to this patch looks good to\nme.\n\n>       \t\tmy $hook_error;\n>       \t\tif (-x $validate_hook) {\n>     - \t\t\tmy $target = abs_path($fn);\n>     + \t\t\trequire Cwd;\n>      @@ git-send-email.perl: sub validate_patch {\n>       \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n>       \t\t\t\tor die(\"chdir: $!\");\n> 15:  246a82b55b2 = 16:  14a37a43db2 git-p4: use 'git hook' to run hooks\n> 16:  e3f8482d803 ! 17:  ad5d0e0e7de commit: use hook.h to execute hooks\n>     @@ Metadata\n>      Author: Emily Shaffer <emilyshaffer@google.com>\n>      \n>       ## Commit message ##\n>     -    commit: use hook.h to execute hooks\n>     +    commit: convert {pre-commit,prepare-commit-msg} hook to hook.h\n>      \n>     -    Teach run_commit_hook() to call hook.h instead of run-command.h. This\n>     -    covers 'pre-commit', 'commit-msg', and\n>     -    'prepare-commit-msg'.\n>     -\n>     -    Additionally, ask the hook library - not run-command - whether any\n>     -    hooks will be run, as it's possible hooks may exist in the config but\n>     -    not the hookdir.\n>     -\n>     -    Because all but 'post-commit' hooks are expected to make some state\n>     -    change, force all but 'post-commit' hook to run in series. 'post-commit'\n>     -    \"is meant primarily for notification, and cannot affect the outcome of\n>     -    `git commit`,\" so it is fine to run in parallel.\n\nI am a little bummed that the patch reordering effectively removes the\njustification for parallel/serial execution for each hook from 'git\nblame'. Oh well.\n\n>     +    Move these hooks hook away from run-command.h to and over to the new\n>     +    hook.h library.\n>      \n>          Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n>          Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n>      \n>       ## commit.c ##\n\nOtherwise the main change here is dealing with other improvements\nearlier in the series, like 'hook_exists()' and 'run_hooks_oneshot()'.\nLooks fine.\n\n> 17:  6ed61071c5e ! 18:  3d3a33e2674 read-cache: convert post-index-change hook to use config\n>     @@ Metadata\n>      Author: Emily Shaffer <emilyshaffer@google.com>\n>      \n>       ## Commit message ##\n>     -    read-cache: convert post-index-change hook to use config\n>     +    read-cache: convert post-index-change to use hook.h\n>      \n>     -    By using hook.h instead of run-command.h to run, post-index-change hooks\n>     -    can now be specified in the config in addition to the hookdir.\n>     -    post-index-change is not run anywhere besides in read-cache.c.\n>     +    Move the post-index-change hook away from run-command.h to and over to\n>     +    the new hook.h library.\n\nI do not think it is necessary to drop the mention about\n'post-index-change' being run anywhere else, but that's just a nit, not\nworth fixing :)\n\n>      \n>          This removes the last direct user of run_hook_ve(), so we can make the\n>          function static now. It'll be removed entirely soon.\n>     @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struc\n>      +\t\t     istate->updated_workdir ? \"1\" : \"0\",\n>      +\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n>      +\t\t     NULL);\n>     -+\trun_hooks(\"post-index-change\", &hook_opt);\n>     -+\trun_hooks_opt_clear(&hook_opt);\n>     ++\trun_hooks_oneshot(\"post-index-change\", &hook_opt);\n>      +\n\nMechanical change to using run_hooks_oneshot(). OK.\n\n>       \tistate->updated_workdir = 0;\n>       \tistate->updated_skipworktree = 0;\n> 18:  e4ef3f4548a ! 19:  893f8666301 receive-pack: convert push-to-checkout hook to hook.h\n>     @@ Metadata\n>       ## Commit message ##\n>          receive-pack: convert push-to-checkout hook to hook.h\n>      \n>     -    By using hook.h instead of run-command.h to invoke push-to-checkout,\n>     -    hooks can now be specified in the config as well as in the hookdir.\n>     -    push-to-checkout is not called anywhere but in builtin/receive-pack.c.\n>     +    Move the push-to-checkout hook away from run-command.h to and over to\n>     +    the new hook.h library.\n\nYou made a comment in the commit before about the final instance of\nrun_hook_ve; do you want to make a similar one here about run_hook_le?\n\nThe range-diff here is mechanical so looks fine to me.\n\n> 19:  e3dda367ec9 = 20:  070433deba5 run-command: remove old run_hook_{le,ve}() hook API\n> 20:  477d75bf579 = 21:  1028e0c1667 run-command: allow stdin for run_processes_parallel\n> 21:  b7c0ee9719a ! 22:  639e59e9ed0 hook: support passing stdin to hooks\n>     @@ Documentation/git-hook.txt: what those are.\n>      \n>       ## builtin/hook.c ##\n>      @@\n>     + #include \"strvec.h\"\n>       \n>     - static const char * const builtin_hook_usage[] = {\n>     - \tN_(\"git hook <command> [...]\"),\n>     --\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>     -+\tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n>     - \tNULL\n>     - };\n>     - \n>     - static const char * const builtin_hook_run_usage[] = {\n>     - \tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>     -+\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n>     - \tNULL\n>     - };\n>     + #define BUILTIN_HOOK_RUN_USAGE \\\n>     +-\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n>     ++\tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n>       \nJust coping with the macro instead of the duplicated strings. Ok.\n\nThe range-diff looks fine to me.\n\n> 22:  4035069a98c ! 23:  7d1925cca48 am: convert 'post-rewrite' hook to hook.h\n>     @@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)\n>       {\n>      -\tstruct child_process cp = CHILD_PROCESS_INIT;\n>      -\tconst char *hook = find_hook(\"post-rewrite\");\n>     -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>     - \tint ret;\n>     - \n>     +-\tint ret;\n>     +-\n>      -\tif (!hook)\n>      -\t\treturn 0;\n>      -\n>     @@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)\n>      -\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n>      -\tcp.stdout_to_stderr = 1;\n>      -\tcp.trace2_hook_name = \"post-rewrite\";\n>     -+\tstrvec_push(&opt.args, \"rebase\");\n>     -+\topt.path_to_stdin = am_path(state, \"rewritten\");\n>     ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>       \n>      -\tret = run_command(&cp);\n>     -+\tret = run_hooks(\"post-rewrite\", &opt);\n>     ++\tstrvec_push(&opt.args, \"rebase\");\n>     ++\topt.path_to_stdin = am_path(state, \"rewritten\");\n>       \n>      -\tclose(cp.in);\n>     -+\trun_hooks_opt_clear(&opt);\n>     - \treturn ret;\n>     +-\treturn ret;\n>     ++\treturn run_hooks_oneshot(\"post-rewrite\", &opt);\n>       }\n>       \n>     + /**\n\nMan, the range-diff did a terrible job of summarizing this :)\n\nThe patch itself looks great, though, with the switch to using\nrun_hooks_oneshot. Thanks.\n\n> 24:  da46c859c1c ! 25:  05d1085f7eb hook: provide stdin by string_list or callback\n>     @@ hook.c: static int pick_next_hook(struct child_process *cp,\n>       \t} else {\n>       \t\tcp->no_stdin = 1;\n>       \t}\n>     -@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n>     - \trun_processes_parallel_tr2(options->jobs,\n>     +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n>     + \trun_processes_parallel_tr2(jobs,\n>       \t\t\t\t   pick_next_hook,\n>       \t\t\t\t   notify_start_failure,\n>      -\t\t\t\t   NULL,\n>     @@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n>       \t\t\t\t   notify_hook_finished,\n>       \t\t\t\t   &cb_data,\n>       \t\t\t\t   \"hook\",\n>     -@@ hook.c: int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n>     +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n>     + \n>     + \tif (options->absolute_path)\n>     + \t\tstrbuf_release(&abs_path);\n>     ++\tfree(my_hook.feed_pipe_cb_data);\n\nNice catch. Thanks.\n\n> 25:  7343be28ef4 ! 26:  4b7175af2e5 hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n> 26:  85bf13a0835 ! 27:  3f24e056410 transport: convert pre-push hook to use config\n>     @@ Metadata\n>      Author: Emily Shaffer <emilyshaffer@google.com>\n>      \n>       ## Commit message ##\n>     -    transport: convert pre-push hook to use config\n>     +    transport: convert pre-push hook to hook.h\n>      \n>     -    By using the hook.h:run_hooks API, pre-push hooks can be specified in\n>     -    the config as well as in the hookdir.\n>     +    Move the pre-push hook away from run-command.h to and over to the new\n>     +    hook.h library.\n>      \n>          Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n>          Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n>     @@ transport.c: static void die_with_unpushed_submodules(struct string_list *needs_\n>      -\tint ret = 0, x;\n>      +\tint ret = 0;\n>      +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>     -+\tstruct strbuf tmp = STRBUF_INIT;\n>       \tstruct ref *r;\n>      -\tstruct child_process proc = CHILD_PROCESS_INIT;\n>      -\tstruct strbuf buf;\n>     @@ transport.c: static void die_with_unpushed_submodules(struct string_list *needs_\n>      -\t\tfinish_command(&proc);\n>      -\t\treturn -1;\n>      -\t}\n>     --\n>     --\tsigchain_push(SIGPIPE, SIG_IGN);\n>     -+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n>     ++\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n>       \n>     +-\tsigchain_push(SIGPIPE, SIG_IGN);\n>     +-\n>      -\tstrbuf_init(&buf, 256);\n>      +\tstrvec_push(&opt.args, transport->remote->name);\n>      +\tstrvec_push(&opt.args, transport->url);\n>       \n>       \tfor (r = remote_refs; r; r = r->next) {\n>     ++\t\tstruct strbuf buf = STRBUF_INIT;\n\nAh. It is not being freed because its lifetime is being managed by\nto_stdin instead. Ok, cool!\n\n>     ++\n>       \t\tif (!r->peer_ref) continue;\n>     -@@ transport.c: static int run_pre_push_hook(struct transport *transport,\n>     + \t\tif (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;\n>     + \t\tif (r->status == REF_STATUS_REJECT_STALE) continue;\n>       \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n>       \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n>       \n>      -\t\tstrbuf_reset(&buf);\n>      -\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n>     -+\t\tstrbuf_reset(&tmp);\n>     -+\t\tstrbuf_addf(&tmp, \"%s %s %s %s\",\n>     ++\t\tstrbuf_addf(&buf, \"%s %s %s %s\",\n>       \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n>       \t\t\t r->name, oid_to_hex(&r->old_oid));\n>      -\n>     @@ transport.c: static int run_pre_push_hook(struct transport *transport,\n>      -\t\t\t\tret = -1;\n>      -\t\t\tbreak;\n>      -\t\t}\n>     -+\t\tstring_list_append(&to_stdin, tmp.buf);\n>     ++\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n>       \t}\n>       \n>      -\tstrbuf_release(&buf);\n>     @@ transport.c: static int run_pre_push_hook(struct transport *transport,\n>      -\tx = finish_command(&proc);\n>      -\tif (!ret)\n>      -\t\tret = x;\n>     -+\tret = run_hooks(\"pre-push\", &opt);\n>     -+\trun_hooks_opt_clear(&opt);\n>     -+\tstrbuf_release(&tmp);\n>     ++\tret = run_hooks_oneshot(\"pre-push\", &opt);\n>     ++\tto_stdin.strdup_strings = 1;\n\nAnd this is a typical change to the oneshot function. Cool.\n\n>      +\tstring_list_clear(&to_stdin, 0);\n>       \n>       \treturn ret;\n>  -:  ----------- > 28:  ecf75f33233 hook tests: test for exact \"pre-push\" hook input\n>  -:  ----------- > 29:  2c961be94b4 hook tests: use a modern style for \"pre-push\" tests\n\nI'll take a look at these separately.\n\n> 27:  331014bad17 ! 30:  1ce456f9d9d reference-transaction: use hook.h to run hooks\n>     @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n>       \t\t\t\tconst char *state)\n>       {\n>      -\tstruct child_process proc = CHILD_PROCESS_INIT;\n>     - \tstruct strbuf buf = STRBUF_INIT;\n>     +-\tstruct strbuf buf = STRBUF_INIT;\n>      -\tconst char *hook;\n>      +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>     -+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n>     ++\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n>       \tint ret = 0, i;\n>     -+\tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n>       \n>      -\thook = find_hook(\"reference-transaction\");\n>      -\tif (!hook)\n>     -+\tif (!hook_exists(\"reference-transaction\"))\n>     - \t\treturn ret;\n>     - \n>     +-\t\treturn ret;\n>     +-\n>      -\tstrvec_pushl(&proc.args, hook, state, NULL);\n>      -\tproc.in = -1;\n>      -\tproc.stdout_to_stderr = 1;\n>     @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n>      -\n>      -\tret = start_command(&proc);\n>      -\tif (ret)\n>     --\t\treturn ret;\n>     --\n>     ++\tif (!hook_exists(\"reference-transaction\"))\n>     + \t\treturn ret;\n>     + \n>      -\tsigchain_push(SIGPIPE, SIG_IGN);\n>      +\tstrvec_push(&opt.args, state);\n>       \n>       \tfor (i = 0; i < transaction->nr; i++) {\n>       \t\tstruct ref_update *update = transaction->updates[i];\n>     -+\t\toid_to_hex_r(o, &update->old_oid);\n>     -+\t\toid_to_hex_r(n, &update->new_oid);\n>     ++\t\tstruct strbuf buf = STRBUF_INIT;\n\nAh, this is doing the same thing as with 'pre-push'. Cool.\n\n>       \n>     - \t\tstrbuf_reset(&buf);\n>     +-\t\tstrbuf_reset(&buf);\n>      -\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n>     --\t\t\t    oid_to_hex(&update->old_oid),\n>     --\t\t\t    oid_to_hex(&update->new_oid),\n>     --\t\t\t    update->refname);\n>     ++\t\tstrbuf_addf(&buf, \"%s %s %s\",\n>     + \t\t\t    oid_to_hex(&update->old_oid),\n>     + \t\t\t    oid_to_hex(&update->new_oid),\n>     + \t\t\t    update->refname);\n>      -\n>      -\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n>      -\t\t\tif (errno != EPIPE)\n>      -\t\t\t\tret = -1;\n>      -\t\t\tbreak;\n>      -\t\t}\n>     -+\t\tstrbuf_addf(&buf, \"%s %s %s\", o, n, update->refname);\n>     -+\t\tstring_list_append(&to_stdin, buf.buf);\n>     ++\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n>       \t}\n>       \n>      -\tclose(proc.in);\n>      -\tsigchain_pop(SIGPIPE);\n>     +-\tstrbuf_release(&buf);\n>      +\topt.feed_pipe = pipe_from_string_list;\n>      +\topt.feed_pipe_ctx = &to_stdin;\n>      +\n>     -+\tret = run_hooks(\"reference-transaction\", &opt);\n>     -+\trun_hooks_opt_clear(&opt);\n>     - \tstrbuf_release(&buf);\n>     ++\tret = run_hooks_oneshot(\"reference-transaction\", &opt);\n>     ++\tto_stdin.strdup_strings = 1;\n\nAnd turning on strdup_strings here means we will free them when we call\nstring_list_clear. Ok.\n\n>      +\tstring_list_clear(&to_stdin, 0);\n>       \n>      -\tret |= finish_command(&proc);\n> 32:  db70b59b3bd ! 35:  ceef2f3e804 receive-pack: convert receive hooks to hook.h\n>     @@ builtin/receive-pack.c: static void hook_output_to_sideband(struct strbuf *outpu\n>      +{\n>      +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>      +\tstruct receive_hook_feed_context ctx;\n>     -+\tint rc;\n>      +\tstruct command *iter = commands;\n>      +\n>      +\t/* if there are no valid commands, don't invoke the hook at all. */\n>     @@ builtin/receive-pack.c: static void hook_output_to_sideband(struct strbuf *outpu\n>      +\tif (!iter)\n>      +\t\treturn 0;\n>      +\n>     -+\t/* pre-receive hooks should run in series as the hook updates refs */\n>     -+\tif (!strcmp(hook_name, \"pre-receive\"))\n>     -+\t\topt.jobs = 1;\n>     -+\n\nHm, interesting. I'll note this as a case to watch for when I add the\n.jobs option back in.\n\n\nThanks. I'll take a look at the newly added/copied over patches next.\n\n - Emily\n"},{"id":"433179","messageId":"YR7sko1vKy1rpJR/@google.com","threadId":"55286","inReplyTo":"xmqqo89txdo5.fsf@gitster.g","subject":"Re: [PATCH v3 5/6] hook: include hooks from the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-19T23:43:14Z","receivedAt":"2021-08-19T23:43:24Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Thu, Aug 19, 2021 at 03:39:06PM -0700, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> > index 96d3d6572c..c394756328 100644\n> > --- a/Documentation/config/hook.txt\n> > +++ b/Documentation/config/hook.txt\n> > @@ -1,3 +1,21 @@\n> > +hook.<name>.command::\n> > +\tA command to execute whenever `hook.<name>` is invoked. `<name>` should\n> > +\tbe a unique \"friendly\" name which you can use to identify this hook\n> > +\tcommand. (You can specify when to invoke this command with\n> > +\t`hook.<name>.event`.) The value can be an executable on your device or a\n> > +\toneliner for your shell. If more than one value is specified for the\n> > +\tsame `<name>`, the last value parsed will be the only command executed.\n> > +\tSee linkgit:git-hook[1].\n> > +\n> > +hook.<name>.event::\n> > +\tThe hook events which should invoke `hook.<name>`. `<name>` should be a\n> > +\tunique \"friendly\" name which you can use to identify this hook. The\n> > +\tvalue should be the name of a hook event, like \"pre-commit\" or \"update\".\n> > +\t(See linkgit:githooks[5] for a complete list of hooks Git knows about.)\n> > +\tOn the specified event, the associated `hook.<name>.command` will be\n> > +\texecuted. More than one event can be specified if you wish for\n> > +\t`hook.<name>` to execute on multiple events. See linkgit:git-hook[1].\n> \n> Looking much better.  It now gives enough information to readers to\n> understand (if not enough to convince that it is a good idea) why an\n> indirection with \"friendly name\" between the event and command is\n> there.  In short, <name> names the command to be run and without\n> indirection, you'd end up having to write:\n> \n>     [hook \"check-whitespace && spellcheck-log-message\"]\n> \tevent = pre-commit\n>     [hook \"check-whitespace && spellcheck-log-message\"]\n> \tevent = another-hookable-event\n> \n> which may give the same expressiveness (and may even be workable if\n> the configuration were machine generated) but it is typo-prone, and\n> a single typo, or even an insignificant whitespace change in the\n> command, would destroy the grouping of \"this command fires upon\n> these events\".\n> \n> It becomes much less typo prone with the indirection, i.e.\n> \n>     [hook \"logcheck\"]\n> \tcommand = check-whitespace && spellcheck-log-message\n> \n>     [hook \"logcheck\"]\n> \tevent = pre-commit\n> \n>     [hook \"logcheck\"]\n> \tevent = another-hookable-event\n> \n> using the \"friendly name\", especially if these entries are spread\n> across different configuration files.\n> \n> My original question was primarily because I thought the\n> second-level <name> corresponded to <event>.  If that were the case,\n> it can trivially be made simpler without making it typo-prone, i.e.\n> \n>     [hook \"pre-commit\"]\n> \tcommand = check-whitespace && spellcheck-log-message\n> \n>     [hook \"another-hookable-event\"]\n> \tcommand = check-whitespace && spellcheck-log-message\n> \n> since the name of the event would be much shorter than the command\n> line.  But since we are not grouping per hookable event (to apply\n> the \"last one wins\" rule to determine which single command is the\n> one that gets to run).\n> \n\nTo be clear, the config schema did work the way you describe until this\nrevision. Ævar suggested the change and it seemed like a good idea to me\n(and the rest of the Google folks) so I changed between v2 and v3 of the\nrestart.\n\n - Emily\n"},{"id":"433180","messageId":"xmqqfsv5xagg.fsf@gitster.g","threadId":"55286","inReplyTo":"YR7sko1vKy1rpJR/@google.com","subject":"Re: [PATCH v3 5/6] hook: include hooks from the config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-19T23:48:31Z","receivedAt":"2021-08-19T23:48:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> On Thu, Aug 19, 2021 at 03:39:06PM -0700, Junio C Hamano wrote:\n>> \n>> My original question was primarily because I thought the\n>> second-level <name> corresponded to <event>.  If that were the case,\n>> it can trivially be made simpler without making it typo-prone, i.e.\n>> \n>>     [hook \"pre-commit\"]\n>> \tcommand = check-whitespace && spellcheck-log-message\n>> \n>>     [hook \"another-hookable-event\"]\n>> \tcommand = check-whitespace && spellcheck-log-message\n>> \n>> since the name of the event would be much shorter than the command\n>> line.  But since we are not grouping per hookable event (to apply\n>> the \"last one wins\" rule to determine which single command is the\n>> one that gets to run).\n>> \n>\n> To be clear, the config schema did work the way you describe until this\n> revision. Ævar suggested the change and it seemed like a good idea to me\n> (and the rest of the Google folks) so I changed between v2 and v3 of the\n> restart.\n\nTo be clear, you do not want to take the above as my suggestion to\ngo back to the previous one, since that is not what I meant.  As\nlong as you and others are happy with what you folks ended up with,\ni.e. the current one that uses <name> that is a short-hand for\n<command>, that is what matters.\n\nThanks.\n"},{"id":"433181","messageId":"YR7xRYWSDAHPH2NS@google.com","threadId":"55286","inReplyTo":"patch-v4-02.36-0f749530777-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 02/36] Makefile: stop hardcoding {command,config}-list.h","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-20T00:03:17Z","receivedAt":"2021-08-20T00:03:28Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:28PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> Change various places that hardcode the names of these two files to\n> refer to either $(GENERATED_H), or to a new generated-hdrs\n> target. That target is consistent with the *-objs targets I recently\n> added in 029bac01a8 (Makefile: add {program,xdiff,test,git,fuzz}-objs\n> & objects targets, 2021-02-23).\n> \n> A subsequent commit will add a new generated hook-list.h. By doing\n> this refactoring we'll only need to add the new file to the\n> GENERATED_H variable, not EXCEPT_HDRS, the vcbuild/README etc.\n> \n> I have not tested the Windows-specific change in config.mak.uname\n> being made here, but we use other variables from the Makefile in the\n> same block, and the GENERATED_H is fully defined before we include\n> config.mak.uname.\n\nIs it not something you can get coverage for via, for example, the\nGitHub Actions CI suite? I wonder if that means we want some test to\ncheck that these generated lists came together correctly?\n\nOtherwise the diff looks straightforward.\n> \n> Hardcoding command-list.h there seems to have been a case of\n> copy/paste programming in 976aaedca0 (msvc: add a Makefile target to\n> pre-generate the Visual Studio solution, 2019-07-29). The\n> config-list.h was added later in 709df95b78 (help: move\n> list_config_help to builtin/help, 2020-04-16).\n> \n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nReviewed-by: Emily Shaffer <emilyshaffer@google.com>\n"},{"id":"433182","messageId":"YR7xftXDd5fJNgu1@google.com","threadId":"55286","inReplyTo":"patch-v4-01.36-81fe1ed90d5-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 01/36] Makefile: mark \"check\" target as .PHONY","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-20T00:04:14Z","receivedAt":"2021-08-20T00:04:25Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:27PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> Fix a bug in 44c9e8594e (Fix up header file dependencies and add\n> sparse checking rules, 2005-07-03), we never marked the phony \"check\"\n> target as such.\n\nA simple enough change, and it gave me an excuse to go and look up what\n.PHONY actually does. Thanks ;)\n\n> \n> Perhaps we should just remove it, since as of a combination of\n> 912f9980d2 (Makefile: help people who run 'make check' by mistake,\n> 2008-11-11) 0bcd9ae85d (sparse: Fix errors due to missing\n> target-specific variables, 2011-04-21) we've been suggesting the user\n> run \"make sparse\" directly.\n> \n> But under that mode it still does something, as well as directing the\n> user to run \"make test\" under non-sparse. So let's punt that and\n> narrowly fix the PHONY bug.\n> \n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nReviewed-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  Makefile | 1 +\n>  1 file changed, 1 insertion(+)\n> \n> diff --git a/Makefile b/Makefile\n> index c6f6246bf63..2ff038069e8 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -2931,6 +2931,7 @@ hdr-check: $(HCO)\n>  style:\n>  \tgit clang-format --style file --diff --extensions c,h\n>  \n> +.PHONY: check\n>  check: config-list.h command-list.h\n>  \t@if sparse; \\\n>  \tthen \\\n> -- \n> 2.33.0.rc0.595.ge31e012651d\n> \n"},{"id":"433183","messageId":"YR7xyey0nWAImrqB@google.com","threadId":"55286","inReplyTo":"patch-v4-03.36-644b31fe281-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 03/36] Makefile: remove an out-of-date comment","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-20T00:05:29Z","receivedAt":"2021-08-20T00:05:40Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:29PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> This comment added in dfea575017 (Makefile: lazily compute header\n> dependencies, 2010-01-26) has been out of date since\n> 92b88eba9f (Makefile: use `git ls-files` to list header files, if\n> possible, 2019-03-04), when we did exactly what it tells us not to do\n> and added $(GENERATED_H) to $(OBJECTS) dependencies.\n> \n> The rest of it was also somewhere between inaccurate and outdated,\n> since as of b8ba629264 (Makefile: fold MISC_H into LIB_H, 2012-06-20)\n> it's not followed by a list of header files, that got moved earlier in\n> the file into LIB_H in 60d24dd255 (Makefile: fold XDIFF_H and VCSSVN_H\n> into LIB_H, 2012-07-06).\n> \n> Let's just remove it entirely, to the extent that we have anything\n> useful to say here the comment on the\n> \"USE_COMPUTED_HEADER_DEPENDENCIES\" variable a few lines above this\n> change does the job for us.\n> \n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nReviewed-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  Makefile | 7 -------\n>  1 file changed, 7 deletions(-)\n> \n> diff --git a/Makefile b/Makefile\n> index 89bf0dd7332..0a540dcd34e 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -2519,13 +2519,6 @@ ifneq ($(dep_files_present),)\n>  include $(dep_files_present)\n>  endif\n>  else\n> -# Dependencies on header files, for platforms that do not support\n> -# the gcc -MMD option.\n> -#\n> -# Dependencies on automatically generated headers such as command-list.h\n> -# should _not_ be included here, since they are necessary even when\n> -# building an object for the first time.\n> -\n>  $(OBJECTS): $(LIB_H) $(GENERATED_H)\n>  endif\n>  \n> -- \n> 2.33.0.rc0.595.ge31e012651d\n> \n"},{"id":"433184","messageId":"YR7yfiJXJutOKhn/@google.com","threadId":"55286","inReplyTo":"patch-v4-04.36-89c4d44b0c3-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 04/36] hook.[ch]: move find_hook() to this new library","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-20T00:08:30Z","receivedAt":"2021-08-20T00:08:41Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:30PM +0200, Ævar Arnfjörð Bjarmason wrote:\n\nThe subject is now a little confusing (and I guess it would have been\nbefore, too). Can we remove \"this new library\" and replace it with\nsomething that makes sense on its own?\n\n> \n> Move the find_hook() function from run-command.c to a new hook.c\n> library. This change establishes a stub library that's pretty\n> pointless right now, but will see much wider use with Emily Shaffer's\n> upcoming \"configuration-based hooks\" series.\n> \n> Eventually all the hook related code will live in hook.[ch]. Let's\n> start that process by moving the simple find_hook() function over\n> as-is.\n> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n\nIt doesn't make sense for me to add a Reviewed-by line here, I guess.\nBut it seems to me to be fine to do this early on instead of at the very\nend.\n\n - Emily\n"},{"id":"433185","messageId":"YR7yuCAaQScM6zvD@google.com","threadId":"55286","inReplyTo":"patch-v4-05.36-3514e0c0251-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 05/36] hook.c: add a hook_exists() wrapper and use it in bugreport.c","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-20T00:09:28Z","receivedAt":"2021-08-20T00:09:37Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:31PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> Add a boolean version of the find_hook() function for those callers\n> who are only interested in checking whether the hook exists, not what\n> the path to it is.\n\nSeems fine.\n\n> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  builtin/bugreport.c | 2 +-\n>  hook.c              | 5 +++++\n>  hook.h              | 5 +++++\n>  3 files changed, 11 insertions(+), 1 deletion(-)\n> \n> diff --git a/builtin/bugreport.c b/builtin/bugreport.c\n> index 596f079a7f9..941c8d5e270 100644\n> --- a/builtin/bugreport.c\n> +++ b/builtin/bugreport.c\n> @@ -82,7 +82,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n>  \t}\n>  \n>  \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n> -\t\tif (find_hook(hook[i]))\n> +\t\tif (hook_exists(hook[i]))\n>  \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n>  }\n>  \n> diff --git a/hook.c b/hook.c\n> index c4dbef1d0ef..97cd799a320 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -35,3 +35,8 @@ const char *find_hook(const char *name)\n>  \t}\n>  \treturn path.buf;\n>  }\n> +\n> +int hook_exists(const char *name)\n> +{\n> +\treturn !!find_hook(name);\n\nLater on I'll change this to refer to the output of list_hooks()\ninstead.\n\n> +}\n> diff --git a/hook.h b/hook.h\n> index 68624f16059..4c547ac15e5 100644\n> --- a/hook.h\n> +++ b/hook.h\n> @@ -8,4 +8,9 @@\n>   */\n>  const char *find_hook(const char *name);\n>  \n> +/*\n> + * A boolean version of find_hook()\n> + */\n> +int hook_exists(const char *hookname);\n> +\n>  #endif\n> -- \n> 2.33.0.rc0.595.ge31e012651d\n> \n"},{"id":"433186","messageId":"YR7zAyUOgCTQquuK@google.com","threadId":"55286","inReplyTo":"patch-v4-06.36-d5ef40f77dc-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 06/36] hook.c users: use \"hook_exists()\" insted of \"find_hook()\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-20T00:10:43Z","receivedAt":"2021-08-20T00:10:53Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:32PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> Use the new hook_exists() function instead of find_hook() where the\n> latter was called in boolean contexts. This make subsequent changes in\n> a series where we further refactor the hook API clearer, as we won't\n> conflate wanting to get the path of the hook with checking for its\n> existence.\n> \n\nNone of these callsites capture the return string from find_hook(), so\nthis looks fine.\n\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nReviewed-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  builtin/commit.c       | 2 +-\n>  builtin/merge.c        | 2 +-\n>  builtin/receive-pack.c | 2 +-\n>  sequencer.c            | 2 +-\n>  4 files changed, 4 insertions(+), 4 deletions(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 51b07ee02ea..aa3c741efa9 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -1052,7 +1052,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>  \t\treturn 0;\n>  \t}\n>  \n> -\tif (!no_verify && find_hook(\"pre-commit\")) {\n> +\tif (!no_verify && hook_exists(\"pre-commit\")) {\n>  \t\t/*\n>  \t\t * Re-read the index as pre-commit hook could have updated it,\n>  \t\t * and write it out as a tree.  We must do this before we invoke\n> diff --git a/builtin/merge.c b/builtin/merge.c\n> index be98d66b0a8..03f244dd5a0 100644\n> --- a/builtin/merge.c\n> +++ b/builtin/merge.c\n> @@ -849,7 +849,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n>  \t * and write it out as a tree.  We must do this before we invoke\n>  \t * the editor and after we invoke run_status above.\n>  \t */\n> -\tif (find_hook(\"pre-merge-commit\"))\n> +\tif (hook_exists(\"pre-merge-commit\"))\n>  \t\tdiscard_cache();\n>  \tread_cache_from(index_file);\n>  \tstrbuf_addbuf(&msg, &merge_msg);\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index 97aebdc15bd..91fa799b66e 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -1464,7 +1464,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n>  \n>  \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n>  \n> -\tif (!find_hook(push_to_checkout_hook))\n> +\tif (!hook_exists(push_to_checkout_hook))\n>  \t\tretval = push_to_deploy(sha1, &env, work_tree);\n>  \telse\n>  \t\tretval = push_to_checkout(sha1, &env, work_tree);\n> diff --git a/sequencer.c b/sequencer.c\n> index ea4199d65a4..9aac08c1545 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -1446,7 +1446,7 @@ static int try_to_commit(struct repository *r,\n>  \t\t}\n>  \t}\n>  \n> -\tif (find_hook(\"prepare-commit-msg\")) {\n> +\tif (hook_exists(\"prepare-commit-msg\")) {\n>  \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n>  \t\tif (res)\n>  \t\t\tgoto out;\n> -- \n> 2.33.0.rc0.595.ge31e012651d\n> \n"},{"id":"433187","messageId":"YR70XmoSUE3D1ZS6@google.com","threadId":"55286","inReplyTo":"patch-v4-28.36-ecf75f33233-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 28/36] hook tests: test for exact \"pre-push\" hook input","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-20T00:16:30Z","receivedAt":"2021-08-20T00:16:41Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:54PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> Extend the tests added in ec55559f937 (push: Add support for pre-push\n> hooks, 2013-01-13) to exhaustively test for the exact input we're\n> expecting. This helps a parallel series that's refactoring how the\n> hook is called, to e.g. make sure that we don't miss a trailing\n> newline\n\nThe reference to \"a parallel series\" I think didn't belong in the\ncommit-msg in the first place, and doesn't make sense now (because this\nis in said series, right?).\n\n> \n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  t/t5571-pre-push-hook.sh | 23 ++++++++++++++++++-----\n>  1 file changed, 18 insertions(+), 5 deletions(-)\n> \n> diff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh\n> index ad8d5804f7b..d2857a6fbc0 100755\n> --- a/t/t5571-pre-push-hook.sh\n> +++ b/t/t5571-pre-push-hook.sh\n> @@ -11,7 +11,7 @@ HOOKDIR=\"$(git rev-parse --git-dir)/hooks\"\n>  HOOK=\"$HOOKDIR/pre-push\"\n>  mkdir -p \"$HOOKDIR\"\n>  write_script \"$HOOK\" <<EOF\n> -cat >/dev/null\n> +cat >actual\n>  exit 0\n>  EOF\n>  \n> @@ -20,10 +20,16 @@ test_expect_success 'setup' '\n>  \tgit init --bare repo1 &&\n>  \tgit remote add parent1 repo1 &&\n>  \ttest_commit one &&\n> -\tgit push parent1 HEAD:foreign\n> +\tcat >expect <<-EOF &&\n> +\tHEAD $(git rev-parse HEAD) refs/heads/foreign $(test_oid zero)\n> +\tEOF\n> +\n> +\ttest_when_finished \"rm actual\" &&\n> +\tgit push parent1 HEAD:foreign &&\n> +\ttest_cmp expect actual\n>  '\n>  write_script \"$HOOK\" <<EOF\n> -cat >/dev/null\n> +cat >actual\n\nHm, I am not sure I like this. It upsets the usual convention of what\n'actual' means (\"output I captured from a 'git' command\"). It is\nnitpicky, but I would be happier to see it cat to some other filename,\nlike 'received-input'.\n\n>  exit 1\n>  EOF\n>  \n> @@ -32,11 +38,18 @@ export COMMIT1\n>  \n>  test_expect_success 'push with failing hook' '\n>  \ttest_commit two &&\n> -\ttest_must_fail git push parent1 HEAD\n> +\tcat >expect <<-EOF &&\n> +\tHEAD $(git rev-parse HEAD) refs/heads/main $(test_oid zero)\n> +\tEOF\n> +\n> +\ttest_when_finished \"rm actual\" &&\n> +\ttest_must_fail git push parent1 HEAD &&\n> +\ttest_cmp expect actual\n>  '\n>  \n>  test_expect_success '--no-verify bypasses hook' '\n> -\tgit push --no-verify parent1 HEAD\n> +\tgit push --no-verify parent1 HEAD &&\n> +\ttest_path_is_missing actual\n\nOther than the naming nit, though, the test changes look reasonable to\nme to ensure we don't goof up the stdin support in hook.c. Thanks.\n\nWith (or, I guess, without) changes,\nReviewed-by: Emily Shaffer <emilyshaffer@google.com>\n\n>  '\n>  \n>  COMMIT2=\"$(git rev-parse HEAD)\"\n> -- \n> 2.33.0.rc0.595.ge31e012651d\n> \n"},{"id":"433188","messageId":"YR7071AlYm4vsvxG@google.com","threadId":"55286","inReplyTo":"patch-v4-29.36-2c961be94b4-20210803T191505Z-avarab@gmail.com","subject":"Re: [PATCH v4 29/36] hook tests: use a modern style for \"pre-push\" tests","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-20T00:18:55Z","receivedAt":"2021-08-20T00:19:12Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 03, 2021 at 09:38:55PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> Indent the here-docs and use \"test_cmp\" instead of \"diff\" in tests\n> added in ec55559f937 (push: Add support for pre-push hooks,\n> 2013-01-13). Let's also use the more typical \"expect\" instead of\n> \"expected\" to be consistent with the rest of the test file.\n> \n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  t/t5571-pre-push-hook.sh | 71 ++++++++++++++++++++--------------------\n>  1 file changed, 35 insertions(+), 36 deletions(-)\n> \n> diff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh\n> index d2857a6fbc0..6d0d5b854ea 100755\n> --- a/t/t5571-pre-push-hook.sh\n> +++ b/t/t5571-pre-push-hook.sh\n> @@ -61,15 +61,15 @@ echo \"$2\" >>actual\n>  cat >>actual\n>  EOF\n>  \n> -cat >expected <<EOF\n> -parent1\n> -repo1\n> -refs/heads/main $COMMIT2 refs/heads/foreign $COMMIT1\n> -EOF\n> -\n>  test_expect_success 'push with hook' '\n> +\tcat >expected <<-EOF &&\n> +\tparent1\n> +\trepo1\n> +\trefs/heads/main $COMMIT2 refs/heads/foreign $COMMIT1\n> +\tEOF\n> +\n>  \tgit push parent1 main:foreign &&\n> -\tdiff expected actual\n> +\ttest_cmp expected actual\n\nYour commit message tells me you're getting rid of 'expected', but here\nit is. Should that just be 'expect'?\n\nOtherwise it looks fine.\n\nReviewed-by: Emily Shaffer <emilyshaffer@google.com>\n"},{"id":"433556","messageId":"8735qygbvv.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YR7xRYWSDAHPH2NS@google.com","subject":"Re: [PATCH v4 02/36] Makefile: stop hardcoding {command,config}-list.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T14:22:42Z","receivedAt":"2021-08-24T14:23:21Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Aug 19 2021, Emily Shaffer wrote:\n\n> On Tue, Aug 03, 2021 at 09:38:28PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> Change various places that hardcode the names of these two files to\n>> refer to either $(GENERATED_H), or to a new generated-hdrs\n>> target. That target is consistent with the *-objs targets I recently\n>> added in 029bac01a8 (Makefile: add {program,xdiff,test,git,fuzz}-objs\n>> & objects targets, 2021-02-23).\n>> \n>> A subsequent commit will add a new generated hook-list.h. By doing\n>> this refactoring we'll only need to add the new file to the\n>> GENERATED_H variable, not EXCEPT_HDRS, the vcbuild/README etc.\n>> \n>> I have not tested the Windows-specific change in config.mak.uname\n>> being made here, but we use other variables from the Makefile in the\n>> same block, and the GENERATED_H is fully defined before we include\n>> config.mak.uname.\n>\n> Is it not something you can get coverage for via, for example, the\n> GitHub Actions CI suite? I wonder if that means we want some test to\n> check that these generated lists came together correctly?\n\nI'll amend this. Yes it's being tested via the CI, and it's the CI that\nfailed before. I should have said \"I have not tested this directly\",\ni.e. I don't have a local Windows machine, but pushing it to the CI\nbuild that uses this works...\n\n> Otherwise the diff looks straightforward.\n>> \n>> Hardcoding command-list.h there seems to have been a case of\n>> copy/paste programming in 976aaedca0 (msvc: add a Makefile target to\n>> pre-generate the Visual Studio solution, 2019-07-29). The\n>> config-list.h was added later in 709df95b78 (help: move\n>> list_config_help to builtin/help, 2020-04-16).\n>> \n>> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> Reviewed-by: Emily Shaffer <emilyshaffer@google.com>\n\n"},{"id":"433557","messageId":"87zgt6evkf.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210819033450.3382652-2-emilyshaffer@google.com","subject":"Re: [PATCH v3 1/6] hook: run a list of hooks instead","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T14:56:10Z","receivedAt":"2021-08-24T15:01:09Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 18 2021, Emily Shaffer wrote:\n\n> @@ -25,7 +25,8 @@ static int run(int argc, const char **argv, const char *prefix)\n>  \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>  \tint ignore_missing = 0;\n>  \tconst char *hook_name;\n> -\tconst char *hook_path;\n> +\tstruct list_head *hooks;\n> +\n>  \tstruct option run_options[] = {\n>  \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n>  \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n\nIn general in this patch series there's a bunch of little whitespace\nchanges like that along with other changes. I think it's probably best\nif I just absorb that in the \"base\" topic instead of doing them\nhere. E.g. in this case we could also add a line between \"struct option\"\nand the rest.\n\nI don't mind either way, but the whitespace churn makes for distracting\nreading...\n\n> @@ -58,15 +59,16 @@ static int run(int argc, const char **argv, const char *prefix)\n>  \tgit_config(git_default_config, NULL);\n>  \n>  \thook_name = argv[0];\n> -\tif (ignore_missing)\n> -\t\treturn run_hooks_oneshot(hook_name, &opt);\n> -\thook_path = find_hook(hook_name);\n> -\tif (!hook_path) {\n> +\thooks = list_hooks(hook_name);\n> +\tif (list_empty(hooks)) {\n> +\t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n> +\t\tif (ignore_missing)\n> +\t\t\treturn 0;\n>  \t\terror(\"cannot find a hook named %s\", hook_name);\n>  \t\treturn 1;\n>  \t}\n>  \n> -\tret = run_hooks(hook_name, hook_path, &opt);\n> +\tret = run_hooks(hook_name, hooks, &opt);\n>  \trun_hooks_opt_clear(&opt);\n>  \treturn ret;\n\nThis memory management is a bit inconsistent. So here we list_hooks(),\npass it to run_hooks(), which clears it for us, OK...\n\n> -int run_hooks(const char *hook_name, const char *hook_path,\n> -\t      struct run_hooks_opt *options)\n> +int run_hooks(const char *hook_name, struct list_head *hooks,\n> +\t\t    struct run_hooks_opt *options)\n>  {\n> -\tstruct strbuf abs_path = STRBUF_INIT;\n> -\tstruct hook my_hook = {\n> -\t\t.hook_path = hook_path,\n> -\t};\n>  \tstruct hook_cb_data cb_data = {\n>  \t\t.rc = 0,\n>  \t\t.hook_name = hook_name,\n> @@ -197,11 +241,9 @@ int run_hooks(const char *hook_name, const char *hook_path,\n>  \tif (!options)\n>  \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n>  \n> -\tif (options->absolute_path) {\n> -\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n> -\t\tmy_hook.hook_path = abs_path.buf;\n> -\t}\n> -\tcb_data.run_me = &my_hook;\n> +\n> +\tcb_data.head = hooks;\n> +\tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n>  \n>  \trun_processes_parallel_tr2(jobs,\n>  \t\t\t\t   pick_next_hook,\n> @@ -213,18 +255,15 @@ int run_hooks(const char *hook_name, const char *hook_path,\n>  \t\t\t\t   \"hook\",\n>  \t\t\t\t   hook_name);\n>  \n> -\n> -\tif (options->absolute_path)\n> -\t\tstrbuf_release(&abs_path);\n> -\tfree(my_hook.feed_pipe_cb_data);\n> +\tclear_hook_list(hooks);\n>  \n>  \treturn cb_data.rc;\n>  }\n\nWhich we can see here will call clear_hook_list(), so far so good, but then...\n\n>  int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n>  {\n> -\tconst char *hook_path;\n> -\tint ret;\n> +\tstruct list_head *hooks;\n> +\tint ret = 0;\n>  \tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n>  \n>  \tif (!options)\n> @@ -233,14 +272,19 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n>  \tif (options->path_to_stdin && options->feed_pipe)\n>  \t\tBUG(\"choose only one method to populate stdin\");\n>  \n> -\thook_path = find_hook(hook_name);\n> -\tif (!hook_path) {\n> -\t\tret = 0;\n> +\thooks = list_hooks(hook_name);\n> +\n> +\t/*\n> +\t * If you need to act on a missing hook, use run_found_hooks()\n> +\t * instead\n> +\t */\n> +\tif (list_empty(hooks))\n>  \t\tgoto cleanup;\n> -\t}\n>  \n> -\tret = run_hooks(hook_name, hook_path, options);\n> +\tret = run_hooks(hook_name, hooks, options);\n> +\n>  cleanup:\n>  \trun_hooks_opt_clear(options);\n> +\tclear_hook_list(hooks);\n\n...the oneshot command also does clear_hook_list(), after calling\nrun_hooks() which cleared it already.  That looks like a mistake,\ni.e. we should always trust run_hooks() to clear it, no?\n"},{"id":"433558","messageId":"87wnoaevbe.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210819033450.3382652-3-emilyshaffer@google.com","subject":"Re: [PATCH v3 2/6] hook: allow parallel hook execution","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T15:01:09Z","receivedAt":"2021-08-24T15:06:33Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 18 2021, Emily Shaffer wrote:\n\n> -'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n> +'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n> +\t<hook-name> [-- <hook-args>]\n\nAs an aside I wondered if it shouldn't be [[-j|--jobs] <n>], but grepped\naround and found that (x|y|z) means a mandatory pick of x, y or z, but\n[x|y|z] means that, plus possibly picking none, I think.\n\nSo this is fine, just something I wondered about...\n\n> +-j::\n> +--jobs::\n> +\tOnly valid for `run`.\n> ++\n> +Specify how many hooks to run simultaneously. If this flag is not specified, use\n> +the value of the `hook.jobs` config. If the config is not specified, use the\n\ns/use the value/uses the value/\n\nAlso we usually say \"of the XYZ config, see linkgit:git-config[1]\", or\nsomething to that effect when we mention config variables. Perhaps we\nshould do the same here.\n\n> +number of CPUs on the current system. Some hooks may be ineligible for\n> +parallelization: for example, 'commit-msg' intends hooks modify the commit\n> +message body and cannot be parallelized.\n\nNot something that *needs* to happen in this series, but I wonder if we\nshouldn't have per-type config here too, so users could force it even\nfor those hook types if they want.\n\n> -#define RUN_HOOKS_OPT_INIT { \\\n> -\t.env = STRVEC_INIT, \\\n> -\t.args = STRVEC_INIT, \\\n> -}\n> -\n>  /*\n>   * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n>   * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n> @@ -111,6 +113,18 @@ struct hook_cb_data {\n>  \tint *invoked_hook;\n>  };\n>  \n> +#define RUN_HOOKS_OPT_INIT_SERIAL { \\\n> +\t.jobs = 1, \\\n> +\t.env = STRVEC_INIT, \\\n> +\t.args = STRVEC_INIT, \\\n> +}\n> +\n> +#define RUN_HOOKS_OPT_INIT_PARALLEL { \\\n> +\t.jobs = 0, \\\n> +\t.env = STRVEC_INIT, \\\n> +\t.args = STRVEC_INIT, \\\n> +}\n> +\n\nDitto earlier comments about whitespace churn, i.e. I can just move this\naround in the base topic, so the diff here is the change/addition, not\nalso moving things around.\n"},{"id":"433559","messageId":"87tujeev1h.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210819033450.3382652-4-emilyshaffer@google.com","subject":"Re: [PATCH v3 3/6] hook: introduce \"git hook list\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T15:08:25Z","receivedAt":"2021-08-24T15:12:31Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 18 2021, Emily Shaffer wrote:\n\n> +static int list(int argc, const char **argv, const char *prefix)\n> +{\n> +\tstruct list_head *head, *pos;\n> +\tconst char *hookname = NULL;\n> +\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n> +\n> +\tstruct option list_options[] = {\n> +\t\tOPT_END(),\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, prefix, list_options,\n> +\t\t\t     builtin_hook_list_usage, 0);\n> +\n> +\tif (argc < 1)\n> +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n> +\t\t\t      builtin_hook_list_usage, list_options);\n\nUntested, but aren't we silently ignoring:\n\n    git hook list pre-receive some extra gar bage here\n\nI.e. shouldn't this be an \"argc != 1\" check?\n\n> +\n> +\thookname = argv[0];\n> +\n> +\thead = hook_list(hookname);\n> +\n> +\tif (list_empty(head))\n> +\t\treturn 1;\n> +\n> +\tlist_for_each(pos, head) {\n> +\t\tstruct hook *item = list_entry(pos, struct hook, list);\n> +\t\titem = list_entry(pos, struct hook, list);\n> +\t\tif (item)\n> +\t\t\tprintf(\"%s\\n\", item->hook_path);\n\nNit/suggestion: use puts(x) instead of printf(\"%s\\n\", x), but that's\nalso a bikeshedding/style preference, so ignore if you disagree...\n\n> +\t}\n> +\n> +\tclear_hook_list(head);\n\nNit/API suggestion: Maybe s/list_for_each/list_for_each_safe/ and\nremove_hook() in the loop would make more sense for this one-shot caller\nthan iterating over the list twice?\n\nAnyway, currently remove_hook() is static, and it's probably good to not\npeek behind the curtain here, so on second thought clear_hook_list() is\nprobably best...\n\n> +\tstrbuf_release(&hookdir_annotation);\n\nThis function did nothing with hookdir_annotation. Looks like leftover\nWIP code, but maybe it's used in (and should be moved to) a later\ncommit, let's keep reading...\n\n> [...]\n>  \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n>  \n> +\n>  \tINIT_LIST_HEAD(hook_head);\n\n..ditto...\n>  \n>  \tif (!hookname)\n> @@ -103,8 +104,6 @@ struct list_head *list_hooks(const char *hookname)\n>  \n>  \tif (have_git_dir()) {\n>  \t\tconst char *hook_path = find_hook(hookname);\n> -\n\n... earlier notes about whitespace churn...\n\n> -\t\t/* Add the hook from the hookdir */\n>  \t\tif (hook_path) {\n>  \t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n>  \t\t\tto_add->hook_path = hook_path;\n\n"},{"id":"433565","messageId":"87r1eiet3o.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210819033450.3382652-4-emilyshaffer@google.com","subject":"Re: [PATCH v3 3/6] hook: introduce \"git hook list\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T15:53:48Z","receivedAt":"2021-08-24T15:54:25Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 18 2021, Emily Shaffer wrote:\n\n> +\thead = hook_list(hookname);\n\nThis doesn't compile, as hook_list() is added in a later\ncommit. Something something earlier suggestion of \"git rebase -i --exec\n'make test'\" :)\n"},{"id":"433567","messageId":"CAPig+cTzx-eXKFa9UyLd4QW4adcuNFHrp5U+_SC_vyC7L7nANA@mail.gmail.com","threadId":"55286","inReplyTo":"87wnoaevbe.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 2/6] hook: allow parallel hook execution","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2021-08-24T16:13:02Z","receivedAt":"2021-08-24T16:13:16Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Tue, Aug 24, 2021 at 11:06 AM Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n> On Wed, Aug 18 2021, Emily Shaffer wrote:\n> > +'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n> > +     <hook-name> [-- <hook-args>]\n>\n> As an aside I wondered if it shouldn't be [[-j|--jobs] <n>], but grepped\n> around and found that (x|y|z) means a mandatory pick of x, y or z, but\n> [x|y|z] means that, plus possibly picking none, I think.\n>\n> So this is fine, just something I wondered about...\n\nIndeed, the text is fine as-is.\n\n> > +Specify how many hooks to run simultaneously. If this flag is not specified, use\n> > +the value of the `hook.jobs` config. If the config is not specified, use the\n>\n> s/use the value/uses the value/\n\nAs a native English speaker, the text as written (even if a bit\nstilted) sounds better than the suggested replacement. A small tweak\nmight improve it slightly:\n\n    If this flag is not specified, the value of the `hooks.job`\n    configuration is used instead (see linkgit:git-config[1]).\n\nbut is not at all worth a re-roll or a lot of discussion.\n\n> Also we usually say \"of the XYZ config, see linkgit:git-config[1]\", or\n> something to that effect when we mention config variables. Perhaps we\n> should do the same here.\n"},{"id":"433615","messageId":"87o89mej3y.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210819033450.3382652-5-emilyshaffer@google.com","subject":"Re: [PATCH v3 4/6] hook: allow running non-native hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T15:55:13Z","receivedAt":"2021-08-24T19:30:14Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 18 2021, Emily Shaffer wrote:\n\n> As the hook architecture and 'git hook run' become more featureful, we\n> may find wrappers wanting to use the hook architecture to run their own\n> hooks, thereby getting nice things like parallelism and idiomatic Git\n> configuration for free. Enable this by letting 'git hook run' bypass the\n> known_hooks() check.\n>\n> We do still want to keep known_hooks() around, though - by die()ing when\n> an internal Git call asks for run_hooks(\"my-new-hook\"), we can remind\n> Git developers to update Documentation/githooks.txt with their new hook,\n> which in turn helps Git users discover this new hook.\n>\n> [...]\n>\n> +It's possible to use this command to refer to hooks which are not native to Git,\n> +for example if a wrapper around Git wishes to expose hooks into its own\n> +operation in a way which is already familiar to Git users. However, wrappers\n> +invoking such hooks should be careful to name their hook events something which\n> +Git is unlikely to use for a native hook later on. For example, Git is much less\n> +likely to create a `mytool-validate-commit` hook than it is to create a\n> +`validate-commit` hook.\n> +\n>  SUBCOMMANDS\n>  -----------\n\nThe goal here makes sense, but...\n\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index d21f303eca..80397d39f5 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -46,7 +46,7 @@ static int list(int argc, const char **argv, const char *prefix)\n>  \n>  \thookname = argv[0];\n>  \n> -\thead = hook_list(hookname);\n> +\thead = list_hooks_gently(hookname);\n>  \n>  \tif (list_empty(head))\n>  \t\treturn 1;\n> @@ -105,7 +105,7 @@ static int run(int argc, const char **argv, const char *prefix)\n>  \tgit_config(git_default_config, NULL);\n>  \n>  \thook_name = argv[0];\n> -\thooks = list_hooks(hook_name);\n> +\thooks = list_hooks_gently(hook_name);\n>  \tif (list_empty(hooks)) {\n>  \t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n>  \t\tif (ignore_missing)\n\nThis introduces a bug v.s. the previous state, e.g. before:\n\n    $ git hook run --ignore-missing foobar\n    fatal: the hook 'foobar' is not known to git, should be in hook-list.h via githooks(5)\n\nBut after we'll silently ignore it. I.e. we've conflated\n--ignore-missing with a new and hypothetical (and this is now a synonym\nof) --ignore-missing-and-allow-unknown-hook-names.\n\nSo we've conflated the user's one-shot \"foobar\" script with wanting to\ncatch a typo in e.g. git-send-email.perl.\n\nAlso instead of the user's typos being caught with a die (here using\nyour BUG(...) version):\n\n    $ git hook list pre-recive\n    BUG: hook.c:115: Don't recognize hook event 'pre-recive'! Is it documented in Documentation/githooks.txt?\n    Aborted\n\nWe'll now silently return 1, so indistinguishabl from typing it properly\nas pre-receive.\n\nAll that being said I think it's arguable that if we're going to allow\n\"git hook run blahblah\" that the die() in the base topic in my\n\"hook-list.h: add a generated list of hooks, like config-list.h\" is more\ntrouble than it's worth.\n\nI.e. do we really need to be concerned about new hooks being added and\nsomeone forgetting a githooks.txt update, or a typo in the git.git code\nthat nobody notices? Probably not.\n\nBut I think the change here is clearly broken vis-a-vis the stated goals\nof its commit message as it stands, i.e. \"[...]we do still want to keep\nknown_hooks() around, though[...]\". Should we fix it by adding a new\ninternal-only flag to the command, or just saying we shouldn't have the\nbehavior at all? What do you think.\n\nAside from that, this change seems to be untested, I tried making this\nnon-gentle for testing, and all tests still passed. I.e. we don't have\nany tests for running such a hook like mytool-validate-commit, but\nshould as part of this change.\n"},{"id":"433616","messageId":"87lf4qeh86.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210819033450.3382652-6-emilyshaffer@google.com","subject":"Re: [PATCH v3 5/6] hook: include hooks from the config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T19:30:25Z","receivedAt":"2021-08-24T20:10:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 18 2021, Emily Shaffer wrote:\n\n> Teach the hook.[hc] library to parse configs to populare the list of\n> hooks to run for a given event.\n\ns/populare/populate/\n\n> Multiple commands can be specified for a given hook by providing\n> multiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n> \"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be run in\n> config order of the \"hook.<name>.event\" lines.\n\nThe \"will be run in order\" probably needs some amending to account for\n--jobs, no?\n\n> For example:\n>\n>   $ git config --list | grep ^hook\n>   hook.bar.command=~/bar.sh\n>   hook.bar.event=pre-commit\n\nPerhaps the example should use:\n\n    git config --get-regexp '^hook\\.'\n\n>   $ git hook run\n>   # Runs ~/bar.sh\n>   # Runs .git/hooks/pre-commit\n\nAnd this \"# Runs\" is not actual output by git, but just an explanation\nfor what happens, better to reword it somehow so it doesn't give that\nimpression.\n\nBut the example also seems to be broken, surely it should be \"git hook\nrun bar\", not \"git hook run\"? Reading ahead, but presumably no-arg\ndoesn't run all known hooks...\n\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  Documentation/config/hook.txt |  18 ++++\n>  Documentation/git-hook.txt    |  57 ++++++++++++-\n>  builtin/hook.c                |   3 +-\n>  hook.c                        | 153 ++++++++++++++++++++++++++++++----\n>  hook.h                        |   7 +-\n>  t/t1800-hook.sh               | 141 ++++++++++++++++++++++++++++++-\n>  6 files changed, 357 insertions(+), 22 deletions(-)\n>\n> diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> index 96d3d6572c..c394756328 100644\n> --- a/Documentation/config/hook.txt\n> +++ b/Documentation/config/hook.txt\n> @@ -1,3 +1,21 @@\n> +hook.<name>.command::\n> +\tA command to execute whenever `hook.<name>` is invoked. `<name>` should\n> +\tbe a unique \"friendly\" name which you can use to identify this hook\n> +\tcommand. (You can specify when to invoke this command with\n> +\t`hook.<name>.event`.) The value can be an executable on your device or a\n> +\toneliner for your shell. If more than one value is specified for the\n> +\tsame `<name>`, the last value parsed will be the only command executed.\n> +\tSee linkgit:git-hook[1].\n\nHrm, so here we say \"If more than one value is specified for ... the\nlast value parsed will be the only command executed\", but in the commit\nmessage it's:\n\n    Multiple commands can be specified for a given hook by providing\n    multiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n    \"hook.<friendly-name>.event = <hook-event>\" lines.\n\nAs we've discussed earlier I've got a preference for the former, but\njust reading this commit message & doc the for the first time I still\ndon't know what you went for, looks like one or the other needs\nupdating. I'm intentionally not reading ahead as I review this.\n\nSaying that it's a \"unique name\", but also discussing what happens if\nit's not unique in the sense that we have multiple \"hook.<name>.*\" is a\nbit confusing. I think I know what you're going for, perhaps something\nlike this would be better to describe it:\n\n    For a \"hook.<name>.{command,event}\" hook entry you'll need to pick a\n    \"<name>\" that's not shared with any other hook, if you do normal\n    single-value config semantics apply and git will use the last\n    provided value.\n\nOr something...\n\n> +hook.<name>.event::\n> +\tThe hook events which should invoke `hook.<name>`. `<name>` should be a\n> +\tunique \"friendly\" name which you can use to identify this hook. The\np> +\tvalue should be the name of a hook event, like \"pre-commit\" or \"update\".\n> +\t(See linkgit:githooks[5] for a complete list of hooks Git knows about.)\n> +\tOn the specified event, the associated `hook.<name>.command` will be\n> +\texecuted. More than one event can be specified if you wish for\n> +\t`hook.<name>` to execute on multiple events. See linkgit:git-hook[1].\n> +\n>  hook.jobs::\n>  \tSpecifies how many hooks can be run simultaneously during parallelized\n>  \thook execution. If unspecified, defaults to the number of processors on\n> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> index d1db084e4f..9c6cbdc2eb 100644\n> --- a/Documentation/git-hook.txt\n> +++ b/Documentation/git-hook.txt\n> @@ -27,12 +27,65 @@ Git is unlikely to use for a native hook later on. For example, Git is much less\n>  likely to create a `mytool-validate-commit` hook than it is to create a\n>  `validate-commit` hook.\n>  \n> +This command parses the default configuration files for pairs of configs like\n> +so:\n> +\n> +  [hook \"linter\"]\n> +    event = pre-commit\n> +    command = ~/bin/linter --c\n> +\n> +In this example, `[hook \"linter\"]` represents one script - `~/bin/linter --c` -\n> +which can be shared by many repos, and even by many hook events, if appropriate.\n\nOK, so now it seems like \"hook.<name>.command\" is 1=1 for \"hook.<name>\"\nand \"command\", and \"hook.<name>.event\" is 1=many, maybe that's correct,\nbut reading on...\n\n> +Commands are run in the order Git encounters their associated\n> +`hook.<name>.event` configs during the configuration parse (see\n> +linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be\n> +added, only one `hook.linter.command` event is valid - Git uses \"last-one-wins\"\n> +to determine which command to run.\n\n...ah, and confirmed here...\n\n> +So if you wanted your linter to run when you commit as well as when you push,\n> +you would configure it like so:\n> +\n> +  [hook \"linter\"]\n> +    event = pre-commit\n> +    event = pre-push\n> +    command = ~/bin/linter --c\n> +\n> +With this config, `~/bin/linter --c` would be run by Git before a commit is\n> +generated (during `pre-commit`) as well as before a push is performed (during\n> +`pre-push`).\n\nAside: I know we're discussing a presumably imaginary linter, but it's a\nbit jarring to see \"--\" for a one-letter short-option, perhaps \"-c\" or\n\"--check\" for the example...\n\n> +And if you wanted to run your linter as well as a secret-leak detector during\n> +only the \"pre-commit\" hook event, you would configure it instead like so:\n> +\n> +  [hook \"linter\"]\n> +    event = pre-commit\n> +    command = ~/bin/linter --c\n> +  [hook \"no-leaks\"]\n> +    event = pre-commit\n> +    command = ~/bin/leak-detector\n\nI think these examples would flow a bit more naturally if we started by\ndiscussing how to setup one configured hook, then two unrelated hooks\n(perhaps a \"commit-msg\" and \"pre-commit\" hook), and then finally the\ncases where a given \"hook.<name>.command\" has multiple\n\"hook.<name>.event\" entries. My assumption in suggesting that is that\nthat's, respectively, the most common to less common use cases.\n\n> +With this config, before a commit is generated (during `pre-commit`), Git would\n> +first start `~/bin/linter --c` and second start `~/bin/leak-detector`. It would\n> +evaluate the output of each when deciding whether to proceed with the commit.\n> +\n> +For a full list of hook events which you can set your `hook.<name>.event` to,\n> +and how hooks are invoked during those events, see linkgit:githooks[5].\n\nLet's discuss what happens with unknown values for `hook.<name>.event`\nhere, i.e. just \"are ignored\".\n\n[I'll discuss my opinions on the new and revised config schema in\nanother mail, just commenting on the patch here in terms of its stated\ngoals].\n\n> +In general, when instructions suggest adding a script to\n> +`.git/hooks/<hook-event>`, you can specify it in the config instead by running\n> +`git config --add hook.<some-name>.command <path-to-script> && git config --add\n> +hook.<some-name>.event <hook-event>` - this way you can share the script between\n> +multiple repos. That is, `cp ~/my-script.sh ~/project/.git/hooks/pre-commit`\n> +would become `git config --add hook.my-script.command ~/my-script.sh && git\n> +config --add hook.my-script.event pre-commit`.\n\nI think this would benefit a lot from being split up into code example\nprose, so:\n\n    You can [...]\n\n    ----\n    git config -add hook.<some-name>.command <path-to-script> &&\n    git config -add hook.<some-name>.event <hook-event>\n    ----\n\nIt's more lines, but I think a lot more readable.\n\nI think the part of this that says \"You can share the script between\nmultiple repos\" could really use some elaboration.\n\nI.e. if the user is following an example where they'd otherwise edit\n.git/hooks/ and then just run \"git config\", they'll add it to\n.git/config.\n\nSo then it won't be shared between multiple repos, what I think this\nparagraph is trying to go for is that instead of say:\n\n    ln -s ~/my-script.sh .git/hooks/pre-commit\n\nYou can instead do, with configurable hooks:\n\n    git config hook.my-pre-commit.command ~/my-script.sh\n    git config --add hook.my-pre-commit.event pre-commit\n\nNotice how I omitted the --add for the first one. It's also confusing if\nwe're documenting \"*.command\" as \"last one wins\" to use --add with it,\nin an example that's discussing how to add it to some local repo, no?\n\nOr is this example really trying to discuss what would happen if you:\n\n    cat >~/my-script.sh\n    ...\n    ^C\n    chmod +x ~/my-script.sh\n    git config --global hook.my-pre-commit.command ~/my-script.sh\n    git config hook.my-pre-commit.event pre-commit\n\nThat's not clear to me, in any case, I think we'd do well to lead with\nthe former, and then discuss the latter. I.e. \"here's what you'd symlink\nbefore, now it's in config like this\", and then discuss how you'd set\n\"global\" or perhaps included config, which isn't possible with the\n.git/hooks/<script> mechanism.\n\n>  SUBCOMMANDS\n>  -----------\n>  \n>  run::\n> -\tRun the `<hook-name>` hook. See linkgit:githooks[5] for\n> -\tthe hook names we support.\n> +\tRuns hooks configured for `<hook-name>`, in the order they are\n> +\tdiscovered during the config parse.\n\nThis and any other things that discuss how hooks are run in detail\nshould really talk about the thing running .git/hooks/<name> *and* any\nconfigured hooks, and on the subject of --jobs and ordering we should\nalso talk about what the order of config v.s. .git/hooks/<name> is.\n\n> +\t/* to enable oneliners, let config-specified hooks run in shell.\n> +\t * config-specified hooks have a name. */\n\nnit: usual style is multi-line comments like:\n\n    /*\n     * some text[...]\n     * some more...\n     */\n\nNot:\n\n    /* text here right away[...]\n     * some more ... */\n\n\n> +\tcp->use_shell = !!run_me->name;\n> +\n>  \t/* add command */\n> -\tif (hook_cb->options->absolute_path)\n> -\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n> -\telse\n> -\t\tstrvec_push(&cp->args, run_me->hook_path);\n> +\tif (run_me->name) {\n> +\t\t/* ...from config */\n> +\t\tstruct strbuf cmd_key = STRBUF_INIT;\n> +\t\tchar *command = NULL;\n> +\n> +\t\tstrbuf_addf(&cmd_key, \"hook.%s.command\", run_me->name);\n\nMissing strbuf_release() for this later?\n\n> +\t\tif (git_config_get_string(cmd_key.buf, &command)) {\n> +\t\t\t/* TODO test me! */\n\n...seems easy enough to just have a test for..., i.e. an *.event entry\nwith no *.command.\n\n> +\t\t\tdie(_(\"'hook.%s.command' must be configured \"\n> +\t\t\t      \"or 'hook.%s.event' must be removed; aborting.\\n\"),\n> +\t\t\t    run_me->name, run_me->name);\n> +\t\t}\n> +\n> +\t\tstrvec_push(&cp->args, command);\n> +\t} else {\n> +\t\t/* ...from hookdir. */\n> +\t\tconst char *hook_path = NULL;\n> +\t\t/*\n> +\t\t *\n\nNit: Too few \\n before the text in a comment earlier, too many here :)\n\n> +\t\t * At this point we are already running, so don't validate\n> +\t\t * whether the hook name is known or not.\n\n...because it was done earlier somewhere, or...?\n\n> +\t\t */\n> +\t\thook_path = find_hook_gently(hook_cb->hook_name);\n> +\t\tif (!hook_path)\n> +\t\t\tBUG(\"hookdir hook in hook list but no hookdir hook present in filesystem\");\n> +\n> +\t\tif (hook_cb->options->absolute_path)\n> +\t\t\thook_path = absolute_path(hook_path);\n> +\n> +\t\tstrvec_push(&cp->args, hook_path);\n> +\t}\n> +\n>  \n>  \t/*\n>  \t * add passed-in argv, without expanding - let the user get back\n> @@ -228,8 +346,11 @@ static int notify_start_failure(struct strbuf *out,\n>  \n>  \thook_cb->rc |= 1;\n>  \n> -\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n> -\t\t    attempted->hook_path);\n> +\tif (attempted->name)\n> +\t\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n> +\t\t    attempted->name);\n> +\telse\n> +\t\tstrbuf_addstr(out, _(\"Couldn't start hook from hooks directory\\n\"));\n>  \n>  \treturn 1;\n>  }\n> diff --git a/hook.h b/hook.h\n> index 6b7b2d14d2..621bd2cde1 100644\n> --- a/hook.h\n> +++ b/hook.h\n> @@ -27,8 +27,11 @@ int hook_exists(const char *hookname);\n>  \n>  struct hook {\n>  \tstruct list_head list;\n> -\t/* The path to the hook */\n> -\tconst char *hook_path;\n> +\t/*\n> +\t * The friendly name of the hook. NULL indicates the hook is from the\n> +\t * hookdir.\n> +\t */\n> +\tconst char *name;\n>  \n>  \t/*\n>  \t * Use this to keep state for your feed_pipe_fn if you are using\n> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> index 217db848b3..ef2432f53a 100755\n> --- a/t/t1800-hook.sh\n> +++ b/t/t1800-hook.sh\n> @@ -1,13 +1,29 @@\n>  #!/bin/bash\n>  \n> -test_description='git-hook command'\n> +test_description='git-hook command and config-managed multihooks'\n>  \n>  . ./test-lib.sh\n>  \n> +setup_hooks () {\n> +\ttest_config hook.ghi.event pre-commit --add\n> +\ttest_config hook.ghi.command \"/path/ghi\" --add\n> +\ttest_config_global hook.def.event pre-commit --add\n> +\ttest_config_global hook.def.command \"/path/def\" --add\n\nIsn't --add redundant here? Seems no test is stressing multi-value\nhook.{ghi,def}.* from a quick glance...\n\n> +}\n> +\n> +setup_hookdir () {\n> +\tmkdir .git/hooks\n> +\twrite_script .git/hooks/pre-commit <<-EOF\n> +\techo \\\"Legacy Hook\\\"\n> +\tEOF\n> +\ttest_when_finished rm -rf .git/hooks\n> +}\n> +\n>  test_expect_success 'git hook usage' '\n>  \ttest_expect_code 129 git hook &&\n>  \ttest_expect_code 129 git hook run &&\n>  \ttest_expect_code 129 git hook run -h &&\n> +\ttest_expect_code 129 git hook list -h &&\n\nDoesn't this belong in a previous commit that added \"git hook list\", not\nhere?\n\n>  \ttest_expect_code 129 git hook run --unknown 2>err &&\n>  \tgrep \"unknown option\" err\n>  '\n> @@ -153,4 +169,127 @@ test_expect_success 'stdin to hooks' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> +test_expect_success 'git hook list orders by config order' '\n> +\tsetup_hooks &&\n> +\n> +\tcat >expected <<-EOF &&\n> +\tdef\n> +\tghi\n> +\tEOF\n> +\n> +\tgit hook list pre-commit >actual &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'git hook list reorders on duplicate event declarations' '\n> +\tsetup_hooks &&\n> +\n> +\t# 'def' is usually configured globally; move it to the end by\n> +\t# configuring it locally.\n> +\ttest_config hook.def.event \"pre-commit\" --add &&\n\nAh, well the --add belongs here, but not needed in setup_hooks, right?\n\n> +\n> +\tcat >expected <<-EOF &&\n> +\tghi\n> +\tdef\n> +\tEOF\n> +\n> +\tgit hook list pre-commit >actual &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'git hook list shows hooks from the hookdir' '\n> +\tsetup_hookdir &&\n> +\n> +\tcat >expected <<-EOF &&\n> +\thook from hookdir\n> +\tEOF\n> +\n> +\tgit hook list pre-commit >actual &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'inline hook definitions execute oneliners' '\n> +\ttest_config hook.oneliner.event \"pre-commit\" &&\n> +\ttest_config hook.oneliner.command \"echo \\\"Hello World\\\"\" &&\n> +\n> +\techo \"Hello World\" >expected &&\n> +\n> +\t# hooks are run with stdout_to_stderr = 1\n> +\tgit hook run pre-commit 2>actual &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'inline hook definitions resolve paths' '\n> +\twrite_script sample-hook.sh <<-EOF &&\n> +\techo \\\"Sample Hook\\\"\n> +\tEOF\n> +\n> +\ttest_when_finished \"rm sample-hook.sh\" &&\n> +\n> +\ttest_config hook.sample-hook.event pre-commit &&\n> +\ttest_config hook.sample-hook.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n> +\n> +\techo \\\"Sample Hook\\\" >expected &&\n> +\n> +\t# hooks are run with stdout_to_stderr = 1\n> +\tgit hook run pre-commit 2>actual &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'hookdir hook included in git hook run' '\n> +\tsetup_hookdir &&\n> +\n> +\techo \\\"Legacy Hook\\\" >expected &&\n> +\n> +\t# hooks are run with stdout_to_stderr = 1\n> +\tgit hook run pre-commit 2>actual &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'stdin to multiple hooks' '\n> +\ttest_config hook.stdin-a.event \"test-hook\" --add &&\n> +\ttest_config hook.stdin-a.command \"xargs -P1 -I% echo a%\" --add &&\n> +\ttest_config hook.stdin-b.event \"test-hook\" --add &&\n> +\ttest_config hook.stdin-b.command \"xargs -P1 -I% echo b%\" --add &&\n> +\n> +\tcat >input <<-EOF &&\n> +\t1\n> +\t2\n> +\t3\n> +\tEOF\n> +\n> +\tcat >expected <<-EOF &&\n> +\ta1\n> +\ta2\n> +\ta3\n> +\tb1\n> +\tb2\n> +\tb3\n> +\tEOF\n\nFor any here-docs without variables, use <<-\\EOF, note the backslash.\n"},{"id":"433617","messageId":"87ilzueh1g.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210819033450.3382652-7-emilyshaffer@google.com","subject":"Re: [PATCH v3 6/6] hook: allow out-of-repo 'git hook' invocations","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T20:12:04Z","receivedAt":"2021-08-24T20:14:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 18 2021, Emily Shaffer wrote:\n\n> Since hooks can now be supplied via the config, and a config can be\n> present without a gitdir via the global and system configs, we can start\n> to allow 'git hook run' to occur without a gitdir. This enables us to do\n> things like run sendemail-validate hooks when running 'git send-email'\n> from a nongit directory.\n\nSensible goal. Perhaps we should note in an earlier commit when\nconfig-based hooks are introduced something like:\n\n    Even though we've added config-based hooks, they currently only work\n    if we can find a .git directory, even though certain commands such\n    as \"git send-email\" (or only that command?) can be run outside of a\n    git directory. A subsequent commit will address that edge-case.\n\n> [...]\n> Notes:\n>     For hookdir hooks, do we want to run them in nongit dir when core.hooksPath\n>     is set? For example, if someone set core.hooksPath in their global config and\n>     then ran 'git hook run sendemail-validate' in a nongit dir?\n> [...]\n>  git.c           |  2 +-\n>  hook.c          |  2 +-\n>  t/t1800-hook.sh | 20 +++++++++++++++-----\n>  3 files changed, 17 insertions(+), 7 deletions(-)\n>\n> diff --git a/git.c b/git.c\n> index 540909c391..39988ee3b0 100644\n> --- a/git.c\n> +++ b/git.c\n> @@ -538,7 +538,7 @@ static struct cmd_struct commands[] = {\n>  \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n>  \t{ \"hash-object\", cmd_hash_object },\n>  \t{ \"help\", cmd_help },\n> -\t{ \"hook\", cmd_hook, RUN_SETUP },\n> +\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n>  \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n>  \t{ \"init\", cmd_init_db },\n>  \t{ \"init-db\", cmd_init_db },\n> diff --git a/hook.c b/hook.c\n> index 581d87cbbd..2e08156546 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -218,7 +218,7 @@ struct list_head *list_hooks_gently(const char *hookname)\n>  \n>  \t/* Add the hook from the hookdir. The placeholder makes it easier to\n>  \t * allocate work in pick_next_hook. */\n> -\tif (find_hook_gently(hookname))\n> +\tif (have_git_dir() && find_hook_gently(hookname))\n>  \t\tappend_or_move_hook(hook_head, NULL);\n>  \n>  \treturn hook_head;\n> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> index ef2432f53a..a7e45c0d16 100755\n> --- a/t/t1800-hook.sh\n> +++ b/t/t1800-hook.sh\n> @@ -118,15 +118,25 @@ test_expect_success 'git hook run -- pass arguments' '\n>  \ttest_cmp expect actual\n>  '\n>  \n> -test_expect_success 'git hook run -- out-of-repo runs excluded' '\n> -\twrite_script .git/hooks/test-hook <<-EOF &&\n> -\techo Test hook\n> -\tEOF\n> +test_expect_success 'git hook run: out-of-repo runs execute global hooks' '\n> +\ttest_config_global hook.global-hook.event test-hook --add &&\n> +\ttest_config_global hook.global-hook.command \"echo no repo no problems\" --add &&\n> +\n> +\techo \"global-hook\" >expect &&\n> +\tnongit git hook list test-hook >actual &&\n> +\ttest_cmp expect actual &&\n> +\n> +\techo \"no repo no problems\" >expect &&\n>  \n> -\tnongit test_must_fail git hook run test-hook\n> +\tnongit git hook run test-hook 2>actual &&\n> +\ttest_cmp expect actual\n>  '\n>  \n>  test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n> +\twrite_script .git/hooks/test-hook <<-EOF &&\n> +\techo Test hook\n> +\tEOF\n> +\n>  \tmkdir my-hooks &&\n>  \twrite_script my-hooks/test-hook <<-\\EOF &&\n>  \techo Hook ran $1 >>actual\n\nIf the only user of this is git-send-email, let's have tests for this in\nt/t9001-send-email.sh. That should also address your \"Notes\" above,\ni.e. let's just test it with core.hooksPath and see what the interaction\nlooks like.\n"},{"id":"433620","messageId":"033201d79927$f5bfa1f0$e13ee5d0$@nexbridge.com","threadId":"55286","inReplyTo":"87ilzueh1g.fsf@evledraar.gmail.com","subject":"RE: [PATCH v3 6/6] hook: allow out-of-repo 'git hook' invocations","fromName":"Randall S. Becker","fromEmail":"rsbecker@nexbridge.com","sentAt":"2021-08-24T20:38:08Z","receivedAt":"2021-08-24T20:38:19Z","isPatch":true,"sender":{"key":"randall.becker@nexbridge.ca","avatar":"https://avatars.githubusercontent.com/u/28956764?v=4"},"body":"On August 24, 2021 4:12 PM, Ævar Arnfjörð Bjarmason wrote:\n>On Wed, Aug 18 2021, Emily Shaffer wrote:\n>\n>> Since hooks can now be supplied via the config, and a config can be\n>> present without a gitdir via the global and system configs, we can\n>> start to allow 'git hook run' to occur without a gitdir. This enables\n>> us to do things like run sendemail-validate hooks when running 'git send-email'\n>> from a nongit directory.\n>\n>Sensible goal. Perhaps we should note in an earlier commit when config-based hooks are introduced something like:\n\nTo clarify the requirements here, if running without a gitdir (and thus without a repository?) how will front-ends know what to supply? Will this just be \"some shell script\" that runs?\n>\n>    Even though we've added config-based hooks, they currently only work\n>    if we can find a .git directory, even though certain commands such\n>    as \"git send-email\" (or only that command?) can be run outside of a\n>    git directory. A subsequent commit will address that edge-case.\n\nSo we cannot assume anything about the repository, correct? Similar to running git version but not git status?\n\n>> [...]\n>> Notes:\n>>     For hookdir hooks, do we want to run them in nongit dir when core.hooksPath\n>>     is set? For example, if someone set core.hooksPath in their global config and\n>>     then ran 'git hook run sendemail-validate' in a nongit dir?\n\nSo this is complete consent to run outside of git? I wonder whether there needs to be an attribute associated with the hook that enables this edge capability. That way we can validate whether the hook should be run or not (from front-end scripts).\n\n"},{"id":"433625","messageId":"87fsuyeds6.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"20210819033450.3382652-1-emilyshaffer@google.com","subject":"Re: [PATCH v3 0/6] config-based hooks restarted","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T20:29:33Z","receivedAt":"2021-08-24T21:25:18Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Aug 18 2021, Emily Shaffer wrote:\n\n> This is the config-based hooks topic rebased onto v4 of Ævar's\n> branch[1]. There is a happy CI build of it on GitHub[2].\n> [...]\n> Right now I'm trying to focus on this series first and foremost, hence\n> sending two rerolls based on the same version of Ævar's base restart.\n> I'll try to perform a code review on Ævar's latest tomorrow.\n> [...]\n\nFirst thank for the review on the base topic at\nhttps://lore.kernel.org/git/YR2jLdYQA5CVzX5h@google.com/ &\nhttps://lore.kernel.org/git/YR7r3h1AG4Zyn7x7@google.com/ and related.\n\nI'll re-roll it soon pending your feedback on comments I left on this\nseries that pertain to it. I.e. there's some things that I think are\nbetter just fixed up in the base topic (from trivial things like\nwhitespace changes, to some behavior changes), makes for easier reading\nrather than going back & forth between the two.\n\nI've read this whole series through, as promised in\nhttps://lore.kernel.org/git/87lf4qeh86.fsf@evledraar.gmail.com/:\n\n    [I'll discuss my opinions on the new and revised config schema in\n    another mail, just commenting on the patch here in terms of its stated\n    goals].\n\nI.e. here's some general comments, numbered for ease of reference:\n\n0)\n\nI think this is in much much better shape vis-as-vis the simplified\nconfig schema that's now being proposed re our discussion starting\naround https://lore.kernel.org/git/87bl6ttmlv.fsf@evledraar.gmail.com/\n\nI.e. the main complexity of the \"skip\" mechanism is gone, and also the\nconflation of hook names with hook commands (the \"rm -rf /\" as a\n<name-or-cmd> discussed in the above).\n\nSo before going any further, I'll just say that I wouldn't object much\nto this design going in as-is. What I'm about to mention here below is\nmuch closer to bikeshedding in my mind than \"this is really to complex\nto go in-tree\", which was my opinion of the config schema before.\n\n1)\n\nOn the current config design: First, before going into any detail on\nthat, I think whatever anyone's opinion is on that that the\ndesign-focused patches as they stand could really use more a more\nextended discussion of the design.\n\nI.e. talk about the previously considered config schema, why it evolved\ninto its current form. The trade-offs involved, and why the patch\nproposed to implement the schema it's implementing over another earlier\nor alternate design.\n\nI.e. https://lore.kernel.org/git/20210819033450.3382652-6-emilyshaffer@google.com\nis two very short paragraphs. We won't be able to summarize all our\nmonth-long discussion on the config design in one commit message, but I\nthink at least discussing it somewhat / linking to relevant on-list\ndiscussions would make future source spelunking easier.\n\n2)\n\nSo that out of the way, a comment on the current config design, which\nshould be read in the context of what I noted in #0. I.e. I'm *much*\nhappier with this version.\n\nThat being said I'm still not convinced that the simple 1=1 mapping of\n\"hook.<name>.command\" and its \"value\" should be followed by the 1=many\nmapping of \"hook.<name>.event\" and its \"value\".\n\nI.e. we're making the trade-off of saving the user from typing out or\nspecifying:\n    \n    [hook \"my-pre-commit\"]\n    command = ~/hooks/pre-commit-or-push\n    event = pre-commit\n    [hook \"my-pre-push\"]\n    command = ~/hooks/pre-commit-or-push\n    event = pre-push\n\nAnd instead being able to do:\n\n    [hook \"my-pre-commit-or-push\"]\n    command = ~/hooks/pre-commit-or-push\n    event = pre-commit\n    event = pre-push\n\nSo for the very common case, saving two config lines. \"Two\" because as\nwe discussed[1] as there's currently no GIT_HOOK_TYPE env var. So this\nform will work pretty much only for that case.\n\nI.e. unlike with .git/hook/<name> the hook run via config can't\ndetermine what <hook-type> it's being run at, so as it stands this is\nonly useful for those hooks listed in githooks(5) where someone would\nwant to do the exact same thing for one or more <hook-name> names. You\ncan't use it as a general routing mechanism for any hook type as it\nstands.\n\nI *think* that's only these two, perhaps \"update\" and \"pre-receive\",\nwith the hook seeing if it consume stdin/has arguments to disambiguate\nthe two.\n\nBut even with a GIT_HOOK_TYPE passed the trade-off, as discussed in [1],\nand downthread in [2], is that by having it 1=many we're closing the\ndoor on any future hook.<name>.<whatever>. I.e. config that would change\nthe behavior of that hook, but you'd want to change it in another way\nfor at least one of the N event types.\n\nWell, \"closing the door\" as in if you'd want that you'd have to split up\nthe section from the \"my-pre-commit-or-push\" example above to the\n\"my-pre-commit\" and \"my-pre-push\" example.\n\nBut again, on the \"is the complexity worth it\" we're then having to\nexplain to users that they can do it one way if the want no config other\nthan hook.<name>.{command,event*}, but another if they have another key\nin that namespace.\n\nYou've said that you wanted to add something like a GIT_HOOK_TYPE\nenvironment variable. Fair enough, and I guess we could add it in a\nre-roll of this series. I'm mainly commenting on the end-state of *this*\nseries in particular. I.e. I think it leaves the user & implementation\nwith a config schema that still seems to be needlessly complex for the\nvery limited benefits that complexity brings us in what you're able to\ndo with it now.\n\nBut some of that goes back to the comments I had on 5/6[3], i.e. I'm\nwilling to be convinced, but I think that the current commit message &\nadded docs aren't really selling the idea of why it's worth it.\n\n3) \n\nAs an extension to my comments on 5/6[3], I think this whole notion of\n\"git hook run <name>\" as invoked by a user of git is just more confusing\nthe more I think about it.\n\nI.e. 5/6[4] is apparently seeking to implement a way to just make that\nfacility a general way for users to run some command on their system to\ndo whatever, instead of say using /usr/bin/parallel or a shell alias.\n\nBut then we also use that command as our own dispatch mechanism for our\nown known hooks, except mostly not, since we mostly use the C API\nourselves directly.\n\nIt's particularly confusing that if you say run \"git hook run\npre-auto-gc\" as a user to test your hook you'll have that hook run in\nthe same way that git-gc(1) would run it. So someone developing a hook\nmight think they can use \"git hook run\" for testing it.\n\nBut if you do the same with say \"git hook run pre-receive\" or anything\nelse that feeds arguments or stdin (e.g. \"update\", or \"pre-receive\"),\nyou'll have your hook happily being run by git, but in a way that's not\nat all how such a hook will be run when it's run by git \"for real\".\n\nSo I wonder if we shouldn't just have the thing die() if you try to run\nany hook that's in githooks(5) itself, except for sendemail-validate and\nthe p4 hooks, since we need to run those ourselves.\n\nOr have those use an internal-only \"git hook--helper\", and start out\nwith \"git hook\" just supporting \"git hook list\", and then later on have\n\"git hook run\" (or perhaps \"git hook run-configured\"?) be an entry point\nfor this facility of running some arbitrary script that's not a \"real\"\nhook.\n\nI don't know, maybe I'm the only one that finds this confusing...\n\n1. https://lore.kernel.org/git/87bl6ttmlv.fsf@evledraar.gmail.com/\n2. https://lore.kernel.org/git/877dh0n1b3.fsf@evledraar.gmail.com/\n3. https://lore.kernel.org/git/87lf4qeh86.fsf@evledraar.gmail.com/\n4. https://lore.kernel.org/git/20210819033450.3382652-6-emilyshaffer@google.com/\n"},{"id":"433676","messageId":"87v93ucv5k.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"033201d79927$f5bfa1f0$e13ee5d0$@nexbridge.com","subject":"Re: [PATCH v3 6/6] hook: allow out-of-repo 'git hook' invocations","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-24T22:45:57Z","receivedAt":"2021-08-24T22:52:59Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Tue, Aug 24 2021, Randall S. Becker wrote:\n\n> On August 24, 2021 4:12 PM, Ævar Arnfjörð Bjarmason wrote:\n>>On Wed, Aug 18 2021, Emily Shaffer wrote:\n>>\n>>> Since hooks can now be supplied via the config, and a config can be\n>>> present without a gitdir via the global and system configs, we can\n>>> start to allow 'git hook run' to occur without a gitdir. This enables\n>>> us to do things like run sendemail-validate hooks when running 'git send-email'\n>>> from a nongit directory.\n>>\n>>Sensible goal. Perhaps we should note in an earlier commit when config-based hooks are introduced something like:\n>\n> To clarify the requirements here, if running without a gitdir (and\n> thus without a repository?) how will front-ends know what to supply?\n> Will this just be \"some shell script\" that runs?\n\nEmily's also aiming to have \"git hook run\" running \"some shell script\",\nbut in this case we're talking about any git program that runs hooks,\nbut doesn't require a repo. I think the only one is git-send-email's\nsendemail-validate hook.\n\n>>\n>>    Even though we've added config-based hooks, they currently only work\n>>    if we can find a .git directory, even though certain commands such\n>>    as \"git send-email\" (or only that command?) can be run outside of a\n>>    git directory. A subsequent commit will address that edge-case.\n>\n> So we cannot assume anything about the repository, correct? Similar to running git version but not git status?\n\nRight, with RUN_SETUP_GENTLY we may not have a repo at all, so like\ncommands such as \"git bundle\", \"git config\", \"git ls-remote\" etc.\n\n>>> [...]\n>>> Notes:\n>>>     For hookdir hooks, do we want to run them in nongit dir when core.hooksPath\n>>>     is set? For example, if someone set core.hooksPath in their global config and\n>>>     then ran 'git hook run sendemail-validate' in a nongit dir?\n>\n> So this is complete consent to run outside of git? I wonder whether\n> there needs to be an attribute associated with the hook that enables\n> this edge capability. That way we can validate whether the hook should\n> be run or not (from front-end scripts).\n\nIf I understand you correctly you're pointing out that anyone with a\nsendemail-validate script could previously have assumed a repo, but not\nanymore. So e.g. someone who always has other config the hook requires\nwhen they run it in repository might unexpectedly have that hook fail if\nthey naïvely set the config for that sendemail-validate hook via \"git\nconfig --global\".\n\nI think that's a good point, and one I hadn't really considered. I think\nit's probably best to just document this edge case for the one (or few?)\nhooks that have this caveat than to not support it. I.e. it seems useful\nto have a sendemail-validate without a repo (just to parse/validate the\nabout-to-be-sent-email), that we needed a repo before was really only an\nemergent effect.\n\nAlso, I think it's not new with this config-based hook mechanism, but\nsomething that I introduced in back when I added core.hooksPath (but I\ndon't think I realized I was adding this edge case at the time).\n"},{"id":"433880","messageId":"YSgEw1D3u+MIPx2t@google.com","threadId":"55286","inReplyTo":"87zgt6evkf.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 1/6] hook: run a list of hooks instead","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-26T21:16:51Z","receivedAt":"2021-08-26T21:17:00Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 24, 2021 at 04:56:10PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Wed, Aug 18 2021, Emily Shaffer wrote:\n> \n> > @@ -25,7 +25,8 @@ static int run(int argc, const char **argv, const char *prefix)\n> >  \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n> >  \tint ignore_missing = 0;\n> >  \tconst char *hook_name;\n> > -\tconst char *hook_path;\n> > +\tstruct list_head *hooks;\n> > +\n> >  \tstruct option run_options[] = {\n> >  \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n> >  \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n> \n> In general in this patch series there's a bunch of little whitespace\n> changes like that along with other changes. I think it's probably best\n> if I just absorb that in the \"base\" topic instead of doing them\n> here. E.g. in this case we could also add a line between \"struct option\"\n> and the rest.\n> \n> I don't mind either way, but the whitespace churn makes for distracting\n> reading...\n\nAh, hm. I don't know if in this specific case it's necessary for me to\neven have this whitespace change, since 'run_options' is still a struct\ndeclaration. I'll just drop this one, but in general whichever\nwhitespace bits you like from this topic, feel free to absorb. Will make\na note to scan through the diff when I rebase onto your next reroll\nchecking for spurious whitespace changes.\n\n> \n> > @@ -58,15 +59,16 @@ static int run(int argc, const char **argv, const char *prefix)\n> >  \tgit_config(git_default_config, NULL);\n> >  \n> >  \thook_name = argv[0];\n> > -\tif (ignore_missing)\n> > -\t\treturn run_hooks_oneshot(hook_name, &opt);\n> > -\thook_path = find_hook(hook_name);\n> > -\tif (!hook_path) {\n> > +\thooks = list_hooks(hook_name);\n> > +\tif (list_empty(hooks)) {\n> > +\t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n> > +\t\tif (ignore_missing)\n> > +\t\t\treturn 0;\n> >  \t\terror(\"cannot find a hook named %s\", hook_name);\n> >  \t\treturn 1;\n> >  \t}\n> >  \n> > -\tret = run_hooks(hook_name, hook_path, &opt);\n> > +\tret = run_hooks(hook_name, hooks, &opt);\n> >  \trun_hooks_opt_clear(&opt);\n> >  \treturn ret;\n> \n> This memory management is a bit inconsistent. So here we list_hooks(),\n> pass it to run_hooks(), which clears it for us, OK...\n> \n> > -int run_hooks(const char *hook_name, const char *hook_path,\n> > -\t      struct run_hooks_opt *options)\n> > +int run_hooks(const char *hook_name, struct list_head *hooks,\n> > +\t\t    struct run_hooks_opt *options)\n> >  {\n> > -\tstruct strbuf abs_path = STRBUF_INIT;\n> > -\tstruct hook my_hook = {\n> > -\t\t.hook_path = hook_path,\n> > -\t};\n> >  \tstruct hook_cb_data cb_data = {\n> >  \t\t.rc = 0,\n> >  \t\t.hook_name = hook_name,\n> > @@ -197,11 +241,9 @@ int run_hooks(const char *hook_name, const char *hook_path,\n> >  \tif (!options)\n> >  \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n> >  \n> > -\tif (options->absolute_path) {\n> > -\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n> > -\t\tmy_hook.hook_path = abs_path.buf;\n> > -\t}\n> > -\tcb_data.run_me = &my_hook;\n> > +\n> > +\tcb_data.head = hooks;\n> > +\tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n> >  \n> >  \trun_processes_parallel_tr2(jobs,\n> >  \t\t\t\t   pick_next_hook,\n> > @@ -213,18 +255,15 @@ int run_hooks(const char *hook_name, const char *hook_path,\n> >  \t\t\t\t   \"hook\",\n> >  \t\t\t\t   hook_name);\n> >  \n> > -\n> > -\tif (options->absolute_path)\n> > -\t\tstrbuf_release(&abs_path);\n> > -\tfree(my_hook.feed_pipe_cb_data);\n> > +\tclear_hook_list(hooks);\n> >  \n> >  \treturn cb_data.rc;\n> >  }\n> \n> Which we can see here will call clear_hook_list(), so far so good, but then...\n> \n> >  int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n> >  {\n> > -\tconst char *hook_path;\n> > -\tint ret;\n> > +\tstruct list_head *hooks;\n> > +\tint ret = 0;\n> >  \tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n> >  \n> >  \tif (!options)\n> > @@ -233,14 +272,19 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n> >  \tif (options->path_to_stdin && options->feed_pipe)\n> >  \t\tBUG(\"choose only one method to populate stdin\");\n> >  \n> > -\thook_path = find_hook(hook_name);\n> > -\tif (!hook_path) {\n> > -\t\tret = 0;\n> > +\thooks = list_hooks(hook_name);\n> > +\n> > +\t/*\n> > +\t * If you need to act on a missing hook, use run_found_hooks()\n> > +\t * instead\n> > +\t */\n> > +\tif (list_empty(hooks))\n> >  \t\tgoto cleanup;\n> > -\t}\n> >  \n> > -\tret = run_hooks(hook_name, hook_path, options);\n> > +\tret = run_hooks(hook_name, hooks, options);\n> > +\n> >  cleanup:\n> >  \trun_hooks_opt_clear(options);\n> > +\tclear_hook_list(hooks);\n> \n> ...the oneshot command also does clear_hook_list(), after calling\n> run_hooks() which cleared it already.  That looks like a mistake,\n> i.e. we should always trust run_hooks() to clear it, no?\n\nAh, good catch. I will update the comment on run_hooks() and fix\n_oneshot() :)\n\n - Emily\n"},{"id":"433885","messageId":"YSgK5Q4uqX8RPXXZ@google.com","threadId":"55286","inReplyTo":"87tujeev1h.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 3/6] hook: introduce \"git hook list\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-26T21:43:01Z","receivedAt":"2021-08-26T21:43:08Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 24, 2021 at 05:08:25PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Wed, Aug 18 2021, Emily Shaffer wrote:\n> \n> > +static int list(int argc, const char **argv, const char *prefix)\n> > +{\n> > +\tstruct list_head *head, *pos;\n> > +\tconst char *hookname = NULL;\n> > +\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n> > +\n> > +\tstruct option list_options[] = {\n> > +\t\tOPT_END(),\n> > +\t};\n> > +\n> > +\targc = parse_options(argc, argv, prefix, list_options,\n> > +\t\t\t     builtin_hook_list_usage, 0);\n> > +\n> > +\tif (argc < 1)\n> > +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n> > +\t\t\t      builtin_hook_list_usage, list_options);\n> \n> Untested, but aren't we silently ignoring:\n> \n>     git hook list pre-receive some extra gar bage here\n> \n> I.e. shouldn't this be an \"argc != 1\" check?\n\nYeah, I think you are right. Will switch.\n\n> \n> > +\n> > +\thookname = argv[0];\n> > +\n> > +\thead = hook_list(hookname);\n> > +\n> > +\tif (list_empty(head))\n> > +\t\treturn 1;\n> > +\n> > +\tlist_for_each(pos, head) {\n> > +\t\tstruct hook *item = list_entry(pos, struct hook, list);\n> > +\t\titem = list_entry(pos, struct hook, list);\n> > +\t\tif (item)\n> > +\t\t\tprintf(\"%s\\n\", item->hook_path);\n> \n> Nit/suggestion: use puts(x) instead of printf(\"%s\\n\", x), but that's\n> also a bikeshedding/style preference, so ignore if you disagree...\n\nI was curious, because today I learned about puts() ;), so I checked\n(sorry for escape gore):\n\n  $ gg puts\\( | wc -l\n  217\n  $ gg \"printf(\\\"%s\\(\\\\\\\\n\\)\\?\\\"\" | wc -l\n  96\n\nSo looks like it is indeed more idiomatic by about 2x to just use\nputs(). Will switch.\n\n> \n> > +\t}\n> > +\n> > +\tclear_hook_list(head);\n> \n> Nit/API suggestion: Maybe s/list_for_each/list_for_each_safe/ and\n> remove_hook() in the loop would make more sense for this one-shot caller\n> than iterating over the list twice?\n> \n> Anyway, currently remove_hook() is static, and it's probably good to not\n> peek behind the curtain here, so on second thought clear_hook_list() is\n> probably best...\n\nSounds like you talked yourself out of it before I could. Noop ;)\n\n> \n> > +\tstrbuf_release(&hookdir_annotation);\n> \n> This function did nothing with hookdir_annotation. Looks like leftover\n> WIP code, but maybe it's used in (and should be moved to) a later\n> commit, let's keep reading...\n\nAh, leftover WIP code indeed. Will drop it. I do think, though, that\n\"hook from hookdir\" is an ugly thing to say in list(), so any better\nsuggestions welcome.\n\n> \n> > [...]\n> >  \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n> >  \n> > +\n> >  \tINIT_LIST_HEAD(hook_head);\n> \n> ..ditto...\nACK\n\n> >  \n> >  \tif (!hookname)\n> > @@ -103,8 +104,6 @@ struct list_head *list_hooks(const char *hookname)\n> >  \n> >  \tif (have_git_dir()) {\n> >  \t\tconst char *hook_path = find_hook(hookname);\n> > -\n> \n> ... earlier notes about whitespace churn...\n> \n> > -\t\t/* Add the hook from the hookdir */\n> >  \t\tif (hook_path) {\n> >  \t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n> >  \t\t\tto_add->hook_path = hook_path;\n> \n\nThanks.\n - Emily\n"},{"id":"433888","messageId":"YSgXarfqwQVmO8Af@google.com","threadId":"55286","inReplyTo":"87wnoaevbe.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 2/6] hook: allow parallel hook execution","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-26T22:36:26Z","receivedAt":"2021-08-26T22:36:34Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 24, 2021 at 05:01:09PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Wed, Aug 18 2021, Emily Shaffer wrote:\n> \n> > -'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n> > +'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n> > +\t<hook-name> [-- <hook-args>]\n> \n> As an aside I wondered if it shouldn't be [[-j|--jobs] <n>], but grepped\n> around and found that (x|y|z) means a mandatory pick of x, y or z, but\n> [x|y|z] means that, plus possibly picking none, I think.\n> \n> So this is fine, just something I wondered about...\n\nHm, I guess I hadn't see [x|y|z] (or paid attention to it). Anyway, yes,\nI think [(-j|--jobs) <n>] is probably most correct here: \"entirely\noptionally you can provide a jobs arg; but if you do it must start with\n-j or --jobs and it must contain a number\". I guess you could also say\n[-j <n> | --jobs <n>] but that might leave the reader to consider\nwhether <n> has a different meaning in each? Anyway, doesn't matter\nreally, and thanks for completely nerd-sniping me ;)\n\n> \n> > +-j::\n> > +--jobs::\n> > +\tOnly valid for `run`.\n> > ++\n> > +Specify how many hooks to run simultaneously. If this flag is not specified, use\n> > +the value of the `hook.jobs` config. If the config is not specified, use the\n> \n> s/use the value/uses the value/\n> \n> Also we usually say \"of the XYZ config, see linkgit:git-config[1]\", or\n> something to that effect when we mention config variables. Perhaps we\n> should do the same here.\n\nDone, thanks.\n\n> \n> > +number of CPUs on the current system. Some hooks may be ineligible for\n> > +parallelization: for example, 'commit-msg' intends hooks modify the commit\n> > +message body and cannot be parallelized.\n> \n> Not something that *needs* to happen in this series, but I wonder if we\n> shouldn't have per-type config here too, so users could force it even\n> for those hook types if they want.\n\nYeah, I'm happy to implement that later when someone complains... ;)\n\n> \n> > -#define RUN_HOOKS_OPT_INIT { \\\n> > -\t.env = STRVEC_INIT, \\\n> > -\t.args = STRVEC_INIT, \\\n> > -}\n> > -\n> >  /*\n> >   * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n> >   * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n> > @@ -111,6 +113,18 @@ struct hook_cb_data {\n> >  \tint *invoked_hook;\n> >  };\n> >  \n> > +#define RUN_HOOKS_OPT_INIT_SERIAL { \\\n> > +\t.jobs = 1, \\\n> > +\t.env = STRVEC_INIT, \\\n> > +\t.args = STRVEC_INIT, \\\n> > +}\n> > +\n> > +#define RUN_HOOKS_OPT_INIT_PARALLEL { \\\n> > +\t.jobs = 0, \\\n> > +\t.env = STRVEC_INIT, \\\n> > +\t.args = STRVEC_INIT, \\\n> > +}\n> > +\n> \n> Ditto earlier comments about whitespace churn, i.e. I can just move this\n> around in the base topic, so the diff here is the change/addition, not\n> also moving things around.\n\nNah, I think I just wasn't paying much attention to where it goes. It\nmakes more sense to put this by the 'struct run_hooks_opt' decl, I'll\nmove mine instead.\n\n - Emily\n"},{"id":"433890","messageId":"YSgYA44krEdk6VFD@google.com","threadId":"55286","inReplyTo":"87r1eiet3o.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 3/6] hook: introduce \"git hook list\"","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-26T22:38:59Z","receivedAt":"2021-08-26T22:39:06Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 24, 2021 at 05:53:48PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Wed, Aug 18 2021, Emily Shaffer wrote:\n> \n> > +\thead = hook_list(hookname);\n> \n> This doesn't compile, as hook_list() is added in a later\n> commit. Something something earlier suggestion of \"git rebase -i --exec\n> 'make test'\" :)\n\n\nAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaargh. Thanks.\n"},{"id":"433891","messageId":"YSgatN5kYwLJtDMQ@google.com","threadId":"55286","inReplyTo":"87o89mej3y.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 4/6] hook: allow running non-native hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-26T22:50:28Z","receivedAt":"2021-08-26T22:50:36Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 24, 2021 at 05:55:13PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Wed, Aug 18 2021, Emily Shaffer wrote:\n> \n> > As the hook architecture and 'git hook run' become more featureful, we\n> > may find wrappers wanting to use the hook architecture to run their own\n> > hooks, thereby getting nice things like parallelism and idiomatic Git\n> > configuration for free. Enable this by letting 'git hook run' bypass the\n> > known_hooks() check.\n> >\n> > We do still want to keep known_hooks() around, though - by die()ing when\n> > an internal Git call asks for run_hooks(\"my-new-hook\"), we can remind\n> > Git developers to update Documentation/githooks.txt with their new hook,\n> > which in turn helps Git users discover this new hook.\n> >\n> > [...]\n> >\n> > +It's possible to use this command to refer to hooks which are not native to Git,\n> > +for example if a wrapper around Git wishes to expose hooks into its own\n> > +operation in a way which is already familiar to Git users. However, wrappers\n> > +invoking such hooks should be careful to name their hook events something which\n> > +Git is unlikely to use for a native hook later on. For example, Git is much less\n> > +likely to create a `mytool-validate-commit` hook than it is to create a\n> > +`validate-commit` hook.\n> > +\n> >  SUBCOMMANDS\n> >  -----------\n> \n> The goal here makes sense, but...\n> \n> > diff --git a/builtin/hook.c b/builtin/hook.c\n> > index d21f303eca..80397d39f5 100644\n> > --- a/builtin/hook.c\n> > +++ b/builtin/hook.c\n> > @@ -46,7 +46,7 @@ static int list(int argc, const char **argv, const char *prefix)\n> >  \n> >  \thookname = argv[0];\n> >  \n> > -\thead = hook_list(hookname);\n> > +\thead = list_hooks_gently(hookname);\n> >  \n> >  \tif (list_empty(head))\n> >  \t\treturn 1;\n> > @@ -105,7 +105,7 @@ static int run(int argc, const char **argv, const char *prefix)\n> >  \tgit_config(git_default_config, NULL);\n> >  \n> >  \thook_name = argv[0];\n> > -\thooks = list_hooks(hook_name);\n> > +\thooks = list_hooks_gently(hook_name);\n> >  \tif (list_empty(hooks)) {\n> >  \t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n> >  \t\tif (ignore_missing)\n> \n> This introduces a bug v.s. the previous state, e.g. before:\n> \n>     $ git hook run --ignore-missing foobar\n>     fatal: the hook 'foobar' is not known to git, should be in hook-list.h via githooks(5)\n> \n> But after we'll silently ignore it. I.e. we've conflated\n> --ignore-missing with a new and hypothetical (and this is now a synonym\n> of) --ignore-missing-and-allow-unknown-hook-names.\n> \n> So we've conflated the user's one-shot \"foobar\" script with wanting to\n> catch a typo in e.g. git-send-email.perl.\n> \n> Also instead of the user's typos being caught with a die (here using\n> your BUG(...) version):\n> \n>     $ git hook list pre-recive\n>     BUG: hook.c:115: Don't recognize hook event 'pre-recive'! Is it documented in Documentation/githooks.txt?\n>     Aborted\n> \n> We'll now silently return 1, so indistinguishabl from typing it properly\n> as pre-receive.\n> \n> All that being said I think it's arguable that if we're going to allow\n> \"git hook run blahblah\" that the die() in the base topic in my\n> \"hook-list.h: add a generated list of hooks, like config-list.h\" is more\n> trouble than it's worth.\n> \n> I.e. do we really need to be concerned about new hooks being added and\n> someone forgetting a githooks.txt update, or a typo in the git.git code\n> that nobody notices? Probably not.\n> \n> But I think the change here is clearly broken vis-a-vis the stated goals\n> of its commit message as it stands, i.e. \"[...]we do still want to keep\n> known_hooks() around, though[...]\". Should we fix it by adding a new\n> internal-only flag to the command, or just saying we shouldn't have the\n> behavior at all? What do you think.\n\nI think it's A) pretty important to make it easy for users to run\nwhatever not-necessarily-git-native hook they want, and B) useful for\nscript Git commands to take advantage of the typo check. So, I'll add a\n`--enforce-known-hookname` (or maybe a better named one, this isn't my\nstrong suit) and switch git-send-email and friends to use it. Like we\ndiscussed off-list, I think it's a good idea to drop the envvar for\nexceptional test names from the codebase entirely.\n\n> \n> Aside from that, this change seems to be untested, I tried making this\n> non-gentle for testing, and all tests still passed. I.e. we don't have\n> any tests for running such a hook like mytool-validate-commit, but\n> should as part of this change.\n\nSure.\n\n\nActually, I was in the middle of typing about how I wouldn't change your\n'test-hook' and so on tests, and it occurs to me that it might actually\nbe a better fit for your series to add this --reject-unknown (or\nwhatever) flag, instead of the envvar magics. So I'll hold off on making\nany changes unless I hear from you.\n\n - Emily\n"},{"id":"433894","messageId":"xmqq7dg7d9dc.fsf@gitster.g","threadId":"55286","inReplyTo":"YSgatN5kYwLJtDMQ@google.com","subject":"Re: [PATCH v3 4/6] hook: allow running non-native hooks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2021-08-27T00:22:39Z","receivedAt":"2021-08-27T00:22:43Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Emily Shaffer <emilyshaffer@google.com> writes:\n\n> I think it's A) pretty important to make it easy for users to run\n> whatever not-necessarily-git-native hook they want, and B) useful for\n> script Git commands to take advantage of the typo check. So, I'll add a\n> `--enforce-known-hookname` (or maybe a better named one, this isn't my\n> strong suit) and switch git-send-email and friends to use it.\n\nI somehow feel this is backwards.  \n\nOnce you write the invocation of \"git hook run <hookname>\" into your\nscript and tested it, there is no further need for typo checking.\n\nWhat's the use case you are trying to help with typo checking?  When\na script takes a hookname from the user and runs \"git hook run $1\",\nthen passing --this-must-be-a-known-hook option that errors out when\nthe named hook does not exist and unrecognised (there is no need to\nerror out if a hook with unusual name the user gave us does\nexist---the user asked us to run it, so we just can run it) might\nmake sense.  But I am somehow not getting the sense that it is the\nexpected use case you are worried about.\n\nIf the reason why you are making the typo-checking an opt-in feature\nis because you want to allow users to run \"git hook run\" with\nminimum typing, I suspect that you may be optimizing for a wrong\ncase.  Interactive users are the ones that benefit from\ntypo-checking the most, and if they are interactive (as opposed to\nbeing a script), they are flexible enough not to say \"git hook run\nfoobar\" when they know foobar does not exist and they know foobar is\nnot a generally accepted hook, no?  So, I think it makes more sense\nto by default allow a hook with a recognizable name to be missing,\nbut complain when a randomly named hook is missing, and to have an\noption that permits a hook to be unrecognised and missing.\n\n\n\n"},{"id":"433924","messageId":"8735qvyw0p.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YSgEw1D3u+MIPx2t@google.com","subject":"Re: [PATCH v3 1/6] hook: run a list of hooks instead","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-08-27T11:15:31Z","receivedAt":"2021-08-27T11:20:15Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Aug 26 2021, Emily Shaffer wrote:\n\n> On Tue, Aug 24, 2021 at 04:56:10PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> \n>> On Wed, Aug 18 2021, Emily Shaffer wrote:\n>> \n>> > @@ -25,7 +25,8 @@ static int run(int argc, const char **argv, const char *prefix)\n>> >  \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>> >  \tint ignore_missing = 0;\n>> >  \tconst char *hook_name;\n>> > -\tconst char *hook_path;\n>> > +\tstruct list_head *hooks;\n>> > +\n>> >  \tstruct option run_options[] = {\n>> >  \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n>> >  \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n>> \n>> In general in this patch series there's a bunch of little whitespace\n>> changes like that along with other changes. I think it's probably best\n>> if I just absorb that in the \"base\" topic instead of doing them\n>> here. E.g. in this case we could also add a line between \"struct option\"\n>> and the rest.\n>> \n>> I don't mind either way, but the whitespace churn makes for distracting\n>> reading...\n>\n> Ah, hm. I don't know if in this specific case it's necessary for me to\n> even have this whitespace change, since 'run_options' is still a struct\n> declaration. I'll just drop this one, but in general whichever\n> whitespace bits you like from this topic, feel free to absorb. Will make\n> a note to scan through the diff when I rebase onto your next reroll\n> checking for spurious whitespace changes.\n>\n>> \n>> > @@ -58,15 +59,16 @@ static int run(int argc, const char **argv, const char *prefix)\n>> >  \tgit_config(git_default_config, NULL);\n>> >  \n>> >  \thook_name = argv[0];\n>> > -\tif (ignore_missing)\n>> > -\t\treturn run_hooks_oneshot(hook_name, &opt);\n>> > -\thook_path = find_hook(hook_name);\n>> > -\tif (!hook_path) {\n>> > +\thooks = list_hooks(hook_name);\n>> > +\tif (list_empty(hooks)) {\n>> > +\t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n>> > +\t\tif (ignore_missing)\n>> > +\t\t\treturn 0;\n>> >  \t\terror(\"cannot find a hook named %s\", hook_name);\n>> >  \t\treturn 1;\n>> >  \t}\n>> >  \n>> > -\tret = run_hooks(hook_name, hook_path, &opt);\n>> > +\tret = run_hooks(hook_name, hooks, &opt);\n>> >  \trun_hooks_opt_clear(&opt);\n>> >  \treturn ret;\n>> \n>> This memory management is a bit inconsistent. So here we list_hooks(),\n>> pass it to run_hooks(), which clears it for us, OK...\n>> \n>> > -int run_hooks(const char *hook_name, const char *hook_path,\n>> > -\t      struct run_hooks_opt *options)\n>> > +int run_hooks(const char *hook_name, struct list_head *hooks,\n>> > +\t\t    struct run_hooks_opt *options)\n>> >  {\n>> > -\tstruct strbuf abs_path = STRBUF_INIT;\n>> > -\tstruct hook my_hook = {\n>> > -\t\t.hook_path = hook_path,\n>> > -\t};\n>> >  \tstruct hook_cb_data cb_data = {\n>> >  \t\t.rc = 0,\n>> >  \t\t.hook_name = hook_name,\n>> > @@ -197,11 +241,9 @@ int run_hooks(const char *hook_name, const char *hook_path,\n>> >  \tif (!options)\n>> >  \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n>> >  \n>> > -\tif (options->absolute_path) {\n>> > -\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n>> > -\t\tmy_hook.hook_path = abs_path.buf;\n>> > -\t}\n>> > -\tcb_data.run_me = &my_hook;\n>> > +\n>> > +\tcb_data.head = hooks;\n>> > +\tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n>> >  \n>> >  \trun_processes_parallel_tr2(jobs,\n>> >  \t\t\t\t   pick_next_hook,\n>> > @@ -213,18 +255,15 @@ int run_hooks(const char *hook_name, const char *hook_path,\n>> >  \t\t\t\t   \"hook\",\n>> >  \t\t\t\t   hook_name);\n>> >  \n>> > -\n>> > -\tif (options->absolute_path)\n>> > -\t\tstrbuf_release(&abs_path);\n>> > -\tfree(my_hook.feed_pipe_cb_data);\n>> > +\tclear_hook_list(hooks);\n>> >  \n>> >  \treturn cb_data.rc;\n>> >  }\n>> \n>> Which we can see here will call clear_hook_list(), so far so good, but then...\n>> \n>> >  int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n>> >  {\n>> > -\tconst char *hook_path;\n>> > -\tint ret;\n>> > +\tstruct list_head *hooks;\n>> > +\tint ret = 0;\n>> >  \tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n>> >  \n>> >  \tif (!options)\n>> > @@ -233,14 +272,19 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n>> >  \tif (options->path_to_stdin && options->feed_pipe)\n>> >  \t\tBUG(\"choose only one method to populate stdin\");\n>> >  \n>> > -\thook_path = find_hook(hook_name);\n>> > -\tif (!hook_path) {\n>> > -\t\tret = 0;\n>> > +\thooks = list_hooks(hook_name);\n>> > +\n>> > +\t/*\n>> > +\t * If you need to act on a missing hook, use run_found_hooks()\n>> > +\t * instead\n>> > +\t */\n>> > +\tif (list_empty(hooks))\n>> >  \t\tgoto cleanup;\n>> > -\t}\n>> >  \n>> > -\tret = run_hooks(hook_name, hook_path, options);\n>> > +\tret = run_hooks(hook_name, hooks, options);\n>> > +\n>> >  cleanup:\n>> >  \trun_hooks_opt_clear(options);\n>> > +\tclear_hook_list(hooks);\n>> \n>> ...the oneshot command also does clear_hook_list(), after calling\n>> run_hooks() which cleared it already.  That looks like a mistake,\n>> i.e. we should always trust run_hooks() to clear it, no?\n>\n> Ah, good catch. I will update the comment on run_hooks() and fix\n> _oneshot() :)\n>\n>  - Emily\n\nI found a further memory issue with this, on \"seen\" running e.g. t0001\nwhen compiled with SANITIZE=leak is broken by this series.\n\nIt's because in clear_hook_list() you clear the list of hooks, but\nforget to clear the malloc'd container, so a missing free() fixes it. As\nin the POC patch at the end of this mail.\n\nBut e.g. \"git hook list <name>\" is still broken, easy enough to fix,\njust also needs fixing of the list_hooks_gently() callsites to e.g. this:\n    \n    diff --git a/builtin/hook.c b/builtin/hook.c\n    index 50233366a8..2cd1831075 100644\n    --- a/builtin/hook.c\n    +++ b/builtin/hook.c\n    @@ -48,8 +48,10 @@ static int list(int argc, const char **argv, const char *prefix)\n     \n     \thead = list_hooks_gently(hookname);\n     \n    -\tif (list_empty(head))\n    +\tif (list_empty(head)) {\n    +\t\tclear_hook_list(head);\n     \t\treturn 1;\n    +\t}\n     \n     \tlist_for_each(pos, head) {\n     \t\tstruct hook *item = list_entry(pos, struct hook, list);\n\nAlthough going to that length to make the SANITIZE=leak run clean is\narguably overdoing it...\n\ndiff --git a/hook.c b/hook.c\nindex 23af86b9ea..e6e1e4173a 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -67,6 +67,7 @@ void clear_hook_list(struct list_head *head)\n \tstruct list_head *pos, *tmp;\n \tlist_for_each_safe(pos, tmp, head)\n \t\tremove_hook(pos);\n+\tfree(head);\n }\n \n static int known_hook(const char *name)\n@@ -142,7 +143,16 @@ const char *find_hook_gently(const char *name)\n \n int hook_exists(const char *name)\n {\n-\treturn !list_empty(list_hooks(name));\n+\tstruct list_head *hooks;\n+\tint exists;\n+\n+\thooks = list_hooks(name);\n+\n+\texists = !list_empty(hooks);\n+\n+\tclear_hook_list(hooks);\n+\n+\treturn exists;\n }\n \n struct hook_config_cb\n"},{"id":"434320","messageId":"YS59ZvJIzCeMxefs@google.com","threadId":"55286","inReplyTo":"87lf4qeh86.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 5/6] hook: include hooks from the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-31T19:05:10Z","receivedAt":"2021-08-31T19:05:21Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 24, 2021 at 09:30:25PM +0200, Ævar Arnfjörð Bjarmason wrote:\n\nDisclaimer: I was writing a pretty involved reply to this and my tmux\nsession crashed, so hopefully I can recall it well enough. Sorry if\nanything is confusing :)\n\n> \n> On Wed, Aug 18 2021, Emily Shaffer wrote:\n> \n> > Teach the hook.[hc] library to parse configs to populare the list of\n> > hooks to run for a given event.\n> \n> s/populare/populate/\nack\n\n> \n> > Multiple commands can be specified for a given hook by providing\n> > multiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n> > \"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be run in\n> > config order of the \"hook.<name>.event\" lines.\n> \n> The \"will be run in order\" probably needs some amending to account for\n> --jobs, no?\n\nI changed it to \"started in order\", and tacked on \"(but may be run in\nparallel)\".\n\n> \n> > For example:\n> >\n> >   $ git config --list | grep ^hook\n> >   hook.bar.command=~/bar.sh\n> >   hook.bar.event=pre-commit\n> \n> Perhaps the example should use:\n> \n>     git config --get-regexp '^hook\\.'\n\nSure, I better not inflict my crappy habits on readers ;)\n\n> \n> >   $ git hook run\n> >   # Runs ~/bar.sh\n> >   # Runs .git/hooks/pre-commit\n> \n> And this \"# Runs\" is not actual output by git, but just an explanation\n> for what happens, better to reword it somehow so it doesn't give that\n> impression.\n> \n> But the example also seems to be broken, surely it should be \"git hook\n> run bar\", not \"git hook run\"? Reading ahead, but presumably no-arg\n> doesn't run all known hooks...\n\nAh, even the suggestion was wrong - it should be `git hook run\npre-commit`. Zzzz. :)\n\n> \n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >  Documentation/config/hook.txt |  18 ++++\n> >  Documentation/git-hook.txt    |  57 ++++++++++++-\n> >  builtin/hook.c                |   3 +-\n> >  hook.c                        | 153 ++++++++++++++++++++++++++++++----\n> >  hook.h                        |   7 +-\n> >  t/t1800-hook.sh               | 141 ++++++++++++++++++++++++++++++-\n> >  6 files changed, 357 insertions(+), 22 deletions(-)\n> >\n> > diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\n> > index 96d3d6572c..c394756328 100644\n> > --- a/Documentation/config/hook.txt\n> > +++ b/Documentation/config/hook.txt\n> > @@ -1,3 +1,21 @@\n> > +hook.<name>.command::\n> > +\tA command to execute whenever `hook.<name>` is invoked. `<name>` should\n> > +\tbe a unique \"friendly\" name which you can use to identify this hook\n> > +\tcommand. (You can specify when to invoke this command with\n> > +\t`hook.<name>.event`.) The value can be an executable on your device or a\n> > +\toneliner for your shell. If more than one value is specified for the\n> > +\tsame `<name>`, the last value parsed will be the only command executed.\n> > +\tSee linkgit:git-hook[1].\n> \n> Hrm, so here we say \"If more than one value is specified for ... the\n> last value parsed will be the only command executed\", but in the commit\n> message it's:\n> \n>     Multiple commands can be specified for a given hook by providing\n>     multiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n>     \"hook.<friendly-name>.event = <hook-event>\" lines.\n> \n> As we've discussed earlier I've got a preference for the former, but\n> just reading this commit message & doc the for the first time I still\n> don't know what you went for, looks like one or the other needs\n> updating. I'm intentionally not reading ahead as I review this.\n> \n> Saying that it's a \"unique name\", but also discussing what happens if\n> it's not unique in the sense that we have multiple \"hook.<name>.*\" is a\n> bit confusing. I think I know what you're going for, perhaps something\n> like this would be better to describe it:\n> \n>     For a \"hook.<name>.{command,event}\" hook entry you'll need to pick a\n>     \"<name>\" that's not shared with any other hook, if you do normal\n>     single-value config semantics apply and git will use the last\n>     provided value.\n> \n> Or something...\n\nYeah, I ended up reworking this a lot. I think the manpage needs some\nstructuring work, to be honest - I wrote a lot about how to use 'git\nhook run' to add hooks to your wrapper around Git, for example, and\nstuck it in a section.\n\nWhile I'm waiting for your next reroll, I'm planning to send the doc to\na tech writer we've got on loan internally and see if they've got any\ntips for us here. Hopefully they can help us out :)\n\nI'm going to snip the rest of the doc comments, because while I know I\ntook action on them last week, I'm not sure I recall what I changed;\nand I'm hoping to get a third party to take a look. I did read them all, I\npromise :)\n\n> > +\t/* to enable oneliners, let config-specified hooks run in shell.\n> > +\t * config-specified hooks have a name. */\n> \n> nit: usual style is multi-line comments like:\n> \n>     /*\n>      * some text[...]\n>      * some more...\n>      */\n> \n> Not:\n> \n>     /* text here right away[...]\n>      * some more ... */\n\nACK. By the way, anybody have tips for making Vim gracefully transition\nfrom\n\n /* happily typing a single line comment\n\nto\n\n /*\n  * happily typing a single line comment that\n  * became super long\n\nWonder if anything is working quite well for anybody, because I mess\nthis up all the time ;)\n\nI guess I could just check for this kind of thing at pre-commit time.\nMaybe with a hook. ;) ;)\n\n> \n> > +\tcp->use_shell = !!run_me->name;\n> > +\n> >  \t/* add command */\n> > -\tif (hook_cb->options->absolute_path)\n> > -\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n> > -\telse\n> > -\t\tstrvec_push(&cp->args, run_me->hook_path);\n> > +\tif (run_me->name) {\n> > +\t\t/* ...from config */\n> > +\t\tstruct strbuf cmd_key = STRBUF_INIT;\n> > +\t\tchar *command = NULL;\n> > +\n> > +\t\tstrbuf_addf(&cmd_key, \"hook.%s.command\", run_me->name);\n> \n> Missing strbuf_release() for this later?\n\nYep, thanks.\n\n> \n> > +\t\tif (git_config_get_string(cmd_key.buf, &command)) {\n> > +\t\t\t/* TODO test me! */\n> \n> ...seems easy enough to just have a test for..., i.e. an *.event entry\n> with no *.command.\n\nYeah, this TODO was probably for myself, so I wouldn't push the reroll\nwithout writing the test. That worked really well....\n\n> \n> > +\t\t\tdie(_(\"'hook.%s.command' must be configured \"\n> > +\t\t\t      \"or 'hook.%s.event' must be removed; aborting.\\n\"),\n> > +\t\t\t    run_me->name, run_me->name);\n> > +\t\t}\n> > +\n> > +\t\tstrvec_push(&cp->args, command);\n> > +\t} else {\n> > +\t\t/* ...from hookdir. */\n> > +\t\tconst char *hook_path = NULL;\n> > +\t\t/*\n> > +\t\t *\n> \n> Nit: Too few \\n before the text in a comment earlier, too many here :)\n*facepalm*\n\n> \n> > +\t\t * At this point we are already running, so don't validate\n> > +\t\t * whether the hook name is known or not.\n> \n> ...because it was done earlier somewhere, or...?\n\nYeah, that validation occurs in list_hooks() instead. I'll make the\ncomment more clear.\n\n> \n> > +\t\t */\n> > +\t\thook_path = find_hook_gently(hook_cb->hook_name);\n> > +\t\tif (!hook_path)\n> > +\t\t\tBUG(\"hookdir hook in hook list but no hookdir hook present in filesystem\");\n> > +\n> > +\t\tif (hook_cb->options->absolute_path)\n> > +\t\t\thook_path = absolute_path(hook_path);\n> > +\n> > +\t\tstrvec_push(&cp->args, hook_path);\n> > +\t}\n> > +\n> >  \n> >  \t/*\n> >  \t * add passed-in argv, without expanding - let the user get back\n> > @@ -228,8 +346,11 @@ static int notify_start_failure(struct strbuf *out,\n> >  \n> >  \thook_cb->rc |= 1;\n> >  \n> > -\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n> > -\t\t    attempted->hook_path);\n> > +\tif (attempted->name)\n> > +\t\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n> > +\t\t    attempted->name);\n> > +\telse\n> > +\t\tstrbuf_addstr(out, _(\"Couldn't start hook from hooks directory\\n\"));\n> >  \n> >  \treturn 1;\n> >  }\n> > diff --git a/hook.h b/hook.h\n> > index 6b7b2d14d2..621bd2cde1 100644\n> > --- a/hook.h\n> > +++ b/hook.h\n> > @@ -27,8 +27,11 @@ int hook_exists(const char *hookname);\n> >  \n> >  struct hook {\n> >  \tstruct list_head list;\n> > -\t/* The path to the hook */\n> > -\tconst char *hook_path;\n> > +\t/*\n> > +\t * The friendly name of the hook. NULL indicates the hook is from the\n> > +\t * hookdir.\n> > +\t */\n> > +\tconst char *name;\n> >  \n> >  \t/*\n> >  \t * Use this to keep state for your feed_pipe_fn if you are using\n> > diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> > index 217db848b3..ef2432f53a 100755\n> > --- a/t/t1800-hook.sh\n> > +++ b/t/t1800-hook.sh\n> > @@ -1,13 +1,29 @@\n> >  #!/bin/bash\n> >  \n> > -test_description='git-hook command'\n> > +test_description='git-hook command and config-managed multihooks'\n> >  \n> >  . ./test-lib.sh\n> >  \n> > +setup_hooks () {\n> > +\ttest_config hook.ghi.event pre-commit --add\n> > +\ttest_config hook.ghi.command \"/path/ghi\" --add\n> > +\ttest_config_global hook.def.event pre-commit --add\n> > +\ttest_config_global hook.def.command \"/path/def\" --add\n> \n> Isn't --add redundant here? Seems no test is stressing multi-value\n> hook.{ghi,def}.* from a quick glance...\n\nSounds like a failing in the test suite ;)\n\nWill remove --add from the command configs, and will stick in test for a\nhook to run in multiple places.\n\nI went back and forth over whether to add the extra .event config in the\nsetup vs. in a specific test, and I decided that by adding it in the\nsetup, we get some implicit \"this is fine\" assurance, as well as the one\nexplicit test (which I added just now) that the hook shows up in both\nplaces.\n\n> \n> > +}\n> > +\n> > +setup_hookdir () {\n> > +\tmkdir .git/hooks\n> > +\twrite_script .git/hooks/pre-commit <<-EOF\n> > +\techo \\\"Legacy Hook\\\"\n> > +\tEOF\n> > +\ttest_when_finished rm -rf .git/hooks\n> > +}\n> > +\n> >  test_expect_success 'git hook usage' '\n> >  \ttest_expect_code 129 git hook &&\n> >  \ttest_expect_code 129 git hook run &&\n> >  \ttest_expect_code 129 git hook run -h &&\n> > +\ttest_expect_code 129 git hook list -h &&\n> \n> Doesn't this belong in a previous commit that added \"git hook list\", not\n> here?\n\nYes, nice. Thanks.\n\n> \n> >  \ttest_expect_code 129 git hook run --unknown 2>err &&\n> >  \tgrep \"unknown option\" err\n> >  '\n> > @@ -153,4 +169,127 @@ test_expect_success 'stdin to hooks' '\n> >  \ttest_cmp expect actual\n> >  '\n> >  \n> > +test_expect_success 'git hook list orders by config order' '\n> > +\tsetup_hooks &&\n> > +\n> > +\tcat >expected <<-EOF &&\n> > +\tdef\n> > +\tghi\n> > +\tEOF\n> > +\n> > +\tgit hook list pre-commit >actual &&\n> > +\ttest_cmp expected actual\n> > +'\n> > +\n> > +test_expect_success 'git hook list reorders on duplicate event declarations' '\n> > +\tsetup_hooks &&\n> > +\n> > +\t# 'def' is usually configured globally; move it to the end by\n> > +\t# configuring it locally.\n> > +\ttest_config hook.def.event \"pre-commit\" --add &&\n> \n> Ah, well the --add belongs here, but not needed in setup_hooks, right?\n\nAddressed above.\n\n> > +\n> > +\tcat >input <<-EOF &&\n> > +\t1\n> > +\t2\n> > +\t3\n> > +\tEOF\n> > +\n> > +\tcat >expected <<-EOF &&\n> > +\ta1\n> > +\ta2\n> > +\ta3\n> > +\tb1\n> > +\tb2\n> > +\tb3\n> > +\tEOF\n> \n> For any here-docs without variables, use <<-\\EOF, note the backslash.\nACK\n\n\nThanks.\n - Emily\n"},{"id":"434366","messageId":"YS6ag97rjkPwJUkS@google.com","threadId":"55286","inReplyTo":"87ilzueh1g.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 6/6] hook: allow out-of-repo 'git hook' invocations","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-08-31T21:09:23Z","receivedAt":"2021-08-31T21:09:31Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 24, 2021 at 10:12:04PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Wed, Aug 18 2021, Emily Shaffer wrote:\n> \n> > Since hooks can now be supplied via the config, and a config can be\n> > present without a gitdir via the global and system configs, we can start\n> > to allow 'git hook run' to occur without a gitdir. This enables us to do\n> > things like run sendemail-validate hooks when running 'git send-email'\n> > from a nongit directory.\n> \n> Sensible goal. Perhaps we should note in an earlier commit when\n> config-based hooks are introduced something like:\n> \n>     Even though we've added config-based hooks, they currently only work\n>     if we can find a .git directory, even though certain commands such\n>     as \"git send-email\" (or only that command?) can be run outside of a\n>     git directory. A subsequent commit will address that edge-case.\n\nHmmm. I personally do not like \"a subsequent commit adds this\" in commit\nmessages. But I'm having trouble expressing why :) To me, \"I know about\nme and everything that came before me\" is fine and \"I wish in the future\nx would happen\" is fine, for commit messages, but \"x will happen in the\nfuture\" feels a little wrong.\n\nAnyway, I think it is a little distracting to include that message in\nthe earlier commit, since the \"doesn't work outside of a repo\" aspect of\nhooks is not changing at all.\n\n> \n> > [...]\n> > Notes:\n> >     For hookdir hooks, do we want to run them in nongit dir when core.hooksPath\n> >     is set? For example, if someone set core.hooksPath in their global config and\n> >     then ran 'git hook run sendemail-validate' in a nongit dir?\n> > [...]\n> >  git.c           |  2 +-\n> >  hook.c          |  2 +-\n> >  t/t1800-hook.sh | 20 +++++++++++++++-----\n> >  3 files changed, 17 insertions(+), 7 deletions(-)\n> >\n> > diff --git a/git.c b/git.c\n> > index 540909c391..39988ee3b0 100644\n> > --- a/git.c\n> > +++ b/git.c\n> > @@ -538,7 +538,7 @@ static struct cmd_struct commands[] = {\n> >  \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n> >  \t{ \"hash-object\", cmd_hash_object },\n> >  \t{ \"help\", cmd_help },\n> > -\t{ \"hook\", cmd_hook, RUN_SETUP },\n> > +\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n> >  \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n> >  \t{ \"init\", cmd_init_db },\n> >  \t{ \"init-db\", cmd_init_db },\n> > diff --git a/hook.c b/hook.c\n> > index 581d87cbbd..2e08156546 100644\n> > --- a/hook.c\n> > +++ b/hook.c\n> > @@ -218,7 +218,7 @@ struct list_head *list_hooks_gently(const char *hookname)\n> >  \n> >  \t/* Add the hook from the hookdir. The placeholder makes it easier to\n> >  \t * allocate work in pick_next_hook. */\n> > -\tif (find_hook_gently(hookname))\n> > +\tif (have_git_dir() && find_hook_gently(hookname))\n> >  \t\tappend_or_move_hook(hook_head, NULL);\n> >  \n> >  \treturn hook_head;\n> > diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> > index ef2432f53a..a7e45c0d16 100755\n> > --- a/t/t1800-hook.sh\n> > +++ b/t/t1800-hook.sh\n> > @@ -118,15 +118,25 @@ test_expect_success 'git hook run -- pass arguments' '\n> >  \ttest_cmp expect actual\n> >  '\n> >  \n> > -test_expect_success 'git hook run -- out-of-repo runs excluded' '\n> > -\twrite_script .git/hooks/test-hook <<-EOF &&\n> > -\techo Test hook\n> > -\tEOF\n> > +test_expect_success 'git hook run: out-of-repo runs execute global hooks' '\n> > +\ttest_config_global hook.global-hook.event test-hook --add &&\n> > +\ttest_config_global hook.global-hook.command \"echo no repo no problems\" --add &&\n> > +\n> > +\techo \"global-hook\" >expect &&\n> > +\tnongit git hook list test-hook >actual &&\n> > +\ttest_cmp expect actual &&\n> > +\n> > +\techo \"no repo no problems\" >expect &&\n> >  \n> > -\tnongit test_must_fail git hook run test-hook\n> > +\tnongit git hook run test-hook 2>actual &&\n> > +\ttest_cmp expect actual\n> >  '\n> >  \n> >  test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n> > +\twrite_script .git/hooks/test-hook <<-EOF &&\n> > +\techo Test hook\n> > +\tEOF\n> > +\n> >  \tmkdir my-hooks &&\n> >  \twrite_script my-hooks/test-hook <<-\\EOF &&\n> >  \techo Hook ran $1 >>actual\n> \n> If the only user of this is git-send-email, let's have tests for this in\n> t/t9001-send-email.sh. That should also address your \"Notes\" above,\n> i.e. let's just test it with core.hooksPath and see what the interaction\n> looks like.\n\nYeah, I think it is probably fine to do allllll the git-send-email\nfixups in this one commit, actually. I think your point is correct that\nright now the only command which can benefit is git-send-email. It might\nalso be reasonable to drop this change from this series, but I won't\nbother doing so unless it looks like this one commit is holding up the\nrest of the series.\n\nI could see, though, a later world where we might want something like a\n\"pre-clone\" or \"post-clone\"(-pre-checkout) hook, now that we don't need\nto have a .gitdir in order to run any hooks. Will make a very brief\n\"later it might be nice if...\" addition to the commit message.\n\n - Emily\n"},{"id":"434490","messageId":"87lf4fv3jf.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"YR7r3h1AG4Zyn7x7@google.com","subject":"Re: [PATCH v4 00/36] Run hooks via \"git run hook\" & hook library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T07:21:41Z","receivedAt":"2021-09-02T07:29:30Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Aug 19 2021, Emily Shaffer wrote:\n\nI'm submitting a re-roll of this soon, just some notes.\n\n> On Tue, Aug 03, 2021 at 09:38:26PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> This is a v4 re-roll of the \"Base for \"config-based-hooks\" topic.\n>>  * Almost all the callers were just \"one-shot\" callers, I introduced a\n>>    new run_hooks_oneshot() function for those, which gets rid of the\n>>    verbosity around memory management, see e.g. the \"builtin/gc.c\" in\n>>    the range-diff below. That run_hooks_oneshot() can also take a NULL\n>>    set of options.\n>\n> I discussed this elswhere, but just to make doubly sure that comment\n> doesn't get lost, here's a link to my mail:\n> https://lore.kernel.org/git/YR2jLdYQA5CVzX5h%40google.com\n>\n>> Range-diff against v3:\n>>  1:  27c94247f87 =  1:  81fe1ed90d5 Makefile: mark \"check\" target as .PHONY\n>>  2:  6e164edb0b0 !  2:  0f749530777 Makefile: stop hardcoding {command,config}-list.h\n>>  3:  ddae86802e2 !  3:  644b31fe281 Makefile: remove an out-of-date comment\n>>  4:  58c37e4f06e =  4:  89c4d44b0c3 hook.[ch]: move find_hook() to this new library\n>>  5:  0cf7e078ef4 =  5:  3514e0c0251 hook.c: add a hook_exists() wrapper and use it in bugreport.c\n>>  -:  ----------- >  6:  d5ef40f77dc hook.c users: use \"hook_exists()\" insted of \"find_hook()\"\n>>  6:  f343fc7ae66 !  7:  4cfd72722c1 hook-list.h: add a generated list of hooks, like config-list.h\n>\n> Since these 6 were broken out and then brought back into this series,\n> I'll review them individually downthread.\n>\n>>  7:  cf4b06bfdf8 !  8:  7cb4a4cb69e hook: add 'run' subcommand\n>>     @@ builtin/hook.c (new)\n>>      +#include \"strbuf.h\"\n>>      +#include \"strvec.h\"\n>>      +\n>>     ++#define BUILTIN_HOOK_RUN_USAGE \\\n>>     ++\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n>>     ++\n>\n> Nice, now we avoid string duplication here...\n>\n>>      +static const char * const builtin_hook_usage[] = {\n>>     -+\tN_(\"git hook <command> [...]\"),\n>\n> ...and there is no point including this vague thing that has a more\n> specific description right after. Ok.\n>\n>>     -+\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>>     ++\tBUILTIN_HOOK_RUN_USAGE,\n>>      +\tNULL\n>>      +};\n> [...]\n>>     @@ builtin/hook.c (new)\n>>      +{\n>>      +\tint i;\n>>      +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>>     -+\tint rc = 0;\n>>      +\tconst char *hook_name;\n>>      +\tconst char *hook_path;\n>>     -+\n>>      +\tstruct option run_options[] = {\n>>      +\t\tOPT_END(),\n>>      +\t};\n>>     ++\tint ret;\n>>      +\n>>      +\targc = parse_options(argc, argv, prefix, run_options,\n>>      +\t\t\t     builtin_hook_run_usage,\n>>     -+\t\t\t     PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);\n>>     -+\n>>     -+\tif (argc > 1) {\n>>     -+\t\tif (strcmp(argv[1], \"--\") &&\n>>     -+\t\t    strcmp(argv[1], \"--end-of-options\"))\n>>     -+\t\t\t/* Having a -- for \"run\" is mandatory */\n>>     -+\t\t\tusage_with_options(builtin_hook_usage, run_options);\n>>     -+\t\t/* Add our arguments, start after -- */\n>>     -+\t\tfor (i = 2 ; i < argc; i++)\n>>     -+\t\t\tstrvec_push(&opt.args, argv[i]);\n>>     -+\t}\n>>     ++\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n>\n> Nice - this code is being broken up and moved later.\n>\n>>      +\n>>     -+\t/* Need to take into account core.hooksPath */\n>>     -+\tgit_config(git_default_config, NULL);\n>>     ++\tif (!argc)\n>>     ++\t\tgoto usage;\n>>      +\n>>      +\t/*\n>>     -+\t * We are not using run_hooks() because we'd like to detect\n>>     -+\t * missing hooks. Let's find it ourselves and call\n>>     -+\t * run_found_hooks() instead.\n>\n> This comment disappears entirely. I am not too terribly upset about it,\n> since this behavior changes in my series anyway, but it seems like it\n> might have been an accident?\n\nI removed it intentionally, I thought it was clear enough from the\ncontrol flow, but added it back in. Thanks.\n\n>>     ++\t * Having a -- for \"run\" when providing <hook-args> is\n>>     ++\t * mandatory.\n>>      +\t */\n>>     ++\tif (argc > 1 && strcmp(argv[1], \"--\") &&\n>>     ++\t    strcmp(argv[1], \"--end-of-options\"))\n>>     ++\t\tgoto usage;\n>>     ++\n>>     ++\t/* Add our arguments, start after -- */\n>>     ++\tfor (i = 2 ; i < argc; i++)\n>>     ++\t\tstrvec_push(&opt.args, argv[i]);\n>>     ++\n>>     ++\t/* Need to take into account core.hooksPath */\n>>     ++\tgit_config(git_default_config, NULL);\n>>     ++\n>>      +\thook_name = argv[0];\n>>      +\thook_path = find_hook(hook_name);\n>>      +\tif (!hook_path) {\n>>      +\t\terror(\"cannot find a hook named %s\", hook_name);\n>>      +\t\treturn 1;\n>>      +\t}\n>>     -+\trc = run_found_hooks(hook_name, hook_path, &opt);\n>>      +\n>>     ++\tret = run_hooks(hook_name, hook_path, &opt);\n>>      +\trun_hooks_opt_clear(&opt);\n>>     -+\n>>     -+\treturn rc;\n>>     ++\treturn ret;\n>>     ++usage:\n>>     ++\tusage_with_options(builtin_hook_run_usage, run_options);\n>>      +}\n>>      +\n>>      +int cmd_hook(int argc, const char **argv, const char *prefix)\n>>     @@ builtin/hook.c (new)\n>>      +\tstruct option builtin_hook_options[] = {\n>>      +\t\tOPT_END(),\n>>      +\t};\n>>     ++\n>>      +\targc = parse_options(argc, argv, NULL, builtin_hook_options,\n>>      +\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n>>      +\tif (!argc)\n>>     -+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n>>     ++\t\tgoto usage;\n>>      +\n>>      +\tif (!strcmp(argv[0], \"run\"))\n>>      +\t\treturn run(argc, argv, prefix);\n>>     -+\telse\n>>     -+\t\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n>>     ++\n>>     ++usage:\n>>     ++\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n>>      +}\n>\n> The goto pattern seems readable enough elsewhere, though.\n>\n>>       ## command-list.txt ##\n>>     @@ hook.c: int hook_exists(const char *name)\n>>      +\tstruct hook_cb_data *hook_cb = pp_cb;\n>>      +\tstruct hook *run_me = hook_cb->run_me;\n>>      +\n>>     ++\tif (!run_me)\n>>     ++\t\treturn 0;\n>>     ++\n> Now we protect ourselves from the repeated calls to pick_next_hook().\n> Good.\n>>      +\tcp->no_stdin = 1;\n>>      +\tcp->env = hook_cb->options->env.v;\n>>      +\tcp->stdout_to_stderr = 1;\n>>     @@ hook.c: int hook_exists(const char *name)\n>>      +\t/* Provide context for errors if necessary */\n>>      +\t*pp_task_cb = run_me;\n>>      +\n>>     ++\t/*\n>>     ++\t * This pick_next_hook() will be called again, we're only\n>>     ++\t * running one hook, so indicate that no more work will be\n>>     ++\t * done.\n>>     ++\t */\n>>     ++\thook_cb->run_me = NULL;\n>>     ++\n> And clearly explaining the caller's behavior here. Sure.\n>>      +\treturn 1;\n>>      +}\n>>      +\n>>     @@ hook.c: int hook_exists(const char *name)\n>>      +\n>>      +\thook_cb->rc |= result;\n>>      +\n>>     -+\treturn 1;\n>>     ++\treturn 0;\n> And finally, report \"everything is fine\" on task finished. Thanks.\n>>      +}\n>>      +\n>>     -+int run_found_hooks(const char *hook_name, const char *hook_path,\n>>     -+\t\t    struct run_hooks_opt *options)\n>>     ++int run_hooks(const char *hook_name, const char *hook_path,\n>>     ++\t      struct run_hooks_opt *options)\n>\n> Ok. With the rename, we only have \"verbose way to call it\" and \"speedy\n> way to call it\". No more \"run_found_hooks()\". Thanks, I think I like\n> this better - I found \"run_found_hooks()\" to be a little ambiguous.\n>\n>>     @@ hook.c: int hook_exists(const char *name)\n>>      +\t\t\t\t   hook_name);\n>>      +\n>>      +\treturn cb_data.rc;\n>>     -+}\n>>     -+\n>>     -+int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n>>     -+{\n>>     -+\tconst char *hook_path;\n>>     -+\tint ret;\n>>     -+\tif (!options)\n>>     -+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n>>     -+\n>>     -+\thook_path = find_hook(hook_name);\n>>     -+\n>>     -+\t/*\n>>     -+\t * If you need to act on a missing hook, use run_found_hooks()\n>>     -+\t * instead\n>>     -+\t */\n>>     -+\tif (!hook_path)\n>>     -+\t\treturn 0;\n>>     -+\n>>     -+\tret = run_found_hooks(hook_name, hook_path, options);\n>>     -+\treturn ret;\n>\n> Ok - so we wait for another patch to introduce run_hooks_oneshot. Fine.\n>\n>>      +}\n>>      \n>>       ## hook.h ##\n>>     @@ hook.h: const char *find_hook(const char *name);\n>>      +\n>>      +\t/* Args to be passed to each hook */\n>>      +\tstruct strvec args;\n>>     -+\n>>     -+\t/*\n>>     -+\t * Number of threads to parallelize across, currently a stub,\n>>     -+\t * we use the parallel API for future-proofing, but we always\n>>     -+\t * have one hook of a given name, so this is always an\n>>     -+\t * implicit 1 for now.\n>>     -+\t */\n>>     -+\tint jobs;\n>\n> Thanks. I think this makes more sense than the weird \"futureproofing\"\n> state it was in before.\n>\n>>       ## t/t1800-hook.sh (new) ##\n>>     @@ t/t1800-hook.sh (new)\n>>      +\n>>      +test_expect_success 'git hook usage' '\n>>      +\ttest_expect_code 129 git hook &&\n>>     -+\ttest_expect_code 129 git hook -h &&\n>>     -+\ttest_expect_code 129 git hook run -h\n>>     ++\ttest_expect_code 129 git hook run &&\n>>     ++\ttest_expect_code 129 git hook run -h &&\n>>     ++\ttest_expect_code 129 git hook run --unknown 2>err &&\n>>     ++\tgrep \"unknown option\" err\n>>      +'\n>\n> Nice. I was able to use this test myself when I added 'git hook list',\n> too.\n>\n> The changes to this patch look good to me.\n>\n>>  8:  7209f73f281 !  9:  2b8500aa675 gc: use hook library for pre-auto-gc hook\n>>     @@ Metadata\n>>       ## Commit message ##\n>>          gc: use hook library for pre-auto-gc hook\n>>      \n>>     -    Using the hook.h library instead of the run-command.h library to run\n>>     -    pre-auto-gc means that those hooks can be set up in config files, as\n>>     -    well as in the hookdir. pre-auto-gc is called only from builtin/gc.c.\n>>     +    Move the pre-auto-gc hook away from run-command.h to and over to the\n>>     +    new hook.h library.\n>>     +\n>>     +    To do this introduce a simple run_hooks_oneshot() wrapper, we'll be\n>>     +    using it extensively for these simple cases of wanting to run a single\n>>     +    hook under a given name, and having it free the memory we allocate for\n>>     +    us.\n>\n> Cool, so we have an excuse for introducing the oneshot wrapper right\n> away. Thanks.\n>\n>>     -@@ builtin/gc.c: static void add_repack_incremental_option(void)\n>>     - \n>>     - static int need_to_gc(void)\n>>     - {\n>>     -+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n>>     -+\n>>     - \t/*\n>>     - \t * Setting gc.auto to 0 or negative can disable the\n>>     - \t * automatic gc.\n>>      @@ builtin/gc.c: static int need_to_gc(void)\n>>       \telse\n>>       \t\treturn 0;\n>>       \n>>      -\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n>>     -+\tif (run_hooks(\"pre-auto-gc\", &hook_opt)) {\n>>     -+\t\trun_hooks_opt_clear(&hook_opt);\n>>     ++\tif (run_hooks_oneshot(\"pre-auto-gc\", NULL))\n>>       \t\treturn 0;\n>>     -+\t}\n>>     -+\trun_hooks_opt_clear(&hook_opt);\n>>       \treturn 1;\n>>       }\n>\n> Nice - the callsite looks much tidier now.\n>\n>>     +\n>>     + ## hook.c ##\n>>     +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n>>       \n>>     + \treturn cb_data.rc;\n>>     + }\n>>     ++\n>>     ++int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n>>     ++{\n>>     ++\tconst char *hook_path;\n>>     ++\tint ret;\n>>     ++\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n>>     ++\n>>     ++\tif (!options)\n>>     ++\t\toptions = &hook_opt_scratch;\n>\n> Ah, it still takes an options (optionally, ha ha) because run_hooks()\n> would require the caller to provide the hook list/hook path. Ok. Seems\n> fine, and I like that the '_oneshot' naming change makes it less\n> surprising that run_hooks_opt_clear() will get called for you.\n>\n>>     ++\n>>     ++\thook_path = find_hook(hook_name);\n>>     ++\tif (!hook_path) {\n>>     ++\t\tret = 0;\n>>     ++\t\tgoto cleanup;\n>>     ++\t}\n>>     ++\n>>     ++\tret = run_hooks(hook_name, hook_path, options);\n>>     ++cleanup:\n>>     ++\trun_hooks_opt_clear(options);\n>>     ++\treturn ret;\n>>     ++}\n>>     +\n>>     + ## hook.h ##\n>>     +@@ hook.h: void run_hooks_opt_clear(struct run_hooks_opt *o);\n>>     + /**\n>>     +  * Takes an already resolved hook found via find_hook() and runs\n>>     +  * it. Does not call run_hooks_opt_clear() for you.\n>>     ++ *\n>>     ++ * See run_hooks_oneshot() for the simpler one-shot API.\n>>     +  */\n>>     + int run_hooks(const char *hookname, const char *hook_path,\n>>     + \t      struct run_hooks_opt *options);\n>>     ++\n>>     ++/**\n>>     ++ * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n>>     ++ * with run_hooks().\n>>     ++ *\n>>     ++ * If \"options\" is provided calls run_hooks_opt_clear() on it for\n>>     ++ * you. If \"options\" is NULL a scratch one will be provided for you\n>>     ++ * before calling run_hooks().\n>\n> \"A scratch one will be provided for you\" doesn't sound quite right -\n> it's not exposed to the caller at all, but the comment sounds like\n> you're handing the caller this scratch struct. Maybe it's better to say\n> \"the default options from RUN_HOOKS_OPT_INIT will be used\"?\n>\n> As a bonus, if you directly reference the initter macro, then it will\n> automatically become clear to folks reading the documentation what the\n> expected parallelism is on their hook, as the parallelism (_SERIES or\n> _PARALLEL) is included on the name of the macro in my change later.\n\n*Nod*, did that.\n\n>>     ++ */\n>>     ++int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n>>     ++\n>>     + #endif\n>\n>>  9:  e9a1e7cf61e ! 10:  3ee55d2c10f rebase: teach pre-rebase to use hook.h\n>>     @@ Metadata\n>>      Author: Emily Shaffer <emilyshaffer@google.com>\n>>      \n>>       ## Commit message ##\n>>     -    rebase: teach pre-rebase to use hook.h\n>>     +    rebase: convert pre-rebase to use hook.h\n>>      \n>>          Move the pre-rebase hook away from run-command.h to and over to the\n>>          new hook.h library.\n>>     @@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix\n>>       \tif (!ok_to_skip_pre_rebase &&\n>>      -\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n>>      -\t\t\targc ? argv[0] : NULL, NULL))\n>>     -+\t    run_hooks(\"pre-rebase\", &hook_opt)) {\n>>     -+\t\trun_hooks_opt_clear(&hook_opt);\n>>     ++\t    run_hooks_oneshot(\"pre-rebase\", &hook_opt))\n>\n> Ok, it just uses the oneshot call instead. Looks good.\n>> 10:  1d087269303 ! 11:  050f20d14f0 am: convert applypatch hooks to use config\n>> 11:  32eec5dc2f0 ! 12:  ac875d284da hooks: convert 'post-checkout' hook to hook library\n>> 12:  e9fa3f67593 ! 13:  69763bc2255 merge: use config-based hooks for post-merge hook\n>\n> The above all just use the oneshot instead, and have some minor tweaks\n> to commit messages to lose references to the config. Looks good.\n>\n>> 13:  12347d901bb ! 14:  2ca1ca1b8e4 git hook run: add an --ignore-missing flag\n>>     @@ Documentation/git-hook.txt: optional `--` (or `--end-of-options`, see linkgit:gi\n>>       linkgit:githooks[5]\n>>      \n>>       ## builtin/hook.c ##\n>>     +@@\n>>     + #include \"strvec.h\"\n>>     + \n>>     + #define BUILTIN_HOOK_RUN_USAGE \\\n>>     +-\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n>>     ++\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n>\n> Ok. We update the usage in the macro here instead. Nice.\n>\n>>      @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n>>     - \t/*\n>>     - \t * We are not using run_hooks() because we'd like to detect\n>>     - \t * missing hooks. Let's find it ourselves and call\n>>     --\t * run_found_hooks() instead.\n>>     -+\t * run_found_hooks() instead...\n>>     - \t */\n>>     + \tgit_config(git_default_config, NULL);\n>>     + \n>>       \thook_name = argv[0];\n>>     ++\tif (ignore_missing)\n>>     ++\t\treturn run_hooks_oneshot(hook_name, &opt);\n>>       \thook_path = find_hook(hook_name);\n>>       \tif (!hook_path) {\n>>     -+\t\t/* ... act like run_hooks() under --ignore-missing */\n>>     -+\t\tif (ignore_missing)\n>>     -+\t\t\treturn 0;\n>>       \t\terror(\"cannot find a hook named %s\", hook_name);\n>>     - \t\treturn 1;\n>>     - \t}\n>\n> Hm. I actually think this was clearer before. I *think* that is because\n> you and I have different opinions on the clarity of multiple returns :)\n>\n> But I think it is a little confusing to say \"ok, we will call an\n> entirely different entry point in case of this one thing which we will\n> have to check anyways\".\n\nTried to reword this per the above..\n\n>> 14:  71d209b4077 ! 15:  5b66b04bec7 send-email: use 'git hook run' for 'sendemail-validate'\n>>     @@ git-send-email.perl: sub validate_patch {\n>>       \tif ($repo) {\n>>      +\t\tmy $hook_name = 'sendemail-validate';\n>>       \t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n>>     --\t\tmy $validate_hook = catfile($hooks_path,\n>>     + \t\trequire File::Spec;\n>>     +-\t\tmy $validate_hook = File::Spec->catfile($hooks_path,\n>>      -\t\t\t\t\t    'sendemail-validate');\n>>     -+\t\tmy $validate_hook = catfile($hooks_path, $hook_name);\n>>     ++\t\tmy $validate_hook = File::Spec->catfile($hooks_path, $hook_name);\n>\n> Ok, we are adapting to some other change around File::Spec that happened\n> to git-send-email.perl elsewhere. The change to this patch looks good to\n> me.\n>\n>>       \t\tmy $hook_error;\n>>       \t\tif (-x $validate_hook) {\n>>     - \t\t\tmy $target = abs_path($fn);\n>>     + \t\t\trequire Cwd;\n>>      @@ git-send-email.perl: sub validate_patch {\n>>       \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n>>       \t\t\t\tor die(\"chdir: $!\");\n>> 15:  246a82b55b2 = 16:  14a37a43db2 git-p4: use 'git hook' to run hooks\n>> 16:  e3f8482d803 ! 17:  ad5d0e0e7de commit: use hook.h to execute hooks\n>>     @@ Metadata\n>>      Author: Emily Shaffer <emilyshaffer@google.com>\n>>      \n>>       ## Commit message ##\n>>     -    commit: use hook.h to execute hooks\n>>     +    commit: convert {pre-commit,prepare-commit-msg} hook to hook.h\n>>      \n>>     -    Teach run_commit_hook() to call hook.h instead of run-command.h. This\n>>     -    covers 'pre-commit', 'commit-msg', and\n>>     -    'prepare-commit-msg'.\n>>     -\n>>     -    Additionally, ask the hook library - not run-command - whether any\n>>     -    hooks will be run, as it's possible hooks may exist in the config but\n>>     -    not the hookdir.\n>>     -\n>>     -    Because all but 'post-commit' hooks are expected to make some state\n>>     -    change, force all but 'post-commit' hook to run in series. 'post-commit'\n>>     -    \"is meant primarily for notification, and cannot affect the outcome of\n>>     -    `git commit`,\" so it is fine to run in parallel.\n\nI tried to address all of the above.\n\n> I am a little bummed that the patch reordering effectively removes the\n> justification for parallel/serial execution for each hook from 'git\n> blame'. Oh well.\n\nBut it'll be there if you add it to your commit to migrate them to the\nparallel macros, and that'll be easier to find with \"blame\" than having\nit tangled up in the previous \"do all the hook things\" changes.\n\nSo a note to reword/rebase that into your series...\n\n>>     +    Move these hooks hook away from run-command.h to and over to the new\n>>     +    hook.h library.\n>>      \n>>          Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n>>          Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n>>      \n>>       ## commit.c ##\n>\n> Otherwise the main change here is dealing with other improvements\n> earlier in the series, like 'hook_exists()' and 'run_hooks_oneshot()'.\n> Looks fine.\n>\n>> 17:  6ed61071c5e ! 18:  3d3a33e2674 read-cache: convert post-index-change hook to use config\n>>     @@ Metadata\n>>      Author: Emily Shaffer <emilyshaffer@google.com>\n>>      \n>>       ## Commit message ##\n>>     -    read-cache: convert post-index-change hook to use config\n>>     +    read-cache: convert post-index-change to use hook.h\n>>      \n>>     -    By using hook.h instead of run-command.h to run, post-index-change hooks\n>>     -    can now be specified in the config in addition to the hookdir.\n>>     -    post-index-change is not run anywhere besides in read-cache.c.\n>>     +    Move the post-index-change hook away from run-command.h to and over to\n>>     +    the new hook.h library.\n>\n> I do not think it is necessary to drop the mention about\n> 'post-index-change' being run anywhere else, but that's just a nit, not\n> worth fixing :)\n\nI removed this in rewording/removing now-false mentions of \"this does\nconfig\".\n\nI didn't add the post-index-change mentiond back in, I figured to do it\nconsistently I'd need to start with pre-auto-gc saying it's only handled\nin builtin/gc.c & all the way down the line...\n\n>>      \n>>          This removes the last direct user of run_hook_ve(), so we can make the\n>>          function static now. It'll be removed entirely soon.\n>>     @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struc\n>>      +\t\t     istate->updated_workdir ? \"1\" : \"0\",\n>>      +\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n>>      +\t\t     NULL);\n>>     -+\trun_hooks(\"post-index-change\", &hook_opt);\n>>     -+\trun_hooks_opt_clear(&hook_opt);\n>>     ++\trun_hooks_oneshot(\"post-index-change\", &hook_opt);\n>>      +\n>\n> Mechanical change to using run_hooks_oneshot(). OK.\n>\n>>       \tistate->updated_workdir = 0;\n>>       \tistate->updated_skipworktree = 0;\n>> 18:  e4ef3f4548a ! 19:  893f8666301 receive-pack: convert push-to-checkout hook to hook.h\n>>     @@ Metadata\n>>       ## Commit message ##\n>>          receive-pack: convert push-to-checkout hook to hook.h\n>>      \n>>     -    By using hook.h instead of run-command.h to invoke push-to-checkout,\n>>     -    hooks can now be specified in the config as well as in the hookdir.\n>>     -    push-to-checkout is not called anywhere but in builtin/receive-pack.c.\n>>     +    Move the push-to-checkout hook away from run-command.h to and over to\n>>     +    the new hook.h library.\n>\n> You made a comment in the commit before about the final instance of\n> run_hook_ve; do you want to make a similar one here about run_hook_le?\n\nWilld.o\n\n> The range-diff here is mechanical so looks fine to me.\n>\n>> 19:  e3dda367ec9 = 20:  070433deba5 run-command: remove old run_hook_{le,ve}() hook API\n>> 20:  477d75bf579 = 21:  1028e0c1667 run-command: allow stdin for run_processes_parallel\n>> 21:  b7c0ee9719a ! 22:  639e59e9ed0 hook: support passing stdin to hooks\n>>     @@ Documentation/git-hook.txt: what those are.\n>>      \n>>       ## builtin/hook.c ##\n>>      @@\n>>     + #include \"strvec.h\"\n>>       \n>>     - static const char * const builtin_hook_usage[] = {\n>>     - \tN_(\"git hook <command> [...]\"),\n>>     --\tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>>     -+\tN_(\"git hook run [<args>] <hook-name> [-- <hook-args>]\"),\n>>     - \tNULL\n>>     - };\n>>     - \n>>     - static const char * const builtin_hook_run_usage[] = {\n>>     - \tN_(\"git hook run <hook-name> [-- <hook-args>]\"),\n>>     -+\tN_(\"git hook run [--to-stdin=<path>] <hook-name> [-- <hook-args>]\"),\n>>     - \tNULL\n>>     - };\n>>     + #define BUILTIN_HOOK_RUN_USAGE \\\n>>     +-\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n>>     ++\tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n>>       \n> Just coping with the macro instead of the duplicated strings. Ok.\n>\n> The range-diff looks fine to me.\n>\n>> 22:  4035069a98c ! 23:  7d1925cca48 am: convert 'post-rewrite' hook to hook.h\n>>     @@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)\n>>       {\n>>      -\tstruct child_process cp = CHILD_PROCESS_INIT;\n>>      -\tconst char *hook = find_hook(\"post-rewrite\");\n>>     -+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>>     - \tint ret;\n>>     - \n>>     +-\tint ret;\n>>     +-\n>>      -\tif (!hook)\n>>      -\t\treturn 0;\n>>      -\n>>     @@ builtin/am.c: static int run_applypatch_msg_hook(struct am_state *state)\n>>      -\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n>>      -\tcp.stdout_to_stderr = 1;\n>>      -\tcp.trace2_hook_name = \"post-rewrite\";\n>>     -+\tstrvec_push(&opt.args, \"rebase\");\n>>     -+\topt.path_to_stdin = am_path(state, \"rewritten\");\n>>     ++\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>>       \n>>      -\tret = run_command(&cp);\n>>     -+\tret = run_hooks(\"post-rewrite\", &opt);\n>>     ++\tstrvec_push(&opt.args, \"rebase\");\n>>     ++\topt.path_to_stdin = am_path(state, \"rewritten\");\n>>       \n>>      -\tclose(cp.in);\n>>     -+\trun_hooks_opt_clear(&opt);\n>>     - \treturn ret;\n>>     +-\treturn ret;\n>>     ++\treturn run_hooks_oneshot(\"post-rewrite\", &opt);\n>>       }\n>>       \n>>     + /**\n>\n> Man, the range-diff did a terrible job of summarizing this :)\n>\n> The patch itself looks great, though, with the switch to using\n> run_hooks_oneshot. Thanks.\n>\n>> 24:  da46c859c1c ! 25:  05d1085f7eb hook: provide stdin by string_list or callback\n>>     @@ hook.c: static int pick_next_hook(struct child_process *cp,\n>>       \t} else {\n>>       \t\tcp->no_stdin = 1;\n>>       \t}\n>>     -@@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n>>     - \trun_processes_parallel_tr2(options->jobs,\n>>     +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n>>     + \trun_processes_parallel_tr2(jobs,\n>>       \t\t\t\t   pick_next_hook,\n>>       \t\t\t\t   notify_start_failure,\n>>      -\t\t\t\t   NULL,\n>>     @@ hook.c: int run_found_hooks(const char *hook_name, const char *hook_path,\n>>       \t\t\t\t   notify_hook_finished,\n>>       \t\t\t\t   &cb_data,\n>>       \t\t\t\t   \"hook\",\n>>     -@@ hook.c: int run_hooks(const char *hook_name, struct run_hooks_opt *options)\n>>     +@@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n>>     + \n>>     + \tif (options->absolute_path)\n>>     + \t\tstrbuf_release(&abs_path);\n>>     ++\tfree(my_hook.feed_pipe_cb_data);\n>\n> Nice catch. Thanks.\n>\n>> 25:  7343be28ef4 ! 26:  4b7175af2e5 hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n>> 26:  85bf13a0835 ! 27:  3f24e056410 transport: convert pre-push hook to use config\n>>     @@ Metadata\n>>      Author: Emily Shaffer <emilyshaffer@google.com>\n>>      \n>>       ## Commit message ##\n>>     -    transport: convert pre-push hook to use config\n>>     +    transport: convert pre-push hook to hook.h\n>>      \n>>     -    By using the hook.h:run_hooks API, pre-push hooks can be specified in\n>>     -    the config as well as in the hookdir.\n>>     +    Move the pre-push hook away from run-command.h to and over to the new\n>>     +    hook.h library.\n>>      \n>>          Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n>>          Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n>>     @@ transport.c: static void die_with_unpushed_submodules(struct string_list *needs_\n>>      -\tint ret = 0, x;\n>>      +\tint ret = 0;\n>>      +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>>     -+\tstruct strbuf tmp = STRBUF_INIT;\n>>       \tstruct ref *r;\n>>      -\tstruct child_process proc = CHILD_PROCESS_INIT;\n>>      -\tstruct strbuf buf;\n>>     @@ transport.c: static void die_with_unpushed_submodules(struct string_list *needs_\n>>      -\t\tfinish_command(&proc);\n>>      -\t\treturn -1;\n>>      -\t}\n>>     --\n>>     --\tsigchain_push(SIGPIPE, SIG_IGN);\n>>     -+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n>>     ++\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n>>       \n>>     +-\tsigchain_push(SIGPIPE, SIG_IGN);\n>>     +-\n>>      -\tstrbuf_init(&buf, 256);\n>>      +\tstrvec_push(&opt.args, transport->remote->name);\n>>      +\tstrvec_push(&opt.args, transport->url);\n>>       \n>>       \tfor (r = remote_refs; r; r = r->next) {\n>>     ++\t\tstruct strbuf buf = STRBUF_INIT;\n>\n> Ah. It is not being freed because its lifetime is being managed by\n> to_stdin instead. Ok, cool!\n>\n>>     ++\n>>       \t\tif (!r->peer_ref) continue;\n>>     -@@ transport.c: static int run_pre_push_hook(struct transport *transport,\n>>     + \t\tif (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;\n>>     + \t\tif (r->status == REF_STATUS_REJECT_STALE) continue;\n>>       \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n>>       \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n>>       \n>>      -\t\tstrbuf_reset(&buf);\n>>      -\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n>>     -+\t\tstrbuf_reset(&tmp);\n>>     -+\t\tstrbuf_addf(&tmp, \"%s %s %s %s\",\n>>     ++\t\tstrbuf_addf(&buf, \"%s %s %s %s\",\n>>       \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n>>       \t\t\t r->name, oid_to_hex(&r->old_oid));\n>>      -\n>>     @@ transport.c: static int run_pre_push_hook(struct transport *transport,\n>>      -\t\t\t\tret = -1;\n>>      -\t\t\tbreak;\n>>      -\t\t}\n>>     -+\t\tstring_list_append(&to_stdin, tmp.buf);\n>>     ++\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n>>       \t}\n>>       \n>>      -\tstrbuf_release(&buf);\n>>     @@ transport.c: static int run_pre_push_hook(struct transport *transport,\n>>      -\tx = finish_command(&proc);\n>>      -\tif (!ret)\n>>      -\t\tret = x;\n>>     -+\tret = run_hooks(\"pre-push\", &opt);\n>>     -+\trun_hooks_opt_clear(&opt);\n>>     -+\tstrbuf_release(&tmp);\n>>     ++\tret = run_hooks_oneshot(\"pre-push\", &opt);\n>>     ++\tto_stdin.strdup_strings = 1;\n>\n> And this is a typical change to the oneshot function. Cool.\n>\n>>      +\tstring_list_clear(&to_stdin, 0);\n>>       \n>>       \treturn ret;\n>>  -:  ----------- > 28:  ecf75f33233 hook tests: test for exact \"pre-push\" hook input\n>>  -:  ----------- > 29:  2c961be94b4 hook tests: use a modern style for \"pre-push\" tests\n>\n> I'll take a look at these separately.\n>\n>> 27:  331014bad17 ! 30:  1ce456f9d9d reference-transaction: use hook.h to run hooks\n>>     @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n>>       \t\t\t\tconst char *state)\n>>       {\n>>      -\tstruct child_process proc = CHILD_PROCESS_INIT;\n>>     - \tstruct strbuf buf = STRBUF_INIT;\n>>     +-\tstruct strbuf buf = STRBUF_INIT;\n>>      -\tconst char *hook;\n>>      +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>>     -+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n>>     ++\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n>>       \tint ret = 0, i;\n>>     -+\tchar o[GIT_MAX_HEXSZ + 1], n[GIT_MAX_HEXSZ + 1];\n>>       \n>>      -\thook = find_hook(\"reference-transaction\");\n>>      -\tif (!hook)\n>>     -+\tif (!hook_exists(\"reference-transaction\"))\n>>     - \t\treturn ret;\n>>     - \n>>     +-\t\treturn ret;\n>>     +-\n>>      -\tstrvec_pushl(&proc.args, hook, state, NULL);\n>>      -\tproc.in = -1;\n>>      -\tproc.stdout_to_stderr = 1;\n>>     @@ refs.c: int ref_update_reject_duplicates(struct string_list *refnames,\n>>      -\n>>      -\tret = start_command(&proc);\n>>      -\tif (ret)\n>>     --\t\treturn ret;\n>>     --\n>>     ++\tif (!hook_exists(\"reference-transaction\"))\n>>     + \t\treturn ret;\n>>     + \n>>      -\tsigchain_push(SIGPIPE, SIG_IGN);\n>>      +\tstrvec_push(&opt.args, state);\n>>       \n>>       \tfor (i = 0; i < transaction->nr; i++) {\n>>       \t\tstruct ref_update *update = transaction->updates[i];\n>>     -+\t\toid_to_hex_r(o, &update->old_oid);\n>>     -+\t\toid_to_hex_r(n, &update->new_oid);\n>>     ++\t\tstruct strbuf buf = STRBUF_INIT;\n>\n> Ah, this is doing the same thing as with 'pre-push'. Cool.\n>\n>>       \n>>     - \t\tstrbuf_reset(&buf);\n>>     +-\t\tstrbuf_reset(&buf);\n>>      -\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n>>     --\t\t\t    oid_to_hex(&update->old_oid),\n>>     --\t\t\t    oid_to_hex(&update->new_oid),\n>>     --\t\t\t    update->refname);\n>>     ++\t\tstrbuf_addf(&buf, \"%s %s %s\",\n>>     + \t\t\t    oid_to_hex(&update->old_oid),\n>>     + \t\t\t    oid_to_hex(&update->new_oid),\n>>     + \t\t\t    update->refname);\n>>      -\n>>      -\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n>>      -\t\t\tif (errno != EPIPE)\n>>      -\t\t\t\tret = -1;\n>>      -\t\t\tbreak;\n>>      -\t\t}\n>>     -+\t\tstrbuf_addf(&buf, \"%s %s %s\", o, n, update->refname);\n>>     -+\t\tstring_list_append(&to_stdin, buf.buf);\n>>     ++\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n>>       \t}\n>>       \n>>      -\tclose(proc.in);\n>>      -\tsigchain_pop(SIGPIPE);\n>>     +-\tstrbuf_release(&buf);\n>>      +\topt.feed_pipe = pipe_from_string_list;\n>>      +\topt.feed_pipe_ctx = &to_stdin;\n>>      +\n>>     -+\tret = run_hooks(\"reference-transaction\", &opt);\n>>     -+\trun_hooks_opt_clear(&opt);\n>>     - \tstrbuf_release(&buf);\n>>     ++\tret = run_hooks_oneshot(\"reference-transaction\", &opt);\n>>     ++\tto_stdin.strdup_strings = 1;\n>\n> And turning on strdup_strings here means we will free them when we call\n> string_list_clear. Ok.\n>\n>>      +\tstring_list_clear(&to_stdin, 0);\n>>       \n>>      -\tret |= finish_command(&proc);\n>> 32:  db70b59b3bd ! 35:  ceef2f3e804 receive-pack: convert receive hooks to hook.h\n>>     @@ builtin/receive-pack.c: static void hook_output_to_sideband(struct strbuf *outpu\n>>      +{\n>>      +\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>>      +\tstruct receive_hook_feed_context ctx;\n>>     -+\tint rc;\n>>      +\tstruct command *iter = commands;\n>>      +\n>>      +\t/* if there are no valid commands, don't invoke the hook at all. */\n>>     @@ builtin/receive-pack.c: static void hook_output_to_sideband(struct strbuf *outpu\n>>      +\tif (!iter)\n>>      +\t\treturn 0;\n>>      +\n>>     -+\t/* pre-receive hooks should run in series as the hook updates refs */\n>>     -+\tif (!strcmp(hook_name, \"pre-receive\"))\n>>     -+\t\topt.jobs = 1;\n>>     -+\n>\n> Hm, interesting. I'll note this as a case to watch for when I add the\n> .jobs option back in.\n>\n>\n> Thanks. I'll take a look at the newly added/copied over patches next.\n>\n>  - Emily\n\n"},{"id":"434524","messageId":"patch-v5-01.36-ac419613fdc-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 01/36] Makefile: mark \"check\" target as .PHONY","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:01Z","receivedAt":"2021-09-02T13:11:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a bug in 44c9e8594e (Fix up header file dependencies and add\nsparse checking rules, 2005-07-03), we never marked the phony \"check\"\ntarget as such.\n\nPerhaps we should just remove it, since as of a combination of\n912f9980d2 (Makefile: help people who run 'make check' by mistake,\n2008-11-11) 0bcd9ae85d (sparse: Fix errors due to missing\ntarget-specific variables, 2011-04-21) we've been suggesting the user\nrun \"make sparse\" directly.\n\nBut under that mode it still does something, as well as directing the\nuser to run \"make test\" under non-sparse. So let's punt that and\nnarrowly fix the PHONY bug.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/Makefile b/Makefile\nindex d1feab008fc..5ec23e6b59a 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2941,6 +2941,7 @@ hdr-check: $(HCO)\n style:\n \tgit clang-format --style file --diff --extensions c,h\n \n+.PHONY: check\n check: config-list.h command-list.h\n \t@if sparse; \\\n \tthen \\\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434525","messageId":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com","subject":"[PATCH v5 00/36] Run hooks via \"git run hook\" & hook library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:00Z","receivedAt":"2021-09-02T13:11:50Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This topic refactors our hook execution to use a new hook.[ch] API and\nrew \"git hook run\" tool in the case of the Perl & Python scripts that\nexecute hooks. It should contain no functional changes in how hooks\nare run.\n\nIt's mainly authored by Emily Schaffer & is followed-up by her series\nto make hooks configurable via normal \"git config\": [1]\n\nSee [2] for the v4 of this. A range-diff follows, but some of the main\nchanges:\n\n * The part where we die() if we're asked about a hook not listed in\n   githooks(5) is gone. This makes things in Emily's follow-on topic\n   much easier.\n\n * Various whitespace/small refactoring changes designed to make\n   Emily's follow-on topic smaller.\n\n * Changed \"/*\" to \"/**\" comments in hook.h as appropriate (API\n   comments).\n\n * I also re-arranged some functions in the header file to make those\n   API comments easier to follow. I.e. we start with structs, and then\n   various functions grouped by their respective functionality.\n\n * Various small commit message / comment rewording etc., some in\n   response to Emily's feedback, others that I found myself.\n\n[The rest here is a summary of how this topic interacts with a re-roll\nof Emily's topic coming after this, feel free to skip it if only\nreading this topic]:\n\nI have my own version of Emily's series rebased on top of this with\nsome significant changes / fixes, which I mainly rebased/hacked up to\nvalidate that all the changes here made sense.\n\nThat version is an extensive edit not of her v3 (which I believe maps\nto her own 30ffe98601e) but the cf1f8e34a34 tip I found at the tip of\nher \"config-based-hooks-restart\" a couple of days ago)[3], which\nalready incorporated some of my own feedback. That version is at my\navar-nasamuffin/config-based-hooks-restart-3[4].\n\nEmily: You should be able to run something like this to get a sensible\nrange-diff of it v.s. what you have now (probably with\ns/nasamuffin/origin/):\n\n    git range-diff \\\n    gitster/ab/config-based-hooks-base..nasamuffin/config-based-hooks-restart \\\n    avar/es-avar/config-based-hooks-6..avar/avar-nasamuffin/config-based-hooks-restart-3\n\nThere's a few miscellaneous fixes in there, e.g. it passes all commits\nwith SANITIZE=leak now, and the \"hook: allow running non-native hooks\"\ncommit is entirely gone except for the doc change, as it wasn't needed\nwith the changes I've made in this v5 (we'd already allow running\nnon-native hooks). Likewise all the fn() and fn_gently() function\nsplit wasn't needed anymore, as it's all \"gentle\" now. It has a\ntrivial CI failure[6] (coccinelle nitpick), but other than that passes\nall tests.\n\n1. https://lore.kernel.org/git/20210819033450.3382652-1-emilyshaffer@google.com/\n2. https://lore.kernel.org/git/cover-v4-00.36-00000000000-20210803T191505Z-avarab@gmail.com/\n3. https://github.com/nasamuffin/git/tree/config-based-hooks-restart\n4. https://github.com/avar/git/tree/avar-nasamuffin/config-based-hooks-restart-3\n5. https://github.com/avar/git/runs/3493829211\n6. https://github.com/avar/git/runs/3493829211\n\nEmily Shaffer (26):\n  hook.c: add a hook_exists() wrapper and use it in bugreport.c\n  hook: add 'run' subcommand\n  gc: use hook library for pre-auto-gc hook\n  rebase: convert pre-rebase to use hook.h\n  am: convert applypatch to use hook.h\n  hooks: convert 'post-checkout' hook to hook library\n  merge: convert post-merge to use hook.h\n  send-email: use 'git hook run' for 'sendemail-validate'\n  git-p4: use 'git hook' to run hooks\n  commit: convert {pre-commit,prepare-commit-msg} hook to hook.h\n  read-cache: convert post-index-change to use hook.h\n  receive-pack: convert push-to-checkout hook to hook.h\n  run-command: remove old run_hook_{le,ve}() hook API\n  run-command: allow stdin for run_processes_parallel\n  hook: support passing stdin to hooks\n  am: convert 'post-rewrite' hook to hook.h\n  run-command: add stdin callback for parallelization\n  hook: provide stdin by string_list or callback\n  hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n  transport: convert pre-push hook to hook.h\n  reference-transaction: use hook.h to run hooks\n  run-command: allow capturing of collated output\n  hooks: allow callers to capture output\n  receive-pack: convert 'update' hook to hook.h\n  post-update: use hook.h library\n  receive-pack: convert receive hooks to hook.h\n\nÆvar Arnfjörð Bjarmason (10):\n  Makefile: mark \"check\" target as .PHONY\n  Makefile: stop hardcoding {command,config}-list.h\n  Makefile: remove an out-of-date comment\n  hook.[ch]: move find_hook() from run-command.c to hook.c\n  hook.c users: use \"hook_exists()\" instead of \"find_hook()\"\n  hook-list.h: add a generated list of hooks, like config-list.h\n  git hook run: add an --ignore-missing flag\n  hook tests: test for exact \"pre-push\" hook input\n  hook tests: use a modern style for \"pre-push\" tests\n  hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n\n .gitignore                          |   2 +\n Documentation/git-hook.txt          |  51 +++++\n Documentation/githooks.txt          |   4 +\n Makefile                            |  26 ++-\n builtin.h                           |   1 +\n builtin/am.c                        |  29 +--\n builtin/bugreport.c                 |  46 +----\n builtin/checkout.c                  |  14 +-\n builtin/clone.c                     |   6 +-\n builtin/commit.c                    |  19 +-\n builtin/fetch.c                     |   1 +\n builtin/gc.c                        |   3 +-\n builtin/hook.c                      |  98 ++++++++++\n builtin/merge.c                     |  21 +-\n builtin/rebase.c                    |   6 +-\n builtin/receive-pack.c              | 285 +++++++++++++---------------\n builtin/submodule--helper.c         |   2 +-\n builtin/worktree.c                  |  29 ++-\n command-list.txt                    |   1 +\n commit.c                            |  16 +-\n commit.h                            |   3 +-\n compat/vcbuild/README               |   2 +-\n config.mak.uname                    |   6 +-\n contrib/buildsystems/CMakeLists.txt |   7 +\n generate-hooklist.sh                |  18 ++\n git-p4.py                           |  72 +------\n git-send-email.perl                 |  20 +-\n git.c                               |   1 +\n hook.c                              | 219 +++++++++++++++++++++\n hook.h                              | 127 +++++++++++++\n read-cache.c                        |  11 +-\n refs.c                              |  41 ++--\n reset.c                             |  14 +-\n run-command.c                       | 157 +++++++--------\n run-command.h                       |  55 +++---\n sequencer.c                         |  86 ++++-----\n submodule.c                         |   1 +\n t/helper/test-run-command.c         |  46 ++++-\n t/t0061-run-command.sh              |  37 ++++\n t/t1800-hook.sh                     | 151 +++++++++++++++\n t/t5571-pre-push-hook.sh            |  94 +++++----\n t/t9001-send-email.sh               |   4 +-\n transport.c                         |  57 ++----\n 43 files changed, 1278 insertions(+), 611 deletions(-)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100755 generate-hooklist.sh\n create mode 100644 hook.c\n create mode 100644 hook.h\n create mode 100755 t/t1800-hook.sh\n\nRange-diff against v4:\n 1:  81fe1ed90d5 =  1:  ac419613fdc Makefile: mark \"check\" target as .PHONY\n 2:  0f749530777 !  2:  a161b7f0a5c Makefile: stop hardcoding {command,config}-list.h\n    @@ Commit message\n         this refactoring we'll only need to add the new file to the\n         GENERATED_H variable, not EXCEPT_HDRS, the vcbuild/README etc.\n     \n    -    I have not tested the Windows-specific change in config.mak.uname\n    -    being made here, but we use other variables from the Makefile in the\n    -    same block, and the GENERATED_H is fully defined before we include\n    -    config.mak.uname.\n    -\n         Hardcoding command-list.h there seems to have been a case of\n         copy/paste programming in 976aaedca0 (msvc: add a Makefile target to\n         pre-generate the Visual Studio solution, 2019-07-29). The\n 3:  644b31fe281 =  3:  ffef1d3257e Makefile: remove an out-of-date comment\n 4:  89c4d44b0c3 !  4:  545e16c6f04 hook.[ch]: move find_hook() to this new library\n    @@ Metadata\n     Author: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## Commit message ##\n    -    hook.[ch]: move find_hook() to this new library\n    +    hook.[ch]: move find_hook() from run-command.c to hook.c\n     \n         Move the find_hook() function from run-command.c to a new hook.c\n         library. This change establishes a stub library that's pretty\n    @@ hook.h (new)\n     +#ifndef HOOK_H\n     +#define HOOK_H\n     +\n    -+/*\n    ++/**\n     + * Returns the path to the hook file, or NULL if the hook is missing\n     + * or disabled. Note that this points to static storage that will be\n    -+ * overwritten by further calls to find_hook and run_hook_*.\n    ++ * overwritten by further calls to find_hook().\n     + */\n     +const char *find_hook(const char *name);\n     +\n 5:  3514e0c0251 !  5:  a9bc4519e9a hook.c: add a hook_exists() wrapper and use it in bugreport.c\n    @@ hook.h\n       */\n      const char *find_hook(const char *name);\n      \n    -+/*\n    ++/**\n     + * A boolean version of find_hook()\n     + */\n     +int hook_exists(const char *hookname);\n 6:  d5ef40f77dc !  6:  e99ec2e6f8f hook.c users: use \"hook_exists()\" insted of \"find_hook()\"\n    @@ Metadata\n     Author: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## Commit message ##\n    -    hook.c users: use \"hook_exists()\" insted of \"find_hook()\"\n    +    hook.c users: use \"hook_exists()\" instead of \"find_hook()\"\n     \n         Use the new hook_exists() function instead of find_hook() where the\n         latter was called in boolean contexts. This make subsequent changes in\n 7:  4cfd72722c1 !  7:  2ffb2332c8a hook-list.h: add a generated list of hooks, like config-list.h\n    @@ Commit message\n         hook-list.h: add a generated list of hooks, like config-list.h\n     \n         Make githooks(5) the source of truth for what hooks git supports, and\n    -    die hooks we don't know about in find_hook(). This ensures that the\n    -    documentation and the C code's idea about existing hooks doesn't\n    -    diverge.\n    +    punt out early on hooks we don't know about in find_hook(). This\n    +    ensures that the documentation and the C code's idea about existing\n    +    hooks doesn't diverge.\n     \n         We still have Perl and Python code running its own hooks, but that'll\n         be addressed by Emily Shaffer's upcoming \"git hook run\" command.\n    @@ Commit message\n         listing only knowing about 1/4 of the p4 hooks. It didn't know about\n         the recent \"reference-transaction\" hook either.\n     \n    +    We could make the find_hook() function die() or BUG() out if the new\n    +    known_hook() returned 0, but let's make it return NULL just as it does\n    +    when it can't find a hook of a known type. Making it die() is overly\n    +    anal, and unlikely to be what we need in catching stupid typos in the\n    +    name of some new hook hardcoded in git.git's sources. By making this\n    +    be tolerant of unknown hook names, changes in a later series to make\n    +    \"git hook run\" run arbitrary user-configured hook names will be easier\n    +    to implement.\n    +\n         I have not been able to directly test the CMake change being made\n         here. Since 4c2c38e800 (ci: modification of main.yml to use cmake for\n         vs-build job, 2020-06-26) some of the Windows CI has a hard dependency\n    @@ generate-hooklist.sh (new)\n     +\tNULL,\n     +};\n     +EOF\n    -\n    - ## hook.c ##\n    -@@\n    - #include \"cache.h\"\n    - #include \"hook.h\"\n    - #include \"run-command.h\"\n    -+#include \"hook-list.h\"\n    -+\n    -+static int known_hook(const char *name)\n    -+{\n    -+\tconst char **p;\n    -+\tsize_t len = strlen(name);\n    -+\tfor (p = hook_name_list; *p; p++) {\n    -+\t\tconst char *hook = *p;\n    -+\n    -+\t\tif (!strncmp(name, hook, len) && hook[len] == '\\0')\n    -+\t\t\treturn 1;\n    -+\t}\n    -+\n    -+\treturn 0;\n    -+}\n    - \n    - const char *find_hook(const char *name)\n    - {\n    - \tstatic struct strbuf path = STRBUF_INIT;\n    - \n    -+\tif (!known_hook(name))\n    -+\t\tdie(_(\"the hook '%s' is not known to git, should be in hook-list.h via githooks(5)\"),\n    -+\t\t    name);\n    -+\n    - \tstrbuf_reset(&path);\n    - \tstrbuf_git_path(&path, \"hooks/%s\", name);\n    - \tif (access(path.buf, X_OK) < 0) {\n 8:  7cb4a4cb69e !  8:  72dd1010f5b hook: add 'run' subcommand\n    @@ builtin/hook.c (new)\n     +\t/* Need to take into account core.hooksPath */\n     +\tgit_config(git_default_config, NULL);\n     +\n    ++\t/*\n    ++\t * We are not using a plain run_hooks() because we'd like to\n    ++\t * detect missing hooks. Let's find it ourselves and call\n    ++\t * run_hooks() instead.\n    ++\t */\n     +\thook_name = argv[0];\n     +\thook_path = find_hook(hook_name);\n     +\tif (!hook_path) {\n    @@ git.c: static struct cmd_struct commands[] = {\n     \n      ## hook.c ##\n     @@\n    + #include \"cache.h\"\n      #include \"hook.h\"\n      #include \"run-command.h\"\n    - #include \"hook-list.h\"\n     +#include \"config.h\"\n      \n    - static int known_hook(const char *name)\n    + const char *find_hook(const char *name)\n      {\n    - \tconst char **p;\n    - \tsize_t len = strlen(name);\n    -+\tstatic int test_hooks_ok = -1;\n    -+\n    - \tfor (p = hook_name_list; *p; p++) {\n    - \t\tconst char *hook = *p;\n    - \n    -@@ hook.c: static int known_hook(const char *name)\n    - \t\t\treturn 1;\n    - \t}\n    - \n    -+\tif (test_hooks_ok == -1)\n    -+\t\ttest_hooks_ok = git_env_bool(\"GIT_TEST_FAKE_HOOKS\", 0);\n    -+\n    -+\tif (test_hooks_ok &&\n    -+\t    (!strcmp(name, \"test-hook\") ||\n    -+\t     !strcmp(name, \"does-not-exist\")))\n    -+\t\treturn 1;\n    -+\n    - \treturn 0;\n    - }\n    - \n     @@ hook.c: int hook_exists(const char *name)\n      {\n      \treturn !!find_hook(name);\n    @@ hook.h\n     @@\n      #ifndef HOOK_H\n      #define HOOK_H\n    -+#include \"strbuf.h\"\n     +#include \"strvec.h\"\n    -+#include \"run-command.h\"\n    - \n    - /*\n    -  * Returns the path to the hook file, or NULL if the hook is missing\n    -@@ hook.h: const char *find_hook(const char *name);\n    -  */\n    - int hook_exists(const char *hookname);\n    - \n    ++\n     +struct hook {\n     +\t/* The path to the hook */\n     +\tconst char *hook_path;\n    @@ hook.h: const char *find_hook(const char *name);\n     +\t.args = STRVEC_INIT, \\\n     +}\n     +\n    -+/*\n    -+ * Callback provided to feed_pipe_fn and consume_sideband_fn.\n    -+ */\n     +struct hook_cb_data {\n     +\t/* rc reflects the cumulative failure state */\n     +\tint rc;\n    @@ hook.h: const char *find_hook(const char *name);\n     +\tstruct hook *run_me;\n     +\tstruct run_hooks_opt *options;\n     +};\n    -+\n    + \n    + /**\n    +  * Returns the path to the hook file, or NULL if the hook is missing\n    +@@ hook.h: const char *find_hook(const char *name);\n    +  */\n    + int hook_exists(const char *hookname);\n    + \n    ++/**\n    ++ * Clear data from an initialized \"struct run_hooks-opt\".\n    ++ */\n     +void run_hooks_opt_clear(struct run_hooks_opt *o);\n     +\n     +/**\n    @@ hook.h: const char *find_hook(const char *name);\n     \n      ## t/t1800-hook.sh (new) ##\n     @@\n    -+#!/bin/bash\n    ++#!/bin/sh\n     +\n     +test_description='git-hook command'\n     +\n    @@ t/t1800-hook.sh (new)\n     +\tgrep \"unknown option\" err\n     +'\n     +\n    -+test_expect_success 'setup GIT_TEST_FAKE_HOOKS=true to permit \"test-hook\" and \"does-not-exist\" names\"' '\n    -+\tGIT_TEST_FAKE_HOOKS=true &&\n    -+\texport GIT_TEST_FAKE_HOOKS\n    -+'\n    -+\n     +test_expect_success 'git hook run: nonexistent hook' '\n     +\tcat >stderr.expect <<-\\EOF &&\n     +\terror: cannot find a hook named test-hook\n 9:  2b8500aa675 !  9:  821cc9bf11e gc: use hook library for pre-auto-gc hook\n    @@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n     +\t}\n     +\n     +\tret = run_hooks(hook_name, hook_path, options);\n    ++\n     +cleanup:\n     +\trun_hooks_opt_clear(options);\n    ++\n     +\treturn ret;\n     +}\n     \n    @@ hook.h: void run_hooks_opt_clear(struct run_hooks_opt *o);\n     + * with run_hooks().\n     + *\n     + * If \"options\" is provided calls run_hooks_opt_clear() on it for\n    -+ * you. If \"options\" is NULL a scratch one will be provided for you\n    -+ * before calling run_hooks().\n    ++ * you. If \"options\" is NULL the default options from\n    ++ * RUN_HOOKS_OPT_INIT will be used.\n     + */\n     +int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n     +\n10:  3ee55d2c10f = 10:  d71c90254ea rebase: convert pre-rebase to use hook.h\n11:  050f20d14f0 = 11:  ea3af2ccc4d am: convert applypatch to use hook.h\n12:  ac875d284da ! 12:  fed0b52f88f hooks: convert 'post-checkout' hook to hook library\n    @@ hook.h: struct run_hooks_opt\n      \t/* Args to be passed to each hook */\n      \tstruct strvec args;\n     +\n    -+\t/* Resolve and run the \"absolute_path(hook)\" instead of\n    ++\t/*\n    ++\t * Resolve and run the \"absolute_path(hook)\" instead of\n     +\t * \"hook\". Used for \"git worktree\" hooks\n     +\t */\n     +\tint absolute_path;\n13:  69763bc2255 = 13:  53d8721a0e3 merge: convert post-merge to use hook.h\n14:  2ca1ca1b8e4 ! 14:  d60827a2856 git hook run: add an --ignore-missing flag\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \t};\n      \tint ret;\n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n    - \tgit_config(git_default_config, NULL);\n    - \n    + \t/*\n    + \t * We are not using a plain run_hooks() because we'd like to\n    + \t * detect missing hooks. Let's find it ourselves and call\n    +-\t * run_hooks() instead.\n    ++\t * run_hooks() instead...\n    + \t */\n      \thook_name = argv[0];\n     +\tif (ignore_missing)\n    ++\t\t/* ... act like a plain run_hooks() under --ignore-missing */\n     +\t\treturn run_hooks_oneshot(hook_name, &opt);\n      \thook_path = find_hook(hook_name);\n      \tif (!hook_path) {\n15:  5b66b04bec7 = 15:  d4976a0821f send-email: use 'git hook run' for 'sendemail-validate'\n16:  14a37a43db2 = 16:  99f3dcd1945 git-p4: use 'git hook' to run hooks\n17:  ad5d0e0e7de = 17:  509761454e6 commit: convert {pre-commit,prepare-commit-msg} hook to hook.h\n18:  3d3a33e2674 = 18:  e2c94d95427 read-cache: convert post-index-change to use hook.h\n19:  893f8666301 ! 19:  fa7d0d24ea2 receive-pack: convert push-to-checkout hook to hook.h\n    @@ Commit message\n         Move the push-to-checkout hook away from run-command.h to and over to\n         the new hook.h library.\n     \n    +    This removes the last direct user of run_hook_le(), so we could remove\n    +    that function now, but let's leave that to a follow-up cleanup commit.\n    +\n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n20:  070433deba5 = 20:  428bb5a6792 run-command: remove old run_hook_{le,ve}() hook API\n21:  1028e0c1667 = 21:  994f6ad8602 run-command: allow stdin for run_processes_parallel\n22:  639e59e9ed0 = 22:  3ccc654a664 hook: support passing stdin to hooks\n23:  7d1925cca48 = 23:  f548e3d15e7 am: convert 'post-rewrite' hook to hook.h\n24:  0c24221b522 = 24:  bb119fa7cc0 run-command: add stdin callback for parallelization\n25:  05d1085f7eb ! 25:  2439f7752b8 hook: provide stdin by string_list or callback\n    @@ hook.c: int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *optio\n      \t\tret = 0;\n     \n      ## hook.h ##\n    -@@ hook.h: int hook_exists(const char *hookname);\n    +@@\n    + #ifndef HOOK_H\n    + #define HOOK_H\n    ++#include \"strbuf.h\"\n    + #include \"strvec.h\"\n    ++#include \"run-command.h\"\n    + \n      struct hook {\n      \t/* The path to the hook */\n      \tconst char *hook_path;\n    @@ hook.h: struct run_hooks_opt\n      };\n      \n      #define RUN_HOOKS_OPT_INIT { \\\n    -@@ hook.h: struct run_hooks_opt\n    - \t.args = STRVEC_INIT, \\\n    - }\n    +@@ hook.h: int run_hooks(const char *hookname, const char *hook_path,\n    +  */\n    + int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n      \n    -+/*\n    ++/**\n     + * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n    -+ * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n    ++ * string_list and set 'run_hooks_opt.feed_pipe' to pipe_from_string_list().\n     + * This will pipe each string in the list to stdin, separated by newlines.  (Do\n     + * not inject your own newlines.)\n     + */\n     +int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n     +\n    - /*\n    -  * Callback provided to feed_pipe_fn and consume_sideband_fn.\n    -  */\n    + #endif\n26:  4b7175af2e5 = 26:  48a380b3a91 hook: convert 'post-rewrite' hook in sequencer.c to hook.h\n27:  3f24e056410 = 27:  af6b9292aaa transport: convert pre-push hook to hook.h\n28:  ecf75f33233 ! 28:  957691f0b6d hook tests: test for exact \"pre-push\" hook input\n    @@ Commit message\n     \n         Extend the tests added in ec55559f937 (push: Add support for pre-push\n         hooks, 2013-01-13) to exhaustively test for the exact input we're\n    -    expecting. This helps a parallel series that's refactoring how the\n    -    hook is called, to e.g. make sure that we don't miss a trailing\n    -    newline.\n    +    expecting. This ensures that we e.g. don't miss a trailing newline.\n    +\n    +    Appending to a file called \"actual\" is the established convention in\n    +    this test for hooks, see the rest of the tests added in\n    +    ec55559f937 (push: Add support for pre-push hooks, 2013-01-13). Let's\n    +    follow that convention here.\n     \n         Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n29:  2c961be94b4 ! 29:  88fe2621549 hook tests: use a modern style for \"pre-push\" tests\n    @@ t/t5571-pre-push-hook.sh: echo \"$2\" >>actual\n     -EOF\n     -\n      test_expect_success 'push with hook' '\n    -+\tcat >expected <<-EOF &&\n    ++\tcat >expect <<-EOF &&\n     +\tparent1\n     +\trepo1\n     +\trefs/heads/main $COMMIT2 refs/heads/foreign $COMMIT1\n    @@ t/t5571-pre-push-hook.sh: echo \"$2\" >>actual\n     +\n      \tgit push parent1 main:foreign &&\n     -\tdiff expected actual\n    -+\ttest_cmp expected actual\n    ++\ttest_cmp expect actual\n      '\n      \n      test_expect_success 'add a branch' '\n30:  1ce456f9d9d = 30:  1d905e81779 reference-transaction: use hook.h to run hooks\n31:  6e5f1f5bd3a = 31:  fac56a9d8af run-command: allow capturing of collated output\n32:  0b6e9c6d07a = 32:  7d185cdf9d1 hooks: allow callers to capture output\n33:  dcf63634338 = 33:  c8150e1239f receive-pack: convert 'update' hook to hook.h\n34:  f352a485e59 = 34:  a20ad847c14 post-update: use hook.h library\n35:  ceef2f3e804 = 35:  79c380be6ed receive-pack: convert receive hooks to hook.h\n36:  b71d7628b40 ! 36:  fe056098534 hooks: fix a TOCTOU in \"did we run a hook?\" heuristic\n    @@ hook.h: struct hook_cb_data {\n     +\tint *invoked_hook;\n      };\n      \n    - void run_hooks_opt_clear(struct run_hooks_opt *o);\n    + /**\n     \n      ## sequencer.c ##\n     @@ sequencer.c: static int run_prepare_commit_msg_hook(struct repository *r,\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434526","messageId":"patch-v5-02.36-a161b7f0a5c-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 02/36] Makefile: stop hardcoding {command,config}-list.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:02Z","receivedAt":"2021-09-02T13:11:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Change various places that hardcode the names of these two files to\nrefer to either $(GENERATED_H), or to a new generated-hdrs\ntarget. That target is consistent with the *-objs targets I recently\nadded in 029bac01a8 (Makefile: add {program,xdiff,test,git,fuzz}-objs\n& objects targets, 2021-02-23).\n\nA subsequent commit will add a new generated hook-list.h. By doing\nthis refactoring we'll only need to add the new file to the\nGENERATED_H variable, not EXCEPT_HDRS, the vcbuild/README etc.\n\nHardcoding command-list.h there seems to have been a case of\ncopy/paste programming in 976aaedca0 (msvc: add a Makefile target to\npre-generate the Visual Studio solution, 2019-07-29). The\nconfig-list.h was added later in 709df95b78 (help: move\nlist_config_help to builtin/help, 2020-04-16).\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile              | 6 ++++--\n compat/vcbuild/README | 2 +-\n config.mak.uname      | 6 +++---\n 3 files changed, 8 insertions(+), 6 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 5ec23e6b59a..4f1f13b5f0a 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -824,6 +824,8 @@ XDIFF_LIB = xdiff/lib.a\n \n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n+.PHONY: generated-hdrs\n+generated-hdrs: $(GENERATED_H)\n \n LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentation/' 2>/dev/null || \\\n \t$(FIND) . \\\n@@ -2919,7 +2921,7 @@ $(SP_OBJ): %.sp: %.c GIT-CFLAGS FORCE\n .PHONY: sparse $(SP_OBJ)\n sparse: $(SP_OBJ)\n \n-EXCEPT_HDRS := command-list.h config-list.h unicode-width.h compat/% xdiff/%\n+EXCEPT_HDRS := $(GENERATED_H) unicode-width.h compat/% xdiff/%\n ifndef GCRYPT_SHA256\n \tEXCEPT_HDRS += sha256/gcrypt.h\n endif\n@@ -2942,7 +2944,7 @@ style:\n \tgit clang-format --style file --diff --extensions c,h\n \n .PHONY: check\n-check: config-list.h command-list.h\n+check: $(GENERATED_H)\n \t@if sparse; \\\n \tthen \\\n \t\techo >&2 \"Use 'make sparse' instead\"; \\\ndiff --git a/compat/vcbuild/README b/compat/vcbuild/README\nindex 51fb083dbbe..29ec1d0f104 100644\n--- a/compat/vcbuild/README\n+++ b/compat/vcbuild/README\n@@ -92,7 +92,7 @@ The Steps of Build Git with VS2008\n    the git operations.\n \n 3. Inside Git's directory run the command:\n-       make command-list.h config-list.h\n+       make generated-hdrs\n    to generate the header file needed to compile git.\n \n 4. Then either build Git with the GNU Make Makefile in the Git projects\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 76516aaa9a5..8aac06eb094 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -735,9 +735,9 @@ vcxproj:\n \t echo '</Project>') >git-remote-http/LinkOrCopyRemoteHttp.targets\n \tgit add -f git/LinkOrCopyBuiltins.targets git-remote-http/LinkOrCopyRemoteHttp.targets\n \n-\t# Add command-list.h and config-list.h\n-\t$(MAKE) MSVC=1 SKIP_VCPKG=1 prefix=/mingw64 config-list.h command-list.h\n-\tgit add -f config-list.h command-list.h\n+\t# Add generated headers\n+\t$(MAKE) MSVC=1 SKIP_VCPKG=1 prefix=/mingw64 $(GENERATED_H)\n+\tgit add -f $(GENERATED_H)\n \n \t# Add scripts\n \trm -f perl/perl.mak\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434527","messageId":"patch-v5-03.36-ffef1d3257e-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 03/36] Makefile: remove an out-of-date comment","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:03Z","receivedAt":"2021-09-02T13:11:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This comment added in dfea575017 (Makefile: lazily compute header\ndependencies, 2010-01-26) has been out of date since\n92b88eba9f (Makefile: use `git ls-files` to list header files, if\npossible, 2019-03-04), when we did exactly what it tells us not to do\nand added $(GENERATED_H) to $(OBJECTS) dependencies.\n\nThe rest of it was also somewhere between inaccurate and outdated,\nsince as of b8ba629264 (Makefile: fold MISC_H into LIB_H, 2012-06-20)\nit's not followed by a list of header files, that got moved earlier in\nthe file into LIB_H in 60d24dd255 (Makefile: fold XDIFF_H and VCSSVN_H\ninto LIB_H, 2012-07-06).\n\nLet's just remove it entirely, to the extent that we have anything\nuseful to say here the comment on the\n\"USE_COMPUTED_HEADER_DEPENDENCIES\" variable a few lines above this\nchange does the job for us.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile | 7 -------\n 1 file changed, 7 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 4f1f13b5f0a..53c67f4a726 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2524,13 +2524,6 @@ ifneq ($(dep_files_present),)\n include $(dep_files_present)\n endif\n else\n-# Dependencies on header files, for platforms that do not support\n-# the gcc -MMD option.\n-#\n-# Dependencies on automatically generated headers such as command-list.h\n-# should _not_ be included here, since they are necessary even when\n-# building an object for the first time.\n-\n $(OBJECTS): $(LIB_H) $(GENERATED_H)\n endif\n \n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434528","messageId":"patch-v5-04.36-545e16c6f04-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 04/36] hook.[ch]: move find_hook() from run-command.c to hook.c","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:04Z","receivedAt":"2021-09-02T13:11:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Move the find_hook() function from run-command.c to a new hook.c\nlibrary. This change establishes a stub library that's pretty\npointless right now, but will see much wider use with Emily Shaffer's\nupcoming \"configuration-based hooks\" series.\n\nEventually all the hook related code will live in hook.[ch]. Let's\nstart that process by moving the simple find_hook() function over\nas-is.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Makefile               |  1 +\n builtin/am.c           |  1 +\n builtin/bugreport.c    |  2 +-\n builtin/commit.c       |  1 +\n builtin/merge.c        |  1 +\n builtin/receive-pack.c |  1 +\n builtin/worktree.c     |  1 +\n hook.c                 | 37 +++++++++++++++++++++++++++++++++++++\n hook.h                 | 11 +++++++++++\n refs.c                 |  1 +\n run-command.c          | 35 +----------------------------------\n run-command.h          |  7 -------\n sequencer.c            |  1 +\n transport.c            |  1 +\n 14 files changed, 59 insertions(+), 42 deletions(-)\n create mode 100644 hook.c\n create mode 100644 hook.h\n\ndiff --git a/Makefile b/Makefile\nindex 53c67f4a726..ae256672780 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -911,6 +911,7 @@ LIB_OBJS += hash-lookup.o\n LIB_OBJS += hashmap.o\n LIB_OBJS += help.o\n LIB_OBJS += hex.o\n+LIB_OBJS += hook.o\n LIB_OBJS += ident.o\n LIB_OBJS += json-writer.o\n LIB_OBJS += kwset.o\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 0c2ad96b70e..c603f3cebdf 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -11,6 +11,7 @@\n #include \"parse-options.h\"\n #include \"dir.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"quote.h\"\n #include \"tempfile.h\"\n #include \"lockfile.h\"\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 9915a5841de..596f079a7f9 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -3,7 +3,7 @@\n #include \"strbuf.h\"\n #include \"help.h\"\n #include \"compat/compiler.h\"\n-#include \"run-command.h\"\n+#include \"hook.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 243c626307c..26c8fad6366 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -19,6 +19,7 @@\n #include \"revision.h\"\n #include \"wt-status.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"refs.h\"\n #include \"log-tree.h\"\n #include \"strbuf.h\"\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex febb0c99c93..2fc78f54e43 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -13,6 +13,7 @@\n #include \"builtin.h\"\n #include \"lockfile.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"diff.h\"\n #include \"diff-merges.h\"\n #include \"refs.h\"\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 2d1f97e1ca7..97aebdc15bd 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -7,6 +7,7 @@\n #include \"pkt-line.h\"\n #include \"sideband.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"commit.h\"\n #include \"object.h\"\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 0d0a80da61f..d22ece93e1a 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -8,6 +8,7 @@\n #include \"branch.h\"\n #include \"refs.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"sigchain.h\"\n #include \"submodule.h\"\n #include \"utf8.h\"\ndiff --git a/hook.c b/hook.c\nnew file mode 100644\nindex 00000000000..c4dbef1d0ef\n--- /dev/null\n+++ b/hook.c\n@@ -0,0 +1,37 @@\n+#include \"cache.h\"\n+#include \"hook.h\"\n+#include \"run-command.h\"\n+\n+const char *find_hook(const char *name)\n+{\n+\tstatic struct strbuf path = STRBUF_INIT;\n+\n+\tstrbuf_reset(&path);\n+\tstrbuf_git_path(&path, \"hooks/%s\", name);\n+\tif (access(path.buf, X_OK) < 0) {\n+\t\tint err = errno;\n+\n+#ifdef STRIP_EXTENSION\n+\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n+\t\tif (access(path.buf, X_OK) >= 0)\n+\t\t\treturn path.buf;\n+\t\tif (errno == EACCES)\n+\t\t\terr = errno;\n+#endif\n+\n+\t\tif (err == EACCES && advice_ignored_hook) {\n+\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n+\n+\t\t\tif (!string_list_lookup(&advise_given, name)) {\n+\t\t\t\tstring_list_insert(&advise_given, name);\n+\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n+\t\t\t\t\t \"it's not set as executable.\\n\"\n+\t\t\t\t\t \"You can disable this warning with \"\n+\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n+\t\t\t\t       path.buf);\n+\t\t\t}\n+\t\t}\n+\t\treturn NULL;\n+\t}\n+\treturn path.buf;\n+}\ndiff --git a/hook.h b/hook.h\nnew file mode 100644\nindex 00000000000..56a7b3c9164\n--- /dev/null\n+++ b/hook.h\n@@ -0,0 +1,11 @@\n+#ifndef HOOK_H\n+#define HOOK_H\n+\n+/**\n+ * Returns the path to the hook file, or NULL if the hook is missing\n+ * or disabled. Note that this points to static storage that will be\n+ * overwritten by further calls to find_hook().\n+ */\n+const char *find_hook(const char *name);\n+\n+#endif\ndiff --git a/refs.c b/refs.c\nindex 8b9f7c3a80a..6211692eaae 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -10,6 +10,7 @@\n #include \"refs.h\"\n #include \"refs/refs-internal.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"object-store.h\"\n #include \"object.h\"\n #include \"tag.h\"\ndiff --git a/run-command.c b/run-command.c\nindex f72e72cce73..352f5be1646 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -8,6 +8,7 @@\n #include \"string-list.h\"\n #include \"quote.h\"\n #include \"config.h\"\n+#include \"hook.h\"\n \n void child_process_init(struct child_process *child)\n {\n@@ -1319,40 +1320,6 @@ int async_with_fork(void)\n #endif\n }\n \n-const char *find_hook(const char *name)\n-{\n-\tstatic struct strbuf path = STRBUF_INIT;\n-\n-\tstrbuf_reset(&path);\n-\tstrbuf_git_path(&path, \"hooks/%s\", name);\n-\tif (access(path.buf, X_OK) < 0) {\n-\t\tint err = errno;\n-\n-#ifdef STRIP_EXTENSION\n-\t\tstrbuf_addstr(&path, STRIP_EXTENSION);\n-\t\tif (access(path.buf, X_OK) >= 0)\n-\t\t\treturn path.buf;\n-\t\tif (errno == EACCES)\n-\t\t\terr = errno;\n-#endif\n-\n-\t\tif (err == EACCES && advice_ignored_hook) {\n-\t\t\tstatic struct string_list advise_given = STRING_LIST_INIT_DUP;\n-\n-\t\t\tif (!string_list_lookup(&advise_given, name)) {\n-\t\t\t\tstring_list_insert(&advise_given, name);\n-\t\t\t\tadvise(_(\"The '%s' hook was ignored because \"\n-\t\t\t\t\t \"it's not set as executable.\\n\"\n-\t\t\t\t\t \"You can disable this warning with \"\n-\t\t\t\t\t \"`git config advice.ignoredHook false`.\"),\n-\t\t\t\t       path.buf);\n-\t\t\t}\n-\t\t}\n-\t\treturn NULL;\n-\t}\n-\treturn path.buf;\n-}\n-\n int run_hook_ve(const char *const *env, const char *name, va_list args)\n {\n \tstruct child_process hook = CHILD_PROCESS_INIT;\ndiff --git a/run-command.h b/run-command.h\nindex af1296769f9..f76b740f927 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -204,13 +204,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/*\n- * Returns the path to the hook file, or NULL if the hook is missing\n- * or disabled. Note that this points to static storage that will be\n- * overwritten by further calls to find_hook and run_hook_*.\n- */\n-const char *find_hook(const char *name);\n-\n /**\n  * Run a hook.\n  * The first argument is a pathname to an index file, or NULL\ndiff --git a/sequencer.c b/sequencer.c\nindex 1ceb4c0d7fd..3214f02f393 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -8,6 +8,7 @@\n #include \"sequencer.h\"\n #include \"tag.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"exec-cmd.h\"\n #include \"utf8.h\"\n #include \"cache-tree.h\"\ndiff --git a/transport.c b/transport.c\nindex 17e9629710a..77e196f75f5 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -2,6 +2,7 @@\n #include \"config.h\"\n #include \"transport.h\"\n #include \"run-command.h\"\n+#include \"hook.h\"\n #include \"pkt-line.h\"\n #include \"fetch-pack.h\"\n #include \"remote.h\"\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434529","messageId":"patch-v5-05.36-a9bc4519e9a-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 05/36] hook.c: add a hook_exists() wrapper and use it in bugreport.c","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:05Z","receivedAt":"2021-09-02T13:12:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nAdd a boolean version of the find_hook() function for those callers\nwho are only interested in checking whether the hook exists, not what\nthe path to it is.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/bugreport.c | 2 +-\n hook.c              | 5 +++++\n hook.h              | 5 +++++\n 3 files changed, 11 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 596f079a7f9..941c8d5e270 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -82,7 +82,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t}\n \n \tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (find_hook(hook[i]))\n+\t\tif (hook_exists(hook[i]))\n \t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n }\n \ndiff --git a/hook.c b/hook.c\nindex c4dbef1d0ef..97cd799a320 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -35,3 +35,8 @@ const char *find_hook(const char *name)\n \t}\n \treturn path.buf;\n }\n+\n+int hook_exists(const char *name)\n+{\n+\treturn !!find_hook(name);\n+}\ndiff --git a/hook.h b/hook.h\nindex 56a7b3c9164..368754b1201 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -8,4 +8,9 @@\n  */\n const char *find_hook(const char *name);\n \n+/**\n+ * A boolean version of find_hook()\n+ */\n+int hook_exists(const char *hookname);\n+\n #endif\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434530","messageId":"patch-v5-06.36-e99ec2e6f8f-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 06/36] hook.c users: use \"hook_exists()\" instead of \"find_hook()\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:06Z","receivedAt":"2021-09-02T13:12:01Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Use the new hook_exists() function instead of find_hook() where the\nlatter was called in boolean contexts. This make subsequent changes in\na series where we further refactor the hook API clearer, as we won't\nconflate wanting to get the path of the hook with checking for its\nexistence.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c       | 2 +-\n builtin/merge.c        | 2 +-\n builtin/receive-pack.c | 2 +-\n sequencer.c            | 2 +-\n 4 files changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 26c8fad6366..f6ca9d04c78 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1052,7 +1052,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && find_hook(\"pre-commit\")) {\n+\tif (!no_verify && hook_exists(\"pre-commit\")) {\n \t\t/*\n \t\t * Re-read the index as pre-commit hook could have updated it,\n \t\t * and write it out as a tree.  We must do this before we invoke\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 2fc78f54e43..d75a438298b 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -850,7 +850,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t * and write it out as a tree.  We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (find_hook(\"pre-merge-commit\"))\n+\tif (hook_exists(\"pre-merge-commit\"))\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 97aebdc15bd..91fa799b66e 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1464,7 +1464,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!find_hook(push_to_checkout_hook))\n+\tif (!hook_exists(push_to_checkout_hook))\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n \telse\n \t\tretval = push_to_checkout(sha1, &env, work_tree);\ndiff --git a/sequencer.c b/sequencer.c\nindex 3214f02f393..8999edf8d7b 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1446,7 +1446,7 @@ static int try_to_commit(struct repository *r,\n \t\t}\n \t}\n \n-\tif (find_hook(\"prepare-commit-msg\")) {\n+\tif (hook_exists(\"prepare-commit-msg\")) {\n \t\tres = run_prepare_commit_msg_hook(r, msg, hook_commit);\n \t\tif (res)\n \t\t\tgoto out;\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434531","messageId":"patch-v5-07.36-2ffb2332c8a-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 07/36] hook-list.h: add a generated list of hooks, like config-list.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:07Z","receivedAt":"2021-09-02T13:12:03Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Make githooks(5) the source of truth for what hooks git supports, and\npunt out early on hooks we don't know about in find_hook(). This\nensures that the documentation and the C code's idea about existing\nhooks doesn't diverge.\n\nWe still have Perl and Python code running its own hooks, but that'll\nbe addressed by Emily Shaffer's upcoming \"git hook run\" command.\n\nThis resolves a long-standing TODO item in bugreport.c of there being\nno centralized listing of hooks, and fixes a bug with the bugreport\nlisting only knowing about 1/4 of the p4 hooks. It didn't know about\nthe recent \"reference-transaction\" hook either.\n\nWe could make the find_hook() function die() or BUG() out if the new\nknown_hook() returned 0, but let's make it return NULL just as it does\nwhen it can't find a hook of a known type. Making it die() is overly\nanal, and unlikely to be what we need in catching stupid typos in the\nname of some new hook hardcoded in git.git's sources. By making this\nbe tolerant of unknown hook names, changes in a later series to make\n\"git hook run\" run arbitrary user-configured hook names will be easier\nto implement.\n\nI have not been able to directly test the CMake change being made\nhere. Since 4c2c38e800 (ci: modification of main.yml to use cmake for\nvs-build job, 2020-06-26) some of the Windows CI has a hard dependency\non CMake, this change works there, and is to my eyes an obviously\ncorrect use of a pattern established in previous CMake changes,\nnamely:\n\n - 061c2240b1 (Introduce CMake support for configuring Git,\n    2020-06-12)\n - 709df95b78 (help: move list_config_help to builtin/help,\n    2020-04-16)\n - 976aaedca0 (msvc: add a Makefile target to pre-generate the Visual\n   Studio solution, 2019-07-29)\n\nThe LC_ALL=C is needed because at least in my locale the dash (\"-\") is\nignored for the purposes of sorting, which results in a different\norder. I'm not aware of anything in git that has a hard dependency on\nthe order, but e.g. the bugreport output would end up using whatever\nlocale was in effect when git was compiled.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nHelped-by: René Scharfe <l.s.r@web.de>\n---\n .gitignore                          |  1 +\n Makefile                            | 10 ++++++-\n builtin/bugreport.c                 | 44 ++++++-----------------------\n contrib/buildsystems/CMakeLists.txt |  7 +++++\n generate-hooklist.sh                | 18 ++++++++++++\n 5 files changed, 43 insertions(+), 37 deletions(-)\n create mode 100755 generate-hooklist.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 311841f9bed..6be9de41ae8 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -190,6 +190,7 @@\n /gitweb/static/gitweb.min.*\n /config-list.h\n /command-list.h\n+/hook-list.h\n *.tar.gz\n *.dsc\n *.deb\ndiff --git a/Makefile b/Makefile\nindex ae256672780..ed06b4e9634 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -824,6 +824,8 @@ XDIFF_LIB = xdiff/lib.a\n \n GENERATED_H += command-list.h\n GENERATED_H += config-list.h\n+GENERATED_H += hook-list.h\n+\n .PHONY: generated-hdrs\n generated-hdrs: $(GENERATED_H)\n \n@@ -2231,7 +2233,9 @@ git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS) $(GITLIBS)\n \n help.sp help.s help.o: command-list.h\n \n-builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX\n+hook.sp hook.s hook.o: hook-list.h\n+\n+builtin/help.sp builtin/help.s builtin/help.o: config-list.h hook-list.h GIT-PREFIX\n builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \\\n \t'-DGIT_HTML_PATH=\"$(htmldir_relative_SQ)\"' \\\n \t'-DGIT_MAN_PATH=\"$(mandir_relative_SQ)\"' \\\n@@ -2264,6 +2268,10 @@ command-list.h: $(wildcard Documentation/git*.txt)\n \t\t$(patsubst %,--exclude-program %,$(EXCLUDED_PROGRAMS)) \\\n \t\tcommand-list.txt >$@+ && mv $@+ $@\n \n+hook-list.h: generate-hooklist.sh Documentation/githooks.txt\n+\t$(QUIET_GEN)$(SHELL_PATH) ./generate-hooklist.sh \\\n+\t\t>$@+ && mv $@+ $@\n+\n SCRIPT_DEFINES = $(SHELL_PATH_SQ):$(DIFF_SQ):$(GIT_VERSION):\\\n \t$(localedir_SQ):$(NO_CURL):$(USE_GETTEXT_SCHEME):$(SANE_TOOL_PATH_SQ):\\\n \t$(gitwebdir_SQ):$(PERL_PATH_SQ):$(SANE_TEXT_GREP):$(PAGER_ENV):\\\ndiff --git a/builtin/bugreport.c b/builtin/bugreport.c\nindex 941c8d5e270..a7a1fcb8a7a 100644\n--- a/builtin/bugreport.c\n+++ b/builtin/bugreport.c\n@@ -4,6 +4,7 @@\n #include \"help.h\"\n #include \"compat/compiler.h\"\n #include \"hook.h\"\n+#include \"hook-list.h\"\n \n \n static void get_system_info(struct strbuf *sys_info)\n@@ -41,39 +42,7 @@ static void get_system_info(struct strbuf *sys_info)\n \n static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n {\n-\t/*\n-\t * NEEDSWORK: Doesn't look like there is a list of all possible hooks;\n-\t * so below is a transcription of `git help hooks`. Later, this should\n-\t * be replaced with some programmatically generated list (generated from\n-\t * doc or else taken from some library which tells us about all the\n-\t * hooks)\n-\t */\n-\tstatic const char *hook[] = {\n-\t\t\"applypatch-msg\",\n-\t\t\"pre-applypatch\",\n-\t\t\"post-applypatch\",\n-\t\t\"pre-commit\",\n-\t\t\"pre-merge-commit\",\n-\t\t\"prepare-commit-msg\",\n-\t\t\"commit-msg\",\n-\t\t\"post-commit\",\n-\t\t\"pre-rebase\",\n-\t\t\"post-checkout\",\n-\t\t\"post-merge\",\n-\t\t\"pre-push\",\n-\t\t\"pre-receive\",\n-\t\t\"update\",\n-\t\t\"post-receive\",\n-\t\t\"post-update\",\n-\t\t\"push-to-checkout\",\n-\t\t\"pre-auto-gc\",\n-\t\t\"post-rewrite\",\n-\t\t\"sendemail-validate\",\n-\t\t\"fsmonitor-watchman\",\n-\t\t\"p4-pre-submit\",\n-\t\t\"post-index-change\",\n-\t};\n-\tint i;\n+\tconst char **p;\n \n \tif (nongit) {\n \t\tstrbuf_addstr(hook_info,\n@@ -81,9 +50,12 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)\n \t\treturn;\n \t}\n \n-\tfor (i = 0; i < ARRAY_SIZE(hook); i++)\n-\t\tif (hook_exists(hook[i]))\n-\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook[i]);\n+\tfor (p = hook_name_list; *p; p++) {\n+\t\tconst char *hook = *p;\n+\n+\t\tif (hook_exists(hook))\n+\t\t\tstrbuf_addf(hook_info, \"%s\\n\", hook);\n+\t}\n }\n \n static const char * const bugreport_usage[] = {\ndiff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt\nindex 171b4124afe..fd1399c440f 100644\n--- a/contrib/buildsystems/CMakeLists.txt\n+++ b/contrib/buildsystems/CMakeLists.txt\n@@ -624,6 +624,13 @@ if(NOT EXISTS ${CMAKE_BINARY_DIR}/config-list.h)\n \t\t\tOUTPUT_FILE ${CMAKE_BINARY_DIR}/config-list.h)\n endif()\n \n+if(NOT EXISTS ${CMAKE_BINARY_DIR}/hook-list.h)\n+\tmessage(\"Generating hook-list.h\")\n+\texecute_process(COMMAND ${SH_EXE} ${CMAKE_SOURCE_DIR}/generate-hooklist.sh\n+\t\t\tWORKING_DIRECTORY ${CMAKE_SOURCE_DIR}\n+\t\t\tOUTPUT_FILE ${CMAKE_BINARY_DIR}/hook-list.h)\n+endif()\n+\n include_directories(${CMAKE_BINARY_DIR})\n \n #build\ndiff --git a/generate-hooklist.sh b/generate-hooklist.sh\nnew file mode 100755\nindex 00000000000..6d4e56d1a31\n--- /dev/null\n+++ b/generate-hooklist.sh\n@@ -0,0 +1,18 @@\n+#!/bin/sh\n+#\n+# Usage: ./generate-hooklist.sh >hook-list.h\n+\n+cat <<EOF\n+/* Automatically generated by generate-hooklist.sh */\n+\n+static const char *hook_name_list[] = {\n+EOF\n+\n+sed -n -e '/^~~~~*$/ {x; s/^.*$/\t\"&\",/; p;}; x' \\\n+\t<Documentation/githooks.txt |\n+\tLC_ALL=C sort\n+\n+cat <<EOF\n+\tNULL,\n+};\n+EOF\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434532","messageId":"patch-v5-08.36-72dd1010f5b-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 08/36] hook: add 'run' subcommand","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:08Z","receivedAt":"2021-09-02T13:12:05Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn order to enable hooks to be run as an external process, by a\nstandalone Git command, or by tools which wrap Git, provide an external\nmeans to run all configured hook commands for a given hook event.\n\nMost of our hooks require more complex functionality than this, but\nlet's start with the bare minimum required to support our simplest\nhooks.\n\nIn terms of implementation the usage_with_options() and \"goto usage\"\npattern here mirrors that of\nbuiltin/{commit-graph,multi-pack-index}.c.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n .gitignore                 |   1 +\n Documentation/git-hook.txt |  38 +++++++++++\n Documentation/githooks.txt |   4 ++\n Makefile                   |   1 +\n builtin.h                  |   1 +\n builtin/hook.c             |  90 ++++++++++++++++++++++++++\n command-list.txt           |   1 +\n git.c                      |   1 +\n hook.c                     | 101 +++++++++++++++++++++++++++++\n hook.h                     |  39 +++++++++++\n t/t1800-hook.sh            | 128 +++++++++++++++++++++++++++++++++++++\n 11 files changed, 405 insertions(+)\n create mode 100644 Documentation/git-hook.txt\n create mode 100644 builtin/hook.c\n create mode 100755 t/t1800-hook.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 6be9de41ae8..66189ca3cdc 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-hook\n /git-http-backend\n /git-http-fetch\n /git-http-push\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nnew file mode 100644\nindex 00000000000..660d6a992a0\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,38 @@\n+git-hook(1)\n+===========\n+\n+NAME\n+----\n+git-hook - run git hooks\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git hook' run <hook-name> [-- <hook-args>]\n+\n+DESCRIPTION\n+-----------\n+\n+This command is an interface to git hooks (see linkgit:githooks[5]).\n+Currently it only provides a convenience wrapper for running hooks for\n+use by git itself. In the future it might gain other functionality.\n+\n+SUBCOMMANDS\n+-----------\n+\n+run::\n+\tRun the `<hook-name>` hook. See linkgit:githooks[5] for\n+\tthe hook names we support.\n++\n+Any positional arguments to the hook should be passed after an\n+optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n+arguments (if any) differ by hook name, see linkgit:githooks[5] for\n+what those are.\n+\n+SEE ALSO\n+--------\n+linkgit:githooks[5]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/githooks.txt b/Documentation/githooks.txt\nindex b51959ff941..a16e62bc8c8 100644\n--- a/Documentation/githooks.txt\n+++ b/Documentation/githooks.txt\n@@ -698,6 +698,10 @@ and \"0\" meaning they were not.\n Only one parameter should be set to \"1\" when the hook runs.  The hook\n running passing \"1\", \"1\" should not be possible.\n \n+SEE ALSO\n+--------\n+linkgit:git-hook[1]\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex ed06b4e9634..bbd416511a3 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1115,6 +1115,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\ndiff --git a/builtin.h b/builtin.h\nindex 16ecd5586f0..91740c15149 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);\n int cmd_grep(int argc, const char **argv, const char *prefix);\n int cmd_hash_object(int argc, const char **argv, const char *prefix);\n int cmd_help(int argc, const char **argv, const char *prefix);\n+int cmd_hook(int argc, const char **argv, const char *prefix);\n int cmd_index_pack(int argc, const char **argv, const char *prefix);\n int cmd_init_db(int argc, const char **argv, const char *prefix);\n int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/hook.c b/builtin/hook.c\nnew file mode 100644\nindex 00000000000..012a2973b38\n--- /dev/null\n+++ b/builtin/hook.c\n@@ -0,0 +1,90 @@\n+#include \"cache.h\"\n+#include \"builtin.h\"\n+#include \"config.h\"\n+#include \"hook.h\"\n+#include \"parse-options.h\"\n+#include \"strbuf.h\"\n+#include \"strvec.h\"\n+\n+#define BUILTIN_HOOK_RUN_USAGE \\\n+\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n+\n+static const char * const builtin_hook_usage[] = {\n+\tBUILTIN_HOOK_RUN_USAGE,\n+\tNULL\n+};\n+\n+static const char * const builtin_hook_run_usage[] = {\n+\tBUILTIN_HOOK_RUN_USAGE,\n+\tNULL\n+};\n+\n+static int run(int argc, const char **argv, const char *prefix)\n+{\n+\tint i;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tconst char *hook_name;\n+\tconst char *hook_path;\n+\tstruct option run_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, run_options,\n+\t\t\t     builtin_hook_run_usage,\n+\t\t\t     PARSE_OPT_KEEP_DASHDASH);\n+\n+\tif (!argc)\n+\t\tgoto usage;\n+\n+\t/*\n+\t * Having a -- for \"run\" when providing <hook-args> is\n+\t * mandatory.\n+\t */\n+\tif (argc > 1 && strcmp(argv[1], \"--\") &&\n+\t    strcmp(argv[1], \"--end-of-options\"))\n+\t\tgoto usage;\n+\n+\t/* Add our arguments, start after -- */\n+\tfor (i = 2 ; i < argc; i++)\n+\t\tstrvec_push(&opt.args, argv[i]);\n+\n+\t/* Need to take into account core.hooksPath */\n+\tgit_config(git_default_config, NULL);\n+\n+\t/*\n+\t * We are not using a plain run_hooks() because we'd like to\n+\t * detect missing hooks. Let's find it ourselves and call\n+\t * run_hooks() instead.\n+\t */\n+\thook_name = argv[0];\n+\thook_path = find_hook(hook_name);\n+\tif (!hook_path) {\n+\t\terror(\"cannot find a hook named %s\", hook_name);\n+\t\treturn 1;\n+\t}\n+\n+\tret = run_hooks(hook_name, hook_path, &opt);\n+\trun_hooks_opt_clear(&opt);\n+\treturn ret;\n+usage:\n+\tusage_with_options(builtin_hook_run_usage, run_options);\n+}\n+\n+int cmd_hook(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option builtin_hook_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, NULL, builtin_hook_options,\n+\t\t\t     builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n+\tif (!argc)\n+\t\tgoto usage;\n+\n+\tif (!strcmp(argv[0], \"run\"))\n+\t\treturn run(argc, argv, prefix);\n+\n+usage:\n+\tusage_with_options(builtin_hook_usage, builtin_hook_options);\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex a289f09ed6f..9ccd8e5aebe 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -103,6 +103,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-hook                                mainporcelain\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\n git-http-push                           synchelpers\ndiff --git a/git.c b/git.c\nindex 18bed9a9964..540909c391f 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nindex 97cd799a320..dd67801c962 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -1,6 +1,7 @@\n #include \"cache.h\"\n #include \"hook.h\"\n #include \"run-command.h\"\n+#include \"config.h\"\n \n const char *find_hook(const char *name)\n {\n@@ -40,3 +41,103 @@ int hook_exists(const char *name)\n {\n \treturn !!find_hook(name);\n }\n+\n+void run_hooks_opt_clear(struct run_hooks_opt *o)\n+{\n+\tstrvec_clear(&o->env);\n+\tstrvec_clear(&o->args);\n+}\n+\n+static int pick_next_hook(struct child_process *cp,\n+\t\t\t  struct strbuf *out,\n+\t\t\t  void *pp_cb,\n+\t\t\t  void **pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *run_me = hook_cb->run_me;\n+\n+\tif (!run_me)\n+\t\treturn 0;\n+\n+\tcp->no_stdin = 1;\n+\tcp->env = hook_cb->options->env.v;\n+\tcp->stdout_to_stderr = 1;\n+\tcp->trace2_hook_name = hook_cb->hook_name;\n+\n+\t/* add command */\n+\tstrvec_push(&cp->args, run_me->hook_path);\n+\n+\t/*\n+\t * add passed-in argv, without expanding - let the user get back\n+\t * exactly what they put in\n+\t */\n+\tstrvec_pushv(&cp->args, hook_cb->options->args.v);\n+\n+\t/* Provide context for errors if necessary */\n+\t*pp_task_cb = run_me;\n+\n+\t/*\n+\t * This pick_next_hook() will be called again, we're only\n+\t * running one hook, so indicate that no more work will be\n+\t * done.\n+\t */\n+\thook_cb->run_me = NULL;\n+\n+\treturn 1;\n+}\n+\n+static int notify_start_failure(struct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cp)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct hook *attempted = pp_task_cp;\n+\n+\thook_cb->rc |= 1;\n+\n+\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n+\t\t    attempted->hook_path);\n+\n+\treturn 1;\n+}\n+\n+static int notify_hook_finished(int result,\n+\t\t\t\tstruct strbuf *out,\n+\t\t\t\tvoid *pp_cb,\n+\t\t\t\tvoid *pp_task_cb)\n+{\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\n+\thook_cb->rc |= result;\n+\n+\treturn 0;\n+}\n+\n+int run_hooks(const char *hook_name, const char *hook_path,\n+\t      struct run_hooks_opt *options)\n+{\n+\tstruct hook my_hook = {\n+\t\t.hook_path = hook_path,\n+\t};\n+\tstruct hook_cb_data cb_data = {\n+\t\t.rc = 0,\n+\t\t.hook_name = hook_name,\n+\t\t.options = options,\n+\t};\n+\tint jobs = 1;\n+\n+\tif (!options)\n+\t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n+\n+\tcb_data.run_me = &my_hook;\n+\n+\trun_processes_parallel_tr2(jobs,\n+\t\t\t\t   pick_next_hook,\n+\t\t\t\t   notify_start_failure,\n+\t\t\t\t   notify_hook_finished,\n+\t\t\t\t   &cb_data,\n+\t\t\t\t   \"hook\",\n+\t\t\t\t   hook_name);\n+\n+\treturn cb_data.rc;\n+}\ndiff --git a/hook.h b/hook.h\nindex 368754b1201..d5bc1dc74c9 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,5 +1,33 @@\n #ifndef HOOK_H\n #define HOOK_H\n+#include \"strvec.h\"\n+\n+struct hook {\n+\t/* The path to the hook */\n+\tconst char *hook_path;\n+};\n+\n+struct run_hooks_opt\n+{\n+\t/* Environment vars to be set for each hook */\n+\tstruct strvec env;\n+\n+\t/* Args to be passed to each hook */\n+\tstruct strvec args;\n+};\n+\n+#define RUN_HOOKS_OPT_INIT { \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n+struct hook_cb_data {\n+\t/* rc reflects the cumulative failure state */\n+\tint rc;\n+\tconst char *hook_name;\n+\tstruct hook *run_me;\n+\tstruct run_hooks_opt *options;\n+};\n \n /**\n  * Returns the path to the hook file, or NULL if the hook is missing\n@@ -13,4 +41,15 @@ const char *find_hook(const char *name);\n  */\n int hook_exists(const char *hookname);\n \n+/**\n+ * Clear data from an initialized \"struct run_hooks-opt\".\n+ */\n+void run_hooks_opt_clear(struct run_hooks_opt *o);\n+\n+/**\n+ * Takes an already resolved hook found via find_hook() and runs\n+ * it. Does not call run_hooks_opt_clear() for you.\n+ */\n+int run_hooks(const char *hookname, const char *hook_path,\n+\t      struct run_hooks_opt *options);\n #endif\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nnew file mode 100755\nindex 00000000000..d0a3796f28e\n--- /dev/null\n+++ b/t/t1800-hook.sh\n@@ -0,0 +1,128 @@\n+#!/bin/sh\n+\n+test_description='git-hook command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'git hook usage' '\n+\ttest_expect_code 129 git hook &&\n+\ttest_expect_code 129 git hook run &&\n+\ttest_expect_code 129 git hook run -h &&\n+\ttest_expect_code 129 git hook run --unknown 2>err &&\n+\tgrep \"unknown option\" err\n+'\n+\n+test_expect_success 'git hook run: nonexistent hook' '\n+\tcat >stderr.expect <<-\\EOF &&\n+\terror: cannot find a hook named test-hook\n+\tEOF\n+\ttest_expect_code 1 git hook run test-hook 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual\n+'\n+\n+test_expect_success 'git hook run: basic' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tEOF\n+\tgit hook run test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run: stdout and stderr both write to our stderr' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo >&1 Will end up on stderr\n+\techo >&2 Will end up on stderr\n+\tEOF\n+\n+\tcat >stderr.expect <<-\\EOF &&\n+\tWill end up on stderr\n+\tWill end up on stderr\n+\tEOF\n+\tgit hook run test-hook >stdout.actual 2>stderr.actual &&\n+\ttest_cmp stderr.expect stderr.actual &&\n+\ttest_must_be_empty stdout.actual\n+'\n+\n+test_expect_success 'git hook run: exit codes are passed along' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 1\n+\tEOF\n+\n+\ttest_expect_code 1 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 2\n+\tEOF\n+\n+\ttest_expect_code 2 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 128\n+\tEOF\n+\n+\ttest_expect_code 128 git hook run test-hook &&\n+\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\texit 129\n+\tEOF\n+\n+\ttest_expect_code 129 git hook run test-hook\n+'\n+\n+test_expect_success 'git hook run arg u ments without -- is not allowed' '\n+\ttest_expect_code 129 git hook run test-hook arg u ments\n+'\n+\n+test_expect_success 'git hook run -- pass arguments' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo $1\n+\techo $2\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\targ\n+\tu ments\n+\tEOF\n+\n+\tgit hook run test-hook -- arg \"u ments\" 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git hook run -- out-of-repo runs excluded' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n+\tnongit test_must_fail git hook run test-hook\n+'\n+\n+test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n+\tmkdir my-hooks &&\n+\twrite_script my-hooks/test-hook <<-\\EOF &&\n+\techo Hook ran $1 >>actual\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tTest hook\n+\tHook ran one\n+\tHook ran two\n+\tHook ran three\n+\tHook ran four\n+\tEOF\n+\n+\t# Test various ways of specifying the path. See also\n+\t# t1350-config-hooks-path.sh\n+\t>actual &&\n+\tgit hook run test-hook -- ignored 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook -- three 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook -- four 2>>actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434533","messageId":"patch-v5-09.36-821cc9bf11e-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 09/36] gc: use hook library for pre-auto-gc hook","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:09Z","receivedAt":"2021-09-02T13:12:08Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the pre-auto-gc hook away from run-command.h to and over to the\nnew hook.h library.\n\nTo do this introduce a simple run_hooks_oneshot() wrapper, we'll be\nusing it extensively for these simple cases of wanting to run a single\nhook under a given name, and having it free the memory we allocate for\nus.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/gc.c |  3 ++-\n hook.c       | 23 +++++++++++++++++++++++\n hook.h       | 13 +++++++++++++\n 3 files changed, 38 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 6ce5ca45126..c91a0786836 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -32,6 +32,7 @@\n #include \"remote.h\"\n #include \"object-store.h\"\n #include \"exec-cmd.h\"\n+#include \"hook.h\"\n \n #define FAILED_RUN \"failed to run %s\"\n \n@@ -394,7 +395,7 @@ static int need_to_gc(void)\n \telse\n \t\treturn 0;\n \n-\tif (run_hook_le(NULL, \"pre-auto-gc\", NULL))\n+\tif (run_hooks_oneshot(\"pre-auto-gc\", NULL))\n \t\treturn 0;\n \treturn 1;\n }\ndiff --git a/hook.c b/hook.c\nindex dd67801c962..1776895289e 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -141,3 +141,26 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \n \treturn cb_data.rc;\n }\n+\n+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n+{\n+\tconst char *hook_path;\n+\tint ret;\n+\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n+\n+\tif (!options)\n+\t\toptions = &hook_opt_scratch;\n+\n+\thook_path = find_hook(hook_name);\n+\tif (!hook_path) {\n+\t\tret = 0;\n+\t\tgoto cleanup;\n+\t}\n+\n+\tret = run_hooks(hook_name, hook_path, options);\n+\n+cleanup:\n+\trun_hooks_opt_clear(options);\n+\n+\treturn ret;\n+}\ndiff --git a/hook.h b/hook.h\nindex d5bc1dc74c9..b7a106a6e2b 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -49,7 +49,20 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);\n /**\n  * Takes an already resolved hook found via find_hook() and runs\n  * it. Does not call run_hooks_opt_clear() for you.\n+ *\n+ * See run_hooks_oneshot() for the simpler one-shot API.\n  */\n int run_hooks(const char *hookname, const char *hook_path,\n \t      struct run_hooks_opt *options);\n+\n+/**\n+ * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n+ * with run_hooks().\n+ *\n+ * If \"options\" is provided calls run_hooks_opt_clear() on it for\n+ * you. If \"options\" is NULL the default options from\n+ * RUN_HOOKS_OPT_INIT will be used.\n+ */\n+int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n+\n #endif\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434534","messageId":"patch-v5-10.36-d71c90254ea-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 10/36] rebase: convert pre-rebase to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:10Z","receivedAt":"2021-09-02T13:12:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the pre-rebase hook away from run-command.h to and over to the\nnew hook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/rebase.c | 6 ++++--\n 1 file changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex c284a7ace19..ee68a1df492 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -28,6 +28,7 @@\n #include \"sequencer.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"hook.h\"\n \n #define DEFAULT_REFLOG_ACTION \"rebase\"\n \n@@ -1313,6 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\n@@ -2022,9 +2024,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \t}\n \n \t/* If a hook exists, give it a chance to interrupt*/\n+\tstrvec_pushl(&hook_opt.args, options.upstream_arg, argc ? argv[0] : NULL, NULL);\n \tif (!ok_to_skip_pre_rebase &&\n-\t    run_hook_le(NULL, \"pre-rebase\", options.upstream_arg,\n-\t\t\targc ? argv[0] : NULL, NULL))\n+\t    run_hooks_oneshot(\"pre-rebase\", &hook_opt))\n \t\tdie(_(\"The pre-rebase hook refused to rebase.\"));\n \n \tif (options.flags & REBASE_DIFFSTAT) {\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434535","messageId":"patch-v5-11.36-ea3af2ccc4d-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 11/36] am: convert applypatch to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:11Z","receivedAt":"2021-09-02T13:12:15Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach pre-applypatch, post-applypatch, and applypatch-msg to use the\nhook.h library instead of the run-command.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 8 +++++---\n 1 file changed, 5 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex c603f3cebdf..e444b18b64a 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -446,9 +446,11 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tassert(state->msg);\n-\tret = run_hook_le(NULL, \"applypatch-msg\", am_path(state, \"final-commit\"), NULL);\n+\tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n+\tret = run_hooks_oneshot(\"applypatch-msg\", &opt);\n \n \tif (!ret) {\n \t\tFREE_AND_NULL(state->msg);\n@@ -1609,7 +1611,7 @@ static void do_commit(const struct am_state *state)\n \tconst char *reflog_msg, *author, *committer = NULL;\n \tstruct strbuf sb = STRBUF_INIT;\n \n-\tif (run_hook_le(NULL, \"pre-applypatch\", NULL))\n+\tif (run_hooks_oneshot(\"pre-applypatch\", NULL))\n \t\texit(1);\n \n \tif (write_cache_as_tree(&tree, 0, NULL))\n@@ -1661,7 +1663,7 @@ static void do_commit(const struct am_state *state)\n \t\tfclose(fp);\n \t}\n \n-\trun_hook_le(NULL, \"post-applypatch\", NULL);\n+\trun_hooks_oneshot(\"post-applypatch\", NULL);\n \n \tstrbuf_release(&sb);\n }\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434536","messageId":"patch-v5-12.36-fed0b52f88f-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 12/36] hooks: convert 'post-checkout' hook to hook library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:12Z","receivedAt":"2021-09-02T13:12:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the running of the 'post-checkout' hook away from run-command.h\nto the new hook.h library. For \"worktree\" this requires a change to it\nto run the hooks from a given directory.\n\nWe could strictly speaking skip the \"absolute_path\" flag and just\ncheck if \"dir\" is specified, but let's split them up for clarity, as\nwell as for any future user who'd like to set \"dir\" but not implicitly\nchange the argument to an absolute path.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/checkout.c | 14 +++++++++-----\n builtin/clone.c    |  6 ++++--\n builtin/worktree.c | 28 ++++++++++++----------------\n hook.c             |  9 +++++++++\n hook.h             |  9 +++++++++\n read-cache.c       |  1 +\n reset.c            | 14 ++++++++++----\n 7 files changed, 54 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex b5d477919a7..863b02a7d7c 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -9,6 +9,7 @@\n #include \"config.h\"\n #include \"diff.h\"\n #include \"dir.h\"\n+#include \"hook.h\"\n #include \"ll-merge.h\"\n #include \"lockfile.h\"\n #include \"merge-recursive.h\"\n@@ -106,13 +107,16 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\treturn run_hook_le(NULL, \"post-checkout\",\n-\t\t\t   oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n-\t\t\t   oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n-\t\t\t   changed ? \"1\" : \"0\", NULL);\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\n-\n+\tstrvec_pushl(&opt.args,\n+\t\t     oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),\n+\t\t     oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),\n+\t\t     changed ? \"1\" : \"0\",\n+\t\t     NULL);\n+\treturn run_hooks_oneshot(\"post-checkout\", &opt);\n }\n \n static int update_some(const struct object_id *oid, struct strbuf *base,\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 66fe66679c8..27fc05ee511 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -32,6 +32,7 @@\n #include \"connected.h\"\n #include \"packfile.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"hook.h\"\n \n /*\n  * Overall FIXMEs:\n@@ -775,6 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tif (option_no_checkout)\n \t\treturn 0;\n@@ -820,8 +822,8 @@ static int checkout(int submodule_progress)\n \tif (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))\n \t\tdie(_(\"unable to write new index file\"));\n \n-\terr |= run_hook_le(NULL, \"post-checkout\", oid_to_hex(null_oid()),\n-\t\t\t   oid_to_hex(&oid), \"1\", NULL);\n+\tstrvec_pushl(&hook_opt.args, oid_to_hex(null_oid()), oid_to_hex(&oid), \"1\", NULL);\n+\terr |= run_hooks_oneshot(\"post-checkout\", &hook_opt);\n \n \tif (!err && (option_recurse_submodules.nr > 0)) {\n \t\tstruct strvec args = STRVEC_INIT;\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex d22ece93e1a..330867c19bf 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -382,22 +382,18 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tconst char *hook = find_hook(\"post-checkout\");\n-\t\tif (hook) {\n-\t\t\tconst char *env[] = { \"GIT_DIR\", \"GIT_WORK_TREE\", NULL };\n-\t\t\tcp.git_cmd = 0;\n-\t\t\tcp.no_stdin = 1;\n-\t\t\tcp.stdout_to_stderr = 1;\n-\t\t\tcp.dir = path;\n-\t\t\tcp.env = env;\n-\t\t\tcp.argv = NULL;\n-\t\t\tcp.trace2_hook_name = \"post-checkout\";\n-\t\t\tstrvec_pushl(&cp.args, absolute_path(hook),\n-\t\t\t\t     oid_to_hex(null_oid()),\n-\t\t\t\t     oid_to_hex(&commit->object.oid),\n-\t\t\t\t     \"1\", NULL);\n-\t\t\tret = run_command(&cp);\n-\t\t}\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(null_oid()),\n+\t\t\t     oid_to_hex(&commit->object.oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\topt.dir = path;\n+\t\topt.absolute_path = 1;\n+\n+\t\tret = run_hooks_oneshot(\"post-checkout\", &opt);\n \t}\n \n \tstrvec_clear(&child_env);\ndiff --git a/hook.c b/hook.c\nindex 1776895289e..8a923e52c1b 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -63,6 +63,7 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\n+\tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n \tstrvec_push(&cp->args, run_me->hook_path);\n@@ -116,6 +117,7 @@ static int notify_hook_finished(int result,\n int run_hooks(const char *hook_name, const char *hook_path,\n \t      struct run_hooks_opt *options)\n {\n+\tstruct strbuf abs_path = STRBUF_INIT;\n \tstruct hook my_hook = {\n \t\t.hook_path = hook_path,\n \t};\n@@ -129,6 +131,10 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n+\tif (options->absolute_path) {\n+\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n+\t\tmy_hook.hook_path = abs_path.buf;\n+\t}\n \tcb_data.run_me = &my_hook;\n \n \trun_processes_parallel_tr2(jobs,\n@@ -139,6 +145,9 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n \n+\tif (options->absolute_path)\n+\t\tstrbuf_release(&abs_path);\n+\n \treturn cb_data.rc;\n }\n \ndiff --git a/hook.h b/hook.h\nindex b7a106a6e2b..3edd937e198 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -14,6 +14,15 @@ struct run_hooks_opt\n \n \t/* Args to be passed to each hook */\n \tstruct strvec args;\n+\n+\t/*\n+\t * Resolve and run the \"absolute_path(hook)\" instead of\n+\t * \"hook\". Used for \"git worktree\" hooks\n+\t */\n+\tint absolute_path;\n+\n+\t/* Path to initial working directory for subprocess */\n+\tconst char *dir;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/read-cache.c b/read-cache.c\nindex 9048ef9e905..7a9ffea0885 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -28,6 +28,7 @@\n #include \"sparse-index.h\"\n #include \"csum-file.h\"\n #include \"promisor-remote.h\"\n+#include \"hook.h\"\n \n /* Mask for the name length in ce_flags in the on-disk index */\n \ndiff --git a/reset.c b/reset.c\nindex 79310ae071b..1237ced8a58 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -7,6 +7,7 @@\n #include \"tree-walk.h\"\n #include \"tree.h\"\n #include \"unpack-trees.h\"\n+#include \"hook.h\"\n \n int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t       const char *switch_to_branch, unsigned flags,\n@@ -125,10 +126,15 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\tret = create_symref(\"HEAD\", switch_to_branch,\n \t\t\t\t\t    reflog_head);\n \t}\n-\tif (run_hook)\n-\t\trun_hook_le(NULL, \"post-checkout\",\n-\t\t\t    oid_to_hex(orig ? orig : null_oid()),\n-\t\t\t    oid_to_hex(oid), \"1\", NULL);\n+\tif (run_hook) {\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstrvec_pushl(&opt.args,\n+\t\t\t     oid_to_hex(orig ? orig : null_oid()),\n+\t\t\t     oid_to_hex(oid),\n+\t\t\t     \"1\",\n+\t\t\t     NULL);\n+\t\trun_hooks_oneshot(\"post-checkout\", &opt);\n+\t}\n \n leave_reset_head:\n \tstrbuf_release(&msg);\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434537","messageId":"patch-v5-13.36-53d8721a0e3-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 13/36] merge: convert post-merge to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:13Z","receivedAt":"2021-09-02T13:12:17Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach post-merge to use the hook.h library instead of the\nrun-command.h library to run hooks.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/merge.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex d75a438298b..ca9b3ba4827 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,6 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\n@@ -489,7 +490,8 @@ static void finish(struct commit *head_commit,\n \t}\n \n \t/* Run a post-merge hook */\n-\trun_hook_le(NULL, \"post-merge\", squash ? \"1\" : \"0\", NULL);\n+\tstrvec_push(&opt.args, squash ? \"1\" : \"0\");\n+\trun_hooks_oneshot(\"post-merge\", &opt);\n \n \tapply_autostash(git_path_merge_autostash(the_repository));\n \tstrbuf_release(&reflog_message);\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434538","messageId":"patch-v5-14.36-d60827a2856-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 14/36] git hook run: add an --ignore-missing flag","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:14Z","receivedAt":"2021-09-02T13:12:18Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"For certain one-shot hooks we'd like to optimistically run them, and\nnot complain if they don't exist. This will be used by send-email in a\nsubsequent commit.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt | 10 +++++++++-\n builtin/hook.c             | 10 ++++++++--\n t/t1800-hook.sh            |  5 +++++\n 3 files changed, 22 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 660d6a992a0..097fb9de63b 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run <hook-name> [-- <hook-args>]\n+'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -29,6 +29,14 @@ optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n arguments (if any) differ by hook name, see linkgit:githooks[5] for\n what those are.\n \n+OPTIONS\n+-------\n+\n+--ignore-missing::\n+\tIgnore any missing hook by quietly returning zero. Used for\n+\ttools that want to do a blind one-shot run of a hook that may\n+\tor may not be present.\n+\n SEE ALSO\n --------\n linkgit:githooks[5]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 012a2973b38..76d49e672f4 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -7,7 +7,7 @@\n #include \"strvec.h\"\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n-\tN_(\"git hook run <hook-name> [-- <hook-args>]\")\n+\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n \n static const char * const builtin_hook_usage[] = {\n \tBUILTIN_HOOK_RUN_USAGE,\n@@ -23,9 +23,12 @@ static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tint ignore_missing = 0;\n \tconst char *hook_name;\n \tconst char *hook_path;\n \tstruct option run_options[] = {\n+\t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n+\t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n \t\tOPT_END(),\n \t};\n \tint ret;\n@@ -55,9 +58,12 @@ static int run(int argc, const char **argv, const char *prefix)\n \t/*\n \t * We are not using a plain run_hooks() because we'd like to\n \t * detect missing hooks. Let's find it ourselves and call\n-\t * run_hooks() instead.\n+\t * run_hooks() instead...\n \t */\n \thook_name = argv[0];\n+\tif (ignore_missing)\n+\t\t/* ... act like a plain run_hooks() under --ignore-missing */\n+\t\treturn run_hooks_oneshot(hook_name, &opt);\n \thook_path = find_hook(hook_name);\n \tif (!hook_path) {\n \t\terror(\"cannot find a hook named %s\", hook_name);\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex d0a3796f28e..feb95b8fc8d 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -20,6 +20,11 @@ test_expect_success 'git hook run: nonexistent hook' '\n \ttest_cmp stderr.expect stderr.actual\n '\n \n+test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '\n+\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n+\ttest_must_be_empty stderr.actual\n+'\n+\n test_expect_success 'git hook run: basic' '\n \twrite_script .git/hooks/test-hook <<-EOF &&\n \techo Test hook\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434539","messageId":"patch-v5-15.36-d4976a0821f-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 15/36] send-email: use 'git hook run' for 'sendemail-validate'","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:15Z","receivedAt":"2021-09-02T13:12:20Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nChange the \"sendmail-validate\" hook to be run via the \"git hook run\"\nwrapper instead of via a direct invocation.\n\nThis is the smallest possibly change to get \"send-email\" using \"git\nhook run\". We still check the hook itself with \"-x\", and set a\n\"GIT_DIR\" variable, both of which are asserted by our tests. We'll\nneed to get rid of this special behavior if we start running N hooks,\nbut for now let's be as close to bug-for-bug compatible as possible.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-send-email.perl   | 20 ++++++++++++--------\n t/t9001-send-email.sh |  4 ++--\n 2 files changed, 14 insertions(+), 10 deletions(-)\n\ndiff --git a/git-send-email.perl b/git-send-email.perl\nindex e65d969d0bb..126850d974b 100755\n--- a/git-send-email.perl\n+++ b/git-send-email.perl\n@@ -195,13 +195,13 @@ sub format_2822_time {\n my $editor;\n \n sub system_or_msg {\n-\tmy ($args, $msg) = @_;\n+\tmy ($args, $msg, $cmd_name) = @_;\n \tsystem(@$args);\n \tmy $signalled = $? & 127;\n \tmy $exit_code = $? >> 8;\n \treturn unless $signalled or $exit_code;\n \n-\tmy @sprintf_args = ($args->[0], $exit_code);\n+\tmy @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);\n \tif (defined $msg) {\n \t\t# Quiet the 'redundant' warning category, except we\n \t\t# need to support down to Perl 5.8, so we can't do a\n@@ -2031,10 +2031,10 @@ sub validate_patch {\n \tmy ($fn, $xfer_encoding) = @_;\n \n \tif ($repo) {\n+\t\tmy $hook_name = 'sendemail-validate';\n \t\tmy $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');\n \t\trequire File::Spec;\n-\t\tmy $validate_hook = File::Spec->catfile($hooks_path,\n-\t\t\t\t\t    'sendemail-validate');\n+\t\tmy $validate_hook = File::Spec->catfile($hooks_path, $hook_name);\n \t\tmy $hook_error;\n \t\tif (-x $validate_hook) {\n \t\t\trequire Cwd;\n@@ -2044,13 +2044,17 @@ sub validate_patch {\n \t\t\tchdir($repo->wc_path() or $repo->repo_path())\n \t\t\t\tor die(\"chdir: $!\");\n \t\t\tlocal $ENV{\"GIT_DIR\"} = $repo->repo_path();\n-\t\t\t$hook_error = system_or_msg([$validate_hook, $target]);\n+\t\t\tmy @validate_hook = (\"git\", \"hook\", \"run\", \"--ignore-missing\", $hook_name, \"--\", $target);\n+\t\t\t$hook_error = system_or_msg(\\@validate_hook, undef,\n+\t\t\t\t\t\t       \"git hook run $hook_name -- <patch>\");\n \t\t\tchdir($cwd_save) or die(\"chdir: $!\");\n \t\t}\n \t\tif ($hook_error) {\n-\t\t\tdie sprintf(__(\"fatal: %s: rejected by sendemail-validate hook\\n\" .\n-\t\t\t\t       \"%s\\n\" .\n-\t\t\t\t       \"warning: no patches were sent\\n\"), $fn, $hook_error);\n+\t\t\t$hook_error = sprintf(__(\"fatal: %s: rejected by %s hook\\n\" .\n+\t\t\t\t\t\t $hook_error . \"\\n\" .\n+\t\t\t\t\t\t \"warning: no patches were sent\\n\"),\n+\t\t\t\t\t      $fn, $hook_name);\n+\t\t\tdie $hook_error;\n \t\t}\n \t}\n \ndiff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh\nindex 57fc10e7f82..9ec7d75f0ff 100755\n--- a/t/t9001-send-email.sh\n+++ b/t/t9001-send-email.sh\n@@ -539,7 +539,7 @@ test_expect_success $PREREQ \"--validate respects relative core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'my-hooks/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n@@ -558,7 +558,7 @@ test_expect_success $PREREQ \"--validate respects absolute core.hooksPath path\" '\n \ttest_path_is_file my-hooks.ran &&\n \tcat >expect <<-EOF &&\n \tfatal: longline.patch: rejected by sendemail-validate hook\n-\tfatal: command '\"'\"'$hooks_path/sendemail-validate'\"'\"' died with exit code 1\n+\tfatal: command '\"'\"'git hook run sendemail-validate -- <patch>'\"'\"' died with exit code 1\n \twarning: no patches were sent\n \tEOF\n \ttest_cmp expect actual\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434540","messageId":"patch-v5-16.36-99f3dcd1945-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 16/36] git-p4: use 'git hook' to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:16Z","receivedAt":"2021-09-02T13:12:29Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nInstead of duplicating the behavior of run-command.h:run_hook_le() in\nPython, we can directly call 'git hook run'. We emulate the existence\ncheck with the --ignore-missing flag.\n\nAs this is the last hook execution in git.git to not go through \"git\nhook run\" or the hook.[ch] library we can now be absolutely sure that\nour assertion in hook.c that only hooks known by the generated (from\ngithooks(5)) hook-list.h are permitted.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n git-p4.py | 72 ++++++-------------------------------------------------\n 1 file changed, 7 insertions(+), 65 deletions(-)\n\ndiff --git a/git-p4.py b/git-p4.py\nindex 2b4500226aa..1f24cbf0bca 100755\n--- a/git-p4.py\n+++ b/git-p4.py\n@@ -207,71 +207,13 @@ def decode_path(path):\n         return path\n \n def run_git_hook(cmd, param=[]):\n-    \"\"\"Execute a hook if the hook exists.\"\"\"\n-    if verbose:\n-        sys.stderr.write(\"Looking for hook: %s\\n\" % cmd)\n-        sys.stderr.flush()\n-\n-    hooks_path = gitConfig(\"core.hooksPath\")\n-    if len(hooks_path) <= 0:\n-        hooks_path = os.path.join(os.environ[\"GIT_DIR\"], \"hooks\")\n-\n-    if not isinstance(param, list):\n-        param=[param]\n-\n-    # resolve hook file name, OS depdenent\n-    hook_file = os.path.join(hooks_path, cmd)\n-    if platform.system() == 'Windows':\n-        if not os.path.isfile(hook_file):\n-            # look for the file with an extension\n-            files = glob.glob(hook_file + \".*\")\n-            if not files:\n-                return True\n-            files.sort()\n-            hook_file = files.pop()\n-            while hook_file.upper().endswith(\".SAMPLE\"):\n-                # The file is a sample hook. We don't want it\n-                if len(files) > 0:\n-                    hook_file = files.pop()\n-                else:\n-                    return True\n-\n-    if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):\n-        return True\n-\n-    return run_hook_command(hook_file, param) == 0\n-\n-def run_hook_command(cmd, param):\n-    \"\"\"Executes a git hook command\n-       cmd = the command line file to be executed. This can be\n-       a file that is run by OS association.\n-\n-       param = a list of parameters to pass to the cmd command\n-\n-       On windows, the extension is checked to see if it should\n-       be run with the Git for Windows Bash shell.  If there\n-       is no file extension, the file is deemed a bash shell\n-       and will be handed off to sh.exe. Otherwise, Windows\n-       will be called with the shell to handle the file assocation.\n-\n-       For non Windows operating systems, the file is called\n-       as an executable.\n-    \"\"\"\n-    cli = [cmd] + param\n-    use_shell = False\n-    if platform.system() == 'Windows':\n-        (root,ext) = os.path.splitext(cmd)\n-        if ext == \"\":\n-            exe_path = os.environ.get(\"EXEPATH\")\n-            if exe_path is None:\n-                exe_path = \"\"\n-            else:\n-                exe_path = os.path.join(exe_path, \"bin\")\n-            cli = [os.path.join(exe_path, \"SH.EXE\")] + cli\n-        else:\n-            use_shell = True\n-    return subprocess.call(cli, shell=use_shell)\n-\n+    \"\"\"args are specified with -a <arg> -a <arg> -a <arg>\"\"\"\n+    args = ['git', 'hook', 'run', '--ignore-missing', cmd]\n+    if param:\n+        args.append(\"--\")\n+        for p in param:\n+            args.append(p)\n+    return subprocess.call(args) == 0\n \n def write_pipe(c, stdin):\n     if verbose:\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434541","messageId":"patch-v5-17.36-509761454e6-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 17/36] commit: convert {pre-commit,prepare-commit-msg} hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:17Z","receivedAt":"2021-09-02T13:12:39Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove these hooks hook away from run-command.h to and over to the new\nhook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n commit.c | 15 ++++++++-------\n 1 file changed, 8 insertions(+), 7 deletions(-)\n\ndiff --git a/commit.c b/commit.c\nindex 143f472c0f2..63d7943a86d 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -21,6 +21,7 @@\n #include \"commit-reach.h\"\n #include \"run-command.h\"\n #include \"shallow.h\"\n+#include \"hook.h\"\n \n static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);\n \n@@ -1698,22 +1699,22 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    const char *name, ...)\n {\n-\tstruct strvec hook_env = STRVEC_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tva_list args;\n-\tint ret;\n+\tconst char *arg;\n \n-\tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n+\tstrvec_pushf(&opt.env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\n \t * Let the hook know that no editor will be launched.\n \t */\n \tif (!editor_is_used)\n-\t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n+\t\tstrvec_push(&opt.env, \"GIT_EDITOR=:\");\n \n \tva_start(args, name);\n-\tret = run_hook_ve(hook_env.v, name, args);\n+\twhile ((arg = va_arg(args, const char *)))\n+\t\tstrvec_push(&opt.args, arg);\n \tva_end(args);\n-\tstrvec_clear(&hook_env);\n \n-\treturn ret;\n+\treturn run_hooks_oneshot(name, &opt);\n }\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434542","messageId":"patch-v5-18.36-e2c94d95427-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 18/36] read-cache: convert post-index-change to use hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:18Z","receivedAt":"2021-09-02T13:12:40Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the post-index-change hook away from run-command.h to and over to\nthe new hook.h library.\n\nThis removes the last direct user of run_hook_ve(), so we can make the\nfunction static now. It'll be removed entirely soon.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n read-cache.c  | 10 +++++++---\n run-command.c |  2 +-\n run-command.h |  1 -\n 3 files changed, 8 insertions(+), 5 deletions(-)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex 7a9ffea0885..875f6c1dea5 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3069,6 +3069,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n \tret = convert_to_sparse(istate);\n \n@@ -3097,9 +3098,12 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n \telse\n \t\tret = close_lock_file_gently(lock);\n \n-\trun_hook_le(NULL, \"post-index-change\",\n-\t\t\tistate->updated_workdir ? \"1\" : \"0\",\n-\t\t\tistate->updated_skipworktree ? \"1\" : \"0\", NULL);\n+\tstrvec_pushl(&hook_opt.args,\n+\t\t     istate->updated_workdir ? \"1\" : \"0\",\n+\t\t     istate->updated_skipworktree ? \"1\" : \"0\",\n+\t\t     NULL);\n+\trun_hooks_oneshot(\"post-index-change\", &hook_opt);\n+\n \tistate->updated_workdir = 0;\n \tistate->updated_skipworktree = 0;\n \ndiff --git a/run-command.c b/run-command.c\nindex 352f5be1646..b4341ba1c7b 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1320,7 +1320,7 @@ int async_with_fork(void)\n #endif\n }\n \n-int run_hook_ve(const char *const *env, const char *name, va_list args)\n+static int run_hook_ve(const char *const *env, const char *name, va_list args)\n {\n \tstruct child_process hook = CHILD_PROCESS_INIT;\n \tconst char *p;\ndiff --git a/run-command.h b/run-command.h\nindex f76b740f927..7a867d41217 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -219,7 +219,6 @@ int run_command(struct child_process *);\n  */\n LAST_ARG_MUST_BE_NULL\n int run_hook_le(const char *const *env, const char *name, ...);\n-int run_hook_ve(const char *const *env, const char *name, va_list args);\n \n /*\n  * Trigger an auto-gc\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434543","messageId":"patch-v5-19.36-fa7d0d24ea2-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 19/36] receive-pack: convert push-to-checkout hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:19Z","receivedAt":"2021-09-02T13:12:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the push-to-checkout hook away from run-command.h to and over to\nthe new hook.h library.\n\nThis removes the last direct user of run_hook_le(), so we could remove\nthat function now, but let's leave that to a follow-up cleanup commit.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 7 +++++--\n 1 file changed, 5 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 91fa799b66e..a7d03bbc7d3 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1435,9 +1435,12 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n-\tif (run_hook_le(env->v, push_to_checkout_hook,\n-\t\t\thash_to_hex(hash), NULL))\n+\tstrvec_pushv(&opt.env, env->v);\n+\tstrvec_push(&opt.args, hash_to_hex(hash));\n+\tif (run_hooks_oneshot(push_to_checkout_hook, &opt))\n \t\treturn \"push-to-checkout hook declined\";\n \telse\n \t\treturn NULL;\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434544","messageId":"patch-v5-20.36-428bb5a6792-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 20/36] run-command: remove old run_hook_{le,ve}() hook API","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:20Z","receivedAt":"2021-09-02T13:12:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nThe new hook.h library has replaced all run-command.h hook-related\nfunctionality. So let's delete this dead code.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n run-command.c | 32 --------------------------------\n run-command.h | 16 ----------------\n 2 files changed, 48 deletions(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex b4341ba1c7b..1399243de8a 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1320,38 +1320,6 @@ int async_with_fork(void)\n #endif\n }\n \n-static int run_hook_ve(const char *const *env, const char *name, va_list args)\n-{\n-\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\tconst char *p;\n-\n-\tp = find_hook(name);\n-\tif (!p)\n-\t\treturn 0;\n-\n-\tstrvec_push(&hook.args, p);\n-\twhile ((p = va_arg(args, const char *)))\n-\t\tstrvec_push(&hook.args, p);\n-\thook.env = env;\n-\thook.no_stdin = 1;\n-\thook.stdout_to_stderr = 1;\n-\thook.trace2_hook_name = name;\n-\n-\treturn run_command(&hook);\n-}\n-\n-int run_hook_le(const char *const *env, const char *name, ...)\n-{\n-\tva_list args;\n-\tint ret;\n-\n-\tva_start(args, name);\n-\tret = run_hook_ve(env, name, args);\n-\tva_end(args);\n-\n-\treturn ret;\n-}\n-\n struct io_pump {\n \t/* initialized by caller */\n \tint fd;\ndiff --git a/run-command.h b/run-command.h\nindex 7a867d41217..cfb6887e4ae 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -204,22 +204,6 @@ int finish_command_in_signal(struct child_process *);\n  */\n int run_command(struct child_process *);\n \n-/**\n- * Run a hook.\n- * The first argument is a pathname to an index file, or NULL\n- * if the hook uses the default index file or no index is needed.\n- * The second argument is the name of the hook.\n- * The further arguments correspond to the hook arguments.\n- * The last argument has to be NULL to terminate the arguments list.\n- * If the hook does not exist or is not executable, the return\n- * value will be zero.\n- * If it is executable, the hook will be executed and the exit\n- * status of the hook is returned.\n- * On execution, .stdout_to_stderr and .no_stdin will be set.\n- */\n-LAST_ARG_MUST_BE_NULL\n-int run_hook_le(const char *const *env, const char *name, ...);\n-\n /*\n  * Trigger an auto-gc\n  */\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434545","messageId":"patch-v5-21.36-994f6ad8602-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 21/36] run-command: allow stdin for run_processes_parallel","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:21Z","receivedAt":"2021-09-02T13:12:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nWhile it makes sense not to inherit stdin from the parent process to\navoid deadlocking, it's not necessary to completely ban stdin to\nchildren. An informed user should be able to configure stdin safely. By\nsetting `some_child.process.no_stdin=1` before calling `get_next_task()`\nwe provide a reasonable default behavior but enable users to set up\nstdin streaming for themselves during the callback.\n\n`some_child.process.stdout_to_stderr`, however, remains unmodifiable by\n`get_next_task()` - the rest of the run_processes_parallel() API depends\non child output in stderr.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n run-command.c | 9 ++++++++-\n 1 file changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/run-command.c b/run-command.c\nindex 1399243de8a..482ee2d76c6 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1627,6 +1627,14 @@ static int pp_start_one(struct parallel_processes *pp)\n \tif (i == pp->max_processes)\n \t\tBUG(\"bookkeeping is hard\");\n \n+\t/*\n+\t * By default, do not inherit stdin from the parent process - otherwise,\n+\t * all children would share stdin! Users may overwrite this to provide\n+\t * something to the child's stdin by having their 'get_next_task'\n+\t * callback assign 0 to .no_stdin and an appropriate integer to .in.\n+\t */\n+\tpp->children[i].process.no_stdin = 1;\n+\n \tcode = pp->get_next_task(&pp->children[i].process,\n \t\t\t\t &pp->children[i].err,\n \t\t\t\t pp->data,\n@@ -1638,7 +1646,6 @@ static int pp_start_one(struct parallel_processes *pp)\n \t}\n \tpp->children[i].process.err = -1;\n \tpp->children[i].process.stdout_to_stderr = 1;\n-\tpp->children[i].process.no_stdin = 1;\n \n \tif (start_command(&pp->children[i].process)) {\n \t\tcode = pp->start_failure(&pp->children[i].err,\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434546","messageId":"patch-v5-22.36-3ccc654a664-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 22/36] hook: support passing stdin to hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:22Z","receivedAt":"2021-09-02T13:12:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome hooks (such as post-rewrite) need to take input via stdin.\nPreviously, callers provided stdin to hooks by setting\nrun-command.h:child_process.in, which takes a FD. Callers would open the\nfile in question themselves before calling run-command(). However, since\nwe will now need to seek to the front of the file and read it again for\nevery hook which runs, hook.h:run_command() takes a path and handles FD\nmanagement itself. Since this file is opened for read only, it should\nnot prevent later parallel execution support.\n\nOn the frontend, this is supported by asking for a file path, rather\nthan by reading stdin. Reading directly from stdin would involve caching\nthe entire stdin (to memory or to disk) and reading it back from the\nbeginning to each hook. We'd want to support cases like insufficient\nmemory or storage for the file. While this may prove useful later, for\nnow the path of least resistance is to just ask the user to make this\ninterim file themselves.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt |  7 ++++++-\n builtin/hook.c             |  4 +++-\n hook.c                     |  8 +++++++-\n hook.h                     |  3 +++\n t/t1800-hook.sh            | 18 ++++++++++++++++++\n 5 files changed, 37 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 097fb9de63b..fa68c1f3912 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -32,6 +32,11 @@ what those are.\n OPTIONS\n -------\n \n+--to-stdin::\n+\tFor \"run\"; Specify a file which will be streamed into the\n+\thook's stdin. The hook will receive the entire file from\n+\tbeginning to EOF.\n+\n --ignore-missing::\n \tIgnore any missing hook by quietly returning zero. Used for\n \ttools that want to do a blind one-shot run of a hook that may\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 76d49e672f4..fae69068201 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -7,7 +7,7 @@\n #include \"strvec.h\"\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n-\tN_(\"git hook run [--ignore-missing] <hook-name> [-- <hook-args>]\")\n+\tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n \n static const char * const builtin_hook_usage[] = {\n \tBUILTIN_HOOK_RUN_USAGE,\n@@ -29,6 +29,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \tstruct option run_options[] = {\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n+\t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n+\t\t\t   N_(\"file to read into hooks' stdin\")),\n \t\tOPT_END(),\n \t};\n \tint ret;\ndiff --git a/hook.c b/hook.c\nindex 8a923e52c1b..d156b0dc800 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -59,7 +59,13 @@ static int pick_next_hook(struct child_process *cp,\n \tif (!run_me)\n \t\treturn 0;\n \n-\tcp->no_stdin = 1;\n+\t/* reopen the file for stdin; run_command closes it. */\n+\tif (hook_cb->options->path_to_stdin) {\n+\t\tcp->no_stdin = 0;\n+\t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else {\n+\t\tcp->no_stdin = 1;\n+\t}\n \tcp->env = hook_cb->options->env.v;\n \tcp->stdout_to_stderr = 1;\n \tcp->trace2_hook_name = hook_cb->hook_name;\ndiff --git a/hook.h b/hook.h\nindex 3edd937e198..253725e197b 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -23,6 +23,9 @@ struct run_hooks_opt\n \n \t/* Path to initial working directory for subprocess */\n \tconst char *dir;\n+\n+\t/* Path to file which should be piped to stdin for each hook */\n+\tconst char *path_to_stdin;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex feb95b8fc8d..6431b19e392 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -130,4 +130,22 @@ test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'stdin to hooks' '\n+\twrite_script .git/hooks/test-hook <<-\\EOF &&\n+\techo BEGIN stdin\n+\tcat\n+\techo END stdin\n+\tEOF\n+\n+\tcat >expect <<-EOF &&\n+\tBEGIN stdin\n+\thello\n+\tEND stdin\n+\tEOF\n+\n+\techo hello >input &&\n+\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434547","messageId":"patch-v5-23.36-f548e3d15e7-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 23/36] am: convert 'post-rewrite' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:23Z","receivedAt":"2021-09-02T13:12:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/am.c | 20 ++++----------------\n 1 file changed, 4 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/am.c b/builtin/am.c\nindex e444b18b64a..9e3d4d9ab44 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -467,24 +467,12 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tconst char *hook = find_hook(\"post-rewrite\");\n-\tint ret;\n-\n-\tif (!hook)\n-\t\treturn 0;\n-\n-\tstrvec_push(&cp.args, hook);\n-\tstrvec_push(&cp.args, \"rebase\");\n-\n-\tcp.in = xopen(am_path(state, \"rewritten\"), O_RDONLY);\n-\tcp.stdout_to_stderr = 1;\n-\tcp.trace2_hook_name = \"post-rewrite\";\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n-\tret = run_command(&cp);\n+\tstrvec_push(&opt.args, \"rebase\");\n+\topt.path_to_stdin = am_path(state, \"rewritten\");\n \n-\tclose(cp.in);\n-\treturn ret;\n+\treturn run_hooks_oneshot(\"post-rewrite\", &opt);\n }\n \n /**\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434548","messageId":"patch-v5-24.36-bb119fa7cc0-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 24/36] run-command: add stdin callback for parallelization","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:24Z","receivedAt":"2021-09-02T13:12:56Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIf a user of the run_processes_parallel() API wants to pipe a large\namount of information to stdin of each parallel command, that\ninformation could exceed the buffer of the pipe allocated for that\nprocess's stdin.  Generally this is solved by repeatedly writing to\nchild_process.in between calls to start_command() and finish_command();\nrun_processes_parallel() did not provide users an opportunity to access\nchild_process at that time.\n\nBecause the data might be extremely large (for example, a list of all\nrefs received during a push from a client) simply taking a string_list\nor strbuf is not as scalable as using a callback; the rest of the\nrun_processes_parallel() API also uses callbacks, so making this feature\nmatch the rest of the API reduces mental load on the user.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  1 +\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 54 +++++++++++++++++++++++++++++++++++--\n run-command.h               | 17 +++++++++++-\n submodule.c                 |  1 +\n t/helper/test-run-command.c | 31 ++++++++++++++++++---\n t/t0061-run-command.sh      | 30 +++++++++++++++++++++\n 8 files changed, 129 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex e064687dbdc..b18ada08842 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1819,6 +1819,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n+\t\t\t\t\t\t    NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex ef2776a9e45..0b73739c160 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2311,7 +2311,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure,\n+\t\t\t\t   update_clone_start_failure, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex d156b0dc800..45983d08aed 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -146,6 +146,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex 482ee2d76c6..f1616858d18 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1492,6 +1492,7 @@ struct parallel_processes {\n \n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n+\tfeed_pipe_fn feed_pipe;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1519,6 +1520,13 @@ static int default_start_failure(struct strbuf *out,\n \treturn 0;\n }\n \n+static int default_feed_pipe(struct strbuf *pipe,\n+\t\t\t     void *pp_cb,\n+\t\t\t     void *pp_task_cb)\n+{\n+\treturn 1;\n+}\n+\n static int default_task_finished(int result,\n \t\t\t\t struct strbuf *out,\n \t\t\t\t void *pp_cb,\n@@ -1549,6 +1557,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    int n,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n+\t\t    feed_pipe_fn feed_pipe,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1567,6 +1576,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->get_next_task = get_next_task;\n \n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n+\tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n \n \tpp->nr_processes = 0;\n@@ -1664,6 +1674,37 @@ static int pp_start_one(struct parallel_processes *pp)\n \treturn 0;\n }\n \n+static void pp_buffer_stdin(struct parallel_processes *pp)\n+{\n+\tint i;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\n+\t/* Buffer stdin for each pipe. */\n+\tfor (i = 0; i < pp->max_processes; i++) {\n+\t\tif (pp->children[i].state == GIT_CP_WORKING &&\n+\t\t    pp->children[i].process.in > 0) {\n+\t\t\tint done;\n+\t\t\tstrbuf_reset(&sb);\n+\t\t\tdone = pp->feed_pipe(&sb, pp->data,\n+\t\t\t\t\t      pp->children[i].data);\n+\t\t\tif (sb.len) {\n+\t\t\t\tif (write_in_full(pp->children[i].process.in,\n+\t\t\t\t\t      sb.buf, sb.len) < 0) {\n+\t\t\t\t\tif (errno != EPIPE)\n+\t\t\t\t\t\tdie_errno(\"write\");\n+\t\t\t\t\tdone = 1;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif (done) {\n+\t\t\t\tclose(pp->children[i].process.in);\n+\t\t\t\tpp->children[i].process.in = 0;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&sb);\n+}\n+\n static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n {\n \tint i;\n@@ -1728,6 +1769,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\tpp->nr_processes--;\n \t\tpp->children[i].state = GIT_CP_FREE;\n \t\tpp->pfd[i].fd = -1;\n+\t\tpp->children[i].process.in = 0;\n \t\tchild_process_init(&pp->children[i].process);\n \n \t\tif (i != pp->output_owner) {\n@@ -1761,6 +1803,7 @@ static int pp_collect_finished(struct parallel_processes *pp)\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n+\t\t\t   feed_pipe_fn feed_pipe,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1769,7 +1812,9 @@ int run_processes_parallel(int n,\n \tint spawn_cap = 4;\n \tstruct parallel_processes pp;\n \n-\tpp_init(&pp, n, get_next_task, start_failure, task_finished, pp_cb);\n+\tsigchain_push(SIGPIPE, SIG_IGN);\n+\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1786,6 +1831,7 @@ int run_processes_parallel(int n,\n \t\t}\n \t\tif (!pp.nr_processes)\n \t\t\tbreak;\n+\t\tpp_buffer_stdin(&pp);\n \t\tpp_buffer_stderr(&pp, output_timeout);\n \t\tpp_output(&pp);\n \t\tcode = pp_collect_finished(&pp);\n@@ -1797,11 +1843,15 @@ int run_processes_parallel(int n,\n \t}\n \n \tpp_cleanup(&pp);\n+\n+\tsigchain_pop(SIGPIPE);\n+\n \treturn 0;\n }\n \n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n+\t\t\t       feed_pipe_fn feed_pipe,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1811,7 +1861,7 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\ttask_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex cfb6887e4ae..80d394664ae 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -422,6 +422,20 @@ typedef int (*start_failure_fn)(struct strbuf *out,\n \t\t\t\tvoid *pp_cb,\n \t\t\t\tvoid *pp_task_cb);\n \n+/**\n+ * This callback is called repeatedly on every child process who requests\n+ * start_command() to create a pipe by setting child_process.in < 0.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel, and\n+ * pp_task_cb is the callback cookie as passed into get_next_task_fn.\n+ * The contents of 'send' will be read into the pipe and passed to the pipe.\n+ *\n+ * Return nonzero to close the pipe.\n+ */\n+typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n+\t\t\t    void *pp_cb,\n+\t\t\t    void *pp_task_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -456,10 +470,11 @@ typedef int (*task_finished_fn)(int result,\n int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n+\t\t\t   feed_pipe_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n /**\ndiff --git a/submodule.c b/submodule.c\nindex 8e611fe1dbf..db1700a502d 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1632,6 +1632,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n+\t\t\t\t   NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 7ae03dc7123..9348184d303 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,\n \t\treturn 0;\n \n \tstrvec_pushv(&cp->args, d->argv);\n+\tcp->in = d->in;\n+\tcp->no_stdin = d->no_stdin;\n \tstrbuf_addstr(err, \"preloaded output of a child\\n\");\n \tnumber_callbacks++;\n+\n+\t*task_cb = xmalloc(sizeof(int));\n+\t*(int*)(*task_cb) = 2;\n \treturn 1;\n }\n \n@@ -55,6 +60,17 @@ static int task_finished(int result,\n \treturn 1;\n }\n \n+static int test_stdin(struct strbuf *pipe, void *cb, void *task_cb)\n+{\n+\tint *lines_remaining = task_cb;\n+\n+\tif (*lines_remaining)\n+\t\tstrbuf_addf(pipe, \"sample stdin %d\\n\", --(*lines_remaining));\n+\n+\treturn !(*lines_remaining);\n+}\n+\n+\n struct testsuite {\n \tstruct string_list tests, failed;\n \tint next;\n@@ -185,7 +201,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_finished, &suite);\n+\t\t\t\t     test_stdin, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -413,15 +429,22 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\n+\tif (!strcmp(argv[1], \"run-command-stdin\")) {\n+\t\tproc.in = -1;\n+\t\tproc.no_stdin = 0;\n+\t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n+\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t}\n \n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 7d599675e35..87759482ad1 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,36 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+cat >expect <<-EOF\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+preloaded output of a child\n+listening for stdin:\n+sample stdin 1\n+sample stdin 0\n+EOF\n+\n+test_expect_success 'run_command listens to stdin' '\n+\twrite_script stdin-script <<-\\EOF &&\n+\techo \"listening for stdin:\"\n+\twhile read line; do\n+\t\techo \"$line\"\n+\tdone\n+\tEOF\n+\ttest-tool run-command run-command-stdin 2 ./stdin-script 2>actual &&\n+\ttest_cmp expect actual\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n asking for a quick stop\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434549","messageId":"patch-v5-25.36-2439f7752b8-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 25/36] hook: provide stdin by string_list or callback","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:25Z","receivedAt":"2021-09-02T13:12:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn cases where a hook requires only a small amount of information via\nstdin, it should be simple for users to provide a string_list alone. But\nin more complicated cases where the stdin is too large to hold in\nmemory, let's instead provide a callback the users can populate line\nafter line.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 33 ++++++++++++++++++++++++++++++++-\n hook.h | 29 +++++++++++++++++++++++++++++\n 2 files changed, 61 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 45983d08aed..7a6ef527443 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -48,6 +48,29 @@ void run_hooks_opt_clear(struct run_hooks_opt *o)\n \tstrvec_clear(&o->args);\n }\n \n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n+{\n+\tint *item_idx;\n+\tstruct hook *ctx = pp_task_cb;\n+\tstruct hook_cb_data *hook_cb = pp_cb;\n+\tstruct string_list *to_pipe = hook_cb->options->feed_pipe_ctx;\n+\n+\t/* Bootstrap the state manager if necessary. */\n+\tif (!ctx->feed_pipe_cb_data) {\n+\t\tctx->feed_pipe_cb_data = xmalloc(sizeof(unsigned int));\n+\t\t*(int*)ctx->feed_pipe_cb_data = 0;\n+\t}\n+\n+\titem_idx = ctx->feed_pipe_cb_data;\n+\n+\tif (*item_idx < to_pipe->nr) {\n+\t\tstrbuf_addf(pipe, \"%s\\n\", to_pipe->items[*item_idx].string);\n+\t\t(*item_idx)++;\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n static int pick_next_hook(struct child_process *cp,\n \t\t\t  struct strbuf *out,\n \t\t\t  void *pp_cb,\n@@ -63,6 +86,10 @@ static int pick_next_hook(struct child_process *cp,\n \tif (hook_cb->options->path_to_stdin) {\n \t\tcp->no_stdin = 0;\n \t\tcp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);\n+\t} else if (hook_cb->options->feed_pipe) {\n+\t\t/* ask for start_command() to make a pipe for us */\n+\t\tcp->in = -1;\n+\t\tcp->no_stdin = 0;\n \t} else {\n \t\tcp->no_stdin = 1;\n \t}\n@@ -146,7 +173,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \trun_processes_parallel_tr2(jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->feed_pipe,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n@@ -154,6 +181,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \n \tif (options->absolute_path)\n \t\tstrbuf_release(&abs_path);\n+\tfree(my_hook.feed_pipe_cb_data);\n \n \treturn cb_data.rc;\n }\n@@ -167,6 +195,9 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n \tif (!options)\n \t\toptions = &hook_opt_scratch;\n \n+\tif (options->path_to_stdin && options->feed_pipe)\n+\t\tBUG(\"choose only one method to populate stdin\");\n+\n \thook_path = find_hook(hook_name);\n \tif (!hook_path) {\n \t\tret = 0;\ndiff --git a/hook.h b/hook.h\nindex 253725e197b..4e26b8d658d 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,10 +1,18 @@\n #ifndef HOOK_H\n #define HOOK_H\n+#include \"strbuf.h\"\n #include \"strvec.h\"\n+#include \"run-command.h\"\n \n struct hook {\n \t/* The path to the hook */\n \tconst char *hook_path;\n+\n+\t/*\n+\t * Use this to keep state for your feed_pipe_fn if you are using\n+\t * run_hooks_opt.feed_pipe. Otherwise, do not touch it.\n+\t */\n+\tvoid *feed_pipe_cb_data;\n };\n \n struct run_hooks_opt\n@@ -26,6 +34,19 @@ struct run_hooks_opt\n \n \t/* Path to file which should be piped to stdin for each hook */\n \tconst char *path_to_stdin;\n+\n+\t/*\n+\t * Callback and state pointer to ask for more content to pipe to stdin.\n+\t * Will be called repeatedly, for each hook. See\n+\t * hook.c:pipe_from_stdin() for an example. Keep per-hook state in\n+\t * hook.feed_pipe_cb_data (per process). Keep initialization context in\n+\t * feed_pipe_ctx (shared by all processes).\n+\t *\n+\t * See 'pipe_from_string_list()' for info about how to specify a\n+\t * string_list as the stdin input instead of writing your own handler.\n+\t */\n+\tfeed_pipe_fn feed_pipe;\n+\tvoid *feed_pipe_ctx;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -77,4 +98,12 @@ int run_hooks(const char *hookname, const char *hook_path,\n  */\n int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n \n+/**\n+ * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n+ * string_list and set 'run_hooks_opt.feed_pipe' to pipe_from_string_list().\n+ * This will pipe each string in the list to stdin, separated by newlines.  (Do\n+ * not inject your own newlines.)\n+ */\n+int pipe_from_string_list(struct strbuf *pipe, void *pp_cb, void *pp_task_cb);\n+\n #endif\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434550","messageId":"patch-v5-26.36-48a380b3a91-20210902T125110Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 26/36] hook: convert 'post-rewrite' hook in sequencer.c to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:26Z","receivedAt":"2021-09-02T13:13:02Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nBy using 'hook.h' for 'post-rewrite', we simplify hook invocations by\nnot needing to put together our own 'struct child_process'.\n\nThe signal handling that's being removed by this commit now takes\nplace in run-command.h:run_processes_parallel(), so it is OK to remove\nthem here.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n sequencer.c | 79 ++++++++++++++++++++++-------------------------------\n 1 file changed, 32 insertions(+), 47 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 8999edf8d7b..549b583b277 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -35,6 +35,7 @@\n #include \"commit-reach.h\"\n #include \"rebase-interactive.h\"\n #include \"reset.h\"\n+#include \"string-list.h\"\n \n #define GIT_REFLOG_ACTION \"GIT_REFLOG_ACTION\"\n \n@@ -1147,33 +1148,27 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *argv[3];\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n-\tstruct strbuf sb = STRBUF_INIT;\n \n-\targv[0] = find_hook(\"post-rewrite\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tstrvec_push(&opt.args, \"amend\");\n \n-\targv[1] = \"amend\";\n-\targv[2] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"post-rewrite\";\n-\n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n-\tstrbuf_addf(&sb, \"%s %s\\n\", oid_to_hex(oldoid), oid_to_hex(newoid));\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\twrite_in_full(proc.in, sb.buf, sb.len);\n-\tclose(proc.in);\n-\tstrbuf_release(&sb);\n-\tsigchain_pop(SIGPIPE);\n-\treturn finish_command(&proc);\n+\tstrbuf_addf(&tmp,\n+\t\t    \"%s %s\",\n+\t\t    oid_to_hex(oldoid),\n+\t\t    oid_to_hex(newoid));\n+\tstring_list_append(&to_stdin, tmp.buf);\n+\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tcode = run_hooks_oneshot(\"post-rewrite\", &opt);\n+\n+\tstrbuf_release(&tmp);\n+\tstring_list_clear(&to_stdin, 0);\n+\treturn code;\n }\n \n void commit_post_rewrite(struct repository *r,\n@@ -4526,30 +4521,20 @@ static int pick_commits(struct repository *r,\n \t\tflush_rewritten_pending();\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n-\t\t\tstruct child_process child = CHILD_PROCESS_INIT;\n-\t\t\tconst char *post_rewrite_hook =\n-\t\t\t\tfind_hook(\"post-rewrite\");\n-\n-\t\t\tchild.in = open(rebase_path_rewritten_list(), O_RDONLY);\n-\t\t\tchild.git_cmd = 1;\n-\t\t\tstrvec_push(&child.args, \"notes\");\n-\t\t\tstrvec_push(&child.args, \"copy\");\n-\t\t\tstrvec_push(&child.args, \"--for-rewrite=rebase\");\n+\t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\n+\t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n+\t\t\tnotes_cp.git_cmd = 1;\n+\t\t\tstrvec_push(&notes_cp.args, \"notes\");\n+\t\t\tstrvec_push(&notes_cp.args, \"copy\");\n+\t\t\tstrvec_push(&notes_cp.args, \"--for-rewrite=rebase\");\n \t\t\t/* we don't care if this copying failed */\n-\t\t\trun_command(&child);\n-\n-\t\t\tif (post_rewrite_hook) {\n-\t\t\t\tstruct child_process hook = CHILD_PROCESS_INIT;\n-\n-\t\t\t\thook.in = open(rebase_path_rewritten_list(),\n-\t\t\t\t\tO_RDONLY);\n-\t\t\t\thook.stdout_to_stderr = 1;\n-\t\t\t\thook.trace2_hook_name = \"post-rewrite\";\n-\t\t\t\tstrvec_push(&hook.args, post_rewrite_hook);\n-\t\t\t\tstrvec_push(&hook.args, \"rebase\");\n-\t\t\t\t/* we don't care if this hook failed */\n-\t\t\t\trun_command(&hook);\n-\t\t\t}\n+\t\t\trun_command(&notes_cp);\n+\n+\t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n+\t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n+\t\t\trun_hooks_oneshot(\"post-rewrite\", &hook_opt);\n \t\t}\n \t\tapply_autostash(rebase_path_autostash());\n \n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434551","messageId":"patch-v5-28.36-957691f0b6d-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 28/36] hook tests: test for exact \"pre-push\" hook input","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:28Z","receivedAt":"2021-09-02T13:13:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Extend the tests added in ec55559f937 (push: Add support for pre-push\nhooks, 2013-01-13) to exhaustively test for the exact input we're\nexpecting. This ensures that we e.g. don't miss a trailing newline.\n\nAppending to a file called \"actual\" is the established convention in\nthis test for hooks, see the rest of the tests added in\nec55559f937 (push: Add support for pre-push hooks, 2013-01-13). Let's\nfollow that convention here.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t5571-pre-push-hook.sh | 23 ++++++++++++++++++-----\n 1 file changed, 18 insertions(+), 5 deletions(-)\n\ndiff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh\nindex ad8d5804f7b..d2857a6fbc0 100755\n--- a/t/t5571-pre-push-hook.sh\n+++ b/t/t5571-pre-push-hook.sh\n@@ -11,7 +11,7 @@ HOOKDIR=\"$(git rev-parse --git-dir)/hooks\"\n HOOK=\"$HOOKDIR/pre-push\"\n mkdir -p \"$HOOKDIR\"\n write_script \"$HOOK\" <<EOF\n-cat >/dev/null\n+cat >actual\n exit 0\n EOF\n \n@@ -20,10 +20,16 @@ test_expect_success 'setup' '\n \tgit init --bare repo1 &&\n \tgit remote add parent1 repo1 &&\n \ttest_commit one &&\n-\tgit push parent1 HEAD:foreign\n+\tcat >expect <<-EOF &&\n+\tHEAD $(git rev-parse HEAD) refs/heads/foreign $(test_oid zero)\n+\tEOF\n+\n+\ttest_when_finished \"rm actual\" &&\n+\tgit push parent1 HEAD:foreign &&\n+\ttest_cmp expect actual\n '\n write_script \"$HOOK\" <<EOF\n-cat >/dev/null\n+cat >actual\n exit 1\n EOF\n \n@@ -32,11 +38,18 @@ export COMMIT1\n \n test_expect_success 'push with failing hook' '\n \ttest_commit two &&\n-\ttest_must_fail git push parent1 HEAD\n+\tcat >expect <<-EOF &&\n+\tHEAD $(git rev-parse HEAD) refs/heads/main $(test_oid zero)\n+\tEOF\n+\n+\ttest_when_finished \"rm actual\" &&\n+\ttest_must_fail git push parent1 HEAD &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success '--no-verify bypasses hook' '\n-\tgit push --no-verify parent1 HEAD\n+\tgit push --no-verify parent1 HEAD &&\n+\ttest_path_is_missing actual\n '\n \n COMMIT2=\"$(git rev-parse HEAD)\"\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434552","messageId":"patch-v5-27.36-af6b9292aaa-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 27/36] transport: convert pre-push hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:27Z","receivedAt":"2021-09-02T13:13:15Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nMove the pre-push hook away from run-command.h to and over to the new\nhook.h library.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n transport.c | 56 ++++++++++++++---------------------------------------\n 1 file changed, 14 insertions(+), 42 deletions(-)\n\ndiff --git a/transport.c b/transport.c\nindex 77e196f75f5..4ca8fc0391d 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1203,63 +1203,35 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)\n static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n-\tint ret = 0, x;\n+\tint ret = 0;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tstruct ref *r;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct strbuf buf;\n-\tconst char *argv[4];\n-\n-\tif (!(argv[0] = find_hook(\"pre-push\")))\n-\t\treturn 0;\n-\n-\targv[1] = transport->remote->name;\n-\targv[2] = transport->url;\n-\targv[3] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.trace2_hook_name = \"pre-push\";\n-\n-\tif (start_command(&proc)) {\n-\t\tfinish_command(&proc);\n-\t\treturn -1;\n-\t}\n+\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\n-\tstrbuf_init(&buf, 256);\n+\tstrvec_push(&opt.args, transport->remote->name);\n+\tstrvec_push(&opt.args, transport->url);\n \n \tfor (r = remote_refs; r; r = r->next) {\n+\t\tstruct strbuf buf = STRBUF_INIT;\n+\n \t\tif (!r->peer_ref) continue;\n \t\tif (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;\n \t\tif (r->status == REF_STATUS_REJECT_STALE) continue;\n \t\tif (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;\n \t\tif (r->status == REF_STATUS_UPTODATE) continue;\n \n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf( &buf, \"%s %s %s %s\\n\",\n+\t\tstrbuf_addf(&buf, \"%s %s %s %s\",\n \t\t\t r->peer_ref->name, oid_to_hex(&r->new_oid),\n \t\t\t r->name, oid_to_hex(&r->old_oid));\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\t/* We do not mind if a hook does not read all refs. */\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n \t}\n \n-\tstrbuf_release(&buf);\n-\n-\tx = close(proc.in);\n-\tif (!ret)\n-\t\tret = x;\n-\n-\tsigchain_pop(SIGPIPE);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n \n-\tx = finish_command(&proc);\n-\tif (!ret)\n-\t\tret = x;\n+\tret = run_hooks_oneshot(\"pre-push\", &opt);\n+\tto_stdin.strdup_strings = 1;\n+\tstring_list_clear(&to_stdin, 0);\n \n \treturn ret;\n }\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434553","messageId":"patch-v5-31.36-fac56a9d8af-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 31/36] run-command: allow capturing of collated output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:31Z","receivedAt":"2021-09-02T13:13:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome callers, for example server-side hooks which wish to relay hook\noutput to clients across a transport, want to capture what would\nnormally print to stderr and do something else with it. Allow that via a\ncallback.\n\nBy calling the callback regardless of whether there's output available,\nwe allow clients to send e.g. a keepalive if necessary.\n\nBecause we expose a strbuf, not a fd or FILE*, there's no need to create\na temporary pipe or similar - we can just skip the print to stderr and\ninstead hand it to the caller.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/fetch.c             |  2 +-\n builtin/submodule--helper.c |  2 +-\n hook.c                      |  1 +\n run-command.c               | 33 +++++++++++++++++++++++++--------\n run-command.h               | 18 +++++++++++++++++-\n submodule.c                 |  2 +-\n t/helper/test-run-command.c | 25 ++++++++++++++++++++-----\n t/t0061-run-command.sh      |  7 +++++++\n 8 files changed, 73 insertions(+), 17 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex b18ada08842..27b63ee5200 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1819,7 +1819,7 @@ static int fetch_multiple(struct string_list *list, int max_children)\n \t\tresult = run_processes_parallel_tr2(max_children,\n \t\t\t\t\t\t    &fetch_next_remote,\n \t\t\t\t\t\t    &fetch_failed_to_start,\n-\t\t\t\t\t\t    NULL,\n+\t\t\t\t\t\t    NULL, NULL,\n \t\t\t\t\t\t    &fetch_finished,\n \t\t\t\t\t\t    &state,\n \t\t\t\t\t\t    \"fetch\", \"parallel/fetch\");\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 0b73739c160..fbceab08b0d 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -2311,7 +2311,7 @@ static int update_submodules(struct submodule_update_clone *suc)\n \tint i;\n \n \trun_processes_parallel_tr2(suc->max_jobs, update_clone_get_next_task,\n-\t\t\t\t   update_clone_start_failure, NULL,\n+\t\t\t\t   update_clone_start_failure, NULL, NULL,\n \t\t\t\t   update_clone_task_finished, suc, \"submodule\",\n \t\t\t\t   \"parallel/update\");\n \ndiff --git a/hook.c b/hook.c\nindex 7a6ef527443..169287ad152 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -174,6 +174,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n+\t\t\t\t   NULL,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\ndiff --git a/run-command.c b/run-command.c\nindex f1616858d18..aacc336f951 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -1493,6 +1493,7 @@ struct parallel_processes {\n \tget_next_task_fn get_next_task;\n \tstart_failure_fn start_failure;\n \tfeed_pipe_fn feed_pipe;\n+\tconsume_sideband_fn consume_sideband;\n \ttask_finished_fn task_finished;\n \n \tstruct {\n@@ -1558,6 +1559,7 @@ static void pp_init(struct parallel_processes *pp,\n \t\t    get_next_task_fn get_next_task,\n \t\t    start_failure_fn start_failure,\n \t\t    feed_pipe_fn feed_pipe,\n+\t\t    consume_sideband_fn consume_sideband,\n \t\t    task_finished_fn task_finished,\n \t\t    void *data)\n {\n@@ -1578,6 +1580,7 @@ static void pp_init(struct parallel_processes *pp,\n \tpp->start_failure = start_failure ? start_failure : default_start_failure;\n \tpp->feed_pipe = feed_pipe ? feed_pipe : default_feed_pipe;\n \tpp->task_finished = task_finished ? task_finished : default_task_finished;\n+\tpp->consume_sideband = consume_sideband;\n \n \tpp->nr_processes = 0;\n \tpp->output_owner = 0;\n@@ -1614,7 +1617,10 @@ static void pp_cleanup(struct parallel_processes *pp)\n \t * When get_next_task added messages to the buffer in its last\n \t * iteration, the buffered output is non empty.\n \t */\n-\tstrbuf_write(&pp->buffered_output, stderr);\n+\tif (pp->consume_sideband)\n+\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\telse\n+\t\tstrbuf_write(&pp->buffered_output, stderr);\n \tstrbuf_release(&pp->buffered_output);\n \n \tsigchain_pop_common();\n@@ -1735,9 +1741,13 @@ static void pp_buffer_stderr(struct parallel_processes *pp, int output_timeout)\n static void pp_output(struct parallel_processes *pp)\n {\n \tint i = pp->output_owner;\n+\n \tif (pp->children[i].state == GIT_CP_WORKING &&\n \t    pp->children[i].err.len) {\n-\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\tif (pp->consume_sideband)\n+\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\telse\n+\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n \t\tstrbuf_reset(&pp->children[i].err);\n \t}\n }\n@@ -1776,11 +1786,15 @@ static int pp_collect_finished(struct parallel_processes *pp)\n \t\t\tstrbuf_addbuf(&pp->buffered_output, &pp->children[i].err);\n \t\t\tstrbuf_reset(&pp->children[i].err);\n \t\t} else {\n-\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t/* Output errors, then all other finished child processes */\n+\t\t\tif (pp->consume_sideband) {\n+\t\t\t\tpp->consume_sideband(&pp->children[i].err, pp->data);\n+\t\t\t\tpp->consume_sideband(&pp->buffered_output, pp->data);\n+\t\t\t} else {\n+\t\t\t\tstrbuf_write(&pp->children[i].err, stderr);\n+\t\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n+\t\t\t}\n \t\t\tstrbuf_reset(&pp->children[i].err);\n-\n-\t\t\t/* Output all other finished child processes */\n-\t\t\tstrbuf_write(&pp->buffered_output, stderr);\n \t\t\tstrbuf_reset(&pp->buffered_output);\n \n \t\t\t/*\n@@ -1804,6 +1818,7 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn get_next_task,\n \t\t\t   start_failure_fn start_failure,\n \t\t\t   feed_pipe_fn feed_pipe,\n+\t\t\t   consume_sideband_fn consume_sideband,\n \t\t\t   task_finished_fn task_finished,\n \t\t\t   void *pp_cb)\n {\n@@ -1814,7 +1829,7 @@ int run_processes_parallel(int n,\n \n \tsigchain_push(SIGPIPE, SIG_IGN);\n \n-\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, task_finished, pp_cb);\n+\tpp_init(&pp, n, get_next_task, start_failure, feed_pipe, consume_sideband, task_finished, pp_cb);\n \twhile (1) {\n \t\tfor (i = 0;\n \t\t    i < spawn_cap && !pp.shutdown &&\n@@ -1852,6 +1867,7 @@ int run_processes_parallel(int n,\n int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t       start_failure_fn start_failure,\n \t\t\t       feed_pipe_fn feed_pipe,\n+\t\t\t       consume_sideband_fn consume_sideband,\n \t\t\t       task_finished_fn task_finished, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label)\n {\n@@ -1861,7 +1877,8 @@ int run_processes_parallel_tr2(int n, get_next_task_fn get_next_task,\n \t\t\t\t   ((n < 1) ? online_cpus() : n));\n \n \tresult = run_processes_parallel(n, get_next_task, start_failure,\n-\t\t\t\t\tfeed_pipe, task_finished, pp_cb);\n+\t\t\t\t\tfeed_pipe, consume_sideband,\n+\t\t\t\t\ttask_finished, pp_cb);\n \n \ttrace2_region_leave(tr2_category, tr2_label, NULL);\n \ndiff --git a/run-command.h b/run-command.h\nindex 80d394664ae..e321d23bbd2 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -436,6 +436,20 @@ typedef int (*feed_pipe_fn)(struct strbuf *pipe,\n \t\t\t    void *pp_cb,\n \t\t\t    void *pp_task_cb);\n \n+/**\n+ * If this callback is provided, instead of collating process output to stderr,\n+ * they will be collated into a new pipe. consume_sideband_fn will be called\n+ * repeatedly. When output is available on that pipe, it will be contained in\n+ * 'output'. But it will be called with an empty 'output' too, to allow for\n+ * keepalives or similar operations if necessary.\n+ *\n+ * pp_cb is the callback cookie as passed into run_processes_parallel.\n+ *\n+ * Since this callback is provided with the collated output, no task cookie is\n+ * provided.\n+ */\n+typedef void (*consume_sideband_fn)(struct strbuf *output, void *pp_cb);\n+\n /**\n  * This callback is called on every child process that finished processing.\n  *\n@@ -471,10 +485,12 @@ int run_processes_parallel(int n,\n \t\t\t   get_next_task_fn,\n \t\t\t   start_failure_fn,\n \t\t\t   feed_pipe_fn,\n+\t\t\t   consume_sideband_fn,\n \t\t\t   task_finished_fn,\n \t\t\t   void *pp_cb);\n int run_processes_parallel_tr2(int n, get_next_task_fn, start_failure_fn,\n-\t\t\t       feed_pipe_fn, task_finished_fn, void *pp_cb,\n+\t\t\t       feed_pipe_fn, consume_sideband_fn,\n+\t\t\t       task_finished_fn, void *pp_cb,\n \t\t\t       const char *tr2_category, const char *tr2_label);\n \n /**\ndiff --git a/submodule.c b/submodule.c\nindex db1700a502d..32364d8bd56 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1632,7 +1632,7 @@ int fetch_populated_submodules(struct repository *r,\n \trun_processes_parallel_tr2(max_parallel_jobs,\n \t\t\t\t   get_next_submodule,\n \t\t\t\t   fetch_start_failure,\n-\t\t\t\t   NULL,\n+\t\t\t\t   NULL, NULL,\n \t\t\t\t   fetch_finish,\n \t\t\t\t   &spf,\n \t\t\t\t   \"submodule\", \"parallel/fetch\");\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex 9348184d303..d53db6d11c4 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -51,6 +51,16 @@ static int no_job(struct child_process *cp,\n \treturn 0;\n }\n \n+static void test_consume_sideband(struct strbuf *output, void *cb)\n+{\n+\tFILE *sideband;\n+\n+\tsideband = fopen(\"./sideband\", \"a\");\n+\n+\tstrbuf_write(output, sideband);\n+\tfclose(sideband);\n+}\n+\n static int task_finished(int result,\n \t\t\t struct strbuf *err,\n \t\t\t void *pp_cb,\n@@ -201,7 +211,7 @@ static int testsuite(int argc, const char **argv)\n \t\tsuite.tests.nr, max_jobs);\n \n \tret = run_processes_parallel(max_jobs, next_test, test_failed,\n-\t\t\t\t     test_stdin, test_finished, &suite);\n+\t\t\t\t     test_stdin, NULL, test_finished, &suite);\n \n \tif (suite.failed.nr > 0) {\n \t\tret = 1;\n@@ -429,23 +439,28 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, NULL, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-no-jobs\"))\n \t\texit(run_processes_parallel(jobs, no_job,\n-\t\t\t\t\t    NULL, NULL, task_finished, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_finished, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-stdin\")) {\n \t\tproc.in = -1;\n \t\tproc.no_stdin = 0;\n \t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n-\t\t\t\t\t     test_stdin, NULL, &proc));\n+\t\t\t\t\t     test_stdin, NULL, NULL, &proc));\n \t}\n \n+\tif (!strcmp(argv[1], \"run-command-sideband\"))\n+\t\texit(run_processes_parallel(jobs, parallel_next, NULL, NULL,\n+\t\t\t\t\t    test_consume_sideband, NULL,\n+\t\t\t\t\t    &proc));\n+\n \tfprintf(stderr, \"check usage\\n\");\n \treturn 1;\n }\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 87759482ad1..e99f6c7f445 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -143,6 +143,13 @@ test_expect_success 'run_command runs in parallel with more tasks than jobs avai\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'run_command can divert output' '\n+\ttest_when_finished rm sideband &&\n+\ttest-tool run-command run-command-sideband 3 sh -c \"printf \\\"%s\\n%s\\n\\\" Hello World\" 2>actual &&\n+\ttest_must_be_empty actual &&\n+\ttest_cmp expect sideband\n+'\n+\n cat >expect <<-EOF\n preloaded output of a child\n listening for stdin:\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434554","messageId":"patch-v5-32.36-7d185cdf9d1-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 32/36] hooks: allow callers to capture output","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:32Z","receivedAt":"2021-09-02T13:13:23Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSome server-side hooks will require capturing output to send over\nsideband instead of printing directly to stderr. Expose that capability.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n hook.c | 3 ++-\n hook.h | 8 ++++++++\n 2 files changed, 10 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 169287ad152..7f93fe40dba 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -174,12 +174,13 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n-\t\t\t\t   NULL,\n+\t\t\t\t   options->consume_sideband,\n \t\t\t\t   notify_hook_finished,\n \t\t\t\t   &cb_data,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n \n+\n \tif (options->absolute_path)\n \t\tstrbuf_release(&abs_path);\n \tfree(my_hook.feed_pipe_cb_data);\ndiff --git a/hook.h b/hook.h\nindex 4e26b8d658d..a85e3e6981b 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -47,6 +47,14 @@ struct run_hooks_opt\n \t */\n \tfeed_pipe_fn feed_pipe;\n \tvoid *feed_pipe_ctx;\n+\n+\t/*\n+\t * Populate this to capture output and prevent it from being printed to\n+\t * stderr. This will be passed directly through to\n+\t * run_command:run_parallel_processes(). See t/helper/test-run-command.c\n+\t * for an example.\n+\t */\n+\tconsume_sideband_fn consume_sideband;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434555","messageId":"patch-v5-30.36-1d905e81779-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 30/36] reference-transaction: use hook.h to run hooks","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:30Z","receivedAt":"2021-09-02T13:13:23Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n refs.c | 40 +++++++++++++---------------------------\n 1 file changed, 13 insertions(+), 27 deletions(-)\n\ndiff --git a/refs.c b/refs.c\nindex 6211692eaae..73d4a939267 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2062,47 +2062,33 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct strbuf buf = STRBUF_INIT;\n-\tconst char *hook;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \tint ret = 0, i;\n \n-\thook = find_hook(\"reference-transaction\");\n-\tif (!hook)\n-\t\treturn ret;\n-\n-\tstrvec_pushl(&proc.args, hook, state, NULL);\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = \"reference-transaction\";\n-\n-\tret = start_command(&proc);\n-\tif (ret)\n+\tif (!hook_exists(\"reference-transaction\"))\n \t\treturn ret;\n \n-\tsigchain_push(SIGPIPE, SIG_IGN);\n+\tstrvec_push(&opt.args, state);\n \n \tfor (i = 0; i < transaction->nr; i++) {\n \t\tstruct ref_update *update = transaction->updates[i];\n+\t\tstruct strbuf buf = STRBUF_INIT;\n \n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"%s %s %s\\n\",\n+\t\tstrbuf_addf(&buf, \"%s %s %s\",\n \t\t\t    oid_to_hex(&update->old_oid),\n \t\t\t    oid_to_hex(&update->new_oid),\n \t\t\t    update->refname);\n-\n-\t\tif (write_in_full(proc.in, buf.buf, buf.len) < 0) {\n-\t\t\tif (errno != EPIPE)\n-\t\t\t\tret = -1;\n-\t\t\tbreak;\n-\t\t}\n+\t\tstring_list_append(&to_stdin, strbuf_detach(&buf, NULL));\n \t}\n \n-\tclose(proc.in);\n-\tsigchain_pop(SIGPIPE);\n-\tstrbuf_release(&buf);\n+\topt.feed_pipe = pipe_from_string_list;\n+\topt.feed_pipe_ctx = &to_stdin;\n+\n+\tret = run_hooks_oneshot(\"reference-transaction\", &opt);\n+\tto_stdin.strdup_strings = 1;\n+\tstring_list_clear(&to_stdin, 0);\n \n-\tret |= finish_command(&proc);\n \treturn ret;\n }\n \n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434556","messageId":"patch-v5-29.36-88fe2621549-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 29/36] hook tests: use a modern style for \"pre-push\" tests","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:29Z","receivedAt":"2021-09-02T13:13:23Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Indent the here-docs and use \"test_cmp\" instead of \"diff\" in tests\nadded in ec55559f937 (push: Add support for pre-push hooks,\n2013-01-13). Let's also use the more typical \"expect\" instead of\n\"expected\" to be consistent with the rest of the test file.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t5571-pre-push-hook.sh | 71 ++++++++++++++++++++--------------------\n 1 file changed, 35 insertions(+), 36 deletions(-)\n\ndiff --git a/t/t5571-pre-push-hook.sh b/t/t5571-pre-push-hook.sh\nindex d2857a6fbc0..cc1a3e23668 100755\n--- a/t/t5571-pre-push-hook.sh\n+++ b/t/t5571-pre-push-hook.sh\n@@ -61,15 +61,15 @@ echo \"$2\" >>actual\n cat >>actual\n EOF\n \n-cat >expected <<EOF\n-parent1\n-repo1\n-refs/heads/main $COMMIT2 refs/heads/foreign $COMMIT1\n-EOF\n-\n test_expect_success 'push with hook' '\n+\tcat >expect <<-EOF &&\n+\tparent1\n+\trepo1\n+\trefs/heads/main $COMMIT2 refs/heads/foreign $COMMIT1\n+\tEOF\n+\n \tgit push parent1 main:foreign &&\n-\tdiff expected actual\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'add a branch' '\n@@ -80,49 +80,48 @@ test_expect_success 'add a branch' '\n COMMIT3=\"$(git rev-parse HEAD)\"\n export COMMIT3\n \n-cat >expected <<EOF\n-parent1\n-repo1\n-refs/heads/other $COMMIT3 refs/heads/foreign $COMMIT2\n-EOF\n-\n test_expect_success 'push to default' '\n+\tcat >expect <<-EOF &&\n+\tparent1\n+\trepo1\n+\trefs/heads/other $COMMIT3 refs/heads/foreign $COMMIT2\n+\tEOF\n \tgit push &&\n-\tdiff expected actual\n+\ttest_cmp expect actual\n '\n \n-cat >expected <<EOF\n-parent1\n-repo1\n-refs/tags/one $COMMIT1 refs/tags/tag1 $ZERO_OID\n-HEAD~ $COMMIT2 refs/heads/prev $ZERO_OID\n-EOF\n-\n test_expect_success 'push non-branches' '\n+\tcat >expect <<-EOF &&\n+\tparent1\n+\trepo1\n+\trefs/tags/one $COMMIT1 refs/tags/tag1 $ZERO_OID\n+\tHEAD~ $COMMIT2 refs/heads/prev $ZERO_OID\n+\tEOF\n+\n \tgit push parent1 one:tag1 HEAD~:refs/heads/prev &&\n-\tdiff expected actual\n+\ttest_cmp expect actual\n '\n \n-cat >expected <<EOF\n-parent1\n-repo1\n-(delete) $ZERO_OID refs/heads/prev $COMMIT2\n-EOF\n-\n test_expect_success 'push delete' '\n+\tcat >expect <<-EOF &&\n+\tparent1\n+\trepo1\n+\t(delete) $ZERO_OID refs/heads/prev $COMMIT2\n+\tEOF\n+\n \tgit push parent1 :prev &&\n-\tdiff expected actual\n+\ttest_cmp expect actual\n '\n \n-cat >expected <<EOF\n-repo1\n-repo1\n-HEAD $COMMIT3 refs/heads/other $ZERO_OID\n-EOF\n-\n test_expect_success 'push to URL' '\n+\tcat >expect <<-EOF &&\n+\trepo1\n+\trepo1\n+\tHEAD $COMMIT3 refs/heads/other $ZERO_OID\n+\tEOF\n+\n \tgit push repo1 HEAD &&\n-\tdiff expected actual\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'set up many-ref tests' '\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434557","messageId":"patch-v5-33.36-c8150e1239f-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 33/36] receive-pack: convert 'update' hook to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:33Z","receivedAt":"2021-09-02T13:13:26Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nThis makes use of the new sideband API in hook.h added in the\npreceding commit.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 62 ++++++++++++++++++++++++++++--------------\n 1 file changed, 41 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a7d03bbc7d3..31ce4ece4e7 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -937,33 +937,53 @@ static int run_receive_hook(struct command *commands,\n \treturn status;\n }\n \n-static int run_update_hook(struct command *cmd)\n+static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n {\n-\tconst char *argv[5];\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tint code;\n+\tint keepalive_active = 0;\n \n-\targv[0] = find_hook(\"update\");\n-\tif (!argv[0])\n-\t\treturn 0;\n+\tif (keepalive_in_sec <= 0)\n+\t\tuse_keepalive = KEEPALIVE_NEVER;\n+\tif (use_keepalive == KEEPALIVE_ALWAYS)\n+\t\tkeepalive_active = 1;\n \n-\targv[1] = cmd->ref_name;\n-\targv[2] = oid_to_hex(&cmd->old_oid);\n-\targv[3] = oid_to_hex(&cmd->new_oid);\n-\targv[4] = NULL;\n+\t/* send a keepalive if there is no data to write */\n+\tif (keepalive_active && !output->len) {\n+\t\tstatic const char buf[] = \"0005\\1\";\n+\t\twrite_or_die(1, buf, sizeof(buf) - 1);\n+\t\treturn;\n+\t}\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.argv = argv;\n-\tproc.trace2_hook_name = \"update\";\n+\tif (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) {\n+\t\tconst char *first_null = memchr(output->buf, '\\0', output->len);\n+\t\tif (first_null) {\n+\t\t\t/* The null bit is excluded. */\n+\t\t\tsize_t before_null = first_null - output->buf;\n+\t\t\tsize_t after_null = output->len - (before_null + 1);\n+\t\t\tkeepalive_active = 1;\n+\t\t\tsend_sideband(1, 2, output->buf, before_null, use_sideband);\n+\t\t\tsend_sideband(1, 2, first_null + 1, after_null, use_sideband);\n+\n+\t\t\treturn;\n+\t\t}\n+\t}\n+\n+\tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n+}\n+\n+static int run_update_hook(struct command *cmd)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\n+\tstrvec_pushl(&opt.args,\n+\t\t     cmd->ref_name,\n+\t\t     oid_to_hex(&cmd->old_oid),\n+\t\t     oid_to_hex(&cmd->new_oid),\n+\t\t     NULL);\n \n-\tcode = start_command(&proc);\n-\tif (code)\n-\t\treturn code;\n \tif (use_sideband)\n-\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\treturn finish_command(&proc);\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\treturn run_hooks_oneshot(\"update\", &opt);\n }\n \n static struct command *find_command_by_refname(struct command *list,\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434558","messageId":"patch-v5-34.36-a20ad847c14-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 34/36] post-update: use hook.h library","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:34Z","receivedAt":"2021-09-02T13:13:31Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 25 ++++++-------------------\n 1 file changed, 6 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 31ce4ece4e7..26e302aab85 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1650,33 +1650,20 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tconst char *hook;\n-\n-\thook = find_hook(\"post-update\");\n-\tif (!hook)\n-\t\treturn;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\n \t\t\tcontinue;\n-\t\tif (!proc.args.nr)\n-\t\t\tstrvec_push(&proc.args, hook);\n-\t\tstrvec_push(&proc.args, cmd->ref_name);\n+\t\tstrvec_push(&opt.args, cmd->ref_name);\n \t}\n-\tif (!proc.args.nr)\n+\tif (!opt.args.nr)\n \t\treturn;\n \n-\tproc.no_stdin = 1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.err = use_sideband ? -1 : 0;\n-\tproc.trace2_hook_name = \"post-update\";\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n \n-\tif (!start_command(&proc)) {\n-\t\tif (use_sideband)\n-\t\t\tcopy_to_sideband(proc.err, -1, NULL);\n-\t\tfinish_command(&proc);\n-\t}\n+\trun_hooks_oneshot(\"post-update\", &opt);\n }\n \n static void check_aliased_update_internal(struct command *cmd,\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434559","messageId":"patch-v5-35.36-79c380be6ed-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 35/36] receive-pack: convert receive hooks to hook.h","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:35Z","receivedAt":"2021-09-02T13:13:31Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/receive-pack.c | 190 ++++++++++++++++++-----------------------\n 1 file changed, 83 insertions(+), 107 deletions(-)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 26e302aab85..c3984680d7f 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -747,7 +747,7 @@ static int check_cert_push_options(const struct string_list *push_options)\n \treturn retval;\n }\n \n-static void prepare_push_cert_sha1(struct child_process *proc)\n+static void prepare_push_cert_sha1(struct run_hooks_opt *opt)\n {\n \tstatic int already_done;\n \n@@ -771,110 +771,42 @@ static void prepare_push_cert_sha1(struct child_process *proc)\n \t\tnonce_status = check_nonce(push_cert.buf, bogs);\n \t}\n \tif (!is_null_oid(&push_cert_oid)) {\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT=%s\",\n \t\t\t     oid_to_hex(&push_cert_oid));\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_SIGNER=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_SIGNER=%s\",\n \t\t\t     sigcheck.signer ? sigcheck.signer : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_KEY=%s\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_KEY=%s\",\n \t\t\t     sigcheck.key ? sigcheck.key : \"\");\n-\t\tstrvec_pushf(&proc->env_array, \"GIT_PUSH_CERT_STATUS=%c\",\n+\t\tstrvec_pushf(&opt->env, \"GIT_PUSH_CERT_STATUS=%c\",\n \t\t\t     sigcheck.result);\n \t\tif (push_cert_nonce) {\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE=%s\",\n \t\t\t\t     push_cert_nonce);\n-\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t     \"GIT_PUSH_CERT_NONCE_STATUS=%s\",\n \t\t\t\t     nonce_status);\n \t\t\tif (nonce_status == NONCE_SLOP)\n-\t\t\t\tstrvec_pushf(&proc->env_array,\n+\t\t\t\tstrvec_pushf(&opt->env,\n \t\t\t\t\t     \"GIT_PUSH_CERT_NONCE_SLOP=%ld\",\n \t\t\t\t\t     nonce_stamp_slop);\n \t\t}\n \t}\n }\n \n+struct receive_hook_feed_context {\n+\tstruct command *cmd;\n+\tint skip_broken;\n+};\n+\n struct receive_hook_feed_state {\n \tstruct command *cmd;\n \tstruct ref_push_report *report;\n \tint skip_broken;\n \tstruct strbuf buf;\n-\tconst struct string_list *push_options;\n };\n \n-typedef int (*feed_fn)(void *, const char **, size_t *);\n-static int run_and_feed_hook(const char *hook_name, feed_fn feed,\n-\t\t\t     struct receive_hook_feed_state *feed_state)\n-{\n-\tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tstruct async muxer;\n-\tconst char *argv[2];\n-\tint code;\n-\n-\targv[0] = find_hook(hook_name);\n-\tif (!argv[0])\n-\t\treturn 0;\n-\n-\targv[1] = NULL;\n-\n-\tproc.argv = argv;\n-\tproc.in = -1;\n-\tproc.stdout_to_stderr = 1;\n-\tproc.trace2_hook_name = hook_name;\n-\n-\tif (feed_state->push_options) {\n-\t\tint i;\n-\t\tfor (i = 0; i < feed_state->push_options->nr; i++)\n-\t\t\tstrvec_pushf(&proc.env_array,\n-\t\t\t\t     \"GIT_PUSH_OPTION_%d=%s\", i,\n-\t\t\t\t     feed_state->push_options->items[i].string);\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT=%d\",\n-\t\t\t     feed_state->push_options->nr);\n-\t} else\n-\t\tstrvec_pushf(&proc.env_array, \"GIT_PUSH_OPTION_COUNT\");\n-\n-\tif (tmp_objdir)\n-\t\tstrvec_pushv(&proc.env_array, tmp_objdir_env(tmp_objdir));\n-\n-\tif (use_sideband) {\n-\t\tmemset(&muxer, 0, sizeof(muxer));\n-\t\tmuxer.proc = copy_to_sideband;\n-\t\tmuxer.in = -1;\n-\t\tcode = start_async(&muxer);\n-\t\tif (code)\n-\t\t\treturn code;\n-\t\tproc.err = muxer.in;\n-\t}\n-\n-\tprepare_push_cert_sha1(&proc);\n-\n-\tcode = start_command(&proc);\n-\tif (code) {\n-\t\tif (use_sideband)\n-\t\t\tfinish_async(&muxer);\n-\t\treturn code;\n-\t}\n-\n-\tsigchain_push(SIGPIPE, SIG_IGN);\n-\n-\twhile (1) {\n-\t\tconst char *buf;\n-\t\tsize_t n;\n-\t\tif (feed(feed_state, &buf, &n))\n-\t\t\tbreak;\n-\t\tif (write_in_full(proc.in, buf, n) < 0)\n-\t\t\tbreak;\n-\t}\n-\tclose(proc.in);\n-\tif (use_sideband)\n-\t\tfinish_async(&muxer);\n-\n-\tsigchain_pop(SIGPIPE);\n-\n-\treturn finish_command(&proc);\n-}\n-\n-static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n+static int feed_receive_hook(void *state_)\n {\n \tstruct receive_hook_feed_state *state = state_;\n \tstruct command *cmd = state->cmd;\n@@ -883,9 +815,7 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t       state->skip_broken && (cmd->error_string || cmd->did_not_exist))\n \t\tcmd = cmd->next;\n \tif (!cmd)\n-\t\treturn -1; /* EOF */\n-\tif (!bufp)\n-\t\treturn 0; /* OK, can feed something. */\n+\t\treturn 1; /* EOF - close the pipe*/\n \tstrbuf_reset(&state->buf);\n \tif (!state->report)\n \t\tstate->report = cmd->report;\n@@ -909,32 +839,36 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)\n \t\t\t    cmd->ref_name);\n \t\tstate->cmd = cmd->next;\n \t}\n-\tif (bufp) {\n-\t\t*bufp = state->buf.buf;\n-\t\t*sizep = state->buf.len;\n-\t}\n \treturn 0;\n }\n \n-static int run_receive_hook(struct command *commands,\n-\t\t\t    const char *hook_name,\n-\t\t\t    int skip_broken,\n-\t\t\t    const struct string_list *push_options)\n+static int feed_receive_hook_cb(struct strbuf *pipe, void *pp_cb, void *pp_task_cb)\n {\n-\tstruct receive_hook_feed_state state;\n-\tint status;\n-\n-\tstrbuf_init(&state.buf, 0);\n-\tstate.cmd = commands;\n-\tstate.skip_broken = skip_broken;\n-\tstate.report = NULL;\n-\tif (feed_receive_hook(&state, NULL, NULL))\n-\t\treturn 0;\n-\tstate.cmd = commands;\n-\tstate.push_options = push_options;\n-\tstatus = run_and_feed_hook(hook_name, feed_receive_hook, &state);\n-\tstrbuf_release(&state.buf);\n-\treturn status;\n+\tstruct hook *hook = pp_task_cb;\n+\tstruct receive_hook_feed_state *feed_state = hook->feed_pipe_cb_data;\n+\tint rc;\n+\n+\t/* first-time setup */\n+\tif (!feed_state) {\n+\t\tstruct hook_cb_data *hook_cb = pp_cb;\n+\t\tstruct run_hooks_opt *opt = hook_cb->options;\n+\t\tstruct receive_hook_feed_context *ctx = opt->feed_pipe_ctx;\n+\t\tif (!ctx)\n+\t\t\tBUG(\"run_hooks_opt.feed_pipe_ctx required for receive hook\");\n+\n+\t\tfeed_state = xmalloc(sizeof(struct receive_hook_feed_state));\n+\t\tstrbuf_init(&feed_state->buf, 0);\n+\t\tfeed_state->cmd = ctx->cmd;\n+\t\tfeed_state->skip_broken = ctx->skip_broken;\n+\t\tfeed_state->report = NULL;\n+\n+\t\thook->feed_pipe_cb_data = feed_state;\n+\t}\n+\n+\trc = feed_receive_hook(feed_state);\n+\tif (!rc)\n+\t\tstrbuf_addbuf(pipe, &feed_state->buf);\n+\treturn rc;\n }\n \n static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n@@ -970,6 +904,48 @@ static void hook_output_to_sideband(struct strbuf *output, void *cb_data)\n \tsend_sideband(1, 2, output->buf, output->len, use_sideband);\n }\n \n+static int run_receive_hook(struct command *commands,\n+\t\t\t    const char *hook_name,\n+\t\t\t    int skip_broken,\n+\t\t\t    const struct string_list *push_options)\n+{\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct receive_hook_feed_context ctx;\n+\tstruct command *iter = commands;\n+\n+\t/* if there are no valid commands, don't invoke the hook at all. */\n+\twhile (iter && skip_broken && (iter->error_string || iter->did_not_exist))\n+\t\titer = iter->next;\n+\tif (!iter)\n+\t\treturn 0;\n+\n+\tif (push_options) {\n+\t\tint i;\n+\t\tfor (i = 0; i < push_options->nr; i++)\n+\t\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_%d=%s\", i,\n+\t\t\t\t     push_options->items[i].string);\n+\t\tstrvec_pushf(&opt.env, \"GIT_PUSH_OPTION_COUNT=%d\", push_options->nr);\n+\t} else\n+\t\tstrvec_push(&opt.env, \"GIT_PUSH_OPTION_COUNT\");\n+\n+\tif (tmp_objdir)\n+\t\tstrvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));\n+\n+\tprepare_push_cert_sha1(&opt);\n+\n+\t/* set up sideband printer */\n+\tif (use_sideband)\n+\t\topt.consume_sideband = hook_output_to_sideband;\n+\n+\t/* set up stdin callback */\n+\tctx.cmd = commands;\n+\tctx.skip_broken = skip_broken;\n+\topt.feed_pipe = feed_receive_hook_cb;\n+\topt.feed_pipe_ctx = &ctx;\n+\n+\treturn run_hooks_oneshot(hook_name, &opt);\n+}\n+\n static int run_update_hook(struct command *cmd)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434560","messageId":"patch-v5-36.36-fe056098534-20210902T125111Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com","subject":"[PATCH v5 36/36] hooks: fix a TOCTOU in \"did we run a hook?\" heuristic","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-02T13:11:36Z","receivedAt":"2021-09-02T13:13:37Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Fix a Time-of-check to time-of-use (TOCTOU) race in code added in\n680ee550d72 (commit: skip discarding the index if there is no\npre-commit hook, 2017-08-14).\n\nWe can fix the race passing around information about whether or not we\nran the hook in question, instead of running hook_exists() after the\nfact to check if the hook in question exists. This problem has been\nnoted on-list when 680ee550d72 was discussed[1], but had not been\nfixed.\n\nIn addition to fixing this for the pre-commit hook as suggested there\nI'm also fixing this for the pre-merge-commit hook. See\n6098817fd7f (git-merge: honor pre-merge-commit hook, 2019-08-07) for\nthe introduction of its previous behavior.\n\nLet's also change this for the push-to-checkout hook. Now instead of\nchecking if the hook exists and either doing a push to checkout or a\npush to deploy we'll always attempt a push to checkout. If the hook\ndoesn't exist we'll fall back on push to deploy. The same behavior as\nbefore, without the TOCTOU race. See 0855331941b (receive-pack:\nsupport push-to-checkout hook, 2014-12-01) for the introduction of the\nprevious behavior.\n\nThis leaves uses of hook_exists() in two places that matter. The\n\"reference-transaction\" check in refs.c, see 67541597670 (refs:\nimplement reference transaction hook, 2020-06-19), and the\nprepare-commit-msg hook, see 66618a50f9c (sequencer: run\n'prepare-commit-msg' hook, 2018-01-24).\n\nIn both of those cases we're saving ourselves CPU time by not\npreparing data for the hook that we'll then do nothing with if we\ndon't have the hook. So using this \"invoked_hook\" pattern doesn't make\nsense in those cases.\n\nMore importantly, in those cases the worst we'll do is miss that we\n\"should\" run the hook because a new hook appeared, whereas in the\npre-commit and pre-merge-commit cases we'll skip an important\ndiscard_cache() on the bases of our faulty guess.\n\nI do think none of these races really matter in practice. It would be\nsome one-off issue as a hook was added or removed. I did think it was\nstupid that we didn't pass a \"did this run?\" flag instead of doing\nthis guessing at a distance though, so now we're not guessing anymore.\n\n1. https://lore.kernel.org/git/20170810191613.kpmhzg4seyxy3cpq@sigill.intra.peff.net/\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/commit.c       | 18 +++++++++++-------\n builtin/merge.c        | 16 ++++++++++------\n builtin/receive-pack.c |  8 +++++---\n commit.c               |  1 +\n commit.h               |  3 ++-\n hook.c                 |  4 ++++\n hook.h                 | 10 ++++++++++\n sequencer.c            |  4 ++--\n 8 files changed, 45 insertions(+), 19 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex f6ca9d04c78..986146e3205 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -725,11 +725,13 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \tint clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);\n \tint old_display_comment_prefix;\n \tint merge_contains_scissors = 0;\n+\tint invoked_hook = 0;\n \n \t/* This checks and barfs if author is badly specified */\n \tdetermine_author_info(author_ident);\n \n-\tif (!no_verify && run_commit_hook(use_editor, index_file, \"pre-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(use_editor, index_file, &invoked_hook,\n+\t\t\t\t\t  \"pre-commit\", NULL))\n \t\treturn 0;\n \n \tif (squash_message) {\n@@ -1052,10 +1054,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (!no_verify && hook_exists(\"pre-commit\")) {\n+\tif (!no_verify && invoked_hook) {\n \t\t/*\n-\t\t * Re-read the index as pre-commit hook could have updated it,\n-\t\t * and write it out as a tree.  We must do this before we invoke\n+\t\t * Re-read the index as the pre-commit-commit hook was invoked\n+\t\t * and could have updated it. We must do this before we invoke\n \t\t * the editor and after we invoke run_status above.\n \t\t */\n \t\tdiscard_cache();\n@@ -1067,7 +1069,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\treturn 0;\n \t}\n \n-\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n+\tif (run_commit_hook(use_editor, index_file, NULL, \"prepare-commit-msg\",\n \t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n \t\treturn 0;\n \n@@ -1084,7 +1086,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t}\n \n \tif (!no_verify &&\n-\t    run_commit_hook(use_editor, index_file, \"commit-msg\", git_path_commit_editmsg(), NULL)) {\n+\t    run_commit_hook(use_editor, index_file, NULL, \"commit-msg\",\n+\t\t\t    git_path_commit_editmsg(), NULL)) {\n \t\treturn 0;\n \t}\n \n@@ -1843,7 +1846,8 @@ int cmd_commit(int argc, const char **argv, const char *prefix)\n \n \trepo_rerere(the_repository, 0);\n \trun_auto_maintenance(quiet);\n-\trun_commit_hook(use_editor, get_index_file(), \"post-commit\", NULL);\n+\trun_commit_hook(use_editor, get_index_file(), NULL, \"post-commit\",\n+\t\t\tNULL);\n \tif (amend && !no_post_rewrite) {\n \t\tcommit_post_rewrite(the_repository, current_head, &oid);\n \t}\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex ca9b3ba4827..f215f264cc8 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -844,15 +844,18 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n {\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *index_file = get_index_file();\n+\tint invoked_hook = 0;\n \n-\tif (!no_verify && run_commit_hook(0 < option_edit, index_file, \"pre-merge-commit\", NULL))\n+\tif (!no_verify && run_commit_hook(0 < option_edit, index_file,\n+\t\t\t\t\t  &invoked_hook, \"pre-merge-commit\",\n+\t\t\t\t\t  NULL))\n \t\tabort_commit(remoteheads, NULL);\n \t/*\n-\t * Re-read the index as pre-merge-commit hook could have updated it,\n-\t * and write it out as a tree.  We must do this before we invoke\n+\t * Re-read the index as the pre-merge-commit hook was invoked\n+\t * and could have updated it. We must do this before we invoke\n \t * the editor and after we invoke run_status above.\n \t */\n-\tif (hook_exists(\"pre-merge-commit\"))\n+\tif (invoked_hook)\n \t\tdiscard_cache();\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n@@ -875,7 +878,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t\tappend_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);\n \twrite_merge_heads(remoteheads);\n \twrite_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len);\n-\tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n+\tif (run_commit_hook(0 < option_edit, get_index_file(), NULL,\n+\t\t\t    \"prepare-commit-msg\",\n \t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n \t\tabort_commit(remoteheads, NULL);\n \tif (0 < option_edit) {\n@@ -884,7 +888,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t}\n \n \tif (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),\n-\t\t\t\t\t  \"commit-msg\",\n+\t\t\t\t\t  NULL, \"commit-msg\",\n \t\t\t\t\t  git_path_merge_msg(the_repository), NULL))\n \t\tabort_commit(remoteheads, NULL);\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex c3984680d7f..ebec6f3bb10 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -1428,10 +1428,12 @@ static const char *push_to_deploy(unsigned char *sha1,\n static const char *push_to_checkout_hook = \"push-to-checkout\";\n \n static const char *push_to_checkout(unsigned char *hash,\n+\t\t\t\t    int *invoked_hook,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\topt.invoked_hook = invoked_hook;\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n \tstrvec_pushv(&opt.env, env->v);\n@@ -1446,6 +1448,7 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n {\n \tconst char *retval, *work_tree, *git_dir = NULL;\n \tstruct strvec env = STRVEC_INIT;\n+\tint invoked_hook = 0;\n \n \tif (worktree && worktree->path)\n \t\twork_tree = worktree->path;\n@@ -1463,10 +1466,9 @@ static const char *update_worktree(unsigned char *sha1, const struct worktree *w\n \n \tstrvec_pushf(&env, \"GIT_DIR=%s\", absolute_path(git_dir));\n \n-\tif (!hook_exists(push_to_checkout_hook))\n+\tretval = push_to_checkout(sha1, &invoked_hook, &env, work_tree);\n+\tif (!invoked_hook)\n \t\tretval = push_to_deploy(sha1, &env, work_tree);\n-\telse\n-\t\tretval = push_to_checkout(sha1, &env, work_tree);\n \n \tstrvec_clear(&env);\n \treturn retval;\ndiff --git a/commit.c b/commit.c\nindex 63d7943a86d..842e47beae2 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1697,6 +1697,7 @@ size_t ignore_non_trailer(const char *buf, size_t len)\n }\n \n int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook,\n \t\t    const char *name, ...)\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\ndiff --git a/commit.h b/commit.h\nindex df42eb434f3..b5a542993c6 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -363,7 +363,8 @@ int compare_commits_by_commit_date(const void *a_, const void *b_, void *unused)\n int compare_commits_by_gen_then_commit_date(const void *a_, const void *b_, void *unused);\n \n LAST_ARG_MUST_BE_NULL\n-int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...);\n+int run_commit_hook(int editor_is_used, const char *index_file,\n+\t\t    int *invoked_hook, const char *name, ...);\n \n /* Sign a commit or tag buffer, storing the result in a header. */\n int sign_with_header(struct strbuf *buf, const char *keyid);\ndiff --git a/hook.c b/hook.c\nindex 7f93fe40dba..d045379ade8 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -144,6 +144,9 @@ static int notify_hook_finished(int result,\n \n \thook_cb->rc |= result;\n \n+\tif (hook_cb->invoked_hook)\n+\t\t*hook_cb->invoked_hook = 1;\n+\n \treturn 0;\n }\n \n@@ -158,6 +161,7 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t.rc = 0,\n \t\t.hook_name = hook_name,\n \t\t.options = options,\n+\t\t.invoked_hook = options->invoked_hook,\n \t};\n \tint jobs = 1;\n \ndiff --git a/hook.h b/hook.h\nindex a85e3e6981b..f6dac75f1cc 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -55,6 +55,15 @@ struct run_hooks_opt\n \t * for an example.\n \t */\n \tconsume_sideband_fn consume_sideband;\n+\n+\t/*\n+\t * A pointer which if provided will be set to 1 or 0 depending\n+\t * on if a hook was invoked (i.e. existed), regardless of\n+\t * whether or not that was successful. Used for avoiding\n+\t * TOCTOU races in code that would otherwise call hook_exist()\n+\t * after a \"maybe hook run\" to see if a hook was invoked.\n+\t */\n+\tint *invoked_hook;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -68,6 +77,7 @@ struct hook_cb_data {\n \tconst char *hook_name;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n+\tint *invoked_hook;\n };\n \n /**\ndiff --git a/sequencer.c b/sequencer.c\nindex 549b583b277..db8044ab47d 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1203,7 +1203,7 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n \t} else {\n \t\targ1 = \"message\";\n \t}\n-\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n+\tif (run_commit_hook(0, r->index_file, NULL, \"prepare-commit-msg\", name,\n \t\t\t    arg1, arg2, NULL))\n \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n \n@@ -1533,7 +1533,7 @@ static int try_to_commit(struct repository *r,\n \t\tgoto out;\n \t}\n \n-\trun_commit_hook(0, r->index_file, \"post-commit\", NULL);\n+\trun_commit_hook(0, r->index_file, NULL, \"post-commit\", NULL);\n \tif (flags & AMEND_MSG)\n \t\tcommit_post_rewrite(r, current_head, oid);\n \n-- \n2.33.0.816.g1ba32acadee\n\n"},{"id":"434601","messageId":"YTFAqvBlILJwwNyi@google.com","threadId":"55286","inReplyTo":"87fsuyeds6.fsf@evledraar.gmail.com","subject":"Re: [PATCH v3 0/6] config-based hooks restarted","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2021-09-02T22:01:56Z","receivedAt":"2021-09-02T22:02:05Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Tue, Aug 24, 2021 at 10:29:33PM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Wed, Aug 18 2021, Emily Shaffer wrote:\n> 0)\n> \n> I think this is in much much better shape vis-as-vis the simplified\n> config schema that's now being proposed re our discussion starting\n> around https://lore.kernel.org/git/87bl6ttmlv.fsf@evledraar.gmail.com/\n> \n> I.e. the main complexity of the \"skip\" mechanism is gone, and also the\n> conflation of hook names with hook commands (the \"rm -rf /\" as a\n> <name-or-cmd> discussed in the above).\n> \n> So before going any further, I'll just say that I wouldn't object much\n> to this design going in as-is. What I'm about to mention here below is\n> much closer to bikeshedding in my mind than \"this is really to complex\n> to go in-tree\", which was my opinion of the config schema before.\n> \n> 1)\n> \n> On the current config design: First, before going into any detail on\n> that, I think whatever anyone's opinion is on that that the\n> design-focused patches as they stand could really use more a more\n> extended discussion of the design.\n> \n> I.e. talk about the previously considered config schema, why it evolved\n> into its current form. The trade-offs involved, and why the patch\n> proposed to implement the schema it's implementing over another earlier\n> or alternate design.\n> \n> I.e. https://lore.kernel.org/git/20210819033450.3382652-6-emilyshaffer@google.com\n> is two very short paragraphs. We won't be able to summarize all our\n> month-long discussion on the config design in one commit message, but I\n> think at least discussing it somewhat / linking to relevant on-list\n> discussions would make future source spelunking easier.\n\nHum. I don't think that it's necessary to summarize the whole discussion\nin the commit message, but I think it's worth it to describe the\nrationale (\"we like this design because it makes x good practice easy\nand y bug hard\") and link out to the discussion for parties who are\ninterested in reading more.\n\n> \n> 2)\n> \n> So that out of the way, a comment on the current config design, which\n> should be read in the context of what I noted in #0. I.e. I'm *much*\n> happier with this version.\n> \n> That being said I'm still not convinced that the simple 1=1 mapping of\n> \"hook.<name>.command\" and its \"value\" should be followed by the 1=many\n> mapping of \"hook.<name>.event\" and its \"value\".\n> \n> I.e. we're making the trade-off of saving the user from typing out or\n> specifying:\n>     \n>     [hook \"my-pre-commit\"]\n>     command = ~/hooks/pre-commit-or-push\n>     event = pre-commit\n>     [hook \"my-pre-push\"]\n>     command = ~/hooks/pre-commit-or-push\n>     event = pre-push\n> \n> And instead being able to do:\n> \n>     [hook \"my-pre-commit-or-push\"]\n>     command = ~/hooks/pre-commit-or-push\n>     event = pre-commit\n>     event = pre-push\n> \n> So for the very common case, saving two config lines. \"Two\" because as\n> we discussed[1] as there's currently no GIT_HOOK_TYPE env var. So this\n> form will work pretty much only for that case.\n> \n> I.e. unlike with .git/hook/<name> the hook run via config can't\n> determine what <hook-type> it's being run at, so as it stands this is\n> only useful for those hooks listed in githooks(5) where someone would\n> want to do the exact same thing for one or more <hook-name> names. You\n> can't use it as a general routing mechanism for any hook type as it\n> stands.\n> \n> I *think* that's only these two, perhaps \"update\" and \"pre-receive\",\n> with the hook seeing if it consume stdin/has arguments to disambiguate\n> the two.\n> \n> But even with a GIT_HOOK_TYPE passed the trade-off, as discussed in [1],\n> and downthread in [2], is that by having it 1=many we're closing the\n> door on any future hook.<name>.<whatever>. I.e. config that would change\n> the behavior of that hook, but you'd want to change it in another way\n> for at least one of the N event types.\n\nI'm not really sure that's the case, to be honest. Even with the config\nscheme as is in this iteration, you can still define it the way you're\ndescribing with no problem, and in fact it makes it easier for users to\napply some special config to all-but-one invocation.\n\nLet's say for example the \"git-secrets\" hook, which we do have defined\nfor at least 3 different events at Google today, and a hypothetical\n\"parallelize me\" config; for the sake of argument let's presume that\nthis is the far future and we've added a GIT_HOOK_TYPE envvar:\n\n[hook \"git-secrets\"]\n  command = /bin/git-secrets\n  event = pre-commit\n  event = pre-merge-commit\n  parallelizable = true\n\n[hook \"git-secrets-mutexed\"]\n  command = /bin/git-secrets\n  event = prepare-commit-msg\n  event = commit-msg\n  parallelizable = false\n\nIf we don't allow \"hook.myhookname.event\" to be multiply configurable,\nthen the user gets this really tedious task of defining every single\n\"hook.git-secrets-$hookevent.parallelizable\" config.\n\n> \n> Well, \"closing the door\" as in if you'd want that you'd have to split up\n> the section from the \"my-pre-commit-or-push\" example above to the\n> \"my-pre-commit\" and \"my-pre-push\" example.\n> \n> But again, on the \"is the complexity worth it\" we're then having to\n> explain to users that they can do it one way if the want no config other\n> than hook.<name>.{command,event*}, but another if they have another key\n> in that namespace.\n\nI am not so worried about \"this will be hard to explain, so we should\nnot do it\" - I think we can make the documentation useful with enough\neffort and expertise. (And yes, I'm feeling optimistic because I have an\nactual technical writer taking a look at the manpage right now.)\n\n> \n> You've said that you wanted to add something like a GIT_HOOK_TYPE\n> environment variable. Fair enough, and I guess we could add it in a\n> re-roll of this series. I'm mainly commenting on the end-state of *this*\n> series in particular. I.e. I think it leaves the user & implementation\n> with a config schema that still seems to be needlessly complex for the\n> very limited benefits that complexity brings us in what you're able to\n> do with it now.\n\nI think that limiting ourselves in the way you're describing will make\nit more difficult to bring additional benefits later. It is certainly\npossible and valid to write your configuration the way you are\ndescribing, without changing this schema.\n\n> \n> But some of that goes back to the comments I had on 5/6[3], i.e. I'm\n> willing to be convinced, but I think that the current commit message &\n> added docs aren't really selling the idea of why it's worth it.\n\nOk. I think your point is not \"the schema is still wrong\" as much as it\nis \"the documentation could be much better\", and I agree.\n\n> \n> 3) \n> \n> As an extension to my comments on 5/6[3], I think this whole notion of\n> \"git hook run <name>\" as invoked by a user of git is just more confusing\n> the more I think about it.\n> \n> I.e. 5/6[4] is apparently seeking to implement a way to just make that\n> facility a general way for users to run some command on their system to\n> do whatever, instead of say using /usr/bin/parallel or a shell alias.\n> \n> But then we also use that command as our own dispatch mechanism for our\n> own known hooks, except mostly not, since we mostly use the C API\n> ourselves directly.\n> \n> It's particularly confusing that if you say run \"git hook run\n> pre-auto-gc\" as a user to test your hook you'll have that hook run in\n> the same way that git-gc(1) would run it. So someone developing a hook\n> might think they can use \"git hook run\" for testing it.\n> \n> But if you do the same with say \"git hook run pre-receive\" or anything\n> else that feeds arguments or stdin (e.g. \"update\", or \"pre-receive\"),\n> you'll have your hook happily being run by git, but in a way that's not\n> at all how such a hook will be run when it's run by git \"for real\".\n> \n> So I wonder if we shouldn't just have the thing die() if you try to run\n> any hook that's in githooks(5) itself, except for sendemail-validate and\n> the p4 hooks, since we need to run those ourselves.\n\nI think this again falls to a documentation issue. I would love to have\na tighter loop when developing a hook that takes weird mid-merge\narguments or whatever; making it easier to test the hook in isolation\nwith known inputs sounds like a good thing to me.\n\nPersonally, \"I ran 'git hook run pre-receive' without being in the\nmiddle of a receive operation and it didn't behave like it was in the\nmiddle of a receive operation!\" doesn't sound all that surprising to me.\n\n> \n> Or have those use an internal-only \"git hook--helper\", and start out\n> with \"git hook\" just supporting \"git hook list\", and then later on have\n> \"git hook run\" (or perhaps \"git hook run-configured\"?) be an entry point\n> for this facility of running some arbitrary script that's not a \"real\"\n> hook.\n> \n> I don't know, maybe I'm the only one that finds this confusing...\n> \n> 1. https://lore.kernel.org/git/87bl6ttmlv.fsf@evledraar.gmail.com/\n> 2. https://lore.kernel.org/git/877dh0n1b3.fsf@evledraar.gmail.com/\n> 3. https://lore.kernel.org/git/87lf4qeh86.fsf@evledraar.gmail.com/\n> 4. https://lore.kernel.org/git/20210819033450.3382652-6-emilyshaffer@google.com/\n\nBesides \"make the docs more obvious\", I don't think there is anything in\nthis mail that I want to act on. I am very happy with the config schema\nas it is, as well as the behavior of 'git hook run'.\n\n\nAs I mentioned here and somewhere else in the review, I am in the\nprocess of getting feedback on the manpage from a tech writer this week,\nso do not expect a reroll from me until at least next week. I saw your\nreroll today and I'll try and look at it (or at least the interdiff)\ntomorrow or Monday.\n\n - Emily\n"},{"id":"435254","messageId":"cover-v4-0.5-00000000000-20210909T122802Z-avarab@gmail.com","threadId":"55286","inReplyTo":"20210819033450.3382652-1-emilyshaffer@google.com","subject":"[PATCH v4 0/5] config-based hooks restarted","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-09T12:41:58Z","receivedAt":"2021-09-09T14:18:53Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"This is a re-roll of Emily's es/config-based-hooks topic that goes on\ntop of the v5 of my ab/config-based-hooks-base, and which doesn't have\nerrors under SANITIZE=leak.\n\nEmily: Sorry, I've got no intention to steal this one too, hopefully\nyou can get around to your own re-roll.\n\nBut per my\nhttps://lore.kernel.org/git/87sfyfgtfh.fsf@evledraar.gmail.com/ the\nlack of this re-roll is is currently blocking the pick-up of my\nre-rolled v5 of ab/config-based-hooks-base at [1], as well as causing\na failure in \"seen\" when combined with my ab/sanitize-leak-ci (and\nhn/reftable, but that's another issue...).\n\nJunio: So hopefully you can pick up the v5[1] of the base topic now &\nthis preliminary v4 of es/config-based-hooks.\n\nThe range-diff below is against Emily's 30ffe98601e, i.e. her v3 at\n[2].\n\nThis version is based on Emily's preliminary cf1f8e34a34\n(nasamuffin/config-based-hooks-restart), which appeared to be her\nAugust 31 rebasing addressing of many outstanding points in the v3\nseries.\n\nMy own changes on top of that were twofold: Adjustments to changes in\nthe base topic (many done to make the overall diff/changes here\nsmaller), and memory leak fixes to get this to pass under\nSANITIZE=leak, there's various other minor but not-notable changes\nhere and there, see the range-diff.\n\n1. https://lore.kernel.org/git/cover-v5-00.36-00000000000-20210902T125110Z-avarab@gmail.com/\n2. https://lore.kernel.org/git/20210819033450.3382652-1-emilyshaffer@google.com/\n\nEmily Shaffer (5):\n  hook: run a list of hooks instead\n  hook: allow parallel hook execution\n  hook: introduce \"git hook list\"\n  hook: include hooks from the config\n  hook: allow out-of-repo 'git hook' invocations\n\n Documentation/config.txt      |   2 +\n Documentation/config/hook.txt |  22 +++\n Documentation/git-hook.txt    | 157 ++++++++++++++++++-\n builtin/am.c                  |   4 +-\n builtin/checkout.c            |   2 +-\n builtin/clone.c               |   2 +-\n builtin/hook.c                |  71 ++++++++-\n builtin/merge.c               |   2 +-\n builtin/rebase.c              |   2 +-\n builtin/receive-pack.c        |   9 +-\n builtin/worktree.c            |   2 +-\n commit.c                      |   2 +-\n git.c                         |   2 +-\n hook.c                        | 277 +++++++++++++++++++++++++++++-----\n hook.h                        |  45 +++++-\n read-cache.c                  |   2 +-\n refs.c                        |   2 +-\n reset.c                       |   3 +-\n sequencer.c                   |   4 +-\n t/t1800-hook.sh               | 194 +++++++++++++++++++++++-\n transport.c                   |   2 +-\n 21 files changed, 734 insertions(+), 74 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n\nRange-diff against v3:\n1:  6d6400329cd ! 1:  2f0cac14965 hook: run a list of hooks instead\n    @@ Commit message\n         executable for a single hook event.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## builtin/hook.c ##\n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n      \tconst char *hook_name;\n     -\tconst char *hook_path;\n     +\tstruct list_head *hooks;\n    -+\n      \tstruct option run_options[] = {\n      \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n      \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n     @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n    - \tgit_config(git_default_config, NULL);\n    - \n    + \t * run_hooks() instead...\n    + \t */\n      \thook_name = argv[0];\n     -\tif (ignore_missing)\n    ++\thooks = list_hooks(hook_name);\n    ++\tif (list_empty(hooks)) {\n    ++\t\tclear_hook_list(hooks);\n    ++\n    + \t\t/* ... act like a plain run_hooks() under --ignore-missing */\n     -\t\treturn run_hooks_oneshot(hook_name, &opt);\n     -\thook_path = find_hook(hook_name);\n     -\tif (!hook_path) {\n    -+\thooks = list_hooks(hook_name);\n    -+\tif (list_empty(hooks)) {\n    -+\t\t/* ... act like run_hooks_oneshot() under --ignore-missing */\n     +\t\tif (ignore_missing)\n     +\t\t\treturn 0;\n      \t\terror(\"cannot find a hook named %s\", hook_name);\n    @@ builtin/hook.c: static int run(int argc, const char **argv, const char *prefix)\n     \n      ## hook.c ##\n     @@\n    - #include \"hook-list.h\"\n    + #include \"run-command.h\"\n      #include \"config.h\"\n      \n     +static void free_hook(struct hook *ptr)\n     +{\n    -+\tif (ptr)\n    -+\t\tfree(ptr->feed_pipe_cb_data);\n    ++\tif (!ptr)\n    ++\t\treturn;\n    ++\n    ++\tfree(ptr->feed_pipe_cb_data);\n     +\tfree(ptr);\n     +}\n     +\n    @@ hook.c\n     +\tstruct list_head *pos, *tmp;\n     +\tlist_for_each_safe(pos, tmp, head)\n     +\t\tremove_hook(pos);\n    ++\tfree(head);\n     +}\n     +\n    - static int known_hook(const char *name)\n    + const char *find_hook(const char *name)\n      {\n    - \tconst char **p;\n    + \tstatic struct strbuf path = STRBUF_INIT;\n     @@ hook.c: const char *find_hook(const char *name)\n      \n      int hook_exists(const char *name)\n      {\n     -\treturn !!find_hook(name);\n    -+\treturn !list_empty(list_hooks(name));\n    ++\tstruct list_head *hooks;\n    ++\tint exists;\n    ++\n    ++\thooks = list_hooks(name);\n    ++\texists = !list_empty(hooks);\n    ++\tclear_hook_list(hooks);\n    ++\n    ++\treturn exists;\n     +}\n     +\n     +struct list_head *list_hooks(const char *hookname)\n    @@ hook.c: static int notify_hook_finished(int result,\n      }\n      \n     -int run_hooks(const char *hook_name, const char *hook_path,\n    --\t      struct run_hooks_opt *options)\n     +int run_hooks(const char *hook_name, struct list_head *hooks,\n    -+\t\t    struct run_hooks_opt *options)\n    + \t      struct run_hooks_opt *options)\n      {\n     -\tstruct strbuf abs_path = STRBUF_INIT;\n     -\tstruct hook my_hook = {\n    @@ hook.c: int run_hooks(const char *hook_name, const char *hook_path,\n     -\t\tmy_hook.hook_path = abs_path.buf;\n     -\t}\n     -\tcb_data.run_me = &my_hook;\n    -+\n     +\tcb_data.head = hooks;\n     +\tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n      \n    @@ hook.c: int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *optio\n      \n     -\tret = run_hooks(hook_name, hook_path, options);\n     +\tret = run_hooks(hook_name, hooks, options);\n    -+\n    + \n      cleanup:\n      \trun_hooks_opt_clear(options);\n    -+\tclear_hook_list(hooks);\n    - \treturn ret;\n    - }\n     \n      ## hook.h ##\n     @@\n    @@ hook.h\n      #include \"run-command.h\"\n     +#include \"list.h\"\n      \n    - /*\n    -  * Returns the path to the hook file, or NULL if the hook is missing\n    -@@ hook.h: const char *find_hook(const char *name);\n    - int hook_exists(const char *hookname);\n    - \n      struct hook {\n     +\tstruct list_head list;\n      \t/* The path to the hook */\n      \tconst char *hook_path;\n      \n    -@@ hook.h: struct hook {\n    - \tvoid *feed_pipe_cb_data;\n    - };\n    - \n    -+/*\n    -+ * Provides a linked list of 'struct hook' detailing commands which should run\n    -+ * in response to the 'hookname' event, in execution order.\n    -+ */\n    -+struct list_head *list_hooks(const char *hookname);\n    -+\n    - struct run_hooks_opt\n    - {\n    - \t/* Environment vars to be set for each hook */\n     @@ hook.h: struct hook_cb_data {\n      \t/* rc reflects the cumulative failure state */\n      \tint rc;\n    @@ hook.h: struct hook_cb_data {\n      \tstruct hook *run_me;\n      \tstruct run_hooks_opt *options;\n      \tint *invoked_hook;\n    +@@ hook.h: struct hook_cb_data {\n    + const char *find_hook(const char *name);\n    + \n    + /**\n    +- * A boolean version of find_hook()\n    ++ * Provides a linked list of 'struct hook' detailing commands which should run\n    ++ * in response to the 'hookname' event, in execution order.\n    ++ */\n    ++struct list_head *list_hooks(const char *hookname);\n    ++\n    ++/**\n    ++ * A boolean version of list_hooks()\n    +  */\n    + int hook_exists(const char *hookname);\n    + \n     @@ hook.h: void run_hooks_opt_clear(struct run_hooks_opt *o);\n    + \n    + /**\n    +  * Takes an already resolved hook found via find_hook() and runs\n    +- * it. Does not call run_hooks_opt_clear() for you.\n    ++ * it. Does not call run_hooks_opt_clear() for you, but does call\n    ++ * clear_hook_list().\n       *\n       * See run_hooks_oneshot() for the simpler one-shot API.\n       */\n     -int run_hooks(const char *hookname, const char *hook_path,\n    --\t      struct run_hooks_opt *options);\n     +int run_hooks(const char *hookname, struct list_head *hooks,\n    -+\t\t    struct run_hooks_opt *options);\n    - \n    - /**\n    -  * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n    -@@ hook.h: int run_hooks(const char *hookname, const char *hook_path,\n    -  */\n    - int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options);\n    + \t      struct run_hooks_opt *options);\n      \n    -+/* Empties the list at 'head', calling 'free_hook()' on each entry */\n    ++/**\n    ++ * Empties the list at 'head', calling 'free_hook()' on each\n    ++ * entry. Called implicitly by run_hooks() (and run_hooks_oneshot()).\n    ++ */\n     +void clear_hook_list(struct list_head *head);\n     +\n    - #endif\n    + /**\n    +  * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n    +  * with run_hooks().\n2:  dfb995ce4d4 ! 2:  b03e70c805e hook: allow parallel hook execution\n    @@ Commit message\n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n         Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n    + ## Documentation/config.txt ##\n    +@@ Documentation/config.txt: include::config/guitool.txt[]\n    + \n    + include::config/help.txt[]\n    + \n    ++include::config/hook.txt[]\n    ++\n    + include::config/http.txt[]\n    + \n    + include::config/i18n.txt[]\n    +\n      ## Documentation/config/hook.txt (new) ##\n     @@\n     +hook.jobs::\n    @@ Documentation/git-hook.txt: OPTIONS\n     +--jobs::\n     +\tOnly valid for `run`.\n     ++\n    -+Specify how many hooks to run simultaneously. If this flag is not specified, use\n    -+the value of the `hook.jobs` config. If the config is not specified, use the\n    -+number of CPUs on the current system. Some hooks may be ineligible for\n    -+parallelization: for example, 'commit-msg' intends hooks modify the commit\n    -+message body and cannot be parallelized.\n    ++Specify how many hooks to run simultaneously. If this flag is not specified,\n    ++uses the value of the `hook.jobs` config, see linkgit:git-config[1]. If the\n    ++config is not specified, uses the number of CPUs on the current system. Some\n    ++hooks may be ineligible for parallelization: for example, 'commit-msg' intends\n    ++hooks modify the commit message body and cannot be parallelized.\n     +\n     +CONFIGURATION\n     +-------------\n    @@ hook.c: static int notify_hook_finished(int result,\n     +}\n     +\n      int run_hooks(const char *hook_name, struct list_head *hooks,\n    - \t\t    struct run_hooks_opt *options)\n    + \t      struct run_hooks_opt *options)\n      {\n     @@ hook.c: int run_hooks(const char *hook_name, struct list_head *hooks,\n      \t\t.options = options,\n    @@ hook.c: int run_hooks(const char *hook_name, struct list_head *hooks,\n      \n      \tif (!options)\n      \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n    - \n    --\n    +@@ hook.c: int run_hooks(const char *hook_name, struct list_head *hooks,\n      \tcb_data.head = hooks;\n      \tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n      \n    @@ hook.h: struct run_hooks_opt\n     +\t */\n     +\tint jobs;\n     +\n    - \t/* Resolve and run the \"absolute_path(hook)\" instead of\n    + \t/*\n    + \t * Resolve and run the \"absolute_path(hook)\" instead of\n      \t * \"hook\". Used for \"git worktree\" hooks\n    - \t */\n     @@ hook.h: struct run_hooks_opt\n      \tint *invoked_hook;\n      };\n      \n     -#define RUN_HOOKS_OPT_INIT { \\\n    --\t.env = STRVEC_INIT, \\\n    --\t.args = STRVEC_INIT, \\\n    --}\n    --\n    - /*\n    -  * To specify a 'struct string_list', set 'run_hooks_opt.feed_pipe_ctx' to the\n    -  * string_list and set 'run_hooks_opt.feed_pipe' to 'pipe_from_string_list()'.\n    -@@ hook.h: struct hook_cb_data {\n    - \tint *invoked_hook;\n    - };\n    - \n     +#define RUN_HOOKS_OPT_INIT_SERIAL { \\\n     +\t.jobs = 1, \\\n     +\t.env = STRVEC_INIT, \\\n    @@ hook.h: struct hook_cb_data {\n     +\n     +#define RUN_HOOKS_OPT_INIT_PARALLEL { \\\n     +\t.jobs = 0, \\\n    -+\t.env = STRVEC_INIT, \\\n    -+\t.args = STRVEC_INIT, \\\n    -+}\n    -+\n    - void run_hooks_opt_clear(struct run_hooks_opt *o);\n    - \n    - /**\n    + \t.env = STRVEC_INIT, \\\n    + \t.args = STRVEC_INIT, \\\n    + }\n     \n      ## read-cache.c ##\n     @@ read-cache.c: static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n3:  c8a04306e90 ! 3:  3e647b8dba7 hook: introduce \"git hook list\"\n    @@ Commit message\n         hooks were configured and whether or not they will run.\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n    +    Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n     \n      ## Documentation/git-hook.txt ##\n     @@ Documentation/git-hook.txt: SYNOPSIS\n    @@ builtin/hook.c: static const char * const builtin_hook_run_usage[] = {\n     +\n     +static int list(int argc, const char **argv, const char *prefix)\n     +{\n    -+\tstruct list_head *head, *pos;\n    ++\tstruct list_head *hooks;\n    ++\tstruct list_head *pos;\n     +\tconst char *hookname = NULL;\n    -+\tstruct strbuf hookdir_annotation = STRBUF_INIT;\n    -+\n     +\tstruct option list_options[] = {\n     +\t\tOPT_END(),\n     +\t};\n    ++\tint ret = 0;\n     +\n     +\targc = parse_options(argc, argv, prefix, list_options,\n     +\t\t\t     builtin_hook_list_usage, 0);\n     +\n    -+\tif (argc < 1)\n    ++\t/*\n    ++\t * The only unnamed argument provided should be the hook-name; if we add\n    ++\t * arguments later they probably should be caught by parse_options.\n    ++\t */\n    ++\tif (argc != 1)\n     +\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n     +\t\t\t      builtin_hook_list_usage, list_options);\n     +\n     +\thookname = argv[0];\n     +\n    -+\thead = hook_list(hookname);\n    ++\thooks = list_hooks(hookname);\n     +\n    -+\tif (list_empty(head))\n    -+\t\treturn 1;\n    ++\tif (list_empty(hooks)) {\n    ++\t\tret = 1;\n    ++\t\tgoto cleanup;\n    ++\t}\n     +\n    -+\tlist_for_each(pos, head) {\n    ++\tlist_for_each(pos, hooks) {\n     +\t\tstruct hook *item = list_entry(pos, struct hook, list);\n     +\t\titem = list_entry(pos, struct hook, list);\n     +\t\tif (item)\n     +\t\t\tprintf(\"%s\\n\", item->hook_path);\n     +\t}\n     +\n    -+\tclear_hook_list(head);\n    -+\tstrbuf_release(&hookdir_annotation);\n    ++cleanup:\n    ++\tclear_hook_list(hooks);\n     +\n    -+\treturn 0;\n    ++\treturn ret;\n     +}\n      static int run(int argc, const char **argv, const char *prefix)\n      {\n    @@ builtin/hook.c: int cmd_hook(int argc, const char **argv, const char *prefix)\n      \t\treturn run(argc, argv, prefix);\n      \n     \n    - ## hook.c ##\n    -@@ hook.c: struct list_head *list_hooks(const char *hookname)\n    - {\n    - \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n    + ## t/t1800-hook.sh ##\n    +@@ t/t1800-hook.sh: test_expect_success 'git hook usage' '\n    + \ttest_expect_code 129 git hook run &&\n    + \ttest_expect_code 129 git hook run -h &&\n    + \ttest_expect_code 129 git hook run --unknown 2>err &&\n    ++\ttest_expect_code 129 git hook list &&\n    ++\ttest_expect_code 129 git hook list -h &&\n    + \tgrep \"unknown option\" err\n    + '\n      \n    -+\n    - \tINIT_LIST_HEAD(hook_head);\n    +@@ t/t1800-hook.sh: test_expect_success 'git hook run -- pass arguments' '\n    + \ttest_cmp expect actual\n    + '\n      \n    - \tif (!hookname)\n    -@@ hook.c: struct list_head *list_hooks(const char *hookname)\n    - \n    - \tif (have_git_dir()) {\n    - \t\tconst char *hook_path = find_hook(hookname);\n    --\n    --\t\t/* Add the hook from the hookdir */\n    - \t\tif (hook_path) {\n    - \t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n    - \t\t\tto_add->hook_path = hook_path;\n    ++test_expect_success 'git hook list: does-not-exist hook' '\n    ++\ttest_expect_code 1 git hook list does-not-exist\n    ++'\n    ++\n    ++test_expect_success 'git hook list: existing hook' '\n    ++\tcat >expect <<-\\EOF &&\n    ++\t.git/hooks/test-hook\n    ++\tEOF\n    ++\tgit hook list test-hook >actual &&\n    ++\ttest_cmp expect actual\n    ++'\n    ++\n    + test_expect_success 'git hook run -- out-of-repo runs excluded' '\n    + \twrite_script .git/hooks/test-hook <<-EOF &&\n    + \techo Test hook\n4:  af14116d0fa < -:  ----------- hook: allow running non-native hooks\n5:  2bbb179962e ! 4:  d0f5b30fb27 hook: include hooks from the config\n    @@ Metadata\n      ## Commit message ##\n         hook: include hooks from the config\n     \n    -    Teach the hook.[hc] library to parse configs to populare the list of\n    +    Teach the hook.[hc] library to parse configs to populate the list of\n         hooks to run for a given event.\n     \n         Multiple commands can be specified for a given hook by providing\n         multiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n    -    \"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be run in\n    -    config order of the \"hook.<name>.event\" lines.\n    +    \"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be started\n    +    in config order of the \"hook.<name>.event\" lines (but may run in\n    +    parallel).\n     \n         For example:\n     \n    -      $ git config --list | grep ^hook\n    +      $ git config --get-regexp \"^hook\\.\"\n           hook.bar.command=~/bar.sh\n           hook.bar.event=pre-commit\n     \n    -      $ git hook run\n    -      # Runs ~/bar.sh\n    -      # Runs .git/hooks/pre-commit\n    +      # Will run ~/bar.sh, then .git/hooks/pre-commit\n    +      $ git hook run pre-commit\n     \n         Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n     \n    @@ Documentation/config/hook.txt\n      \thook execution. If unspecified, defaults to the number of processors on\n     \n      ## Documentation/git-hook.txt ##\n    -@@ Documentation/git-hook.txt: Git is unlikely to use for a native hook later on. For example, Git is much less\n    - likely to create a `mytool-validate-commit` hook than it is to create a\n    - `validate-commit` hook.\n    +@@ Documentation/git-hook.txt: This command is an interface to git hooks (see linkgit:githooks[5]).\n    + Currently it only provides a convenience wrapper for running hooks for\n    + use by git itself. In the future it might gain other functionality.\n      \n    -+This command parses the default configuration files for pairs of configs like\n    ++It's possible to use this command to refer to hooks which are not native to Git,\n    ++for example if a wrapper around Git wishes to expose hooks into its own\n    ++operation in a way which is already familiar to Git users. However, wrappers\n    ++invoking such hooks should be careful to name their hook events something which\n    ++Git is unlikely to use for a native hook later on. For example, Git is much less\n    ++likely to create a `mytool-validate-commit` hook than it is to create a\n    ++`validate-commit` hook.\n    ++\n    ++This command parses the default configuration files for sets of configs like\n     +so:\n     +\n     +  [hook \"linter\"]\n     +    event = pre-commit\n    -+    command = ~/bin/linter --c\n    ++    command = ~/bin/linter --cpp20\n     +\n    -+In this example, `[hook \"linter\"]` represents one script - `~/bin/linter --c` -\n    -+which can be shared by many repos, and even by many hook events, if appropriate.\n    ++In this example, `[hook \"linter\"]` represents one script - `~/bin/linter\n    ++--cpp20` - which can be shared by many repos, and even by many hook events, if\n    ++appropriate.\n    ++\n    ++To add an unrelated hook which runs on a different event, for example a\n    ++spell-checker for your commit messages, you would write a configuration like so:\n    ++\n    ++  [hook \"linter\"]\n    ++    event = pre-commit\n    ++    command = ~/bin/linter --cpp20\n    ++  [hook \"spellcheck\"]\n    ++    event = commit-msg\n    ++    command = ~/bin/spellchecker\n    ++\n    ++With this config, when you run 'git commit', first `~/bin/linter --cpp20` will\n    ++have a chance to check your files to be committed (during the `pre-commit` hook\n    ++event`), and then `~/bin/spellchecker` will have a chance to check your commit\n    ++message (during the `commit-msg` hook event).\n     +\n     +Commands are run in the order Git encounters their associated\n     +`hook.<name>.event` configs during the configuration parse (see\n    @@ Documentation/git-hook.txt: Git is unlikely to use for a native hook later on. F\n     +  [hook \"linter\"]\n     +    event = pre-commit\n     +    event = pre-push\n    -+    command = ~/bin/linter --c\n    ++    command = ~/bin/linter --cpp20\n     +\n    -+With this config, `~/bin/linter --c` would be run by Git before a commit is\n    ++With this config, `~/bin/linter --cpp20` would be run by Git before a commit is\n     +generated (during `pre-commit`) as well as before a push is performed (during\n     +`pre-push`).\n     +\n    @@ Documentation/git-hook.txt: Git is unlikely to use for a native hook later on. F\n     +\n     +  [hook \"linter\"]\n     +    event = pre-commit\n    -+    command = ~/bin/linter --c\n    ++    command = ~/bin/linter --cpp20\n     +  [hook \"no-leaks\"]\n     +    event = pre-commit\n     +    command = ~/bin/leak-detector\n     +\n     +With this config, before a commit is generated (during `pre-commit`), Git would\n    -+first start `~/bin/linter --c` and second start `~/bin/leak-detector`. It would\n    -+evaluate the output of each when deciding whether to proceed with the commit.\n    ++first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It\n    ++would evaluate the output of each when deciding whether to proceed with the\n    ++commit.\n     +\n     +For a full list of hook events which you can set your `hook.<name>.event` to,\n     +and how hooks are invoked during those events, see linkgit:githooks[5].\n     +\n    ++Git will ignore any `hook.<name>.event` that specifies an event it doesn't\n    ++recognize. This is intended so that tools which wrap Git can use the hook\n    ++infrastructure to run their own hooks; see <<WRAPPERS>> for more guidance.\n    ++\n     +In general, when instructions suggest adding a script to\n    -+`.git/hooks/<hook-event>`, you can specify it in the config instead by running\n    -+`git config --add hook.<some-name>.command <path-to-script> && git config --add\n    -+hook.<some-name>.event <hook-event>` - this way you can share the script between\n    -+multiple repos. That is, `cp ~/my-script.sh ~/project/.git/hooks/pre-commit`\n    -+would become `git config --add hook.my-script.command ~/my-script.sh && git\n    -+config --add hook.my-script.event pre-commit`.\n    ++`.git/hooks/<hook-event>`, you can specify it in the config instead by running:\n    ++\n    ++----\n    ++git config hook.<some-name>.command <path-to-script>\n    ++git config --add hook.<some-name>.event <hook-event>\n    ++----\n    ++\n    ++This way you can share the script between multiple repos. That is, `cp\n    ++~/my-script.sh ~/project/.git/hooks/pre-commit` would become:\n    ++\n    ++----\n    ++git config hook.my-script.command ~/my-script.sh\n    ++git config --add hook.my-script.event pre-commit\n    ++----\n     +\n      SUBCOMMANDS\n      -----------\n    @@ Documentation/git-hook.txt: Git is unlikely to use for a native hook later on. F\n      +\n      Any positional arguments to the hook should be passed after an\n      optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n    +@@ Documentation/git-hook.txt: config is not specified, uses the number of CPUs on the current system. Some\n    + hooks may be ineligible for parallelization: for example, 'commit-msg' intends\n    + hooks modify the commit message body and cannot be parallelized.\n    + \n    ++[[WRAPPERS]]\n    ++WRAPPERS\n    ++--------\n    ++\n    ++`git hook run` has been designed to make it easy for tools which wrap Git to\n    ++configure and execute hooks using the Git hook infrastructure. It is possible to\n    ++provide arguments, environment variables (TODO this is missing from reroll TODO),\n    ++and stdin via the command line, as well as specifying parallel or series\n    ++execution if the user has provided multiple hooks.\n    ++\n    ++Assuming your wrapper wants to support a hook named \"mywrapper-start-tests\", you\n    ++can have your users specify their hooks like so:\n    ++\n    ++  [hook \"setup-test-dashboard\"]\n    ++    event = mywrapper-start-tests\n    ++    command = ~/mywrapper/setup-dashboard.py --tap\n    ++\n    ++Then, in your 'mywrapper' tool, you can invoke any users' configured hooks by\n    ++running:\n    ++\n    ++----\n    ++git hook run mywrapper-start-tests \\\n    ++  # providing something to stdin\n    ++  --stdin some-tempfile-123 \\\n    ++  # setting an env var (TODO THIS IS MISSING TODO)\n    ++  --env MYWRAPPER_EXECUTION_MODE=foo \\\n    ++  # execute hooks in serial\n    ++  --jobs 1 \\\n    ++  # plus some arguments of your own...\n    ++  -- \\\n    ++  --testname bar \\\n    ++  baz\n    ++----\n    ++\n    ++Take care to name your wrapper's hook events in a way which is unlikely to\n    ++overlap with Git's native hooks (see linkgit:githooks[5]) - a hook event named\n    ++`mywrappertool-validate-commit` is much less likely to be added to native Git\n    ++than a hook event named `validate-commit`. If Git begins to use a hook event\n    ++named the same thing as your wrapper hook, it may invoke your users' hooks in\n    ++unintended and unsupported ways.\n    ++\n    + CONFIGURATION\n    + -------------\n    + include::config/hook.txt[]\n     \n      ## builtin/hook.c ##\n     @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix)\n     +\t\t\t\t\t\t  : _(\"hook from hookdir\"));\n      \t}\n      \n    - \tclear_hook_list(head);\n    + cleanup:\n     \n      ## hook.c ##\n     @@ hook.c: static void free_hook(struct hook *ptr)\n    + \tif (!ptr)\n    + \t\treturn;\n    + \n    ++\tfree(ptr->name);\n    + \tfree(ptr->feed_pipe_cb_data);\n      \tfree(ptr);\n      }\n      \n    @@ hook.c: static void free_hook(struct hook *ptr)\n     +\n     +\tif (!to_add) {\n     +\t\t/* adding a new hook, not moving an old one */\n    -+\t\tto_add = xmalloc(sizeof(*to_add));\n    -+\t\tto_add->name = name;\n    -+\t\tto_add->feed_pipe_cb_data = NULL;\n    ++\t\tto_add = xcalloc(1, sizeof(*to_add));\n    ++\t\tto_add->name = xstrdup_or_null(name);\n     +\t}\n     +\n     +\tlist_add_tail(&to_add->list, head);\n    @@ hook.c: static void free_hook(struct hook *ptr)\n      {\n      \tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n     @@ hook.c: int hook_exists(const char *name)\n    + \treturn exists;\n    + }\n      \n    - struct hook_config_cb\n    - {\n    --\tstruct strbuf *hook_key;\n    ++struct hook_config_cb\n    ++{\n     +\tconst char *hook_event;\n    - \tstruct list_head *list;\n    - };\n    - \n    ++\tstruct list_head *list;\n    ++};\n    ++\n     +/*\n     + * Callback for git_config which adds configured hooks to a hook list.  Hooks\n     + * can be configured by specifying both hook.<friend-name>.command = <path> and\n    @@ hook.c: int hook_exists(const char *name)\n     +\t */\n     +\tstrbuf_add(&subsection_cpy, subsection, subsection_len);\n     +\n    -+\tappend_or_move_hook(data->list, strbuf_detach(&subsection_cpy, NULL));\n    -+\n    ++\tappend_or_move_hook(data->list, subsection_cpy.buf);\n    ++\tstrbuf_release(&subsection_cpy);\n     +\n     +\treturn 0;\n     +}\n     +\n      struct list_head *list_hooks(const char *hookname)\n    - {\n    - \tif (!known_hook(hookname))\n    -@@ hook.c: struct list_head *list_hooks(const char *hookname)\n    - struct list_head *list_hooks_gently(const char *hookname)\n      {\n      \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n     +\tstruct hook_config_cb cb_data = {\n    @@ hook.c: struct list_head *list_hooks(const char *hookname)\n      \t\tBUG(\"null hookname was provided to hook_list()!\");\n      \n     -\tif (have_git_dir()) {\n    --\t\tconst char *hook_path = find_hook_gently(hookname);\n    +-\t\tconst char *hook_path = find_hook(hookname);\n    ++\t/* Add the hooks from the config, e.g. hook.myhook.event = pre-commit */\n    ++\tgit_config(hook_config_lookup, &cb_data);\n    + \n    +-\t\t/* Add the hook from the hookdir */\n     -\t\tif (hook_path) {\n     -\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n     -\t\t\tto_add->hook_path = hook_path;\n    @@ hook.c: struct list_head *list_hooks(const char *hookname)\n     -\t\t\tlist_add_tail(&to_add->list, hook_head);\n     -\t\t}\n     -\t}\n    -+\t/* Add the hooks from the config, e.g. hook.myhook.event = pre-commit */\n    -+\tgit_config(hook_config_lookup, &cb_data);\n    -+\n     +\t/* Add the hook from the hookdir. The placeholder makes it easier to\n     +\t * allocate work in pick_next_hook. */\n    -+\tif (find_hook_gently(hookname))\n    ++\tif (find_hook(hookname))\n     +\t\tappend_or_move_hook(hook_head, NULL);\n      \n      \treturn hook_head;\n    @@ hook.c: static int pick_next_hook(struct child_process *cp,\n      \tcp->trace2_hook_name = hook_cb->hook_name;\n      \tcp->dir = hook_cb->options->dir;\n      \n    -+\t/* to enable oneliners, let config-specified hooks run in shell.\n    -+\t * config-specified hooks have a name. */\n    ++\t/*\n    ++\t * to enable oneliners, let config-specified hooks run in shell.\n    ++\t * config-specified hooks have a name.\n    ++\t */\n     +\tcp->use_shell = !!run_me->name;\n     +\n      \t/* add command */\n    @@ hook.c: static int pick_next_hook(struct child_process *cp,\n     +\t\t}\n     +\n     +\t\tstrvec_push(&cp->args, command);\n    ++\t\tfree(command);\n    ++\t\tstrbuf_release(&cmd_key);\n     +\t} else {\n     +\t\t/* ...from hookdir. */\n     +\t\tconst char *hook_path = NULL;\n     +\t\t/*\n    -+\t\t *\n     +\t\t * At this point we are already running, so don't validate\n    -+\t\t * whether the hook name is known or not.\n    ++\t\t * whether the hook name is known or not. Validation was\n    ++\t\t * performed earlier in list_hooks().\n     +\t\t */\n    -+\t\thook_path = find_hook_gently(hook_cb->hook_name);\n    ++\t\thook_path = find_hook(hook_cb->hook_name);\n     +\t\tif (!hook_path)\n     +\t\t\tBUG(\"hookdir hook in hook list but no hookdir hook present in filesystem\");\n     +\n    @@ hook.c: static int notify_start_failure(struct strbuf *out,\n      }\n     \n      ## hook.h ##\n    -@@ hook.h: int hook_exists(const char *hookname);\n    +@@\n      \n      struct hook {\n      \tstruct list_head list;\n    @@ hook.h: int hook_exists(const char *hookname);\n     +\t * The friendly name of the hook. NULL indicates the hook is from the\n     +\t * hookdir.\n     +\t */\n    -+\tconst char *name;\n    ++\tchar *name;\n      \n      \t/*\n      \t * Use this to keep state for your feed_pipe_fn if you are using\n     \n      ## t/t1800-hook.sh ##\n     @@\n    - #!/bin/bash\n    + #!/bin/sh\n      \n     -test_description='git-hook command'\n     +test_description='git-hook command and config-managed multihooks'\n    @@ t/t1800-hook.sh\n      . ./test-lib.sh\n      \n     +setup_hooks () {\n    ++\ttest_config hook.ghi.command \"/path/ghi\"\n     +\ttest_config hook.ghi.event pre-commit --add\n    -+\ttest_config hook.ghi.command \"/path/ghi\" --add\n    ++\ttest_config hook.ghi.event test-hook --add\n    ++\ttest_config_global hook.def.command \"/path/def\"\n     +\ttest_config_global hook.def.event pre-commit --add\n    -+\ttest_config_global hook.def.command \"/path/def\" --add\n     +}\n     +\n     +setup_hookdir () {\n    @@ t/t1800-hook.sh\n      \ttest_expect_code 129 git hook run -h &&\n     +\ttest_expect_code 129 git hook list -h &&\n      \ttest_expect_code 129 git hook run --unknown 2>err &&\n    - \tgrep \"unknown option\" err\n    - '\n    + \ttest_expect_code 129 git hook list &&\n    + \ttest_expect_code 129 git hook list -h &&\n    +@@ t/t1800-hook.sh: test_expect_success 'git hook list: does-not-exist hook' '\n    + \n    + test_expect_success 'git hook list: existing hook' '\n    + \tcat >expect <<-\\EOF &&\n    +-\t.git/hooks/test-hook\n    ++\thook from hookdir\n    + \tEOF\n    + \tgit hook list test-hook >actual &&\n    + \ttest_cmp expect actual\n     @@ t/t1800-hook.sh: test_expect_success 'stdin to hooks' '\n      \ttest_cmp expect actual\n      '\n    @@ t/t1800-hook.sh: test_expect_success 'stdin to hooks' '\n     +test_expect_success 'git hook list orders by config order' '\n     +\tsetup_hooks &&\n     +\n    -+\tcat >expected <<-EOF &&\n    ++\tcat >expected <<-\\EOF &&\n     +\tdef\n     +\tghi\n     +\tEOF\n    @@ t/t1800-hook.sh: test_expect_success 'stdin to hooks' '\n     +\t# configuring it locally.\n     +\ttest_config hook.def.event \"pre-commit\" --add &&\n     +\n    -+\tcat >expected <<-EOF &&\n    ++\tcat >expected <<-\\EOF &&\n     +\tghi\n     +\tdef\n     +\tEOF\n    @@ t/t1800-hook.sh: test_expect_success 'stdin to hooks' '\n     +\ttest_cmp expected actual\n     +'\n     +\n    ++test_expect_success 'hook can be configured for multiple events' '\n    ++\tsetup_hooks &&\n    ++\n    ++\t# 'ghi' should be included in both 'pre-commit' and 'test-hook'\n    ++\tgit hook list pre-commit >actual &&\n    ++\tgrep \"ghi\" actual &&\n    ++\tgit hook list test-hook >actual &&\n    ++\tgrep \"ghi\" actual\n    ++'\n    ++\n     +test_expect_success 'git hook list shows hooks from the hookdir' '\n     +\tsetup_hookdir &&\n     +\n    -+\tcat >expected <<-EOF &&\n    ++\tcat >expected <<-\\EOF &&\n     +\thook from hookdir\n     +\tEOF\n     +\n    @@ t/t1800-hook.sh: test_expect_success 'stdin to hooks' '\n     +'\n     +\n     +test_expect_success 'inline hook definitions resolve paths' '\n    -+\twrite_script sample-hook.sh <<-EOF &&\n    ++\twrite_script sample-hook.sh <<-\\EOF &&\n     +\techo \\\"Sample Hook\\\"\n     +\tEOF\n     +\n    @@ t/t1800-hook.sh: test_expect_success 'stdin to hooks' '\n     +\ttest_config hook.stdin-b.event \"test-hook\" --add &&\n     +\ttest_config hook.stdin-b.command \"xargs -P1 -I% echo b%\" --add &&\n     +\n    -+\tcat >input <<-EOF &&\n    ++\tcat >input <<-\\EOF &&\n     +\t1\n     +\t2\n     +\t3\n     +\tEOF\n     +\n    -+\tcat >expected <<-EOF &&\n    ++\tcat >expected <<-\\EOF &&\n     +\ta1\n     +\ta2\n     +\ta3\n    @@ t/t1800-hook.sh: test_expect_success 'stdin to hooks' '\n     +\techo 3\n     +\tEOF\n     +\n    -+\tcat >expected <<-EOF &&\n    ++\tcat >expected <<-\\EOF &&\n     +\t1\n     +\t2\n     +\t3\n    @@ t/t1800-hook.sh: test_expect_success 'stdin to hooks' '\n     +\n     +\trm -rf .git/hooks\n     +'\n    ++\n    ++test_expect_success 'rejects hooks with no commands configured' '\n    ++\ttest_config hook.broken.event \"test-hook\" &&\n    ++\n    ++\techo broken >expected &&\n    ++\tgit hook list test-hook >actual &&\n    ++\ttest_cmp expected actual &&\n    ++\ttest_must_fail git hook run test-hook\n    ++'\n    ++\n      test_done\n6:  30ffe98601e ! 5:  5d5e9726fd8 hook: allow out-of-repo 'git hook' invocations\n    @@ git.c: static struct cmd_struct commands[] = {\n      \t{ \"init-db\", cmd_init_db },\n     \n      ## hook.c ##\n    -@@ hook.c: struct list_head *list_hooks_gently(const char *hookname)\n    +@@ hook.c: struct list_head *list_hooks(const char *hookname)\n      \n      \t/* Add the hook from the hookdir. The placeholder makes it easier to\n      \t * allocate work in pick_next_hook. */\n    --\tif (find_hook_gently(hookname))\n    -+\tif (have_git_dir() && find_hook_gently(hookname))\n    +-\tif (find_hook(hookname))\n    ++\tif (have_git_dir() && find_hook(hookname))\n      \t\tappend_or_move_hook(hook_head, NULL);\n      \n      \treturn hook_head;\n     \n      ## t/t1800-hook.sh ##\n    -@@ t/t1800-hook.sh: test_expect_success 'git hook run -- pass arguments' '\n    +@@ t/t1800-hook.sh: test_expect_success 'git hook list: existing hook' '\n      \ttest_cmp expect actual\n      '\n      \n-- \n2.33.0.867.g88ec4638586\n\n"},{"id":"435253","messageId":"patch-v4-1.5-2f0cac14965-20210909T122802Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-0.5-00000000000-20210909T122802Z-avarab@gmail.com","subject":"[PATCH v4 1/5] hook: run a list of hooks instead","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-09T12:41:59Z","receivedAt":"2021-09-09T14:18:54Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTo prepare for multihook support, teach hook.[hc] to take a list of\nhooks at run_hooks and run_found_hooks. Right now the list is always one\nentry, but in the future we will allow users to supply more than one\nexecutable for a single hook event.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n builtin/hook.c |  14 ++++---\n hook.c         | 109 ++++++++++++++++++++++++++++++++++++-------------\n hook.h         |  22 ++++++++--\n 3 files changed, 107 insertions(+), 38 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex fae69068201..398980523aa 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -25,7 +25,7 @@ static int run(int argc, const char **argv, const char *prefix)\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint ignore_missing = 0;\n \tconst char *hook_name;\n-\tconst char *hook_path;\n+\tstruct list_head *hooks;\n \tstruct option run_options[] = {\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n@@ -63,16 +63,18 @@ static int run(int argc, const char **argv, const char *prefix)\n \t * run_hooks() instead...\n \t */\n \thook_name = argv[0];\n-\tif (ignore_missing)\n+\thooks = list_hooks(hook_name);\n+\tif (list_empty(hooks)) {\n+\t\tclear_hook_list(hooks);\n+\n \t\t/* ... act like a plain run_hooks() under --ignore-missing */\n-\t\treturn run_hooks_oneshot(hook_name, &opt);\n-\thook_path = find_hook(hook_name);\n-\tif (!hook_path) {\n+\t\tif (ignore_missing)\n+\t\t\treturn 0;\n \t\terror(\"cannot find a hook named %s\", hook_name);\n \t\treturn 1;\n \t}\n \n-\tret = run_hooks(hook_name, hook_path, &opt);\n+\tret = run_hooks(hook_name, hooks, &opt);\n \trun_hooks_opt_clear(&opt);\n \treturn ret;\n usage:\ndiff --git a/hook.c b/hook.c\nindex d045379ade8..2b2c16a9095 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -3,6 +3,30 @@\n #include \"run-command.h\"\n #include \"config.h\"\n \n+static void free_hook(struct hook *ptr)\n+{\n+\tif (!ptr)\n+\t\treturn;\n+\n+\tfree(ptr->feed_pipe_cb_data);\n+\tfree(ptr);\n+}\n+\n+static void remove_hook(struct list_head *to_remove)\n+{\n+\tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n+\tlist_del(to_remove);\n+\tfree_hook(hook_to_remove);\n+}\n+\n+void clear_hook_list(struct list_head *head)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, head)\n+\t\tremove_hook(pos);\n+\tfree(head);\n+}\n+\n const char *find_hook(const char *name)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n@@ -39,7 +63,38 @@ const char *find_hook(const char *name)\n \n int hook_exists(const char *name)\n {\n-\treturn !!find_hook(name);\n+\tstruct list_head *hooks;\n+\tint exists;\n+\n+\thooks = list_hooks(name);\n+\texists = !list_empty(hooks);\n+\tclear_hook_list(hooks);\n+\n+\treturn exists;\n+}\n+\n+struct list_head *list_hooks(const char *hookname)\n+{\n+\tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\n+\tINIT_LIST_HEAD(hook_head);\n+\n+\tif (!hookname)\n+\t\tBUG(\"null hookname was provided to hook_list()!\");\n+\n+\tif (have_git_dir()) {\n+\t\tconst char *hook_path = find_hook(hookname);\n+\n+\t\t/* Add the hook from the hookdir */\n+\t\tif (hook_path) {\n+\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n+\t\t\tto_add->hook_path = hook_path;\n+\t\t\tto_add->feed_pipe_cb_data = NULL;\n+\t\t\tlist_add_tail(&to_add->list, hook_head);\n+\t\t}\n+\t}\n+\n+\treturn hook_head;\n }\n \n void run_hooks_opt_clear(struct run_hooks_opt *o)\n@@ -99,7 +154,10 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->dir = hook_cb->options->dir;\n \n \t/* add command */\n-\tstrvec_push(&cp->args, run_me->hook_path);\n+\tif (hook_cb->options->absolute_path)\n+\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n+\telse\n+\t\tstrvec_push(&cp->args, run_me->hook_path);\n \n \t/*\n \t * add passed-in argv, without expanding - let the user get back\n@@ -110,12 +168,12 @@ static int pick_next_hook(struct child_process *cp,\n \t/* Provide context for errors if necessary */\n \t*pp_task_cb = run_me;\n \n-\t/*\n-\t * This pick_next_hook() will be called again, we're only\n-\t * running one hook, so indicate that no more work will be\n-\t * done.\n-\t */\n-\thook_cb->run_me = NULL;\n+\t/* Get the next entry ready */\n+\tif (hook_cb->run_me->list.next == hook_cb->head)\n+\t\thook_cb->run_me = NULL;\n+\telse\n+\t\thook_cb->run_me = list_entry(hook_cb->run_me->list.next,\n+\t\t\t\t\t     struct hook, list);\n \n \treturn 1;\n }\n@@ -150,13 +208,9 @@ static int notify_hook_finished(int result,\n \treturn 0;\n }\n \n-int run_hooks(const char *hook_name, const char *hook_path,\n+int run_hooks(const char *hook_name, struct list_head *hooks,\n \t      struct run_hooks_opt *options)\n {\n-\tstruct strbuf abs_path = STRBUF_INIT;\n-\tstruct hook my_hook = {\n-\t\t.hook_path = hook_path,\n-\t};\n \tstruct hook_cb_data cb_data = {\n \t\t.rc = 0,\n \t\t.hook_name = hook_name,\n@@ -168,11 +222,8 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n \n-\tif (options->absolute_path) {\n-\t\tstrbuf_add_absolute_path(&abs_path, hook_path);\n-\t\tmy_hook.hook_path = abs_path.buf;\n-\t}\n-\tcb_data.run_me = &my_hook;\n+\tcb_data.head = hooks;\n+\tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n \n \trun_processes_parallel_tr2(jobs,\n \t\t\t\t   pick_next_hook,\n@@ -184,18 +235,15 @@ int run_hooks(const char *hook_name, const char *hook_path,\n \t\t\t\t   \"hook\",\n \t\t\t\t   hook_name);\n \n-\n-\tif (options->absolute_path)\n-\t\tstrbuf_release(&abs_path);\n-\tfree(my_hook.feed_pipe_cb_data);\n+\tclear_hook_list(hooks);\n \n \treturn cb_data.rc;\n }\n \n int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n {\n-\tconst char *hook_path;\n-\tint ret;\n+\tstruct list_head *hooks;\n+\tint ret = 0;\n \tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n \n \tif (!options)\n@@ -204,13 +252,16 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n \tif (options->path_to_stdin && options->feed_pipe)\n \t\tBUG(\"choose only one method to populate stdin\");\n \n-\thook_path = find_hook(hook_name);\n-\tif (!hook_path) {\n-\t\tret = 0;\n+\thooks = list_hooks(hook_name);\n+\n+\t/*\n+\t * If you need to act on a missing hook, use run_found_hooks()\n+\t * instead\n+\t */\n+\tif (list_empty(hooks))\n \t\tgoto cleanup;\n-\t}\n \n-\tret = run_hooks(hook_name, hook_path, options);\n+\tret = run_hooks(hook_name, hooks, options);\n \n cleanup:\n \trun_hooks_opt_clear(options);\ndiff --git a/hook.h b/hook.h\nindex f6dac75f1cc..49b4c335f86 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -3,8 +3,10 @@\n #include \"strbuf.h\"\n #include \"strvec.h\"\n #include \"run-command.h\"\n+#include \"list.h\"\n \n struct hook {\n+\tstruct list_head list;\n \t/* The path to the hook */\n \tconst char *hook_path;\n \n@@ -75,6 +77,7 @@ struct hook_cb_data {\n \t/* rc reflects the cumulative failure state */\n \tint rc;\n \tconst char *hook_name;\n+\tstruct list_head *head;\n \tstruct hook *run_me;\n \tstruct run_hooks_opt *options;\n \tint *invoked_hook;\n@@ -88,7 +91,13 @@ struct hook_cb_data {\n const char *find_hook(const char *name);\n \n /**\n- * A boolean version of find_hook()\n+ * Provides a linked list of 'struct hook' detailing commands which should run\n+ * in response to the 'hookname' event, in execution order.\n+ */\n+struct list_head *list_hooks(const char *hookname);\n+\n+/**\n+ * A boolean version of list_hooks()\n  */\n int hook_exists(const char *hookname);\n \n@@ -99,13 +108,20 @@ void run_hooks_opt_clear(struct run_hooks_opt *o);\n \n /**\n  * Takes an already resolved hook found via find_hook() and runs\n- * it. Does not call run_hooks_opt_clear() for you.\n+ * it. Does not call run_hooks_opt_clear() for you, but does call\n+ * clear_hook_list().\n  *\n  * See run_hooks_oneshot() for the simpler one-shot API.\n  */\n-int run_hooks(const char *hookname, const char *hook_path,\n+int run_hooks(const char *hookname, struct list_head *hooks,\n \t      struct run_hooks_opt *options);\n \n+/**\n+ * Empties the list at 'head', calling 'free_hook()' on each\n+ * entry. Called implicitly by run_hooks() (and run_hooks_oneshot()).\n+ */\n+void clear_hook_list(struct list_head *head);\n+\n /**\n  * Calls find_hook() on your \"hook_name\" and runs the hooks (if any)\n  * with run_hooks().\n-- \n2.33.0.867.g88ec4638586\n\n"},{"id":"435255","messageId":"patch-v4-2.5-b03e70c805e-20210909T122802Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-0.5-00000000000-20210909T122802Z-avarab@gmail.com","subject":"[PATCH v4 2/5] hook: allow parallel hook execution","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-09T12:42:00Z","receivedAt":"2021-09-09T14:19:05Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIn many cases, there's no reason not to allow hooks to execute in\nparallel, if more than one was provided. hook.c already calls\nrun_processes_parallel(), so all we need to do is allow the job count we\nhand to run_processes_parallel() to be greater than 1.\n\nIf users have specified no alternative, we can use the processor count\nfrom online_cpus() to run an efficient number of tasks at once. However,\nusers can also override this number if they want by configuring\n'hook.jobs'.\n\nTo avoid looking up 'hook.jobs' in cases where we don't end up with any\nhooks to run anyways, teach the hook runner commands to notice if\n.jobs==0 and do a config or online_cpus() lookup if so, when we already\nknow we have jobs to run.\n\nSerial execution can still be performed by setting .jobs == 1.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nHelped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/config.txt      |  2 ++\n Documentation/config/hook.txt |  4 ++++\n Documentation/git-hook.txt    | 17 ++++++++++++++++-\n builtin/am.c                  |  4 ++--\n builtin/checkout.c            |  2 +-\n builtin/clone.c               |  2 +-\n builtin/hook.c                |  4 +++-\n builtin/merge.c               |  2 +-\n builtin/rebase.c              |  2 +-\n builtin/receive-pack.c        |  9 +++++----\n builtin/worktree.c            |  2 +-\n commit.c                      |  2 +-\n hook.c                        | 35 ++++++++++++++++++++++++++++++++---\n hook.h                        | 16 +++++++++++++++-\n read-cache.c                  |  2 +-\n refs.c                        |  2 +-\n reset.c                       |  3 ++-\n sequencer.c                   |  4 ++--\n transport.c                   |  2 +-\n 19 files changed, 92 insertions(+), 24 deletions(-)\n create mode 100644 Documentation/config/hook.txt\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 0c0e6b859f1..6fb218f649d 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -391,6 +391,8 @@ include::config/guitool.txt[]\n \n include::config/help.txt[]\n \n+include::config/hook.txt[]\n+\n include::config/http.txt[]\n \n include::config/i18n.txt[]\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nnew file mode 100644\nindex 00000000000..96d3d6572c1\n--- /dev/null\n+++ b/Documentation/config/hook.txt\n@@ -0,0 +1,4 @@\n+hook.jobs::\n+\tSpecifies how many hooks can be run simultaneously during parallelized\n+\thook execution. If unspecified, defaults to the number of processors on\n+\tthe current system.\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex fa68c1f3912..79e82479ec6 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,8 @@ git-hook - run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--to-stdin=<path>] [--ignore-missing] <hook-name> [-- <hook-args>]\n+'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n+\t<hook-name> [-- <hook-args>]\n \n DESCRIPTION\n -----------\n@@ -42,6 +43,20 @@ OPTIONS\n \ttools that want to do a blind one-shot run of a hook that may\n \tor may not be present.\n \n+-j::\n+--jobs::\n+\tOnly valid for `run`.\n++\n+Specify how many hooks to run simultaneously. If this flag is not specified,\n+uses the value of the `hook.jobs` config, see linkgit:git-config[1]. If the\n+config is not specified, uses the number of CPUs on the current system. Some\n+hooks may be ineligible for parallelization: for example, 'commit-msg' intends\n+hooks modify the commit message body and cannot be parallelized.\n+\n+CONFIGURATION\n+-------------\n+include::config/hook.txt[]\n+\n SEE ALSO\n --------\n linkgit:githooks[5]\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 9e3d4d9ab44..c7ffc7eafc5 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -446,7 +446,7 @@ static void am_destroy(const struct am_state *state)\n static int run_applypatch_msg_hook(struct am_state *state)\n {\n \tint ret;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \n \tassert(state->msg);\n \tstrvec_push(&opt.args, am_path(state, \"final-commit\"));\n@@ -467,7 +467,7 @@ static int run_applypatch_msg_hook(struct am_state *state)\n  */\n static int run_post_rewrite_hook(const struct am_state *state)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tstrvec_push(&opt.args, \"rebase\");\n \topt.path_to_stdin = am_path(state, \"rewritten\");\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 863b02a7d7c..6b99d31c6ba 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -107,7 +107,7 @@ struct branch_info {\n static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,\n \t\t\t      int changed)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \n \t/* \"new_commit\" can be NULL when checking out from the index before\n \t   a commit exists. */\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 27fc05ee511..986c3b1932a 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -776,7 +776,7 @@ static int checkout(int submodule_progress)\n \tstruct tree *tree;\n \tstruct tree_desc t;\n \tint err = 0;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \n \tif (option_no_checkout)\n \t\treturn 0;\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 398980523aa..9b6272cfd3b 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -22,7 +22,7 @@ static const char * const builtin_hook_run_usage[] = {\n static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \tint ignore_missing = 0;\n \tconst char *hook_name;\n \tstruct list_head *hooks;\n@@ -31,6 +31,8 @@ static int run(int argc, const char **argv, const char *prefix)\n \t\t\t N_(\"exit quietly with a zero exit code if the requested hook cannot be found\")),\n \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n \t\t\t   N_(\"file to read into hooks' stdin\")),\n+\t\tOPT_INTEGER('j', \"jobs\", &opt.jobs,\n+\t\t\t    N_(\"run up to <n> hooks simultaneously\")),\n \t\tOPT_END(),\n \t};\n \tint ret;\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex f215f264cc8..c01eb535d6b 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -448,7 +448,7 @@ static void finish(struct commit *head_commit,\n \t\t   const struct object_id *new_head, const char *msg)\n {\n \tstruct strbuf reflog_message = STRBUF_INIT;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tconst struct object_id *head = &head_commit->object.oid;\n \n \tif (!msg)\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex ee68a1df492..a4bbb9abb35 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1314,7 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)\n \tchar *squash_onto_name = NULL;\n \tint reschedule_failed_exec = -1;\n \tint allow_preemptive_ff = 1;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct option builtin_rebase_options[] = {\n \t\tOPT_STRING(0, \"onto\", &options.onto_name,\n \t\t\t   N_(\"revision\"),\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex ebec6f3bb10..7460124b743 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -909,7 +909,7 @@ static int run_receive_hook(struct command *commands,\n \t\t\t    int skip_broken,\n \t\t\t    const struct string_list *push_options)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct receive_hook_feed_context ctx;\n \tstruct command *iter = commands;\n \n@@ -948,7 +948,7 @@ static int run_receive_hook(struct command *commands,\n \n static int run_update_hook(struct command *cmd)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tstrvec_pushl(&opt.args,\n \t\t     cmd->ref_name,\n@@ -1432,7 +1432,8 @@ static const char *push_to_checkout(unsigned char *hash,\n \t\t\t\t    struct strvec *env,\n \t\t\t\t    const char *work_tree)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n+\n \topt.invoked_hook = invoked_hook;\n \n \tstrvec_pushf(env, \"GIT_WORK_TREE=%s\", absolute_path(work_tree));\n@@ -1628,7 +1629,7 @@ static const char *update(struct command *cmd, struct shallow_info *si)\n static void run_update_post_hook(struct command *commands)\n {\n \tstruct command *cmd;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tfor (cmd = commands; cmd; cmd = cmd->next) {\n \t\tif (cmd->error_string || cmd->did_not_exist)\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 330867c19bf..30905067906 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -382,7 +382,7 @@ static int add_worktree(const char *path, const char *refname,\n \t * is_junk is cleared, but do return appropriate code when hook fails.\n \t */\n \tif (!ret && opts->checkout) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \n \t\tstrvec_pushl(&opt.env, \"GIT_DIR\", \"GIT_WORK_TREE\", NULL);\n \t\tstrvec_pushl(&opt.args,\ndiff --git a/commit.c b/commit.c\nindex 842e47beae2..a38bd047524 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1700,7 +1700,7 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n \t\t    int *invoked_hook,\n \t\t    const char *name, ...)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL;\n \tva_list args;\n \tconst char *arg;\n \ndiff --git a/hook.c b/hook.c\nindex 2b2c16a9095..600030c59ec 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -208,6 +208,28 @@ static int notify_hook_finished(int result,\n \treturn 0;\n }\n \n+/*\n+ * Determines how many jobs to use after we know we want to parallelize. First\n+ * priority is the config 'hook.jobs' and second priority is the number of CPUs.\n+ */\n+static int configured_hook_jobs(void)\n+{\n+\t/*\n+\t * The config and the CPU count probably won't change during the process\n+\t * lifetime, so cache the result in case we invoke multiple hooks during\n+\t * one process.\n+\t */\n+\tstatic int jobs = 0;\n+\tif (jobs)\n+\t\treturn jobs;\n+\n+\tif (git_config_get_int(\"hook.jobs\", &jobs))\n+\t\t/* if the config isn't set, fall back to CPU count. */\n+\t\tjobs = online_cpus();\n+\n+\treturn jobs;\n+}\n+\n int run_hooks(const char *hook_name, struct list_head *hooks,\n \t      struct run_hooks_opt *options)\n {\n@@ -217,7 +239,6 @@ int run_hooks(const char *hook_name, struct list_head *hooks,\n \t\t.options = options,\n \t\t.invoked_hook = options->invoked_hook,\n \t};\n-\tint jobs = 1;\n \n \tif (!options)\n \t\tBUG(\"a struct run_hooks_opt must be provided to run_hooks\");\n@@ -225,7 +246,11 @@ int run_hooks(const char *hook_name, struct list_head *hooks,\n \tcb_data.head = hooks;\n \tcb_data.run_me = list_first_entry(hooks, struct hook, list);\n \n-\trun_processes_parallel_tr2(jobs,\n+\t/* INIT_PARALLEL sets jobs to 0, so go look up how many to use. */\n+\tif (!options->jobs)\n+\t\toptions->jobs = configured_hook_jobs();\n+\n+\trun_processes_parallel_tr2(options->jobs,\n \t\t\t\t   pick_next_hook,\n \t\t\t\t   notify_start_failure,\n \t\t\t\t   options->feed_pipe,\n@@ -244,7 +269,11 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n {\n \tstruct list_head *hooks;\n \tint ret = 0;\n-\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT;\n+\t/*\n+\t * Turn on parallelism by default. Hooks which don't want it should\n+\t * specify their options accordingly.\n+\t */\n+\tstruct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tif (!options)\n \t\toptions = &hook_opt_scratch;\ndiff --git a/hook.h b/hook.h\nindex 49b4c335f86..fe16ab35028 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -25,6 +25,13 @@ struct run_hooks_opt\n \t/* Args to be passed to each hook */\n \tstruct strvec args;\n \n+\t/*\n+\t * Number of threads to parallelize across. Set to 0 to use the\n+\t * 'hook.jobs' config or, if that config is unset, the number of cores\n+\t * on the system.\n+\t */\n+\tint jobs;\n+\n \t/*\n \t * Resolve and run the \"absolute_path(hook)\" instead of\n \t * \"hook\". Used for \"git worktree\" hooks\n@@ -68,7 +75,14 @@ struct run_hooks_opt\n \tint *invoked_hook;\n };\n \n-#define RUN_HOOKS_OPT_INIT { \\\n+#define RUN_HOOKS_OPT_INIT_SERIAL { \\\n+\t.jobs = 1, \\\n+\t.env = STRVEC_INIT, \\\n+\t.args = STRVEC_INIT, \\\n+}\n+\n+#define RUN_HOOKS_OPT_INIT_PARALLEL { \\\n+\t.jobs = 0, \\\n \t.env = STRVEC_INIT, \\\n \t.args = STRVEC_INIT, \\\n }\ndiff --git a/read-cache.c b/read-cache.c\nindex 875f6c1dea5..98e9fb8b04e 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3069,7 +3069,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l\n {\n \tint ret;\n \tint was_full = !istate->sparse_index;\n-\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \tret = convert_to_sparse(istate);\n \ndiff --git a/refs.c b/refs.c\nindex 73d4a939267..5543b8cdaba 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2062,7 +2062,7 @@ int ref_update_reject_duplicates(struct string_list *refnames,\n static int run_transaction_hook(struct ref_transaction *transaction,\n \t\t\t\tconst char *state)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \tint ret = 0, i;\n \ndiff --git a/reset.c b/reset.c\nindex 1237ced8a58..63a9c513409 100644\n--- a/reset.c\n+++ b/reset.c\n@@ -127,7 +127,8 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action,\n \t\t\t\t\t    reflog_head);\n \t}\n \tif (run_hook) {\n-\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\t\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n+\n \t\tstrvec_pushl(&opt.args,\n \t\t\t     oid_to_hex(orig ? orig : null_oid()),\n \t\t\t     oid_to_hex(oid),\ndiff --git a/sequencer.c b/sequencer.c\nindex db8044ab47d..979cd01c303 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1148,7 +1148,7 @@ int update_head_with_reflog(const struct commit *old_head,\n static int run_rewrite_hook(const struct object_id *oldoid,\n \t\t\t    const struct object_id *newoid)\n {\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct strbuf tmp = STRBUF_INIT;\n \tstruct string_list to_stdin = STRING_LIST_INIT_DUP;\n \tint code;\n@@ -4522,7 +4522,7 @@ static int pick_commits(struct repository *r,\n \t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n \t\t\t\tst.st_size > 0) {\n \t\t\tstruct child_process notes_cp = CHILD_PROCESS_INIT;\n-\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n+\t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \n \t\t\tnotes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY);\n \t\t\tnotes_cp.git_cmd = 1;\ndiff --git a/transport.c b/transport.c\nindex 4ca8fc0391d..33da71a108b 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1204,7 +1204,7 @@ static int run_pre_push_hook(struct transport *transport,\n \t\t\t     struct ref *remote_refs)\n {\n \tint ret = 0;\n-\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n+\tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL;\n \tstruct ref *r;\n \tstruct string_list to_stdin = STRING_LIST_INIT_NODUP;\n \n-- \n2.33.0.867.g88ec4638586\n\n"},{"id":"435256","messageId":"patch-v4-3.5-3e647b8dba7-20210909T122802Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-0.5-00000000000-20210909T122802Z-avarab@gmail.com","subject":"[PATCH v4 3/5] hook: introduce \"git hook list\"","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-09T12:42:01Z","receivedAt":"2021-09-09T14:19:09Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nIf more than one hook will be run, it may be useful to see a list of\nwhich hooks should be run. At very least, it will be useful for us to\ntest the semantics of multihooks ourselves.\n\nFor now, only list the hooks which will run in the order they will run\nin; later, it might be useful to include more information like where the\nhooks were configured and whether or not they will run.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-hook.txt |  5 ++++\n builtin/hook.c             | 52 ++++++++++++++++++++++++++++++++++++++\n t/t1800-hook.sh            | 14 ++++++++++\n 3 files changed, 71 insertions(+)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 79e82479ec6..88588b38143 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -10,6 +10,7 @@ SYNOPSIS\n [verse]\n 'git hook' run [--to-stdin=<path>] [--ignore-missing] [(-j|--jobs) <n>]\n \t<hook-name> [-- <hook-args>]\n+'git hook' list <hook-name>\n \n DESCRIPTION\n -----------\n@@ -30,6 +31,10 @@ optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n arguments (if any) differ by hook name, see linkgit:githooks[5] for\n what those are.\n \n+list::\n+\tPrint a list of hooks which will be run on `<hook-name>` event. If no\n+\thooks are configured for that event, print nothing and return 1.\n+\n OPTIONS\n -------\n \ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 9b6272cfd3b..1e6b15d565a 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -8,8 +8,11 @@\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n \tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n+#define BUILTIN_HOOK_LIST_USAGE \\\n+\tN_(\"git hook list <hook-name>\")\n \n static const char * const builtin_hook_usage[] = {\n+\tBUILTIN_HOOK_LIST_USAGE,\n \tBUILTIN_HOOK_RUN_USAGE,\n \tNULL\n };\n@@ -19,6 +22,53 @@ static const char * const builtin_hook_run_usage[] = {\n \tNULL\n };\n \n+static const char *const builtin_hook_list_usage[] = {\n+\tBUILTIN_HOOK_LIST_USAGE,\n+\tNULL\n+};\n+\n+static int list(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct list_head *hooks;\n+\tstruct list_head *pos;\n+\tconst char *hookname = NULL;\n+\tstruct option list_options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tint ret = 0;\n+\n+\targc = parse_options(argc, argv, prefix, list_options,\n+\t\t\t     builtin_hook_list_usage, 0);\n+\n+\t/*\n+\t * The only unnamed argument provided should be the hook-name; if we add\n+\t * arguments later they probably should be caught by parse_options.\n+\t */\n+\tif (argc != 1)\n+\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\t\t      builtin_hook_list_usage, list_options);\n+\n+\thookname = argv[0];\n+\n+\thooks = list_hooks(hookname);\n+\n+\tif (list_empty(hooks)) {\n+\t\tret = 1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\tlist_for_each(pos, hooks) {\n+\t\tstruct hook *item = list_entry(pos, struct hook, list);\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item)\n+\t\t\tprintf(\"%s\\n\", item->hook_path);\n+\t}\n+\n+cleanup:\n+\tclear_hook_list(hooks);\n+\n+\treturn ret;\n+}\n static int run(int argc, const char **argv, const char *prefix)\n {\n \tint i;\n@@ -94,6 +144,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \tif (!argc)\n \t\tgoto usage;\n \n+\tif (!strcmp(argv[0], \"list\"))\n+\t\treturn list(argc, argv, prefix);\n \tif (!strcmp(argv[0], \"run\"))\n \t\treturn run(argc, argv, prefix);\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 6431b19e392..7a1dae4e95e 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -9,6 +9,8 @@ test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook run &&\n \ttest_expect_code 129 git hook run -h &&\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n+\ttest_expect_code 129 git hook list &&\n+\ttest_expect_code 129 git hook list -h &&\n \tgrep \"unknown option\" err\n '\n \n@@ -97,6 +99,18 @@ test_expect_success 'git hook run -- pass arguments' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'git hook list: does-not-exist hook' '\n+\ttest_expect_code 1 git hook list does-not-exist\n+'\n+\n+test_expect_success 'git hook list: existing hook' '\n+\tcat >expect <<-\\EOF &&\n+\t.git/hooks/test-hook\n+\tEOF\n+\tgit hook list test-hook >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'git hook run -- out-of-repo runs excluded' '\n \twrite_script .git/hooks/test-hook <<-EOF &&\n \techo Test hook\n-- \n2.33.0.867.g88ec4638586\n\n"},{"id":"435257","messageId":"patch-v4-5.5-5d5e9726fd8-20210909T122802Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-0.5-00000000000-20210909T122802Z-avarab@gmail.com","subject":"[PATCH v4 5/5] hook: allow out-of-repo 'git hook' invocations","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-09T12:42:03Z","receivedAt":"2021-09-09T14:19:11Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nSince hooks can now be supplied via the config, and a config can be\npresent without a gitdir via the global and system configs, we can start\nto allow 'git hook run' to occur without a gitdir. This enables us to do\nthings like run sendemail-validate hooks when running 'git send-email'\nfrom a nongit directory.\n\nIt still doesn't make sense to look for hooks in the hookdir in nongit\nrepos, though, as there is no hookdir.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n git.c           |  2 +-\n hook.c          |  2 +-\n t/t1800-hook.sh | 20 +++++++++++++++-----\n 3 files changed, 17 insertions(+), 7 deletions(-)\n\ndiff --git a/git.c b/git.c\nindex 540909c391f..39988ee3b02 100644\n--- a/git.c\n+++ b/git.c\n@@ -538,7 +538,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n-\t{ \"hook\", cmd_hook, RUN_SETUP },\n+\t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/hook.c b/hook.c\nindex b825fa7c7ae..dc3033cb4c7 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -181,7 +181,7 @@ struct list_head *list_hooks(const char *hookname)\n \n \t/* Add the hook from the hookdir. The placeholder makes it easier to\n \t * allocate work in pick_next_hook. */\n-\tif (find_hook(hookname))\n+\tif (have_git_dir() && find_hook(hookname))\n \t\tappend_or_move_hook(hook_head, NULL);\n \n \treturn hook_head;\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 68e7ff7de7e..6b6ba30e88e 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -128,15 +128,25 @@ test_expect_success 'git hook list: existing hook' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'git hook run -- out-of-repo runs excluded' '\n-\twrite_script .git/hooks/test-hook <<-EOF &&\n-\techo Test hook\n-\tEOF\n+test_expect_success 'git hook run: out-of-repo runs execute global hooks' '\n+\ttest_config_global hook.global-hook.event test-hook --add &&\n+\ttest_config_global hook.global-hook.command \"echo no repo no problems\" --add &&\n+\n+\techo \"global-hook\" >expect &&\n+\tnongit git hook list test-hook >actual &&\n+\ttest_cmp expect actual &&\n+\n+\techo \"no repo no problems\" >expect &&\n \n-\tnongit test_must_fail git hook run test-hook\n+\tnongit git hook run test-hook 2>actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo Test hook\n+\tEOF\n+\n \tmkdir my-hooks &&\n \twrite_script my-hooks/test-hook <<-\\EOF &&\n \techo Hook ran $1 >>actual\n-- \n2.33.0.867.g88ec4638586\n\n"},{"id":"435258","messageId":"patch-v4-4.5-d0f5b30fb27-20210909T122802Z-avarab@gmail.com","threadId":"55286","inReplyTo":"cover-v4-0.5-00000000000-20210909T122802Z-avarab@gmail.com","subject":"[PATCH v4 4/5] hook: include hooks from the config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-09T12:42:02Z","receivedAt":"2021-09-09T14:19:12Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Emily Shaffer <emilyshaffer@google.com>\n\nTeach the hook.[hc] library to parse configs to populate the list of\nhooks to run for a given event.\n\nMultiple commands can be specified for a given hook by providing\nmultiple \"hook.<friendly-name>.command = <path-to-hook>\" and\n\"hook.<friendly-name>.event = <hook-event>\" lines. Hooks will be started\nin config order of the \"hook.<name>.event\" lines (but may run in\nparallel).\n\nFor example:\n\n  $ git config --get-regexp \"^hook\\.\"\n  hook.bar.command=~/bar.sh\n  hook.bar.event=pre-commit\n\n  # Will run ~/bar.sh, then .git/hooks/pre-commit\n  $ git hook run pre-commit\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/config/hook.txt |  18 ++++\n Documentation/git-hook.txt    | 135 +++++++++++++++++++++++++++-\n builtin/hook.c                |   3 +-\n hook.c                        | 161 +++++++++++++++++++++++++++++----\n hook.h                        |   7 +-\n t/t1800-hook.sh               | 164 +++++++++++++++++++++++++++++++++-\n 6 files changed, 465 insertions(+), 23 deletions(-)\n\ndiff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt\nindex 96d3d6572c1..c3947563280 100644\n--- a/Documentation/config/hook.txt\n+++ b/Documentation/config/hook.txt\n@@ -1,3 +1,21 @@\n+hook.<name>.command::\n+\tA command to execute whenever `hook.<name>` is invoked. `<name>` should\n+\tbe a unique \"friendly\" name which you can use to identify this hook\n+\tcommand. (You can specify when to invoke this command with\n+\t`hook.<name>.event`.) The value can be an executable on your device or a\n+\toneliner for your shell. If more than one value is specified for the\n+\tsame `<name>`, the last value parsed will be the only command executed.\n+\tSee linkgit:git-hook[1].\n+\n+hook.<name>.event::\n+\tThe hook events which should invoke `hook.<name>`. `<name>` should be a\n+\tunique \"friendly\" name which you can use to identify this hook. The\n+\tvalue should be the name of a hook event, like \"pre-commit\" or \"update\".\n+\t(See linkgit:githooks[5] for a complete list of hooks Git knows about.)\n+\tOn the specified event, the associated `hook.<name>.command` will be\n+\texecuted. More than one event can be specified if you wish for\n+\t`hook.<name>` to execute on multiple events. See linkgit:git-hook[1].\n+\n hook.jobs::\n \tSpecifies how many hooks can be run simultaneously during parallelized\n \thook execution. If unspecified, defaults to the number of processors on\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 88588b38143..51bda42fb83 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -19,12 +19,102 @@ This command is an interface to git hooks (see linkgit:githooks[5]).\n Currently it only provides a convenience wrapper for running hooks for\n use by git itself. In the future it might gain other functionality.\n \n+It's possible to use this command to refer to hooks which are not native to Git,\n+for example if a wrapper around Git wishes to expose hooks into its own\n+operation in a way which is already familiar to Git users. However, wrappers\n+invoking such hooks should be careful to name their hook events something which\n+Git is unlikely to use for a native hook later on. For example, Git is much less\n+likely to create a `mytool-validate-commit` hook than it is to create a\n+`validate-commit` hook.\n+\n+This command parses the default configuration files for sets of configs like\n+so:\n+\n+  [hook \"linter\"]\n+    event = pre-commit\n+    command = ~/bin/linter --cpp20\n+\n+In this example, `[hook \"linter\"]` represents one script - `~/bin/linter\n+--cpp20` - which can be shared by many repos, and even by many hook events, if\n+appropriate.\n+\n+To add an unrelated hook which runs on a different event, for example a\n+spell-checker for your commit messages, you would write a configuration like so:\n+\n+  [hook \"linter\"]\n+    event = pre-commit\n+    command = ~/bin/linter --cpp20\n+  [hook \"spellcheck\"]\n+    event = commit-msg\n+    command = ~/bin/spellchecker\n+\n+With this config, when you run 'git commit', first `~/bin/linter --cpp20` will\n+have a chance to check your files to be committed (during the `pre-commit` hook\n+event`), and then `~/bin/spellchecker` will have a chance to check your commit\n+message (during the `commit-msg` hook event).\n+\n+Commands are run in the order Git encounters their associated\n+`hook.<name>.event` configs during the configuration parse (see\n+linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be\n+added, only one `hook.linter.command` event is valid - Git uses \"last-one-wins\"\n+to determine which command to run.\n+\n+So if you wanted your linter to run when you commit as well as when you push,\n+you would configure it like so:\n+\n+  [hook \"linter\"]\n+    event = pre-commit\n+    event = pre-push\n+    command = ~/bin/linter --cpp20\n+\n+With this config, `~/bin/linter --cpp20` would be run by Git before a commit is\n+generated (during `pre-commit`) as well as before a push is performed (during\n+`pre-push`).\n+\n+And if you wanted to run your linter as well as a secret-leak detector during\n+only the \"pre-commit\" hook event, you would configure it instead like so:\n+\n+  [hook \"linter\"]\n+    event = pre-commit\n+    command = ~/bin/linter --cpp20\n+  [hook \"no-leaks\"]\n+    event = pre-commit\n+    command = ~/bin/leak-detector\n+\n+With this config, before a commit is generated (during `pre-commit`), Git would\n+first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It\n+would evaluate the output of each when deciding whether to proceed with the\n+commit.\n+\n+For a full list of hook events which you can set your `hook.<name>.event` to,\n+and how hooks are invoked during those events, see linkgit:githooks[5].\n+\n+Git will ignore any `hook.<name>.event` that specifies an event it doesn't\n+recognize. This is intended so that tools which wrap Git can use the hook\n+infrastructure to run their own hooks; see <<WRAPPERS>> for more guidance.\n+\n+In general, when instructions suggest adding a script to\n+`.git/hooks/<hook-event>`, you can specify it in the config instead by running:\n+\n+----\n+git config hook.<some-name>.command <path-to-script>\n+git config --add hook.<some-name>.event <hook-event>\n+----\n+\n+This way you can share the script between multiple repos. That is, `cp\n+~/my-script.sh ~/project/.git/hooks/pre-commit` would become:\n+\n+----\n+git config hook.my-script.command ~/my-script.sh\n+git config --add hook.my-script.event pre-commit\n+----\n+\n SUBCOMMANDS\n -----------\n \n run::\n-\tRun the `<hook-name>` hook. See linkgit:githooks[5] for\n-\tthe hook names we support.\n+\tRuns hooks configured for `<hook-name>`, in the order they are\n+\tdiscovered during the config parse.\n +\n Any positional arguments to the hook should be passed after an\n optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The\n@@ -58,6 +148,47 @@ config is not specified, uses the number of CPUs on the current system. Some\n hooks may be ineligible for parallelization: for example, 'commit-msg' intends\n hooks modify the commit message body and cannot be parallelized.\n \n+[[WRAPPERS]]\n+WRAPPERS\n+--------\n+\n+`git hook run` has been designed to make it easy for tools which wrap Git to\n+configure and execute hooks using the Git hook infrastructure. It is possible to\n+provide arguments, environment variables (TODO this is missing from reroll TODO),\n+and stdin via the command line, as well as specifying parallel or series\n+execution if the user has provided multiple hooks.\n+\n+Assuming your wrapper wants to support a hook named \"mywrapper-start-tests\", you\n+can have your users specify their hooks like so:\n+\n+  [hook \"setup-test-dashboard\"]\n+    event = mywrapper-start-tests\n+    command = ~/mywrapper/setup-dashboard.py --tap\n+\n+Then, in your 'mywrapper' tool, you can invoke any users' configured hooks by\n+running:\n+\n+----\n+git hook run mywrapper-start-tests \\\n+  # providing something to stdin\n+  --stdin some-tempfile-123 \\\n+  # setting an env var (TODO THIS IS MISSING TODO)\n+  --env MYWRAPPER_EXECUTION_MODE=foo \\\n+  # execute hooks in serial\n+  --jobs 1 \\\n+  # plus some arguments of your own...\n+  -- \\\n+  --testname bar \\\n+  baz\n+----\n+\n+Take care to name your wrapper's hook events in a way which is unlikely to\n+overlap with Git's native hooks (see linkgit:githooks[5]) - a hook event named\n+`mywrappertool-validate-commit` is much less likely to be added to native Git\n+than a hook event named `validate-commit`. If Git begins to use a hook event\n+named the same thing as your wrapper hook, it may invoke your users' hooks in\n+unintended and unsupported ways.\n+\n CONFIGURATION\n -------------\n include::config/hook.txt[]\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 1e6b15d565a..bb8fdde6bad 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -61,7 +61,8 @@ static int list(int argc, const char **argv, const char *prefix)\n \t\tstruct hook *item = list_entry(pos, struct hook, list);\n \t\titem = list_entry(pos, struct hook, list);\n \t\tif (item)\n-\t\t\tprintf(\"%s\\n\", item->hook_path);\n+\t\t\tprintf(\"%s\\n\", item->name ? item->name\n+\t\t\t\t\t\t  : _(\"hook from hookdir\"));\n \t}\n \n cleanup:\ndiff --git a/hook.c b/hook.c\nindex 600030c59ec..b825fa7c7ae 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -8,10 +8,54 @@ static void free_hook(struct hook *ptr)\n \tif (!ptr)\n \t\treturn;\n \n+\tfree(ptr->name);\n \tfree(ptr->feed_pipe_cb_data);\n \tfree(ptr);\n }\n \n+/*\n+ * Walks the linked list at 'head' to check if any hook named 'name'\n+ * already exists. Returns a pointer to that hook if so, otherwise returns NULL.\n+ */\n+static struct hook *find_hook_by_name(struct list_head *head,\n+\t\t\t\t\t const char *name)\n+{\n+\tstruct list_head *pos = NULL, *tmp = NULL;\n+\tstruct hook *found = NULL;\n+\n+\tlist_for_each_safe(pos, tmp, head) {\n+\t\tstruct hook *it = list_entry(pos, struct hook, list);\n+\t\tif (!strcmp(it->name, name)) {\n+\t\t\tlist_del(pos);\n+\t\t\tfound = it;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\treturn found;\n+}\n+\n+/*\n+ * Adds a hook if it's not already in the list, or moves it to the tail of the\n+ * list if it was already there. name == NULL indicates it's from the hookdir;\n+ * just append it in this case.\n+ */\n+static void append_or_move_hook(struct list_head *head, const char *name)\n+{\n+\tstruct hook *to_add = NULL;\n+\n+\t/* if it's not from hookdir, check if the hook is already in the list */\n+\tif (name)\n+\t\tto_add = find_hook_by_name(head, name);\n+\n+\tif (!to_add) {\n+\t\t/* adding a new hook, not moving an old one */\n+\t\tto_add = xcalloc(1, sizeof(*to_add));\n+\t\tto_add->name = xstrdup_or_null(name);\n+\t}\n+\n+\tlist_add_tail(&to_add->list, head);\n+}\n+\n static void remove_hook(struct list_head *to_remove)\n {\n \tstruct hook *hook_to_remove = list_entry(to_remove, struct hook, list);\n@@ -73,26 +117,72 @@ int hook_exists(const char *name)\n \treturn exists;\n }\n \n+struct hook_config_cb\n+{\n+\tconst char *hook_event;\n+\tstruct list_head *list;\n+};\n+\n+/*\n+ * Callback for git_config which adds configured hooks to a hook list.  Hooks\n+ * can be configured by specifying both hook.<friend-name>.command = <path> and\n+ * hook.<friendly-name>.event = <hook-event>.\n+ */\n+static int hook_config_lookup(const char *key, const char *value, void *cb_data)\n+{\n+\tstruct hook_config_cb *data = cb_data;\n+\tconst char *subsection, *parsed_key;\n+\tsize_t subsection_len = 0;\n+\tstruct strbuf subsection_cpy = STRBUF_INIT;\n+\n+\t/*\n+\t * Don't bother doing the expensive parse if there's no\n+\t * chance that the config matches 'hook.myhook.event = hook_event'.\n+\t */\n+\tif (!value || strcmp(value, data->hook_event))\n+\t\treturn 0;\n+\n+\t/* Looking for \"hook.friendlyname.event = hook_event\" */\n+\tif (parse_config_key(key,\n+\t\t\t    \"hook\",\n+\t\t\t    &subsection,\n+\t\t\t    &subsection_len,\n+\t\t\t    &parsed_key) ||\n+\t    strcmp(parsed_key, \"event\"))\n+\t\treturn 0;\n+\n+\t/*\n+\t * 'subsection' is a pointer to the internals of 'key', which we don't\n+\t * own the memory for. Copy it away to the hook list.\n+\t */\n+\tstrbuf_add(&subsection_cpy, subsection, subsection_len);\n+\n+\tappend_or_move_hook(data->list, subsection_cpy.buf);\n+\tstrbuf_release(&subsection_cpy);\n+\n+\treturn 0;\n+}\n+\n struct list_head *list_hooks(const char *hookname)\n {\n \tstruct list_head *hook_head = xmalloc(sizeof(struct list_head));\n+\tstruct hook_config_cb cb_data = {\n+\t\t.hook_event = hookname,\n+\t\t.list = hook_head,\n+\t};\n \n \tINIT_LIST_HEAD(hook_head);\n \n \tif (!hookname)\n \t\tBUG(\"null hookname was provided to hook_list()!\");\n \n-\tif (have_git_dir()) {\n-\t\tconst char *hook_path = find_hook(hookname);\n+\t/* Add the hooks from the config, e.g. hook.myhook.event = pre-commit */\n+\tgit_config(hook_config_lookup, &cb_data);\n \n-\t\t/* Add the hook from the hookdir */\n-\t\tif (hook_path) {\n-\t\t\tstruct hook *to_add = xmalloc(sizeof(*to_add));\n-\t\t\tto_add->hook_path = hook_path;\n-\t\t\tto_add->feed_pipe_cb_data = NULL;\n-\t\t\tlist_add_tail(&to_add->list, hook_head);\n-\t\t}\n-\t}\n+\t/* Add the hook from the hookdir. The placeholder makes it easier to\n+\t * allocate work in pick_next_hook. */\n+\tif (find_hook(hookname))\n+\t\tappend_or_move_hook(hook_head, NULL);\n \n \treturn hook_head;\n }\n@@ -153,11 +243,47 @@ static int pick_next_hook(struct child_process *cp,\n \tcp->trace2_hook_name = hook_cb->hook_name;\n \tcp->dir = hook_cb->options->dir;\n \n+\t/*\n+\t * to enable oneliners, let config-specified hooks run in shell.\n+\t * config-specified hooks have a name.\n+\t */\n+\tcp->use_shell = !!run_me->name;\n+\n \t/* add command */\n-\tif (hook_cb->options->absolute_path)\n-\t\tstrvec_push(&cp->args, absolute_path(run_me->hook_path));\n-\telse\n-\t\tstrvec_push(&cp->args, run_me->hook_path);\n+\tif (run_me->name) {\n+\t\t/* ...from config */\n+\t\tstruct strbuf cmd_key = STRBUF_INIT;\n+\t\tchar *command = NULL;\n+\n+\t\tstrbuf_addf(&cmd_key, \"hook.%s.command\", run_me->name);\n+\t\tif (git_config_get_string(cmd_key.buf, &command)) {\n+\t\t\t/* TODO test me! */\n+\t\t\tdie(_(\"'hook.%s.command' must be configured \"\n+\t\t\t      \"or 'hook.%s.event' must be removed; aborting.\\n\"),\n+\t\t\t    run_me->name, run_me->name);\n+\t\t}\n+\n+\t\tstrvec_push(&cp->args, command);\n+\t\tfree(command);\n+\t\tstrbuf_release(&cmd_key);\n+\t} else {\n+\t\t/* ...from hookdir. */\n+\t\tconst char *hook_path = NULL;\n+\t\t/*\n+\t\t * At this point we are already running, so don't validate\n+\t\t * whether the hook name is known or not. Validation was\n+\t\t * performed earlier in list_hooks().\n+\t\t */\n+\t\thook_path = find_hook(hook_cb->hook_name);\n+\t\tif (!hook_path)\n+\t\t\tBUG(\"hookdir hook in hook list but no hookdir hook present in filesystem\");\n+\n+\t\tif (hook_cb->options->absolute_path)\n+\t\t\thook_path = absolute_path(hook_path);\n+\n+\t\tstrvec_push(&cp->args, hook_path);\n+\t}\n+\n \n \t/*\n \t * add passed-in argv, without expanding - let the user get back\n@@ -187,8 +313,11 @@ static int notify_start_failure(struct strbuf *out,\n \n \thook_cb->rc |= 1;\n \n-\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n-\t\t    attempted->hook_path);\n+\tif (attempted->name)\n+\t\tstrbuf_addf(out, _(\"Couldn't start hook '%s'\\n\"),\n+\t\t    attempted->name);\n+\telse\n+\t\tstrbuf_addstr(out, _(\"Couldn't start hook from hooks directory\\n\"));\n \n \treturn 1;\n }\ndiff --git a/hook.h b/hook.h\nindex fe16ab35028..4b728991089 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -7,8 +7,11 @@\n \n struct hook {\n \tstruct list_head list;\n-\t/* The path to the hook */\n-\tconst char *hook_path;\n+\t/*\n+\t * The friendly name of the hook. NULL indicates the hook is from the\n+\t * hookdir.\n+\t */\n+\tchar *name;\n \n \t/*\n \t * Use this to keep state for your feed_pipe_fn if you are using\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 7a1dae4e95e..68e7ff7de7e 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -1,13 +1,30 @@\n #!/bin/sh\n \n-test_description='git-hook command'\n+test_description='git-hook command and config-managed multihooks'\n \n . ./test-lib.sh\n \n+setup_hooks () {\n+\ttest_config hook.ghi.command \"/path/ghi\"\n+\ttest_config hook.ghi.event pre-commit --add\n+\ttest_config hook.ghi.event test-hook --add\n+\ttest_config_global hook.def.command \"/path/def\"\n+\ttest_config_global hook.def.event pre-commit --add\n+}\n+\n+setup_hookdir () {\n+\tmkdir .git/hooks\n+\twrite_script .git/hooks/pre-commit <<-EOF\n+\techo \\\"Legacy Hook\\\"\n+\tEOF\n+\ttest_when_finished rm -rf .git/hooks\n+}\n+\n test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook &&\n \ttest_expect_code 129 git hook run &&\n \ttest_expect_code 129 git hook run -h &&\n+\ttest_expect_code 129 git hook list -h &&\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n \ttest_expect_code 129 git hook list -h &&\n@@ -105,7 +122,7 @@ test_expect_success 'git hook list: does-not-exist hook' '\n \n test_expect_success 'git hook list: existing hook' '\n \tcat >expect <<-\\EOF &&\n-\t.git/hooks/test-hook\n+\thook from hookdir\n \tEOF\n \tgit hook list test-hook >actual &&\n \ttest_cmp expect actual\n@@ -162,4 +179,147 @@ test_expect_success 'stdin to hooks' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'git hook list orders by config order' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tdef\n+\tghi\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git hook list reorders on duplicate event declarations' '\n+\tsetup_hooks &&\n+\n+\t# 'def' is usually configured globally; move it to the end by\n+\t# configuring it locally.\n+\ttest_config hook.def.event \"pre-commit\" --add &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tghi\n+\tdef\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hook can be configured for multiple events' '\n+\tsetup_hooks &&\n+\n+\t# 'ghi' should be included in both 'pre-commit' and 'test-hook'\n+\tgit hook list pre-commit >actual &&\n+\tgrep \"ghi\" actual &&\n+\tgit hook list test-hook >actual &&\n+\tgrep \"ghi\" actual\n+'\n+\n+test_expect_success 'git hook list shows hooks from the hookdir' '\n+\tsetup_hookdir &&\n+\n+\tcat >expected <<-\\EOF &&\n+\thook from hookdir\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions execute oneliners' '\n+\ttest_config hook.oneliner.event \"pre-commit\" &&\n+\ttest_config hook.oneliner.command \"echo \\\"Hello World\\\"\" &&\n+\n+\techo \"Hello World\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'inline hook definitions resolve paths' '\n+\twrite_script sample-hook.sh <<-\\EOF &&\n+\techo \\\"Sample Hook\\\"\n+\tEOF\n+\n+\ttest_when_finished \"rm sample-hook.sh\" &&\n+\n+\ttest_config hook.sample-hook.event pre-commit &&\n+\ttest_config hook.sample-hook.command \"\\\"$(pwd)/sample-hook.sh\\\"\" &&\n+\n+\techo \\\"Sample Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'hookdir hook included in git hook run' '\n+\tsetup_hookdir &&\n+\n+\techo \\\"Legacy Hook\\\" >expected &&\n+\n+\t# hooks are run with stdout_to_stderr = 1\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'stdin to multiple hooks' '\n+\ttest_config hook.stdin-a.event \"test-hook\" --add &&\n+\ttest_config hook.stdin-a.command \"xargs -P1 -I% echo a%\" --add &&\n+\ttest_config hook.stdin-b.event \"test-hook\" --add &&\n+\ttest_config hook.stdin-b.command \"xargs -P1 -I% echo b%\" --add &&\n+\n+\tcat >input <<-\\EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tcat >expected <<-\\EOF &&\n+\ta1\n+\ta2\n+\ta3\n+\tb1\n+\tb2\n+\tb3\n+\tEOF\n+\n+\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'multiple hooks in series' '\n+\ttest_config hook.series-1.event \"test-hook\" &&\n+\ttest_config hook.series-1.command \"echo 1\" --add &&\n+\ttest_config hook.series-2.event \"test-hook\" &&\n+\ttest_config hook.series-2.command \"echo 2\" --add &&\n+\tmkdir .git/hooks &&\n+\twrite_script .git/hooks/test-hook <<-EOF &&\n+\techo 3\n+\tEOF\n+\n+\tcat >expected <<-\\EOF &&\n+\t1\n+\t2\n+\t3\n+\tEOF\n+\n+\tgit hook run -j1 test-hook 2>actual &&\n+\ttest_cmp expected actual &&\n+\n+\trm -rf .git/hooks\n+'\n+\n+test_expect_success 'rejects hooks with no commands configured' '\n+\ttest_config hook.broken.event \"test-hook\" &&\n+\n+\techo broken >expected &&\n+\tgit hook list test-hook >actual &&\n+\ttest_cmp expected actual &&\n+\ttest_must_fail git hook run test-hook\n+'\n+\n test_done\n-- \n2.33.0.867.g88ec4638586\n\n"},{"id":"435260","messageId":"patch-1.1-38b21b65bee-20210909T125613Z-avarab@gmail.com","threadId":"55286","inReplyTo":"patch-v4-1.5-2f0cac14965-20210909T122802Z-avarab@gmail.com","subject":"[PATCH v4] fixup! hook: run a list of hooks instead","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-09-09T12:59:14Z","receivedAt":"2021-09-09T14:24:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"---\n\nAnd of course just as I sent out\n<cover-v4-0.5-00000000000-20210909T122802Z-avarab@gmail.com> I noticed\nthat there was one last memory leak left, it just wasn't stressed by\nt1800-hook.sh, but some of the test of the tests, including\nt0000-basic.sh!\n\nThat test coverage should be improved, but in the meantime Junio:\nHere's a fixup to the just-sent you can hopefully squash into the\n1/5. Sorry about the screwup.\n\n hook.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex dc3033cb4c7..4b4205bce6c 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -416,8 +416,10 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options)\n \t * If you need to act on a missing hook, use run_found_hooks()\n \t * instead\n \t */\n-\tif (list_empty(hooks))\n+\tif (list_empty(hooks)) {\n+\t\tclear_hook_list(hooks);\n \t\tgoto cleanup;\n+\t}\n \n \tret = run_hooks(hook_name, hooks, options);\n \n-- \n2.33.0.867.g88ec4638586\n\n"},{"id":"438058","messageId":"87a6jmiezr.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"patch-v5-24.36-bb119fa7cc0-20210902T125110Z-avarab@gmail.com","subject":"ab/config-based-hooks-N status (was Re: [PATCH v5 24/36] run-command: add stdin callback for parallelization)","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-06T11:03:38Z","receivedAt":"2021-10-06T11:12:31Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Sep 02 2021, Ævar Arnfjörð Bjarmason wrote:\n\nEmily, there's a...:\n\n> diff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\n> index 7ae03dc7123..9348184d303 100644\n> --- a/t/helper/test-run-command.c\n> +++ b/t/helper/test-run-command.c\n> @@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,\n>  \t\treturn 0;\n>  \n>  \tstrvec_pushv(&cp->args, d->argv);\n> +\tcp->in = d->in;\n> +\tcp->no_stdin = d->no_stdin;\n>  \tstrbuf_addstr(err, \"preloaded output of a child\\n\");\n>  \tnumber_callbacks++;\n> +\n> +\t*task_cb = xmalloc(sizeof(int));\n> +\t*(int*)(*task_cb) = 2;\n>  \treturn 1;\n>  }\n\nProbably trivial to solve failure here in t0061-run-command.sh if you\ncompile with SANITIZE=leak. This failed in combination with my[1] (but\nfor anyone reading along, this patch has been ejected from \"seen\" a\nwhile ago).\n\nMore generally: The equivalent of 01-07/36 of this series is being\nmerged into \"next\". As described in a plan to submit this topic\nincrementally I was hoping to do 08-20/36 next, i.e. up to \"run-command:\nremove old run_hook_{le,ve}() hook API\". See [2] for that plan.\n\nYou've been inactive on-list recently, it would be nice to time this so\nthat by the time it gets to 21-36/36 (which I was planning to split in\ntwo per [2]) that you'd have time to review/help with outstanding issues\netc, for eventually re-submitting your \"config based hooks\" on top once\nthis all lands.\n\n1. https://lore.kernel.org/git/patch-02.10-9a8804e1d9a-20211006T094705Z-avarab@gmail.com/\n2. https://lore.kernel.org/git/875yut8nns.fsf@evledraar.gmail.com/\n"},{"id":"438535","messageId":"87lf2yjt3v.fsf@evledraar.gmail.com","threadId":"55286","inReplyTo":"87a6jmiezr.fsf@evledraar.gmail.com","subject":"Re: ab/config-based-hooks-N status (was Re: [PATCH v5 24/36] run-command: add stdin callback for parallelization)","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2021-10-12T12:59:45Z","receivedAt":"2021-10-12T13:00:25Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Oct 06 2021, Ævar Arnfjörð Bjarmason wrote:\n\n> On Thu, Sep 02 2021, Ævar Arnfjörð Bjarmason wrote:\n>\n> Emily, there's a...:\n>\n>> diff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\n>> index 7ae03dc7123..9348184d303 100644\n>> --- a/t/helper/test-run-command.c\n>> +++ b/t/helper/test-run-command.c\n>> @@ -32,8 +32,13 @@ static int parallel_next(struct child_process *cp,\n>>  \t\treturn 0;\n>>  \n>>  \tstrvec_pushv(&cp->args, d->argv);\n>> +\tcp->in = d->in;\n>> +\tcp->no_stdin = d->no_stdin;\n>>  \tstrbuf_addstr(err, \"preloaded output of a child\\n\");\n>>  \tnumber_callbacks++;\n>> +\n>> +\t*task_cb = xmalloc(sizeof(int));\n>> +\t*(int*)(*task_cb) = 2;\n>>  \treturn 1;\n>>  }\n>\n> Probably trivial to solve failure here in t0061-run-command.sh if you\n> compile with SANITIZE=leak. This failed in combination with my[1] (but\n> for anyone reading along, this patch has been ejected from \"seen\" a\n> while ago).\n>\n> More generally: The equivalent of 01-07/36 of this series is being\n> merged into \"next\". As described in a plan to submit this topic\n> incrementally I was hoping to do 08-20/36 next, i.e. up to \"run-command:\n> remove old run_hook_{le,ve}() hook API\". See [2] for that plan.\n>\n> You've been inactive on-list recently, it would be nice to time this so\n> that by the time it gets to 21-36/36 (which I was planning to split in\n> two per [2]) that you'd have time to review/help with outstanding issues\n> etc, for eventually re-submitting your \"config based hooks\" on top once\n> this all lands.\n>\n> 1. https://lore.kernel.org/git/patch-02.10-9a8804e1d9a-20211006T094705Z-avarab@gmail.com/\n> 2. https://lore.kernel.org/git/875yut8nns.fsf@evledraar.gmail.com/\n\nSince I had a reason to look at this again, this fixes it. I've squashed\nit into my \"base\" branch, but it won't be in the next batch I submit (I\nhave the cut-off point before this commit):\n\ndiff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c\nindex fa25bcbbc0d..e9b4214d163 100644\n--- a/t/helper/test-run-command.c\n+++ b/t/helper/test-run-command.c\n@@ -61,11 +61,21 @@ static void test_consume_sideband(struct strbuf *output, void *cb)\n \tfclose(sideband);\n }\n \n+static int task_free(int result,\n+\t\t     struct strbuf *err,\n+\t\t     void *pp_cb,\n+\t\t     void *pp_task_cb)\n+{\n+\tfree(pp_task_cb);\n+\treturn 0;\n+}\n+\n static int task_finished(int result,\n \t\t\t struct strbuf *err,\n \t\t\t void *pp_cb,\n \t\t\t void *pp_task_cb)\n {\n+\ttask_free(0, NULL, NULL, pp_task_cb);\n \tstrbuf_addstr(err, \"asking for a quick stop\\n\");\n \treturn 1;\n }\n@@ -438,7 +448,7 @@ int cmd__run_command(int argc, const char **argv)\n \n \tif (!strcmp(argv[1], \"run-command-parallel\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n-\t\t\t\t\t    NULL, NULL, NULL, NULL, &proc));\n+\t\t\t\t\t    NULL, NULL, NULL, task_free, &proc));\n \n \tif (!strcmp(argv[1], \"run-command-abort\"))\n \t\texit(run_processes_parallel(jobs, parallel_next,\n@@ -452,12 +462,12 @@ int cmd__run_command(int argc, const char **argv)\n \t\tproc.in = -1;\n \t\tproc.no_stdin = 0;\n \t\texit (run_processes_parallel(jobs, parallel_next, NULL,\n-\t\t\t\t\t     test_stdin, NULL, NULL, &proc));\n+\t\t\t\t\t     test_stdin, NULL, task_free, &proc));\n \t}\n \n \tif (!strcmp(argv[1], \"run-command-sideband\"))\n \t\texit(run_processes_parallel(jobs, parallel_next, NULL, NULL,\n-\t\t\t\t\t    test_consume_sideband, NULL,\n+\t\t\t\t\t    test_consume_sideband, task_free,\n \t\t\t\t\t    &proc));\n \n \tfprintf(stderr, \"check usage\\n\");\n"}]}