From patchwork Thu Sep 9 12:41:59 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= X-Patchwork-Id: 12483235 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-15.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,USER_AGENT_GIT autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 6AE2AC433FE for ; Thu, 9 Sep 2021 14:18:54 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 4ED8561059 for ; Thu, 9 Sep 2021 14:18:54 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S236735AbhIIOUC (ORCPT ); Thu, 9 Sep 2021 10:20:02 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:41866 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S243773AbhIIOTw (ORCPT ); Thu, 9 Sep 2021 10:19:52 -0400 Received: from mail-wm1-x32d.google.com (mail-wm1-x32d.google.com [IPv6:2a00:1450:4864:20::32d]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 09816C10DC43 for ; Thu, 9 Sep 2021 05:42:12 -0700 (PDT) Received: by mail-wm1-x32d.google.com with SMTP id e26so1268970wmk.2 for ; Thu, 09 Sep 2021 05:42:11 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=pH1SX2/ve5m5WQmbfBz+guRJpnO+MiXLeJ2Bw6zz3fk=; b=Wk9i9Qp4HDUOOIlQVJN8TqKUNKrhMIRS9YYR/FAB01de+ndzkBOAvN9NQwj775jgTR BBCN6NCHst/NrWZ3JA/uzJhluSD7XNwq47WtTjfYaaprGIOhfYgo67PJ/FtADQcvCNtK vRvcwnnr+EK8rCVSx+HuUR4vqiT+j0YauRcPdZol3q19CyGM4rmjCF2LB3qVhzdrRatc IALmLB4+s4v+WptyaySaK5O82+TtirtVYMVkbgQcg1Rrd57qpt71wOITkEkw/11aTKpq CsJqqRaR7DXsxO2Qcfnltjcqigkyq+yMKvRha/pPESg05V94f773lUf6lFsJ7x7AR69N oAzA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=pH1SX2/ve5m5WQmbfBz+guRJpnO+MiXLeJ2Bw6zz3fk=; b=iYsbXI13MJ5ytkSer5k256v/Pbe5ylFT80dC+riXxhs3E67uwj78QITODkLElVa5+p FTxKAvliRIvCweuNSWmaHn2USofEVAfdHpXCct9XP78FqZpi+XRkc80vueZk2QnmOtaa b0AQkSvb2t3kJ/S+s/I4pgC0zO/q1LowsgWlWlWlf7Xsj2xQcKYxrOGMW+rQhU64abvx lfQ3n/0ELthvRNiUV4A+9Pgggrdqq+X48+sy9Df5FNPNefsYLA+D7qcPaNH6XhiBPYf2 5LXWSAdjZ3IKBbveSeENpIfWUTZCclCEMLmtm9V657Wn5KBX2MZH9fDsLzDcs3xFdxoV yTtg== X-Gm-Message-State: AOAM5321eRIm8WZP7y8QTztHYkul+uQ7Eyj/3ZVpC4uzSNMo+fIuHm/0 bjXo7nkJIPkMmkN/nRNrmI0Q60ZmWC9b2Q== X-Google-Smtp-Source: ABdhPJxwnMgAfwYFq6giqgP5N64NEUq5mvCWLi+Kx8qOKFCuq1A67BWGkO9ZaismwgGy26TXSIfEvA== X-Received: by 2002:a7b:cc14:: with SMTP id f20mr2749751wmh.137.1631191330095; Thu, 09 Sep 2021 05:42:10 -0700 (PDT) Received: from vm.nix.is (vm.nix.is. [2a01:4f8:120:2468::2]) by smtp.gmail.com with ESMTPSA id j25sm1742081wrc.12.2021.09.09.05.42.09 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 09 Sep 2021 05:42:09 -0700 (PDT) From: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= To: git@vger.kernel.org Cc: Emily Shaffer , Junio C Hamano , Jeff King , Taylor Blau , Felipe Contreras , Eric Sunshine , "brian m . carlson" , Josh Steadmon , Jonathan Tan , Derrick Stolee , =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFy?= =?utf-8?b?bWFzb24=?= Subject: [PATCH v4 1/5] hook: run a list of hooks instead Date: Thu, 9 Sep 2021 14:41:59 +0200 Message-Id: X-Mailer: git-send-email 2.33.0.867.g88ec4638586 In-Reply-To: References: <20210819033450.3382652-1-emilyshaffer@google.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org From: Emily Shaffer To prepare for multihook support, teach hook.[hc] to take a list of hooks at run_hooks and run_found_hooks. Right now the list is always one entry, but in the future we will allow users to supply more than one executable for a single hook event. Signed-off-by: Emily Shaffer Signed-off-by: Ævar Arnfjörð Bjarmason --- builtin/hook.c | 14 ++++--- hook.c | 109 ++++++++++++++++++++++++++++++++++++------------- hook.h | 22 ++++++++-- 3 files changed, 107 insertions(+), 38 deletions(-) diff --git a/builtin/hook.c b/builtin/hook.c index fae69068201..398980523aa 100644 --- a/builtin/hook.c +++ b/builtin/hook.c @@ -25,7 +25,7 @@ static int run(int argc, const char **argv, const char *prefix) struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; int ignore_missing = 0; const char *hook_name; - const char *hook_path; + struct list_head *hooks; struct option run_options[] = { OPT_BOOL(0, "ignore-missing", &ignore_missing, N_("exit quietly with a zero exit code if the requested hook cannot be found")), @@ -63,16 +63,18 @@ static int run(int argc, const char **argv, const char *prefix) * run_hooks() instead... */ hook_name = argv[0]; - if (ignore_missing) + hooks = list_hooks(hook_name); + if (list_empty(hooks)) { + clear_hook_list(hooks); + /* ... act like a plain run_hooks() under --ignore-missing */ - return run_hooks_oneshot(hook_name, &opt); - hook_path = find_hook(hook_name); - if (!hook_path) { + if (ignore_missing) + return 0; error("cannot find a hook named %s", hook_name); return 1; } - ret = run_hooks(hook_name, hook_path, &opt); + ret = run_hooks(hook_name, hooks, &opt); run_hooks_opt_clear(&opt); return ret; usage: diff --git a/hook.c b/hook.c index d045379ade8..2b2c16a9095 100644 --- a/hook.c +++ b/hook.c @@ -3,6 +3,30 @@ #include "run-command.h" #include "config.h" +static void free_hook(struct hook *ptr) +{ + if (!ptr) + return; + + free(ptr->feed_pipe_cb_data); + free(ptr); +} + +static void remove_hook(struct list_head *to_remove) +{ + struct hook *hook_to_remove = list_entry(to_remove, struct hook, list); + list_del(to_remove); + free_hook(hook_to_remove); +} + +void clear_hook_list(struct list_head *head) +{ + struct list_head *pos, *tmp; + list_for_each_safe(pos, tmp, head) + remove_hook(pos); + free(head); +} + const char *find_hook(const char *name) { static struct strbuf path = STRBUF_INIT; @@ -39,7 +63,38 @@ const char *find_hook(const char *name) int hook_exists(const char *name) { - return !!find_hook(name); + struct list_head *hooks; + int exists; + + hooks = list_hooks(name); + exists = !list_empty(hooks); + clear_hook_list(hooks); + + return exists; +} + +struct list_head *list_hooks(const char *hookname) +{ + struct list_head *hook_head = xmalloc(sizeof(struct list_head)); + + INIT_LIST_HEAD(hook_head); + + if (!hookname) + BUG("null hookname was provided to hook_list()!"); + + if (have_git_dir()) { + const char *hook_path = find_hook(hookname); + + /* Add the hook from the hookdir */ + if (hook_path) { + struct hook *to_add = xmalloc(sizeof(*to_add)); + to_add->hook_path = hook_path; + to_add->feed_pipe_cb_data = NULL; + list_add_tail(&to_add->list, hook_head); + } + } + + return hook_head; } void run_hooks_opt_clear(struct run_hooks_opt *o) @@ -99,7 +154,10 @@ static int pick_next_hook(struct child_process *cp, cp->dir = hook_cb->options->dir; /* add command */ - strvec_push(&cp->args, run_me->hook_path); + if (hook_cb->options->absolute_path) + strvec_push(&cp->args, absolute_path(run_me->hook_path)); + else + strvec_push(&cp->args, run_me->hook_path); /* * add passed-in argv, without expanding - let the user get back @@ -110,12 +168,12 @@ static int pick_next_hook(struct child_process *cp, /* Provide context for errors if necessary */ *pp_task_cb = run_me; - /* - * This pick_next_hook() will be called again, we're only - * running one hook, so indicate that no more work will be - * done. - */ - hook_cb->run_me = NULL; + /* Get the next entry ready */ + if (hook_cb->run_me->list.next == hook_cb->head) + hook_cb->run_me = NULL; + else + hook_cb->run_me = list_entry(hook_cb->run_me->list.next, + struct hook, list); return 1; } @@ -150,13 +208,9 @@ static int notify_hook_finished(int result, return 0; } -int run_hooks(const char *hook_name, const char *hook_path, +int run_hooks(const char *hook_name, struct list_head *hooks, struct run_hooks_opt *options) { - struct strbuf abs_path = STRBUF_INIT; - struct hook my_hook = { - .hook_path = hook_path, - }; struct hook_cb_data cb_data = { .rc = 0, .hook_name = hook_name, @@ -168,11 +222,8 @@ int run_hooks(const char *hook_name, const char *hook_path, if (!options) BUG("a struct run_hooks_opt must be provided to run_hooks"); - if (options->absolute_path) { - strbuf_add_absolute_path(&abs_path, hook_path); - my_hook.hook_path = abs_path.buf; - } - cb_data.run_me = &my_hook; + cb_data.head = hooks; + cb_data.run_me = list_first_entry(hooks, struct hook, list); run_processes_parallel_tr2(jobs, pick_next_hook, @@ -184,18 +235,15 @@ int run_hooks(const char *hook_name, const char *hook_path, "hook", hook_name); - - if (options->absolute_path) - strbuf_release(&abs_path); - free(my_hook.feed_pipe_cb_data); + clear_hook_list(hooks); return cb_data.rc; } int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options) { - const char *hook_path; - int ret; + struct list_head *hooks; + int ret = 0; struct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT; if (!options) @@ -204,13 +252,16 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options) if (options->path_to_stdin && options->feed_pipe) BUG("choose only one method to populate stdin"); - hook_path = find_hook(hook_name); - if (!hook_path) { - ret = 0; + hooks = list_hooks(hook_name); + + /* + * If you need to act on a missing hook, use run_found_hooks() + * instead + */ + if (list_empty(hooks)) goto cleanup; - } - ret = run_hooks(hook_name, hook_path, options); + ret = run_hooks(hook_name, hooks, options); cleanup: run_hooks_opt_clear(options); diff --git a/hook.h b/hook.h index f6dac75f1cc..49b4c335f86 100644 --- a/hook.h +++ b/hook.h @@ -3,8 +3,10 @@ #include "strbuf.h" #include "strvec.h" #include "run-command.h" +#include "list.h" struct hook { + struct list_head list; /* The path to the hook */ const char *hook_path; @@ -75,6 +77,7 @@ struct hook_cb_data { /* rc reflects the cumulative failure state */ int rc; const char *hook_name; + struct list_head *head; struct hook *run_me; struct run_hooks_opt *options; int *invoked_hook; @@ -88,7 +91,13 @@ struct hook_cb_data { const char *find_hook(const char *name); /** - * A boolean version of find_hook() + * Provides a linked list of 'struct hook' detailing commands which should run + * in response to the 'hookname' event, in execution order. + */ +struct list_head *list_hooks(const char *hookname); + +/** + * A boolean version of list_hooks() */ int hook_exists(const char *hookname); @@ -99,13 +108,20 @@ void run_hooks_opt_clear(struct run_hooks_opt *o); /** * Takes an already resolved hook found via find_hook() and runs - * it. Does not call run_hooks_opt_clear() for you. + * it. Does not call run_hooks_opt_clear() for you, but does call + * clear_hook_list(). * * See run_hooks_oneshot() for the simpler one-shot API. */ -int run_hooks(const char *hookname, const char *hook_path, +int run_hooks(const char *hookname, struct list_head *hooks, struct run_hooks_opt *options); +/** + * Empties the list at 'head', calling 'free_hook()' on each + * entry. Called implicitly by run_hooks() (and run_hooks_oneshot()). + */ +void clear_hook_list(struct list_head *head); + /** * Calls find_hook() on your "hook_name" and runs the hooks (if any) * with run_hooks(). From patchwork Thu Sep 9 12:42:00 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= X-Patchwork-Id: 12483239 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-15.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED,USER_AGENT_GIT autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id EAE0EC433EF for ; Thu, 9 Sep 2021 14:19:05 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id D211661059 for ; Thu, 9 Sep 2021 14:19:05 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S237019AbhIIOUK (ORCPT ); Thu, 9 Sep 2021 10:20:10 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:41608 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1346601AbhIIOTx (ORCPT ); Thu, 9 Sep 2021 10:19:53 -0400 Received: from mail-wm1-x32a.google.com (mail-wm1-x32a.google.com [IPv6:2a00:1450:4864:20::32a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 6901DC10DC45 for ; Thu, 9 Sep 2021 05:42:13 -0700 (PDT) Received: by mail-wm1-x32a.google.com with SMTP id g74so1250034wmg.5 for ; Thu, 09 Sep 2021 05:42:13 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=wQFAkh6iPGl4+gHDzwwaymLh9mq6wirma5ptMQKbIvY=; b=cvTi6lqadFffL7tAofQNqk/dhirGnppusXGbhFFx8UWJogM8OblBL7PaH57XunDc+Z fCtq7Fql9iuiiM+9uU1CUE32M4s3OQ23sHT06mE6gC2sB11Q8feicNn9sXFSkyd8JTrM 7J5yFG+kOCHkZToLl/rYz9wbkfTi1UZO6B0IXftgnKXVBxpc1OshovHK/OU/h57m0YMx q9OEvbTnHwxBHTzMXg7ZNLslpY7bbKjLm+7zj/WDk1paqTAzsYOyIx9iVh9k8Ej8h3Kc MCTizMJG8Ow0A67Jq6OrUTSpam4ThL0DV+9QCDmbKq2WO+tj88zL08xHXYqPx/e+V5ns 6Jhw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=wQFAkh6iPGl4+gHDzwwaymLh9mq6wirma5ptMQKbIvY=; b=XOGT1fqxm5lF+mxeYWJGe4AoyDO8q2p4JfOeSIKXnupD+XZVSHcXO2qbTH4fK2qJDy wLj0136N4Gj3l8V3IM+heTmJ2UNz82wdM64c6zKcm1arV79hUanaAr4906moQ7Ls2r3a ftodZRPQWM0QHcbU1hje1qXN3+AbxXQsFQ7MRoPExJLkloKUPR+8IY8yXcpOEGlFCf92 LOjLMnrbHBo9PIVz2PBF0VLCg8ANvhySUraCYZUmDgNqB7eB8LGmA3I2CxgGpTUuJcJd MFVEqLhdi49+LcOKUnYACj5hTVMSpPm/TuA2svTkmtxyLJbjVaWIHqGuMJGYXVrM8ndP PJUg== X-Gm-Message-State: AOAM532EhG7bUj/atpJ6Pb9Lk+iq0Sod1XmsvZH4C6kEp4zWySWMWrSK VeO2vNAQK9jUvY3HuoApi0Yq/Bg77ftAaQ== X-Google-Smtp-Source: ABdhPJytgvMzX7zz7vebkfphZ6qqtHH5xyI38q2ApVrZQI7xlQACF2QcJ2jBERpiTaCI81v+HHEKlA== X-Received: by 2002:a7b:cb9a:: with SMTP id m26mr2716491wmi.77.1631191331288; Thu, 09 Sep 2021 05:42:11 -0700 (PDT) Received: from vm.nix.is (vm.nix.is. [2a01:4f8:120:2468::2]) by smtp.gmail.com with ESMTPSA id j25sm1742081wrc.12.2021.09.09.05.42.10 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 09 Sep 2021 05:42:10 -0700 (PDT) From: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= To: git@vger.kernel.org Cc: Emily Shaffer , Junio C Hamano , Jeff King , Taylor Blau , Felipe Contreras , Eric Sunshine , "brian m . carlson" , Josh Steadmon , Jonathan Tan , Derrick Stolee , =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFy?= =?utf-8?b?bWFzb24=?= Subject: [PATCH v4 2/5] hook: allow parallel hook execution Date: Thu, 9 Sep 2021 14:42:00 +0200 Message-Id: X-Mailer: git-send-email 2.33.0.867.g88ec4638586 In-Reply-To: References: <20210819033450.3382652-1-emilyshaffer@google.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org From: Emily Shaffer In many cases, there's no reason not to allow hooks to execute in parallel, if more than one was provided. hook.c already calls run_processes_parallel(), so all we need to do is allow the job count we hand to run_processes_parallel() to be greater than 1. If users have specified no alternative, we can use the processor count from online_cpus() to run an efficient number of tasks at once. However, users can also override this number if they want by configuring 'hook.jobs'. To avoid looking up 'hook.jobs' in cases where we don't end up with any hooks to run anyways, teach the hook runner commands to notice if .jobs==0 and do a config or online_cpus() lookup if so, when we already know we have jobs to run. Serial execution can still be performed by setting .jobs == 1. Signed-off-by: Emily Shaffer Helped-by: Ævar Arnfjörð Bjarmason --- Documentation/config.txt | 2 ++ Documentation/config/hook.txt | 4 ++++ Documentation/git-hook.txt | 17 ++++++++++++++++- builtin/am.c | 4 ++-- builtin/checkout.c | 2 +- builtin/clone.c | 2 +- builtin/hook.c | 4 +++- builtin/merge.c | 2 +- builtin/rebase.c | 2 +- builtin/receive-pack.c | 9 +++++---- builtin/worktree.c | 2 +- commit.c | 2 +- hook.c | 35 ++++++++++++++++++++++++++++++++--- hook.h | 16 +++++++++++++++- read-cache.c | 2 +- refs.c | 2 +- reset.c | 3 ++- sequencer.c | 4 ++-- transport.c | 2 +- 19 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 Documentation/config/hook.txt diff --git a/Documentation/config.txt b/Documentation/config.txt index 0c0e6b859f1..6fb218f649d 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -391,6 +391,8 @@ include::config/guitool.txt[] include::config/help.txt[] +include::config/hook.txt[] + include::config/http.txt[] include::config/i18n.txt[] diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt new file mode 100644 index 00000000000..96d3d6572c1 --- /dev/null +++ b/Documentation/config/hook.txt @@ -0,0 +1,4 @@ +hook.jobs:: + Specifies how many hooks can be run simultaneously during parallelized + hook execution. If unspecified, defaults to the number of processors on + the current system. diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt index fa68c1f3912..79e82479ec6 100644 --- a/Documentation/git-hook.txt +++ b/Documentation/git-hook.txt @@ -8,7 +8,8 @@ git-hook - run git hooks SYNOPSIS -------- [verse] -'git hook' run [--to-stdin=] [--ignore-missing] [-- ] +'git hook' run [--to-stdin=] [--ignore-missing] [(-j|--jobs) ] + [-- ] DESCRIPTION ----------- @@ -42,6 +43,20 @@ OPTIONS tools that want to do a blind one-shot run of a hook that may or may not be present. +-j:: +--jobs:: + Only valid for `run`. ++ +Specify how many hooks to run simultaneously. If this flag is not specified, +uses the value of the `hook.jobs` config, see linkgit:git-config[1]. If the +config is not specified, uses the number of CPUs on the current system. Some +hooks may be ineligible for parallelization: for example, 'commit-msg' intends +hooks modify the commit message body and cannot be parallelized. + +CONFIGURATION +------------- +include::config/hook.txt[] + SEE ALSO -------- linkgit:githooks[5] diff --git a/builtin/am.c b/builtin/am.c index 9e3d4d9ab44..c7ffc7eafc5 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -446,7 +446,7 @@ static void am_destroy(const struct am_state *state) static int run_applypatch_msg_hook(struct am_state *state) { int ret; - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL; assert(state->msg); strvec_push(&opt.args, am_path(state, "final-commit")); @@ -467,7 +467,7 @@ static int run_applypatch_msg_hook(struct am_state *state) */ static int run_post_rewrite_hook(const struct am_state *state) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL; strvec_push(&opt.args, "rebase"); opt.path_to_stdin = am_path(state, "rewritten"); diff --git a/builtin/checkout.c b/builtin/checkout.c index 863b02a7d7c..6b99d31c6ba 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -107,7 +107,7 @@ struct branch_info { static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit, int changed) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL; /* "new_commit" can be NULL when checking out from the index before a commit exists. */ diff --git a/builtin/clone.c b/builtin/clone.c index 27fc05ee511..986c3b1932a 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -776,7 +776,7 @@ static int checkout(int submodule_progress) struct tree *tree; struct tree_desc t; int err = 0; - struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_SERIAL; if (option_no_checkout) return 0; diff --git a/builtin/hook.c b/builtin/hook.c index 398980523aa..9b6272cfd3b 100644 --- a/builtin/hook.c +++ b/builtin/hook.c @@ -22,7 +22,7 @@ static const char * const builtin_hook_run_usage[] = { static int run(int argc, const char **argv, const char *prefix) { int i; - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL; int ignore_missing = 0; const char *hook_name; struct list_head *hooks; @@ -31,6 +31,8 @@ static int run(int argc, const char **argv, const char *prefix) N_("exit quietly with a zero exit code if the requested hook cannot be found")), OPT_STRING(0, "to-stdin", &opt.path_to_stdin, N_("path"), N_("file to read into hooks' stdin")), + OPT_INTEGER('j', "jobs", &opt.jobs, + N_("run up to hooks simultaneously")), OPT_END(), }; int ret; diff --git a/builtin/merge.c b/builtin/merge.c index f215f264cc8..c01eb535d6b 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -448,7 +448,7 @@ static void finish(struct commit *head_commit, const struct object_id *new_head, const char *msg) { struct strbuf reflog_message = STRBUF_INIT; - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL; const struct object_id *head = &head_commit->object.oid; if (!msg) diff --git a/builtin/rebase.c b/builtin/rebase.c index ee68a1df492..a4bbb9abb35 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1314,7 +1314,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) char *squash_onto_name = NULL; int reschedule_failed_exec = -1; int allow_preemptive_ff = 1; - struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL; struct option builtin_rebase_options[] = { OPT_STRING(0, "onto", &options.onto_name, N_("revision"), diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index ebec6f3bb10..7460124b743 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -909,7 +909,7 @@ static int run_receive_hook(struct command *commands, int skip_broken, const struct string_list *push_options) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL; struct receive_hook_feed_context ctx; struct command *iter = commands; @@ -948,7 +948,7 @@ static int run_receive_hook(struct command *commands, static int run_update_hook(struct command *cmd) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL; strvec_pushl(&opt.args, cmd->ref_name, @@ -1432,7 +1432,8 @@ static const char *push_to_checkout(unsigned char *hash, struct strvec *env, const char *work_tree) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL; + opt.invoked_hook = invoked_hook; strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree)); @@ -1628,7 +1629,7 @@ static const char *update(struct command *cmd, struct shallow_info *si) static void run_update_post_hook(struct command *commands) { struct command *cmd; - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL; for (cmd = commands; cmd; cmd = cmd->next) { if (cmd->error_string || cmd->did_not_exist) diff --git a/builtin/worktree.c b/builtin/worktree.c index 330867c19bf..30905067906 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -382,7 +382,7 @@ static int add_worktree(const char *path, const char *refname, * is_junk is cleared, but do return appropriate code when hook fails. */ if (!ret && opts->checkout) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL; strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL); strvec_pushl(&opt.args, diff --git a/commit.c b/commit.c index 842e47beae2..a38bd047524 100644 --- a/commit.c +++ b/commit.c @@ -1700,7 +1700,7 @@ int run_commit_hook(int editor_is_used, const char *index_file, int *invoked_hook, const char *name, ...) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_SERIAL; va_list args; const char *arg; diff --git a/hook.c b/hook.c index 2b2c16a9095..600030c59ec 100644 --- a/hook.c +++ b/hook.c @@ -208,6 +208,28 @@ static int notify_hook_finished(int result, return 0; } +/* + * Determines how many jobs to use after we know we want to parallelize. First + * priority is the config 'hook.jobs' and second priority is the number of CPUs. + */ +static int configured_hook_jobs(void) +{ + /* + * The config and the CPU count probably won't change during the process + * lifetime, so cache the result in case we invoke multiple hooks during + * one process. + */ + static int jobs = 0; + if (jobs) + return jobs; + + if (git_config_get_int("hook.jobs", &jobs)) + /* if the config isn't set, fall back to CPU count. */ + jobs = online_cpus(); + + return jobs; +} + int run_hooks(const char *hook_name, struct list_head *hooks, struct run_hooks_opt *options) { @@ -217,7 +239,6 @@ int run_hooks(const char *hook_name, struct list_head *hooks, .options = options, .invoked_hook = options->invoked_hook, }; - int jobs = 1; if (!options) BUG("a struct run_hooks_opt must be provided to run_hooks"); @@ -225,7 +246,11 @@ int run_hooks(const char *hook_name, struct list_head *hooks, cb_data.head = hooks; cb_data.run_me = list_first_entry(hooks, struct hook, list); - run_processes_parallel_tr2(jobs, + /* INIT_PARALLEL sets jobs to 0, so go look up how many to use. */ + if (!options->jobs) + options->jobs = configured_hook_jobs(); + + run_processes_parallel_tr2(options->jobs, pick_next_hook, notify_start_failure, options->feed_pipe, @@ -244,7 +269,11 @@ int run_hooks_oneshot(const char *hook_name, struct run_hooks_opt *options) { struct list_head *hooks; int ret = 0; - struct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT; + /* + * Turn on parallelism by default. Hooks which don't want it should + * specify their options accordingly. + */ + struct run_hooks_opt hook_opt_scratch = RUN_HOOKS_OPT_INIT_PARALLEL; if (!options) options = &hook_opt_scratch; diff --git a/hook.h b/hook.h index 49b4c335f86..fe16ab35028 100644 --- a/hook.h +++ b/hook.h @@ -25,6 +25,13 @@ struct run_hooks_opt /* Args to be passed to each hook */ struct strvec args; + /* + * Number of threads to parallelize across. Set to 0 to use the + * 'hook.jobs' config or, if that config is unset, the number of cores + * on the system. + */ + int jobs; + /* * Resolve and run the "absolute_path(hook)" instead of * "hook". Used for "git worktree" hooks @@ -68,7 +75,14 @@ struct run_hooks_opt int *invoked_hook; }; -#define RUN_HOOKS_OPT_INIT { \ +#define RUN_HOOKS_OPT_INIT_SERIAL { \ + .jobs = 1, \ + .env = STRVEC_INIT, \ + .args = STRVEC_INIT, \ +} + +#define RUN_HOOKS_OPT_INIT_PARALLEL { \ + .jobs = 0, \ .env = STRVEC_INIT, \ .args = STRVEC_INIT, \ } diff --git a/read-cache.c b/read-cache.c index 875f6c1dea5..98e9fb8b04e 100644 --- a/read-cache.c +++ b/read-cache.c @@ -3069,7 +3069,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l { int ret; int was_full = !istate->sparse_index; - struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL; ret = convert_to_sparse(istate); diff --git a/refs.c b/refs.c index 73d4a939267..5543b8cdaba 100644 --- a/refs.c +++ b/refs.c @@ -2062,7 +2062,7 @@ int ref_update_reject_duplicates(struct string_list *refnames, static int run_transaction_hook(struct ref_transaction *transaction, const char *state) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL; struct string_list to_stdin = STRING_LIST_INIT_NODUP; int ret = 0, i; diff --git a/reset.c b/reset.c index 1237ced8a58..63a9c513409 100644 --- a/reset.c +++ b/reset.c @@ -127,7 +127,8 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action, reflog_head); } if (run_hook) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL; + strvec_pushl(&opt.args, oid_to_hex(orig ? orig : null_oid()), oid_to_hex(oid), diff --git a/sequencer.c b/sequencer.c index db8044ab47d..979cd01c303 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1148,7 +1148,7 @@ int update_head_with_reflog(const struct commit *old_head, static int run_rewrite_hook(const struct object_id *oldoid, const struct object_id *newoid) { - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL; struct strbuf tmp = STRBUF_INIT; struct string_list to_stdin = STRING_LIST_INIT_DUP; int code; @@ -4522,7 +4522,7 @@ static int pick_commits(struct repository *r, if (!stat(rebase_path_rewritten_list(), &st) && st.st_size > 0) { struct child_process notes_cp = CHILD_PROCESS_INIT; - struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_PARALLEL; notes_cp.in = open(rebase_path_rewritten_list(), O_RDONLY); notes_cp.git_cmd = 1; diff --git a/transport.c b/transport.c index 4ca8fc0391d..33da71a108b 100644 --- a/transport.c +++ b/transport.c @@ -1204,7 +1204,7 @@ static int run_pre_push_hook(struct transport *transport, struct ref *remote_refs) { int ret = 0; - struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_PARALLEL; struct ref *r; struct string_list to_stdin = STRING_LIST_INIT_NODUP; From patchwork Thu Sep 9 12:42:01 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= X-Patchwork-Id: 12483241 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-15.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED,USER_AGENT_GIT autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id DD700C433F5 for ; Thu, 9 Sep 2021 14:19:09 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id B97CE61059 for ; Thu, 9 Sep 2021 14:19:09 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1344360AbhIIOUR (ORCPT ); Thu, 9 Sep 2021 10:20:17 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:41836 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1348599AbhIIOTx (ORCPT ); Thu, 9 Sep 2021 10:19:53 -0400 Received: from mail-wm1-x32f.google.com (mail-wm1-x32f.google.com [IPv6:2a00:1450:4864:20::32f]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id CA367C10DC46 for ; Thu, 9 Sep 2021 05:42:13 -0700 (PDT) Received: by mail-wm1-x32f.google.com with SMTP id z9-20020a7bc149000000b002e8861aff59so1402961wmi.0 for ; Thu, 09 Sep 2021 05:42:13 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=L4+ryNm5+CD2dBR5sVF6+8JwaQ9mh+UY5gTpMjTHb8k=; b=Vdw9k6Qc46ua/wr+fWhx1yPtVMv/cXamfT4/x0WaQ3pT3rSaSMtKrnAHtvw1gk3jnC 8EF0n4CzAotQorGEs8/M1TAA0GLbz6at5R2/AE/U+BcoXmeirVcgw582v2ONiQKBgtzR E/tvdLi5Zgleb+6pmAHykQbEDMRCioU8rf3YHCUEGxj0JuKjySxWuDDXst6Cu8E6GmoE +4CYeS8Xm8w9zUQeOShf95zHIU1+rm38hoETTpdFqNBTRQerVenVfG1zyoADYZRMsIRl 0vfZMrU6tYDv8YghzETfGZ2lcz3zYRmUwdHNjNbNKRTZCDodgy/hqioJAI0hXkNyfHBX jr3g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=L4+ryNm5+CD2dBR5sVF6+8JwaQ9mh+UY5gTpMjTHb8k=; b=o4EvBdBoL5b+J2ThphVTogPBi2hDXNOYg5Xmkx2vdO6kdcFTtWpDd4LM4YEvEdbsRZ Jgs5FhNIBXSUn+oRNCvui041fxdoIjbkMKFaQHWjBMv/uhE3Pj4K6YU3kzE6oy+06hjc JPxuwVQ88p0x8/1nwDAy2iK5lU35rINfnE4A8663S1QJeEX7oonTNQuUnDl7q8S6P1RQ ASv+O8uWrA5In/NQ2+N5mdUd6lruzEJ7TNKB5MQjvrPJ1F4r20fn1Te02/h7qF6bwio4 kZk5waS9F2B9rS44VAXbfXUHL0I/PRcWvMhcF5PkDFL+qQvCxE4EcWQ+HSnbpxrJ1Dac ZskA== X-Gm-Message-State: AOAM533Bxh3obDdujWUgV/ukjzl138xsLHFxjpatxPohbYpiVvWsPdh4 r2vhWSQUw/wsu5niCMgJDiUPjymwQVspOw== X-Google-Smtp-Source: ABdhPJz5Bl+rJmoDOrxIafliSRcrskYZtYCJeWIqi/Q51ScnapMCUVFQifiTLGfGhSCKcW/+OaglAQ== X-Received: by 2002:a1c:7c12:: with SMTP id x18mr2832671wmc.114.1631191332050; Thu, 09 Sep 2021 05:42:12 -0700 (PDT) Received: from vm.nix.is (vm.nix.is. [2a01:4f8:120:2468::2]) by smtp.gmail.com with ESMTPSA id j25sm1742081wrc.12.2021.09.09.05.42.11 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 09 Sep 2021 05:42:11 -0700 (PDT) From: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= To: git@vger.kernel.org Cc: Emily Shaffer , Junio C Hamano , Jeff King , Taylor Blau , Felipe Contreras , Eric Sunshine , "brian m . carlson" , Josh Steadmon , Jonathan Tan , Derrick Stolee , =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFy?= =?utf-8?b?bWFzb24=?= Subject: [PATCH v4 3/5] hook: introduce "git hook list" Date: Thu, 9 Sep 2021 14:42:01 +0200 Message-Id: X-Mailer: git-send-email 2.33.0.867.g88ec4638586 In-Reply-To: References: <20210819033450.3382652-1-emilyshaffer@google.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org From: Emily Shaffer If more than one hook will be run, it may be useful to see a list of which hooks should be run. At very least, it will be useful for us to test the semantics of multihooks ourselves. For now, only list the hooks which will run in the order they will run in; later, it might be useful to include more information like where the hooks were configured and whether or not they will run. Signed-off-by: Emily Shaffer Signed-off-by: Ævar Arnfjörð Bjarmason --- Documentation/git-hook.txt | 5 ++++ builtin/hook.c | 52 ++++++++++++++++++++++++++++++++++++++ t/t1800-hook.sh | 14 ++++++++++ 3 files changed, 71 insertions(+) diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt index 79e82479ec6..88588b38143 100644 --- a/Documentation/git-hook.txt +++ b/Documentation/git-hook.txt @@ -10,6 +10,7 @@ SYNOPSIS [verse] 'git hook' run [--to-stdin=] [--ignore-missing] [(-j|--jobs) ] [-- ] +'git hook' list DESCRIPTION ----------- @@ -30,6 +31,10 @@ optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The arguments (if any) differ by hook name, see linkgit:githooks[5] for what those are. +list:: + Print a list of hooks which will be run on `` event. If no + hooks are configured for that event, print nothing and return 1. + OPTIONS ------- diff --git a/builtin/hook.c b/builtin/hook.c index 9b6272cfd3b..1e6b15d565a 100644 --- a/builtin/hook.c +++ b/builtin/hook.c @@ -8,8 +8,11 @@ #define BUILTIN_HOOK_RUN_USAGE \ N_("git hook run [--ignore-missing] [--to-stdin=] [-- ]") +#define BUILTIN_HOOK_LIST_USAGE \ + N_("git hook list ") static const char * const builtin_hook_usage[] = { + BUILTIN_HOOK_LIST_USAGE, BUILTIN_HOOK_RUN_USAGE, NULL }; @@ -19,6 +22,53 @@ static const char * const builtin_hook_run_usage[] = { NULL }; +static const char *const builtin_hook_list_usage[] = { + BUILTIN_HOOK_LIST_USAGE, + NULL +}; + +static int list(int argc, const char **argv, const char *prefix) +{ + struct list_head *hooks; + struct list_head *pos; + const char *hookname = NULL; + struct option list_options[] = { + OPT_END(), + }; + int ret = 0; + + argc = parse_options(argc, argv, prefix, list_options, + builtin_hook_list_usage, 0); + + /* + * The only unnamed argument provided should be the hook-name; if we add + * arguments later they probably should be caught by parse_options. + */ + if (argc != 1) + usage_msg_opt(_("You must specify a hook event name to list."), + builtin_hook_list_usage, list_options); + + hookname = argv[0]; + + hooks = list_hooks(hookname); + + if (list_empty(hooks)) { + ret = 1; + goto cleanup; + } + + list_for_each(pos, hooks) { + struct hook *item = list_entry(pos, struct hook, list); + item = list_entry(pos, struct hook, list); + if (item) + printf("%s\n", item->hook_path); + } + +cleanup: + clear_hook_list(hooks); + + return ret; +} static int run(int argc, const char **argv, const char *prefix) { int i; @@ -94,6 +144,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix) if (!argc) goto usage; + if (!strcmp(argv[0], "list")) + return list(argc, argv, prefix); if (!strcmp(argv[0], "run")) return run(argc, argv, prefix); diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh index 6431b19e392..7a1dae4e95e 100755 --- a/t/t1800-hook.sh +++ b/t/t1800-hook.sh @@ -9,6 +9,8 @@ test_expect_success 'git hook usage' ' test_expect_code 129 git hook run && test_expect_code 129 git hook run -h && test_expect_code 129 git hook run --unknown 2>err && + test_expect_code 129 git hook list && + test_expect_code 129 git hook list -h && grep "unknown option" err ' @@ -97,6 +99,18 @@ test_expect_success 'git hook run -- pass arguments' ' test_cmp expect actual ' +test_expect_success 'git hook list: does-not-exist hook' ' + test_expect_code 1 git hook list does-not-exist +' + +test_expect_success 'git hook list: existing hook' ' + cat >expect <<-\EOF && + .git/hooks/test-hook + EOF + git hook list test-hook >actual && + test_cmp expect actual +' + test_expect_success 'git hook run -- out-of-repo runs excluded' ' write_script .git/hooks/test-hook <<-EOF && echo Test hook From patchwork Thu Sep 9 12:42:02 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= X-Patchwork-Id: 12483245 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-15.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED,USER_AGENT_GIT autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id AF4ABC433FE for ; Thu, 9 Sep 2021 14:19:12 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 9457161131 for ; Thu, 9 Sep 2021 14:19:12 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1346495AbhIIOUU (ORCPT ); Thu, 9 Sep 2021 10:20:20 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:41616 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1348640AbhIIOTx (ORCPT ); Thu, 9 Sep 2021 10:19:53 -0400 Received: from mail-wr1-x432.google.com (mail-wr1-x432.google.com [IPv6:2a00:1450:4864:20::432]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 3E564C10DC47 for ; Thu, 9 Sep 2021 05:42:15 -0700 (PDT) Received: by mail-wr1-x432.google.com with SMTP id x6so2340298wrv.13 for ; Thu, 09 Sep 2021 05:42:15 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=T8JCehz2ndrXmIMuuT2TnO4zX08B5Kp8U9kXu9laL3A=; b=UUUemHJNhAOpAlW9UFJWg/U91zIxOd+LgysuzzU3TojXeO2/ZuVN3o1Y9qjRCUvqMy ffLRcNfrD5r+Fn9LQn5eWRQMSQF5RVjdzzkWiLN70S+SpHCU3/YbTUy3GCvf45bf319Z 6jueuSL58+jMrAVFjYG98kqDiqAdcHxMqDVpXMA7AXHr9YvZA98EltJl4VFNDV7WZhGa qGb1FGOqC8CDnu4PWqre3iiO6JTxJGRKzCACjDqcj5dFInZhf5AQ34ROwHkpyOZhZcOD 1YZvqVqZf0hUV27cVP/aagA8sdl/1Q+J7Q8a3PTuAyyOBP2LZps2vK/ee6LgymPcw3BJ wQKA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=T8JCehz2ndrXmIMuuT2TnO4zX08B5Kp8U9kXu9laL3A=; b=WhamSjj66yoVuaRAkjqO9zmlmwIxNMdtrsqkZz+AswM9fbkGmCjU55iChMt51O1uKl KqKhSkqvkzRkU90KqtSc3BKmD8JPt1+iAAtfP09RhpIikFDtc8BQxHooKJ6orfK3JAN9 KYT5bAZ2S+vbrXT4452cWekG9IxstN3N3ZKhTyhd0ND8ywQVKbZARuvYOMBMpMytbl2v mbKvmc90zqbnLq6Hd/68bdWs62P7KRNH1EbF4Fl7dz/OiTQsmxQKmpRcQ7puwrun9zgk 2cfrhsEr08OvuHZcdAdDQ6OEP+KBWysS5x/6fLJxIRUCsg3EE32z04RHb1Q4YycQU7Au /fKA== X-Gm-Message-State: AOAM531lfD2zEpPljvFIt+XRCgIl/2Ki/fjdGYeIPPttQIxssbNOjofm SAFlvq4upKhHVqcpGh3m5SVspZpyqbnSnw== X-Google-Smtp-Source: ABdhPJz1WCjLVLuoRaWujvFMkRgQfS8PKWheLxDOPRPaG8BJ4zxt6IxEgDl6ZFc29PGMy7v5rd9KhQ== X-Received: by 2002:a5d:4b4b:: with SMTP id w11mr216322wrs.302.1631191333127; Thu, 09 Sep 2021 05:42:13 -0700 (PDT) Received: from vm.nix.is (vm.nix.is. [2a01:4f8:120:2468::2]) by smtp.gmail.com with ESMTPSA id j25sm1742081wrc.12.2021.09.09.05.42.12 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 09 Sep 2021 05:42:12 -0700 (PDT) From: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= To: git@vger.kernel.org Cc: Emily Shaffer , Junio C Hamano , Jeff King , Taylor Blau , Felipe Contreras , Eric Sunshine , "brian m . carlson" , Josh Steadmon , Jonathan Tan , Derrick Stolee Subject: [PATCH v4 4/5] hook: include hooks from the config Date: Thu, 9 Sep 2021 14:42:02 +0200 Message-Id: X-Mailer: git-send-email 2.33.0.867.g88ec4638586 In-Reply-To: References: <20210819033450.3382652-1-emilyshaffer@google.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org From: Emily Shaffer 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 multiple "hook..command = " and "hook..event = " lines. Hooks will be started in config order of the "hook..event" lines (but may run in parallel). For example: $ 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 --- Documentation/config/hook.txt | 18 ++++ Documentation/git-hook.txt | 135 +++++++++++++++++++++++++++- builtin/hook.c | 3 +- hook.c | 161 +++++++++++++++++++++++++++++---- hook.h | 7 +- t/t1800-hook.sh | 164 +++++++++++++++++++++++++++++++++- 6 files changed, 465 insertions(+), 23 deletions(-) diff --git a/Documentation/config/hook.txt b/Documentation/config/hook.txt index 96d3d6572c1..c3947563280 100644 --- a/Documentation/config/hook.txt +++ b/Documentation/config/hook.txt @@ -1,3 +1,21 @@ +hook..command:: + A command to execute whenever `hook.` is invoked. `` should + be a unique "friendly" name which you can use to identify this hook + command. (You can specify when to invoke this command with + `hook..event`.) The value can be an executable on your device or a + oneliner for your shell. If more than one value is specified for the + same ``, the last value parsed will be the only command executed. + See linkgit:git-hook[1]. + +hook..event:: + The hook events which should invoke `hook.`. `` should be a + unique "friendly" name which you can use to identify this hook. The + value should be the name of a hook event, like "pre-commit" or "update". + (See linkgit:githooks[5] for a complete list of hooks Git knows about.) + On the specified event, the associated `hook..command` will be + executed. More than one event can be specified if you wish for + `hook.` to execute on multiple events. See linkgit:git-hook[1]. + hook.jobs:: Specifies how many hooks can be run simultaneously during parallelized hook execution. If unspecified, defaults to the number of processors on diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt index 88588b38143..51bda42fb83 100644 --- a/Documentation/git-hook.txt +++ b/Documentation/git-hook.txt @@ -19,12 +19,102 @@ This command is an interface to git hooks (see linkgit:githooks[5]). Currently it only provides a convenience wrapper for running hooks for use by git itself. In the future it might gain other functionality. +It's possible to use this command to refer to hooks which are not native to Git, +for example if a wrapper around Git wishes to expose hooks into its own +operation in a way which is already familiar to Git users. However, wrappers +invoking such hooks should be careful to name their hook events something which +Git is unlikely to use for a native hook later on. For example, Git is much less +likely to create a `mytool-validate-commit` hook than it is to create a +`validate-commit` hook. + +This command parses the default configuration files for sets of configs like +so: + + [hook "linter"] + event = pre-commit + command = ~/bin/linter --cpp20 + +In this example, `[hook "linter"]` represents one script - `~/bin/linter +--cpp20` - which can be shared by many repos, and even by many hook events, if +appropriate. + +To add an unrelated hook which runs on a different event, for example a +spell-checker for your commit messages, you would write a configuration like so: + + [hook "linter"] + event = pre-commit + command = ~/bin/linter --cpp20 + [hook "spellcheck"] + event = commit-msg + command = ~/bin/spellchecker + +With this config, when you run 'git commit', first `~/bin/linter --cpp20` will +have a chance to check your files to be committed (during the `pre-commit` hook +event`), and then `~/bin/spellchecker` will have a chance to check your commit +message (during the `commit-msg` hook event). + +Commands are run in the order Git encounters their associated +`hook..event` configs during the configuration parse (see +linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be +added, only one `hook.linter.command` event is valid - Git uses "last-one-wins" +to determine which command to run. + +So if you wanted your linter to run when you commit as well as when you push, +you would configure it like so: + + [hook "linter"] + event = pre-commit + event = pre-push + command = ~/bin/linter --cpp20 + +With this config, `~/bin/linter --cpp20` would be run by Git before a commit is +generated (during `pre-commit`) as well as before a push is performed (during +`pre-push`). + +And if you wanted to run your linter as well as a secret-leak detector during +only the "pre-commit" hook event, you would configure it instead like so: + + [hook "linter"] + event = pre-commit + command = ~/bin/linter --cpp20 + [hook "no-leaks"] + event = pre-commit + command = ~/bin/leak-detector + +With this config, before a commit is generated (during `pre-commit`), Git would +first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It +would evaluate the output of each when deciding whether to proceed with the +commit. + +For a full list of hook events which you can set your `hook..event` to, +and how hooks are invoked during those events, see linkgit:githooks[5]. + +Git will ignore any `hook..event` that specifies an event it doesn't +recognize. This is intended so that tools which wrap Git can use the hook +infrastructure to run their own hooks; see <> for more guidance. + +In general, when instructions suggest adding a script to +`.git/hooks/`, you can specify it in the config instead by running: + +---- +git config hook..command +git config --add hook..event +---- + +This way you can share the script between multiple repos. That is, `cp +~/my-script.sh ~/project/.git/hooks/pre-commit` would become: + +---- +git config hook.my-script.command ~/my-script.sh +git config --add hook.my-script.event pre-commit +---- + SUBCOMMANDS ----------- run:: - Run the `` hook. See linkgit:githooks[5] for - the hook names we support. + Runs hooks configured for ``, in the order they are + discovered during the config parse. + Any positional arguments to the hook should be passed after an optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The @@ -58,6 +148,47 @@ config is not specified, uses the number of CPUs on the current system. Some hooks may be ineligible for parallelization: for example, 'commit-msg' intends hooks modify the commit message body and cannot be parallelized. +[[WRAPPERS]] +WRAPPERS +-------- + +`git hook run` has been designed to make it easy for tools which wrap Git to +configure and execute hooks using the Git hook infrastructure. It is possible to +provide arguments, environment variables (TODO this is missing from reroll TODO), +and stdin via the command line, as well as specifying parallel or series +execution if the user has provided multiple hooks. + +Assuming your wrapper wants to support a hook named "mywrapper-start-tests", you +can have your users specify their hooks like so: + + [hook "setup-test-dashboard"] + event = mywrapper-start-tests + command = ~/mywrapper/setup-dashboard.py --tap + +Then, in your 'mywrapper' tool, you can invoke any users' configured hooks by +running: + +---- +git hook run mywrapper-start-tests \ + # providing something to stdin + --stdin some-tempfile-123 \ + # setting an env var (TODO THIS IS MISSING TODO) + --env MYWRAPPER_EXECUTION_MODE=foo \ + # execute hooks in serial + --jobs 1 \ + # plus some arguments of your own... + -- \ + --testname bar \ + baz +---- + +Take care to name your wrapper's hook events in a way which is unlikely to +overlap with Git's native hooks (see linkgit:githooks[5]) - a hook event named +`mywrappertool-validate-commit` is much less likely to be added to native Git +than a hook event named `validate-commit`. If Git begins to use a hook event +named the same thing as your wrapper hook, it may invoke your users' hooks in +unintended and unsupported ways. + CONFIGURATION ------------- include::config/hook.txt[] diff --git a/builtin/hook.c b/builtin/hook.c index 1e6b15d565a..bb8fdde6bad 100644 --- a/builtin/hook.c +++ b/builtin/hook.c @@ -61,7 +61,8 @@ static int list(int argc, const char **argv, const char *prefix) struct hook *item = list_entry(pos, struct hook, list); item = list_entry(pos, struct hook, list); if (item) - printf("%s\n", item->hook_path); + printf("%s\n", item->name ? item->name + : _("hook from hookdir")); } cleanup: diff --git a/hook.c b/hook.c index 600030c59ec..b825fa7c7ae 100644 --- a/hook.c +++ b/hook.c @@ -8,10 +8,54 @@ static void free_hook(struct hook *ptr) if (!ptr) return; + free(ptr->name); free(ptr->feed_pipe_cb_data); free(ptr); } +/* + * Walks the linked list at 'head' to check if any hook named 'name' + * already exists. Returns a pointer to that hook if so, otherwise returns NULL. + */ +static struct hook *find_hook_by_name(struct list_head *head, + const char *name) +{ + struct list_head *pos = NULL, *tmp = NULL; + struct hook *found = NULL; + + list_for_each_safe(pos, tmp, head) { + struct hook *it = list_entry(pos, struct hook, list); + if (!strcmp(it->name, name)) { + list_del(pos); + found = it; + break; + } + } + return found; +} + +/* + * Adds a hook if it's not already in the list, or moves it to the tail of the + * list if it was already there. name == NULL indicates it's from the hookdir; + * just append it in this case. + */ +static void append_or_move_hook(struct list_head *head, const char *name) +{ + struct hook *to_add = NULL; + + /* if it's not from hookdir, check if the hook is already in the list */ + if (name) + to_add = find_hook_by_name(head, name); + + if (!to_add) { + /* adding a new hook, not moving an old one */ + to_add = xcalloc(1, sizeof(*to_add)); + to_add->name = xstrdup_or_null(name); + } + + list_add_tail(&to_add->list, head); +} + static void remove_hook(struct list_head *to_remove) { struct hook *hook_to_remove = list_entry(to_remove, struct hook, list); @@ -73,26 +117,72 @@ int hook_exists(const char *name) return exists; } +struct hook_config_cb +{ + const char *hook_event; + struct list_head *list; +}; + +/* + * Callback for git_config which adds configured hooks to a hook list. Hooks + * can be configured by specifying both hook..command = and + * hook..event = . + */ +static int hook_config_lookup(const char *key, const char *value, void *cb_data) +{ + struct hook_config_cb *data = cb_data; + const char *subsection, *parsed_key; + size_t subsection_len = 0; + struct strbuf subsection_cpy = STRBUF_INIT; + + /* + * Don't bother doing the expensive parse if there's no + * chance that the config matches 'hook.myhook.event = hook_event'. + */ + if (!value || strcmp(value, data->hook_event)) + return 0; + + /* Looking for "hook.friendlyname.event = hook_event" */ + if (parse_config_key(key, + "hook", + &subsection, + &subsection_len, + &parsed_key) || + strcmp(parsed_key, "event")) + return 0; + + /* + * 'subsection' is a pointer to the internals of 'key', which we don't + * own the memory for. Copy it away to the hook list. + */ + strbuf_add(&subsection_cpy, subsection, subsection_len); + + append_or_move_hook(data->list, subsection_cpy.buf); + strbuf_release(&subsection_cpy); + + return 0; +} + struct list_head *list_hooks(const char *hookname) { struct list_head *hook_head = xmalloc(sizeof(struct list_head)); + struct hook_config_cb cb_data = { + .hook_event = hookname, + .list = hook_head, + }; INIT_LIST_HEAD(hook_head); if (!hookname) BUG("null hookname was provided to hook_list()!"); - if (have_git_dir()) { - const char *hook_path = find_hook(hookname); + /* Add the hooks from the config, e.g. hook.myhook.event = pre-commit */ + git_config(hook_config_lookup, &cb_data); - /* Add the hook from the hookdir */ - if (hook_path) { - struct hook *to_add = xmalloc(sizeof(*to_add)); - to_add->hook_path = hook_path; - to_add->feed_pipe_cb_data = NULL; - list_add_tail(&to_add->list, hook_head); - } - } + /* Add the hook from the hookdir. The placeholder makes it easier to + * allocate work in pick_next_hook. */ + if (find_hook(hookname)) + append_or_move_hook(hook_head, NULL); return hook_head; } @@ -153,11 +243,47 @@ static int pick_next_hook(struct child_process *cp, cp->trace2_hook_name = hook_cb->hook_name; cp->dir = hook_cb->options->dir; + /* + * to enable oneliners, let config-specified hooks run in shell. + * config-specified hooks have a name. + */ + cp->use_shell = !!run_me->name; + /* add command */ - if (hook_cb->options->absolute_path) - strvec_push(&cp->args, absolute_path(run_me->hook_path)); - else - strvec_push(&cp->args, run_me->hook_path); + if (run_me->name) { + /* ...from config */ + struct strbuf cmd_key = STRBUF_INIT; + char *command = NULL; + + strbuf_addf(&cmd_key, "hook.%s.command", run_me->name); + if (git_config_get_string(cmd_key.buf, &command)) { + /* TODO test me! */ + die(_("'hook.%s.command' must be configured " + "or 'hook.%s.event' must be removed; aborting.\n"), + run_me->name, run_me->name); + } + + strvec_push(&cp->args, command); + free(command); + strbuf_release(&cmd_key); + } else { + /* ...from hookdir. */ + const char *hook_path = NULL; + /* + * At this point we are already running, so don't validate + * whether the hook name is known or not. Validation was + * performed earlier in list_hooks(). + */ + hook_path = find_hook(hook_cb->hook_name); + if (!hook_path) + BUG("hookdir hook in hook list but no hookdir hook present in filesystem"); + + if (hook_cb->options->absolute_path) + hook_path = absolute_path(hook_path); + + strvec_push(&cp->args, hook_path); + } + /* * add passed-in argv, without expanding - let the user get back @@ -187,8 +313,11 @@ static int notify_start_failure(struct strbuf *out, hook_cb->rc |= 1; - strbuf_addf(out, _("Couldn't start hook '%s'\n"), - attempted->hook_path); + if (attempted->name) + strbuf_addf(out, _("Couldn't start hook '%s'\n"), + attempted->name); + else + strbuf_addstr(out, _("Couldn't start hook from hooks directory\n")); return 1; } diff --git a/hook.h b/hook.h index fe16ab35028..4b728991089 100644 --- a/hook.h +++ b/hook.h @@ -7,8 +7,11 @@ struct hook { struct list_head list; - /* The path to the hook */ - const char *hook_path; + /* + * The friendly name of the hook. NULL indicates the hook is from the + * hookdir. + */ + char *name; /* * Use this to keep state for your feed_pipe_fn if you are using diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh index 7a1dae4e95e..68e7ff7de7e 100755 --- a/t/t1800-hook.sh +++ b/t/t1800-hook.sh @@ -1,13 +1,30 @@ #!/bin/sh -test_description='git-hook command' +test_description='git-hook command and config-managed multihooks' . ./test-lib.sh +setup_hooks () { + test_config hook.ghi.command "/path/ghi" + test_config hook.ghi.event pre-commit --add + test_config hook.ghi.event test-hook --add + test_config_global hook.def.command "/path/def" + test_config_global hook.def.event pre-commit --add +} + +setup_hookdir () { + mkdir .git/hooks + write_script .git/hooks/pre-commit <<-EOF + echo \"Legacy Hook\" + EOF + test_when_finished rm -rf .git/hooks +} + test_expect_success 'git hook usage' ' test_expect_code 129 git hook && test_expect_code 129 git hook run && test_expect_code 129 git hook run -h && + test_expect_code 129 git hook list -h && test_expect_code 129 git hook run --unknown 2>err && test_expect_code 129 git hook list && test_expect_code 129 git hook list -h && @@ -105,7 +122,7 @@ test_expect_success 'git hook list: does-not-exist hook' ' test_expect_success 'git hook list: existing hook' ' cat >expect <<-\EOF && - .git/hooks/test-hook + hook from hookdir EOF git hook list test-hook >actual && test_cmp expect actual @@ -162,4 +179,147 @@ test_expect_success 'stdin to hooks' ' test_cmp expect actual ' +test_expect_success 'git hook list orders by config order' ' + setup_hooks && + + cat >expected <<-\EOF && + def + ghi + EOF + + git hook list pre-commit >actual && + test_cmp expected actual +' + +test_expect_success 'git hook list reorders on duplicate event declarations' ' + setup_hooks && + + # 'def' is usually configured globally; move it to the end by + # configuring it locally. + test_config hook.def.event "pre-commit" --add && + + cat >expected <<-\EOF && + ghi + def + EOF + + git hook list pre-commit >actual && + test_cmp expected actual +' + +test_expect_success 'hook can be configured for multiple events' ' + setup_hooks && + + # 'ghi' should be included in both 'pre-commit' and 'test-hook' + git hook list pre-commit >actual && + grep "ghi" actual && + git hook list test-hook >actual && + grep "ghi" actual +' + +test_expect_success 'git hook list shows hooks from the hookdir' ' + setup_hookdir && + + cat >expected <<-\EOF && + hook from hookdir + EOF + + git hook list pre-commit >actual && + test_cmp expected actual +' + +test_expect_success 'inline hook definitions execute oneliners' ' + test_config hook.oneliner.event "pre-commit" && + test_config hook.oneliner.command "echo \"Hello World\"" && + + echo "Hello World" >expected && + + # hooks are run with stdout_to_stderr = 1 + git hook run pre-commit 2>actual && + test_cmp expected actual +' + +test_expect_success 'inline hook definitions resolve paths' ' + write_script sample-hook.sh <<-\EOF && + echo \"Sample Hook\" + EOF + + test_when_finished "rm sample-hook.sh" && + + test_config hook.sample-hook.event pre-commit && + test_config hook.sample-hook.command "\"$(pwd)/sample-hook.sh\"" && + + echo \"Sample Hook\" >expected && + + # hooks are run with stdout_to_stderr = 1 + git hook run pre-commit 2>actual && + test_cmp expected actual +' + +test_expect_success 'hookdir hook included in git hook run' ' + setup_hookdir && + + echo \"Legacy Hook\" >expected && + + # hooks are run with stdout_to_stderr = 1 + git hook run pre-commit 2>actual && + test_cmp expected actual +' + +test_expect_success 'stdin to multiple hooks' ' + test_config hook.stdin-a.event "test-hook" --add && + test_config hook.stdin-a.command "xargs -P1 -I% echo a%" --add && + test_config hook.stdin-b.event "test-hook" --add && + test_config hook.stdin-b.command "xargs -P1 -I% echo b%" --add && + + cat >input <<-\EOF && + 1 + 2 + 3 + EOF + + cat >expected <<-\EOF && + a1 + a2 + a3 + b1 + b2 + b3 + EOF + + git hook run --to-stdin=input test-hook 2>actual && + test_cmp expected actual +' + +test_expect_success 'multiple hooks in series' ' + test_config hook.series-1.event "test-hook" && + test_config hook.series-1.command "echo 1" --add && + test_config hook.series-2.event "test-hook" && + test_config hook.series-2.command "echo 2" --add && + mkdir .git/hooks && + write_script .git/hooks/test-hook <<-EOF && + echo 3 + EOF + + cat >expected <<-\EOF && + 1 + 2 + 3 + EOF + + git hook run -j1 test-hook 2>actual && + test_cmp expected actual && + + rm -rf .git/hooks +' + +test_expect_success 'rejects hooks with no commands configured' ' + test_config hook.broken.event "test-hook" && + + echo broken >expected && + git hook list test-hook >actual && + test_cmp expected actual && + test_must_fail git hook run test-hook +' + test_done From patchwork Thu Sep 9 12:42:03 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= X-Patchwork-Id: 12483243 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-15.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED,USER_AGENT_GIT autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 861DBC433EF for ; Thu, 9 Sep 2021 14:19:11 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 689DA61131 for ; Thu, 9 Sep 2021 14:19:11 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S244271AbhIIOUT (ORCPT ); Thu, 9 Sep 2021 10:20:19 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:41870 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1348612AbhIIOTx (ORCPT ); Thu, 9 Sep 2021 10:19:53 -0400 Received: from mail-wr1-x431.google.com (mail-wr1-x431.google.com [IPv6:2a00:1450:4864:20::431]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 853F1C10DC48 for ; Thu, 9 Sep 2021 05:42:15 -0700 (PDT) Received: by mail-wr1-x431.google.com with SMTP id v10so2380816wrd.4 for ; Thu, 09 Sep 2021 05:42:15 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=cgYfONj+HBmQH587+jI3XQjsFeFBrsmXwTixygeOUVg=; b=fBcNeZj6JGngpNji52r7FVPD4YyS2zTBQE1aVq6LBzlEenwzcrW1q7oJTNN+khw/49 Ct0ODDIko2MmYqG7U/mYBp1VNTH/xziWZFl9HLffG3rCEunNsossk94A2Hfzmxd6Xq2/ k7IpQWjILgv9471E+E8320rQ9HAujX2tIraN3axkniZhTgR/2HlLKOKcbpH2+SwwltIP DB8fg38b2g0j9IEFgkMkOSNtlwW/bneRrKg2dKx47QgCqgZNbZan8EGXDaoBqzWqz/HC C/ZRoDU2PFksCJH18m1WptnqMSWBHB6Z6DJvYrO9fVc0YYG91qxu7yDxy43w33b6M2Lj pwag== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=cgYfONj+HBmQH587+jI3XQjsFeFBrsmXwTixygeOUVg=; b=v6BRWguJ8cRWMVPXdRaAin0eQ6uC+QlcwLt/SNmFvCI4YPciR+rp4XmyTQgV/L/tQI aNVN2txQ12Aj0njQd4ezZhbp4XK93WgyF2P5DaxIDOr60j+fpED4GAJXAaPqs+/IRu4l SPM5UMXApz4UVa55HHA1mPoo69+l37yMQcEvm8ft7BwkZrr85lGantiRarezNXAsZZiV KRnaM+G+4ddvyKGdZc16DlwAyhkK30xCmP/fRta++hljHLX4MJeTjp+cO4QsPYkNZ9kq dlu3nS74oDqbxZXpyH8nkfGKm+0Ss3N8T1N31sBoNUp+21aCCv6JPOoAxotPa3QsYK5X QMKw== X-Gm-Message-State: AOAM532G8G+o6v6ssIXEqsfTVkyFaCpmvu+hBgaMz4ctiS6zV01DOG79 kR9GbeHuIO9Knc4XWk8e1PW7toy1GrvAwA== X-Google-Smtp-Source: ABdhPJxO0zflogSeMRYYnfmNwzx3ZqisrCfxd4GirB23jRul/vBTzVUDnOEVgFSvtBNS/M6aC3tehg== X-Received: by 2002:a5d:58e9:: with SMTP id f9mr3268646wrd.154.1631191333876; Thu, 09 Sep 2021 05:42:13 -0700 (PDT) Received: from vm.nix.is (vm.nix.is. [2a01:4f8:120:2468::2]) by smtp.gmail.com with ESMTPSA id j25sm1742081wrc.12.2021.09.09.05.42.13 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 09 Sep 2021 05:42:13 -0700 (PDT) From: =?utf-8?b?w4Z2YXIgQXJuZmrDtnLDsCBCamFybWFzb24=?= To: git@vger.kernel.org Cc: Emily Shaffer , Junio C Hamano , Jeff King , Taylor Blau , Felipe Contreras , Eric Sunshine , "brian m . carlson" , Josh Steadmon , Jonathan Tan , Derrick Stolee Subject: [PATCH v4 5/5] hook: allow out-of-repo 'git hook' invocations Date: Thu, 9 Sep 2021 14:42:03 +0200 Message-Id: X-Mailer: git-send-email 2.33.0.867.g88ec4638586 In-Reply-To: References: <20210819033450.3382652-1-emilyshaffer@google.com> MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org From: Emily Shaffer Since hooks can now be supplied via the config, and a config can be present without a gitdir via the global and system configs, we can start to allow 'git hook run' to occur without a gitdir. This enables us to do things like run sendemail-validate hooks when running 'git send-email' from a nongit directory. It still doesn't make sense to look for hooks in the hookdir in nongit repos, though, as there is no hookdir. Signed-off-by: Emily Shaffer --- git.c | 2 +- hook.c | 2 +- t/t1800-hook.sh | 20 +++++++++++++++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/git.c b/git.c index 540909c391f..39988ee3b02 100644 --- a/git.c +++ b/git.c @@ -538,7 +538,7 @@ static struct cmd_struct commands[] = { { "grep", cmd_grep, RUN_SETUP_GENTLY }, { "hash-object", cmd_hash_object }, { "help", cmd_help }, - { "hook", cmd_hook, RUN_SETUP }, + { "hook", cmd_hook, RUN_SETUP_GENTLY }, { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT }, { "init", cmd_init_db }, { "init-db", cmd_init_db }, diff --git a/hook.c b/hook.c index b825fa7c7ae..dc3033cb4c7 100644 --- a/hook.c +++ b/hook.c @@ -181,7 +181,7 @@ struct list_head *list_hooks(const char *hookname) /* Add the hook from the hookdir. The placeholder makes it easier to * allocate work in pick_next_hook. */ - if (find_hook(hookname)) + if (have_git_dir() && find_hook(hookname)) append_or_move_hook(hook_head, NULL); return hook_head; diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh index 68e7ff7de7e..6b6ba30e88e 100755 --- a/t/t1800-hook.sh +++ b/t/t1800-hook.sh @@ -128,15 +128,25 @@ test_expect_success 'git hook list: existing hook' ' test_cmp expect actual ' -test_expect_success 'git hook run -- out-of-repo runs excluded' ' - write_script .git/hooks/test-hook <<-EOF && - echo Test hook - EOF +test_expect_success 'git hook run: out-of-repo runs execute global hooks' ' + test_config_global hook.global-hook.event test-hook --add && + test_config_global hook.global-hook.command "echo no repo no problems" --add && + + echo "global-hook" >expect && + nongit git hook list test-hook >actual && + test_cmp expect actual && + + echo "no repo no problems" >expect && - nongit test_must_fail git hook run test-hook + nongit git hook run test-hook 2>actual && + test_cmp expect actual ' test_expect_success 'git -c core.hooksPath= hook run' ' + write_script .git/hooks/test-hook <<-EOF && + echo Test hook + EOF + mkdir my-hooks && write_script my-hooks/test-hook <<-\EOF && echo Hook ran $1 >>actual