{"thread":{"id":"53527","subject":"[PATCH v2 0/4] propose config-based hooks","startedAt":"2020-05-21T18:54:24Z","lastAt":"2020-10-29T20:04:30Z","messageCount":69,"participants":["Emily Shaffer","Phillip Wood","Johannes Schindelin","Junio C Hamano","Jonathan Tan","Martin Ågren","Josh Steadmon","Jonathan Nieder","Ævar Arnfjörð Bjarmason"],"isPatch":true,"patchVersion":2,"patchTotal":4},"messages":[{"id":"398384","messageId":"20200521185414.43760-1-emilyshaffer@google.com","threadId":"53527","inReplyTo":null,"subject":"[PATCH v2 0/4] propose config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-05-21T18:54:10Z","receivedAt":"2020-05-21T18:54:24Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"This series implements \"Stage 1\" of the config-based hooks rollout\nprocess as proposed in the design doc. It does not touch the existing\nhook implementation or change the way that Git functions - it only adds\na new, independent command.\n\nIn the design doc, I mentioned the possibility of including 'git hook\nadd' and 'git hook edit' in this stage. However, I'd like to get input\nfrom our UX team internally before I get started - I know my own limits,\nand coming up with good UX design is one of them ;) Unfortunately, I\nwon't be able to get time with them until the first week of June, so I\nhaven't included those commands here.\n\nThe series is listed as v2 because I included the updated design doc\nwith changes pointed out by Junio and brian. That's a good place to\nstart if you're reviewing the series for the first time. (I'm also\nbreaking thread with the contributor summit notes to bring the series to\nthe attention of more contributors who may be interested.)\n\nOne point I'd like discussion on especially is the '--porcelain'\ncommand. The intent was to make it very easy for non-builtins to run\nhooks; but I'm starting to wonder whether it makes more sense to include\na `git hook run <hookname>`, which makes parallelization possible in the\nfuture if we decide to implement that. Even if we decide it makes sense\nto keep 'list --porcelain', I'm not sure what information to include;\nproviding simply the line to pass to 'sh' seems a little thin.\n\nThe next stage from here is to migrate internal callers who use\n'find_hook()' now to call the hook library (and teach the hook library\nto call find_hook()), which will essentially turn on config-based hooks;\ndoes it make sense to include that stage at the same time as this\nseries so we aren't checking in unused code?\n\nThanks all.\n - Emily\n\nEmily Shaffer (4):\n  doc: propose hooks managed by the config\n  hook: scaffolding for git-hook subcommand\n  hook: add list command\n  hook: add --porcelain to list command\n\n .gitignore                                    |   1 +\n Documentation/Makefile                        |   1 +\n Documentation/git-hook.txt                    |  63 ++++\n .../technical/config-based-hooks.txt          | 320 ++++++++++++++++++\n Makefile                                      |   2 +\n builtin.h                                     |   1 +\n builtin/hook.c                                |  77 +++++\n git.c                                         |   1 +\n hook.c                                        |  90 +++++\n hook.h                                        |  15 +\n t/t1360-config-based-hooks.sh                 |  69 ++++\n 11 files changed, 640 insertions(+)\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\n-- \n2.27.0.rc0.183.gde8f92d652-goog\n\n"},{"id":"398385","messageId":"20200521185414.43760-2-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200521185414.43760-1-emilyshaffer@google.com","subject":"[PATCH v2 1/4] doc: propose hooks managed by the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-05-21T18:54:11Z","receivedAt":"2020-05-21T18:54:30Z","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 Documentation/Makefile                        |   1 +\n .../technical/config-based-hooks.txt          | 320 ++++++++++++++++++\n 2 files changed, 321 insertions(+)\n create mode 100644 Documentation/technical/config-based-hooks.txt\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex 15d9d04f31..5b21f31d31 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -80,6 +80,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..59cdc25a47\n--- /dev/null\n+++ b/Documentation/technical/config-based-hooks.txt\n@@ -0,0 +1,320 @@\n+Configuration-based hook management\n+===================================\n+\n+== Motivation\n+\n+Treat hooks as a first-class citizen by replacing the .git/hook/hookname path as\n+the only source of hooks to execute, in a way which is friendly to users with\n+multiple repos which have similar needs.\n+\n+Redefine \"hook\" as an event rather than a single script, allowing users to\n+perform unrelated actions on a single event.\n+\n+Take a step closer to safety when copying zipped Git repositories from untrusted\n+users.\n+\n+Make it easier for users to discover Git's hook feature and automate their\n+workflows.\n+\n+== User interfaces\n+\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+==== `hook`\n+\n+Primarily contains subsections for each hook event. These subsections define\n+hook command execution order; hook commands can be specified by passing the\n+command directly if no additional configuration is needed, or by passing the\n+name of a `hookcmd`. If Git does not find a `hookcmd` whose subsection matches\n+the value of the given command string, Git will try to execute the string\n+directly. Hooks are executed by passing the resolved command string to the\n+shell. Hook event subsections can also contain per-hook-event settings.\n+\n+Also contains top-level hook execution settings, for example,\n+`hook.warnHookDir`, `hook.runHookDir`, or `hook.disableAll`.\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+  error = ignore\n+\n+[hook]\n+  runHookDir = interactive\n+----\n+\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 to which\n+it's normally subscribed _except_ `pre-commit`.\n+\n+----\n+[hookcmd \"perl-linter\"]\n+  command = /usr/bin/lint-it --language=perl\n+  skip = true\n+  pre-commit-skip = false\n+----\n+\n+=== Command-line API\n+\n+Users should be able to view, reorder, and create hook commands via the command\n+line. External tools should be able to view a list of hooks in the correct order\n+to run.\n+\n+*`git hook list <hook-event>`*\n+\n+*`git hook list (--system|--global|--local|--worktree)`*\n+\n+*`git hook edit <hook-event>`*\n+\n+*`git hook add <hook-command> <hook-event> <options...>`*\n+\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`.\n+\n+== Implementation\n+\n+=== Library\n+\n+`hook.c` and `hook.h` are responsible for interacting with the config files. In\n+the case when the code generating a hook event doesn't have special concerns\n+about how to run the hooks, the hook library will provide a basic API to call\n+all hooks in config order with an `argv_array` provided by the code which\n+generates the hook event:\n+\n+*`int run_hooks(const char *hookname, struct argv_array *args)`*\n+\n+This call includes the hook command provided by `run-command.h:find_hook()`;\n+eventually, this legacy hook will be gated by a config `hook.runHookDir`. The\n+config is checked against a number of cases:\n+\n+- \"no\": the legacy hook will not be run\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\".\n+\n+If the caller wants to do something more complicated, the hook library can also\n+provide a callback API:\n+\n+*`int for_each_hookcmd(const char *hookname, hookcmd_function *cb)`*\n+\n+Finally, to facilitate the builtin, the library will also provide the following\n+APIs to interact with the config:\n+\n+----\n+int set_hook_commands(const char *hookname, struct string_list *commands,\n+\tenum config_scope scope);\n+int set_hookcmd(const char *hookcmd, struct hookcmd options);\n+\n+int list_hook_commands(const char *hookname, struct string_list *commands);\n+int list_hooks_in_scope(enum config_scope scope, struct string_list *commands);\n+----\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+----\n+struct hookcmd {\n+  const char *name;\n+  const char *command;\n+\n+  /* for illustration only; not planned at present */\n+  int parallelizable;\n+  const char *hookcmd_before;\n+  const char *hookcmd_after;\n+  enum recovery_action on_fail;\n+}\n+----\n+\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 path\n+\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+\n+`git hook list --porcelain <hook-event>` is implemented. Users can replace their\n+`.git/hooks/<hook-event>` scripts with a trampoline based on `git hook list`'s\n+output. Modifier commands like `git hook add` and `git hook edit` can be\n+implemented around this time as well.\n+\n+==== Stage 2\n+\n+`hook.h:run_hooks()` is taught to include `run-command.h:find_hook()` at the\n+end; calls to `find_hook()` are replaced with calls to `run_hooks()`. Users can\n+opt-in to config-based hooks simply by creating some in their config; otherwise\n+users should remain unaffected by the change.\n+\n+==== Stage 3\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 4\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+\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. The\n+goal is to avoid an overcomplicated design to work around a problem which has\n+ceased to exist.\n+\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+== 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+=== 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+\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 table\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+|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+|No\n+|No\n+\n+|Discoverability\n+|Better (in `git help git`)\n+|Same as before\n+|Same as before\n+\n+|Hard to run unexpected hook\n+|If configured\n+|No\n+|No\n+|===\n+\n+== Future work\n+\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+\n+Users with many hooks might want to run them simultaneously, if the hooks don't\n+modify state; if one hook depends on another's output, then users will want to\n+specify those dependencies. If we decide to solve this problem, we may want to\n+look to modern build systems for inspiration on how to manage dependencies and\n+parallel tasks.\n+\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 one-line hooks like this if they were configured outside of the local\n+scope; or another approach, like a list of safe projects, might be useful. It\n+may also be sufficient (or at least useful) to teach a `hook.disableAll` config\n+or similar flag to the Git executable.\n+\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+== 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.27.0.rc0.183.gde8f92d652-goog\n\n"},{"id":"398386","messageId":"20200521185414.43760-3-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200521185414.43760-1-emilyshaffer@google.com","subject":"[PATCH v2 2/4] hook: scaffolding for git-hook subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-05-21T18:54:12Z","receivedAt":"2020-05-21T18:54:31Z","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---\n .gitignore                    |  1 +\n Documentation/git-hook.txt    | 19 +++++++++++++++++++\n Makefile                      |  1 +\n builtin.h                     |  1 +\n builtin/hook.c                | 21 +++++++++++++++++++++\n git.c                         |  1 +\n t/t1360-config-based-hooks.sh | 11 +++++++++++\n 7 files changed, 55 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 ee509a2ad2..0694a34884 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -75,6 +75,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..2d50c414cc\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,19 @@\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+You can list, add, and modify hooks with this command.\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex 3d3a39fc19..fce6ee154e 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1080,6 +1080,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 a5ae15bfe5..4e736499c0 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -157,6 +157,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/git.c b/git.c\nindex a2d337eed7..99372529a2 100644\n--- a/git.c\n+++ b/git.c\n@@ -517,6 +517,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/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.27.0.rc0.183.gde8f92d652-goog\n\n"},{"id":"398387","messageId":"20200521185414.43760-4-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200521185414.43760-1-emilyshaffer@google.com","subject":"[PATCH v2 3/4] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-05-21T18:54:13Z","receivedAt":"2020-05-21T18:54:34Z","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; at minimum, this subsection must 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  ~/baz/from/hookcmd.sh\n  ~/bar.sh\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt    | 37 +++++++++++++-\n Makefile                      |  1 +\n builtin/hook.c                | 55 +++++++++++++++++++--\n hook.c                        | 90 +++++++++++++++++++++++++++++++++++\n hook.h                        | 15 ++++++\n t/t1360-config-based-hooks.sh | 51 +++++++++++++++++++-\n 6 files changed, 242 insertions(+), 7 deletions(-)\n create mode 100644 hook.c\n create mode 100644 hook.h\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 2d50c414cc..e458586e96 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,12 +8,47 @@ git-hook - Manage configured hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook'\n+'git hook' list <hook-name>\n \n DESCRIPTION\n -----------\n You can list, 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 config order. \"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+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 note the config scope where the relevant\n+`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex fce6ee154e..b7bbf3be7b 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -894,6 +894,7 @@ LIB_OBJS += grep.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 += interdiff.o\n LIB_OBJS += json-writer.o\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex b2bbc84d4d..cfd8e388bd 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -1,21 +1,68 @@\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 hook *item;\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(\"a hookname must be provided to operate on.\",\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 (!head) {\n+\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n+\t\t       hookname.buf);\n+\t\treturn 0;\n+\t}\n+\n+\tlist_for_each(pos, head) {\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item)\n+\t\t\tprintf(\"%s:\\t%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();\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..9dfc1a885e\n--- /dev/null\n+++ b/hook.c\n@@ -0,0 +1,90 @@\n+#include \"cache.h\"\n+\n+#include \"hook.h\"\n+#include \"config.h\"\n+\n+static LIST_HEAD(hook_head);\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 emplace_hook(struct list_head *pos, const char *command)\n+{\n+\tstruct hook *to_add = malloc(sizeof(struct hook));\n+\tto_add->origin = current_config_scope();\n+\tstrbuf_init(&to_add->command, 0);\n+\tstrbuf_addstr(&to_add->command, command);\n+\n+\tlist_add_tail(&to_add->list, pos);\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(void)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, &hook_head)\n+\t\tremove_hook(pos);\n+}\n+\n+static int hook_config_lookup(const char *key, const char *value, void *hook_key_cb)\n+{\n+\tconst char *hook_key = hook_key_cb;\n+\n+\tif (!strcmp(key, hook_key)) {\n+\t\tconst char *command = value;\n+\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\t\tstruct list_head *pos = NULL, *tmp = NULL;\n+\n+\t\t/* Check if a hookcmd with that name exists. */\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\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\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\tlist_for_each_safe(pos, tmp, &hook_head) {\n+\t\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n+\t\t\t/*\n+\t\t\t * The list of hooks to run can be reordered by being redeclared\n+\t\t\t * in the config. Options about hook ordering should be checked\n+\t\t\t * here.\n+\t\t\t */\n+\t\t\tif (0 == strcmp(hook->command.buf, command))\n+\t\t\t\tremove_hook(pos);\n+\t\t}\n+\t\templace_hook(pos, command);\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+\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, (void*)hook_key.buf);\n+\n+\treturn &hook_head;\n+}\ndiff --git a/hook.h b/hook.h\nnew file mode 100644\nindex 0000000000..aaf6511cff\n--- /dev/null\n+++ b/hook.h\n@@ -0,0 +1,15 @@\n+#include \"config.h\"\n+#include \"list.h\"\n+#include \"strbuf.h\"\n+\n+struct hook\n+{\n+\tstruct list_head list;\n+\tenum config_scope origin;\n+\tstruct strbuf command;\n+};\n+\n+struct list_head* hook_list(const struct strbuf *hookname);\n+\n+void free_hook(struct hook *ptr);\n+void clear_hook_list(void);\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 34b0df5216..4e46d7dd4e 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -4,8 +4,55 @@ 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+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 'setup hooks in global, and local' '\n+\tgit config --add --local hook.pre-commit.command \"/path/ghi\" &&\n+\tgit config --add --global hook.pre-commit.command \"/path/def\"\n+'\n+\n+test_expect_success 'git hook list orders by config order' '\n+\tcat >expected <<-\\EOF &&\n+\tglobal:\t/path/def\n+\tlocal:\t/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+\tgit config --add --local hook.pre-commit.command \"abc\" &&\n+\tgit config --add --global hookcmd.abc.command \"/path/abc\" &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tglobal:\t/path/def\n+\tlocal:\t/path/ghi\n+\tlocal:\t/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+\tgit config --add --local hook.pre-commit.command \"/path/def\" &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tlocal:\t/path/ghi\n+\tlocal:\t/path/abc\n+\tlocal:\t/path/def\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual\n '\n \n test_done\n-- \n2.27.0.rc0.183.gde8f92d652-goog\n\n"},{"id":"398388","messageId":"20200521185414.43760-5-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200521185414.43760-1-emilyshaffer@google.com","subject":"[PATCH v2 4/4] hook: add --porcelain to list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-05-21T18:54:14Z","receivedAt":"2020-05-21T18:54:35Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach 'git hook list --porcelain <hookname>', which prints simply the\ncommands to be run in the order suggested by the config. This option is\nintended for use by user scripts, wrappers, or out-of-process Git\ncommands which still want to execute hooks. For example, the following\nsnippet might be added to git-send-email.perl to introduce a\n`pre-send-email` hook:\n\n  sub pre_send_email {\n    open(my $fh, 'git hook list --porcelain pre-send-email |');\n    chomp(my @hooks = <$fh>);\n    close($fh);\n\n    foreach $hook (@hooks) {\n            system $hook\n    }\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt    | 13 +++++++++++--\n builtin/hook.c                | 17 +++++++++++++----\n t/t1360-config-based-hooks.sh | 11 +++++++++++\n 3 files changed, 35 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex e458586e96..0854035ce2 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - Manage configured hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' list <hook-name>\n+'git hook' list [--porcelain] <hook-name>\n \n DESCRIPTION\n -----------\n@@ -43,11 +43,20 @@ Local config\n COMMANDS\n --------\n \n-list <hook-name>::\n+list [--porcelain] <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 note the config scope where the relevant\n `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n++\n+If `--porcelain` is specified, instead print the commands alone, separated by\n+newlines, for easy parsing by a script.\n+\n+OPTIONS\n+-------\n+--porcelain::\n+\tWith `list`, print the commands in the order they should be run,\n+\tseparated by newlines, for easy parsing by a script.\n \n GIT\n ---\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex cfd8e388bd..2e51c84c81 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -16,8 +16,11 @@ 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+\tint porcelain = 0;\n \n \tstruct option list_options[] = {\n+\t\tOPT_BOOL(0, \"porcelain\", &porcelain,\n+\t\t\t \"format for execution by a script\"),\n \t\tOPT_END(),\n \t};\n \n@@ -29,6 +32,8 @@ static int list(int argc, const char **argv, const char *prefix)\n \t\t\t      builtin_hook_usage, list_options);\n \t}\n \n+\n+\n \tstrbuf_addstr(&hookname, argv[0]);\n \n \thead = hook_list(&hookname);\n@@ -41,10 +46,14 @@ 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\tif (item)\n-\t\t\tprintf(\"%s:\\t%s\\n\",\n-\t\t\t       config_scope_name(item->origin),\n-\t\t\t       item->command.buf);\n+\t\tif (item) {\n+\t\t\tif (porcelain)\n+\t\t\t\tprintf(\"%s\\n\", item->command.buf);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s:\\t%s\\n\",\n+\t\t\t\t       config_scope_name(item->origin),\n+\t\t\t\t       item->command.buf);\n+\t\t}\n \t}\n \n \tclear_hook_list();\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 4e46d7dd4e..3296d8af45 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -55,4 +55,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 --porcelain prints just the command' '\n+\tcat >expected <<-\\EOF &&\n+\t/path/ghi\n+\t/path/abc\n+\t/path/def\n+\tEOF\n+\n+\tgit hook list --porcelain pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.27.0.rc0.183.gde8f92d652-goog\n\n"},{"id":"398414","messageId":"355b7780-0096-42a9-01df-387ec17cb206@gmail.com","threadId":"53527","inReplyTo":"20200521185414.43760-2-emilyshaffer@google.com","subject":"Re: [PATCH v2 1/4] doc: propose hooks managed by the config","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2020-05-22T10:13:07Z","receivedAt":"2020-05-22T10:13:19Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Emily\n\nThanks for working on this\n\nOn 21/05/2020 19:54, Emily Shaffer wrote:\n> Begin a design document for config-based hooks, managed via git-hook.\n> Focus on an overview of the implementation and motivation for design\n> decisions. Briefly discuss the alternatives considered before this\n> point. Also, attempt to redefine terms to fit into a multihook world.\n> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  Documentation/Makefile                        |   1 +\n>  .../technical/config-based-hooks.txt          | 320 ++++++++++++++++++\n>  2 files changed, 321 insertions(+)\n>  create mode 100644 Documentation/technical/config-based-hooks.txt\n> \n> diff --git a/Documentation/Makefile b/Documentation/Makefile\n> index 15d9d04f31..5b21f31d31 100644\n> --- a/Documentation/Makefile\n> +++ b/Documentation/Makefile\n> @@ -80,6 +80,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\n> diff --git a/Documentation/technical/config-based-hooks.txt b/Documentation/technical/config-based-hooks.txt\n> new file mode 100644\n> index 0000000000..59cdc25a47\n> --- /dev/null\n> +++ b/Documentation/technical/config-based-hooks.txt\n> @@ -0,0 +1,320 @@\n> +Configuration-based hook management\n> +===================================\n> +\n> +== Motivation\n> +\n> +Treat hooks as a first-class citizen by replacing the .git/hook/hookname path as\n> +the only source of hooks to execute, in a way which is friendly to users with\n> +multiple repos which have similar needs.\n> +\n> +Redefine \"hook\" as an event rather than a single script, allowing users to\n> +perform unrelated actions on a single event.\n> +\n> +Take a step closer to safety when copying zipped Git repositories from untrusted\n> +users.\n\nHaving read through this (admittedly fairly quickly) I'm not sure what\nthat step is\n\n> +\n> +Make it easier for users to discover Git's hook feature and automate their\n> +workflows.\n> +\n> +== User interfaces\n> +\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> +==== `hook`\n> +\n> +Primarily contains subsections for each hook event. These subsections define\n> +hook command execution order;\n\nMay be \"The order of these subsections define the hook command execution\norder\" ?\n\n> hook commands can be specified by passing the\n> +command directly if no additional configuration is needed, or by passing the\n> +name of a `hookcmd`.\n\nI know what you mean by \"passing\" but as this section is talking about\nconfig settings perhaps it should refer to the keys and values.\n\n> If Git does not find a `hookcmd` whose subsection matches\n> +the value of the given command string, Git will try to execute the string\n> +directly. Hooks are executed by passing the resolved command string to the\n> +shell.\n\nDo we really need to invoke the shell just to split a command-line and\nlook up the command in $PATH? If we used split_commandline() in alias.c\nthen we could avoid invoking this extra process for each hook command.\n\n> Hook event subsections can also contain per-hook-event settings.\n> +\n> +Also contains top-level hook execution settings, for example,\n> +`hook.warnHookDir`, `hook.runHookDir`, or `hook.disableAll`.\n\n(see sections ...) ? for the forward references to these settings?\n\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> +  error = ignore\n> +\n> +[hook]\n> +  runHookDir = interactive\n> +----\n> +\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 to which\n> +it's normally subscribed _except_ `pre-commit`.\n> +\n> +----\n> +[hookcmd \"perl-linter\"]\n> +  command = /usr/bin/lint-it --language=perl\n> +  skip = true\n> +  pre-commit-skip = false\n> +----\n> +\n> +=== Command-line API\n> +\n> +Users should be able to view, reorder, and create hook commands via the command\n> +line. External tools should be able to view a list of hooks in the correct order\n> +to run.\n> +\n> +*`git hook list <hook-event>`*\n> +\n> +*`git hook list (--system|--global|--local|--worktree)`*\n> +\n> +*`git hook edit <hook-event>`*\n> +\n> +*`git hook add <hook-command> <hook-event> <options...>`*\n> +\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`.\n\nrebase -i is not necessarily an exemplar of user interface design, what\nsort of thing do you have in mind?\n\n> +\n> +== Implementation\n> +\n> +=== Library\n> +\n> +`hook.c` and `hook.h` are responsible for interacting with the config files. In\n> +the case when the code generating a hook event doesn't have special concerns\n> +about how to run the hooks, the hook library will provide a basic API to call\n> +all hooks in config order with an `argv_array` provided by the code which\n> +generates the hook event:\n> +\n> +*`int run_hooks(const char *hookname, struct argv_array *args)`*\n> +\n> +This call includes the hook command provided by `run-command.h:find_hook()`;\n> +eventually, this legacy hook will be gated by a config `hook.runHookDir`. The\n> +config is checked against a number of cases:\n> +\n> +- \"no\": the legacy hook will not be run\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\".\n> +\n> +If the caller wants to do something more complicated, the hook library can also\n> +provide a callback API:\n> +\n> +*`int for_each_hookcmd(const char *hookname, hookcmd_function *cb)`*\n> +\n> +Finally, to facilitate the builtin, the library will also provide the following\n> +APIs to interact with the config:\n> +\n> +----\n> +int set_hook_commands(const char *hookname, struct string_list *commands,\n> +\tenum config_scope scope);\n> +int set_hookcmd(const char *hookcmd, struct hookcmd options);\n> +\n> +int list_hook_commands(const char *hookname, struct string_list *commands);\n> +int list_hooks_in_scope(enum config_scope scope, struct string_list *commands);\n> +----\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> +----\n> +struct hookcmd {\n> +  const char *name;\n> +  const char *command;\n> +\n> +  /* for illustration only; not planned at present */\n> +  int parallelizable;\n> +  const char *hookcmd_before;\n> +  const char *hookcmd_after;\n> +  enum recovery_action on_fail;\n> +}\n> +----\n> +\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 path\n> +\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> +\n> +`git hook list --porcelain <hook-event>` is implemented. Users can replace their\n> +`.git/hooks/<hook-event>` scripts with a trampoline based on `git hook list`'s\n> +output. Modifier commands like `git hook add` and `git hook edit` can be\n> +implemented around this time as well.\n> +\n> +==== Stage 2\n> +\n> +`hook.h:run_hooks()` is taught to include `run-command.h:find_hook()` at the\n> +end; calls to `find_hook()` are replaced with calls to `run_hooks()`. Users can\n> +opt-in to config-based hooks simply by creating some in their config; otherwise\n> +users should remain unaffected by the change.\n> +\n> +==== Stage 3\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 4\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> +\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. The\n> +goal is to avoid an overcomplicated design to work around a problem which has\n> +ceased to exist.\n> +\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.\n\nThat's an important point\n\n> Contributors with UX expertise are encouraged to\n> +share their suggestions.\n> +\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> +=== 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> +\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 table\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> +|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> +|No\n> +|No\n> +\n> +|Discoverability\n> +|Better (in `git help git`)\n> +|Same as before\n> +|Same as before\n> +\n> +|Hard to run unexpected hook\n> +|If configured\n> +|No\n> +|No\n> +|===\n> +\n> +== Future work\n> +\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> +\n> +Users with many hooks might want to run them simultaneously, if the hooks don't\n> +modify state; if one hook depends on another's output, then users will want to\n> +specify those dependencies. If we decide to solve this problem, we may want to\n> +look to modern build systems for inspiration on how to manage dependencies and\n> +parallel tasks.\n> +\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 one-line hooks like this if they were configured outside of the local\n> +scope;\n\nDoes \"disabling one-line hooks\" mean \"disable passing command line\narguments to the hook\"? I'm not sure that gains much security - can't I\njust set 'hook.pre-receive.command = ./delete-everything' and include\ndelete-everything in my malicious repo?\n\nBest Wishes\n\nPhillip\n\n> or another approach, like a list of safe projects, might be useful. It\n> +may also be sufficient (or at least useful) to teach a `hook.disableAll` config\n> +or similar flag to the Git executable.\n> +\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> +== 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> \n\n"},{"id":"398415","messageId":"41e4fcf3-5d39-71a2-35b2-ee143cd59033@gmail.com","threadId":"53527","inReplyTo":"20200521185414.43760-4-emilyshaffer@google.com","subject":"Re: [PATCH v2 3/4] hook: add list command","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2020-05-22T10:27:44Z","receivedAt":"2020-05-22T10:27:50Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Emily\n\nOn 21/05/2020 19:54, Emily Shaffer wrote:\n> Teach '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; at minimum, this subsection must 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>   ~/baz/from/hookcmd.sh\n>   ~/bar.sh\n> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  Documentation/git-hook.txt    | 37 +++++++++++++-\n>  Makefile                      |  1 +\n>  builtin/hook.c                | 55 +++++++++++++++++++--\n>  hook.c                        | 90 +++++++++++++++++++++++++++++++++++\n>  hook.h                        | 15 ++++++\n>  t/t1360-config-based-hooks.sh | 51 +++++++++++++++++++-\n>  6 files changed, 242 insertions(+), 7 deletions(-)\n>  create mode 100644 hook.c\n>  create mode 100644 hook.h\n> \n> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> index 2d50c414cc..e458586e96 100644\n> --- a/Documentation/git-hook.txt\n> +++ b/Documentation/git-hook.txt\n> @@ -8,12 +8,47 @@ git-hook - Manage configured hooks\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -'git hook'\n> +'git hook' list <hook-name>\n>  \n>  DESCRIPTION\n>  -----------\n>  You can list, 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 config order. \"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> +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 note the config scope where the relevant\n> +`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n> +\n>  GIT\n>  ---\n>  Part of the linkgit:git[1] suite\n> diff --git a/Makefile b/Makefile\n> index fce6ee154e..b7bbf3be7b 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -894,6 +894,7 @@ LIB_OBJS += grep.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 += interdiff.o\n>  LIB_OBJS += json-writer.o\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index b2bbc84d4d..cfd8e388bd 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -1,21 +1,68 @@\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 hook *item;\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(\"a hookname must be provided to operate on.\",\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 (!head) {\n> +\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> +\t\t       hookname.buf);\n> +\t\treturn 0;\n> +\t}\n> +\n> +\tlist_for_each(pos, head) {\n> +\t\titem = list_entry(pos, struct hook, list);\n> +\t\tif (item)\n> +\t\t\tprintf(\"%s:\\t%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();\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..9dfc1a885e\n> --- /dev/null\n> +++ b/hook.c\n> @@ -0,0 +1,90 @@\n> +#include \"cache.h\"\n> +\n> +#include \"hook.h\"\n> +#include \"config.h\"\n> +\n> +static LIST_HEAD(hook_head);\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 emplace_hook(struct list_head *pos, const char *command)\n> +{\n> +\tstruct hook *to_add = malloc(sizeof(struct hook));\n> +\tto_add->origin = current_config_scope();\n> +\tstrbuf_init(&to_add->command, 0);\n> +\tstrbuf_addstr(&to_add->command, command);\n> +\n> +\tlist_add_tail(&to_add->list, pos);\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(void)\n> +{\n> +\tstruct list_head *pos, *tmp;\n> +\tlist_for_each_safe(pos, tmp, &hook_head)\n> +\t\tremove_hook(pos);\n> +}\n> +\n> +static int hook_config_lookup(const char *key, const char *value, void *hook_key_cb)\n> +{\n> +\tconst char *hook_key = hook_key_cb;\n> +\n> +\tif (!strcmp(key, hook_key)) {\n> +\t\tconst char *command = value;\n> +\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n> +\t\tstruct list_head *pos = NULL, *tmp = NULL;\n> +\n> +\t\t/* Check if a hookcmd with that name exists. */\n> +\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n> +\t\tgit_config_get_value(hookcmd_name.buf, &command);\n\nThis looks dodgy to me. This code is called by git_config() as it parses\nthe config files, so it has not had a chance to fully populate the\nconfig cache used by git_config_get_value(). I think the test below\npasses because the hookcmd setting is set in the global file and the\nhook setting is set in the local file so when we have already parsed the\nhookcmd setting when we come to look it up. The same comment applies to\nthe hypothetical ordering config mentioned below. I think it would be\nbetter to collect the list of hook.<event>.command settings in this\ncallback and then look up any hookcmd settings for those hook commands\nafter we've finished reading all of the config files.\n\n> +\n> +\t\tif (!command)\n> +\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\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\tlist_for_each_safe(pos, tmp, &hook_head) {\n> +\t\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n> +\t\t\t/*\n> +\t\t\t * The list of hooks to run can be reordered by being redeclared\n> +\t\t\t * in the config. Options about hook ordering should be checked\n> +\t\t\t * here.\n> +\t\t\t */\n> +\t\t\tif (0 == strcmp(hook->command.buf, command))\n> +\t\t\t\tremove_hook(pos);\n> +\t\t}\n> +\t\templace_hook(pos, command);\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> +\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, (void*)hook_key.buf);\n> +\n> +\treturn &hook_head;\n> +}\n> diff --git a/hook.h b/hook.h\n> new file mode 100644\n> index 0000000000..aaf6511cff\n> --- /dev/null\n> +++ b/hook.h\n> @@ -0,0 +1,15 @@\n> +#include \"config.h\"\n> +#include \"list.h\"\n> +#include \"strbuf.h\"\n> +\n> +struct hook\n> +{\n> +\tstruct list_head list;\n> +\tenum config_scope origin;\n> +\tstruct strbuf command;\n> +};\n> +\n> +struct list_head* hook_list(const struct strbuf *hookname);\n> +\n> +void free_hook(struct hook *ptr);\n> +void clear_hook_list(void);\n> diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n> index 34b0df5216..4e46d7dd4e 100755\n> --- a/t/t1360-config-based-hooks.sh\n> +++ b/t/t1360-config-based-hooks.sh\n> @@ -4,8 +4,55 @@ 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> +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 'setup hooks in global, and local' '\n> +\tgit config --add --local hook.pre-commit.command \"/path/ghi\" &&\n\nCan I make a plea for the use of test_config please. Writing tests which\nrely on previous tests for their set-up creates a chain of hidden\ndependencies that make it hard to add/alter tests later or run a subset\nof the tests when developing a new patch. t3404-rebase-interactive.sh is\na prime example of this and I dread touching it.\n\n> +\tgit config --add --global hook.pre-commit.command \"/path/def\"\n> +'\n> +\n> +test_expect_success 'git hook list orders by config order' '\n> +\tcat >expected <<-\\EOF &&\n> +\tglobal:\t/path/def\n> +\tlocal:\t/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> +\tgit config --add --local hook.pre-commit.command \"abc\" &&\n> +\tgit config --add --global hookcmd.abc.command \"/path/abc\" &&\n> +\n> +\tcat >expected <<-\\EOF &&\n> +\tglobal:\t/path/def\n> +\tlocal:\t/path/ghi\n> +\tlocal:\t/path/abc\n\nWe should make it clear in the documentation that the config origin\napplies to the hook setting, even though we display the hookcmd command\nwhich is set globally here for the last hook.\n\nBest Wishes\n\nPhillip\n\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> +\tgit config --add --local hook.pre-commit.command \"/path/def\" &&\n> +\n> +\tcat >expected <<-\\EOF &&\n> +\tlocal:\t/path/ghi\n> +\tlocal:\t/path/abc\n> +\tlocal:\t/path/def\n> +\tEOF\n> +\n> +\tgit hook list pre-commit >actual &&\n> +\ttest_cmp expected actual\n>  '\n>  \n>  test_done\n> \n\n"},{"id":"398486","messageId":"nycvar.QRO.7.76.6.2005250055180.56@tvgsbejvaqbjf.bet","threadId":"53527","inReplyTo":"20200521185414.43760-4-emilyshaffer@google.com","subject":"Re: [PATCH v2 3/4] hook: add list command","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2020-05-24T23:00:03Z","receivedAt":"2020-05-25T13:16:56Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Emily,\n\nOn Thu, 21 May 2020, Emily Shaffer wrote:\n\n> diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n> index 34b0df5216..4e46d7dd4e 100755\n> --- a/t/t1360-config-based-hooks.sh\n> +++ b/t/t1360-config-based-hooks.sh\n> @@ -4,8 +4,55 @@ 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> +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 'setup hooks in global, and local' '\n> +\tgit config --add --local hook.pre-commit.command \"/path/ghi\" &&\n> +\tgit config --add --global hook.pre-commit.command \"/path/def\"\n> +'\n> +\n> +test_expect_success 'git hook list orders by config order' '\n> +\tcat >expected <<-\\EOF &&\n> +\tglobal:\t/path/def\n> +\tlocal:\t/path/ghi\n> +\tEOF\n> +\n> +\tgit hook list pre-commit >actual &&\n> +\ttest_cmp expected actual\n\nThis, as well as the next two test cases, won't work on Windows, as you\nalmost certainly realized from looking at the failed GitHub workflow run\nof your branch.\n\nThe reason is that Unix-like absolute paths like `/path/def` do _not_ do\nwhat you think on Windows: they are relative to the MSYS2 root (because\nthe shell script runs in an MSYS2 Bash). The Git executable, however, has\nnot the slightest idea about MSYS2 and does not handle those. To remedy\nthat, the MSYS2 Bash prefixes those paths with the absolute\n_Windows-style_ path when passing them to `git.exe` (in your case,\nactually in the `setup hooks` test case above).\n\nSo you will need to squash this (or an equivalent fix) into your patch:\n\n-- snip --\nFrom f2568d47509130a9c35590d907797d2eb813ac0d Mon Sep 17 00:00:00 2001\nFrom: Johannes Schindelin <johannes.schindelin@gmx.de>\nDate: Mon, 25 May 2020 15:03:16 +0200\nSubject: [PATCH] fixup??? hook: add list command\n\nThis is needed to make the tests pass on Windows, where Unix-like\nabsolute paths are not what you think they are.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n t/t1360-config-based-hooks.sh | 39 +++++++++++++++++++++--------------\n 1 file changed, 24 insertions(+), 15 deletions(-)\n\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 3296d8af4587..c862655fd4d9 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -18,10 +18,19 @@ test_expect_success 'setup hooks in global, and local' '\n \tgit config --add --global hook.pre-commit.command \"/path/def\"\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 test_expect_success 'git hook list orders by config order' '\n-\tcat >expected <<-\\EOF &&\n-\tglobal:\t/path/def\n-\tlocal:\t/path/ghi\n+\tcat >expected <<-EOF &&\n+\tglobal:\t$ROOT/path/def\n+\tlocal:\t$ROOT/path/ghi\n \tEOF\n\n \tgit hook list pre-commit >actual &&\n@@ -32,10 +41,10 @@ test_expect_success 'git hook list dereferences a hookcmd' '\n \tgit config --add --local hook.pre-commit.command \"abc\" &&\n \tgit config --add --global hookcmd.abc.command \"/path/abc\" &&\n\n-\tcat >expected <<-\\EOF &&\n-\tglobal:\t/path/def\n-\tlocal:\t/path/ghi\n-\tlocal:\t/path/abc\n+\tcat >expected <<-EOF &&\n+\tglobal:\t$ROOT/path/def\n+\tlocal:\t$ROOT/path/ghi\n+\tlocal:\t$ROOT/path/abc\n \tEOF\n\n \tgit hook list pre-commit >actual &&\n@@ -45,10 +54,10 @@ test_expect_success 'git hook list dereferences a hookcmd' '\n test_expect_success 'git hook list reorders on duplicate commands' '\n \tgit config --add --local hook.pre-commit.command \"/path/def\" &&\n\n-\tcat >expected <<-\\EOF &&\n-\tlocal:\t/path/ghi\n-\tlocal:\t/path/abc\n-\tlocal:\t/path/def\n+\tcat >expected <<-EOF &&\n+\tlocal:\t$ROOT/path/ghi\n+\tlocal:\t$ROOT/path/abc\n+\tlocal:\t$ROOT/path/def\n \tEOF\n\n \tgit hook list pre-commit >actual &&\n@@ -56,10 +65,10 @@ test_expect_success 'git hook list reorders on duplicate commands' '\n '\n\n test_expect_success 'git hook list --porcelain prints just the command' '\n-\tcat >expected <<-\\EOF &&\n-\t/path/ghi\n-\t/path/abc\n-\t/path/def\n+\tcat >expected <<-EOF &&\n+\t$ROOT/path/ghi\n+\t$ROOT/path/abc\n+\t$ROOT/path/def\n \tEOF\n\n \tgit hook list --porcelain pre-commit >actual &&\n--\n2.27.0.rc1.windows.1\n\n-- snap --\n\nCiao,\nDscho\n\n> +'\n> +\n> +test_expect_success 'git hook list dereferences a hookcmd' '\n> +\tgit config --add --local hook.pre-commit.command \"abc\" &&\n> +\tgit config --add --global hookcmd.abc.command \"/path/abc\" &&\n> +\n> +\tcat >expected <<-\\EOF &&\n> +\tglobal:\t/path/def\n> +\tlocal:\t/path/ghi\n> +\tlocal:\t/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> +\tgit config --add --local hook.pre-commit.command \"/path/def\" &&\n> +\n> +\tcat >expected <<-\\EOF &&\n> +\tlocal:\t/path/ghi\n> +\tlocal:\t/path/abc\n> +\tlocal:\t/path/def\n> +\tEOF\n> +\n> +\tgit hook list pre-commit >actual &&\n> +\ttest_cmp expected actual\n>  '\n>\n>  test_done\n> --\n> 2.27.0.rc0.183.gde8f92d652-goog\n>\n>\n>\n"},{"id":"398487","messageId":"nycvar.QRO.7.76.6.2005250053080.56@tvgsbejvaqbjf.bet","threadId":"53527","inReplyTo":"20200521185414.43760-5-emilyshaffer@google.com","subject":"Re: [PATCH v2 4/4] hook: add --porcelain to list command","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2020-05-24T23:00:17Z","receivedAt":"2020-05-25T13:17:08Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Emily,\n\nOn Thu, 21 May 2020, Emily Shaffer wrote:\n\n> diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n> index 4e46d7dd4e..3296d8af45 100755\n> --- a/t/t1360-config-based-hooks.sh\n> +++ b/t/t1360-config-based-hooks.sh\n> @@ -55,4 +55,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 --porcelain prints just the command' '\n> +\tcat >expected <<-\\EOF &&\n> +\t/path/ghi\n> +\t/path/abc\n> +\t/path/def\n> +\tEOF\n> +\n> +\tgit hook list --porcelain pre-commit >actual &&\n> +\ttest_cmp expected actual\n> +'\n\nAs you surely found out from the GitHub workflow running in your fork,\nthis does not work on Windows. I need this (and strongly suggest you\nsquash that into your patch):\n\n-- snipsnap --\nFrom 97e3dfa6155785363c881ce2dcaf4f5ddead83ed Mon Sep 17 00:00:00 2001\nFrom: Johannes Schindelin <johannes.schindelin@gmx.de>\nDate: Mon, 25 May 2020 15:04:24 +0200\nSubject: [PATCH] fixup??? hook: add --porcelain to list command\n\nThis is required to let the test pass on Windows, where Git reports\nWindows-style absolute paths and has no idea about the pseudo Unix\nabsolute paths that the Bash knows about.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n t/t1360-config-based-hooks.sh | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex c862655fd4d9..fce7335e97b9 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -65,10 +65,10 @@ test_expect_success 'git hook list reorders on duplicate commands' '\n '\n\n test_expect_success 'git hook list --porcelain prints just the command' '\n-\tcat >expected <<-EOF &&\n-\t$ROOT/path/ghi\n-\t$ROOT/path/abc\n-\t$ROOT/path/def\n+\tcat >expected <<-\\EOF &&\n+\t/path/ghi\n+\t/path/abc\n+\t/path/def\n \tEOF\n\n \tgit hook list --porcelain pre-commit >actual &&\n--\n2.27.0.rc1.windows.1\n\n"},{"id":"398489","messageId":"nycvar.QRO.7.76.6.2005250228080.56@tvgsbejvaqbjf.bet","threadId":"53527","inReplyTo":"nycvar.QRO.7.76.6.2005250053080.56@tvgsbejvaqbjf.bet","subject":"Re: [PATCH v2 4/4] hook: add --porcelain to list command","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2020-05-25T00:29:07Z","receivedAt":"2020-05-25T14:45:57Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Emily,\n\nOn Mon, 25 May 2020, Johannes Schindelin wrote:\n\n> Hi Emily,\n>\n> On Thu, 21 May 2020, Emily Shaffer wrote:\n>\n> > diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n> > index 4e46d7dd4e..3296d8af45 100755\n> > --- a/t/t1360-config-based-hooks.sh\n> > +++ b/t/t1360-config-based-hooks.sh\n> > @@ -55,4 +55,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 --porcelain prints just the command' '\n> > +\tcat >expected <<-\\EOF &&\n> > +\t/path/ghi\n> > +\t/path/abc\n> > +\t/path/def\n> > +\tEOF\n> > +\n> > +\tgit hook list --porcelain pre-commit >actual &&\n> > +\ttest_cmp expected actual\n> > +'\n>\n> As you surely found out from the GitHub workflow running in your fork,\n> this does not work on Windows. I need this (and strongly suggest you\n> squash that into your patch):\n>\n> -- snipsnap --\n> From 97e3dfa6155785363c881ce2dcaf4f5ddead83ed Mon Sep 17 00:00:00 2001\n> From: Johannes Schindelin <johannes.schindelin@gmx.de>\n> Date: Mon, 25 May 2020 15:04:24 +0200\n> Subject: [PATCH] fixup??? hook: add --porcelain to list command\n>\n> This is required to let the test pass on Windows, where Git reports\n> Windows-style absolute paths and has no idea about the pseudo Unix\n> absolute paths that the Bash knows about.\n>\n> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n> ---\n>  t/t1360-config-based-hooks.sh | 8 ++++----\n>  1 file changed, 4 insertions(+), 4 deletions(-)\n>\n> diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n> index c862655fd4d9..fce7335e97b9 100755\n> --- a/t/t1360-config-based-hooks.sh\n> +++ b/t/t1360-config-based-hooks.sh\n> @@ -65,10 +65,10 @@ test_expect_success 'git hook list reorders on duplicate commands' '\n>  '\n>\n>  test_expect_success 'git hook list --porcelain prints just the command' '\n> -\tcat >expected <<-EOF &&\n> -\t$ROOT/path/ghi\n> -\t$ROOT/path/abc\n> -\t$ROOT/path/def\n> +\tcat >expected <<-\\EOF &&\n> +\t/path/ghi\n> +\t/path/abc\n> +\t/path/def\n\nDue to an oversight on my part, this is actually the _reverse_ diff, and\nthe corresponding part in my mail answering your PATCH 3/4 should be\nskipped from that fixup. Sorry for that.\n\nCiao,\nDscho\n\n>  \tEOF\n>\n>  \tgit hook list --porcelain pre-commit >actual &&\n> --\n> 2.27.0.rc1.windows.1\n>\n>\n"},{"id":"398710","messageId":"20200527233749.GC196295@google.com","threadId":"53527","inReplyTo":"nycvar.QRO.7.76.6.2005250055180.56@tvgsbejvaqbjf.bet","subject":"Re: [PATCH v2 3/4] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-05-27T23:37:49Z","receivedAt":"2020-05-27T23:37:58Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Mon, May 25, 2020 at 01:00:03AM +0200, Johannes Schindelin wrote:\n> cc: git@vger.kernel.org\n> \n> Hi Emily,\n> \n> On Thu, 21 May 2020, Emily Shaffer wrote:\n> \n> > diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n> > index 34b0df5216..4e46d7dd4e 100755\n> > --- a/t/t1360-config-based-hooks.sh\n> > +++ b/t/t1360-config-based-hooks.sh\n> > @@ -4,8 +4,55 @@ 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> > +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 'setup hooks in global, and local' '\n> > +\tgit config --add --local hook.pre-commit.command \"/path/ghi\" &&\n> > +\tgit config --add --global hook.pre-commit.command \"/path/def\"\n> > +'\n> > +\n> > +test_expect_success 'git hook list orders by config order' '\n> > +\tcat >expected <<-\\EOF &&\n> > +\tglobal:\t/path/def\n> > +\tlocal:\t/path/ghi\n> > +\tEOF\n> > +\n> > +\tgit hook list pre-commit >actual &&\n> > +\ttest_cmp expected actual\n> \n> This, as well as the next two test cases, won't work on Windows, as you\n> almost certainly realized from looking at the failed GitHub workflow run\n> of your branch.\n\nThanks very much for sending this - to be honest, the failed workflow\nrun appeared to be because of the earlier SDK download issue, which I\nhave not rebased on top of a fix for yet, so I missed any actionable\nfailures when I ran the CI locally. I'll take it into account, much\nappreciated.\n\n - Emily\n"},{"id":"399365","messageId":"20200609202635.GC148632@google.com","threadId":"53527","inReplyTo":"355b7780-0096-42a9-01df-387ec17cb206@gmail.com","subject":"Re: [PATCH v2 1/4] doc: propose hooks managed by the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-06-09T20:26:35Z","receivedAt":"2020-06-09T20:26:44Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, May 22, 2020 at 11:13:07AM +0100, Phillip Wood wrote:\n> \n> Hi Emily\n> \n> Thanks for working on this\n> \n> On 21/05/2020 19:54, Emily Shaffer wrote:\n> > Begin a design document for config-based hooks, managed via git-hook.\n> > Focus on an overview of the implementation and motivation for design\n> > decisions. Briefly discuss the alternatives considered before this\n> > point. Also, attempt to redefine terms to fit into a multihook world.\n> > \n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >  Documentation/Makefile                        |   1 +\n> >  .../technical/config-based-hooks.txt          | 320 ++++++++++++++++++\n> >  2 files changed, 321 insertions(+)\n> >  create mode 100644 Documentation/technical/config-based-hooks.txt\n> > \n> > diff --git a/Documentation/Makefile b/Documentation/Makefile\n> > index 15d9d04f31..5b21f31d31 100644\n> > --- a/Documentation/Makefile\n> > +++ b/Documentation/Makefile\n> > @@ -80,6 +80,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\n> > diff --git a/Documentation/technical/config-based-hooks.txt b/Documentation/technical/config-based-hooks.txt\n> > new file mode 100644\n> > index 0000000000..59cdc25a47\n> > --- /dev/null\n> > +++ b/Documentation/technical/config-based-hooks.txt\n> > @@ -0,0 +1,320 @@\n> > +Configuration-based hook management\n> > +===================================\n> > +\n> > +== Motivation\n> > +\n> > +Treat hooks as a first-class citizen by replacing the .git/hook/hookname path as\n> > +the only source of hooks to execute, in a way which is friendly to users with\n> > +multiple repos which have similar needs.\n> > +\n> > +Redefine \"hook\" as an event rather than a single script, allowing users to\n> > +perform unrelated actions on a single event.\n> > +\n> > +Take a step closer to safety when copying zipped Git repositories from untrusted\n> > +users.\n> \n> Having read through this (admittedly fairly quickly) I'm not sure what\n> that step is\n\nOk, I'll try to clarify it a little here.\n\n> \n> > +\n> > +Make it easier for users to discover Git's hook feature and automate their\n> > +workflows.\n> > +\n> > +== User interfaces\n> > +\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> > +==== `hook`\n> > +\n> > +Primarily contains subsections for each hook event. These subsections define\n> > +hook command execution order;\n> \n> May be \"The order of these subsections define the hook command execution\n> order\" ?\n\nNice. Took it verbatim.\n\n> \n> > hook commands can be specified by passing the\n> > +command directly if no additional configuration is needed, or by passing the\n> > +name of a `hookcmd`.\n> \n> I know what you mean by \"passing\" but as this section is talking about\n> config settings perhaps it should refer to the keys and values.\n\nSure.\n\n> \n> > If Git does not find a `hookcmd` whose subsection matches\n> > +the value of the given command string, Git will try to execute the string\n> > +directly. Hooks are executed by passing the resolved command string to the\n> > +shell.\n> \n> Do we really need to invoke the shell just to split a command-line and\n> look up the command in $PATH? If we used split_commandline() in alias.c\n> then we could avoid invoking this extra process for each hook command.\n\nI'll want to experiment a little bit with this and figure out what works\nbest - you may be right, and I could also be wrong about platform\ncompatibility doing it the way I described. I haven't written this bit\nyet - so I'd like to update this section of the design doc when I get to\nthe implementation, so that it matches.\n\n> \n> > Hook event subsections can also contain per-hook-event settings.\n> > +\n> > +Also contains top-level hook execution settings, for example,\n> > +`hook.warnHookDir`, `hook.runHookDir`, or `hook.disableAll`.\n> \n> (see sections ...) ? for the forward references to these settings?\n\nSure. I think the best way to do this is if I use anchors for all the\nsections; this works without me specifying it in Asciidoctor but needs\nto be explicitly specified in Asciidoc. So I'll make sure to include\nthat with the next iteration.\n\n> \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> > +  error = ignore\n> > +\n> > +[hook]\n> > +  runHookDir = interactive\n> > +----\n> > +\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 to which\n> > +it's normally subscribed _except_ `pre-commit`.\n> > +\n> > +----\n> > +[hookcmd \"perl-linter\"]\n> > +  command = /usr/bin/lint-it --language=perl\n> > +  skip = true\n> > +  pre-commit-skip = false\n> > +----\n> > +\n> > +=== Command-line API\n> > +\n> > +Users should be able to view, reorder, and create hook commands via the command\n> > +line. External tools should be able to view a list of hooks in the correct order\n> > +to run.\n> > +\n> > +*`git hook list <hook-event>`*\n> > +\n> > +*`git hook list (--system|--global|--local|--worktree)`*\n> > +\n> > +*`git hook edit <hook-event>`*\n> > +\n> > +*`git hook add <hook-command> <hook-event> <options...>`*\n> > +\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`.\n> \n> rebase -i is not necessarily an exemplar of user interface design, what\n> sort of thing do you have in mind?\n\nThanks for patience on this - I didn't really have a clear idea before\nwhen I wrote the doc because I don't have much expertise in user\ninterfaces. However, since then I worked with some UX experts here, so\nI'll make a better writeup in the next iteration - I've got a much\nclearer idea of how that should look, now.\n\n> \n> > +\n> > +== Implementation\n> > +\n> > +=== Library\n> > +\n> > +`hook.c` and `hook.h` are responsible for interacting with the config files. In\n> > +the case when the code generating a hook event doesn't have special concerns\n> > +about how to run the hooks, the hook library will provide a basic API to call\n> > +all hooks in config order with an `argv_array` provided by the code which\n> > +generates the hook event:\n> > +\n> > +*`int run_hooks(const char *hookname, struct argv_array *args)`*\n> > +\n> > +This call includes the hook command provided by `run-command.h:find_hook()`;\n> > +eventually, this legacy hook will be gated by a config `hook.runHookDir`. The\n> > +config is checked against a number of cases:\n> > +\n> > +- \"no\": the legacy hook will not be run\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\".\n> > +\n> > +If the caller wants to do something more complicated, the hook library can also\n> > +provide a callback API:\n> > +\n> > +*`int for_each_hookcmd(const char *hookname, hookcmd_function *cb)`*\n> > +\n> > +Finally, to facilitate the builtin, the library will also provide the following\n> > +APIs to interact with the config:\n> > +\n> > +----\n> > +int set_hook_commands(const char *hookname, struct string_list *commands,\n> > +\tenum config_scope scope);\n> > +int set_hookcmd(const char *hookcmd, struct hookcmd options);\n> > +\n> > +int list_hook_commands(const char *hookname, struct string_list *commands);\n> > +int list_hooks_in_scope(enum config_scope scope, struct string_list *commands);\n> > +----\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> > +----\n> > +struct hookcmd {\n> > +  const char *name;\n> > +  const char *command;\n> > +\n> > +  /* for illustration only; not planned at present */\n> > +  int parallelizable;\n> > +  const char *hookcmd_before;\n> > +  const char *hookcmd_after;\n> > +  enum recovery_action on_fail;\n> > +}\n> > +----\n> > +\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 path\n> > +\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> > +\n> > +`git hook list --porcelain <hook-event>` is implemented. Users can replace their\n> > +`.git/hooks/<hook-event>` scripts with a trampoline based on `git hook list`'s\n> > +output. Modifier commands like `git hook add` and `git hook edit` can be\n> > +implemented around this time as well.\n> > +\n> > +==== Stage 2\n> > +\n> > +`hook.h:run_hooks()` is taught to include `run-command.h:find_hook()` at the\n> > +end; calls to `find_hook()` are replaced with calls to `run_hooks()`. Users can\n> > +opt-in to config-based hooks simply by creating some in their config; otherwise\n> > +users should remain unaffected by the change.\n> > +\n> > +==== Stage 3\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 4\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> > +\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. The\n> > +goal is to avoid an overcomplicated design to work around a problem which has\n> > +ceased to exist.\n> > +\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.\n> \n> That's an important point\n> \n> > Contributors with UX expertise are encouraged to\n> > +share their suggestions.\n> > +\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> > +=== 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> > +\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 table\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> > +|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> > +|No\n> > +|No\n> > +\n> > +|Discoverability\n> > +|Better (in `git help git`)\n> > +|Same as before\n> > +|Same as before\n> > +\n> > +|Hard to run unexpected hook\n> > +|If configured\n> > +|No\n> > +|No\n> > +|===\n> > +\n> > +== Future work\n> > +\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> > +\n> > +Users with many hooks might want to run them simultaneously, if the hooks don't\n> > +modify state; if one hook depends on another's output, then users will want to\n> > +specify those dependencies. If we decide to solve this problem, we may want to\n> > +look to modern build systems for inspiration on how to manage dependencies and\n> > +parallel tasks.\n> > +\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 one-line hooks like this if they were configured outside of the local\n> > +scope;\n> \n> Does \"disabling one-line hooks\" mean \"disable passing command line\n> arguments to the hook\"? I'm not sure that gains much security - can't I\n> just set 'hook.pre-receive.command = ./delete-everything' and include\n> delete-everything in my malicious repo?\n\nNo, I meant something more along the lines of:\n\n- hookcmds cannot be specified at the repo/worktree level\n- hook.pre-receive.command's value *must* be a hookcmd name\n\nI'll try to make that more clear next round.\n\nThanks for reading.\n - Emily\n\n> > or another approach, like a list of safe projects, might be useful. It\n> > +may also be sufficient (or at least useful) to teach a `hook.disableAll` config\n> > +or similar flag to the Git executable.\n> > +\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> > +== 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> > \n> \n"},{"id":"399370","messageId":"20200609214926.GD148632@google.com","threadId":"53527","inReplyTo":"41e4fcf3-5d39-71a2-35b2-ee143cd59033@gmail.com","subject":"Re: [PATCH v2 3/4] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-06-09T21:49:26Z","receivedAt":"2020-06-09T21:49:37Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, May 22, 2020 at 11:27:44AM +0100, Phillip Wood wrote:\n> \n> Hi Emily\n> \n> On 21/05/2020 19:54, Emily Shaffer wrote:\n> > Teach '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; at minimum, this subsection must 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> >   ~/baz/from/hookcmd.sh\n> >   ~/bar.sh\n> > \n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >  Documentation/git-hook.txt    | 37 +++++++++++++-\n> >  Makefile                      |  1 +\n> >  builtin/hook.c                | 55 +++++++++++++++++++--\n> >  hook.c                        | 90 +++++++++++++++++++++++++++++++++++\n> >  hook.h                        | 15 ++++++\n> >  t/t1360-config-based-hooks.sh | 51 +++++++++++++++++++-\n> >  6 files changed, 242 insertions(+), 7 deletions(-)\n> >  create mode 100644 hook.c\n> >  create mode 100644 hook.h\n> > \n> > diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> > index 2d50c414cc..e458586e96 100644\n> > --- a/Documentation/git-hook.txt\n> > +++ b/Documentation/git-hook.txt\n> > @@ -8,12 +8,47 @@ git-hook - Manage configured hooks\n> >  SYNOPSIS\n> >  --------\n> >  [verse]\n> > -'git hook'\n> > +'git hook' list <hook-name>\n> >  \n> >  DESCRIPTION\n> >  -----------\n> >  You can list, 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 config order. \"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> > +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 note the config scope where the relevant\n> > +`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n> > +\n> >  GIT\n> >  ---\n> >  Part of the linkgit:git[1] suite\n> > diff --git a/Makefile b/Makefile\n> > index fce6ee154e..b7bbf3be7b 100644\n> > --- a/Makefile\n> > +++ b/Makefile\n> > @@ -894,6 +894,7 @@ LIB_OBJS += grep.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 += interdiff.o\n> >  LIB_OBJS += json-writer.o\n> > diff --git a/builtin/hook.c b/builtin/hook.c\n> > index b2bbc84d4d..cfd8e388bd 100644\n> > --- a/builtin/hook.c\n> > +++ b/builtin/hook.c\n> > @@ -1,21 +1,68 @@\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 hook *item;\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(\"a hookname must be provided to operate on.\",\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 (!head) {\n> > +\t\tprintf(_(\"no commands configured for hook '%s'\\n\"),\n> > +\t\t       hookname.buf);\n> > +\t\treturn 0;\n> > +\t}\n> > +\n> > +\tlist_for_each(pos, head) {\n> > +\t\titem = list_entry(pos, struct hook, list);\n> > +\t\tif (item)\n> > +\t\t\tprintf(\"%s:\\t%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();\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..9dfc1a885e\n> > --- /dev/null\n> > +++ b/hook.c\n> > @@ -0,0 +1,90 @@\n> > +#include \"cache.h\"\n> > +\n> > +#include \"hook.h\"\n> > +#include \"config.h\"\n> > +\n> > +static LIST_HEAD(hook_head);\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 emplace_hook(struct list_head *pos, const char *command)\n> > +{\n> > +\tstruct hook *to_add = malloc(sizeof(struct hook));\n> > +\tto_add->origin = current_config_scope();\n> > +\tstrbuf_init(&to_add->command, 0);\n> > +\tstrbuf_addstr(&to_add->command, command);\n> > +\n> > +\tlist_add_tail(&to_add->list, pos);\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(void)\n> > +{\n> > +\tstruct list_head *pos, *tmp;\n> > +\tlist_for_each_safe(pos, tmp, &hook_head)\n> > +\t\tremove_hook(pos);\n> > +}\n> > +\n> > +static int hook_config_lookup(const char *key, const char *value, void *hook_key_cb)\n> > +{\n> > +\tconst char *hook_key = hook_key_cb;\n> > +\n> > +\tif (!strcmp(key, hook_key)) {\n> > +\t\tconst char *command = value;\n> > +\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n> > +\t\tstruct list_head *pos = NULL, *tmp = NULL;\n> > +\n> > +\t\t/* Check if a hookcmd with that name exists. */\n> > +\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n> > +\t\tgit_config_get_value(hookcmd_name.buf, &command);\n> \n> This looks dodgy to me. This code is called by git_config() as it parses\n> the config files, so it has not had a chance to fully populate the\n> config cache used by git_config_get_value(). I think the test below\n> passes because the hookcmd setting is set in the global file and the\n> hook setting is set in the local file so when we have already parsed the\n> hookcmd setting when we come to look it up. The same comment applies to\n> the hypothetical ordering config mentioned below. I think it would be\n> better to collect the list of hook.<event>.command settings in this\n> callback and then look up any hookcmd settings for those hook commands\n> after we've finished reading all of the config files.\n\ngit_config_get_value() calls repo_read_config(the_repository) if the\nconfig hasn't been fully parsed yet, so I think what you're worrying\nabout is not an issue. It's ugly, I agree, but since the new hotness\n(git_config_get_value() and friends) doesn't offer the same\nfunctionality as the old solution (config origin) this seemed like an\nokay approach. As I understand it, moving this hookcmd lookup section\noutside of the config callback will save us up to one additional pass\nthrough the configs, at the expense of a more convoluted code path.\n\n> \n> > +\n> > +\t\tif (!command)\n> > +\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\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\tlist_for_each_safe(pos, tmp, &hook_head) {\n> > +\t\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n> > +\t\t\t/*\n> > +\t\t\t * The list of hooks to run can be reordered by being redeclared\n> > +\t\t\t * in the config. Options about hook ordering should be checked\n> > +\t\t\t * here.\n> > +\t\t\t */\n> > +\t\t\tif (0 == strcmp(hook->command.buf, command))\n> > +\t\t\t\tremove_hook(pos);\n> > +\t\t}\n> > +\t\templace_hook(pos, command);\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> > +\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, (void*)hook_key.buf);\n> > +\n> > +\treturn &hook_head;\n> > +}\n> > diff --git a/hook.h b/hook.h\n> > new file mode 100644\n> > index 0000000000..aaf6511cff\n> > --- /dev/null\n> > +++ b/hook.h\n> > @@ -0,0 +1,15 @@\n> > +#include \"config.h\"\n> > +#include \"list.h\"\n> > +#include \"strbuf.h\"\n> > +\n> > +struct hook\n> > +{\n> > +\tstruct list_head list;\n> > +\tenum config_scope origin;\n> > +\tstruct strbuf command;\n> > +};\n> > +\n> > +struct list_head* hook_list(const struct strbuf *hookname);\n> > +\n> > +void free_hook(struct hook *ptr);\n> > +void clear_hook_list(void);\n> > diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n> > index 34b0df5216..4e46d7dd4e 100755\n> > --- a/t/t1360-config-based-hooks.sh\n> > +++ b/t/t1360-config-based-hooks.sh\n> > @@ -4,8 +4,55 @@ 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> > +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 'setup hooks in global, and local' '\n> > +\tgit config --add --local hook.pre-commit.command \"/path/ghi\" &&\n> \n> Can I make a plea for the use of test_config please. Writing tests which\n> rely on previous tests for their set-up creates a chain of hidden\n> dependencies that make it hard to add/alter tests later or run a subset\n> of the tests when developing a new patch. t3404-rebase-interactive.sh is\n> a prime example of this and I dread touching it.\n\nSure. I'll redo them.\n\n> \n> > +\tgit config --add --global hook.pre-commit.command \"/path/def\"\n> > +'\n> > +\n> > +test_expect_success 'git hook list orders by config order' '\n> > +\tcat >expected <<-\\EOF &&\n> > +\tglobal:\t/path/def\n> > +\tlocal:\t/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> > +\tgit config --add --local hook.pre-commit.command \"abc\" &&\n> > +\tgit config --add --global hookcmd.abc.command \"/path/abc\" &&\n> > +\n> > +\tcat >expected <<-\\EOF &&\n> > +\tglobal:\t/path/def\n> > +\tlocal:\t/path/ghi\n> > +\tlocal:\t/path/abc\n> \n> We should make it clear in the documentation that the config origin\n> applies to the hook setting, even though we display the hookcmd command\n> which is set globally here for the last hook.\n\nOne of the suggestions from our UX team last week was to make this list\noutput clearer to indicate the origin of the command plus the origin of\nthe hookcmd object; I'll try to straighten this out and make sure the\ndoc agrees.\n\n - Emily\n"},{"id":"402255","messageId":"20200728222455.3023400-1-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200521185414.43760-1-emilyshaffer@google.com","subject":"[PATCH v3 0/6] propose config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-07-28T22:24:49Z","receivedAt":"2020-07-28T22:25:05Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Hi all,\n\nAfter taking a few weeks to work on other items, I've got another update\nto the config-based hook series. Patches 5 and 6 are RFC - a sketch of\nhow the hook library could run the appropriate set of hooks. There's\nmore work to do, which I'll outline later in the cover letter.\n\nSince last time, I took into account review comments, including Dscho's\nfixups to make the tests work in Windows. It seems those tests are\npassing now, according to the GH Actions run:\nhttps://github.com/nasamuffin/git/actions/runs/186242637\n\nOne thing I didn't decide on was the benefit of moving the hookcmd\nresolution outside of the hook config pass; that code is unchanged. I\nstill haven't decided quite which approach I like better, but it's still\non my mind.\n\nIn the 'run_hook()' implementation I flipped the 'use_shell' bit, which\nby my understanding only uses a shell if it can't find the command in\nPATH; this seems like a reasonable approach especially because the code\nis so brief, but I'm interested in hearing why I'm wrong or it won't\nwork well :)\n\nThere is still some work I've got locally which isn't quite ready:\n - support for hook.runHookDir. This is turning into a yak shave about\n   who decides where and when to display or run the hookdir hook. I\n   think I've got it mostly figured out and there's a patch locally, but\n   it's not polished.\n - Drafts for 'git hook add' and 'git hook edit'. These features are\n   probably the most complicated part of the series, but it's possible\n   to use config-based hooks without them. In the interest of getting\n   something out for people to try on their own, I'll probably leave\n   these for later.\n - Support for stdin redirection to hooks. Since this means we want to\n   point the same stdin to multiple processes, I'm thinking it will be\n   slightly complicated. Maybe someone has a hint for me? :) Without\n   having looked at what's available or not yet, I'm planning to do this\n   by reading the whole stdin to memory and then streaming it to each\n   process in turn, as I can't seek back to the beginning of the stream\n   when I start each new process.\n - Conversion of codebase to use the hook library instead. Partly, this\n   is gated on the previous point - there are plenty of callers who,\n   instead of using run-command's run_hook_*(), just use find_hook() and\n   roll their own struct child_process so they can use stdin/stdout. I\n   do plan to consider the hook lib's run_hooks() implementation as\n   non-final until I start this process - I'm expecting to learn more\n   about what I do and don't have to support when I do this.\n\nThanks, all. Hopefully I can do better than a 2-month wait for the\nseries after this one... although I imagine I cursed myself by saying\nthat. :)\n\n - Emily\n\n\nEmily Shaffer (6):\n  doc: propose hooks managed by the config\n  hook: scaffolding for git-hook subcommand\n  hook: add list command\n  hook: add --porcelain to list command\n  parse-options: parse into argv_array\n  hook: add 'run' subcommand\n\n .gitignore                                    |   1 +\n Documentation/Makefile                        |   1 +\n Documentation/git-hook.txt                    |  63 ++++\n Documentation/technical/api-parse-options.txt |   5 +\n .../technical/config-based-hooks.txt          | 354 ++++++++++++++++++\n Makefile                                      |   2 +\n builtin.h                                     |   1 +\n builtin/hook.c                                | 107 ++++++\n git.c                                         |   1 +\n hook.c                                        | 132 +++++++\n hook.h                                        |  18 +\n parse-options-cb.c                            |  16 +\n parse-options.h                               |   4 +\n t/t1360-config-based-hooks.sh                 | 115 ++++++\n 14 files changed, 820 insertions(+)\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\n-- \n2.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"402256","messageId":"20200728222455.3023400-2-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200728222455.3023400-1-emilyshaffer@google.com","subject":"[PATCH v3 1/6] doc: propose hooks managed by the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-07-28T22:24:50Z","receivedAt":"2020-07-28T22:25:07Z","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 Documentation/Makefile                        |   1 +\n .../technical/config-based-hooks.txt          | 354 ++++++++++++++++++\n 2 files changed, 355 insertions(+)\n create mode 100644 Documentation/technical/config-based-hooks.txt\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex ecd0b340b1..5483995113 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -80,6 +80,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..c6e762b192\n--- /dev/null\n+++ b/Documentation/technical/config-based-hooks.txt\n@@ -0,0 +1,354 @@\n+Configuration-based hook management\n+===================================\n+:sectanchors:\n+\n+[[motivation]]\n+== Motivation\n+\n+Treat hooks as a first-class citizen by replacing the .git/hook/hookname path as\n+the only source of hooks to execute, in a way which is friendly to users with\n+multiple repos which have similar needs.\n+\n+Redefine \"hook\" as an event rather than a single script, allowing users to\n+perform 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+[[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. These order of these\n+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. Hook event subsections can\n+also contain per-hook-event settings.\n+\n+Also contains top-level hook execution settings, for example,\n+`hook.warnHookDir`, `hook.runHookDir`, or `hook.disableAll`. (These settings are\n+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+  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 to which\n+it's normally subscribed _except_ `pre-commit`.\n+\n+----\n+[hookcmd \"perl-linter\"]\n+  command = /usr/bin/lint-it --language=perl\n+  skip = true\n+  pre-commit-skip = false\n+----\n+\n+[[command-line-api]]\n+=== Command-line API\n+\n+Users should be able to view, reorder, and create hook commands via the command\n+line. External tools should be able to view a list of hooks in the correct order\n+to run.\n+\n+*`git hook list <hook-event>`*\n+\n+*`git hook list (--system|--global|--local|--worktree)`*\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`.\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. In\n+the case when the code generating a hook event doesn't have special concerns\n+about how to run the hooks, the hook library will provide a basic API to call\n+all hooks in config order with an `argv_array` provided by the code which\n+generates the hook event:\n+\n+*`int run_hooks(const char *hookname, struct argv_array *args)`*\n+\n+This call includes the hook command provided by `run-command.h:find_hook()`;\n+eventually, this legacy hook will be gated by a config `hook.runHookDir`. The\n+config is checked against a number of cases:\n+\n+- \"no\": the legacy hook will not be run\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\".\n+\n+If the caller wants to do something more complicated, the hook library can also\n+provide a callback API:\n+\n+*`int for_each_hookcmd(const char *hookname, hookcmd_function *cb)`*\n+\n+Finally, to facilitate the builtin, the library will also provide the following\n+APIs to interact with the config:\n+\n+----\n+int set_hook_commands(const char *hookname, struct string_list *commands,\n+\tenum config_scope scope);\n+int set_hookcmd(const char *hookcmd, struct hookcmd options);\n+\n+int list_hook_commands(const char *hookname, struct string_list *commands);\n+int list_hooks_in_scope(enum config_scope scope, struct string_list *commands);\n+----\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+----\n+struct hookcmd {\n+  const char *name;\n+  const char *command;\n+\n+  /* for illustration only; not planned at present */\n+  int parallelizable;\n+  const char *hookcmd_before;\n+  const char *hookcmd_after;\n+  enum recovery_action on_fail;\n+}\n+----\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. Users can replace their\n+`.git/hooks/<hook-event>` scripts with a trampoline based on `git hook list`'s\n+output. Modifier commands like `git hook add` and `git hook edit` can be\n+implemented around this time as well.\n+\n+[[stage-2]]\n+==== Stage 2\n+\n+`hook.h:run_hooks()` is taught to include `run-command.h:find_hook()` at the\n+end; calls to `find_hook()` are replaced with calls to `run_hooks()`. Users can\n+opt-in to config-based hooks simply by creating some in their config; otherwise\n+users should remain unaffected by the change.\n+\n+[[stage-3]]\n+==== Stage 3\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-4]]\n+==== Stage 4\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. The\n+goal is to avoid an overcomplicated design to work around a problem which has\n+ceased to exist.\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+[[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+\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+|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+|No\n+|No\n+\n+|Discoverability\n+|Better (in `git help git`)\n+|Same as before\n+|Same as before\n+\n+|Hard to run unexpected hook\n+|If configured\n+|No\n+|No\n+|===\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\n+\n+Users with many hooks might want to run them simultaneously, if the hooks don't\n+modify state; if one hook depends on another's output, then users will want to\n+specify those dependencies. If we decide to solve this problem, we may want to\n+look to modern build systems for inspiration on how to manage dependencies and\n+parallel tasks.\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+[[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.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"402258","messageId":"20200728222455.3023400-3-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200728222455.3023400-1-emilyshaffer@google.com","subject":"[PATCH v3 2/6] hook: scaffolding for git-hook subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-07-28T22:24:51Z","receivedAt":"2020-07-28T22:25:09Z","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---\n .gitignore                    |  1 +\n Documentation/git-hook.txt    | 19 +++++++++++++++++++\n Makefile                      |  1 +\n builtin.h                     |  1 +\n builtin/hook.c                | 21 +++++++++++++++++++++\n git.c                         |  1 +\n t/t1360-config-based-hooks.sh | 11 +++++++++++\n 7 files changed, 55 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 ee509a2ad2..0694a34884 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -75,6 +75,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..2d50c414cc\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,19 @@\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+You can list, add, and modify hooks with this command.\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex 372139f1f2..e13e58e23f 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1077,6 +1077,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 a5ae15bfe5..4e736499c0 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -157,6 +157,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/git.c b/git.c\nindex 2f021b97f3..7f3328c63f 100644\n--- a/git.c\n+++ b/git.c\n@@ -517,6 +517,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/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.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"402260","messageId":"20200728222455.3023400-4-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200728222455.3023400-1-emilyshaffer@google.com","subject":"[PATCH v3 3/6] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-07-28T22:24:52Z","receivedAt":"2020-07-28T22:25:11Z","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; at minimum, this subsection must 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  ~/baz/from/hookcmd.sh\n  ~/bar.sh\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt    | 37 +++++++++++++-\n Makefile                      |  1 +\n builtin/hook.c                | 55 +++++++++++++++++++--\n hook.c                        | 90 +++++++++++++++++++++++++++++++++++\n hook.h                        | 15 ++++++\n t/t1360-config-based-hooks.sh | 68 +++++++++++++++++++++++++-\n 6 files changed, 259 insertions(+), 7 deletions(-)\n create mode 100644 hook.c\n create mode 100644 hook.h\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 2d50c414cc..e458586e96 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,12 +8,47 @@ git-hook - Manage configured hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook'\n+'git hook' list <hook-name>\n \n DESCRIPTION\n -----------\n You can list, 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 config order. \"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+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 note the config scope where the relevant\n+`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex e13e58e23f..50e7c911d1 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -891,6 +891,7 @@ LIB_OBJS += grep.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 += interdiff.o\n LIB_OBJS += json-writer.o\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex b2bbc84d4d..a0759a4c26 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -1,21 +1,68 @@\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 hook *item;\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(\"a hookname must be provided to operate on.\",\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\treturn 0;\n+\t}\n+\n+\tlist_for_each(pos, head) {\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item)\n+\t\t\tprintf(\"%s:\\t%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();\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..9dfc1a885e\n--- /dev/null\n+++ b/hook.c\n@@ -0,0 +1,90 @@\n+#include \"cache.h\"\n+\n+#include \"hook.h\"\n+#include \"config.h\"\n+\n+static LIST_HEAD(hook_head);\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 emplace_hook(struct list_head *pos, const char *command)\n+{\n+\tstruct hook *to_add = malloc(sizeof(struct hook));\n+\tto_add->origin = current_config_scope();\n+\tstrbuf_init(&to_add->command, 0);\n+\tstrbuf_addstr(&to_add->command, command);\n+\n+\tlist_add_tail(&to_add->list, pos);\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(void)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, &hook_head)\n+\t\tremove_hook(pos);\n+}\n+\n+static int hook_config_lookup(const char *key, const char *value, void *hook_key_cb)\n+{\n+\tconst char *hook_key = hook_key_cb;\n+\n+\tif (!strcmp(key, hook_key)) {\n+\t\tconst char *command = value;\n+\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\t\tstruct list_head *pos = NULL, *tmp = NULL;\n+\n+\t\t/* Check if a hookcmd with that name exists. */\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\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\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\tlist_for_each_safe(pos, tmp, &hook_head) {\n+\t\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n+\t\t\t/*\n+\t\t\t * The list of hooks to run can be reordered by being redeclared\n+\t\t\t * in the config. Options about hook ordering should be checked\n+\t\t\t * here.\n+\t\t\t */\n+\t\t\tif (0 == strcmp(hook->command.buf, command))\n+\t\t\t\tremove_hook(pos);\n+\t\t}\n+\t\templace_hook(pos, command);\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+\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, (void*)hook_key.buf);\n+\n+\treturn &hook_head;\n+}\ndiff --git a/hook.h b/hook.h\nnew file mode 100644\nindex 0000000000..aaf6511cff\n--- /dev/null\n+++ b/hook.h\n@@ -0,0 +1,15 @@\n+#include \"config.h\"\n+#include \"list.h\"\n+#include \"strbuf.h\"\n+\n+struct hook\n+{\n+\tstruct list_head list;\n+\tenum config_scope origin;\n+\tstruct strbuf command;\n+};\n+\n+struct list_head* hook_list(const struct strbuf *hookname);\n+\n+void free_hook(struct hook *ptr);\n+void clear_hook_list(void);\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 34b0df5216..46d1ed354a 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -4,8 +4,72 @@ 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 list orders by config order' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal:\t$ROOT/path/def\n+\tlocal:\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 dereferences a hookcmd' '\n+\tsetup_hooks &&\n+\tsetup_hookcmd &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal:\t$ROOT/path/def\n+\tlocal:\t$ROOT/path/ghi\n+\tlocal:\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+\ttest_config hook.pre-commit.command \"/path/def\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tlocal:\t$ROOT/path/ghi\n+\tlocal:\t$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.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"402261","messageId":"20200728222455.3023400-6-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200728222455.3023400-1-emilyshaffer@google.com","subject":"[RFC PATCH v3 5/6] parse-options: parse into argv_array","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-07-28T22:24:54Z","receivedAt":"2020-07-28T22:25:14Z","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 argv_array as a passthrough (that is, including the\nargument as well as its value). string_list and argv_array 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\nargv_array 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 argv_array 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 Documentation/technical/api-parse-options.txt |  5 +++++\n parse-options-cb.c                            | 16 ++++++++++++++++\n parse-options.h                               |  4 ++++\n 3 files changed, 25 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex 2e2e7c10c6..1e97343338 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -173,6 +173,11 @@ 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_ARGV_ARRAY(short, long, &struct argv_array, arg_str, description)`::\n+\tIntroduce an option with a string argument.\n+\tThe string argument is stored as an element in `argv_array`.\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 86cd393013..94c2dd397a 100644\n--- a/parse-options-cb.c\n+++ b/parse-options-cb.c\n@@ -205,6 +205,22 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset)\n \treturn 0;\n }\n \n+int parse_opt_argv_array(const struct option *opt, const char *arg, int unset)\n+{\n+\tstruct argv_array *v = opt->value;\n+\n+\tif (unset) {\n+\t\targv_array_clear(v);\n+\t\treturn 0;\n+\t}\n+\n+\tif (!arg)\n+\t\treturn -1;\n+\n+\targv_array_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 46af942093..e2e2de75c8 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_ARGV_ARRAY(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_argv_array }\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_argv_array(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.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"402257","messageId":"20200728222455.3023400-5-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200728222455.3023400-1-emilyshaffer@google.com","subject":"[PATCH v3 4/6] hook: add --porcelain to list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-07-28T22:24:53Z","receivedAt":"2020-07-28T22:25:15Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach 'git hook list --porcelain <hookname>', which prints simply the\ncommands to be run in the order suggested by the config. This option is\nintended for use by user scripts, wrappers, or out-of-process Git\ncommands which still want to execute hooks. For example, the following\nsnippet might be added to git-send-email.perl to introduce a\n`pre-send-email` hook:\n\n  sub pre_send_email {\n    open(my $fh, 'git hook list --porcelain pre-send-email |');\n    chomp(my @hooks = <$fh>);\n    close($fh);\n\n    foreach $hook (@hooks) {\n            system $hook\n    }\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt    | 13 +++++++++++--\n builtin/hook.c                | 17 +++++++++++++----\n t/t1360-config-based-hooks.sh | 12 ++++++++++++\n 3 files changed, 36 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex e458586e96..0854035ce2 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - Manage configured hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' list <hook-name>\n+'git hook' list [--porcelain] <hook-name>\n \n DESCRIPTION\n -----------\n@@ -43,11 +43,20 @@ Local config\n COMMANDS\n --------\n \n-list <hook-name>::\n+list [--porcelain] <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 note the config scope where the relevant\n `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n++\n+If `--porcelain` is specified, instead print the commands alone, separated by\n+newlines, for easy parsing by a script.\n+\n+OPTIONS\n+-------\n+--porcelain::\n+\tWith `list`, print the commands in the order they should be run,\n+\tseparated by newlines, for easy parsing by a script.\n \n GIT\n ---\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex a0759a4c26..0d92124ca6 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -16,8 +16,11 @@ 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+\tint porcelain = 0;\n \n \tstruct option list_options[] = {\n+\t\tOPT_BOOL(0, \"porcelain\", &porcelain,\n+\t\t\t \"format for execution by a script\"),\n \t\tOPT_END(),\n \t};\n \n@@ -29,6 +32,8 @@ static int list(int argc, const char **argv, const char *prefix)\n \t\t\t      builtin_hook_usage, list_options);\n \t}\n \n+\n+\n \tstrbuf_addstr(&hookname, argv[0]);\n \n \thead = hook_list(&hookname);\n@@ -41,10 +46,14 @@ 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\tif (item)\n-\t\t\tprintf(\"%s:\\t%s\\n\",\n-\t\t\t       config_scope_name(item->origin),\n-\t\t\t       item->command.buf);\n+\t\tif (item) {\n+\t\t\tif (porcelain)\n+\t\t\t\tprintf(\"%s\\n\", item->command.buf);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s:\\t%s\\n\",\n+\t\t\t\t       config_scope_name(item->origin),\n+\t\t\t\t       item->command.buf);\n+\t\t}\n \t}\n \n \tclear_hook_list();\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 46d1ed354a..ebf8f38d68 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -72,4 +72,16 @@ test_expect_success 'git hook list reorders on duplicate commands' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list --porcelain prints just the command' '\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 --porcelain pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"402259","messageId":"20200728222455.3023400-7-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200728222455.3023400-1-emilyshaffer@google.com","subject":"[RFC PATCH v3 6/6] hook: add 'run' subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-07-28T22:24:55Z","receivedAt":"2020-07-28T22:25:16Z","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 in config order, in series. As alternate\nordering or parallelism is supported in the future, we should add knobs\nto 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. For now, there is no way to disable them.\n\nUsers may wish to provide hook commands like 'git config\nhook.pre-commit.command \"~/linter.sh --pre-commit\"'. To enable this, the\ncontents of the 'hook.*.command' and 'hookcmd.*.command' strings are\nfirst split by space or quotes into an argv_array, then expanded with\n'expand_user_path()'.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/hook.c                | 30 +++++++++++++++++++++++++\n hook.c                        | 42 +++++++++++++++++++++++++++++++++++\n hook.h                        |  3 +++\n t/t1360-config-based-hooks.sh | 28 +++++++++++++++++++++++\n 4 files changed, 103 insertions(+)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 0d92124ca6..cd61fad5fb 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -5,9 +5,11 @@\n #include \"hook.h\"\n #include \"parse-options.h\"\n #include \"strbuf.h\"\n+#include \"argv-array.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@@ -62,6 +64,32 @@ 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 argv_array env_argv = ARGV_ARRAY_INIT;\n+\tstruct argv_array arg_argv = ARGV_ARRAY_INIT;\n+\n+\tstruct option run_options[] = {\n+\t\tOPT_ARGV_ARRAY('e', \"env\", &env_argv, N_(\"var\"),\n+\t\t\t       N_(\"environment variables for hook to use\")),\n+\t\tOPT_ARGV_ARRAY('a', \"arg\", &arg_argv, N_(\"args\"),\n+\t\t\t       N_(\"argument to pass to hook\")),\n+\t\tOPT_END(),\n+\t};\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(_(\"a hookname must be provided to operate on.\"),\n+\t\t\t      builtin_hook_usage, run_options);\n+\n+\tstrbuf_addstr(&hookname, argv[0]);\n+\n+\treturn run_hooks(env_argv.argv, &hookname, &arg_argv);\n+}\n+\n int cmd_hook(int argc, const char **argv, const char *prefix)\n {\n \tstruct option builtin_hook_options[] = {\n@@ -72,6 +100,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \n \tif (!strcmp(argv[1], \"list\"))\n \t\treturn list(argc - 1, argv + 1, prefix);\n+\tif (!strcmp(argv[1], \"run\"))\n+\t\treturn run(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\nindex 9dfc1a885e..902e213173 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 static LIST_HEAD(hook_head);\n \n@@ -78,6 +79,7 @@ static int hook_config_lookup(const char *key, const char *value, void *hook_key\n struct list_head* hook_list(const struct strbuf* hookname)\n {\n \tstruct strbuf hook_key = STRBUF_INIT;\n+\tconst char *legacy_hook_path = NULL;\n \n \tif (!hookname)\n \t\treturn NULL;\n@@ -86,5 +88,45 @@ struct list_head* hook_list(const struct strbuf* hookname)\n \n \tgit_config(hook_config_lookup, (void*)hook_key.buf);\n \n+\tlegacy_hook_path = find_hook(hookname->buf);\n+\n+\t/* TODO: check hook.runHookDir */\n+\tif (legacy_hook_path)\n+\t\templace_hook(&hook_head, legacy_hook_path);\n+\n \treturn &hook_head;\n }\n+\n+int run_hooks(const char *const *env, const struct strbuf *hookname,\n+\t      const struct argv_array *args)\n+{\n+\tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n+\tint rc = 0;\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\t/* add command */\n+\t\targv_array_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\tif (args)\n+\t\t\targv_array_pushv(&hook_proc.args, args->argv);\n+\n+\t\thook_proc.env = env;\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\trc |= run_command(&hook_proc);\n+\t}\n+\n+\treturn rc;\n+}\ndiff --git a/hook.h b/hook.h\nindex aaf6511cff..cf598d6ccb 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 \"argv-array.h\"\n \n struct hook\n {\n@@ -10,6 +11,8 @@ struct hook\n };\n \n struct list_head* hook_list(const struct strbuf *hookname);\n+int run_hooks(const char *const *env, const struct strbuf *hookname,\n+\t      const struct argv_array *args);\n \n void free_hook(struct hook *ptr);\n void clear_hook_list(void);\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex ebf8f38d68..ee8114250d 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -84,4 +84,32 @@ test_expect_success 'git hook list --porcelain prints just the command' '\n \ttest_cmp expected 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+\tcat >~/sample-hook.sh <<-EOF &&\n+\techo \\\"Sample Hook\\\"\n+\tEOF\n+\n+\ttest_when_finished \"rm ~/sample-hook.sh\" &&\n+\n+\tchmod +x ~/sample-hook.sh &&\n+\n+\ttest_config hook.pre-commit.command \"~/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_done\n-- \n2.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"402354","messageId":"xmqqmu3i9kvg.fsf@gitster.c.googlers.com","threadId":"53527","inReplyTo":"20200728222455.3023400-6-emilyshaffer@google.com","subject":"Re: [RFC PATCH v3 5/6] parse-options: parse into argv_array","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2020-07-29T19:33:39Z","receivedAt":"2020-07-29T19:33:47Z","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> parse-options already knows how to read into a string_list, and it knows\n> how to read into an argv_array as a passthrough (that is, including the\n> argument as well as its value). string_list and argv_array serve similar\n> purposes but are somewhat painful to convert between; so, let's teach\n> parse-options to read values of string arguments directly into an\n> argv_array without preserving the argument name.\n>\n> This is useful if collecting generic arguments to pass through to\n> another command, for example, 'git hook run --arg \"--quiet\" --arg\n> \"--format=pretty\" some-hook'. The resulting argv_array would contain\n> { \"--quiet\", \"--format=pretty\" }.\n>\n> The implementation is based on that of OPT_STRING_LIST.\n\nBe it argv_array or strvec, I think this is a useful thing to do.\n\nI grepped for the users of OPT_STRING_LIST() to see if some of them\nare better served by this, but none of them stood out as candidates\nthat are particularly good match.\n\n> +int parse_opt_argv_array(const struct option *opt, const char *arg, int unset)\n> +{\n> +\tstruct argv_array *v = opt->value;\n> +\n> +\tif (unset) {\n> +\t\targv_array_clear(v);\n> +\t\treturn 0;\n> +\t}\n> +\n> +\tif (!arg)\n> +\t\treturn -1;\n\nI think the calling parse_options() loop would catch this negative\nreturn and raise an error, but is it better for this code to stay\nsilent or would it be better to say that opt->long_name/short_name \nis not a boolean?\n\n> +\targv_array_push(v, arg);\n> +\treturn 0;\n> +}\n"},{"id":"402507","messageId":"xmqqsgd8606c.fsf@gitster.c.googlers.com","threadId":"53527","inReplyTo":"xmqqmu3i9kvg.fsf@gitster.c.googlers.com","subject":"Re: [RFC PATCH v3 5/6] parse-options: parse into argv_array","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2020-07-30T23:41:15Z","receivedAt":"2020-07-30T23:41:20Z","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> Be it argv_array or strvec, I think this is a useful thing to do.\n>\n> I grepped for the users of OPT_STRING_LIST() to see if some of them\n> are better served by this, but none of them stood out as candidates\n> that are particularly good match.\n>\n>> +int parse_opt_argv_array(const struct option *opt, const char *arg, int unset)\n>> +{\n>> +\tstruct argv_array *v = opt->value;\n>> +\n>> +\tif (unset) {\n>> +\t\targv_array_clear(v);\n>> +\t\treturn 0;\n>> +\t}\n>> +\n>> +\tif (!arg)\n>> +\t\treturn -1;\n>\n> I think the calling parse_options() loop would catch this negative\n> return and raise an error, but is it better for this code to stay\n> silent or would it be better to say that opt->long_name/short_name \n> is not a boolean?\n\nI am still waiting for this to be answered, but I queued the whole\ntopic, these last two steps included, just to see how bad adjusting\nto the strvec API migration would be.  It wasn't too bad.\n\nI would not recommend you, or other contributors who use argv-array\nAPI in their topics, to build on top of jk/strvec, not just yet, as\nI expect it to go through at least one more reroll to update the\ndetails.\n\nThanks.\n\n"},{"id":"403820","messageId":"3fda8c55-1373-03a8-9cc1-484345e7fa3e@gmail.com","threadId":"53527","inReplyTo":"20200609214926.GD148632@google.com","subject":"Re: [PATCH v2 3/4] hook: add list command","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2020-08-17T13:36:56Z","receivedAt":"2020-08-17T13:37:04Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Emily\n\nsorry it has taken me so long to reply\n\nOn 09/06/2020 22:49, Emily Shaffer wrote:\n> On Fri, May 22, 2020 at 11:27:44AM +0100, Phillip Wood wrote:\n>>\n>> Hi Emily\n>>\n>> On 21/05/2020 19:54, Emily Shaffer wrote:\n>>> [...]\n>>> +static int hook_config_lookup(const char *key, const char *value, void *hook_key_cb)\n>>> +{\n>>> +\tconst char *hook_key = hook_key_cb;\n>>> +\n>>> +\tif (!strcmp(key, hook_key)) {\n>>> +\t\tconst char *command = value;\n>>> +\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n>>> +\t\tstruct list_head *pos = NULL, *tmp = NULL;\n>>> +\n>>> +\t\t/* Check if a hookcmd with that name exists. */\n>>> +\t\tstrbuf_addf(&hookcmd_name, \"hookcmd.%s.command\", command);\n>>> +\t\tgit_config_get_value(hookcmd_name.buf, &command);\n>>\n>> This looks dodgy to me. This code is called by git_config() as it parses\n>> the config files, so it has not had a chance to fully populate the\n>> config cache used by git_config_get_value(). I think the test below\n>> passes because the hookcmd setting is set in the global file and the\n>> hook setting is set in the local file so when we have already parsed the\n>> hookcmd setting when we come to look it up. The same comment applies to\n>> the hypothetical ordering config mentioned below. I think it would be\n>> better to collect the list of hook.<event>.command settings in this\n>> callback and then look up any hookcmd settings for those hook commands\n>> after we've finished reading all of the config files.\n> \n> git_config_get_value() calls repo_read_config(the_repository) if the\n> config hasn't been fully parsed yet, so I think what you're worrying\n> about is not an issue. It's ugly, I agree, but since the new hotness\n> (git_config_get_value() and friends) doesn't offer the same\n> functionality as the old solution (config origin) this seemed like an\n> okay approach. As I understand it, moving this hookcmd lookup section\n> outside of the config callback will save us up to one additional pass\n> through the configs, at the expense of a more convoluted code path.\n\nOh I didn't realize that, thanks for explaining it. Below you mention \nshowing the origin for hookcmds as well as the origin of the command \nwhich would mean having to change this code anyway I think.\n\n>>\n>>> +\n>>> +\t\tif (!command)\n>>> +\t\t\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\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\tlist_for_each_safe(pos, tmp, &hook_head) {\n>>> +\t\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n>>> +\t\t\t/*\n>>> +\t\t\t * The list of hooks to run can be reordered by being redeclared\n>>> +\t\t\t * in the config. Options about hook ordering should be checked\n>>> +\t\t\t * here.\n>>> +\t\t\t */\n>>> +\t\t\tif (0 == strcmp(hook->command.buf, command))\n>>> +\t\t\t\tremove_hook(pos);\n>>> +\t\t}\n>>> +\t\templace_hook(pos, command);\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>>> +\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, (void*)hook_key.buf);\n>>> +\n>>> +\treturn &hook_head;\n>>> +}\n>>> diff --git a/hook.h b/hook.h\n>>> new file mode 100644\n>>> index 0000000000..aaf6511cff\n>>> --- /dev/null\n>>> +++ b/hook.h\n>>> @@ -0,0 +1,15 @@\n>>> +#include \"config.h\"\n>>> +#include \"list.h\"\n>>> +#include \"strbuf.h\"\n>>> +\n>>> +struct hook\n>>> +{\n>>> +\tstruct list_head list;\n>>> +\tenum config_scope origin;\n>>> +\tstruct strbuf command;\n>>> +};\n>>> +\n>>> +struct list_head* hook_list(const struct strbuf *hookname);\n>>> +\n>>> +void free_hook(struct hook *ptr);\n>>> +void clear_hook_list(void);\n>>> diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n>>> index 34b0df5216..4e46d7dd4e 100755\n>>> --- a/t/t1360-config-based-hooks.sh\n>>> +++ b/t/t1360-config-based-hooks.sh\n>>> @@ -4,8 +4,55 @@ 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>>> +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 'setup hooks in global, and local' '\n>>> +\tgit config --add --local hook.pre-commit.command \"/path/ghi\" &&\n>>\n>> Can I make a plea for the use of test_config please. Writing tests which\n>> rely on previous tests for their set-up creates a chain of hidden\n>> dependencies that make it hard to add/alter tests later or run a subset\n>> of the tests when developing a new patch. t3404-rebase-interactive.sh is\n>> a prime example of this and I dread touching it.\n> \n> Sure. I'll redo them.\n\nThat's great, thanks\n\nBest Wishes\n\nPhillip\n\n>>\n>>> +\tgit config --add --global hook.pre-commit.command \"/path/def\"\n>>> +'\n>>> +\n>>> +test_expect_success 'git hook list orders by config order' '\n>>> +\tcat >expected <<-\\EOF &&\n>>> +\tglobal:\t/path/def\n>>> +\tlocal:\t/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>>> +\tgit config --add --local hook.pre-commit.command \"abc\" &&\n>>> +\tgit config --add --global hookcmd.abc.command \"/path/abc\" &&\n>>> +\n>>> +\tcat >expected <<-\\EOF &&\n>>> +\tglobal:\t/path/def\n>>> +\tlocal:\t/path/ghi\n>>> +\tlocal:\t/path/abc\n>>\n>> We should make it clear in the documentation that the config origin\n>> applies to the hook setting, even though we display the hookcmd command\n>> which is set globally here for the last hook.\n> \n> One of the suggestions from our UX team last week was to make this list\n> output clearer to indicate the origin of the command plus the origin of\n> the hookcmd object; I'll try to straighten this out and make sure the\n> doc agrees.\n> \n>   - Emily\n> \n"},{"id":"405215","messageId":"20200909004939.1942347-1-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200728222455.3023400-1-emilyshaffer@google.com","subject":"[PATCH v4 0/9] propose config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:30Z","receivedAt":"2020-09-09T00:49:58Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Since v3, the biggest change is the conversion of commit hooks to use the new\nhook machinery. The first change (\"commit: use config-based hooks\") is the\nimportant part; the second change (\"run_commit_hook: take strvec instead of varargs\")\nis probably subjective, but I thought it was a decent tech debt reduction.\n\nI wanted to send this reroll quickly since I had promised it in standup last\nweek, but I've got pretty good progress locally on the patch for configuring\n\"hook.runHookDir\"; I'm planning to send that soon, probably this week.\n\n - Emily\n\nEmily Shaffer (9):\n  doc: propose hooks managed by the config\n  hook: scaffolding for git-hook subcommand\n  hook: add list command\n  hook: add --porcelain to list command\n  parse-options: parse into strvec\n  hook: add 'run' subcommand\n  hook: replace run-command.h:find_hook\n  commit: use config-based hooks\n  run_commit_hook: take strvec instead of varargs\n\n .gitignore                                    |   1 +\n Documentation/Makefile                        |   1 +\n Documentation/git-hook.txt                    |  63 ++++\n Documentation/technical/api-parse-options.txt |   5 +\n .../technical/config-based-hooks.txt          | 354 ++++++++++++++++++\n Makefile                                      |   2 +\n builtin.h                                     |   1 +\n builtin/commit.c                              |  49 +--\n builtin/hook.c                                | 107 ++++++\n builtin/merge.c                               |  23 +-\n commit.c                                      |  12 +-\n commit.h                                      |   5 +-\n git.c                                         |   1 +\n hook.c                                        | 155 ++++++++\n hook.h                                        |  19 +\n parse-options-cb.c                            |  16 +\n parse-options.h                               |   4 +\n sequencer.c                                   |  15 +-\n t/t1360-config-based-hooks.sh                 | 115 ++++++\n ...3-pre-commit-and-pre-merge-commit-hooks.sh |  13 +\n 20 files changed, 918 insertions(+), 43 deletions(-)\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\n-- \n2.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405216","messageId":"20200909004939.1942347-2-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"[PATCH v4 1/9] doc: propose hooks managed by the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:31Z","receivedAt":"2020-09-09T00:50:05Z","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 Documentation/Makefile                        |   1 +\n .../technical/config-based-hooks.txt          | 354 ++++++++++++++++++\n 2 files changed, 355 insertions(+)\n create mode 100644 Documentation/technical/config-based-hooks.txt\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex 80d1908a44..58d6b3acbe 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -81,6 +81,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..c6e762b192\n--- /dev/null\n+++ b/Documentation/technical/config-based-hooks.txt\n@@ -0,0 +1,354 @@\n+Configuration-based hook management\n+===================================\n+:sectanchors:\n+\n+[[motivation]]\n+== Motivation\n+\n+Treat hooks as a first-class citizen by replacing the .git/hook/hookname path as\n+the only source of hooks to execute, in a way which is friendly to users with\n+multiple repos which have similar needs.\n+\n+Redefine \"hook\" as an event rather than a single script, allowing users to\n+perform 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+[[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. These order of these\n+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. Hook event subsections can\n+also contain per-hook-event settings.\n+\n+Also contains top-level hook execution settings, for example,\n+`hook.warnHookDir`, `hook.runHookDir`, or `hook.disableAll`. (These settings are\n+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+  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 to which\n+it's normally subscribed _except_ `pre-commit`.\n+\n+----\n+[hookcmd \"perl-linter\"]\n+  command = /usr/bin/lint-it --language=perl\n+  skip = true\n+  pre-commit-skip = false\n+----\n+\n+[[command-line-api]]\n+=== Command-line API\n+\n+Users should be able to view, reorder, and create hook commands via the command\n+line. External tools should be able to view a list of hooks in the correct order\n+to run.\n+\n+*`git hook list <hook-event>`*\n+\n+*`git hook list (--system|--global|--local|--worktree)`*\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`.\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. In\n+the case when the code generating a hook event doesn't have special concerns\n+about how to run the hooks, the hook library will provide a basic API to call\n+all hooks in config order with an `argv_array` provided by the code which\n+generates the hook event:\n+\n+*`int run_hooks(const char *hookname, struct argv_array *args)`*\n+\n+This call includes the hook command provided by `run-command.h:find_hook()`;\n+eventually, this legacy hook will be gated by a config `hook.runHookDir`. The\n+config is checked against a number of cases:\n+\n+- \"no\": the legacy hook will not be run\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\".\n+\n+If the caller wants to do something more complicated, the hook library can also\n+provide a callback API:\n+\n+*`int for_each_hookcmd(const char *hookname, hookcmd_function *cb)`*\n+\n+Finally, to facilitate the builtin, the library will also provide the following\n+APIs to interact with the config:\n+\n+----\n+int set_hook_commands(const char *hookname, struct string_list *commands,\n+\tenum config_scope scope);\n+int set_hookcmd(const char *hookcmd, struct hookcmd options);\n+\n+int list_hook_commands(const char *hookname, struct string_list *commands);\n+int list_hooks_in_scope(enum config_scope scope, struct string_list *commands);\n+----\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+----\n+struct hookcmd {\n+  const char *name;\n+  const char *command;\n+\n+  /* for illustration only; not planned at present */\n+  int parallelizable;\n+  const char *hookcmd_before;\n+  const char *hookcmd_after;\n+  enum recovery_action on_fail;\n+}\n+----\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. Users can replace their\n+`.git/hooks/<hook-event>` scripts with a trampoline based on `git hook list`'s\n+output. Modifier commands like `git hook add` and `git hook edit` can be\n+implemented around this time as well.\n+\n+[[stage-2]]\n+==== Stage 2\n+\n+`hook.h:run_hooks()` is taught to include `run-command.h:find_hook()` at the\n+end; calls to `find_hook()` are replaced with calls to `run_hooks()`. Users can\n+opt-in to config-based hooks simply by creating some in their config; otherwise\n+users should remain unaffected by the change.\n+\n+[[stage-3]]\n+==== Stage 3\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-4]]\n+==== Stage 4\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. The\n+goal is to avoid an overcomplicated design to work around a problem which has\n+ceased to exist.\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+[[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+\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+|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+|No\n+|No\n+\n+|Discoverability\n+|Better (in `git help git`)\n+|Same as before\n+|Same as before\n+\n+|Hard to run unexpected hook\n+|If configured\n+|No\n+|No\n+|===\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\n+\n+Users with many hooks might want to run them simultaneously, if the hooks don't\n+modify state; if one hook depends on another's output, then users will want to\n+specify those dependencies. If we decide to solve this problem, we may want to\n+look to modern build systems for inspiration on how to manage dependencies and\n+parallel tasks.\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+[[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.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405217","messageId":"20200909004939.1942347-3-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"[PATCH v4 2/9] hook: scaffolding for git-hook subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:32Z","receivedAt":"2020-09-09T00:50:08Z","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---\n .gitignore                    |  1 +\n Documentation/git-hook.txt    | 19 +++++++++++++++++++\n Makefile                      |  1 +\n builtin.h                     |  1 +\n builtin/hook.c                | 21 +++++++++++++++++++++\n git.c                         |  1 +\n t/t1360-config-based-hooks.sh | 11 +++++++++++\n 7 files changed, 55 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 ee509a2ad2..0694a34884 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -75,6 +75,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..2d50c414cc\n--- /dev/null\n+++ b/Documentation/git-hook.txt\n@@ -0,0 +1,19 @@\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+You can list, add, and modify hooks with this command.\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex 65f8cfb236..6eee75555e 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1077,6 +1077,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 a5ae15bfe5..4e736499c0 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -157,6 +157,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/git.c b/git.c\nindex 8bd1d7551d..1cdb3221a5 100644\n--- a/git.c\n+++ b/git.c\n@@ -519,6 +519,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/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.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405218","messageId":"20200909004939.1942347-4-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"[PATCH v4 3/9] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:33Z","receivedAt":"2020-09-09T00:50:17Z","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; at minimum, this subsection must 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  ~/baz/from/hookcmd.sh\n  ~/bar.sh\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt    |  37 +++++++++++-\n Makefile                      |   1 +\n builtin/hook.c                |  55 ++++++++++++++++--\n hook.c                        | 102 ++++++++++++++++++++++++++++++++++\n hook.h                        |  15 +++++\n t/t1360-config-based-hooks.sh |  68 ++++++++++++++++++++++-\n 6 files changed, 271 insertions(+), 7 deletions(-)\n create mode 100644 hook.c\n create mode 100644 hook.h\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex 2d50c414cc..e458586e96 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,12 +8,47 @@ git-hook - Manage configured hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook'\n+'git hook' list <hook-name>\n \n DESCRIPTION\n -----------\n You can list, 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 config order. \"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+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 note the config scope where the relevant\n+`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/Makefile b/Makefile\nindex 6eee75555e..804de45b16 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -890,6 +890,7 @@ LIB_OBJS += grep.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 += interdiff.o\n LIB_OBJS += json-writer.o\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex b2bbc84d4d..a0759a4c26 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -1,21 +1,68 @@\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 hook *item;\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(\"a hookname must be provided to operate on.\",\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\treturn 0;\n+\t}\n+\n+\tlist_for_each(pos, head) {\n+\t\titem = list_entry(pos, struct hook, list);\n+\t\tif (item)\n+\t\t\tprintf(\"%s:\\t%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();\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..b006950eb8\n--- /dev/null\n+++ b/hook.c\n@@ -0,0 +1,102 @@\n+#include \"cache.h\"\n+\n+#include \"hook.h\"\n+#include \"config.h\"\n+\n+/*\n+ * NEEDSWORK: a stateful hook_head means we can't run two hook events in the\n+ * background at the same time - which might be ok, or might not.\n+ *\n+ * Maybe it's better to cache a list head per hookname, since we can probably\n+ * guess that the hook list won't change during a user-initiated operation. For\n+ * now, within list_hooks, call clear_hook_list() at the outset.\n+ */\n+static LIST_HEAD(hook_head);\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 emplace_hook(struct list_head *pos, const char *command)\n+{\n+\tstruct hook *to_add = malloc(sizeof(struct hook));\n+\tto_add->origin = current_config_scope();\n+\tstrbuf_init(&to_add->command, 0);\n+\t/* even with use_shell, run_command() needs quotes */\n+\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n+\n+\tlist_add_tail(&to_add->list, pos);\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(void)\n+{\n+\tstruct list_head *pos, *tmp;\n+\tlist_for_each_safe(pos, tmp, &hook_head)\n+\t\tremove_hook(pos);\n+}\n+\n+static int hook_config_lookup(const char *key, const char *value, void *hook_key_cb)\n+{\n+\tconst char *hook_key = hook_key_cb;\n+\n+\tif (!strcmp(key, hook_key)) {\n+\t\tconst char *command = value;\n+\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n+\t\tstruct list_head *pos = NULL, *tmp = NULL;\n+\n+\t\t/* Check if a hookcmd with that name exists. */\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\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\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\tlist_for_each_safe(pos, tmp, &hook_head) {\n+\t\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n+\t\t\t/*\n+\t\t\t * The list of hooks to run can be reordered by being redeclared\n+\t\t\t * in the config. Options about hook ordering should be checked\n+\t\t\t * here.\n+\t\t\t */\n+\t\t\tif (0 == strcmp(hook->command.buf, command))\n+\t\t\t\tremove_hook(pos);\n+\t\t}\n+\t\templace_hook(pos, command);\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+\n+\tif (!hookname)\n+\t\treturn NULL;\n+\n+\t/* hook_head is stateful */\n+\tclear_hook_list();\n+\n+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname->buf);\n+\n+\tgit_config(hook_config_lookup, (void*)hook_key.buf);\n+\n+\treturn &hook_head;\n+}\ndiff --git a/hook.h b/hook.h\nnew file mode 100644\nindex 0000000000..aaf6511cff\n--- /dev/null\n+++ b/hook.h\n@@ -0,0 +1,15 @@\n+#include \"config.h\"\n+#include \"list.h\"\n+#include \"strbuf.h\"\n+\n+struct hook\n+{\n+\tstruct list_head list;\n+\tenum config_scope origin;\n+\tstruct strbuf command;\n+};\n+\n+struct list_head* hook_list(const struct strbuf *hookname);\n+\n+void free_hook(struct hook *ptr);\n+void clear_hook_list(void);\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 34b0df5216..46d1ed354a 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -4,8 +4,72 @@ 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 list orders by config order' '\n+\tsetup_hooks &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal:\t$ROOT/path/def\n+\tlocal:\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 dereferences a hookcmd' '\n+\tsetup_hooks &&\n+\tsetup_hookcmd &&\n+\n+\tcat >expected <<-EOF &&\n+\tglobal:\t$ROOT/path/def\n+\tlocal:\t$ROOT/path/ghi\n+\tlocal:\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+\ttest_config hook.pre-commit.command \"/path/def\" --add &&\n+\n+\tcat >expected <<-EOF &&\n+\tlocal:\t$ROOT/path/ghi\n+\tlocal:\t$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.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405219","messageId":"20200909004939.1942347-5-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"[PATCH v4 4/9] hook: add --porcelain to list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:34Z","receivedAt":"2020-09-09T00:50:17Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Teach 'git hook list --porcelain <hookname>', which prints simply the\ncommands to be run in the order suggested by the config. This option is\nintended for use by user scripts, wrappers, or out-of-process Git\ncommands which still want to execute hooks. For example, the following\nsnippet might be added to git-send-email.perl to introduce a\n`pre-send-email` hook:\n\n  sub pre_send_email {\n    open(my $fh, 'git hook list --porcelain pre-send-email |');\n    chomp(my @hooks = <$fh>);\n    close($fh);\n\n    foreach $hook (@hooks) {\n            system $hook\n    }\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n Documentation/git-hook.txt    | 13 +++++++++++--\n builtin/hook.c                | 17 +++++++++++++----\n t/t1360-config-based-hooks.sh | 12 ++++++++++++\n 3 files changed, 36 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\nindex e458586e96..0854035ce2 100644\n--- a/Documentation/git-hook.txt\n+++ b/Documentation/git-hook.txt\n@@ -8,7 +8,7 @@ git-hook - Manage configured hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' list <hook-name>\n+'git hook' list [--porcelain] <hook-name>\n \n DESCRIPTION\n -----------\n@@ -43,11 +43,20 @@ Local config\n COMMANDS\n --------\n \n-list <hook-name>::\n+list [--porcelain] <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 note the config scope where the relevant\n `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n++\n+If `--porcelain` is specified, instead print the commands alone, separated by\n+newlines, for easy parsing by a script.\n+\n+OPTIONS\n+-------\n+--porcelain::\n+\tWith `list`, print the commands in the order they should be run,\n+\tseparated by newlines, for easy parsing by a script.\n \n GIT\n ---\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex a0759a4c26..0d92124ca6 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -16,8 +16,11 @@ 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+\tint porcelain = 0;\n \n \tstruct option list_options[] = {\n+\t\tOPT_BOOL(0, \"porcelain\", &porcelain,\n+\t\t\t \"format for execution by a script\"),\n \t\tOPT_END(),\n \t};\n \n@@ -29,6 +32,8 @@ static int list(int argc, const char **argv, const char *prefix)\n \t\t\t      builtin_hook_usage, list_options);\n \t}\n \n+\n+\n \tstrbuf_addstr(&hookname, argv[0]);\n \n \thead = hook_list(&hookname);\n@@ -41,10 +46,14 @@ 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\tif (item)\n-\t\t\tprintf(\"%s:\\t%s\\n\",\n-\t\t\t       config_scope_name(item->origin),\n-\t\t\t       item->command.buf);\n+\t\tif (item) {\n+\t\t\tif (porcelain)\n+\t\t\t\tprintf(\"%s\\n\", item->command.buf);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s:\\t%s\\n\",\n+\t\t\t\t       config_scope_name(item->origin),\n+\t\t\t\t       item->command.buf);\n+\t\t}\n \t}\n \n \tclear_hook_list();\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex 46d1ed354a..ebf8f38d68 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -72,4 +72,16 @@ test_expect_success 'git hook list reorders on duplicate commands' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list --porcelain prints just the command' '\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 --porcelain pre-commit >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405220","messageId":"20200909004939.1942347-6-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"[PATCH v4 5/9] parse-options: parse into strvec","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:35Z","receivedAt":"2020-09-09T00:50:23Z","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 Documentation/technical/api-parse-options.txt |  5 +++++\n parse-options-cb.c                            | 16 ++++++++++++++++\n parse-options.h                               |  4 ++++\n 3 files changed, 25 insertions(+)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex 5a60bbfa7f..b4f1fc4a1a 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -173,6 +173,11 @@ 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_ARGV_ARRAY(short, long, &struct argv_array, arg_str, description)`::\n+\tIntroduce an option with a string argument.\n+\tThe string argument is stored as an element in `argv_array`.\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 d9d3b0819f..d2b8b7b98a 100644\n--- a/parse-options-cb.c\n+++ b/parse-options-cb.c\n@@ -205,6 +205,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 46af942093..177259488b 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.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405221","messageId":"20200909004939.1942347-7-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"[PATCH v4 6/9] hook: add 'run' subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:36Z","receivedAt":"2020-09-09T00:50:24Z","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 in config order, in series. As alternate\nordering or parallelism is supported in the future, we should add knobs\nto 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. For now, there is no way to disable them.\n\nUsers may wish to provide hook commands like 'git config\nhook.pre-commit.command \"~/linter.sh --pre-commit\"'. To enable this, the\ncontents of the 'hook.*.command' and 'hookcmd.*.command' strings are\nfirst split by space or quotes into an argv_array, then expanded with\n'expand_user_path()'.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/hook.c                | 30 ++++++++++++++++++++\n hook.c                        | 52 ++++++++++++++++++++++++++++++++---\n hook.h                        |  3 ++\n t/t1360-config-based-hooks.sh | 28 +++++++++++++++++++\n 4 files changed, 109 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 0d92124ca6..a8f8b03699 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -5,9 +5,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@@ -62,6 +64,32 @@ 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 strvec envs = STRVEC_INIT;\n+\tstruct strvec args = STRVEC_INIT;\n+\n+\tstruct option run_options[] = {\n+\t\tOPT_STRVEC('e', \"env\", &envs, N_(\"var\"),\n+\t\t\t   N_(\"environment variables for hook to use\")),\n+\t\tOPT_STRVEC('a', \"arg\", &args, N_(\"args\"),\n+\t\t\t   N_(\"argument to pass to hook\")),\n+\t\tOPT_END(),\n+\t};\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(_(\"a hookname must be provided to operate on.\"),\n+\t\t\t      builtin_hook_usage, run_options);\n+\n+\tstrbuf_addstr(&hookname, argv[0]);\n+\n+\treturn run_hooks(envs.v, &hookname, &args);\n+}\n+\n int cmd_hook(int argc, const char **argv, const char *prefix)\n {\n \tstruct option builtin_hook_options[] = {\n@@ -72,6 +100,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix)\n \n \tif (!strcmp(argv[1], \"list\"))\n \t\treturn list(argc - 1, argv + 1, prefix);\n+\tif (!strcmp(argv[1], \"run\"))\n+\t\treturn run(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\nindex b006950eb8..0dab981681 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 /*\n  * NEEDSWORK: a stateful hook_head means we can't run two hook events in the\n@@ -21,13 +22,15 @@ void free_hook(struct hook *ptr)\n \t}\n }\n \n-static void emplace_hook(struct list_head *pos, const char *command)\n+static void emplace_hook(struct list_head *pos, const char *command, int quoted)\n {\n \tstruct hook *to_add = malloc(sizeof(struct hook));\n \tto_add->origin = current_config_scope();\n \tstrbuf_init(&to_add->command, 0);\n-\t/* even with use_shell, run_command() needs quotes */\n-\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n+\tif (quoted)\n+\t\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n+\telse\n+\t\tstrbuf_addstr(&to_add->command, command);\n \n \tlist_add_tail(&to_add->list, pos);\n }\n@@ -78,7 +81,7 @@ static int hook_config_lookup(const char *key, const char *value, void *hook_key\n \t\t\tif (0 == strcmp(hook->command.buf, command))\n \t\t\t\tremove_hook(pos);\n \t\t}\n-\t\templace_hook(pos, command);\n+\t\templace_hook(pos, command, 0);\n \t}\n \n \treturn 0;\n@@ -87,6 +90,7 @@ static int hook_config_lookup(const char *key, const char *value, void *hook_key\n struct list_head* hook_list(const struct strbuf* hookname)\n {\n \tstruct strbuf hook_key = STRBUF_INIT;\n+\tconst char *legacy_hook_path = NULL;\n \n \tif (!hookname)\n \t\treturn NULL;\n@@ -98,5 +102,45 @@ struct list_head* hook_list(const struct strbuf* hookname)\n \n \tgit_config(hook_config_lookup, (void*)hook_key.buf);\n \n+\tlegacy_hook_path = find_hook(hookname->buf);\n+\n+\t/* TODO: check hook.runHookDir */\n+\tif (legacy_hook_path)\n+\t\templace_hook(&hook_head, legacy_hook_path, 1);\n+\n \treturn &hook_head;\n }\n+\n+int run_hooks(const char *const *env, const struct strbuf *hookname,\n+\t      const struct strvec *args)\n+{\n+\tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n+\tint rc = 0;\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\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\tif (args)\n+\t\t\tstrvec_pushv(&hook_proc.args, args->v);\n+\n+\t\thook_proc.env = env;\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\trc |= run_command(&hook_proc);\n+\t}\n+\n+\treturn rc;\n+}\ndiff --git a/hook.h b/hook.h\nindex aaf6511cff..d020788a6b 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 {\n@@ -10,6 +11,8 @@ struct hook\n };\n \n struct list_head* hook_list(const struct strbuf *hookname);\n+int run_hooks(const char *const *env, const struct strbuf *hookname,\n+\t      const struct strvec *args);\n \n void free_hook(struct hook *ptr);\n void clear_hook_list(void);\ndiff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\nindex ebf8f38d68..ee8114250d 100755\n--- a/t/t1360-config-based-hooks.sh\n+++ b/t/t1360-config-based-hooks.sh\n@@ -84,4 +84,32 @@ test_expect_success 'git hook list --porcelain prints just the command' '\n \ttest_cmp expected 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+\tcat >~/sample-hook.sh <<-EOF &&\n+\techo \\\"Sample Hook\\\"\n+\tEOF\n+\n+\ttest_when_finished \"rm ~/sample-hook.sh\" &&\n+\n+\tchmod +x ~/sample-hook.sh &&\n+\n+\ttest_config hook.pre-commit.command \"~/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_done\n-- \n2.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405222","messageId":"20200909004939.1942347-8-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"[PATCH v4 7/9] hook: replace run-command.h:find_hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:37Z","receivedAt":"2020-09-09T00:50:25Z","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\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n hook.c | 9 +++++++++\n hook.h | 1 +\n 2 files changed, 10 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex 0dab981681..7c7b922369 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -111,6 +111,15 @@ struct list_head* hook_list(const struct strbuf* hookname)\n \treturn &hook_head;\n }\n \n+int hook_exists(const char *hookname)\n+{\n+\tconst char *value = NULL;\n+\tstruct strbuf hook_key = STRBUF_INIT;\n+\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n+\n+\treturn (!git_config_get_value(hook_key.buf, &value)) || !!find_hook(hookname);\n+}\n+\n int run_hooks(const char *const *env, const struct strbuf *hookname,\n \t      const struct strvec *args)\n {\ndiff --git a/hook.h b/hook.h\nindex d020788a6b..d94511b609 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -11,6 +11,7 @@ struct hook\n };\n \n struct list_head* hook_list(const struct strbuf *hookname);\n+int hook_exists(const char *hookname);\n int run_hooks(const char *const *env, const struct strbuf *hookname,\n \t      const struct strvec *args);\n \n-- \n2.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405223","messageId":"20200909004939.1942347-10-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"[PATCH v4 9/9] run_commit_hook: take strvec instead of varargs","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:39Z","receivedAt":"2020-09-09T00:50:28Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"Taking varargs in run_commit_hook() led to some bizarre patterns, like\ncallers using two string variables (which may or may not be filled) to\nexpress different argument lists for the commit hooks. Because\nrun_commit_hook() no longer needs to call a variadic function for the\nhook run itself, we can use strvec to make the calling code more\nconventional.\n\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/commit.c | 46 +++++++++++++++++++++++-----------------------\n builtin/merge.c  | 20 ++++++++++++++++----\n commit.c         | 13 ++-----------\n commit.h         |  5 +++--\n sequencer.c      | 15 ++++++++-------\n 5 files changed, 52 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex a19c6478eb..f029d4f5ac 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -691,8 +691,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \tstruct strbuf committer_ident = STRBUF_INIT;\n \tint committable;\n \tstruct strbuf sb = STRBUF_INIT;\n-\tconst char *hook_arg1 = NULL;\n-\tconst char *hook_arg2 = NULL;\n+\tstruct strvec hook_args = STRVEC_INIT;\n \tint clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);\n \tint old_display_comment_prefix;\n \tint merge_contains_scissors = 0;\n@@ -700,7 +699,8 @@ 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, index_file, \"pre-commit\",\n+\t\t\t\t\t  &hook_args))\n \t\treturn 0;\n \n \tif (squash_message) {\n@@ -722,27 +722,28 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\t}\n \t}\n \n+\tstrvec_push(&hook_args, git_path_commit_editmsg());\n+\n \tif (have_option_m && !fixup_message) {\n \t\tstrbuf_addbuf(&sb, &message);\n-\t\thook_arg1 = \"message\";\n+\t\tstrvec_push(&hook_args, \"message\");\n \t} else if (logfile && !strcmp(logfile, \"-\")) {\n \t\tif (isatty(0))\n \t\t\tfprintf(stderr, _(\"(reading log message from standard input)\\n\"));\n \t\tif (strbuf_read(&sb, 0, 0) < 0)\n \t\t\tdie_errno(_(\"could not read log from standard input\"));\n-\t\thook_arg1 = \"message\";\n+\t\tstrvec_push(&hook_args, \"message\");\n \t} else if (logfile) {\n \t\tif (strbuf_read_file(&sb, logfile, 0) < 0)\n \t\t\tdie_errno(_(\"could not read log file '%s'\"),\n \t\t\t\t  logfile);\n-\t\thook_arg1 = \"message\";\n+\t\tstrvec_push(&hook_args, \"message\");\n \t} else if (use_message) {\n \t\tchar *buffer;\n \t\tbuffer = strstr(use_message_buffer, \"\\n\\n\");\n \t\tif (buffer)\n \t\t\tstrbuf_addstr(&sb, skip_blank_lines(buffer + 2));\n-\t\thook_arg1 = \"commit\";\n-\t\thook_arg2 = use_message;\n+\t\tstrvec_pushl(&hook_args, \"commit\", use_message, NULL);\n \t} else if (fixup_message) {\n \t\tstruct pretty_print_context ctx = {0};\n \t\tstruct commit *commit;\n@@ -754,7 +755,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\t\t\t      &sb, &ctx);\n \t\tif (have_option_m)\n \t\t\tstrbuf_addbuf(&sb, &message);\n-\t\thook_arg1 = \"message\";\n+\t\tstrvec_push(&hook_args, \"message\");\n \t} else if (!stat(git_path_merge_msg(the_repository), &statbuf)) {\n \t\tsize_t merge_msg_start;\n \n@@ -765,9 +766,9 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\tif (!stat(git_path_squash_msg(the_repository), &statbuf)) {\n \t\t\tif (strbuf_read_file(&sb, git_path_squash_msg(the_repository), 0) < 0)\n \t\t\t\tdie_errno(_(\"could not read SQUASH_MSG\"));\n-\t\t\thook_arg1 = \"squash\";\n+\t\t\tstrvec_push(&hook_args, \"squash\");\n \t\t} else\n-\t\t\thook_arg1 = \"merge\";\n+\t\t\tstrvec_push(&hook_args, \"merge\");\n \n \t\tmerge_msg_start = sb.len;\n \t\tif (strbuf_read_file(&sb, git_path_merge_msg(the_repository), 0) < 0)\n@@ -781,11 +782,11 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t} else if (!stat(git_path_squash_msg(the_repository), &statbuf)) {\n \t\tif (strbuf_read_file(&sb, git_path_squash_msg(the_repository), 0) < 0)\n \t\t\tdie_errno(_(\"could not read SQUASH_MSG\"));\n-\t\thook_arg1 = \"squash\";\n+\t\tstrvec_push(&hook_args, \"squash\");\n \t} else if (template_file) {\n \t\tif (strbuf_read_file(&sb, template_file, 0) < 0)\n \t\t\tdie_errno(_(\"could not read '%s'\"), template_file);\n-\t\thook_arg1 = \"template\";\n+\t\tstrvec_push(&hook_args, \"template\");\n \t\tclean_message_contents = 0;\n \t}\n \n@@ -794,11 +795,9 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t * just set the argument(s) to the prepare-commit-msg hook.\n \t */\n \telse if (whence == FROM_MERGE)\n-\t\thook_arg1 = \"merge\";\n-\telse if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) {\n-\t\thook_arg1 = \"commit\";\n-\t\thook_arg2 = \"CHERRY_PICK_HEAD\";\n-\t}\n+\t\tstrvec_push(&hook_args, \"merge\");\n+\telse if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK)\n+\t\tstrvec_pushl(&hook_args, \"commit\", \"CHERRY_PICK_HEAD\", NULL);\n \n \tif (squash_message) {\n \t\t/*\n@@ -806,8 +805,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\t * then we're possibly hijacking other commit log options.\n \t\t * Reset the hook args to tell the real story.\n \t\t */\n-\t\thook_arg1 = \"message\";\n-\t\thook_arg2 = \"\";\n+\t\tstrvec_clear(&hook_args);\n+\t\tstrvec_pushl(&hook_args, git_path_commit_editmsg(), \"message\", NULL);\n \t}\n \n \ts->fp = fopen_for_writing(git_path_commit_editmsg());\n@@ -1001,8 +1000,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-\t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n+\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\", &hook_args))\n \t\treturn 0;\n \n \tif (use_editor) {\n@@ -1017,8 +1015,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n \t\tstrvec_clear(&env);\n \t}\n \n+\tstrvec_clear(&hook_args);\n+\tstrvec_push(&hook_args, git_path_commit_editmsg());\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, \"commit-msg\", &hook_args)) {\n \t\treturn 0;\n \t}\n \ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex c1a9d0083d..863c9039a3 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -821,10 +821,14 @@ static void write_merge_heads(struct commit_list *);\n static void prepare_to_commit(struct commit_list *remoteheads)\n {\n \tstruct strbuf msg = STRBUF_INIT;\n+\tstruct strvec hook_args = STRVEC_INIT;\n+\tstruct strbuf hook_name = 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, index_file,\n+\t\t\t\t\t  \"pre-merge-commit\", &hook_args))\n \t\tabort_commit(remoteheads, NULL);\n+\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@@ -832,6 +836,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \t */\n \tif (hook_exists(\"pre-merge-commit\"))\n \t\tdiscard_cache();\n+\n \tread_cache_from(index_file);\n \tstrbuf_addbuf(&msg, &merge_msg);\n \tif (squash)\n@@ -851,17 +856,22 @@ 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+\n+\tstrvec_clear(&hook_args);\n+\tstrvec_pushl(&hook_args, git_path_merge_msg(the_repository), \"merge\", NULL);\n \tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n-\t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n+\t\t\t    &hook_args))\n \t\tabort_commit(remoteheads, NULL);\n+\n \tif (0 < option_edit) {\n \t\tif (launch_editor(git_path_merge_msg(the_repository), NULL, NULL))\n \t\t\tabort_commit(remoteheads, NULL);\n \t}\n \n+\tstrvec_clear(&hook_args);\n+\tstrvec_push(&hook_args, git_path_merge_msg(the_repository));\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  git_path_merge_msg(the_repository), NULL))\n+\t\t\t\t\t  \"commit-msg\", &hook_args))\n \t\tabort_commit(remoteheads, NULL);\n \n \tread_merge_msg(&msg);\n@@ -871,6 +881,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n \tstrbuf_release(&merge_msg);\n \tstrbuf_addbuf(&merge_msg, &msg);\n \tstrbuf_release(&msg);\n+\tstrbuf_release(&hook_name);\n+\tstrvec_clear(&hook_args);\n }\n \n static int merge_trivial(struct commit *head, struct commit_list *remoteheads)\ndiff --git a/commit.c b/commit.c\nindex c7a243e848..726407152c 100644\n--- a/commit.c\n+++ b/commit.c\n@@ -1629,12 +1629,9 @@ 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    const char *name, ...)\n+\t\t    const char *name, struct strvec *args)\n {\n \tstruct strvec hook_env = STRVEC_INIT;\n-\tva_list args;\n-\tconst char *arg;\n-\tstruct strvec hook_args = STRVEC_INIT;\n \tstruct strbuf hook_name = STRBUF_INIT;\n \tint ret;\n \n@@ -1648,14 +1645,8 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n \tif (!editor_is_used)\n \t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n \n-\tva_start(args, name);\n-\twhile ((arg = va_arg(args, const char *)))\n-\t\tstrvec_push(&hook_args, arg);\n-\tva_end(args);\n-\n-\tret = run_hooks(hook_env.v, &hook_name, &hook_args);\n+\tret = run_hooks(hook_env.v, &hook_name, args);\n \tstrvec_clear(&hook_env);\n-\tstrvec_clear(&hook_args);\n \tstrbuf_release(&hook_name);\n \n \treturn ret;\ndiff --git a/commit.h b/commit.h\nindex e901538909..978da3c3e0 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -9,6 +9,7 @@\n #include \"string-list.h\"\n #include \"pretty.h\"\n #include \"commit-slab.h\"\n+#include \"strvec.h\"\n \n #define COMMIT_NOT_FROM_GRAPH 0xFFFFFFFF\n #define GENERATION_NUMBER_INFINITY 0xFFFFFFFF\n@@ -353,7 +354,7 @@ void verify_merge_signature(struct commit *commit, int verbose,\n 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    const char *name, struct strvec *args);\n \n #endif /* COMMIT_H */\ndiff --git a/sequencer.c b/sequencer.c\nindex cc3f8fa88e..5dd4b134d6 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1124,22 +1124,23 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n \t\t\t\t       const char *commit)\n {\n \tint ret = 0;\n-\tconst char *name, *arg1 = NULL, *arg2 = NULL;\n+\tstruct strvec args = STRVEC_INIT;\n+\tconst char *name = git_path_commit_editmsg();\n \n-\tname = git_path_commit_editmsg();\n+\tstrvec_push(&args, name);\n \tif (write_message(msg->buf, msg->len, name, 0))\n \t\treturn -1;\n \n \tif (commit) {\n-\t\targ1 = \"commit\";\n-\t\targ2 = commit;\n+\t\tstrvec_push(&args, \"commit\");\n+\t\tstrvec_push(&args, commit);\n \t} else {\n-\t\targ1 = \"message\";\n+\t\tstrvec_push(&args, \"message\");\n \t}\n-\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n-\t\t\t    arg1, arg2, NULL))\n+\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", &args))\n \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n \n+\tstrvec_clear(&args);\n \treturn ret;\n }\n \n-- \n2.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405224","messageId":"20200909004939.1942347-9-emilyshaffer@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"[PATCH v4 8/9] commit: use config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-09T00:49:38Z","receivedAt":"2020-09-09T00:50:31Z","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\nSigned-off-by: Emily Shaffer <emilyshaffer@google.com>\n---\n builtin/commit.c                                 |  3 ++-\n builtin/merge.c                                  |  3 ++-\n commit.c                                         | 13 ++++++++++++-\n t/t7503-pre-commit-and-pre-merge-commit-hooks.sh | 13 +++++++++++++\n 4 files changed, 29 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 69ac78d5e5..a19c6478eb 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@@ -985,7 +986,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 74829a838e..c1a9d0083d 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -41,6 +41,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@@ -829,7 +830,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/commit.c b/commit.c\nindex 4ce8cb38d5..c7a243e848 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@@ -1632,8 +1633,13 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n {\n \tstruct strvec hook_env = STRVEC_INIT;\n \tva_list args;\n+\tconst char *arg;\n+\tstruct strvec hook_args = STRVEC_INIT;\n+\tstruct strbuf hook_name = STRBUF_INIT;\n \tint ret;\n \n+\tstrbuf_addstr(&hook_name, name);\n+\n \tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n \n \t/*\n@@ -1643,9 +1649,14 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n \t\tstrvec_push(&hook_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(&hook_args, arg);\n \tva_end(args);\n+\n+\tret = run_hooks(hook_env.v, &hook_name, &hook_args);\n \tstrvec_clear(&hook_env);\n+\tstrvec_clear(&hook_args);\n+\tstrbuf_release(&hook_name);\n \n \treturn ret;\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 b3485450a2..cef8085dcc 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@@ -103,6 +103,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.28.0.rc0.142.g3c755180ce-goog\n\n"},{"id":"405285","messageId":"xmqqwo12u1wz.fsf@gitster.c.googlers.com","threadId":"53527","inReplyTo":"20200909004939.1942347-8-emilyshaffer@google.com","subject":"Re: [PATCH v4 7/9] hook: replace run-command.h:find_hook","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2020-09-09T20:32:12Z","receivedAt":"2020-09-09T20:32:19Z","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> Add a helper to easily determine whether any hooks exist for a given\n> hook event.\n>\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  hook.c | 9 +++++++++\n>  hook.h | 1 +\n>  2 files changed, 10 insertions(+)\n\nShould we consider the last three patches still work-in-progress\ntechnology demonstration, or are these meant as a proposal for a new\nAPI element as-is?\n\nIt is perfectly fine if it is the former.  I just want to make sure\nwe share a common understanding on the direction in which we want\nthese patches to take us.  Here is my take:\n\n - For now, a hook/event that is aware of the config-based hook\n   system is supposed to use hook_exists(), while the traditional\n   ones still use find_hook().  We expect more and more will be\n   converted to the former over time.\n\n - Invoking hook scripts under the new world order is done by\n   including hook.h and calling run_hooks(), not by driving the\n   run-command API yourself (I count run_hook_ve() as part of the\n   latter) like the traditional code did.  We expect more and more\n   will be converted to the former over time.\n\n - From the point of view of the end users who have been happily\n   using scripts in $GIT_DIR/hooks, everything will stay the same.\n   hook_exists() will find them (by calling find_hook() as a\n   fallback) and run_hooks() will run them (by relying on\n   hook_list() to include them).\n\nI am guessing that the above gives us a high-level description.\n\nThe new interface needs to be described in hook.h once the series\ngraduates from the technology demonstration state, in order to help\nothers who want to help updating the callsites of traditional hooks\nto the new API.  And the above three-bullet point list is my attempt\nto figure out what kind of things need to be documented to help\nthem.\n\nI am not seeing anything in run_hooks() that consumes input from us\nover pipe, by the way, without which we cannot do things like the\n\"pre-receive\" hooks under the new world order.  Are they planned to\ncome in the future, after these \"we feed anything they need from the\ncommand line and from the enviornment\" hooks are dealt with in this\nfirst pass?\n\nThanks.\n\n> diff --git a/hook.c b/hook.c\n> index 0dab981681..7c7b922369 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -111,6 +111,15 @@ struct list_head* hook_list(const struct strbuf* hookname)\n>  \treturn &hook_head;\n>  }\n>  \n> +int hook_exists(const char *hookname)\n> +{\n> +\tconst char *value = NULL;\n> +\tstruct strbuf hook_key = STRBUF_INIT;\n> +\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n> +\n> +\treturn (!git_config_get_value(hook_key.buf, &value)) || !!find_hook(hookname);\n> +}\n> +\n>  int run_hooks(const char *const *env, const struct strbuf *hookname,\n>  \t      const struct strvec *args)\n>  {\n> diff --git a/hook.h b/hook.h\n> index d020788a6b..d94511b609 100644\n> --- a/hook.h\n> +++ b/hook.h\n> @@ -11,6 +11,7 @@ struct hook\n>  };\n>  \n>  struct list_head* hook_list(const struct strbuf *hookname);\n> +int hook_exists(const char *hookname);\n>  int run_hooks(const char *const *env, const struct strbuf *hookname,\n>  \t      const struct strvec *args);\n"},{"id":"405289","messageId":"xmqqeenau0eg.fsf@gitster.c.googlers.com","threadId":"53527","inReplyTo":"20200909004939.1942347-1-emilyshaffer@google.com","subject":"Re: [PATCH v4 0/9] propose config-based hooks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2020-09-09T21:04:55Z","receivedAt":"2020-09-09T21:05: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> Since v3, the biggest change is the conversion of commit hooks to use the new\n> hook machinery. The first change (\"commit: use config-based hooks\") is the\n> important part; the second change (\"run_commit_hook: take strvec instead of varargs\")\n> is probably subjective, but I thought it was a decent tech debt reduction.\n>\n> I wanted to send this reroll quickly since I had promised it in standup last\n> week, but I've got pretty good progress locally on the patch for configuring\n> \"hook.runHookDir\"; I'm planning to send that soon, probably this week.\n\nI've had the attached merge-fix patch as a way to adjust argv_array\nto strvec transition [*1*], but now *most* but not all parts of this\nseries have been migrated to the strvec API, you should apply some\nparts in the merge-fix patch to your copy.  I think the changes in\nthe old \"merge-fix\" patch to *.c and *.h are already in your series\nthat has been rebased on a newer 'master' that has strvec, but\ndocumentation and possibly in-code comments may need to be adjusted.\n\nAnother way to sanity check the result would be to run this:\n\n    $ git diff master..es/config-hooks | grep -i argv.array\n\nThanks.  \n\n[Footnote]\n\n*1* The way I work with a topic that causes conflicts with other\n    topics is to merge a new version of topic and letting the rerere\n    records I created while resolving the conflicts with the\n    previous round.  After textual conflicts are thusly resolved, if\n    there are further changes that do not cause textual conflict\n    that are necessary, they are written in the form of a\n    \"merge-fix\" patch like the attached.\n\n-- >8 --\n\n Documentation/technical/api-parse-options.txt  |  4 ++--\n Documentation/technical/config-based-hooks.txt |  4 ++--\n builtin/hook.c                                 | 16 ++++++++--------\n hook.c                                         |  6 +++---\n hook.h                                         |  4 ++--\n parse-options-cb.c                             |  8 ++++----\n parse-options.h                                |  6 +++---\n 7 files changed, 24 insertions(+), 24 deletions(-)\n\ndiff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt\nindex b4f1fc4a1a..679bd98629 100644\n--- a/Documentation/technical/api-parse-options.txt\n+++ b/Documentation/technical/api-parse-options.txt\n@@ -173,9 +173,9 @@ 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_ARGV_ARRAY(short, long, &struct argv_array, arg_str, description)`::\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 `argv_array`.\n+\tThe string argument is stored as an element in `strvec`.\n \tUse of `--no-option` will clear the list of preceding values.\n \n `OPT_INTEGER(short, long, &int_var, description)`::\ndiff --git a/Documentation/technical/config-based-hooks.txt b/Documentation/technical/config-based-hooks.txt\nindex c6e762b192..4443f70ded 100644\n--- a/Documentation/technical/config-based-hooks.txt\n+++ b/Documentation/technical/config-based-hooks.txt\n@@ -106,10 +106,10 @@ a concise config afterwards. It may take a form similar to `git rebase\n `hook.c` and `hook.h` are responsible for interacting with the config files. In\n the case when the code generating a hook event doesn't have special concerns\n about how to run the hooks, the hook library will provide a basic API to call\n-all hooks in config order with an `argv_array` provided by the code which\n+all hooks in config order with an `strvec` provided by the code which\n generates the hook event:\n \n-*`int run_hooks(const char *hookname, struct argv_array *args)`*\n+*`int run_hooks(const char *hookname, struct strvec *args)`*\n \n This call includes the hook command provided by `run-command.h:find_hook()`;\n eventually, this legacy hook will be gated by a config `hook.runHookDir`. The\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex cd61fad5fb..debcb5a77a 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -5,7 +5,7 @@\n #include \"hook.h\"\n #include \"parse-options.h\"\n #include \"strbuf.h\"\n-#include \"argv-array.h\"\n+#include \"strvec.h\"\n \n static const char * const builtin_hook_usage[] = {\n \tN_(\"git hook list <hookname>\"),\n@@ -67,14 +67,14 @@ 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 argv_array env_argv = ARGV_ARRAY_INIT;\n-\tstruct argv_array arg_argv = ARGV_ARRAY_INIT;\n+\tstruct strvec env_argv = STRVEC_INIT;\n+\tstruct strvec arg_argv = STRVEC_INIT;\n \n \tstruct option run_options[] = {\n-\t\tOPT_ARGV_ARRAY('e', \"env\", &env_argv, N_(\"var\"),\n-\t\t\t       N_(\"environment variables for hook to use\")),\n-\t\tOPT_ARGV_ARRAY('a', \"arg\", &arg_argv, N_(\"args\"),\n-\t\t\t       N_(\"argument to pass to hook\")),\n+\t\tOPT_STRVEC('e', \"env\", &env_argv, N_(\"var\"),\n+\t\t\t   N_(\"environment variables for hook to use\")),\n+\t\tOPT_STRVEC('a', \"arg\", &arg_argv, N_(\"args\"),\n+\t\t\t   N_(\"argument to pass to hook\")),\n \t\tOPT_END(),\n \t};\n \n@@ -87,7 +87,7 @@ static int run(int argc, const char **argv, const char *prefix)\n \n \tstrbuf_addstr(&hookname, argv[0]);\n \n-\treturn run_hooks(env_argv.argv, &hookname, &arg_argv);\n+\treturn run_hooks(env_argv.v, &hookname, &arg_argv);\n }\n \n int cmd_hook(int argc, const char **argv, const char *prefix)\ndiff --git a/hook.c b/hook.c\nindex 902e213173..40d319adb1 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -98,7 +98,7 @@ struct list_head* hook_list(const struct strbuf* hookname)\n }\n \n int run_hooks(const char *const *env, const struct strbuf *hookname,\n-\t      const struct argv_array *args)\n+\t      const struct strvec *args)\n {\n \tstruct list_head *to_run, *pos = NULL, *tmp = NULL;\n \tint rc = 0;\n@@ -110,14 +110,14 @@ int run_hooks(const char *const *env, const struct strbuf *hookname,\n \t\tstruct hook *hook = list_entry(pos, struct hook, list);\n \n \t\t/* add command */\n-\t\targv_array_push(&hook_proc.args, hook->command.buf);\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\tif (args)\n-\t\t\targv_array_pushv(&hook_proc.args, args->argv);\n+\t\t\tstrvec_pushv(&hook_proc.args, args->v);\n \n \t\thook_proc.env = env;\n \t\thook_proc.no_stdin = 1;\ndiff --git a/hook.h b/hook.h\nindex cf598d6ccb..d020788a6b 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,7 +1,7 @@\n #include \"config.h\"\n #include \"list.h\"\n #include \"strbuf.h\"\n-#include \"argv-array.h\"\n+#include \"strvec.h\"\n \n struct hook\n {\n@@ -12,7 +12,7 @@ struct hook\n \n struct list_head* hook_list(const struct strbuf *hookname);\n int run_hooks(const char *const *env, const struct strbuf *hookname,\n-\t      const struct argv_array *args);\n+\t      const struct strvec *args);\n \n void free_hook(struct hook *ptr);\n void clear_hook_list(void);\ndiff --git a/parse-options-cb.c b/parse-options-cb.c\nindex 4f993cd734..d2b8b7b98a 100644\n--- a/parse-options-cb.c\n+++ b/parse-options-cb.c\n@@ -205,19 +205,19 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset)\n \treturn 0;\n }\n \n-int parse_opt_argv_array(const struct option *opt, const char *arg, int unset)\n+int parse_opt_strvec(const struct option *opt, const char *arg, int unset)\n {\n-\tstruct argv_array *v = opt->value;\n+\tstruct strvec *v = opt->value;\n \n \tif (unset) {\n-\t\targv_array_clear(v);\n+\t\tstrvec_clear(v);\n \t\treturn 0;\n \t}\n \n \tif (!arg)\n \t\treturn -1;\n \n-\targv_array_push(v, arg);\n+\tstrvec_push(v, arg);\n \treturn 0;\n }\n \ndiff --git a/parse-options.h b/parse-options.h\nindex e2e2de75c8..177259488b 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -177,9 +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_ARGV_ARRAY(s, l, v, a, h) \\\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_argv_array }\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@@ -299,7 +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_argv_array(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.28.0-558-g7a0184fd7b\n\n"},{"id":"405362","messageId":"20200910190836.GD331156@google.com","threadId":"53527","inReplyTo":"xmqqwo12u1wz.fsf@gitster.c.googlers.com","subject":"Re: [PATCH v4 7/9] hook: replace run-command.h:find_hook","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-10T19:08:36Z","receivedAt":"2020-09-10T19:10:54Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Sep 09, 2020 at 01:32:12PM -0700, Junio C Hamano wrote:\n> \n> Emily Shaffer <emilyshaffer@google.com> writes:\n> \n> > Add a helper to easily determine whether any hooks exist for a given\n> > hook event.\n> >\n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >  hook.c | 9 +++++++++\n> >  hook.h | 1 +\n> >  2 files changed, 10 insertions(+)\n> \n> Should we consider the last three patches still work-in-progress\n> technology demonstration, or are these meant as a proposal for a new\n> API element as-is?\n\nThe former. I'm irritated with myself for spending a long time fidgeting\nwith the wording on this reroll and still forgetting to mark the last\nthree \"RFC\" as I had planned to do.\n\n> It is perfectly fine if it is the former.  I just want to make sure\n> we share a common understanding on the direction in which we want\n> these patches to take us.  Here is my take:\n> \n>  - For now, a hook/event that is aware of the config-based hook\n>    system is supposed to use hook_exists(), while the traditional\n>    ones still use find_hook().  We expect more and more will be\n>    converted to the former over time.\n> \n>  - Invoking hook scripts under the new world order is done by\n>    including hook.h and calling run_hooks(), not by driving the\n>    run-command API yourself (I count run_hook_ve() as part of the\n>    latter) like the traditional code did.  We expect more and more\n>    will be converted to the former over time.\n> \n>  - From the point of view of the end users who have been happily\n>    using scripts in $GIT_DIR/hooks, everything will stay the same.\n>    hook_exists() will find them (by calling find_hook() as a\n>    fallback) and run_hooks() will run them (by relying on\n>    hook_list() to include them).\n> \n> I am guessing that the above gives us a high-level description.\n\nYes. I am also working on a patch locally to include a config -\noptionally users could shut off the $GIT_DIR/hooks, but I don't see us\nmaking that the default behavior any time soon (or ever).\n\n> \n> The new interface needs to be described in hook.h once the series\n> graduates from the technology demonstration state, in order to help\n> others who want to help updating the callsites of traditional hooks\n> to the new API.  And the above three-bullet point list is my attempt\n> to figure out what kind of things need to be documented to help\n> them.\n\nSure. Agreed. Thanks for pointing it out - I had planned on updating the\n`git help hook` manpage but adding API comments in hook.h had slipped my\nmind, so the reminder is useful.\n\n> \n> I am not seeing anything in run_hooks() that consumes input from us\n> over pipe, by the way, without which we cannot do things like the\n> \"pre-receive\" hooks under the new world order.  Are they planned to\n> come in the future, after these \"we feed anything they need from the\n> command line and from the enviornment\" hooks are dealt with in this\n> first pass?\n\nI included this conversion to demonstrate the tech and give people\nsomething to look at (and shout to stop if so needed). I do plan to\ninclude hooks which need piped input; in fact, I'm hoping to target one\nsuch for the next conversion I do. The todo list looks like so:\n\n 1. semantics for checking hook.runHookDir config\n 2. convert all the hooks which take input in interesting ways (or, just\n all the hooks)\n 3. add user friendliness via 'git hook add', 'git hook edit', etc\n\n The config semantics are in progress and I'm hoping to send this week.\n\n As for submission plan, I don't mind including new architecture (if\n unused) except for the code bloat; I'd rather push all the\n \"conversions\" simultaneously, so users don't have to wonder \"is this\n hook a new and supported one, or not?\".  I don't mind adding the\n niceties ('git hook add' etc) later as the config is a little annoying\n for a human to write themselves, but not impossible.\n\n  - Emily\n"},{"id":"405381","messageId":"6ed77ee3-4d75-9ef9-0621-13a056e8ca14@gmail.com","threadId":"53527","inReplyTo":"20200909004939.1942347-10-emilyshaffer@google.com","subject":"Re: [PATCH v4 9/9] run_commit_hook: take strvec instead of varargs","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2020-09-10T14:16:32Z","receivedAt":"2020-09-10T21:14:42Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Emily\n\nOn 09/09/2020 01:49, Emily Shaffer wrote:\n> Taking varargs in run_commit_hook() led to some bizarre patterns, like\n> callers using two string variables (which may or may not be filled) to\n> express different argument lists for the commit hooks. Because\n> run_commit_hook() no longer needs to call a variadic function for the\n> hook run itself, we can use strvec to make the calling code more\n> conventional.\n> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>   builtin/commit.c | 46 +++++++++++++++++++++++-----------------------\n>   builtin/merge.c  | 20 ++++++++++++++++----\n>   commit.c         | 13 ++-----------\n>   commit.h         |  5 +++--\n>   sequencer.c      | 15 ++++++++-------\n>   5 files changed, 52 insertions(+), 47 deletions(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index a19c6478eb..f029d4f5ac 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -691,8 +691,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \tstruct strbuf committer_ident = STRBUF_INIT;\n>   \tint committable;\n>   \tstruct strbuf sb = STRBUF_INIT;\n> -\tconst char *hook_arg1 = NULL;\n> -\tconst char *hook_arg2 = NULL;\n> +\tstruct strvec hook_args = STRVEC_INIT;\n>   \tint clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE);\n>   \tint old_display_comment_prefix;\n>   \tint merge_contains_scissors = 0;\n> @@ -700,7 +699,8 @@ 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, index_file, \"pre-commit\",\n> +\t\t\t\t\t  &hook_args))\n>   \t\treturn 0;\n>   \n>   \tif (squash_message) {\n> @@ -722,27 +722,28 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \t\t}\n>   \t}\n>   \n> +\tstrvec_push(&hook_args, git_path_commit_editmsg());\n\nThis is a long way from the call where we use hook_args. With the \nvariadic interface it is clear by looking at the call to \nrun_commit_hook() what the first argument is and that is always the same.\n\n>   \tif (have_option_m && !fixup_message) {\n>   \t\tstrbuf_addbuf(&sb, &message);\n> -\t\thook_arg1 = \"message\";\n> +\t\tstrvec_push(&hook_args, \"message\");\n>   \t} else if (logfile && !strcmp(logfile, \"-\")) {\n>   \t\tif (isatty(0))\n>   \t\t\tfprintf(stderr, _(\"(reading log message from standard input)\\n\"));\n>   \t\tif (strbuf_read(&sb, 0, 0) < 0)\n>   \t\t\tdie_errno(_(\"could not read log from standard input\"));\n> -\t\thook_arg1 = \"message\";\n> +\t\tstrvec_push(&hook_args, \"message\");\n>   \t} else if (logfile) {\n>   \t\tif (strbuf_read_file(&sb, logfile, 0) < 0)\n>   \t\t\tdie_errno(_(\"could not read log file '%s'\"),\n>   \t\t\t\t  logfile);\n> -\t\thook_arg1 = \"message\";\n> +\t\tstrvec_push(&hook_args, \"message\");\n>   \t} else if (use_message) {\n>   \t\tchar *buffer;\n>   \t\tbuffer = strstr(use_message_buffer, \"\\n\\n\");\n>   \t\tif (buffer)\n>   \t\t\tstrbuf_addstr(&sb, skip_blank_lines(buffer + 2));\n> -\t\thook_arg1 = \"commit\";\n> -\t\thook_arg2 = use_message;\n> +\t\tstrvec_pushl(&hook_args, \"commit\", use_message, NULL);\n>   \t} else if (fixup_message) {\n>   \t\tstruct pretty_print_context ctx = {0};\n>   \t\tstruct commit *commit;\n> @@ -754,7 +755,7 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \t\t\t\t      &sb, &ctx);\n>   \t\tif (have_option_m)\n>   \t\t\tstrbuf_addbuf(&sb, &message);\n> -\t\thook_arg1 = \"message\";\n> +\t\tstrvec_push(&hook_args, \"message\");\n>   \t} else if (!stat(git_path_merge_msg(the_repository), &statbuf)) {\n>   \t\tsize_t merge_msg_start;\n>   \n> @@ -765,9 +766,9 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \t\tif (!stat(git_path_squash_msg(the_repository), &statbuf)) {\n>   \t\t\tif (strbuf_read_file(&sb, git_path_squash_msg(the_repository), 0) < 0)\n>   \t\t\t\tdie_errno(_(\"could not read SQUASH_MSG\"));\n> -\t\t\thook_arg1 = \"squash\";\n> +\t\t\tstrvec_push(&hook_args, \"squash\");\n>   \t\t} else\n> -\t\t\thook_arg1 = \"merge\";\n> +\t\t\tstrvec_push(&hook_args, \"merge\");\n>   \n>   \t\tmerge_msg_start = sb.len;\n>   \t\tif (strbuf_read_file(&sb, git_path_merge_msg(the_repository), 0) < 0)\n> @@ -781,11 +782,11 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \t} else if (!stat(git_path_squash_msg(the_repository), &statbuf)) {\n>   \t\tif (strbuf_read_file(&sb, git_path_squash_msg(the_repository), 0) < 0)\n>   \t\t\tdie_errno(_(\"could not read SQUASH_MSG\"));\n> -\t\thook_arg1 = \"squash\";\n> +\t\tstrvec_push(&hook_args, \"squash\");\n>   \t} else if (template_file) {\n>   \t\tif (strbuf_read_file(&sb, template_file, 0) < 0)\n>   \t\t\tdie_errno(_(\"could not read '%s'\"), template_file);\n> -\t\thook_arg1 = \"template\";\n> +\t\tstrvec_push(&hook_args, \"template\");\n>   \t\tclean_message_contents = 0;\n>   \t}\n>   \n> @@ -794,11 +795,9 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \t * just set the argument(s) to the prepare-commit-msg hook.\n>   \t */\n>   \telse if (whence == FROM_MERGE)\n> -\t\thook_arg1 = \"merge\";\n> -\telse if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) {\n> -\t\thook_arg1 = \"commit\";\n> -\t\thook_arg2 = \"CHERRY_PICK_HEAD\";\n> -\t}\n> +\t\tstrvec_push(&hook_args, \"merge\");\n> +\telse if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK)\n> +\t\tstrvec_pushl(&hook_args, \"commit\", \"CHERRY_PICK_HEAD\", NULL);\n>   \n>   \tif (squash_message) {\n>   \t\t/*\n> @@ -806,8 +805,8 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \t\t * then we're possibly hijacking other commit log options.\n>   \t\t * Reset the hook args to tell the real story.\n>   \t\t */\n> -\t\thook_arg1 = \"message\";\n> -\t\thook_arg2 = \"\";\n> +\t\tstrvec_clear(&hook_args);\n> +\t\tstrvec_pushl(&hook_args, git_path_commit_editmsg(), \"message\", NULL);\n\nIt's a shame we have to clear the strvec and remember to re-add \ngit_path_commit_editmsg() here.\n\n>   \t}\n>   \n>   \ts->fp = fopen_for_writing(git_path_commit_editmsg());\n> @@ -1001,8 +1000,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> -\t\t\t    git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n> +\tif (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\", &hook_args))\n>   \t\treturn 0;\n>   \n>   \tif (use_editor) {\n> @@ -1017,8 +1015,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,\n>   \t\tstrvec_clear(&env);\n>   \t}\n>   \n> +\tstrvec_clear(&hook_args);\n> +\tstrvec_push(&hook_args, git_path_commit_editmsg());\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, \"commit-msg\", &hook_args)) {\n>   \t\treturn 0;\n>   \t}\n>   \n> diff --git a/builtin/merge.c b/builtin/merge.c\n> index c1a9d0083d..863c9039a3 100644\n> --- a/builtin/merge.c\n> +++ b/builtin/merge.c\n> @@ -821,10 +821,14 @@ static void write_merge_heads(struct commit_list *);\n>   static void prepare_to_commit(struct commit_list *remoteheads)\n>   {\n>   \tstruct strbuf msg = STRBUF_INIT;\n> +\tstruct strvec hook_args = STRVEC_INIT;\n> +\tstruct strbuf hook_name = STRBUF_INIT;\n\nAs far as I can see hook_name is never used except to free it at the end.\n\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, index_file,\n> +\t\t\t\t\t  \"pre-merge-commit\", &hook_args))\n>   \t\tabort_commit(remoteheads, NULL);\n> +\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> @@ -832,6 +836,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n>   \t */\n>   \tif (hook_exists(\"pre-merge-commit\"))\n>   \t\tdiscard_cache();\n> +\n>   \tread_cache_from(index_file);\n>   \tstrbuf_addbuf(&msg, &merge_msg);\n>   \tif (squash)\n> @@ -851,17 +856,22 @@ 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> +\n> +\tstrvec_clear(&hook_args);\n> +\tstrvec_pushl(&hook_args, git_path_merge_msg(the_repository), \"merge\", NULL);\n>   \tif (run_commit_hook(0 < option_edit, get_index_file(), \"prepare-commit-msg\",\n> -\t\t\t    git_path_merge_msg(the_repository), \"merge\", NULL))\n> +\t\t\t    &hook_args))\n>   \t\tabort_commit(remoteheads, NULL);\n> +\n>   \tif (0 < option_edit) {\n>   \t\tif (launch_editor(git_path_merge_msg(the_repository), NULL, NULL))\n>   \t\t\tabort_commit(remoteheads, NULL);\n>   \t}\n>   \n> +\tstrvec_clear(&hook_args);\n> +\tstrvec_push(&hook_args, git_path_merge_msg(the_repository));\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  git_path_merge_msg(the_repository), NULL))\n> +\t\t\t\t\t  \"commit-msg\", &hook_args))\n>   \t\tabort_commit(remoteheads, NULL);\n>   \n>   \tread_merge_msg(&msg);\n> @@ -871,6 +881,8 @@ static void prepare_to_commit(struct commit_list *remoteheads)\n>   \tstrbuf_release(&merge_msg);\n>   \tstrbuf_addbuf(&merge_msg, &msg);\n>   \tstrbuf_release(&msg);\n> +\tstrbuf_release(&hook_name);\n> +\tstrvec_clear(&hook_args);\n>   }\n>   \n>   static int merge_trivial(struct commit *head, struct commit_list *remoteheads)\n> diff --git a/commit.c b/commit.c\n> index c7a243e848..726407152c 100644\n> --- a/commit.c\n> +++ b/commit.c\n> @@ -1629,12 +1629,9 @@ 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    const char *name, ...)\n> +\t\t    const char *name, struct strvec *args)\n>   {\n>   \tstruct strvec hook_env = STRVEC_INIT;\n> -\tva_list args;\n> -\tconst char *arg;\n> -\tstruct strvec hook_args = STRVEC_INIT;\n>   \tstruct strbuf hook_name = STRBUF_INIT;\n>   \tint ret;\n>   \n> @@ -1648,14 +1645,8 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n>   \tif (!editor_is_used)\n>   \t\tstrvec_push(&hook_env, \"GIT_EDITOR=:\");\n>   \n> -\tva_start(args, name);\n> -\twhile ((arg = va_arg(args, const char *)))\n> -\t\tstrvec_push(&hook_args, arg);\n> -\tva_end(args);\n> -\n> -\tret = run_hooks(hook_env.v, &hook_name, &hook_args);\n> +\tret = run_hooks(hook_env.v, &hook_name, args);\n>   \tstrvec_clear(&hook_env);\n> -\tstrvec_clear(&hook_args);\n>   \tstrbuf_release(&hook_name);\n>   \n>   \treturn ret;\n> diff --git a/commit.h b/commit.h\n> index e901538909..978da3c3e0 100644\n> --- a/commit.h\n> +++ b/commit.h\n> @@ -9,6 +9,7 @@\n>   #include \"string-list.h\"\n>   #include \"pretty.h\"\n>   #include \"commit-slab.h\"\n> +#include \"strvec.h\"\n>   \n>   #define COMMIT_NOT_FROM_GRAPH 0xFFFFFFFF\n>   #define GENERATION_NUMBER_INFINITY 0xFFFFFFFF\n> @@ -353,7 +354,7 @@ void verify_merge_signature(struct commit *commit, int verbose,\n>   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    const char *name, struct strvec *args);\n>   \n>   #endif /* COMMIT_H */\n> diff --git a/sequencer.c b/sequencer.c\n> index cc3f8fa88e..5dd4b134d6 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -1124,22 +1124,23 @@ static int run_prepare_commit_msg_hook(struct repository *r,\n>   \t\t\t\t       const char *commit)\n>   {\n>   \tint ret = 0;\n> -\tconst char *name, *arg1 = NULL, *arg2 = NULL;\n> +\tstruct strvec args = STRVEC_INIT;\n> +\tconst char *name = git_path_commit_editmsg();\n>   \n> -\tname = git_path_commit_editmsg();\n> +\tstrvec_push(&args, name);\n\nI think you could drop name altogether and just pass \ngit_path_commit_editmsg() instead.\n\n>   \tif (write_message(msg->buf, msg->len, name, 0))\n>   \t\treturn -1;\n>   \n>   \tif (commit) {\n> -\t\targ1 = \"commit\";\n> -\t\targ2 = commit;\n> +\t\tstrvec_push(&args, \"commit\");\n> +\t\tstrvec_push(&args, commit);\n\nComplete nit pick but the other conversions all used strvec_pushl() \nrather than two strvec_push() calls.\n\nI don't have a strong opinion about these changes (though I'm not \nparticularly enthusiastic). Having to push the arguments in order is not \nparticularly convenient and the use of strvec_pushl() means we are \nreplacing a small number of variadic calls to run_commit_hook() with a \nlarger number of calls to a different variadic interface.\n\nBest Wishes\n\nPhillip\n\n>   \t} else {\n> -\t\targ1 = \"message\";\n> +\t\tstrvec_push(&args, \"message\");\n>   \t}\n> -\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n> -\t\t\t    arg1, arg2, NULL))\n> +\tif (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", &args))\n>   \t\tret = error(_(\"'prepare-commit-msg' hook failed\"));\n>   \n> +\tstrvec_clear(&args);\n>   \treturn ret;\n>   }\n>   \n> \n"},{"id":"405383","messageId":"a81b058c-b193-7020-30c9-9238407889ba@gmail.com","threadId":"53527","inReplyTo":"20200909004939.1942347-9-emilyshaffer@google.com","subject":"Re: [PATCH v4 8/9] commit: use config-based hooks","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2020-09-10T13:50:39Z","receivedAt":"2020-09-10T21:33:53Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Emily\n\nOn 09/09/2020 01:49, Emily Shaffer wrote:\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> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>   builtin/commit.c                                 |  3 ++-\n>   builtin/merge.c                                  |  3 ++-\n>   commit.c                                         | 13 ++++++++++++-\n>   t/t7503-pre-commit-and-pre-merge-commit-hooks.sh | 13 +++++++++++++\n>   4 files changed, 29 insertions(+), 3 deletions(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 69ac78d5e5..a19c6478eb 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> @@ -985,7 +986,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 74829a838e..c1a9d0083d 100644\n> --- a/builtin/merge.c\n> +++ b/builtin/merge.c\n> @@ -41,6 +41,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> @@ -829,7 +830,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/commit.c b/commit.c\n> index 4ce8cb38d5..c7a243e848 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> @@ -1632,8 +1633,13 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n>   {\n>   \tstruct strvec hook_env = STRVEC_INIT;\n>   \tva_list args;\n> +\tconst char *arg;\n> +\tstruct strvec hook_args = STRVEC_INIT;\n> +\tstruct strbuf hook_name = STRBUF_INIT;\n>   \tint ret;\n>   \n> +\tstrbuf_addstr(&hook_name, name);\n\nSeeing this makes me wonder if it would be better for run_hooks() to \ntake a string for the name rather than an strbuf, I suspect that \nvirtually all callers have a fixed hook name.\n\nBest Wishes\n\nPhillip\n\n>   \tstrvec_pushf(&hook_env, \"GIT_INDEX_FILE=%s\", index_file);\n>   \n>   \t/*\n> @@ -1643,9 +1649,14 @@ int run_commit_hook(int editor_is_used, const char *index_file,\n>   \t\tstrvec_push(&hook_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(&hook_args, arg);\n>   \tva_end(args);\n> +\n> +\tret = run_hooks(hook_env.v, &hook_name, &hook_args);\n>   \tstrvec_clear(&hook_env);\n> +\tstrvec_clear(&hook_args);\n> +\tstrbuf_release(&hook_name);\n>   \n>   \treturn ret;\n>   }\n> diff --git a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh\n> index b3485450a2..cef8085dcc 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> @@ -103,6 +103,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> \n"},{"id":"405388","messageId":"xmqqy2lhnuh0.fsf@gitster.c.googlers.com","threadId":"53527","inReplyTo":"a81b058c-b193-7020-30c9-9238407889ba@gmail.com","subject":"Re: [PATCH v4 8/9] commit: use config-based hooks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2020-09-10T22:21:47Z","receivedAt":"2020-09-10T22:21:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>> +\tconst char *arg;\n>> +\tstruct strvec hook_args = STRVEC_INIT;\n>> +\tstruct strbuf hook_name = STRBUF_INIT;\n>>   \tint ret;\n>>   +\tstrbuf_addstr(&hook_name, name);\n>\n> Seeing this makes me wonder if it would be better for run_hooks() to\n> take a string for the name rather than an strbuf, I suspect that\n> virtually all callers have a fixed hook name.\n\nYeah, that is a good point.  It is always a good discipline to keep\nthe type of the parameters callers need to pass to the minimum.\n\n\n\n"},{"id":"405403","messageId":"753f9bef-29ae-88b7-336e-fd799ffd1e9a@gmail.com","threadId":"53527","inReplyTo":"6ed77ee3-4d75-9ef9-0621-13a056e8ca14@gmail.com","subject":"Re: [PATCH v4 9/9] run_commit_hook: take strvec instead of varargs","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2020-09-11T13:20:00Z","receivedAt":"2020-09-11T13:23:16Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 10/09/2020 15:16, Phillip Wood wrote:\n> Hi Emily\n> \n> On 09/09/2020 01:49, Emily Shaffer wrote:\n>> Taking varargs in run_commit_hook() led to some bizarre patterns, like\n>> callers using two string variables (which may or may not be filled) to\n>> express different argument lists for the commit hooks. Because\n>> run_commit_hook() no longer needs to call a variadic function for the\n>> hook run itself, we can use strvec to make the calling code more\n>> conventional.\n>>\n>> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n>> ---\n>>   builtin/commit.c | 46 +++++++++++++++++++++++-----------------------\n>>   builtin/merge.c  | 20 ++++++++++++++++----\n>>   commit.c         | 13 ++-----------\n>>   commit.h         |  5 +++--\n>>   sequencer.c      | 15 ++++++++-------\n>>   5 files changed, 52 insertions(+), 47 deletions(-)\n>>\n>> diff --git a/builtin/commit.c b/builtin/commit.c\n>> index a19c6478eb..f029d4f5ac 100644\n>> --- a/builtin/commit.c\n>> +++ b/builtin/commit.c\n>> @@ -691,8 +691,7 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>       struct strbuf committer_ident = STRBUF_INIT;\n>>       int committable;\n>>       struct strbuf sb = STRBUF_INIT;\n>> -    const char *hook_arg1 = NULL;\n>> -    const char *hook_arg2 = NULL;\n>> +    struct strvec hook_args = STRVEC_INIT;\n>>       int clean_message_contents = (cleanup_mode != \n>> COMMIT_MSG_CLEANUP_NONE);\n>>       int old_display_comment_prefix;\n>>       int merge_contains_scissors = 0;\n>> @@ -700,7 +699,8 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>       /* This checks and barfs if author is badly specified */\n>>       determine_author_info(author_ident);\n>> -    if (!no_verify && run_commit_hook(use_editor, index_file, \n>> \"pre-commit\", NULL))\n>> +    if (!no_verify && run_commit_hook(use_editor, index_file, \n>> \"pre-commit\",\n>> +                      &hook_args))\n>>           return 0;\n>>       if (squash_message) {\n>> @@ -722,27 +722,28 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>           }\n>>       }\n>> +    strvec_push(&hook_args, git_path_commit_editmsg());\n> \n> This is a long way from the call where we use hook_args. With the \n> variadic interface it is clear by looking at the call to \n> run_commit_hook() what the first argument is and that is always the same.\n> \n>>       if (have_option_m && !fixup_message) {\n>>           strbuf_addbuf(&sb, &message);\n>> -        hook_arg1 = \"message\";\n>> +        strvec_push(&hook_args, \"message\");\n>>       } else if (logfile && !strcmp(logfile, \"-\")) {\n>>           if (isatty(0))\n>>               fprintf(stderr, _(\"(reading log message from standard \n>> input)\\n\"));\n>>           if (strbuf_read(&sb, 0, 0) < 0)\n>>               die_errno(_(\"could not read log from standard input\"));\n>> -        hook_arg1 = \"message\";\n>> +        strvec_push(&hook_args, \"message\");\n>>       } else if (logfile) {\n>>           if (strbuf_read_file(&sb, logfile, 0) < 0)\n>>               die_errno(_(\"could not read log file '%s'\"),\n>>                     logfile);\n>> -        hook_arg1 = \"message\";\n>> +        strvec_push(&hook_args, \"message\");\n>>       } else if (use_message) {\n>>           char *buffer;\n>>           buffer = strstr(use_message_buffer, \"\\n\\n\");\n>>           if (buffer)\n>>               strbuf_addstr(&sb, skip_blank_lines(buffer + 2));\n>> -        hook_arg1 = \"commit\";\n>> -        hook_arg2 = use_message;\n>> +        strvec_pushl(&hook_args, \"commit\", use_message, NULL);\n>>       } else if (fixup_message) {\n>>           struct pretty_print_context ctx = {0};\n>>           struct commit *commit;\n>> @@ -754,7 +755,7 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>                         &sb, &ctx);\n>>           if (have_option_m)\n>>               strbuf_addbuf(&sb, &message);\n>> -        hook_arg1 = \"message\";\n>> +        strvec_push(&hook_args, \"message\");\n>>       } else if (!stat(git_path_merge_msg(the_repository), &statbuf)) {\n>>           size_t merge_msg_start;\n>> @@ -765,9 +766,9 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>           if (!stat(git_path_squash_msg(the_repository), &statbuf)) {\n>>               if (strbuf_read_file(&sb, \n>> git_path_squash_msg(the_repository), 0) < 0)\n>>                   die_errno(_(\"could not read SQUASH_MSG\"));\n>> -            hook_arg1 = \"squash\";\n>> +            strvec_push(&hook_args, \"squash\");\n>>           } else\n>> -            hook_arg1 = \"merge\";\n>> +            strvec_push(&hook_args, \"merge\");\n>>           merge_msg_start = sb.len;\n>>           if (strbuf_read_file(&sb, \n>> git_path_merge_msg(the_repository), 0) < 0)\n>> @@ -781,11 +782,11 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>       } else if (!stat(git_path_squash_msg(the_repository), &statbuf)) {\n>>           if (strbuf_read_file(&sb, \n>> git_path_squash_msg(the_repository), 0) < 0)\n>>               die_errno(_(\"could not read SQUASH_MSG\"));\n>> -        hook_arg1 = \"squash\";\n>> +        strvec_push(&hook_args, \"squash\");\n>>       } else if (template_file) {\n>>           if (strbuf_read_file(&sb, template_file, 0) < 0)\n>>               die_errno(_(\"could not read '%s'\"), template_file);\n>> -        hook_arg1 = \"template\";\n>> +        strvec_push(&hook_args, \"template\");\n>>           clean_message_contents = 0;\n>>       }\n>> @@ -794,11 +795,9 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>        * just set the argument(s) to the prepare-commit-msg hook.\n>>        */\n>>       else if (whence == FROM_MERGE)\n>> -        hook_arg1 = \"merge\";\n>> -    else if (is_from_cherry_pick(whence) || whence == \n>> FROM_REBASE_PICK) {\n>> -        hook_arg1 = \"commit\";\n>> -        hook_arg2 = \"CHERRY_PICK_HEAD\";\n>> -    }\n>> +        strvec_push(&hook_args, \"merge\");\n>> +    else if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK)\n>> +        strvec_pushl(&hook_args, \"commit\", \"CHERRY_PICK_HEAD\", NULL);\n>>       if (squash_message) {\n>>           /*\n>> @@ -806,8 +805,8 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>            * then we're possibly hijacking other commit log options.\n>>            * Reset the hook args to tell the real story.\n>>            */\n>> -        hook_arg1 = \"message\";\n>> -        hook_arg2 = \"\";\n>> +        strvec_clear(&hook_args);\n>> +        strvec_pushl(&hook_args, git_path_commit_editmsg(), \n>> \"message\", NULL);\n> \n> It's a shame we have to clear the strvec and remember to re-add \n> git_path_commit_editmsg() here.\n> \n>>       }\n>>       s->fp = fopen_for_writing(git_path_commit_editmsg());\n>> @@ -1001,8 +1000,7 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>           return 0;\n>>       }\n>> -    if (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\",\n>> -                git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL))\n>> +    if (run_commit_hook(use_editor, index_file, \"prepare-commit-msg\", \n>> &hook_args))\n>>           return 0;\n>>       if (use_editor) {\n>> @@ -1017,8 +1015,10 @@ static int prepare_to_commit(const char \n>> *index_file, const char *prefix,\n>>           strvec_clear(&env);\n>>       }\n>> +    strvec_clear(&hook_args);\n>> +    strvec_push(&hook_args, git_path_commit_editmsg());\n>>       if (!no_verify &&\n>> -        run_commit_hook(use_editor, index_file, \"commit-msg\", \n>> git_path_commit_editmsg(), NULL)) {\n>> +        run_commit_hook(use_editor, index_file, \"commit-msg\", \n>> &hook_args)) {\n>>           return 0;\n>>       }\n >[...]\n> \n> I don't have a strong opinion about these changes (though I'm not \n> particularly enthusiastic). Having to push the arguments in order is not \n> particularly convenient and the use of strvec_pushl() means we are \n> replacing a small number of variadic calls to run_commit_hook() with a \n> larger number of calls to a different variadic interface.\n\nOn reflection I think it is the conversion in builtin/commit.c rather \nthan the change in the API that makes me uncomfortable. If it kept \n`hook_arg1` and `hook_arg2` and just did\n\nstrvec_push(&hook_args, git_path_commit_editmsg())\\\nstrvec_push(&hook_args, hook_arg1);\nif (hook_arg2)\n\tstrvec_push(&hook_args, hook_arg2);\nrun_commit_hook(..., &hook_args);\n\nIt would keep the fixed first argument near the call to \nrun_commit_hook() and avoid the problem of having to clear hook_args in \nthe hunk at line 806.\n\nThank you for adding the last couple of patches that show an example \nconversion, it is really helpful to see how the API would be used.\n\nBest Wishes\n\nPhillip\n\n> Best Wishes\n> \n> Phillip\n> \n>>       } else {\n>> -        arg1 = \"message\";\n>> +        strvec_push(&args, \"message\");\n>>       }\n>> -    if (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", name,\n>> -                arg1, arg2, NULL))\n>> +    if (run_commit_hook(0, r->index_file, \"prepare-commit-msg\", &args))\n>>           ret = error(_(\"'prepare-commit-msg' hook failed\"));\n>> +    strvec_clear(&args);\n>>       return ret;\n>>   }\n>>\n\n"},{"id":"405404","messageId":"9146d342-7b3c-091f-4904-e7d8696544be@gmail.com","threadId":"53527","inReplyTo":"20200909004939.1942347-4-emilyshaffer@google.com","subject":"Re: [PATCH v4 3/9] hook: add list command","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2020-09-11T13:27:42Z","receivedAt":"2020-09-11T13:43:52Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Emily\n\nOn 09/09/2020 01:49, Emily Shaffer wrote:\n> Teach '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; at minimum, this subsection must 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>    ~/baz/from/hookcmd.sh\n>    ~/bar.sh\n> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>   Documentation/git-hook.txt    |  37 +++++++++++-\n>   Makefile                      |   1 +\n>   builtin/hook.c                |  55 ++++++++++++++++--\n>   hook.c                        | 102 ++++++++++++++++++++++++++++++++++\n>   hook.h                        |  15 +++++\n>   t/t1360-config-based-hooks.sh |  68 ++++++++++++++++++++++-\n>   6 files changed, 271 insertions(+), 7 deletions(-)\n>   create mode 100644 hook.c\n>   create mode 100644 hook.h\n> \n> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> index 2d50c414cc..e458586e96 100644\n> --- a/Documentation/git-hook.txt\n> +++ b/Documentation/git-hook.txt\n> @@ -8,12 +8,47 @@ git-hook - Manage configured hooks\n>   SYNOPSIS\n>   --------\n>   [verse]\n> -'git hook'\n> +'git hook' list <hook-name>\n>   \n>   DESCRIPTION\n>   -----------\n>   You can list, 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 config order. \"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\nI think it would be helpful to have a couple of lines explaining what \nthe example configuration sets up\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 note the config scope where the relevant\n> +`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n\nThanks for clarifying that it is the origin of the \nhook.<hook-name>.command that is printed. An example of the output of \nthe config above would be useful I think.\n\n>   GIT\n>   ---\n>   Part of the linkgit:git[1] suite\n> diff --git a/Makefile b/Makefile\n> index 6eee75555e..804de45b16 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -890,6 +890,7 @@ LIB_OBJS += grep.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 += interdiff.o\n>   LIB_OBJS += json-writer.o\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index b2bbc84d4d..a0759a4c26 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -1,21 +1,68 @@\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 hook *item;\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(\"a hookname must be provided to operate on.\",\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\treturn 0;\n> +\t}\n> +\n> +\tlist_for_each(pos, head) {\n> +\t\titem = list_entry(pos, struct hook, list);\n> +\t\tif (item)\n> +\t\t\tprintf(\"%s:\\t%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();\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..b006950eb8\n> --- /dev/null\n> +++ b/hook.c\n> @@ -0,0 +1,102 @@\n> +#include \"cache.h\"\n> +\n> +#include \"hook.h\"\n> +#include \"config.h\"\n> +\n> +/*\n> + * NEEDSWORK: a stateful hook_head means we can't run two hook events in the\n> + * background at the same time - which might be ok, or might not.\n> + *\n> + * Maybe it's better to cache a list head per hookname, since we can probably\n> + * guess that the hook list won't change during a user-initiated operation. For\n> + * now, within list_hooks, call clear_hook_list() at the outset.\n> + */\n> +static LIST_HEAD(hook_head);\n\nI can see a cache might be useful for the sequencer which needs to run \nthe prepare-msg hook for each commit (it should probably not be running \nthe post-commit hook but does at the moment) and for am which runs some \nhooks for each patch but until then I'm not sure why we need a global \nvariable here, can't we just declare `hook_head` in `list_hook()`?\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 emplace_hook(struct list_head *pos, const char *command)\n> +{\n> +\tstruct hook *to_add = malloc(sizeof(struct hook));\n> +\tto_add->origin = current_config_scope();\n> +\tstrbuf_init(&to_add->command, 0);\n> +\t/* even with use_shell, run_command() needs quotes */\n> +\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n> +\n> +\tlist_add_tail(&to_add->list, pos);\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(void)\n> +{\n> +\tstruct list_head *pos, *tmp;\n> +\tlist_for_each_safe(pos, tmp, &hook_head)\n> +\t\tremove_hook(pos);\n> +}\n> +\n> +static int hook_config_lookup(const char *key, const char *value, void *hook_key_cb)\n> +{\n> +\tconst char *hook_key = hook_key_cb;\n> +\n> +\tif (!strcmp(key, hook_key)) {\n> +\t\tconst char *command = value;\n> +\t\tstruct strbuf hookcmd_name = STRBUF_INIT;\n> +\t\tstruct list_head *pos = NULL, *tmp = NULL;\n> +\n> +\t\t/* Check if a hookcmd with that name exists. */\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\tBUG(\"git_config_get_value overwrote a string it shouldn't have\");\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\tlist_for_each_safe(pos, tmp, &hook_head) {\n> +\t\t\tstruct hook *hook = list_entry(pos, struct hook, list);\n> +\t\t\t/*\n> +\t\t\t * The list of hooks to run can be reordered by being redeclared\n> +\t\t\t * in the config. Options about hook ordering should be checked\n> +\t\t\t * here.\n> +\t\t\t */\n> +\t\t\tif (0 == strcmp(hook->command.buf, command))\n\nWe normally write this as !strcmp(...)\n\n> +\t\t\t\tremove_hook(pos);\n> +\t\t}\n> +\t\templace_hook(pos, command);\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> +\n> +\tif (!hookname)\n> +\t\treturn NULL;\n> +\n> +\t/* hook_head is stateful */\n> +\tclear_hook_list();\n> +\n> +\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname->buf);\n> +\n> +\tgit_config(hook_config_lookup, (void*)hook_key.buf);\n> +\n> +\treturn &hook_head;\n> +}\n> diff --git a/hook.h b/hook.h\n> new file mode 100644\n> index 0000000000..aaf6511cff\n> --- /dev/null\n> +++ b/hook.h\n> @@ -0,0 +1,15 @@\n> +#include \"config.h\"\n> +#include \"list.h\"\n> +#include \"strbuf.h\"\n> +\n> +struct hook\n> +{\n> +\tstruct list_head list;\n> +\tenum config_scope origin;\n> +\tstruct strbuf command;\n> +};\n> +\n> +struct list_head* hook_list(const struct strbuf *hookname);\n> +\n> +void free_hook(struct hook *ptr);\n> +void clear_hook_list(void);\n> diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n> index 34b0df5216..46d1ed354a 100755\n> --- a/t/t1360-config-based-hooks.sh\n> +++ b/t/t1360-config-based-hooks.sh\n> @@ -4,8 +4,72 @@ 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\nThanks for changing the tests to be independent of each other\n\nBest Wishes\n\nPhillip\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> +\tglobal:\t$ROOT/path/def\n> +\tlocal:\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 dereferences a hookcmd' '\n> +\tsetup_hooks &&\n> +\tsetup_hookcmd &&\n> +\n> +\tcat >expected <<-EOF &&\n> +\tglobal:\t$ROOT/path/def\n> +\tlocal:\t$ROOT/path/ghi\n> +\tlocal:\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> +\ttest_config hook.pre-commit.command \"/path/def\" --add &&\n> +\n> +\tcat >expected <<-EOF &&\n> +\tlocal:\t$ROOT/path/ghi\n> +\tlocal:\t$ROOT/path/def\n> +\tEOF\n> +\n> +\tgit hook list pre-commit >actual &&\n> +\ttest_cmp expected actual\n>   '\n>   \n>   test_done\n> \n\n"},{"id":"405408","messageId":"45208197-9e2d-6127-026f-b8ee5591677d@gmail.com","threadId":"53527","inReplyTo":"20200909004939.1942347-7-emilyshaffer@google.com","subject":"Re: [PATCH v4 6/9] hook: add 'run' subcommand","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2020-09-11T13:30:58Z","receivedAt":"2020-09-11T14:57:43Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Emily\n\nOn 09/09/2020 01:49, Emily Shaffer wrote:\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> For now, the hook commands will in config order, in series. As alternate\n> ordering or parallelism is supported in the future, we should add knobs\n> to use those to the command line as well.\n> \n> As with the legacy hook implementation, all stdout generated by hook\n> commands is redirected to stderr. Piping from stdin is not yet\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> \n> Users may wish to provide hook commands like 'git config\n> hook.pre-commit.command \"~/linter.sh --pre-commit\"'. To enable this, the\n> contents of the 'hook.*.command' and 'hookcmd.*.command' strings are\n> first split by space or quotes into an argv_array, then expanded with\n> 'expand_user_path()'.\n> \n > [...]\n> diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh\n> index ebf8f38d68..ee8114250d 100755\n> --- a/t/t1360-config-based-hooks.sh\n> +++ b/t/t1360-config-based-hooks.sh\n> @@ -84,4 +84,32 @@ test_expect_success 'git hook list --porcelain prints just the command' '\n>   \ttest_cmp expected 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> +\tcat >~/sample-hook.sh <<-EOF &&\n> +\techo \\\"Sample Hook\\\"\n> +\tEOF\n\nI think this could use `write_script`. I'm rather scared of the '~' in \nthe script path, can we write it to the test directory please.\n\nBest Wishes\n\nPhillip\n\n> +\ttest_when_finished \"rm ~/sample-hook.sh\" &&\n> +\n> +\tchmod +x ~/sample-hook.sh &&\n> +\n> +\ttest_config hook.pre-commit.command \"~/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_done\n> \n\n"},{"id":"405416","messageId":"20200911165123.GE331156@google.com","threadId":"53527","inReplyTo":"9146d342-7b3c-091f-4904-e7d8696544be@gmail.com","subject":"Re: [PATCH v4 3/9] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-11T16:51:23Z","receivedAt":"2020-09-11T16:51:38Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Sep 11, 2020 at 02:27:42PM +0100, Phillip Wood wrote:\n> \n> Hi Emily\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> I think it would be helpful to have a couple of lines explaining what the\n> example configuration sets up\n\nSure.\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 note the config scope where the relevant\n> > +`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n> \n> Thanks for clarifying that it is the origin of the hook.<hook-name>.command\n> that is printed. An example of the output of the config above would be\n> useful I think.\n\nOh, that's a good idea - you're absolutely right. I'll do that.\n\n> > +/*\n> > + * NEEDSWORK: a stateful hook_head means we can't run two hook events in the\n> > + * background at the same time - which might be ok, or might not.\n> > + *\n> > + * Maybe it's better to cache a list head per hookname, since we can probably\n> > + * guess that the hook list won't change during a user-initiated operation. For\n> > + * now, within list_hooks, call clear_hook_list() at the outset.\n> > + */\n> > +static LIST_HEAD(hook_head);\n> \n> I can see a cache might be useful for the sequencer which needs to run the\n> prepare-msg hook for each commit (it should probably not be running the\n> post-commit hook but does at the moment) and for am which runs some hooks\n> for each patch but until then I'm not sure why we need a global variable\n> here, can't we just declare `hook_head` in `list_hook()`?\n\nYeah, I agree. I'll make that change with the next reroll.\n\nThanks for reading.\n - Emily\n"},{"id":"406227","messageId":"20200923225910.1276940-1-jonathantanmy@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-2-emilyshaffer@google.com","subject":"Re: [PATCH v4 1/9] doc: propose hooks managed by the config","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2020-09-23T22:59:10Z","receivedAt":"2020-09-23T22:59:17Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"For this review, I'll just concern myself with overall design and\nstructure.\n\nFor this patch, overall I think it's better if there's a clear\ndistinction between what we are implementing now and what we are\nimplementing later.\n\n> +[[motivation]]\n> +== Motivation\n> +\n> +Treat hooks as a first-class citizen by replacing the .git/hook/hookname path as\n> +the only source of hooks to execute, in a way which is friendly to users with\n> +multiple repos which have similar needs.\n\nI don't understand what \"first-class citizen\" here means - probably\nbetter to just omit that phrase and describe the new way of doing hooks.\n\n> +[[config-schema-hook]]\n> +==== `hook`\n> +\n> +Primarily contains subsections for each hook event. These order of these\n> +subsections defines the hook command execution order\n\nThe execution order is defined by the order of a multivalue config\nvariable, I think, not the order of subsections? Besides, I believe that\nthere's one subsection per hook event (e.g. hook.\"pre-commit\"), not one\nsubsection per command.\n\n> ; 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.\n\n[snip]\n\n> Hook event subsections can\n> +also contain per-hook-event settings.\n\nIf this is not yet implemented, maybe list under \"future work\".\n\n> +\n> +Also contains top-level hook execution settings, for example,\n> +`hook.warnHookDir`, `hook.runHookDir`, or `hook.disableAll`. (These settings are\n> +described more in <<library,Library>>.)\n\nI think it's clearer if you list this under \"future work\" - I didn't see\nany implementation of this.\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> +  error = ignore\n\nIs \"error\" implemented?\n\n> +\n> +[hook]\n> +  runHookDir = interactive\n\nSame question for \"runHookDir\".\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 to which\n> +it's normally subscribed _except_ `pre-commit`.\n> +\n> +----\n> +[hookcmd \"perl-linter\"]\n> +  command = /usr/bin/lint-it --language=perl\n> +  skip = true\n> +  pre-commit-skip = false\n> +----\n\nAnd the skips. (And several more below which I will skip.)\n\n> +If the caller wants to do something more complicated, the hook library can also\n> +provide a callback API:\n> +\n> +*`int for_each_hookcmd(const char *hookname, hookcmd_function *cb)`*\n\nIs there a use case that would need such a function?\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. Users can replace their\n> +`.git/hooks/<hook-event>` scripts with a trampoline based on `git hook list`'s\n> +output. Modifier commands like `git hook add` and `git hook edit` can be\n> +implemented around this time as well.\n\nThis seems to contradict patch 8, which teaches Git to use the configs\ndirectly without any change to .git/hooks/<hook-event> (at least for\ncertain commit-related hooks).\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\nWith this schema, and with the \"skip\" behavior described above (but not\nimplemented in this patch set), rudimentary ordering can already be\ndone; because a hook is removed and reinserted whenever it appears in\nthe config, even a hook X in the system config can be made to run after a\nhook Y in the worktree config by adding Y then X in the worktree config,\nand if we want to disable X instead, we can just add \"skip\" to X.\n"},{"id":"406228","messageId":"20200923230451.1279511-1-jonathantanmy@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-4-emilyshaffer@google.com","subject":"Re: [PATCH v4 3/9] hook: add list command","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2020-09-23T23:04:51Z","receivedAt":"2020-09-23T23:04:58Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":">   $ git hook list pre-commit\n>   ~/baz/from/hookcmd.sh\n>   ~/bar.sh\n\nIn the tests below, there is a \"local:\" prefix (or similar). It's\nclearer if the commit message has that too.\n\nAlso, looking at a later commit, the \"list\" command probably should\ninclude the legacy hook if it exists.\n\n> +static void emplace_hook(struct list_head *pos, const char *command)\n> +{\n> +\tstruct hook *to_add = malloc(sizeof(struct hook));\n> +\tto_add->origin = current_config_scope();\n> +\tstrbuf_init(&to_add->command, 0);\n> +\t/* even with use_shell, run_command() needs quotes */\n> +\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n> +\n> +\tlist_add_tail(&to_add->list, pos);\n> +}\n\nIt might be odd to a programmer reading this that an existing \"struct\nhook\" with the same name is not reused - the scanning of the list done\nin hook_config_lookup() could probably go here instead.\n\n> +test_expect_success 'git hook list orders by config order' '\n> +\tsetup_hooks &&\n> +\n> +\tcat >expected <<-EOF &&\n> +\tglobal:\t$ROOT/path/def\n> +\tlocal:\t$ROOT/path/ghi\n\nWill the \"global\" strings etc. be translated? If yes, it's probably not\nworth it to align the paths in this way.\n"},{"id":"406229","messageId":"20200923232031.1283731-1-jonathantanmy@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-8-emilyshaffer@google.com","subject":"Re: [PATCH v4 7/9] hook: replace run-command.h:find_hook","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2020-09-23T23:20:31Z","receivedAt":"2020-09-23T23:20:35Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"> +int hook_exists(const char *hookname)\n> +{\n> +\tconst char *value = NULL;\n> +\tstruct strbuf hook_key = STRBUF_INIT;\n> +\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n> +\n> +\treturn (!git_config_get_value(hook_key.buf, &value)) || !!find_hook(hookname);\n> +}\n\nI was surprised that this didn't share code with hook_list. Upon further\nthought, hook_list might be expensive if hooks are present, but if we\ncan cache results, I think it's worth it. A caller that calls this\nfunction usually will run hooks if they are present, so it's not wasted\nwork to construct the hook list.\n"},{"id":"406230","messageId":"20200923234734.1294057-1-jonathantanmy@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-9-emilyshaffer@google.com","subject":"Re: [PATCH v4 8/9] commit: use config-based hooks","fromName":"Jonathan Tan","fromEmail":"jonathantanmy@google.com","sentAt":"2020-09-23T23:47:34Z","receivedAt":"2020-09-23T23:47:39Z","isPatch":true,"sender":{"key":"jonathantanmy@fastmail.com","avatar":null},"body":"> -\tif (!no_verify && find_hook(\"pre-commit\")) {\n> +\tif (!no_verify && hook_exists(\"pre-commit\")) {\n\nA reviewer would probably need to look at all instances of \"pre-commit\"\n(and likewise for the other hooks) but if the plan is to convert all\nhooks, then the reviewer wouldn't need to do this since we could just\ndelete the \"find_hook\" function.\n\nOverall comments about the design and scope of the patch set:\n\n - I think that the abilities of the current patch set regarding\n   overriding order of globally-set hook commands is sufficient. We\n   should also have some way of disabling globally-set hooks, perhaps\n   by implementing the \"skip\" variable mentioned in patch 1 or by\n   allowing the redefinition of hookcmd sections (e.g. by redefining a\n   command to \"/usr/bin/true\"). To me, these provide substantial\n   user-facing value, and would be sufficient for a first version - and\n   other things like parallelization can come later.\n\n - As for the UI that should be exposed through the \"git hook\" command,\n   I think that \"git hook list\" and \"git hook run\" are sufficient.\n   Editing the config files are not too difficult, and \"git hook add\"\n   etc. can be added later.\n\n - As for whether (1) it is OK for none of the hooks to be converted (and\n   instead rely on the user to edit their hook scripts to call \"git hook\n   run ???\"), or if (2) we should require some hooks to be\n   converted, or if (3) we should require all hooks to be converted: I'd\n   rather have (2) or (3) so that we don't have dead code. I prefer (3),\n   especially since a reviewer wouldn't have to worry about leftover\n   usages of old functions like find_hook() (as I mentioned at the start\n   of this email), but I'm not fully opposed to (2) either.\n"},{"id":"406285","messageId":"20200924215411.GG331156@google.com","threadId":"53527","inReplyTo":"20200923225910.1276940-1-jonathantanmy@google.com","subject":"Re: [PATCH v4 1/9] doc: propose hooks managed by the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-09-24T21:54:11Z","receivedAt":"2020-09-24T21:54:21Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Sep 23, 2020 at 03:59:10PM -0700, Jonathan Tan wrote:\n> \n> For this review, I'll just concern myself with overall design and\n> structure.\n\nThanks - the design doc is now slightly old, so it's nice to have some\nfresh eyes on it.\n\n> \n> For this patch, overall I think it's better if there's a clear\n> distinction between what we are implementing now and what we are\n> implementing later.\n\nI took a light hand when I checked for this - the topic isn't complete\nyet, and there's some work in the design doc which I want to include in\nthis topic, but which hasn't been sent around (or written) yet.\n\n> \n> > +[[motivation]]\n> > +== Motivation\n> > +\n> > +Treat hooks as a first-class citizen by replacing the .git/hook/hookname path as\n> > +the only source of hooks to execute, in a way which is friendly to users with\n> > +multiple repos which have similar needs.\n> \n> I don't understand what \"first-class citizen\" here means - probably\n> better to just omit that phrase and describe the new way of doing hooks.\n\nSure.\n\n> \n> > +[[config-schema-hook]]\n> > +==== `hook`\n> > +\n> > +Primarily contains subsections for each hook event. These order of these\n> > +subsections defines the hook command execution order\n> \n> The execution order is defined by the order of a multivalue config\n> variable, I think, not the order of subsections? Besides, I believe that\n> there's one subsection per hook event (e.g. hook.\"pre-commit\"), not one\n> subsection per command.\n\nOk. Have changed to \"The order of variables in these subsections\ndefines...\"\n\n> \n> > ; 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.\n> \n> [snip]\n> \n> > Hook event subsections can\n> > +also contain per-hook-event settings.\n> \n> If this is not yet implemented, maybe list under \"future work\".\n\nGood idea. Done.\n\n> \n> > +\n> > +Also contains top-level hook execution settings, for example,\n> > +`hook.warnHookDir`, `hook.runHookDir`, or `hook.disableAll`. (These settings are\n> > +described more in <<library,Library>>.)\n> \n> I think it's clearer if you list this under \"future work\" - I didn't see\n> any implementation of this.\n\nYeah, this is out of sync with what the implementation ended up looking\nlike; disableAll might still be a useful thing to include in the initial\nfeature topic, so I won't remove it, but warnHookDir is not necessary.\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> > +  error = ignore\n> \n> Is \"error\" implemented?\n\nNo, have marked it with a comment.\n\n> \n> > +\n> > +[hook]\n> > +  runHookDir = interactive\n> \n> Same question for \"runHookDir\".\n\nIt is in the reroll I'm trying to get out this week :)\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 to which\n> > +it's normally subscribed _except_ `pre-commit`.\n> > +\n> > +----\n> > +[hookcmd \"perl-linter\"]\n> > +  command = /usr/bin/lint-it --language=perl\n> > +  skip = true\n> > +  pre-commit-skip = false\n> > +----\n> \n> And the skips. (And several more below which I will skip.)\n\nAgain, this is in the reroll I'm working on.\n\n> \n> > +If the caller wants to do something more complicated, the hook library can also\n> > +provide a callback API:\n> > +\n> > +*`int for_each_hookcmd(const char *hookname, hookcmd_function *cb)`*\n> \n> Is there a use case that would need such a function?\n\nI'm not sure yet - but I'm not quite ready to cut it from the design\ndoc, until I finish migrating the existing hooks and know that it's not\nneeded. At that point it'll make sense to move it into the future work\nsection.\n\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. Users can replace their\n> > +`.git/hooks/<hook-event>` scripts with a trampoline based on `git hook list`'s\n> > +output. Modifier commands like `git hook add` and `git hook edit` can be\n> > +implemented around this time as well.\n> \n> This seems to contradict patch 8, which teaches Git to use the configs\n> directly without any change to .git/hooks/<hook-event> (at least for\n> certain commit-related hooks).\n\nYeah, I think this needs to be rephrased; at this point locally I've\ncompletely removed the --porcelain patch - I'm pretty sure it needs to\nbe a format string instead.\n\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> With this schema, and with the \"skip\" behavior described above (but not\n> implemented in this patch set), rudimentary ordering can already be\n> done; because a hook is removed and reinserted whenever it appears in\n> the config, even a hook X in the system config can be made to run after a\n> hook Y in the worktree config by adding Y then X in the worktree config,\n> and if we want to disable X instead, we can just add \"skip\" to X.\n\nYep, that's why reordering is in the future work section :)\n\nThe problem with config ordering is like such: if I want everyone in\nmy enterprise to run 'git-secrets --prepare-commit-msg' as the very last\nprepare-commit-msg hook, but I can only ship them an /etc/gitconfig,\nthen the best I can do is email my users and ask them to run 'git config\nhook.prepare-commit-msg.command git-secrets-prepare-commit-msg' in every\nnew repo and include a 'hookcmd.git-secrets-prepare-commit-msg.command'\nconfig in the /etc/gitconfig I ship. (I mention git-secrets here because\nit's possible other hooks could have introduced credential secrets into\nmy user's commit message after git-secrets already ran.)\n\n - Emily\n"},{"id":"406509","messageId":"CAN0heSq1iVtJycjgh-iv2O6vNr6CLn69N3286hzkfu5sXLjf5w@mail.gmail.com","threadId":"53527","inReplyTo":"20200909004939.1942347-4-emilyshaffer@google.com","subject":"Re: [PATCH v4 3/9] hook: add list command","fromName":"Martin Ågren","fromEmail":"martin.agren@gmail.com","sentAt":"2020-09-27T19:23:35Z","receivedAt":"2020-09-27T19:24:04Z","isPatch":true,"sender":{"key":"martin.agren@gmail.com","avatar":null},"body":"Hi Emily,\n\nOn Wed, 9 Sep 2020 at 02:54, Emily Shaffer <emilyshaffer@google.com> wrote:\n\n>  DESCRIPTION\n>  -----------\n>  You can list, add, and modify hooks with this command.\n\n(BTW, I think this patch could teach this to say \"You can list hooks\nwith this command.\" If/when we add the other commands, we can expand\non this.)\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\nI propose s/\"hook\"/`hook`/ and similar to set this as monospace since we\nare discussing configuration sections. If we want to avoid starting\nsentences with \"hook\" (or `hookcmd`; do we?), maybe something like \"The\nsection `hook` ...\" would work fine.\n\n> +particular hook event; commands are run in config order. \"hookcmd\" is used to\n\n\"config order\" feels a bit too colloquial/vague. You use the same phrase\nin the commit message and I think it works well there for the indented\naudience. But for this document, I'm not so sure. How about\n\n  Commands are run in the order they are encountered as the Git\n  configuration files are processed (see linkgit:git-config[1]).\n\n? It's also quite possible that \"config order\" hits the exact right tone\n-- please trust your judgment.\n\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> +  [hook \"post-commit\"]\n> +    command = \"linter\"\n> +    command = \"~/typocheck.sh\"\n> +\n> +  [hookcmd \"linter\"]\n> +    command = \"/bin/linter --c\"\n\nHmm. \"hook\", \"command\" and \"hookcmd\". Should that be \"cmd\", or\n\"hookcommand\"? I'd favour the latter, but the current proposal somehow\nfeels asymmetric. (If code uses, and is consistent about using,\n\"hookcmd\" that's another thing entirely, I think. It's just that for the\nconfiguration, it looks a bit odd.)\n\n> +List the hooks which have been configured for <hook-name>. Hooks appear\n\n`<hook-name>` with backticks.\n\n> +in the order they should be run, and note the config scope where the relevant\n> +`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n\nI had to read and re-read this a few times. The \"and note the\" does not\nmean \"and please observe that\", but rather \"and they make note of\". Not\nsure how that can be done clearer. The second thing that tripped me up\nwas that last part. Maybe end the sentence after \"specified\", then add\nsomething like \"The scope is not affected by if and where\n`hookcmd.<hook-name>.command` appears.\".\n\nI think you could add\n\n  CONFIGURATION\n  -------------\n  include::config/hook.txt[]\n\nhere and add such a file\n\n  hook.<hook-name>.command::\n         ...\n\n  hookcmd.<hook-name>.command::\n         ...\n\nwhere you define/describe those items. And you can include it from\nconfig.txt as well.\n\nMartin\n"},{"id":"406556","messageId":"20200928192903.GA4095761@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-5-emilyshaffer@google.com","subject":"Re: [PATCH v4 4/9] hook: add --porcelain to list command","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2020-09-28T19:29:03Z","receivedAt":"2020-09-28T19:29:12Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2020.09.08 17:49, Emily Shaffer wrote:\n> Teach 'git hook list --porcelain <hookname>', which prints simply the\n> commands to be run in the order suggested by the config. This option is\n> intended for use by user scripts, wrappers, or out-of-process Git\n> commands which still want to execute hooks. For example, the following\n> snippet might be added to git-send-email.perl to introduce a\n> `pre-send-email` hook:\n> \n>   sub pre_send_email {\n>     open(my $fh, 'git hook list --porcelain pre-send-email |');\n>     chomp(my @hooks = <$fh>);\n>     close($fh);\n> \n>     foreach $hook (@hooks) {\n>             system $hook\n>     }\n> \n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  Documentation/git-hook.txt    | 13 +++++++++++--\n>  builtin/hook.c                | 17 +++++++++++++----\n>  t/t1360-config-based-hooks.sh | 12 ++++++++++++\n>  3 files changed, 36 insertions(+), 6 deletions(-)\n> \n> diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt\n> index e458586e96..0854035ce2 100644\n> --- a/Documentation/git-hook.txt\n> +++ b/Documentation/git-hook.txt\n> @@ -8,7 +8,7 @@ git-hook - Manage configured hooks\n>  SYNOPSIS\n>  --------\n>  [verse]\n> -'git hook' list <hook-name>\n> +'git hook' list [--porcelain] <hook-name>\n>  \n>  DESCRIPTION\n>  -----------\n> @@ -43,11 +43,20 @@ Local config\n>  COMMANDS\n>  --------\n>  \n> -list <hook-name>::\n> +list [--porcelain] <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 note the config scope where the relevant\n>  `hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n> ++\n> +If `--porcelain` is specified, instead print the commands alone, separated by\n> +newlines, for easy parsing by a script.\n> +\n> +OPTIONS\n> +-------\n> +--porcelain::\n> +\tWith `list`, print the commands in the order they should be run,\n> +\tseparated by newlines, for easy parsing by a script.\n\nRather than a hard-coded porcelain format, perhaps we could accept a\nformat string to allow callers to specify which items they want, for\ngreater forwards-compatibility?\n\nAlso, we may want a \"-z / --null\" option like in `git config` to delimit\nby null bytes rather than newlines, in case any commands end up with\nembedded newlines.\n"},{"id":"406558","messageId":"20200928192926.GB4095761@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-7-emilyshaffer@google.com","subject":"Re: [PATCH v4 6/9] hook: add 'run' subcommand","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2020-09-28T19:29:26Z","receivedAt":"2020-09-28T19:29:34Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2020.09.08 17:49, Emily Shaffer wrote:\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> For now, the hook commands will in config order, in series. As alternate\n\nLooks like a small typo here:\ns/will in config order/will run in config order/\n"},{"id":"406968","messageId":"20201005212751.GH331156@google.com","threadId":"53527","inReplyTo":"20200923234734.1294057-1-jonathantanmy@google.com","subject":"Re: [PATCH v4 8/9] commit: use config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-10-05T21:27:51Z","receivedAt":"2020-10-05T21:28:02Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Sep 23, 2020 at 04:47:34PM -0700, Jonathan Tan wrote:\n> \n> > -\tif (!no_verify && find_hook(\"pre-commit\")) {\n> > +\tif (!no_verify && hook_exists(\"pre-commit\")) {\n> \n> A reviewer would probably need to look at all instances of \"pre-commit\"\n> (and likewise for the other hooks) but if the plan is to convert all\n> hooks, then the reviewer wouldn't need to do this since we could just\n> delete the \"find_hook\" function.\n> \n> Overall comments about the design and scope of the patch set:\n> \n>  - I think that the abilities of the current patch set regarding\n>    overriding order of globally-set hook commands is sufficient. We\n>    should also have some way of disabling globally-set hooks, perhaps\n>    by implementing the \"skip\" variable mentioned in patch 1 or by\n>    allowing the redefinition of hookcmd sections (e.g. by redefining a\n>    command to \"/usr/bin/true\"). To me, these provide substantial\n>    user-facing value, and would be sufficient for a first version - and\n>    other things like parallelization can come later.\n\nOK. I will send 'skip' in the next reroll. Thanks for pointing it out!\n\n> \n>  - As for the UI that should be exposed through the \"git hook\" command,\n>    I think that \"git hook list\" and \"git hook run\" are sufficient.\n>    Editing the config files are not too difficult, and \"git hook add\"\n>    etc. can be added later.\n> \n>  - As for whether (1) it is OK for none of the hooks to be converted (and\n>    instead rely on the user to edit their hook scripts to call \"git hook\n>    run ???\"), or if (2) we should require some hooks to be\n>    converted, or if (3) we should require all hooks to be converted: I'd\n>    rather have (2) or (3) so that we don't have dead code. I prefer (3),\n>    especially since a reviewer wouldn't have to worry about leftover\n>    usages of old functions like find_hook() (as I mentioned at the start\n>    of this email), but I'm not fully opposed to (2) either.\n\nI personally prefer (3) - I think the user experience with (2) in a\nrelease (or even in 'next', which all Googlers use) is pretty bad. The\ndownside, of course, is that a large topic gets merged all at once and\nmakes some pretty nasty reviewer overhead.\n\nJunio, I wonder if you can give any advice here? What would be really\nideal for me would be to do something like Stolee has been doing with\nhis maintenance series - config-based hooks pt. I containing the library\ncode and config-based hooks pt. II containing the conversion of\npreexisting hooks. Does that make the overhead for you significantly\nworse?\n\n - Emily\n"},{"id":"406972","messageId":"20201005232418.GA1393696@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-3-emilyshaffer@google.com","subject":"Re: [PATCH v4 2/9] hook: scaffolding for git-hook subcommand","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2020-10-05T23:24:18Z","receivedAt":"2020-10-05T23:24:28Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Hi,\n\nEmily Shaffer wrote:\n\n> Introduce infrastructure for a new subcommand, git-hook, which will be\n> used to ease config-based hook management. This command will handle\n> parsing configs to compose a list of hooks to run for a given event, as\n> well as adding or modifying hook configs in an interactive fashion.\n>\n> Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> ---\n>  .gitignore                    |  1 +\n>  Documentation/git-hook.txt    | 19 +++++++++++++++++++\n>  Makefile                      |  1 +\n>  builtin.h                     |  1 +\n>  builtin/hook.c                | 21 +++++++++++++++++++++\n>  git.c                         |  1 +\n>  t/t1360-config-based-hooks.sh | 11 +++++++++++\n>  7 files changed, 55 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\noptional: I could imagine this being squashed into patch 3 --- that way,\nthe command has functionality as soon as it exists.  Alternatively:\n\n[...]\n> --- /dev/null\n> +++ b/Documentation/git-hook.txt\n> @@ -0,0 +1,19 @@\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> +You can list, add, and modify hooks with this command.\n\nThis could say something like \"This is a placeholder command that will\ngain functionality in subsequent patches\" to make the current state\nclear.\n\n[...]\n> --- a/git.c\n> +++ b/git.c\n> @@ -519,6 +519,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\nThis makes the command require that it run within a git repository,\nbut I can imagine wanting to list hooks outside of any.  Should it use\nRUN_SETUP_GENTLY instead?\n\nThanks,\nJonathan\n"},{"id":"406973","messageId":"20201005232714.GB1393696@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-4-emilyshaffer@google.com","subject":"Re: [PATCH v4 3/9] hook: add list command","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2020-10-05T23:27:14Z","receivedAt":"2020-10-05T23:27:21Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Emily Shaffer wrote:\n\n> --- a/Documentation/git-hook.txt\n> +++ b/Documentation/git-hook.txt\n> @@ -8,12 +8,47 @@ git-hook - Manage configured hooks\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 note the config scope where the relevant\n> +`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n\nA little bit of futureproofing: this may want to mention that the\noutput is intended to be human-readable and is subject to change over\ntime (scripters beware!).\n\nThanks,\nJonathan\n"},{"id":"406974","messageId":"20201005233016.GC1393696@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-6-emilyshaffer@google.com","subject":"Re: [PATCH v4 5/9] parse-options: parse into strvec","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2020-10-05T23:30:16Z","receivedAt":"2020-10-05T23:30:20Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Emily Shaffer wrote:\n\n> This is useful if collecting generic arguments to pass through to\n> another command, for example, 'git hook run --arg \"--quiet\" --arg\n> \"--format=pretty\" some-hook'. The resulting strvec would contain\n> { \"--quiet\", \"--format=pretty\" }.\n\nAn alternative is to use OPT_STRING_LIST and then convert in the\ncaller.  One advantage of that is that it would guarantee the behavior\nwith --no-arg etc is going to match exactly.\n\nI prefer this OPT_STRVEC approach nonetheless.  Can the\nparse_opt_strvec and parse_opt_string_list functions get comments\npointing to each other as an alternative way to encourage that kind of\nconsistency?\n\n[...]\n> --- a/Documentation/technical/api-parse-options.txt\n> +++ b/Documentation/technical/api-parse-options.txt\n> @@ -173,6 +173,11 @@ 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_ARGV_ARRAY(short, long, &struct argv_array, arg_str, description)`::\n\nnit: this should be OPT_STRVEC\n\nThanks,\nJonathan\n"},{"id":"406976","messageId":"20201005233903.GD1393696@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-7-emilyshaffer@google.com","subject":"Re: [PATCH v4 6/9] hook: add 'run' subcommand","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2020-10-05T23:39:03Z","receivedAt":"2020-10-05T23:39:08Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Hi,\n\nEmily Shaffer 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\nExciting!\n\nI would even be tempted to put this earlier in the series: providing a\n\"git hook run\" command that only supports legacy hooks and then\nimproving it from there to support config-based hooks.  This ordering is\nalso fine, though.\n\n[...]\n> ---\n>  builtin/hook.c                | 30 ++++++++++++++++++++\n>  hook.c                        | 52 ++++++++++++++++++++++++++++++++---\n>  hook.h                        |  3 ++\n>  t/t1360-config-based-hooks.sh | 28 +++++++++++++++++++\n>  4 files changed, 109 insertions(+), 4 deletions(-)\n\nNeeds docs.\n\n[...]\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -5,9 +5,11 @@\n[...]\n> +static int run(int argc, const char **argv, const char *prefix)\n> +{\n> +\tstruct strbuf hookname = STRBUF_INIT;\n> +\tstruct strvec envs = STRVEC_INIT;\n> +\tstruct strvec args = STRVEC_INIT;\n> +\n> +\tstruct option run_options[] = {\n> +\t\tOPT_STRVEC('e', \"env\", &envs, N_(\"var\"),\n> +\t\t\t   N_(\"environment variables for hook to use\")),\n> +\t\tOPT_STRVEC('a', \"arg\", &args, N_(\"args\"),\n> +\t\t\t   N_(\"argument to pass to hook\")),\n> +\t\tOPT_END(),\n> +\t};\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(_(\"a hookname must be provided to operate on.\"),\n> +\t\t\t      builtin_hook_usage, run_options);\n\nError message nit: what does it mean to operate on a hookname?\n\nPerhaps this should allude to the usage string?\n\n\tusage_msg_opt(_(\"missing <hookname> parameter\"), ...);\n\nOr to match the conversational approach of commands like \"clone\":\n\n\tusage_msg_opt(_(\"You must specify a hook to run.\"), ...);\n\n[...]\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>  /*\n>   * NEEDSWORK: a stateful hook_head means we can't run two hook events in the\n> @@ -21,13 +22,15 @@ void free_hook(struct hook *ptr)\n>  \t}\n>  }\n>  \n> -static void emplace_hook(struct list_head *pos, const char *command)\n> +static void emplace_hook(struct list_head *pos, const char *command, int quoted)\n>  {\n>  \tstruct hook *to_add = malloc(sizeof(struct hook));\n>  \tto_add->origin = current_config_scope();\n>  \tstrbuf_init(&to_add->command, 0);\n> -\t/* even with use_shell, run_command() needs quotes */\n> -\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n> +\tif (quoted)\n> +\t\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n> +\telse\n> +\t\tstrbuf_addstr(&to_add->command, command);\n>  \n>  \tlist_add_tail(&to_add->list, pos);\n>  }\n\nThis would need to use sq_quote_* to be safe, but we can do something\nsimpler: if we accumulate parameters in an argv_array passed to\nrun_command, then they will be safely passed to the shell without\ntriggering expansion.\n\nThanks,\nJonathan\n"},{"id":"406977","messageId":"20201005234237.GE1393696@google.com","threadId":"53527","inReplyTo":"20200909004939.1942347-8-emilyshaffer@google.com","subject":"Re: [PATCH v4 7/9] hook: replace run-command.h:find_hook","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2020-10-05T23:42:37Z","receivedAt":"2020-10-05T23:42:43Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Emily Shaffer wrote:\n\n> Subject: hook: replace run-command.h:find_hook\n\ntiny nit: This doesn't remove find_hook, so this may want to be\ndescribed as \"add replacement for\" instead of \"replace\".\n\n[...]\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -111,6 +111,15 @@ struct list_head* hook_list(const struct strbuf* hookname)\n>  \treturn &hook_head;\n>  }\n>  \n> +int hook_exists(const char *hookname)\n> +{\n> +\tconst char *value = NULL;\n> +\tstruct strbuf hook_key = STRBUF_INIT;\n> +\tstrbuf_addf(&hook_key, \"hook.%s.command\", hookname);\n> +\n> +\treturn (!git_config_get_value(hook_key.buf, &value)) || !!find_hook(hookname);\n> +}\n\nThis feels a bit fragile, since it can go out of sync with run_hooks.\nI think I'd prefer if they shared code and this function either\nreturned a parsed structure that could be used later to run hooks or\ncached the result keyed by hookname.\n\nThanks,\nJonathan\n"},{"id":"406978","messageId":"20201005234839.GF1393696@google.com","threadId":"53527","inReplyTo":"20201005212751.GH331156@google.com","subject":"Re: [PATCH v4 8/9] commit: use config-based hooks","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2020-10-05T23:48:39Z","receivedAt":"2020-10-05T23:48:44Z","isPatch":true,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Emily Shaffer wrote:\n> On Wed, Sep 23, 2020 at 04:47:34PM -0700, Jonathan Tan wrote:\n\n>>  - As for whether (1) it is OK for none of the hooks to be converted (and\n>>    instead rely on the user to edit their hook scripts to call \"git hook\n>>    run ???\"), or if (2) we should require some hooks to be\n>>    converted, or if (3) we should require all hooks to be converted: I'd\n>>    rather have (2) or (3) so that we don't have dead code. I prefer (3),\n>>    especially since a reviewer wouldn't have to worry about leftover\n>>    usages of old functions like find_hook() (as I mentioned at the start\n>>    of this email), but I'm not fully opposed to (2) either.\n>\n> I personally prefer (3) - I think the user experience with (2) in a\n> release (or even in 'next', which all Googlers use) is pretty bad. The\n> downside, of course, is that a large topic gets merged all at once and\n> makes some pretty nasty reviewer overhead.\n\nOne approach is to build up a series with \"git hook run\" and \"git hook\nlist\" demonstrating and testing the functionality and [PATCH n+1/n]\nextra patches at the end converting existing hooks.  The user\nexperience from \"git hook run\" and even \"git hook list\" supporting a\npreview of the future without built-in commands living in that future\nyet would not be so bad, methinks.  And then a final series could\nupdate the built-in commands' usage of hooks and would still be fairly\nsmall.\n\nIn other words, I think I like (1), except *without* the\nrecommendation for users to edit their hook scripts to call \"git hook\nrun\" --- instead, the recommendation would be \"try running this\ncommand if you want to see what hooks will do in the future\".\n\nThanks,\nJonathan\n"},{"id":"406984","messageId":"xmqq362s6jrq.fsf@gitster.c.googlers.com","threadId":"53527","inReplyTo":"20201005233016.GC1393696@google.com","subject":"Re: [PATCH v4 5/9] parse-options: parse into strvec","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2020-10-06T04:49:45Z","receivedAt":"2020-10-06T04:49:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jonathan Nieder <jrnieder@gmail.com> writes:\n\n> Emily Shaffer wrote:\n>\n>> This is useful if collecting generic arguments to pass through to\n>> another command, for example, 'git hook run --arg \"--quiet\" --arg\n>> \"--format=pretty\" some-hook'. The resulting strvec would contain\n>> { \"--quiet\", \"--format=pretty\" }.\n>\n> An alternative is to use OPT_STRING_LIST and then convert in the\n> caller.  One advantage of that is that it would guarantee the behavior\n> with --no-arg etc is going to match exactly.\n>\n> I prefer this OPT_STRVEC approach nonetheless.  Can the\n> parse_opt_strvec and parse_opt_string_list functions get comments\n> pointing to each other as an alternative way to encourage that kind of\n> consistency?\n>\n> [...]\n>> --- a/Documentation/technical/api-parse-options.txt\n>> +++ b/Documentation/technical/api-parse-options.txt\n>> @@ -173,6 +173,11 @@ 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_ARGV_ARRAY(short, long, &struct argv_array, arg_str, description)`::\n>\n> nit: this should be OPT_STRVEC\n\nSigh.  I thought I caught all of these with a SQUASH fix-up patch\nthe last round.  Thanks for being extra careful.\n"},{"id":"406998","messageId":"20201006190616.GI331156@google.com","threadId":"53527","inReplyTo":"20201005232418.GA1393696@google.com","subject":"Re: [PATCH v4 2/9] hook: scaffolding for git-hook subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-10-06T19:06:16Z","receivedAt":"2020-10-06T19:06:24Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Mon, Oct 05, 2020 at 04:24:18PM -0700, Jonathan Nieder wrote:\n> \n> Hi,\n> \n> Emily Shaffer wrote:\n> \n> > Introduce infrastructure for a new subcommand, git-hook, which will be\n> > used to ease config-based hook management. This command will handle\n> > parsing configs to compose a list of hooks to run for a given event, as\n> > well as adding or modifying hook configs in an interactive fashion.\n> >\n> > Signed-off-by: Emily Shaffer <emilyshaffer@google.com>\n> > ---\n> >  .gitignore                    |  1 +\n> >  Documentation/git-hook.txt    | 19 +++++++++++++++++++\n> >  Makefile                      |  1 +\n> >  builtin.h                     |  1 +\n> >  builtin/hook.c                | 21 +++++++++++++++++++++\n> >  git.c                         |  1 +\n> >  t/t1360-config-based-hooks.sh | 11 +++++++++++\n> >  7 files changed, 55 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> \n> optional: I could imagine this being squashed into patch 3 --- that way,\n> the command has functionality as soon as it exists.  Alternatively:\n\nI would prefer to leave it on its own. Managing changes like\nbuiltin<->standalone or even the one you mentioned below about\nRUN_SETUP_GENTLY is somewhat easier to manage when they aren't in the\nsame patch as the business logic, IMO.\n\n> \n> [...]\n> > --- /dev/null\n> > +++ b/Documentation/git-hook.txt\n> > @@ -0,0 +1,19 @@\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> > +You can list, add, and modify hooks with this command.\n> \n> This could say something like \"This is a placeholder command that will\n> gain functionality in subsequent patches\" to make the current state\n> clear.\n\nDone.\n\n> \n> [...]\n> > --- a/git.c\n> > +++ b/git.c\n> > @@ -519,6 +519,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> This makes the command require that it run within a git repository,\n> but I can imagine wanting to list hooks outside of any.  Should it use\n> RUN_SETUP_GENTLY instead?\n\nNice catch. I'll add a test to the list patch to that effect also.\n"},{"id":"406999","messageId":"20201006190842.GJ331156@google.com","threadId":"53527","inReplyTo":"20201005234839.GF1393696@google.com","subject":"Re: [PATCH v4 8/9] commit: use config-based hooks","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-10-06T19:08:42Z","receivedAt":"2020-10-06T19:08:50Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Mon, Oct 05, 2020 at 04:48:39PM -0700, Jonathan Nieder wrote:\n> \n> Emily Shaffer wrote:\n> > On Wed, Sep 23, 2020 at 04:47:34PM -0700, Jonathan Tan wrote:\n> \n> >>  - As for whether (1) it is OK for none of the hooks to be converted (and\n> >>    instead rely on the user to edit their hook scripts to call \"git hook\n> >>    run ???\"), or if (2) we should require some hooks to be\n> >>    converted, or if (3) we should require all hooks to be converted: I'd\n> >>    rather have (2) or (3) so that we don't have dead code. I prefer (3),\n> >>    especially since a reviewer wouldn't have to worry about leftover\n> >>    usages of old functions like find_hook() (as I mentioned at the start\n> >>    of this email), but I'm not fully opposed to (2) either.\n> >\n> > I personally prefer (3) - I think the user experience with (2) in a\n> > release (or even in 'next', which all Googlers use) is pretty bad. The\n> > downside, of course, is that a large topic gets merged all at once and\n> > makes some pretty nasty reviewer overhead.\n> \n> One approach is to build up a series with \"git hook run\" and \"git hook\n> list\" demonstrating and testing the functionality and [PATCH n+1/n]\n> extra patches at the end converting existing hooks.  The user\n> experience from \"git hook run\" and even \"git hook list\" supporting a\n> preview of the future without built-in commands living in that future\n> yet would not be so bad, methinks.  And then a final series could\n> update the built-in commands' usage of hooks and would still be fairly\n> small.\n> \n> In other words, I think I like (1), except *without* the\n> recommendation for users to edit their hook scripts to call \"git hook\n> run\" --- instead, the recommendation would be \"try running this\n> command if you want to see what hooks will do in the future\".\n\nOk. I'll fix up the wording in the design doc and follow through with my\nplan to split the series into two parts.\n\n - Emily\n"},{"id":"407003","messageId":"20201006201957.GK331156@google.com","threadId":"53527","inReplyTo":"CAN0heSq1iVtJycjgh-iv2O6vNr6CLn69N3286hzkfu5sXLjf5w@mail.gmail.com","subject":"Re: [PATCH v4 3/9] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-10-06T20:20:44Z","receivedAt":"2020-10-06T20:20:52Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Sun, Sep 27, 2020 at 09:23:35PM +0200, Martin Ågren wrote:\n> \n> Hi Emily,\n\nFirstly, thanks for the doc review - this is great stuff.\n\n> \n> On Wed, 9 Sep 2020 at 02:54, Emily Shaffer <emilyshaffer@google.com> wrote:\n> \n> >  DESCRIPTION\n> >  -----------\n> >  You can list, add, and modify hooks with this command.\n> \n> (BTW, I think this patch could teach this to say \"You can list hooks\n> with this command.\" If/when we add the other commands, we can expand\n> on this.)\n\nDone. I sort of glued this together with Jonathan Nieder's suggestion in\nthe setup patch, and ended up saying \"later you will be able to blah\".\n\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> \n> I propose s/\"hook\"/`hook`/ and similar to set this as monospace since we\n> are discussing configuration sections. If we want to avoid starting\n> sentences with \"hook\" (or `hookcmd`; do we?), maybe something like \"The\n> section `hook` ...\" would work fine.\n\nNice - done. I don't see much problem with starting a sentence with\nmonospaced lower-cased section name... someone can disagree with me :)\n\n> \n> > +particular hook event; commands are run in config order. \"hookcmd\" is used to\n> \n> \"config order\" feels a bit too colloquial/vague. You use the same phrase\n> in the commit message and I think it works well there for the indented\n> audience. But for this document, I'm not so sure. How about\n> \n>   Commands are run in the order they are encountered as the Git\n>   configuration files are processed (see linkgit:git-config[1]).\n\nI don't mind colloquial - I think that improves the readability of user\ndocumentation - but you're right that it's vague. \"...commands are run\nin the order Git encounters them during the configuration parse (see\nlinkgitblah)\" seemed like an okay balance to me.\n\n> \n> ? It's also quite possible that \"config order\" hits the exact right tone\n> -- please trust your judgment.\n\nNah, I think you're right that \"config order\" is easily understood by\nGit devs, but probably not by Git users. I like that linking out to the\nconfig doc invites users to also learn a little more about how config\nfiles work :)\n\n> \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> > +  [hook \"post-commit\"]\n> > +    command = \"linter\"\n> > +    command = \"~/typocheck.sh\"\n> > +\n> > +  [hookcmd \"linter\"]\n> > +    command = \"/bin/linter --c\"\n> \n> Hmm. \"hook\", \"command\" and \"hookcmd\". Should that be \"cmd\", or\n> \"hookcommand\"? I'd favour the latter, but the current proposal somehow\n> feels asymmetric. (If code uses, and is consistent about using,\n> \"hookcmd\" that's another thing entirely, I think. It's just that for the\n> configuration, it looks a bit odd.)\n\nI'm not entirely in love with the name \"hookcmd\" but somehow I like\n\"hookcommand\" even less - especially since you end up with\n\"hook.command\" referencing a \"hookcommand\" which also has a\n\"hookcommand.command\" - blech.\n\nSome possible alternatives to \"hookcmd\":\n- hookmodule/hook-module\n- reusable-hook\n- hook-with-options/hook-options (nah, this sounds like it means\n  \"options for hook execution\")\n- hook-details/detailed-hook\n- named-hook\n\nI'll think on this more. I like \"named-hook\" quite a lot. Very\ninterested in hearing other ideas - \"the two hardest problems in\ncomputer science are naming, cache invalidation, and off-by-one errors\"\n;)\n\n> \n> > +List the hooks which have been configured for <hook-name>. Hooks appear\n> \n> `<hook-name>` with backticks.\n> \n> > +in the order they should be run, and note the config scope where the relevant\n> > +`hook.<hook-name>.command` was specified, not the `hookcmd` (if applicable).\n> \n> I had to read and re-read this a few times. The \"and note the\" does not\n> mean \"and please observe that\", but rather \"and they make note of\". Not\n> sure how that can be done clearer. The second thing that tripped me up\n> was that last part. Maybe end the sentence after \"specified\", then add\n> something like \"The scope is not affected by if and where\n> `hookcmd.<hook-name>.command` appears.\".\n\nOccam's Razor suggests \"Hooks appear in the order they should be run,\nand print the config scope blah\". Thanks for pointing out \"and note\nthat\" collision - I never use that phrase so it didn't occur to me!\n\n> \n> I think you could add\n> \n>   CONFIGURATION\n>   -------------\n>   include::config/hook.txt[]\n> \n> here and add such a file\n> \n>   hook.<hook-name>.command::\n>          ...\n> \n>   hookcmd.<hook-name>.command::\n>          ...\n> \n> where you define/describe those items. And you can include it from\n> config.txt as well.\n\nYes, totally. Thanks.\n\n - Emily\n"},{"id":"407006","messageId":"20201006204608.GL331156@google.com","threadId":"53527","inReplyTo":"20200923230451.1279511-1-jonathantanmy@google.com","subject":"Re: [PATCH v4 3/9] hook: add list command","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-10-06T20:46:08Z","receivedAt":"2020-10-06T20:46:16Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Sep 23, 2020 at 04:04:51PM -0700, Jonathan Tan wrote:\n> \n> >   $ git hook list pre-commit\n> >   ~/baz/from/hookcmd.sh\n> >   ~/bar.sh\n> \n> In the tests below, there is a \"local:\" prefix (or similar). It's\n> clearer if the commit message has that too.\n> \n> Also, looking at a later commit, the \"list\" command probably should\n> include the legacy hook if it exists.\n\nHave added it as a separate patch for v5, hopefully that will make more\nsense.\n\n> \n> > +static void emplace_hook(struct list_head *pos, const char *command)\n> > +{\n> > +\tstruct hook *to_add = malloc(sizeof(struct hook));\n> > +\tto_add->origin = current_config_scope();\n> > +\tstrbuf_init(&to_add->command, 0);\n> > +\t/* even with use_shell, run_command() needs quotes */\n> > +\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n> > +\n> > +\tlist_add_tail(&to_add->list, pos);\n> > +}\n> \n> It might be odd to a programmer reading this that an existing \"struct\n> hook\" with the same name is not reused - the scanning of the list done\n> in hook_config_lookup() could probably go here instead.\n\nSure, done.\n\n> \n> > +test_expect_success 'git hook list orders by config order' '\n> > +\tsetup_hooks &&\n> > +\n> > +\tcat >expected <<-EOF &&\n> > +\tglobal:\t$ROOT/path/def\n> > +\tlocal:\t$ROOT/path/ghi\n> \n> Will the \"global\" strings etc. be translated? If yes, it's probably not\n> worth it to align the paths in this way.\n\nAsked more offline. Jonathan was saying that translation might result in\nscope name + tab character leaving the path in different columns\ndepending on the scope anyways, so there's no point in using a tab\ncharacter instead of a space character here. That seems reasonable; I'll\nswitch.\n\n - Emily\n\n"},{"id":"407020","messageId":"20201006225712.GM331156@google.com","threadId":"53527","inReplyTo":"20201005233903.GD1393696@google.com","subject":"Re: [PATCH v4 6/9] hook: add 'run' subcommand","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-10-06T22:57:12Z","receivedAt":"2020-10-06T22:57:19Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Mon, Oct 05, 2020 at 04:39:03PM -0700, Jonathan Nieder wrote:\n> \n> Hi,\n> \n> Emily Shaffer 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> Exciting!\n> \n> I would even be tempted to put this earlier in the series: providing a\n> \"git hook run\" command that only supports legacy hooks and then\n> improving it from there to support config-based hooks.  This ordering is\n> also fine, though.\n\nOh, interesting! I sort of wish I had started with that ordering... but\nnow it seems a little unwieldy to switch. I'd probably want to do 100%\nof the run_hook_(ve|le) conversions first, in that case, and delete the\nold hook API. But at this point I think it would be a pretty large\namount of overhead to switch.\n\n> \n> [...]\n> > ---\n> >  builtin/hook.c                | 30 ++++++++++++++++++++\n> >  hook.c                        | 52 ++++++++++++++++++++++++++++++++---\n> >  hook.h                        |  3 ++\n> >  t/t1360-config-based-hooks.sh | 28 +++++++++++++++++++\n> >  4 files changed, 109 insertions(+), 4 deletions(-)\n> \n> Needs docs.\n\nDone\n\n> \n> [...]\n> > --- a/builtin/hook.c\n> > +++ b/builtin/hook.c\n> > @@ -5,9 +5,11 @@\n> [...]\n> > +static int run(int argc, const char **argv, const char *prefix)\n> > +{\n> > +\tstruct strbuf hookname = STRBUF_INIT;\n> > +\tstruct strvec envs = STRVEC_INIT;\n> > +\tstruct strvec args = STRVEC_INIT;\n> > +\n> > +\tstruct option run_options[] = {\n> > +\t\tOPT_STRVEC('e', \"env\", &envs, N_(\"var\"),\n> > +\t\t\t   N_(\"environment variables for hook to use\")),\n> > +\t\tOPT_STRVEC('a', \"arg\", &args, N_(\"args\"),\n> > +\t\t\t   N_(\"argument to pass to hook\")),\n> > +\t\tOPT_END(),\n> > +\t};\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(_(\"a hookname must be provided to operate on.\"),\n> > +\t\t\t      builtin_hook_usage, run_options);\n> \n> Error message nit: what does it mean to operate on a hookname?\n> \n> Perhaps this should allude to the usage string?\n> \n> \tusage_msg_opt(_(\"missing <hookname> parameter\"), ...);\n> \n> Or to match the conversational approach of commands like \"clone\":\n> \n> \tusage_msg_opt(_(\"You must specify a hook to run.\"), ...);\n> \n\nYeah, I like this one. I noticed the same error (untranslated, even!) is\nused for list, so I'll fix that too.\n\n> [...]\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> >  /*\n> >   * NEEDSWORK: a stateful hook_head means we can't run two hook events in the\n> > @@ -21,13 +22,15 @@ void free_hook(struct hook *ptr)\n> >  \t}\n> >  }\n> >  \n> > -static void emplace_hook(struct list_head *pos, const char *command)\n> > +static void emplace_hook(struct list_head *pos, const char *command, int quoted)\n> >  {\n> >  \tstruct hook *to_add = malloc(sizeof(struct hook));\n> >  \tto_add->origin = current_config_scope();\n> >  \tstrbuf_init(&to_add->command, 0);\n> > -\t/* even with use_shell, run_command() needs quotes */\n> > -\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n> > +\tif (quoted)\n> > +\t\tstrbuf_addf(&to_add->command, \"'%s'\", command);\n> > +\telse\n> > +\t\tstrbuf_addstr(&to_add->command, command);\n> >  \n> >  \tlist_add_tail(&to_add->list, pos);\n> >  }\n> \n> This would need to use sq_quote_* to be safe, but we can do something\n> simpler: if we accumulate parameters in an argv_array passed to\n> run_command, then they will be safely passed to the shell without\n> triggering expansion.\n\nThanks. I'll do that - no point in duplicating the work :)\n\n - Emily\n\n\n\n"},{"id":"407041","messageId":"87mu0ygzk1.fsf@evledraar.gmail.com","threadId":"53527","inReplyTo":"20200909004939.1942347-2-emilyshaffer@google.com","subject":"Re: [PATCH v4 1/9] doc: propose hooks managed by the config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2020-10-07T09:23:10Z","receivedAt":"2020-10-07T09:23:16Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Wed, Sep 09 2020, Emily Shaffer wrote:\n\nFirst, thanks a lot for working on this. As you may have found I've done\nsome small amount of actual work in this area before, but mostly just\nblathered about it on the ML.\n\n> Begin a design document for config-based hooks, managed via git-hook.\n> Focus on an overview of the implementation and motivation for design\n> decisions. Briefly discuss the alternatives considered before this\n> point. Also, attempt to redefine terms to fit into a multihook world.\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...or by setting core.hooksPath in their local/global/system\nconfig. Granted it doesn't cover the malicious hook injection case\nyou're also trying to solve, but does address e.g. having a git server\nwith a lot of centralized hooks.\n\nThe \"trampoline script\" also isn't needed for the common case you\nmention, you just symlink the .git/hooks directory (as e.g. GitLab\ndoes). People usually use a trampoline script for e.g. using GNU\nparallel or something to execute N hooks.\n\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...which seems like an easy thing to add later by having a \"hookdir\" in\naddition to \"hookcmd\", i.e. just specify a glob there instead of a\ncmd/path.\n\nYou already use \"hookdir\" for something else though, so that's a bit\nconfusing, perhaps s/hookcmd/definehookcmd/ would be less confusing, or\nperhaps more confusing...\n\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\n> +\n> +Users with many hooks might want to run them simultaneously, if the hooks don't\n> +modify state; if one hook depends on another's output, then users will want to\n> +specify those dependencies. If we decide to solve this problem, we may want to\n> +look to modern build systems for inspiration on how to manage dependencies and\n> +parallel tasks.\n\nIf you're taking requests it would make me very happy if we had\nparallelism in this from day one. It's the kind of thing that's hard to\ndo by default once a feature is shipped since people will implicitly\ndepend on it not being there, i.e. we won't know what we're breaking.\n\nI think doing it this way is simple, covers most use cases, and solves a\nlot of the problems you note:\n\n1. Don't use config order to execute hooks, use glob'd name order\n   regardless of origin. I.e. a system-level hook is called \"001-first\"\n   is executed before a local hook called \"999-at-the-end\" (or the other\n   way around, i.e. hook origin doesn't matter).\n\n2. We execute hooks parallel in that glob order, i.e. a pthread for-loop\n   that starts the 001-first task first, eventually getting to\n   999-at-the-end N at a time. I.e. the same as:\n\n       parallel --jobs N --halt-on-error soon,fail=1\" ::: <hooks-in-glob-order>\n\n   This allows for parallelism but guarantees the very useful case of\n   having a global log hook being guaranteed to execute.\n\n3. A hook can define \"parallel=no\" in its config. We'll then run it\n   while no other hook is running.\n\n4. We don't attempt to do dependencies etc, if you need that sort of\n   complexity you can just make one of the hooks be a hook runner as\n   users do now for the common \"make it parallel\" case.\n\nIt's a relatively small change to the code you have already. I.e. the\nfor_each() in run_hooks() would be called N times for each continuous\nglob'd parallel/non-parallel segment, and hook_list()'s config parsing\nwould learn to spew those out as a list-of-lists.\n\nThis also gives you a rudimentary implementation of the dependency\nschema you proposed for free. I.e. a definition of (pseudocode):\n\n    hookcmd=000-first\n    parallel=no\n\n    hookcmd=250-middle-abc\n    hookcmd=250-middle-xyz\n\n    hookcmd=300-gather\n    parallel=no\n\n    hookcmd=999-the-end\n\nWould result in the pseudocode execution of;\n\n    segments=[[000-first],\n              [250-middle-abc, 250-middle-xyz],\n              [300-gather],\n              [999-the-end]]\n    for each s in segments:\n        ok = run_in_parallel(s)\n        last if !ok # or, depending on \"early exit?\" config\n\nI.e.:\n\n * The common case of people adding N hooks won't take sum(N) time.\n\n * parallel=no hooks aren't run in parallel with other non-parallel\n   hooks\n\n * We support a rudimentary dependency schema as a side-effect,\n   i.e. defining 300-gather as non-parallel allows it to act as the sole\n   \"reduce\" step in a map/reduce in a \"map\" step started with the 250-*\n   hooks.\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\nI think this part of the doc should note a bit of the context in\nhttps://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/\n\nI.e. even if we get a 100% secure hook implementation we've done\npractically nothing for overall security, since we'll still run the\npager, aliases etc. from that local repo.\n\nThis is a great step in the right direction, but it behooves us to note\nthat, so some user reading this documentation without context doesn't\nthink inspecting untrusted repositories like that is safe just because\nthey set the right hook settings in their config (once what's being\nproposed here is implemented).\n"},{"id":"408133","messageId":"20201022005848.GD2774782@google.com","threadId":"53527","inReplyTo":"87mu0ygzk1.fsf@evledraar.gmail.com","subject":"Re: [PATCH v4 1/9] doc: propose hooks managed by the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-10-22T00:58:48Z","receivedAt":"2020-10-22T00:58:56Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Wed, Oct 07, 2020 at 11:23:10AM +0200, Ævar Arnfjörð Bjarmason wrote:\n> \n> \n> On Wed, Sep 09 2020, Emily Shaffer wrote:\n> \n> First, thanks a lot for working on this. As you may have found I've done\n> some small amount of actual work in this area before, but mostly just\n> blathered about it on the ML.\n> \n> > Begin a design document for config-based hooks, managed via git-hook.\n> > Focus on an overview of the implementation and motivation for design\n> > decisions. Briefly discuss the alternatives considered before this\n> > point. Also, attempt to redefine terms to fit into a multihook world.\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> ...or by setting core.hooksPath in their local/global/system\n> config. Granted it doesn't cover the malicious hook injection case\n> you're also trying to solve, but does address e.g. having a git server\n> with a lot of centralized hooks.\n\nAha, setting core.hooksPath in the global/system config had not occurred\nto me.\n\n> \n> The \"trampoline script\" also isn't needed for the common case you\n> mention, you just symlink the .git/hooks directory (as e.g. GitLab\n> does). People usually use a trampoline script for e.g. using GNU\n> parallel or something to execute N hooks.\n\nHm, I don't think that's quite true. Symlinking out .git/hooks doesn't\ngive me more than one $HOOKDIR/pre-commit - it just gives me a different\none. So if I wanted to run three different hooks, $HOOKDIR/pre-commit\nwould need to do the work of all three, regardless of where $HOOKDIR\npoints. That's what I meant when I said \"multihooks\" in this section.\n\nBut I think what you're trying to say is this: the \"status quo\" section\ndoesn't fully cover the status quo. There are more tricks than I\nmentioned, e.g. 'git config --global core.hooksPath\n/home/emily/githook/' to get the same set of hooks to run everywhere.\nThis approach still has some drawbacks - for example, it doesn't allow\nme to use language-specific linters if I have repos in various\nlanguages, without exempting an individual repo from the ~/githook/ by\n'git config --local core.hooksPath\n/home/emily/my-python-thing/.git/hook'.\n\nIt looks like, then, the \"status quo\" section needs some rework for the\nnext iteration.\n\n> \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> ...which seems like an easy thing to add later by having a \"hookdir\" in\n> addition to \"hookcmd\", i.e. just specify a glob there instead of a\n> cmd/path.\n\nHum, interesting! Something like so:\n\n[hook.pre-commit]\n  command = last-minute-checks\n\n[hookdir.last-minute-checks]\n  dir = /home/emily/last-minute-checks/*\n\nAnd then the hooks library knows to go and run everything in\n~/last-minute-checks/. This is easier to keep fresh than:\n\n[hook.pre-commit]\n  command = /home/emily/last-minute-checks/c-linter\n  command = /home/emily/last-minute-checks/check-for-debug-prints\n  command = /home/emily/last-minute-checks/check-for-notes\n  ...\n\nI actually like the idea of this for folks who might have a small number\nof hooks they wrote for themselves. I wonder if it's applicable for\nsomething like git-secrets, which presumably users would grab with a\n'git clone' later.\n\nIt doesn't seem at odds with the rest of the design - how would you feel\nabout me adding it to the \"future work\" section at the end? Future work,\nrather than \"Emily will do this in the next couple of rounds\", because:\n - I think nobody already has their hooks in $HOOKDIR/hook/pre-commit.d\n   without a corresponding trampoline in $HOOKDIR/hook/pre-commit; so\n   they could still call that trampoline, for now\n - I think it might be prone to some bikeshedding - e.g. should we\n   recurse into ~/last-minute-checks/linters/c/? how far? what if some\n   script requires magic options? etc? But as I'm typing those questions\n   out they sound mostly trivial or ridiculous, so maybe my assessment\n   is wrong here.\n - It sounds like you might be keen to write it, or at the very least,\n   more keen than me\n - Practically speaking, I am not sure I have time to do it alongside\n   the rest of the series. Again, my bikeshedding assessment could be\n   wrong, and this extra feature could be totally trivial.\n\n> You already use \"hookdir\" for something else though, so that's a bit\n> confusing, perhaps s/hookcmd/definehookcmd/ would be less confusing, or\n> perhaps more confusing...\n\n\"Hookdir\" might be the wrong word to use, too - maybe it's better to\nmirror \"hookspath\" there. Eitherway, \"hookdir\" and \"hookspath\" are\nsimilar enough that I think it would be confusing, and \"hookcmd\" is\nalready getting some side-eye from me for not being a great choice.\n\nSome thoughts for \"a path to a directory in which multiple scripts for a\nsingle hook live\":\n - hookset\n - hookbatch (ugh, redundant with MS scripting)\n - hook.pre-commit.all-of = ~/last-minute-checks/\n -  \"   \"  .everything-in = \"   \"\n...?\n\nI think I named a couple silly ideas for \"hookcmd\" in another mail.\n\n> \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\n> > +\n> > +Users with many hooks might want to run them simultaneously, if the hooks don't\n> > +modify state; if one hook depends on another's output, then users will want to\n> > +specify those dependencies. If we decide to solve this problem, we may want to\n> > +look to modern build systems for inspiration on how to manage dependencies and\n> > +parallel tasks.\n> \n> If you're taking requests it would make me very happy if we had\n> parallelism in this from day one. It's the kind of thing that's hard to\n> do by default once a feature is shipped since people will implicitly\n> depend on it not being there, i.e. we won't know what we're breaking.\n\nHm. This might be tricky.\n\nSome hooks are inherently not able to be parallelized - for example,\nhooks which modify a given file, like the commit message draft. In\ngeneral, based on the handful of hooks I've converted locally, it's hard\nto check whether a callsite assumes a hook could have modified state.\nUsually this seems to be done with a call to find_hook() (\"was there a\nhook that might have run?\") and then reopening the file. Sometimes a\nfile is reopened unconditionally. Sometimes the find_hook() call is\nvery far away from the run_hook_le() call.\n\nThe rest, then, which only read a file and say yes or no, probably don't\nneed to have a strict ordering - at least as far as Git is concerned.\nAnd I think that's what you're worried about:\n\n[hook.theoretical-parallelizable-event]\n  command = check-and-mark-a-file-foo\n  command = check-file-foo-and-do-something-else\n  command = do-something-totally-unrelated\n\nOn day 1 of this feature, as written, this is safe. But if we aren't\ncareful and we start to parallelize *without* setting up dependency\nordering, e.g. 'git config --global hook.parallelize', and turn that on\nby default without warning anyone, then the author of this config will\nbe unhappy.\n\nBut as I read further, you're talking about specifically *not* allowing\ndependency ordering...\n\n> \n> I think doing it this way is simple, covers most use cases, and solves a\n> lot of the problems you note:\n> \n> 1. Don't use config order to execute hooks, use glob'd name order\n>    regardless of origin. I.e. a system-level hook is called \"001-first\"\n>    is executed before a local hook called \"999-at-the-end\" (or the other\n>    way around, i.e. hook origin doesn't matter).\n\nCan you say a little more about why different ordering schema would\nmatter, if we effectively don't care which jobs are in parallel with\nwhich, as you describe? I'm not quite following.\n\n> \n> 2. We execute hooks parallel in that glob order, i.e. a pthread for-loop\n>    that starts the 001-first task first, eventually getting to\n>    999-at-the-end N at a time. I.e. the same as:\n> \n>        parallel --jobs N --halt-on-error soon,fail=1\" ::: <hooks-in-glob-order>\n> \n>    This allows for parallelism but guarantees the very useful case of\n>    having a global log hook being guaranteed to execute.\n\nAh, I think you're suggesting the glob order specifically to make up for\n--halt-on-error in this case.\n\n> \n> 3. A hook can define \"parallel=no\" in its config. We'll then run it\n>    while no other hook is running.\n> \n> 4. We don't attempt to do dependencies etc, if you need that sort of\n>    complexity you can just make one of the hooks be a hook runner as\n>    users do now for the common \"make it parallel\" case.\n\nIf we aren't attempting any magical ordering, then I don't really see a\nbig difference between glob vs. config order - presumably for most users\nthe effect would be same, e.g. N = $(nproc * hyperthreading), M = (number of scripts I\ncare to run) probably will often result in M < N, so all jobs would run\nsimultaneously anyways.\n\n> \n> It's a relatively small change to the code you have already. I.e. the\n> for_each() in run_hooks() would be called N times for each continuous\n> glob'd parallel/non-parallel segment, and hook_list()'s config parsing\n> would learn to spew those out as a list-of-lists.\n> \n> This also gives you a rudimentary implementation of the dependency\n> schema you proposed for free. I.e. a definition of (pseudocode):\n> \n>     hookcmd=000-first\n>     parallel=no\n> \n>     hookcmd=250-middle-abc\n>     hookcmd=250-middle-xyz\n> \n>     hookcmd=300-gather\n>     parallel=no\n> \n>     hookcmd=999-the-end\n> \n> Would result in the pseudocode execution of;\n> \n>     segments=[[000-first],\n>               [250-middle-abc, 250-middle-xyz],\n\nHum. This seems to say \"folks who started their hooks with the same\nnumber agree that their hooks should also run simultaneously\" - which\nsounds like an even harder problem than \"how do I know my ordering\nnumber isn't the same as someone else's in another config file\". Or else\nI'm misunderstanding your pseudo :)\n\nAh, I see later you mention it directly as a dependency schema. I think\nthis offers the same set of problems I saw trying to use this as an\nordering schema, but worse in all the usual ways parallelism provides.\nIt is still impossible for someone writing a global or system config to\nknow where in the dependency chain more local hooks reside.\n\n>               [300-gather],\n>               [999-the-end]]\n>     for each s in segments:\n>         ok = run_in_parallel(s)\n>         last if !ok # or, depending on \"early exit?\" config\n> \n> I.e.:\n> \n>  * The common case of people adding N hooks won't take sum(N) time.\n> \n>  * parallel=no hooks aren't run in parallel with other non-parallel\n>    hooks\n> \n>  * We support a rudimentary dependency schema as a side-effect,\n>    i.e. defining 300-gather as non-parallel allows it to act as the sole\n>    \"reduce\" step in a map/reduce in a \"map\" step started with the 250-*\n>    hooks.\n\nAs I understand it, the main concerns you have about getting\nparallelization to happen on day 1 are like so:\n\n - keep users from assuming serial execution\n - avoid a messy schema change to deal with dependencies\n\nI see the benefit of the former; I don't like the new schema proposed by\nthe latter. I do see that not turning it on day 1 would prevent us from\nturning it on by default later, in case users did something silly like\nassume dependencies.\n\nHrm.\n\nI think we could turn on parallelization day 1 by providing an\nexplicitly-parallel API in hook.h (and a similar 'git hook run foo\n--parallel' flag), and being more careful when converting hooks to call\nrun_hooks_parallel() instead of run_hooks(). That way hooks which will\nnever be parallelizable (e.g. commit-msg) won't get burned later by us\ntrying to be clever. Everyone else who can be parallelized is, in config\norder, with no dependency management whatsoever. That leaves the door\nopen for us to add dependency management however we want later on, but\nusers can still roll their own with a launcher script today.\n\nI know I rambled a lot - I was trying to convince myself :) For now, I'd\nprefer to add more detail to the \"future work\" section of the doc and\nthen not touch this problem with a very long pole... ;) Thoughts\nwelcome.\n\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> I think this part of the doc should note a bit of the context in\n> https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/\n> \n> I.e. even if we get a 100% secure hook implementation we've done\n> practically nothing for overall security, since we'll still run the\n> pager, aliases etc. from that local repo.\n> \n> This is a great step in the right direction, but it behooves us to note\n> that, so some user reading this documentation without context doesn't\n> think inspecting untrusted repositories like that is safe just because\n> they set the right hook settings in their config (once what's being\n> proposed here is implemented).\n\nYeah, I agree. I'll try to make that clearer in the doc in the next\nreroll.\n\nVery sorry again for having missed this - I think the first weeks of\nOctober I was working from my local todo list instead of from the list\nof replies in mutt. Urk.\n\n - Emily\n"},{"id":"408275","messageId":"87zh4chi73.fsf@evledraar.gmail.com","threadId":"53527","inReplyTo":"20201022005848.GD2774782@google.com","subject":"Re: [PATCH v4 1/9] doc: propose hooks managed by the config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2020-10-23T19:10:24Z","receivedAt":"2020-10-23T19:10:29Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Oct 22 2020, Emily Shaffer wrote:\n\n> On Wed, Oct 07, 2020 at 11:23:10AM +0200, Ævar Arnfjörð Bjarmason wrote:\n>> \n>> \n>> On Wed, Sep 09 2020, Emily Shaffer wrote:\n>> \n>> First, thanks a lot for working on this. As you may have found I've done\n>> some small amount of actual work in this area before, but mostly just\n>> blathered about it on the ML.\n>> \n>> > Begin a design document for config-based hooks, managed via git-hook.\n>> > Focus on an overview of the implementation and motivation for design\n>> > decisions. Briefly discuss the alternatives considered before this\n>> > point. Also, attempt to redefine terms to fit into a multihook world.\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>> ...or by setting core.hooksPath in their local/global/system\n>> config. Granted it doesn't cover the malicious hook injection case\n>> you're also trying to solve, but does address e.g. having a git server\n>> with a lot of centralized hooks.\n>\n> Aha, setting core.hooksPath in the global/system config had not occurred\n> to me.\n\nIt's a useful hack.\n\n>> \n>> The \"trampoline script\" also isn't needed for the common case you\n>> mention, you just symlink the .git/hooks directory (as e.g. GitLab\n>> does). People usually use a trampoline script for e.g. using GNU\n>> parallel or something to execute N hooks.\n>\n> Hm, I don't think that's quite true. Symlinking out .git/hooks doesn't\n> give me more than one $HOOKDIR/pre-commit - it just gives me a different\n> one. So if I wanted to run three different hooks, $HOOKDIR/pre-commit\n> would need to do the work of all three, regardless of where $HOOKDIR\n> points. That's what I meant when I said \"multihooks\" in this section.\n>\n> But I think what you're trying to say is this: the \"status quo\" section\n> doesn't fully cover the status quo. There are more tricks than I\n> mentioned, e.g. 'git config --global core.hooksPath\n> /home/emily/githook/' to get the same set of hooks to run everywhere.\n> This approach still has some drawbacks - for example, it doesn't allow\n> me to use language-specific linters if I have repos in various\n> languages, without exempting an individual repo from the ~/githook/ by\n> 'git config --local core.hooksPath\n> /home/emily/my-python-thing/.git/hook'.\n>\n> It looks like, then, the \"status quo\" section needs some rework for the\n> next iteration.\n\nRe-reading your original patch I think I just misread that. I thought\nyou were saying a stub script was needed in the .git to point to a\nmulti-hook script, but I was pointing out that you can just symlink to\nthe multi-hook script (as e.g. GitLab does), but reading it again & this\nI don't thin you meant that at all. Nevermind.\n\n>> \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>> ...which seems like an easy thing to add later by having a \"hookdir\" in\n>> addition to \"hookcmd\", i.e. just specify a glob there instead of a\n>> cmd/path.\n>\n> Hum, interesting! Something like so:\n>\n> [hook.pre-commit]\n>   command = last-minute-checks\n>\n> [hookdir.last-minute-checks]\n>   dir = /home/emily/last-minute-checks/*\n>\n> And then the hooks library knows to go and run everything in\n> ~/last-minute-checks/. This is easier to keep fresh than:\n>\n> [hook.pre-commit]\n>   command = /home/emily/last-minute-checks/c-linter\n>   command = /home/emily/last-minute-checks/check-for-debug-prints\n>   command = /home/emily/last-minute-checks/check-for-notes\n>   ...\n>\n> I actually like the idea of this for folks who might have a small number\n> of hooks they wrote for themselves. I wonder if it's applicable for\n> something like git-secrets, which presumably users would grab with a\n> 'git clone' later.\n>\n> It doesn't seem at odds with the rest of the design - how would you feel\n> about me adding it to the \"future work\" section at the end? Future work,\n> rather than \"Emily will do this in the next couple of rounds\", because:\n>  - I think nobody already has their hooks in $HOOKDIR/hook/pre-commit.d\n>    without a corresponding trampoline in $HOOKDIR/hook/pre-commit; so\n>    they could still call that trampoline, for now\n>  - I think it might be prone to some bikeshedding - e.g. should we\n>    recurse into ~/last-minute-checks/linters/c/? how far? what if some\n>    script requires magic options? etc? But as I'm typing those questions\n>    out they sound mostly trivial or ridiculous, so maybe my assessment\n>    is wrong here.\n>  - It sounds like you might be keen to write it, or at the very least,\n>    more keen than me\n>  - Practically speaking, I am not sure I have time to do it alongside\n>    the rest of the series. Again, my bikeshedding assessment could be\n>    wrong, and this extra feature could be totally trivial.\n>\n>> You already use \"hookdir\" for something else though, so that's a bit\n>> confusing, perhaps s/hookcmd/definehookcmd/ would be less confusing, or\n>> perhaps more confusing...\n>\n> \"Hookdir\" might be the wrong word to use, too - maybe it's better to\n> mirror \"hookspath\" there. Eitherway, \"hookdir\" and \"hookspath\" are\n> similar enough that I think it would be confusing, and \"hookcmd\" is\n> already getting some side-eye from me for not being a great choice.\n>\n> Some thoughts for \"a path to a directory in which multiple scripts for a\n> single hook live\":\n>  - hookset\n>  - hookbatch (ugh, redundant with MS scripting)\n>  - hook.pre-commit.all-of = ~/last-minute-checks/\n>  -  \"   \"  .everything-in = \"   \"\n> ...?\n>\n> I think I named a couple silly ideas for \"hookcmd\" in another mail.\n\nTo both of the above: Yeah I'm not saying you need to do the work, just\nthat I think it would be a useful case to bikeshed now since it seems\ninevitable that we'll get a \"find hooks in this dir by glob\" once we\nhave this facility. So having a config syntax for that which isn't\noverly confusing / extensible to that case would be useful, i.e. as the\ncurrent syntax uses \"dir\" already.\n\n>> \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\n>> > +\n>> > +Users with many hooks might want to run them simultaneously, if the hooks don't\n>> > +modify state; if one hook depends on another's output, then users will want to\n>> > +specify those dependencies. If we decide to solve this problem, we may want to\n>> > +look to modern build systems for inspiration on how to manage dependencies and\n>> > +parallel tasks.\n>> \n>> If you're taking requests it would make me very happy if we had\n>> parallelism in this from day one. It's the kind of thing that's hard to\n>> do by default once a feature is shipped since people will implicitly\n>> depend on it not being there, i.e. we won't know what we're breaking.\n>\n> Hm. This might be tricky.\n>\n> Some hooks are inherently not able to be parallelized - for example,\n> hooks which modify a given file, like the commit message draft. In\n> general, based on the handful of hooks I've converted locally, it's hard\n> to check whether a callsite assumes a hook could have modified state.\n> Usually this seems to be done with a call to find_hook() (\"was there a\n> hook that might have run?\") and then reopening the file. Sometimes a\n> file is reopened unconditionally. Sometimes the find_hook() call is\n> very far away from the run_hook_le() call.\n>\n> The rest, then, which only read a file and say yes or no, probably don't\n> need to have a strict ordering - at least as far as Git is concerned.\n> And I think that's what you're worried about:\n>\n> [hook.theoretical-parallelizable-event]\n>   command = check-and-mark-a-file-foo\n>   command = check-file-foo-and-do-something-else\n>   command = do-something-totally-unrelated\n>\n> On day 1 of this feature, as written, this is safe. But if we aren't\n> careful and we start to parallelize *without* setting up dependency\n> ordering, e.g. 'git config --global hook.parallelize', and turn that on\n> by default without warning anyone, then the author of this config will\n> be unhappy.\n>\n> But as I read further, you're talking about specifically *not* allowing\n> dependency ordering...\n>\n>> \n>> I think doing it this way is simple, covers most use cases, and solves a\n>> lot of the problems you note:\n>> \n>> 1. Don't use config order to execute hooks, use glob'd name order\n>>    regardless of origin. I.e. a system-level hook is called \"001-first\"\n>>    is executed before a local hook called \"999-at-the-end\" (or the other\n>>    way around, i.e. hook origin doesn't matter).\n>\n> Can you say a little more about why different ordering schema would\n> matter, if we effectively don't care which jobs are in parallel with\n> which, as you describe? I'm not quite following.\n>\n>> \n>> 2. We execute hooks parallel in that glob order, i.e. a pthread for-loop\n>>    that starts the 001-first task first, eventually getting to\n>>    999-at-the-end N at a time. I.e. the same as:\n>> \n>>        parallel --jobs N --halt-on-error soon,fail=1\" ::: <hooks-in-glob-order>\n>> \n>>    This allows for parallelism but guarantees the very useful case of\n>>    having a global log hook being guaranteed to execute.\n>\n> Ah, I think you're suggesting the glob order specifically to make up for\n> --halt-on-error in this case.\n>\n>> \n>> 3. A hook can define \"parallel=no\" in its config. We'll then run it\n>>    while no other hook is running.\n>> \n>> 4. We don't attempt to do dependencies etc, if you need that sort of\n>>    complexity you can just make one of the hooks be a hook runner as\n>>    users do now for the common \"make it parallel\" case.\n>\n> If we aren't attempting any magical ordering, then I don't really see a\n> big difference between glob vs. config order - presumably for most users\n> the effect would be same, e.g. N = $(nproc * hyperthreading), M = (number of scripts I\n> care to run) probably will often result in M < N, so all jobs would run\n> simultaneously anyways.\n>\n>> \n>> It's a relatively small change to the code you have already. I.e. the\n>> for_each() in run_hooks() would be called N times for each continuous\n>> glob'd parallel/non-parallel segment, and hook_list()'s config parsing\n>> would learn to spew those out as a list-of-lists.\n>> \n>> This also gives you a rudimentary implementation of the dependency\n>> schema you proposed for free. I.e. a definition of (pseudocode):\n>> \n>>     hookcmd=000-first\n>>     parallel=no\n>> \n>>     hookcmd=250-middle-abc\n>>     hookcmd=250-middle-xyz\n>> \n>>     hookcmd=300-gather\n>>     parallel=no\n>> \n>>     hookcmd=999-the-end\n>> \n>> Would result in the pseudocode execution of;\n>> \n>>     segments=[[000-first],\n>>               [250-middle-abc, 250-middle-xyz],\n>\n> Hum. This seems to say \"folks who started their hooks with the same\n> number agree that their hooks should also run simultaneously\" - which\n> sounds like an even harder problem than \"how do I know my ordering\n> number isn't the same as someone else's in another config file\". Or else\n> I'm misunderstanding your pseudo :)\n\nThe prefix number isn't meaningful in that way, i.e. if you have 10\nthreads and 5 hooks starting with 250-* they won't all be invoked at the\nsame time.\n\n> Ah, I see later you mention it directly as a dependency schema. I think\n> this offers the same set of problems I saw trying to use this as an\n> ordering schema, but worse in all the usual ways parallelism provides.\n> It is still impossible for someone writing a global or system config to\n> know where in the dependency chain more local hooks reside.\n>\n>>               [300-gather],\n>>               [999-the-end]]\n>>     for each s in segments:\n>>         ok = run_in_parallel(s)\n>>         last if !ok # or, depending on \"early exit?\" config\n>> \n>> I.e.:\n>> \n>>  * The common case of people adding N hooks won't take sum(N) time.\n>> \n>>  * parallel=no hooks aren't run in parallel with other non-parallel\n>>    hooks\n>> \n>>  * We support a rudimentary dependency schema as a side-effect,\n>>    i.e. defining 300-gather as non-parallel allows it to act as the sole\n>>    \"reduce\" step in a map/reduce in a \"map\" step started with the 250-*\n>>    hooks.\n>\n> As I understand it, the main concerns you have about getting\n> parallelization to happen on day 1 are like so:\n>\n>  - keep users from assuming serial execution\n>  - avoid a messy schema change to deal with dependencies\n>\n> I see the benefit of the former; I don't like the new schema proposed by\n> the latter. I do see that not turning it on day 1 would prevent us from\n> turning it on by default later, in case users did something silly like\n> assume dependencies.\n>\n> Hrm.\n>\n> I think we could turn on parallelization day 1 by providing an\n> explicitly-parallel API in hook.h (and a similar 'git hook run foo\n> --parallel' flag), and being more careful when converting hooks to call\n> run_hooks_parallel() instead of run_hooks(). That way hooks which will\n> never be parallelizable (e.g. commit-msg) won't get burned later by us\n> trying to be clever. Everyone else who can be parallelized is, in config\n> order, with no dependency management whatsoever. That leaves the door\n> open for us to add dependency management however we want later on, but\n> users can still roll their own with a launcher script today.\n>\n> I know I rambled a lot - I was trying to convince myself :) For now, I'd\n> prefer to add more detail to the \"future work\" section of the doc and\n> then not touch this problem with a very long pole... ;) Thoughts\n> welcome.\n\nI'm replying to much of the above in general here, particularly since\nmuch of it was in the form of a question you answered yourself later :)\n\nYes as you point out the reason I'm raising the parallel thing now is\n\"keep users from assuming serial execution\", i.e. any implementation\nthat isn't like that from day 1 will need more verbose syntax to opt-in\nto that.\n\nI think parallel is the sane default, although there's a really strong\ncase as you point out with the \"commit-msg\" hook for treating that on a\nhook-type basis. E.g. commit-msg (in-place editing of as single file)\nbeing non-parallel by default, but e.g. post-commit, pre-applypatch,\npre-receive and other \"should we proceed?\" hooks being parallel.\n\nBut I'm also raising a general concern with the design of the API /\ncommand around this.\n\nI don't see the need for having a git hook list/edit/add command at\nall. We should just keep this simpler and be able to point to \"git\nconfig --add/--get-regexp\" etc.\n\nIt seems the reason to introduce this command API around it is because\nyou're imagining that git needs to manage hooks whose relative execution\norder is important, and to later on once this lands aim to implement a\nmuch more complex dependency management schema.\n\nI just can't imagine a case that needs that where say those 10 hooks\nneed to execute in exact order 1/2/3/4 where the author of that tight\ncoupling wouldn't also desire to roll that all into one script, or at\nleast that it's an obscure enough case that we can just say \"do that\".\n\nWhereas I do think \"run a bunch of independent checks, if all pass\nproceed\" is *the* common case, e.g. adding a bunch of pre-receive\nhooks. If we tell the user we'll treat those as independent programs we\ncan run them in parallel. The vast majority of users will benefit from\nthe default faster execution.\n\nThe \"glob order\" case I mentioned is extra complexity on top of that,\nyes, but I think that concession is sane for the common case of \"yes\nparallel, but I want to always run the always-exit-0 log\nhook\". E.g. I've used this to setup a hook to run push\nattempts/successes in a hook framework that runs N pre-receive hooks.\n\nAll that being said I'm open to being convinced, I just don't see what\nthe target user is, and the submitted docs don't really make a case for\nit. I.e. there's plenty of \"what\" not \"why would someone want this...\".\n\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>> I think this part of the doc should note a bit of the context in\n>> https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/\n>> \n>> I.e. even if we get a 100% secure hook implementation we've done\n>> practically nothing for overall security, since we'll still run the\n>> pager, aliases etc. from that local repo.\n>> \n>> This is a great step in the right direction, but it behooves us to note\n>> that, so some user reading this documentation without context doesn't\n>> think inspecting untrusted repositories like that is safe just because\n>> they set the right hook settings in their config (once what's being\n>> proposed here is implemented).\n>\n> Yeah, I agree. I'll try to make that clearer in the doc in the next\n> reroll.\n>\n> Very sorry again for having missed this - I think the first weeks of\n> October I was working from my local todo list instead of from the list\n> of replies in mutt. Urk.\n\n*nod*\n"},{"id":"408648","messageId":"20201029153858.GG2774782@google.com","threadId":"53527","inReplyTo":"87zh4chi73.fsf@evledraar.gmail.com","subject":"Re: [PATCH v4 1/9] doc: propose hooks managed by the config","fromName":"Emily Shaffer","fromEmail":"emilyshaffer@google.com","sentAt":"2020-10-29T15:38:58Z","receivedAt":"2020-10-29T15:39:10Z","isPatch":true,"sender":{"key":"nasamuffin@google.com","avatar":"https://avatars.githubusercontent.com/u/1606826?v=4"},"body":"On Fri, Oct 23, 2020 at 09:10:24PM +0200, Ævar Arnfjörð Bjarmason wrote:\n\n> >> You already use \"hookdir\" for something else though, so that's a bit\n> >> confusing, perhaps s/hookcmd/definehookcmd/ would be less confusing, or\n> >> perhaps more confusing...\n> >\n> > \"Hookdir\" might be the wrong word to use, too - maybe it's better to\n> > mirror \"hookspath\" there. Eitherway, \"hookdir\" and \"hookspath\" are\n> > similar enough that I think it would be confusing, and \"hookcmd\" is\n> > already getting some side-eye from me for not being a great choice.\n> >\n> > Some thoughts for \"a path to a directory in which multiple scripts for a\n> > single hook live\":\n> >  - hookset\n> >  - hookbatch (ugh, redundant with MS scripting)\n> >  - hook.pre-commit.all-of = ~/last-minute-checks/\n> >  -  \"   \"  .everything-in = \"   \"\n> > ...?\n> >\n> > I think I named a couple silly ideas for \"hookcmd\" in another mail.\n> \n> To both of the above: Yeah I'm not saying you need to do the work, just\n> that I think it would be a useful case to bikeshed now since it seems\n> inevitable that we'll get a \"find hooks in this dir by glob\" once we\n> have this facility. So having a config syntax for that which isn't\n> overly confusing / extensible to that case would be useful, i.e. as the\n> current syntax uses \"dir\" already.\n\nYeah. I'm not sure that it needs to happen right away. Because\nhook.*.command // hookcommand.*.command gets passed right into\nrun_command()-with-shell, it's possible for a user who's keen to also\nset `hook.*.command = find -type f /some/path | xargs` in the meantime.\nAnd also because it's passed right into run_command()-with-shell, it's\nhard to do some smart wildcarding on the .command config and try to\nfigure out the right syntax. I'd just as soon see something explicit\nlike the configs I mentioned above, which can be added pretty easily\nafter the fact. I think what you're mostly saying, though, is \"Leave\nsome words for glob execution!\" and that I can appreciate.\n\n> > Hum. This seems to say \"folks who started their hooks with the same\n> > number agree that their hooks should also run simultaneously\" - which\n> > sounds like an even harder problem than \"how do I know my ordering\n> > number isn't the same as someone else's in another config file\". Or else\n> > I'm misunderstanding your pseudo :)\n> \n> The prefix number isn't meaningful in that way, i.e. if you have 10\n> threads and 5 hooks starting with 250-* they won't all be invoked at the\n> same time.\n\nOk. I misunderstood, then.\n\n> > I know I rambled a lot - I was trying to convince myself :) For now, I'd\n> > prefer to add more detail to the \"future work\" section of the doc and\n> > then not touch this problem with a very long pole... ;) Thoughts\n> > welcome.\n> \n> I'm replying to much of the above in general here, particularly since\n> much of it was in the form of a question you answered yourself later :)\n> \n> Yes as you point out the reason I'm raising the parallel thing now is\n> \"keep users from assuming serial execution\", i.e. any implementation\n> that isn't like that from day 1 will need more verbose syntax to opt-in\n> to that.\n> \n> I think parallel is the sane default, although there's a really strong\n> case as you point out with the \"commit-msg\" hook for treating that on a\n> hook-type basis. E.g. commit-msg (in-place editing of as single file)\n> being non-parallel by default, but e.g. post-commit, pre-applypatch,\n> pre-receive and other \"should we proceed?\" hooks being parallel.\n\nYeah. I think you've sold me. So what I will do is thus: before I send\nthe next reroll (as I'm pretty much done, locally, and hope to be ready\nfor nits next time) I'll take a look in 'git help githooks' and see\nwhich ones expect writes to occur. I think there are more than just\n\"commit-msg\". I'll add a bit to run_hooks() and a corresponding flag to\n'git hook run', plus relevant documentation. I'll also plan to add\nexplicit documentation to 'git help githooks' mentioning parallel vs.\nserial execution.\n\nBut I will plan on writing it stupidly - user configurable job number\nbut no dependency checking; and let the user turn off parallel execution\nfor everyone (hook.jobs=1) or for just one hook\n(hook.pre-commit.parallel = false (?)). Like you and Jonathan N say, we\ncan add more sugar like hookcmd.*.depends later on when we need it.\n\n> \n> But I'm also raising a general concern with the design of the API /\n> command around this.\n> \n> I don't see the need for having a git hook list/edit/add command at\n> all. We should just keep this simpler and be able to point to \"git\n> config --add/--get-regexp\" etc.\n> \n> It seems the reason to introduce this command API around it is because\n> you're imagining that git needs to manage hooks whose relative execution\n> order is important, and to later on once this lands aim to implement a\n> much more complex dependency management schema.\n\nNo, I don't think that's the reason to have list/edit/add. The reason is\nmore for discoverability (if I 'git help git' or 'git^TAB', do I see\nsomething handy in the command list that I didn't know about before?)\nand user friendliness (\"I can't remember the right config options to set\nthis up every dang time\"). And 'list', I think, is handy for giving\nusers a dry run of what they can expect to see happen (and where to fix\nthem, since it lists the origin). Yes, a user could put it all together\nfrom invocations of 'git config', but I personally think it's more\nuseful for Git to tell me what Git is going to do/what Git wants than\nfor my meat brain to try and guess :)\n\n> \n> I just can't imagine a case that needs that where say those 10 hooks\n> need to execute in exact order 1/2/3/4 where the author of that tight\n> coupling wouldn't also desire to roll that all into one script, or at\n> least that it's an obscure enough case that we can just say \"do that\".\n> \n> Whereas I do think \"run a bunch of independent checks, if all pass\n> proceed\" is *the* common case, e.g. adding a bunch of pre-receive\n> hooks. If we tell the user we'll treat those as independent programs we\n> can run them in parallel. The vast majority of users will benefit from\n> the default faster execution.\n> \n> The \"glob order\" case I mentioned is extra complexity on top of that,\n> yes, but I think that concession is sane for the common case of \"yes\n> parallel, but I want to always run the always-exit-0 log\n> hook\". E.g. I've used this to setup a hook to run push\n> attempts/successes in a hook framework that runs N pre-receive hooks.\n\nReading this, I think I'm still missing something key about what you\nthink glob ordering provides. I'm not following why having the log hook\nset early requires glob ordering over config ordering (since the config\nordering schema allows reordering via replacement), and I'm not\nfollowing why it's required to halt on failure.\n\n> \n> All that being said I'm open to being convinced, I just don't see what\n> the target user is, and the submitted docs don't really make a case for\n> it. I.e. there's plenty of \"what\" not \"why would someone want this...\".\n\nACK. I'll try and go over the doc again before I reroll.\n\n - Emily\n"},{"id":"408656","messageId":"87tuuchk8o.fsf@evledraar.gmail.com","threadId":"53527","inReplyTo":"20201029153858.GG2774782@google.com","subject":"Re: [PATCH v4 1/9] doc: propose hooks managed by the config","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2020-10-29T20:04:23Z","receivedAt":"2020-10-29T20:04:30Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Oct 29 2020, Emily Shaffer wrote:\n\n> On Fri, Oct 23, 2020 at 09:10:24PM +0200, Ævar Arnfjörð Bjarmason wrote:\n>\n>> >> You already use \"hookdir\" for something else though, so that's a bit\n>> >> confusing, perhaps s/hookcmd/definehookcmd/ would be less confusing, or\n>> >> perhaps more confusing...\n>> >\n>> > \"Hookdir\" might be the wrong word to use, too - maybe it's better to\n>> > mirror \"hookspath\" there. Eitherway, \"hookdir\" and \"hookspath\" are\n>> > similar enough that I think it would be confusing, and \"hookcmd\" is\n>> > already getting some side-eye from me for not being a great choice.\n>> >\n>> > Some thoughts for \"a path to a directory in which multiple scripts for a\n>> > single hook live\":\n>> >  - hookset\n>> >  - hookbatch (ugh, redundant with MS scripting)\n>> >  - hook.pre-commit.all-of = ~/last-minute-checks/\n>> >  -  \"   \"  .everything-in = \"   \"\n>> > ...?\n>> >\n>> > I think I named a couple silly ideas for \"hookcmd\" in another mail.\n>> \n>> To both of the above: Yeah I'm not saying you need to do the work, just\n>> that I think it would be a useful case to bikeshed now since it seems\n>> inevitable that we'll get a \"find hooks in this dir by glob\" once we\n>> have this facility. So having a config syntax for that which isn't\n>> overly confusing / extensible to that case would be useful, i.e. as the\n>> current syntax uses \"dir\" already.\n>\n> Yeah. I'm not sure that it needs to happen right away. Because\n> hook.*.command // hookcommand.*.command gets passed right into\n> run_command()-with-shell, it's possible for a user who's keen to also\n> set `hook.*.command = find -type f /some/path | xargs` in the meantime.\n> And also because it's passed right into run_command()-with-shell, it's\n> hard to do some smart wildcarding on the .command config and try to\n> figure out the right syntax. I'd just as soon see something explicit\n> like the configs I mentioned above, which can be added pretty easily\n> after the fact. I think what you're mostly saying, though, is \"Leave\n> some words for glob execution!\" and that I can appreciate.\n\nYeah, or rather, just now in config key naming think about if the key\nnaming makes sense if it's expanded to support such glob inclusion,\nwhich seems like a desired addition. But I won't belabor that point.\n\nJust one thing to add: We don't really need to come up with a syntax &\nsemantics for glob inclusion special to this, we'd use the sort of glob\npatterns \"Conditional includes\" use, as documented in  git-config(1).\n\n>> > Hum. This seems to say \"folks who started their hooks with the same\n>> > number agree that their hooks should also run simultaneously\" - which\n>> > sounds like an even harder problem than \"how do I know my ordering\n>> > number isn't the same as someone else's in another config file\". Or else\n>> > I'm misunderstanding your pseudo :)\n>> \n>> The prefix number isn't meaningful in that way, i.e. if you have 10\n>> threads and 5 hooks starting with 250-* they won't all be invoked at the\n>> same time.\n>\n> Ok. I misunderstood, then.\n>\n>> > I know I rambled a lot - I was trying to convince myself :) For now, I'd\n>> > prefer to add more detail to the \"future work\" section of the doc and\n>> > then not touch this problem with a very long pole... ;) Thoughts\n>> > welcome.\n>> \n>> I'm replying to much of the above in general here, particularly since\n>> much of it was in the form of a question you answered yourself later :)\n>> \n>> Yes as you point out the reason I'm raising the parallel thing now is\n>> \"keep users from assuming serial execution\", i.e. any implementation\n>> that isn't like that from day 1 will need more verbose syntax to opt-in\n>> to that.\n>> \n>> I think parallel is the sane default, although there's a really strong\n>> case as you point out with the \"commit-msg\" hook for treating that on a\n>> hook-type basis. E.g. commit-msg (in-place editing of as single file)\n>> being non-parallel by default, but e.g. post-commit, pre-applypatch,\n>> pre-receive and other \"should we proceed?\" hooks being parallel.\n>\n> Yeah. I think you've sold me. So what I will do is thus: before I send\n> the next reroll (as I'm pretty much done, locally, and hope to be ready\n> for nits next time) I'll take a look in 'git help githooks' and see\n> which ones expect writes to occur. I think there are more than just\n> \"commit-msg\". I'll add a bit to run_hooks() and a corresponding flag to\n> 'git hook run', plus relevant documentation. I'll also plan to add\n> explicit documentation to 'git help githooks' mentioning parallel vs.\n> serial execution.\n\nSounds good.\n\n> But I will plan on writing it stupidly - user configurable job number\n> but no dependency checking; and let the user turn off parallel execution\n> for everyone (hook.jobs=1) or for just one hook\n> (hook.pre-commit.parallel = false (?)). Like you and Jonathan N say, we\n> can add more sugar like hookcmd.*.depends later on when we need it.\n\nYeah, that sounds great. As long as there's parallelism that stuff can\nalways be tweaked later.\n\n>> \n>> But I'm also raising a general concern with the design of the API /\n>> command around this.\n>> \n>> I don't see the need for having a git hook list/edit/add command at\n>> all. We should just keep this simpler and be able to point to \"git\n>> config --add/--get-regexp\" etc.\n>> \n>> It seems the reason to introduce this command API around it is because\n>> you're imagining that git needs to manage hooks whose relative execution\n>> order is important, and to later on once this lands aim to implement a\n>> much more complex dependency management schema.\n>\n> No, I don't think that's the reason to have list/edit/add. The reason is\n> more for discoverability (if I 'git help git' or 'git^TAB', do I see\n> something handy in the command list that I didn't know about before?)\n> and user friendliness (\"I can't remember the right config options to set\n> this up every dang time\"). And 'list', I think, is handy for giving\n> users a dry run of what they can expect to see happen (and where to fix\n> them, since it lists the origin). Yes, a user could put it all together\n> from invocations of 'git config', but I personally think it's more\n> useful for Git to tell me what Git is going to do/what Git wants than\n> for my meat brain to try and guess :)\n\nOkey, that makes sense & I've got nothing against that, just clarifying\nsince it *looked* like it was the first step in some future addition of\ncomplexity around this.\n\nIt would be nice if the docs for the new command were modified to state\nthat clearly, even to the point of saying \"this is really just sugar for\nthis similar git-config invocation\".\n\n>> \n>> I just can't imagine a case that needs that where say those 10 hooks\n>> need to execute in exact order 1/2/3/4 where the author of that tight\n>> coupling wouldn't also desire to roll that all into one script, or at\n>> least that it's an obscure enough case that we can just say \"do that\".\n>> \n>> Whereas I do think \"run a bunch of independent checks, if all pass\n>> proceed\" is *the* common case, e.g. adding a bunch of pre-receive\n>> hooks. If we tell the user we'll treat those as independent programs we\n>> can run them in parallel. The vast majority of users will benefit from\n>> the default faster execution.\n>> \n>> The \"glob order\" case I mentioned is extra complexity on top of that,\n>> yes, but I think that concession is sane for the common case of \"yes\n>> parallel, but I want to always run the always-exit-0 log\n>> hook\". E.g. I've used this to setup a hook to run push\n>> attempts/successes in a hook framework that runs N pre-receive hooks.\n>\n> Reading this, I think I'm still missing something key about what you\n> think glob ordering provides. \n\nFor context, I feel strongly that we should do parallel by default for\nimplementing something like this, it's great that per the above\ndiscussion you're open to that.\n\nThis \"glob ordering\" is an entirely separate idea I'm not strongly\nadvocating, there's pros & cons of doing that v.s. config ordering.\n\n * Con: less obvious than config order, you write hooks \"a c b\" in the\n   config and we execute in \"a b c\" order.\n\n * Pro: Sidesteps the issues you noted in \"Execution ordering\" in the\n   docs you're adding, i.e. now it'll be impossible to execute a\n   repo-local hook before a system-wide one, you can override that with\n   having a local one called \"000-something\".\n\n   I.e. now we'd read the config in the normal config order, and thus if\n   there's a system hook there's no way to define a local hook to run\n   first, until we get some sort of override for that.\n\n> I'm not following why having the log hook set early requires glob\n> ordering over config ordering (since the config ordering schema allows\n> reordering via replacement)\n> [...]\n>  and I'm not following why it's required to halt on failure.\n\nI realize I didn't elaborate on this, there's some past discussion[1][2]\nabout this. \n\nI.e. when running N hooks sometimes you'd want to run them all (e.g. to\nsend notifications), but for others such as pre-receive.d guard checks\nyou don't have to run all N, if one check (say one checks commit format\nvalidity, another code syntax) fails you'd like to abort early.\n\nSo halting on failure is just saving CPU, you might have 10 hooks that\neach take 1 second, no point in making the user wait on all 10 checks\nfor 10 seconds if a failure of any fails the push.\n\nBut OTOH you have other use-cases where users want to run them all\n(talked about in the [1][2] discussion above), so it's been anticipated\nas something we'd grow config for with multi-hook support.\n\nThe glob ordering allows common cases for things that aren't possible\nwith config-order with such early abort.\n\nE.g. consider a server with some common system-wide pre-receive.d hook\n(e.g. author e-mail envelope check), and a SOX/PCI controlled repository\nwhere some compliance thing says all push attempts must be logged.\n\nYou could then do:\n\n    /etc/git/hooks/pre-receive.d/email-check\n    /path/to/repo/hooks/pre-receive.d/000-log-push-attempt-to-db\n    /path/to/repo/hooks/pre-receive.d/some-other-check\n\nAnd we'd always run the 000-* hook first, whereas in the current schema\nyou can't do that without editing the system-wide config.\n\n>> \n>> All that being said I'm open to being convinced, I just don't see what\n>> the target user is, and the submitted docs don't really make a case for\n>> it. I.e. there's plenty of \"what\" not \"why would someone want this...\".\n>\n> ACK. I'll try and go over the doc again before I reroll.\n>\n>  - Emily\n\n1. https://lore.kernel.org/git/87wojjsv9p.fsf@evledraar.gmail.com/\n2. https://public-inbox.org/git/CACBZZX6j6q2DUN_Z-Pnent1u714dVNPFBrL_PiEQyLmCzLUVxg@mail.gmail.com/\n"}]}