From patchwork Thu May 21 18:54:11 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11563749 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id B241F138A for ; Thu, 21 May 2020 18:54:30 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 8F04920DD4 for ; Thu, 21 May 2020 18:54:30 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="AKdJT7gO" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730024AbgEUSy3 (ORCPT ); Thu, 21 May 2020 14:54:29 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40084 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1729726AbgEUSy0 (ORCPT ); Thu, 21 May 2020 14:54:26 -0400 Received: from mail-qv1-xf49.google.com (mail-qv1-xf49.google.com [IPv6:2607:f8b0:4864:20::f49]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 58E96C061A0E for ; Thu, 21 May 2020 11:54:25 -0700 (PDT) Received: by mail-qv1-xf49.google.com with SMTP id m9so8035132qvl.18 for ; Thu, 21 May 2020 11:54:25 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20161025; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=5IN9jRVdTYi702FnvwbU5V6LCjl26rOkGBkqDTIyIls=; b=AKdJT7gOJqbKaLWO53Am/r7mSSafWfy4mB+8LIOcjDbnYaI04c8Ry6W3hGI/VdM4B3 aVYbO+cY2JO9WbQrsBeYv45laKIbXirskEYDgF8E5er9XXkt9VWAH3P4ribAzicDLgt3 jmqrVsINLzoa96sneOJ+24nD+as4XzQJbd2oLk8BMIlzsG3HuFapIGRhX6tEB67MezF7 7YAQVNIJXfqDgXWlHCEm4hUnwrsbjGExH8tlTAp81fp8fT2FZ7BBbFFAytSjCZOxdF/v ztj4AWo8ckmyjQHdhE1vFz2TgbOG9aXilJd2/+h3qmGAYTWHnZ2LPko6lZp3T9iB+OnG 1IkA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=5IN9jRVdTYi702FnvwbU5V6LCjl26rOkGBkqDTIyIls=; b=D6ALteN5Vwt5Q689zV+b1oem8CgvzqqIWDwzIvBjyqZt+fYETAYZzFx+hufxirkCfM QlYpCTDxoAZ6AczA1TqEWt0yHnNZNS1dPels65rsHLforQHj3pYNQuyahZAa28EzdYD8 U/NSORj+1zBLKCjzl0TK1oWI0DDJqEmzH8nDRcRAaHFAAMBCB3wMGuUuYaGqcownf+es 7b8ixnWMHNf5flKmzdyepL1wKyXuovxHnezeyzm/sl/oC73lMEbcFuC5/rXsxD57F7Cz 5EyZ423a+Q6Pn2bQLo1AgrB1CatBMUEBdQkG0O5ymj804J40JrC+M8WqL5Dj0/nGe8iM LXRw== X-Gm-Message-State: AOAM531wDeJPo8FGkdt9O8fPJ5ujSNgLDGRq89IBvd+sW5b6ym4Pjclk 6cm9abpJXSAhrealpZccVYaGsFoeNYw+jHJzyuejIOOQrqXmIWfG747VmheoflkV8k4aAWuhnFk tR1mEwGgkEeUbGQP/KkTIIRID8Op2NmYaGdnXaEH2ZtPn1zVTojJcdkvpk1Ks+8rDY72fdI87uw == X-Google-Smtp-Source: ABdhPJxb4QWoEeBGMDqLOx82p6FiU7fF1yiJS9PMxAwhjO2kiotdgOAylNKHlPSBt/o7Nv+Hxt6twv78KronsTkAEFo= X-Received: by 2002:ad4:5692:: with SMTP id bc18mr107667qvb.233.1590087264387; Thu, 21 May 2020 11:54:24 -0700 (PDT) Date: Thu, 21 May 2020 11:54:11 -0700 In-Reply-To: <20200521185414.43760-1-emilyshaffer@google.com> Message-Id: <20200521185414.43760-2-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200521185414.43760-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.27.0.rc0.183.gde8f92d652-goog Subject: [PATCH v2 1/4] doc: propose hooks managed by the config From: Emily Shaffer To: git@vger.kernel.org Cc: Emily Shaffer Sender: git-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org Begin a design document for config-based hooks, managed via git-hook. Focus on an overview of the implementation and motivation for design decisions. Briefly discuss the alternatives considered before this point. Also, attempt to redefine terms to fit into a multihook world. Signed-off-by: Emily Shaffer --- Documentation/Makefile | 1 + .../technical/config-based-hooks.txt | 320 ++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 Documentation/technical/config-based-hooks.txt diff --git a/Documentation/Makefile b/Documentation/Makefile index 15d9d04f31..5b21f31d31 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -80,6 +80,7 @@ SP_ARTICLES += $(API_DOCS) TECH_DOCS += MyFirstContribution TECH_DOCS += MyFirstObjectWalk TECH_DOCS += SubmittingPatches +TECH_DOCS += technical/config-based-hooks TECH_DOCS += technical/hash-function-transition TECH_DOCS += technical/http-protocol TECH_DOCS += technical/index-format diff --git a/Documentation/technical/config-based-hooks.txt b/Documentation/technical/config-based-hooks.txt new file mode 100644 index 0000000000..59cdc25a47 --- /dev/null +++ b/Documentation/technical/config-based-hooks.txt @@ -0,0 +1,320 @@ +Configuration-based hook management +=================================== + +== Motivation + +Treat hooks as a first-class citizen by replacing the .git/hook/hookname path as +the only source of hooks to execute, in a way which is friendly to users with +multiple repos which have similar needs. + +Redefine "hook" as an event rather than a single script, allowing users to +perform unrelated actions on a single event. + +Take a step closer to safety when copying zipped Git repositories from untrusted +users. + +Make it easier for users to discover Git's hook feature and automate their +workflows. + +== User interfaces + +=== Config schema + +Hooks can be introduced by editing the configuration manually. There are two new +sections added, `hook` and `hookcmd`. + +==== `hook` + +Primarily contains subsections for each hook event. These subsections define +hook command execution order; hook commands can be specified by passing the +command directly if no additional configuration is needed, or by passing the +name of a `hookcmd`. If Git does not find a `hookcmd` whose subsection matches +the value of the given command string, Git will try to execute the string +directly. Hooks are executed by passing the resolved command string to the +shell. Hook event subsections can also contain per-hook-event settings. + +Also contains top-level hook execution settings, for example, +`hook.warnHookDir`, `hook.runHookDir`, or `hook.disableAll`. + +---- +[hook "pre-commit"] + command = perl-linter + command = /usr/bin/git-secrets --pre-commit + +[hook "pre-applypatch"] + command = perl-linter + error = ignore + +[hook] + runHookDir = interactive +---- + +==== `hookcmd` + +Defines a hook command and its attributes, which will be used when a hook event +occurs. Unqualified attributes are assumed to apply to this hook during all hook +events, but event-specific attributes can also be supplied. The example runs +`/usr/bin/lint-it --language=perl `, but for repos which +include this config, the hook command will be skipped for all events to which +it's normally subscribed _except_ `pre-commit`. + +---- +[hookcmd "perl-linter"] + command = /usr/bin/lint-it --language=perl + skip = true + pre-commit-skip = false +---- + +=== Command-line API + +Users should be able to view, reorder, and create hook commands via the command +line. External tools should be able to view a list of hooks in the correct order +to run. + +*`git hook list `* + +*`git hook list (--system|--global|--local|--worktree)`* + +*`git hook edit `* + +*`git hook add `* + +=== Hook editor + +The tool which is presented by `git hook edit `. Ideally, this +tool should be easier to use than manually editing the config, and then produce +a concise config afterwards. It may take a form similar to `git rebase +--interactive`. + +== Implementation + +=== Library + +`hook.c` and `hook.h` are responsible for interacting with the config files. In +the case when the code generating a hook event doesn't have special concerns +about how to run the hooks, the hook library will provide a basic API to call +all hooks in config order with an `argv_array` provided by the code which +generates the hook event: + +*`int run_hooks(const char *hookname, struct argv_array *args)`* + +This call includes the hook command provided by `run-command.h:find_hook()`; +eventually, this legacy hook will be gated by a config `hook.runHookDir`. The +config is checked against a number of cases: + +- "no": the legacy hook will not be run +- "interactive": Git will prompt the user before running the legacy hook +- "warn": Git will print a warning to stderr before running the legacy hook +- "yes" (default): Git will silently run the legacy hook + +In case this list is expanded in the future, if a value for `hook.runHookDir` is +given which Git does not recognize, Git should discard that config entry. For +example, if "warn" was specified at system level and "junk" was specified at +global level, Git would resolve the value to "warn"; if the only time the config +was set was to "junk", Git would use the default value of "yes". + +If the caller wants to do something more complicated, the hook library can also +provide a callback API: + +*`int for_each_hookcmd(const char *hookname, hookcmd_function *cb)`* + +Finally, to facilitate the builtin, the library will also provide the following +APIs to interact with the config: + +---- +int set_hook_commands(const char *hookname, struct string_list *commands, + enum config_scope scope); +int set_hookcmd(const char *hookcmd, struct hookcmd options); + +int list_hook_commands(const char *hookname, struct string_list *commands); +int list_hooks_in_scope(enum config_scope scope, struct string_list *commands); +---- + +`struct hookcmd` is expected to grow in size over time as more functionality is +added to hooks; so that other parts of the code don't need to understand the +config schema, `struct hookcmd` should contain logical values instead of string +pairs. + +---- +struct hookcmd { + const char *name; + const char *command; + + /* for illustration only; not planned at present */ + int parallelizable; + const char *hookcmd_before; + const char *hookcmd_after; + enum recovery_action on_fail; +} +---- + +=== Builtin + +`builtin/hook.c` is responsible for providing the frontend. It's responsible for +formatting user-provided data and then calling the library API to set the +configs as appropriate. The builtin frontend is not responsible for calling the +config directly, so that other areas of Git can rely on the hook library to +understand the most recent config schema for hooks. + +=== Migration path + +==== Stage 0 + +Hooks are called by running `run-command.h:find_hook()` with the hookname and +executing the result. The hook library and builtin do not exist. Hooks only +exist as specially named scripts within `.git/hooks/`. + +==== Stage 1 + +`git hook list --porcelain ` is implemented. Users can replace their +`.git/hooks/` scripts with a trampoline based on `git hook list`'s +output. Modifier commands like `git hook add` and `git hook edit` can be +implemented around this time as well. + +==== Stage 2 + +`hook.h:run_hooks()` is taught to include `run-command.h:find_hook()` at the +end; calls to `find_hook()` are replaced with calls to `run_hooks()`. Users can +opt-in to config-based hooks simply by creating some in their config; otherwise +users should remain unaffected by the change. + +==== Stage 3 + +The call to `find_hook()` inside of `run_hooks()` learns to check for a config, +`hook.runHookDir`. Users can opt into managing their hooks completely via the +config this way. + +==== Stage 4 + +`.git/hooks` is removed from the template and the hook directory is considered +deprecated. To avoid breaking older repos, the default of `hook.runHookDir` is +not changed, and `find_hook()` is not removed. + +== Caveats + +=== Security and repo config + +Part of the motivation behind this refactor is to mitigate hooks as an attack +vector;footnote:[https://lore.kernel.org/git/20171002234517.GV19555@aiede.mtv.corp.google.com/] +however, as the design stands, users can still provide hooks in the repo-level +config, which is included when a repo is zipped and sent elsewhere. The +security of the repo-level config is still under discussion; this design +generally assumes the repo-level config is secure, which is not true yet. The +goal is to avoid an overcomplicated design to work around a problem which has +ceased to exist. + +=== Ease of use + +The config schema is nontrivial; that's why it's important for the `git hook` +modifier commands to be usable. Contributors with UX expertise are encouraged to +share their suggestions. + +== Alternative approaches + +A previous summary of alternatives exists in the +archives.footnote:[https://lore.kernel.org/git/20191116011125.GG22855@google.com] + +=== Status quo + +Today users can implement multihooks themselves by using a "trampoline script" +as their hook, and pointing that script to a directory or list of other scripts +they wish to run. + +=== Hook directories + +Other contributors have suggested Git learn about the existence of a directory +such as `.git/hooks/.d` and execute those hooks in alphabetical order. + +=== Comparison table + +.Comparison of alternatives +|=== +|Feature |Config-based hooks |Hook directories |Status quo + +|Supports multiple hooks +|Natively +|Natively +|With user effort + +|Safer for zipped repos +|A little +|No +|No + +|Previous hooks just work +|If configured +|Yes +|Yes + +|Can install one hook to many repos +|Yes +|No +|No + +|Discoverability +|Better (in `git help git`) +|Same as before +|Same as before + +|Hard to run unexpected hook +|If configured +|No +|No +|=== + +== Future work + +=== Execution ordering + +We may find that config order is insufficient for some users; for example, +config order makes it difficult to add a new hook to the system or global config +which runs at the end of the hook list. A new ordering schema should be: + +1) Specified by a `hook.order` config, so that users will not unexpectedly see +their order change; + +2) Either dependency or numerically based. + +Dependency-based ordering is prone to classic linked-list problems, like a +cycles and handling of missing dependencies. But, it paves the way for enabling +parallelization if some tasks truly depend on others. + +Numerical ordering makes it tricky for Git to generate suggested ordering +numbers for each command, but is easy to determine a definitive order. + +=== Parallelization + +Users with many hooks might want to run them simultaneously, if the hooks don't +modify state; if one hook depends on another's output, then users will want to +specify those dependencies. If we decide to solve this problem, we may want to +look to modern build systems for inspiration on how to manage dependencies and +parallel tasks. + +=== Securing hookdir hooks + +With the design as written in this doc, it's still possible for a malicious user +to modify `.git/config` to include `hook.pre-receive.command = rm -rf /`, then +zip their repo and send it to another user. It may be necessary to teach Git to +only allow one-line hooks like this if they were configured outside of the local +scope; or another approach, like a list of safe projects, might be useful. It +may also be sufficient (or at least useful) to teach a `hook.disableAll` config +or similar flag to the Git executable. + +=== Submodule inheritance + +It's possible some submodules may want to run the identical set of hooks that +their superrepo runs. While a globally-configured hook set is helpful, it's not +a great solution for users who have multiple repos-with-submodules under the +same user. It would be useful for submodules to learn how to run hooks from +their superrepo's config, or inherit that hook setting. + +== Glossary + +*hook event* + +A point during Git's execution where user scripts may be run, for example, +_prepare-commit-msg_ or _pre-push_. + +*hook command* + +A user script or executable which will be run on one or more hook events. From patchwork Thu May 21 18:54:12 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11563751 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id 2176114B7 for ; Thu, 21 May 2020 18:54:31 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 09E3B20DD4 for ; Thu, 21 May 2020 18:54:31 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="aZEgH0a8" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730035AbgEUSya (ORCPT ); Thu, 21 May 2020 14:54:30 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40092 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1729940AbgEUSy2 (ORCPT ); Thu, 21 May 2020 14:54:28 -0400 Received: from mail-yb1-xb4a.google.com (mail-yb1-xb4a.google.com [IPv6:2607:f8b0:4864:20::b4a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id A3505C061A0E for ; Thu, 21 May 2020 11:54:27 -0700 (PDT) Received: by mail-yb1-xb4a.google.com with SMTP id z5so2022751ybg.11 for ; Thu, 21 May 2020 11:54:27 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20161025; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=u2kvFFY9oVhlLIkyIXbtq9xt/mBrcL0FUluvBhTftHM=; b=aZEgH0a8OIXJtyG/424RcpZr6TI4WJ2gL6njip1NUiGV0sK24rsRVZGbSPAkgnaAi7 +7WOD4mLBsnhOZ5SIFd8XpOBxStyWKhMcr6xK2YRNMfCgxDSo/8Wjct+AQdserHnkBvO 6PRhH76pIpCR9/XzqhOttxBHcO2nQuZ7qzC3L6AOcg75GPV/QWd4Rg4i+5aMd89DPISc e8iZNFYK1305lq5e4TdWh3sLKxbBLGVu7lJskHhTOzfWa97ydXYIjRNjTU0pOUu9fEC2 G68WeWm4/nXWuZMY3w8QRspmPIFDRSX7zu4+x2WgUbDMKfQ1Kx/RbeDI/Ksg65gNoI1W EB+g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=u2kvFFY9oVhlLIkyIXbtq9xt/mBrcL0FUluvBhTftHM=; b=uZTWYWBnlhaafZPJyqYeM5f9jhGKSN5osXnORZ4VdswF4yoxJFpPqWpsDFekwG3JZd UX9DSCyWmxVWF2PxyukUHl118RNuiXUMk69DzAq2cVEpXuMtyQ+woWEt2vx35vhFrPyT j9xj6R8ZVaO/aCErO3YG0A4czzSmkkhknz6YD0GfdNFh9eJMVbzqL7o/3se9UYVYdsvS oaLHbm/sOGmODMwB7hz/iqj20P8JiNR4pJMv6GpIH9ZyBz/phuqB5H5ecEryHIJzulWk BzLKD4wlMewR010cy0ADtdu3p+hF0UedYlsYKLG37HuLag+cf1y9bkKo6GUcpfpmeiL/ D4Hg== X-Gm-Message-State: AOAM532AipXsqsK5zSJTHY6JOP8jn2qkvCyVg9B9Qk1Lm9VJQm9fpe5h Ry5/B3ov4MOJjn2tSgWtTKxfMsAbrpwS/VcHci/VTLazrSPkurFdT3HI6R3FCmu9ZbtBtP/sQal x38wavKvUgrwSQjfJ7TR6d4Kq3N0HMC4wpy4cqwDMon2x5kfqta/1/DkTwuuO/w+n3rXgeH/aPQ == X-Google-Smtp-Source: ABdhPJxaD9qsftWG+poMSIk4TFCxpu9JglFuVrr02Sw4f0hQ9jtqMcucuEVnKV8M8b3+IJ2VzT+IaIhaWlbJD1Kfzvw= X-Received: by 2002:a25:e655:: with SMTP id d82mr7521984ybh.419.1590087266837; Thu, 21 May 2020 11:54:26 -0700 (PDT) Date: Thu, 21 May 2020 11:54:12 -0700 In-Reply-To: <20200521185414.43760-1-emilyshaffer@google.com> Message-Id: <20200521185414.43760-3-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200521185414.43760-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.27.0.rc0.183.gde8f92d652-goog Subject: [PATCH v2 2/4] hook: scaffolding for git-hook subcommand From: Emily Shaffer To: git@vger.kernel.org Cc: Emily Shaffer Sender: git-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org Introduce infrastructure for a new subcommand, git-hook, which will be used to ease config-based hook management. This command will handle parsing configs to compose a list of hooks to run for a given event, as well as adding or modifying hook configs in an interactive fashion. Signed-off-by: Emily Shaffer --- .gitignore | 1 + Documentation/git-hook.txt | 19 +++++++++++++++++++ Makefile | 1 + builtin.h | 1 + builtin/hook.c | 21 +++++++++++++++++++++ git.c | 1 + t/t1360-config-based-hooks.sh | 11 +++++++++++ 7 files changed, 55 insertions(+) create mode 100644 Documentation/git-hook.txt create mode 100644 builtin/hook.c create mode 100755 t/t1360-config-based-hooks.sh diff --git a/.gitignore b/.gitignore index ee509a2ad2..0694a34884 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ /git-grep /git-hash-object /git-help +/git-hook /git-http-backend /git-http-fetch /git-http-push diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt new file mode 100644 index 0000000000..2d50c414cc --- /dev/null +++ b/Documentation/git-hook.txt @@ -0,0 +1,19 @@ +git-hook(1) +=========== + +NAME +---- +git-hook - Manage configured hooks + +SYNOPSIS +-------- +[verse] +'git hook' + +DESCRIPTION +----------- +You can list, add, and modify hooks with this command. + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Makefile b/Makefile index 3d3a39fc19..fce6ee154e 100644 --- a/Makefile +++ b/Makefile @@ -1080,6 +1080,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o BUILTIN_OBJS += builtin/grep.o BUILTIN_OBJS += builtin/hash-object.o BUILTIN_OBJS += builtin/help.o +BUILTIN_OBJS += builtin/hook.o BUILTIN_OBJS += builtin/index-pack.o BUILTIN_OBJS += builtin/init-db.o BUILTIN_OBJS += builtin/interpret-trailers.o diff --git a/builtin.h b/builtin.h index a5ae15bfe5..4e736499c0 100644 --- a/builtin.h +++ b/builtin.h @@ -157,6 +157,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix); int cmd_grep(int argc, const char **argv, const char *prefix); int cmd_hash_object(int argc, const char **argv, const char *prefix); int cmd_help(int argc, const char **argv, const char *prefix); +int cmd_hook(int argc, const char **argv, const char *prefix); int cmd_index_pack(int argc, const char **argv, const char *prefix); int cmd_init_db(int argc, const char **argv, const char *prefix); int cmd_interpret_trailers(int argc, const char **argv, const char *prefix); diff --git a/builtin/hook.c b/builtin/hook.c new file mode 100644 index 0000000000..b2bbc84d4d --- /dev/null +++ b/builtin/hook.c @@ -0,0 +1,21 @@ +#include "cache.h" + +#include "builtin.h" +#include "parse-options.h" + +static const char * const builtin_hook_usage[] = { + N_("git hook"), + NULL +}; + +int cmd_hook(int argc, const char **argv, const char *prefix) +{ + struct option builtin_hook_options[] = { + OPT_END(), + }; + + argc = parse_options(argc, argv, prefix, builtin_hook_options, + builtin_hook_usage, 0); + + return 0; +} diff --git a/git.c b/git.c index a2d337eed7..99372529a2 100644 --- a/git.c +++ b/git.c @@ -517,6 +517,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 }, { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT }, { "init", cmd_init_db }, { "init-db", cmd_init_db }, diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh new file mode 100755 index 0000000000..34b0df5216 --- /dev/null +++ b/t/t1360-config-based-hooks.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +test_description='config-managed multihooks, including git-hook command' + +. ./test-lib.sh + +test_expect_success 'git hook command does not crash' ' + git hook +' + +test_done From patchwork Thu May 21 18:54:13 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11563753 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id D80E21392 for ; Thu, 21 May 2020 18:54:34 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id BA73E20DD4 for ; Thu, 21 May 2020 18:54:34 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="cHINImwT" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730064AbgEUSyd (ORCPT ); Thu, 21 May 2020 14:54:33 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40100 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1729726AbgEUSya (ORCPT ); Thu, 21 May 2020 14:54:30 -0400 Received: from mail-qv1-xf49.google.com (mail-qv1-xf49.google.com [IPv6:2607:f8b0:4864:20::f49]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 09F08C061A0E for ; Thu, 21 May 2020 11:54:30 -0700 (PDT) Received: by mail-qv1-xf49.google.com with SMTP id d11so8049990qvv.10 for ; Thu, 21 May 2020 11:54:30 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20161025; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=0IfROp4fOd28BxrJ5FA/aqBfnpuKB/iqi/YkOVeGX4I=; b=cHINImwTY7Lv5bXl0KPKUVb5MAc5N6TO0kCIIxlZ13MBRBe5NXHNPEPZ94WiUE7nvD 7A6WVJUWgTJbpzf/mKHjTH4HFZdicPo1wzBstfeCxlzYhRHm+bwB45HJGWvf6zZ2f16b DdiPwWAlQNvrpWTP0B8DQ7bFnMbPYCvVU889NiTrFf5JHJ2KJMjU2+zmv0HapiUjkjQA MFs1TQHZ3Gi9k9VCyRiiAL1Nh2LDCvZqkBSNPfRVTIU2SBKgxrT3ZZnKKuQBt7f5qEex d8AOeg41+Icgr8dxIsNaiFHANGiQ5ukO2bPKH+WFB09+TjVUSkgIJfUxaEfUsg8V81/1 ZMQw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=0IfROp4fOd28BxrJ5FA/aqBfnpuKB/iqi/YkOVeGX4I=; b=Hn8DQgvY0Xir/ZxFG8haIOipHPRRrLqJ5qqD1OU1PyTo547KHTkPuADI71muieR6cJ 2EVBB1QSg7vZirOE2/7YVrE9HA26YJyWzdz4Pe7iRooLL6QGXAsPaFfN0n9+Z1Mg1ITj t6+DRHx1oA8YZrmX4ii9DNqJWl3lKLJnZqXjWux+9yjCmfJLJGFJZtulzv9UY+Il5FjW OVWSePnoBWMfXkOl78bWJn9F/cWVYmH/oDcHOE77Jrokk8UrrTijzE4VjVj2WRAFmWCU Dm1IPGdNAZtyRg2M6Tc+uJXyvkiyExQCaOBFth5o7Zt95Ns6iFT5zYLKe0/BE9YmW583 ykdw== X-Gm-Message-State: AOAM532vFC7iSkhlyDkO5K0GtmHw7iByqXpD3bDMA2xwDBHq6I05ndj+ 4mrvMD5g5MYE1uPdxiHoPARhUoF2tp0a98JQ6f4u5ceLQ066u3JspVFha6sFigalSq96BFFNVda 1ReSuWwcjm2hqmrPS/21PyBICMQY+8hBQlr+ESFfdU+dBmcuQUOQBcSQBzteroct27o0pMnGndg == X-Google-Smtp-Source: ABdhPJyOeyPqb8piRWBOZCCWPQxuBSXK2P7McS2FYM3eM2Ydti723udXiBc7xEKT3X1Kc7ekZAM70COghj+BLHwTu8k= X-Received: by 2002:a05:6214:5b1:: with SMTP id by17mr136944qvb.37.1590087269142; Thu, 21 May 2020 11:54:29 -0700 (PDT) Date: Thu, 21 May 2020 11:54:13 -0700 In-Reply-To: <20200521185414.43760-1-emilyshaffer@google.com> Message-Id: <20200521185414.43760-4-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200521185414.43760-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.27.0.rc0.183.gde8f92d652-goog Subject: [PATCH v2 3/4] hook: add list command From: Emily Shaffer To: git@vger.kernel.org Cc: Emily Shaffer Sender: git-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org Teach 'git hook list ', which checks the known configs in order to create an ordered list of hooks to run on a given hook event. Multiple commands can be specified for a given hook by providing multiple "hook..command = " lines. Hooks will be run in config order. If more properties need to be set on a given hook in the future, commands can also be specified by providing "hook..command = ", as well as a "[hookcmd ]" subsection; at minimum, this subsection must contain a "hookcmd..command = " line. For example: $ git config --list | grep ^hook hook.pre-commit.command=baz hook.pre-commit.command=~/bar.sh hookcmd.baz.command=~/baz/from/hookcmd.sh $ git hook list pre-commit ~/baz/from/hookcmd.sh ~/bar.sh Signed-off-by: Emily Shaffer Signed-off-by: Johannes Schindelin --- Documentation/git-hook.txt | 37 +++++++++++++- Makefile | 1 + builtin/hook.c | 55 +++++++++++++++++++-- hook.c | 90 +++++++++++++++++++++++++++++++++++ hook.h | 15 ++++++ t/t1360-config-based-hooks.sh | 51 +++++++++++++++++++- 6 files changed, 242 insertions(+), 7 deletions(-) create mode 100644 hook.c create mode 100644 hook.h diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt index 2d50c414cc..e458586e96 100644 --- a/Documentation/git-hook.txt +++ b/Documentation/git-hook.txt @@ -8,12 +8,47 @@ git-hook - Manage configured hooks SYNOPSIS -------- [verse] -'git hook' +'git hook' list DESCRIPTION ----------- You can list, add, and modify hooks with this command. +This command parses the default configuration files for sections "hook" and +"hookcmd". "hook" is used to describe the commands which will be run during a +particular hook event; commands are run in config order. "hookcmd" is used to +describe attributes of a specific command. If additional attributes don't need +to be specified, a command to run can be specified directly in the "hook" +section; if a "hookcmd" by that name isn't found, Git will attempt to run the +provided value directly. For example: + +Global config +---- + [hook "post-commit"] + command = "linter" + command = "~/typocheck.sh" + + [hookcmd "linter"] + command = "/bin/linter --c" +---- + +Local config +---- + [hook "prepare-commit-msg"] + command = "linter" + [hook "post-commit"] + command = "python ~/run-test-suite.py" +---- + +COMMANDS +-------- + +list :: + +List the hooks which have been configured for . Hooks appear +in the order they should be run, and note the config scope where the relevant +`hook..command` was specified, not the `hookcmd` (if applicable). + GIT --- Part of the linkgit:git[1] suite diff --git a/Makefile b/Makefile index fce6ee154e..b7bbf3be7b 100644 --- a/Makefile +++ b/Makefile @@ -894,6 +894,7 @@ LIB_OBJS += grep.o LIB_OBJS += hashmap.o LIB_OBJS += help.o LIB_OBJS += hex.o +LIB_OBJS += hook.o LIB_OBJS += ident.o LIB_OBJS += interdiff.o LIB_OBJS += json-writer.o diff --git a/builtin/hook.c b/builtin/hook.c index b2bbc84d4d..cfd8e388bd 100644 --- a/builtin/hook.c +++ b/builtin/hook.c @@ -1,21 +1,68 @@ #include "cache.h" #include "builtin.h" +#include "config.h" +#include "hook.h" #include "parse-options.h" +#include "strbuf.h" static const char * const builtin_hook_usage[] = { - N_("git hook"), + N_("git hook list "), NULL }; -int cmd_hook(int argc, const char **argv, const char *prefix) +static int list(int argc, const char **argv, const char *prefix) { - struct option builtin_hook_options[] = { + struct list_head *head, *pos; + struct hook *item; + struct strbuf hookname = STRBUF_INIT; + + struct option list_options[] = { OPT_END(), }; - argc = parse_options(argc, argv, prefix, builtin_hook_options, + argc = parse_options(argc, argv, prefix, list_options, builtin_hook_usage, 0); + if (argc < 1) { + usage_msg_opt("a hookname must be provided to operate on.", + builtin_hook_usage, list_options); + } + + strbuf_addstr(&hookname, argv[0]); + + head = hook_list(&hookname); + + if (!head) { + printf(_("no commands configured for hook '%s'\n"), + hookname.buf); + return 0; + } + + list_for_each(pos, head) { + item = list_entry(pos, struct hook, list); + if (item) + printf("%s:\t%s\n", + config_scope_name(item->origin), + item->command.buf); + } + + clear_hook_list(); + strbuf_release(&hookname); + return 0; } + +int cmd_hook(int argc, const char **argv, const char *prefix) +{ + struct option builtin_hook_options[] = { + OPT_END(), + }; + if (argc < 2) + usage_with_options(builtin_hook_usage, builtin_hook_options); + + if (!strcmp(argv[1], "list")) + return list(argc - 1, argv + 1, prefix); + + usage_with_options(builtin_hook_usage, builtin_hook_options); +} diff --git a/hook.c b/hook.c new file mode 100644 index 0000000000..9dfc1a885e --- /dev/null +++ b/hook.c @@ -0,0 +1,90 @@ +#include "cache.h" + +#include "hook.h" +#include "config.h" + +static LIST_HEAD(hook_head); + +void free_hook(struct hook *ptr) +{ + if (ptr) { + strbuf_release(&ptr->command); + free(ptr); + } +} + +static void emplace_hook(struct list_head *pos, const char *command) +{ + struct hook *to_add = malloc(sizeof(struct hook)); + to_add->origin = current_config_scope(); + strbuf_init(&to_add->command, 0); + strbuf_addstr(&to_add->command, command); + + list_add_tail(&to_add->list, pos); +} + +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(void) +{ + struct list_head *pos, *tmp; + list_for_each_safe(pos, tmp, &hook_head) + remove_hook(pos); +} + +static int hook_config_lookup(const char *key, const char *value, void *hook_key_cb) +{ + const char *hook_key = hook_key_cb; + + if (!strcmp(key, hook_key)) { + const char *command = value; + struct strbuf hookcmd_name = STRBUF_INIT; + struct list_head *pos = NULL, *tmp = NULL; + + /* Check if a hookcmd with that name exists. */ + strbuf_addf(&hookcmd_name, "hookcmd.%s.command", command); + git_config_get_value(hookcmd_name.buf, &command); + + if (!command) + BUG("git_config_get_value overwrote a string it shouldn't have"); + + /* + * TODO: implement an option-getting callback, e.g. + * get configs by pattern hookcmd.$value.* + * for each key+value, do_callback(key, value, cb_data) + */ + + list_for_each_safe(pos, tmp, &hook_head) { + struct hook *hook = list_entry(pos, struct hook, list); + /* + * The list of hooks to run can be reordered by being redeclared + * in the config. Options about hook ordering should be checked + * here. + */ + if (0 == strcmp(hook->command.buf, command)) + remove_hook(pos); + } + emplace_hook(pos, command); + } + + return 0; +} + +struct list_head* hook_list(const struct strbuf* hookname) +{ + struct strbuf hook_key = STRBUF_INIT; + + if (!hookname) + return NULL; + + strbuf_addf(&hook_key, "hook.%s.command", hookname->buf); + + git_config(hook_config_lookup, (void*)hook_key.buf); + + return &hook_head; +} diff --git a/hook.h b/hook.h new file mode 100644 index 0000000000..aaf6511cff --- /dev/null +++ b/hook.h @@ -0,0 +1,15 @@ +#include "config.h" +#include "list.h" +#include "strbuf.h" + +struct hook +{ + struct list_head list; + enum config_scope origin; + struct strbuf command; +}; + +struct list_head* hook_list(const struct strbuf *hookname); + +void free_hook(struct hook *ptr); +void clear_hook_list(void); diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh index 34b0df5216..4e46d7dd4e 100755 --- a/t/t1360-config-based-hooks.sh +++ b/t/t1360-config-based-hooks.sh @@ -4,8 +4,55 @@ test_description='config-managed multihooks, including git-hook command' . ./test-lib.sh -test_expect_success 'git hook command does not crash' ' - git hook +test_expect_success 'git hook rejects commands without a mode' ' + test_must_fail git hook pre-commit +' + + +test_expect_success 'git hook rejects commands without a hookname' ' + test_must_fail git hook list +' + +test_expect_success 'setup hooks in global, and local' ' + git config --add --local hook.pre-commit.command "/path/ghi" && + git config --add --global hook.pre-commit.command "/path/def" +' + +test_expect_success 'git hook list orders by config order' ' + cat >expected <<-\EOF && + global: /path/def + local: /path/ghi + EOF + + git hook list pre-commit >actual && + test_cmp expected actual +' + +test_expect_success 'git hook list dereferences a hookcmd' ' + git config --add --local hook.pre-commit.command "abc" && + git config --add --global hookcmd.abc.command "/path/abc" && + + cat >expected <<-\EOF && + global: /path/def + local: /path/ghi + local: /path/abc + EOF + + git hook list pre-commit >actual && + test_cmp expected actual +' + +test_expect_success 'git hook list reorders on duplicate commands' ' + git config --add --local hook.pre-commit.command "/path/def" && + + cat >expected <<-\EOF && + local: /path/ghi + local: /path/abc + local: /path/def + EOF + + git hook list pre-commit >actual && + test_cmp expected actual ' test_done From patchwork Thu May 21 18:54:14 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11563755 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id 9321E14B7 for ; Thu, 21 May 2020 18:54:35 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 7B6FE20DD4 for ; Thu, 21 May 2020 18:54:35 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="TZt90AlV" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1730074AbgEUSye (ORCPT ); Thu, 21 May 2020 14:54:34 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40110 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1730071AbgEUSyd (ORCPT ); Thu, 21 May 2020 14:54:33 -0400 Received: from mail-yb1-xb4a.google.com (mail-yb1-xb4a.google.com [IPv6:2607:f8b0:4864:20::b4a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 3CBFDC061A0F for ; Thu, 21 May 2020 11:54:32 -0700 (PDT) Received: by mail-yb1-xb4a.google.com with SMTP id 5so6415440ybe.17 for ; Thu, 21 May 2020 11:54:32 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20161025; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=UyyyqzHhRLQ6JaolD9JAOwdk4Z7TICBWzomUYHy8LyQ=; b=TZt90AlV8aAYoXI2XIYgwUEy32hAGhVlqUt3S+Cw4NEJb7CdwZqPyXqtmLSxoFaWu6 cnMAkKUGawa2rT0QQRGT42BSNl4kzAKtlRmml/L3CYeDOmo3rOQj7M+zjzccjyCZouHf W9YEz9YdmGtkwvMWJ/KlzKNvRohVXmZCYHQkxOKHFdYmfS8DIPQdepndczIrq0tmorf/ 3zf4Af6Z+91dK0gf63lkQRFC8S0AlrAwT1/BGkj53vVZQLG+xA3ZpkZtBUgU2WB9D0Jb +s2YxsxYvCGf9GCyUfNxCn7nlqLa2QDQvC9KKGQesO81JWP+eLnuzgm0UKkAJO1gOBf0 iA0A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=UyyyqzHhRLQ6JaolD9JAOwdk4Z7TICBWzomUYHy8LyQ=; b=ts0f6fY7lLKmwAt+TrfSOTYtMZiCrRCV5wqcqxPZh5bCC0kb2tvOlxnXYJUGyWwd0M uFxRMmS6AQtqlWi+PRi23zwM8snzlG7EoLRzpozx0c1RZAS1qvfU8ehdwoXJ5i5UBdSf ZjI/nwQ4zDH4qWCtcKvWr7VgcYKlhTpyxcAS1ea/NTv9SyV/0FGRq9ESsXm+DkARqZGE nvsv2d11e6YX6lQOQFdMI4n7J0Z9dWdzreqXemQmTZLVloJmuBGfcii7z84Lgi3s4cP2 ewR9V14EURbLg53cP7YOWMr2xF36qfy2kILvOX43EKI/yqdVTinL4cY4guiXejkHRcB+ SEqg== X-Gm-Message-State: AOAM530UK5JWuI28T0qPEHWcdGcTvPJaccOAcMAeHofLB74d4Cnoy797 kvR4KSEV92P4OoAAFUXotA8IenX2zdrUqg1UQK0Cbq2MD3pCjnLloIjcOxM4Wtzk7YpcNDcG17T JQCsVc99mgDXKP8j/JpX4laYhp7r2ZuKhBBcrjCggfz6McY6pDU3lSPGsn37j+SgDc2yoj4mMhA == X-Google-Smtp-Source: ABdhPJxF+sL4BqDS204eyIRwI12SIUY/DQplHjnIvpIBZOVAbbWq0F14W9p/MKwVmbR6smvuHk2vTyGXjjVMkZXKnNs= X-Received: by 2002:a25:bc81:: with SMTP id e1mr15141342ybk.311.1590087271399; Thu, 21 May 2020 11:54:31 -0700 (PDT) Date: Thu, 21 May 2020 11:54:14 -0700 In-Reply-To: <20200521185414.43760-1-emilyshaffer@google.com> Message-Id: <20200521185414.43760-5-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200521185414.43760-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.27.0.rc0.183.gde8f92d652-goog Subject: [PATCH v2 4/4] hook: add --porcelain to list command From: Emily Shaffer To: git@vger.kernel.org Cc: Emily Shaffer Sender: git-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org Teach 'git hook list --porcelain ', which prints simply the commands to be run in the order suggested by the config. This option is intended for use by user scripts, wrappers, or out-of-process Git commands which still want to execute hooks. For example, the following snippet might be added to git-send-email.perl to introduce a `pre-send-email` hook: sub pre_send_email { open(my $fh, 'git hook list --porcelain pre-send-email |'); chomp(my @hooks = <$fh>); close($fh); foreach $hook (@hooks) { system $hook } Signed-off-by: Emily Shaffer Signed-off-by: Johannes Schindelin --- Documentation/git-hook.txt | 13 +++++++++++-- builtin/hook.c | 17 +++++++++++++---- t/t1360-config-based-hooks.sh | 11 +++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Documentation/git-hook.txt b/Documentation/git-hook.txt index e458586e96..0854035ce2 100644 --- a/Documentation/git-hook.txt +++ b/Documentation/git-hook.txt @@ -8,7 +8,7 @@ git-hook - Manage configured hooks SYNOPSIS -------- [verse] -'git hook' list +'git hook' list [--porcelain] DESCRIPTION ----------- @@ -43,11 +43,20 @@ Local config COMMANDS -------- -list :: +list [--porcelain] :: List the hooks which have been configured for . Hooks appear in the order they should be run, and note the config scope where the relevant `hook..command` was specified, not the `hookcmd` (if applicable). ++ +If `--porcelain` is specified, instead print the commands alone, separated by +newlines, for easy parsing by a script. + +OPTIONS +------- +--porcelain:: + With `list`, print the commands in the order they should be run, + separated by newlines, for easy parsing by a script. GIT --- diff --git a/builtin/hook.c b/builtin/hook.c index cfd8e388bd..2e51c84c81 100644 --- a/builtin/hook.c +++ b/builtin/hook.c @@ -16,8 +16,11 @@ static int list(int argc, const char **argv, const char *prefix) struct list_head *head, *pos; struct hook *item; struct strbuf hookname = STRBUF_INIT; + int porcelain = 0; struct option list_options[] = { + OPT_BOOL(0, "porcelain", &porcelain, + "format for execution by a script"), OPT_END(), }; @@ -29,6 +32,8 @@ static int list(int argc, const char **argv, const char *prefix) builtin_hook_usage, list_options); } + + strbuf_addstr(&hookname, argv[0]); head = hook_list(&hookname); @@ -41,10 +46,14 @@ static int list(int argc, const char **argv, const char *prefix) list_for_each(pos, head) { item = list_entry(pos, struct hook, list); - if (item) - printf("%s:\t%s\n", - config_scope_name(item->origin), - item->command.buf); + if (item) { + if (porcelain) + printf("%s\n", item->command.buf); + else + printf("%s:\t%s\n", + config_scope_name(item->origin), + item->command.buf); + } } clear_hook_list(); diff --git a/t/t1360-config-based-hooks.sh b/t/t1360-config-based-hooks.sh index 4e46d7dd4e..3296d8af45 100755 --- a/t/t1360-config-based-hooks.sh +++ b/t/t1360-config-based-hooks.sh @@ -55,4 +55,15 @@ test_expect_success 'git hook list reorders on duplicate commands' ' test_cmp expected actual ' +test_expect_success 'git hook list --porcelain prints just the command' ' + cat >expected <<-\EOF && + /path/ghi + /path/abc + /path/def + EOF + + git hook list --porcelain pre-commit >actual && + test_cmp expected actual +' + test_done