aboutsummaryrefslogtreecommitdiff
path: root/hook.c
diff options
context:
space:
mode:
authorAdrian Ratiu <adrian.ratiu@collabora.com>2026-02-19 00:23:48 +0200
committerJunio C Hamano <gitster@pobox.com>2026-02-19 13:23:41 -0800
commit03b4043b9182bd3d36541371fa39f04d6d038286 (patch)
tree46af18d57733c40b3438b40b6029d22124c95a30 /hook.c
parent9fdaa6788924d4bb5ffc3a5908dae8a50e072f77 (diff)
downloadgit-03b4043b9182bd3d36541371fa39f04d6d038286.tar.xz
hook: include hooks from the config
Teach the hook.[hc] library to parse configs to populate the list of hooks to run for a given event. Multiple commands can be specified for a given hook by providing "hook.<friendly-name>.command = <path-to-hook>" and "hook.<friendly-name>.event = <hook-event>" lines. Hooks will be started in config order of the "hook.<name>.event" lines and will be run sequentially (.jobs == 1) like before. Running the hooks in parallel will be enabled in a future patch. The "traditional" hook from the hookdir is run last, if present. A strmap cache is added to struct repository to avoid re-reading the configs on each rook run. This is useful for hooks like the ref-transaction which gets executed multiple times per process. Examples: $ git config --get-regexp "^hook\." hook.bar.command=~/bar.sh hook.bar.event=pre-commit # Will run ~/bar.sh, then .git/hooks/pre-commit $ git hook run pre-commit Signed-off-by: Emily Shaffer <emilyshaffer@google.com> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Diffstat (limited to 'hook.c')
-rw-r--r--hook.c197
1 files changed, 196 insertions, 1 deletions
diff --git a/hook.c b/hook.c
index 979a97a538..8a9b405f76 100644
--- a/hook.c
+++ b/hook.c
@@ -4,9 +4,11 @@
#include "gettext.h"
#include "hook.h"
#include "path.h"
+#include "parse.h"
#include "run-command.h"
#include "config.h"
#include "strbuf.h"
+#include "strmap.h"
#include "environment.h"
#include "setup.h"
@@ -54,6 +56,10 @@ static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)
if (h->kind == HOOK_TRADITIONAL)
free((void *)h->u.traditional.path);
+ else if (h->kind == HOOK_CONFIGURED) {
+ free((void *)h->u.configured.friendly_name);
+ free((void *)h->u.configured.command);
+ }
if (cb_data_free)
cb_data_free(h->feed_pipe_cb_data);
@@ -101,6 +107,187 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,
string_list_append(hook_list, hook_path)->util = h;
}
+static void unsorted_string_list_remove(struct string_list *list,
+ const char *str)
+{
+ struct string_list_item *item = unsorted_string_list_lookup(list, str);
+ if (item)
+ unsorted_string_list_delete_item(list, item - list->items, 0);
+}
+
+/*
+ * Callback struct to collect all hook.* keys in a single config pass.
+ * commands: friendly-name to command map.
+ * event_hooks: event-name to list of friendly-names map.
+ * disabled_hooks: set of friendly-names with hook.name.enabled = false.
+ */
+struct hook_all_config_cb {
+ struct strmap commands;
+ struct strmap event_hooks;
+ struct string_list disabled_hooks;
+};
+
+/* repo_config() callback that collects all hook.* configuration in one pass. */
+static int hook_config_lookup_all(const char *key, const char *value,
+ const struct config_context *ctx UNUSED,
+ void *cb_data)
+{
+ struct hook_all_config_cb *data = cb_data;
+ const char *name, *subkey;
+ char *hook_name;
+ size_t name_len = 0;
+
+ if (parse_config_key(key, "hook", &name, &name_len, &subkey))
+ return 0;
+
+ if (!value)
+ return config_error_nonbool(key);
+
+ /* Extract name, ensuring it is null-terminated. */
+ hook_name = xmemdupz(name, name_len);
+
+ if (!strcmp(subkey, "event")) {
+ struct string_list *hooks =
+ strmap_get(&data->event_hooks, value);
+
+ if (!hooks) {
+ hooks = xcalloc(1, sizeof(*hooks));
+ string_list_init_dup(hooks);
+ strmap_put(&data->event_hooks, value, hooks);
+ }
+
+ /* Re-insert if necessary to preserve last-seen order. */
+ unsorted_string_list_remove(hooks, hook_name);
+ string_list_append(hooks, hook_name);
+ } else if (!strcmp(subkey, "command")) {
+ /* Store command overwriting the old value */
+ char *old = strmap_put(&data->commands, hook_name,
+ xstrdup(value));
+ free(old);
+ }
+
+ free(hook_name);
+ return 0;
+}
+
+/*
+ * The hook config cache maps each hook event name to a string_list where
+ * every item's string is the hook's friendly-name and its util pointer is
+ * the corresponding command string. Both strings are owned by the map.
+ *
+ * Disabled hooks and hooks missing a command are already filtered out at
+ * parse time, so callers can iterate the list directly.
+ */
+void hook_cache_clear(struct strmap *cache)
+{
+ struct hashmap_iter iter;
+ struct strmap_entry *e;
+
+ strmap_for_each_entry(cache, &iter, e) {
+ struct string_list *hooks = e->value;
+ string_list_clear(hooks, 1); /* free util (command) pointers */
+ free(hooks);
+ }
+ strmap_clear(cache, 0);
+}
+
+/* Populate `cache` with the complete hook configuration */
+static void build_hook_config_map(struct repository *r, struct strmap *cache)
+{
+ struct hook_all_config_cb cb_data;
+ struct hashmap_iter iter;
+ struct strmap_entry *e;
+
+ strmap_init(&cb_data.commands);
+ strmap_init(&cb_data.event_hooks);
+ string_list_init_dup(&cb_data.disabled_hooks);
+
+ /* Parse all configs in one run. */
+ repo_config(r, hook_config_lookup_all, &cb_data);
+
+ /* Construct the cache from parsed configs. */
+ strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
+ struct string_list *hook_names = e->value;
+ struct string_list *hooks = xcalloc(1, sizeof(*hooks));
+
+ string_list_init_dup(hooks);
+
+ for (size_t i = 0; i < hook_names->nr; i++) {
+ const char *hname = hook_names->items[i].string;
+ char *command;
+
+ command = strmap_get(&cb_data.commands, hname);
+ if (!command)
+ die(_("'hook.%s.command' must be configured or "
+ "'hook.%s.event' must be removed;"
+ " aborting."), hname, hname);
+
+ /* util stores the command; owned by the cache. */
+ string_list_append(hooks, hname)->util =
+ xstrdup(command);
+ }
+
+ strmap_put(cache, e->key, hooks);
+ }
+
+ strmap_clear(&cb_data.commands, 1);
+ string_list_clear(&cb_data.disabled_hooks, 0);
+ strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
+ string_list_clear(e->value, 0);
+ free(e->value);
+ }
+ strmap_clear(&cb_data.event_hooks, 0);
+}
+
+/* Return the hook config map for `r`, populating it first if needed. */
+static struct strmap *get_hook_config_cache(struct repository *r)
+{
+ struct strmap *cache = NULL;
+
+ if (r) {
+ /*
+ * For in-repo calls, the map is stored in r->hook_config_cache,
+ * so repeated invocations don't parse the configs, so allocate
+ * it just once on the first call.
+ */
+ if (!r->hook_config_cache) {
+ r->hook_config_cache = xcalloc(1, sizeof(*cache));
+ strmap_init(r->hook_config_cache);
+ build_hook_config_map(r, r->hook_config_cache);
+ }
+ cache = r->hook_config_cache;
+ }
+
+ return cache;
+}
+
+static void list_hooks_add_configured(struct repository *r,
+ const char *hookname,
+ struct string_list *list,
+ struct run_hooks_opt *options)
+{
+ struct strmap *cache = get_hook_config_cache(r);
+ struct string_list *configured_hooks = strmap_get(cache, hookname);
+
+ /* Iterate through configured hooks and initialize internal states */
+ for (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {
+ const char *friendly_name = configured_hooks->items[i].string;
+ const char *command = configured_hooks->items[i].util;
+ struct hook *hook = xcalloc(1, sizeof(struct hook));
+
+ if (options && options->feed_pipe_cb_data_alloc)
+ hook->feed_pipe_cb_data =
+ options->feed_pipe_cb_data_alloc(
+ options->feed_pipe_ctx);
+
+ hook->kind = HOOK_CONFIGURED;
+ hook->u.configured.friendly_name = xstrdup(friendly_name);
+ hook->u.configured.command = xstrdup(command);
+
+ string_list_append(list, friendly_name)->util = hook;
+ }
+}
+
struct string_list *list_hooks(struct repository *r, const char *hookname,
struct run_hooks_opt *options)
{
@@ -112,6 +299,9 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,
hook_head = xmalloc(sizeof(struct string_list));
string_list_init_dup(hook_head);
+ /* Add hooks from the config, e.g. hook.myhook.event = pre-commit */
+ list_hooks_add_configured(r, hookname, hook_head, options);
+
/* Add the default "traditional" hooks from hookdir. */
list_hooks_add_default(r, hookname, hook_head, options);
@@ -164,8 +354,13 @@ static int pick_next_hook(struct child_process *cp,
cp->dir = hook_cb->options->dir;
/* Add hook exec paths or commands */
- if (h->kind == HOOK_TRADITIONAL)
+ if (h->kind == HOOK_TRADITIONAL) {
strvec_push(&cp->args, h->u.traditional.path);
+ } else if (h->kind == HOOK_CONFIGURED) {
+ /* to enable oneliners, let config-specified hooks run in shell. */
+ cp->use_shell = true;
+ strvec_push(&cp->args, h->u.configured.command);
+ }
if (!cp->args.nr)
BUG("hook must have at least one command or exec path");