From patchwork Tue Jul 28 22:24:50 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11689993 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 DFD2F159A for ; Tue, 28 Jul 2020 22:25:07 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id BF3912074F for ; Tue, 28 Jul 2020 22:25:07 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="esPz2Vt1" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1729636AbgG1WZG (ORCPT ); Tue, 28 Jul 2020 18:25:06 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:53034 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1729270AbgG1WZG (ORCPT ); Tue, 28 Jul 2020 18:25:06 -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 77EEDC061794 for ; Tue, 28 Jul 2020 15:25:05 -0700 (PDT) Received: by mail-yb1-xb4a.google.com with SMTP id v11so19513125ybm.22 for ; Tue, 28 Jul 2020 15:25:05 -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=+xnRc527+ndTQclAjGDcRD7EsBwPE4iVGEmbFtoccu0=; b=esPz2Vt1kwWNVvMjPPxZg0BikwZOpSm0ztymjcQyIAXG/f+60bamV0PRv59kHkErrB I36NFoizRkuVA3JqYx3lfa61q5/9VirWxOd5ceh/wbgulbP6fghZ0upuLZX67/UGXBss NCI4uA2TUufvNJJxCvEEX3HL09V2AXV3q8jQ9CtH4n72QfJFBQeiwp9Mw3fezNWH5pjn 4vX94J02xkQTsbyNlXjYUaKtgiEN7PY0ztkT+0+Zu85RVrFjmHABm2yr6x8+7gdO3z1D ut79WCMDNi2CAfPmsSn3U0ASInf9IY9lWbVI+PZxxZ0jcfEDfKX9+0CRNL8qhzBvSHrb ZkrQ== 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=+xnRc527+ndTQclAjGDcRD7EsBwPE4iVGEmbFtoccu0=; b=RZQDT1XK1E+xt2THhl0iFeeB0h2OOML8dby38LYKxO6Knpttz61mYL8crC9kezTHTp pS2KNKhUPonroUExOqMRSbsv40t7MMgwWslZKnwsOzb7EuOX8WmeGcCDE/ybzICTA2Ce DMlTDsQlGSTeE6Rz+ymF7oKCwDM+NyWWKpAnc/3d5PEDhm06pJyBRiU617DXa0j0dBov 6o+4pMQgv9CFBeCATjKOeynxp/EWiA9HqwGiLUmkfunXmRh63lo7aZkMjb/rN130CRPe djJatievYhL4lY5+XkqGqdfA+xjuZx9dZ9PxFkQaj1/VHWyGLrhTJBtJa1HKgeQT/ISC Cpiw== X-Gm-Message-State: AOAM531pAMtKTFHRcsiziZRIf5+jyao0pyDRY4ZJUz9C0uXq+0hn6laY YxK+pPpXVn/Vlm/W3cTxxB5CEwQFeQBi9cb3SRWVbuvgxkGGOFzovT0IRWclt3AvzY2NcylhJwp mHIdKr5bGNqn5kvwOQ/FVRcyfQAijtZ893yqeq7jUyn0x1EaL+vpEjtTxNsHs6aLKjxUZZhFUsg == X-Google-Smtp-Source: ABdhPJwqnioKTss0lTEXHraK02REJknsSMShhilMSZnjOZ8PnUdCog0HI3rDcwO7zYJzjCvG12KXP2R5mw32SjPogW0= X-Received: by 2002:a25:ab34:: with SMTP id u49mr45963116ybi.203.1595975104500; Tue, 28 Jul 2020 15:25:04 -0700 (PDT) Date: Tue, 28 Jul 2020 15:24:50 -0700 In-Reply-To: <20200728222455.3023400-1-emilyshaffer@google.com> Message-Id: <20200728222455.3023400-2-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200728222455.3023400-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.28.0.rc0.142.g3c755180ce-goog Subject: [PATCH v3 1/6] 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 | 354 ++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 Documentation/technical/config-based-hooks.txt diff --git a/Documentation/Makefile b/Documentation/Makefile index ecd0b340b1..5483995113 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..c6e762b192 --- /dev/null +++ b/Documentation/technical/config-based-hooks.txt @@ -0,0 +1,354 @@ +Configuration-based hook management +=================================== +:sectanchors: + +[[motivation]] +== 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 by making it more apparent to users which scripts will be run during +normal Git operations. + +Make it easier for users to discover Git's hook feature and automate their +workflows. + +[[user-interfaces]] +== User interfaces + +[[config-schema]] +=== Config schema + +Hooks can be introduced by editing the configuration manually. There are two new +sections added, `hook` and `hookcmd`. + +[[config-schema-hook]] +==== `hook` + +Primarily contains subsections for each hook event. These order of these +subsections defines the hook command execution order; hook commands can be +specified by setting the value directly to the command if no additional +configuration is needed, or by setting the value as 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`. (These settings are +described more in <>.) + +---- +[hook "pre-commit"] + command = perl-linter + command = /usr/bin/git-secrets --pre-commit + +[hook "pre-applypatch"] + command = perl-linter + error = ignore + +[hook] + runHookDir = interactive +---- + +[[config-schema-hookcmd]] +==== `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]] +=== 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]] +=== 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]] +== Implementation + +[[library]] +=== 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 + +`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]] +=== Migration path + +[[stage-0]] +==== 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]] +==== 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]] +==== 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]] +==== 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]] +==== 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]] +== Caveats + +[[security]] +=== 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]] +=== 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. + +[[alternatives]] +== Alternative approaches + +A previous summary of alternatives exists in the +archives.footnote:[https://lore.kernel.org/git/20191116011125.GG22855@google.com] + +[[status-quo]] +=== 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]] +=== 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]] +=== 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]] +== Future work + +[[execution-ordering]] +=== 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]] +=== 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]] +=== 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 inlined hooks like this if they were configured outside of the local +scope (in other words, only run hookcmds, and only allow hookcmds to be +configured in global or system 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]] +=== 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]] +== 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 Tue Jul 28 22:24:51 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11689995 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 A2E3F14DD for ; Tue, 28 Jul 2020 22:25:09 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 8C26F2075D for ; Tue, 28 Jul 2020 22:25:09 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="QSowmb6W" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1729705AbgG1WZH (ORCPT ); Tue, 28 Jul 2020 18:25:07 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:53038 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1729270AbgG1WZH (ORCPT ); Tue, 28 Jul 2020 18:25:07 -0400 Received: from mail-yb1-xb49.google.com (mail-yb1-xb49.google.com [IPv6:2607:f8b0:4864:20::b49]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 00E26C061794 for ; Tue, 28 Jul 2020 15:25:07 -0700 (PDT) Received: by mail-yb1-xb49.google.com with SMTP id v11so19513203ybm.22 for ; Tue, 28 Jul 2020 15:25:06 -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=OiBaCt7vuG0WQAD5GyO1mUctvePuwfQ1pCAke1H+Vng=; b=QSowmb6W3rMxz+pOofEovoMFS7zoXDZpn3xv3Num05zLSXRm+AJXjk5b2P2bctlepY 9Ks3yuYrkyBn00T96l5cxU/V8NVuRaPCKCzPCtrkAGWNj7EV3rYTS9K+E/j6TlQloUMX NBq3Fz1NBZg/+WNCWx+9qCPshpft5McKMs0YFj4Q73783Jp3F+FgHPeR67NnmszTmCQE hf8olQPT1IsSsHKs4+140b1G2jKpVXPC3wO+hQ0pleVVIIk2rNUANrpMKmc3Ll1oyg1n pKMXRIgkeQBYJbWB9HIhDLiOwJ1jDWOxvhdgcJdZnV7rGhEcdO37pLXNBi6cPumD2ZGc k9Dw== 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=OiBaCt7vuG0WQAD5GyO1mUctvePuwfQ1pCAke1H+Vng=; b=DqNFZhp6AKrbsNVuLlN1ap00hmR/YoEvTcEzcbHlX3UzEOKt1LrDt6Uxyt1FloUwyA YS7k0LuXu9SrUDa2SftZTED4N5l3YwrU67c1ppe0I7WSpHcnqPAhKLFKbAJgDNb0tWAd 23IHyeJ//C4gogTjHM8XTjXBhnPdV0XfLUVTadVY9Tn5lUKsuQqEyd+OmpXxsINrf3GB +XkzOK4rg0xi94FnvQhds9Hd0u3Szik2Ba8QXYV7TxbK4CX5vrr11cm80KPVCvSmKiJt /6J7i0Nx5o5PR0FA6ZQpsjZd7On3FEb7cqebRt71/VGyHHBwru2Qx9rh3D7fJx5rrKyG Z0Uw== X-Gm-Message-State: AOAM533QQDVIoVH+PF9fBQK420x2u/yuTFO+7umEr89T4rFB+CCUlRg7 QPEpJlfuuxiw9sO1pQzR4S0GDINJ0dr11wwpdfEbTNGgkuogOYPzXM5PWpzqxSZ9reYWESQJVE9 MZV3/LwvonbDB7cAU6eNbllVii7JHvCg1ZHINCgdxiSui2cHFHIMLP1G6U2Bc+oGKzpb0FthbbA == X-Google-Smtp-Source: ABdhPJzIxd6vkOZ2CAjpRfYeOmzAOlVZrY+7rGodynVjCoHpfWR6lpI2C60EDPSYCIvcM+A9rvslxVZU8yy2/lhNEK4= X-Received: by 2002:a5b:28c:: with SMTP id x12mr26106756ybl.112.1595975106178; Tue, 28 Jul 2020 15:25:06 -0700 (PDT) Date: Tue, 28 Jul 2020 15:24:51 -0700 In-Reply-To: <20200728222455.3023400-1-emilyshaffer@google.com> Message-Id: <20200728222455.3023400-3-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200728222455.3023400-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.28.0.rc0.142.g3c755180ce-goog Subject: [PATCH v3 2/6] 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 372139f1f2..e13e58e23f 100644 --- a/Makefile +++ b/Makefile @@ -1077,6 +1077,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 2f021b97f3..7f3328c63f 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 Tue Jul 28 22:24:52 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11689999 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 5105C6C1 for ; Tue, 28 Jul 2020 22:25:11 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 35D982074F for ; Tue, 28 Jul 2020 22:25:11 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="ojzcYbOz" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1729792AbgG1WZK (ORCPT ); Tue, 28 Jul 2020 18:25:10 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:53046 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1729270AbgG1WZI (ORCPT ); Tue, 28 Jul 2020 18:25:08 -0400 Received: from mail-pl1-x649.google.com (mail-pl1-x649.google.com [IPv6:2607:f8b0:4864:20::649]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id C21A7C061794 for ; Tue, 28 Jul 2020 15:25:08 -0700 (PDT) Received: by mail-pl1-x649.google.com with SMTP id f4so12934407plo.3 for ; Tue, 28 Jul 2020 15:25:08 -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=XPJDYrzEHgkpMeXkN26sTR/pLc7HrnLfTcwEgSVPND4=; b=ojzcYbOzXWgt+s6n/al1k9Rl6JyLt7yeOGchnzrkjt6Fd6g5vf0xYW/Gc+lBCycQNP WmqDn5l8fVGZnmLtiuqOarNyWjRVbmTzumvoUJD4MnEgwmHODa0OGuFt9/rJTA6zFD46 rLet0IuOPA3mthKmiOscwhKypz13RPpd+nVRCFnrf1Y/5yJidoRIyZwelNa7lC7ZbkJl sa0EH575uaJ0tTe1f1EItTEayN9FuFXVWI7yslgmJotiKCqQIHg2k/DeBSJGR6RR10Og 2u66X0RcLfbo6YFRTP+DM/AE8yr6Mm6HMAvnuTaSMzn3I/Xi6AUaRLsYqe9ADMSlI8OP cLQQ== 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=XPJDYrzEHgkpMeXkN26sTR/pLc7HrnLfTcwEgSVPND4=; b=XLKUoqS2P26cSmzfQqMeeqEFjDEtU2vvclCXkGqoy16doMex2O3KYyIEZVpFF0pnCn iFPwE5xbEsUoFx3OokhBHthXgXMSZkTaGrdiF4RXji/FVfZwUtoOfDEtDeT4xWueY+c+ xpCBGZeW0fTDCzxWH/l81+i/7GrSKkYFrjQd2fvYtu7CsCCsco5W8a15v+7oVivVgMnL kT8h3gRsfT0Q283bfZUjHvzcJLxD9kqcvDk0XgXtKM3QYa8LA/iVOaQ+gsMc7J4t/5Cq ghYYcw425uR3B4PFrmjTGU+TSxExLe19br+n7tVCSG9+GNwioa8oPVIcuxA9j5R2tMOC JN6Q== X-Gm-Message-State: AOAM530lVU38QBioAxc6VjZd/5gIvpeBbOQdEMZVfk4pWoYYUnI19CUk 802zdgO0/W7JmgLngpQgjbNdUFTgekIIXGpYmvo+C+nDllUEvgStJxMWr95aySfSJY/cHMV37PV dcph43apUtsvClo8kXkj+HMSkfORaCG89ESpV4IjvhD/53RirR3MsDTQiRg8Uh51f7bHuPHPAsw == X-Google-Smtp-Source: ABdhPJydsogVcn2x2XuSQM9G+2BxCCJoUI+waYe79OuCH+qDjS/VxGWMHceW93ivGHuEcgUNylSac7VFlzerwdyhvsA= X-Received: by 2002:a63:ac53:: with SMTP id z19mr25718299pgn.181.1595975108058; Tue, 28 Jul 2020 15:25:08 -0700 (PDT) Date: Tue, 28 Jul 2020 15:24:52 -0700 In-Reply-To: <20200728222455.3023400-1-emilyshaffer@google.com> Message-Id: <20200728222455.3023400-4-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200728222455.3023400-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.28.0.rc0.142.g3c755180ce-goog Subject: [PATCH v3 3/6] 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 --- Documentation/git-hook.txt | 37 +++++++++++++- Makefile | 1 + builtin/hook.c | 55 +++++++++++++++++++-- hook.c | 90 +++++++++++++++++++++++++++++++++++ hook.h | 15 ++++++ t/t1360-config-based-hooks.sh | 68 +++++++++++++++++++++++++- 6 files changed, 259 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 e13e58e23f..50e7c911d1 100644 --- a/Makefile +++ b/Makefile @@ -891,6 +891,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..a0759a4c26 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 (list_empty(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..46d1ed354a 100755 --- a/t/t1360-config-based-hooks.sh +++ b/t/t1360-config-based-hooks.sh @@ -4,8 +4,72 @@ test_description='config-managed multihooks, including git-hook command' . ./test-lib.sh -test_expect_success 'git hook command does not crash' ' - git hook +ROOT= +if test_have_prereq MINGW +then + # In Git for Windows, Unix-like paths work only in shell scripts; + # `git.exe`, however, will prefix them with the pseudo root directory + # (of the Unix shell). Let's accommodate for that. + ROOT="$(cd / && pwd)" +fi + +setup_hooks () { + test_config hook.pre-commit.command "/path/ghi" --add + test_config_global hook.pre-commit.command "/path/def" --add +} + +setup_hookcmd () { + test_config hook.pre-commit.command "abc" --add + test_config_global hookcmd.abc.command "/path/abc" --add +} + +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 'git hook list orders by config order' ' + setup_hooks && + + cat >expected <<-EOF && + global: $ROOT/path/def + local: $ROOT/path/ghi + EOF + + git hook list pre-commit >actual && + test_cmp expected actual +' + +test_expect_success 'git hook list dereferences a hookcmd' ' + setup_hooks && + setup_hookcmd && + + cat >expected <<-EOF && + global: $ROOT/path/def + local: $ROOT/path/ghi + local: $ROOT/path/abc + EOF + + git hook list pre-commit >actual && + test_cmp expected actual +' + +test_expect_success 'git hook list reorders on duplicate commands' ' + setup_hooks && + + test_config hook.pre-commit.command "/path/def" --add && + + cat >expected <<-EOF && + local: $ROOT/path/ghi + local: $ROOT/path/def + EOF + + git hook list pre-commit >actual && + test_cmp expected actual ' test_done From patchwork Tue Jul 28 22:24:53 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11690003 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 5BC7614DD for ; Tue, 28 Jul 2020 22:25:15 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 437052070B for ; Tue, 28 Jul 2020 22:25:15 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="k9w1+D5r" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1729816AbgG1WZN (ORCPT ); Tue, 28 Jul 2020 18:25:13 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:53052 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1729270AbgG1WZL (ORCPT ); Tue, 28 Jul 2020 18:25:11 -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 C6A2BC061794 for ; Tue, 28 Jul 2020 15:25:10 -0700 (PDT) Received: by mail-yb1-xb4a.google.com with SMTP id n21so7468111ybf.18 for ; Tue, 28 Jul 2020 15:25:10 -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=6OE/uE69yRebrRJPaGprkkwEu88LuiF7HtXoJUEcU5w=; b=k9w1+D5r+kVCEIn4FMZCnM/Hn5ca8ce9vxDi5U2VLU29+AQwar4n+ylWo1v/37t2XQ nOQMEKEVRwkAm89+PcxNtikID0ZZJxdnghJy5ks0ccV5oSzZT2PXePa1Uy+OhUPuVAP5 wOE6qHm/Zib/a1VRLUtPTP7opFPnXKgk2LVRw6G40X/PtVlUoj5NQUj4doeH1Iuzv0Z+ 9WPo8bxvvpgxKv4SGQJJEgI60JZ/0bNt2WtiFDiX+vp0u08RSCtiTv9j6v8zsB9CmuYl leVEidp/ITDVV6QYs3msZzuoSj+rpWx4IjUq5x+skJwYSXQxnbreimemNvtc+JP8mkcL rDsA== 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=6OE/uE69yRebrRJPaGprkkwEu88LuiF7HtXoJUEcU5w=; b=Bwp6D5BxGX6+oGORN/6Q7n/bKod9r/mUh4hAEyy0ohTC87Dmv/N88TD0hTFBMlQXvu 9oCPUiZlpMsJR9IT/7mW0J9cncp+kGE815W6pirEx5P+oS44R9pN0/1idYgb3GQz/xSb RGomk9sPXO7DdCA/hXHAOHg+ENZnmYoUJKmavaEEs8b/Dv009xs7k892/wNb8fYGgKU4 iMQRqO/aBlXumIY/CzJ3aEugMzWMcztqqDUjXNQcEcPH/Ybsfg3/nw59llKAnkDYbnj/ O4aOSVVthcCqBfrrORq/IyUo+wkDhs/ATGtdijjo2hi1CPbKSUEgcVOYbZJYOIp6w/e4 BYUg== X-Gm-Message-State: AOAM5307xqK88850naNaYmkIKZsezT9Zi2IMSMIpp54PenaT1pj8Tgao ocNBuQmln+ccHnoCrvlWfTvlF6WRTS542+U4lDqpgiNp8NGomy++xFmFxGe0cxKmB7jbbZ68VOu G3VafevddhzkUH2nM/7PI/Rl7ew/LtC2i1+Ni6owfO2Obogb7mn2k+tDd2K1WaTGiZ8AltnHqiQ == X-Google-Smtp-Source: ABdhPJw4hwBi9uP+cLl3avIsIZUMZHVo9qg+xtcVCTwzEPJSxSVvnEJSMGwM4/AejzLM7jZ2aKBf7HJB9ZGgd+XKCCI= X-Received: by 2002:a25:9c06:: with SMTP id c6mr47599723ybo.403.1595975109970; Tue, 28 Jul 2020 15:25:09 -0700 (PDT) Date: Tue, 28 Jul 2020 15:24:53 -0700 In-Reply-To: <20200728222455.3023400-1-emilyshaffer@google.com> Message-Id: <20200728222455.3023400-5-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200728222455.3023400-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.28.0.rc0.142.g3c755180ce-goog Subject: [PATCH v3 4/6] 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 --- Documentation/git-hook.txt | 13 +++++++++++-- builtin/hook.c | 17 +++++++++++++---- t/t1360-config-based-hooks.sh | 12 ++++++++++++ 3 files changed, 36 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 a0759a4c26..0d92124ca6 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 46d1ed354a..ebf8f38d68 100755 --- a/t/t1360-config-based-hooks.sh +++ b/t/t1360-config-based-hooks.sh @@ -72,4 +72,16 @@ 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' ' + setup_hooks && + + cat >expected <<-EOF && + $ROOT/path/def + $ROOT/path/ghi + EOF + + git hook list --porcelain pre-commit >actual && + test_cmp expected actual +' + test_done From patchwork Tue Jul 28 22:24:54 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11690001 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 BF2EF6C1 for ; Tue, 28 Jul 2020 22:25:14 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id A35EE2070B for ; Tue, 28 Jul 2020 22:25:14 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="uu+h6A/g" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1729820AbgG1WZO (ORCPT ); Tue, 28 Jul 2020 18:25:14 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:53058 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1729808AbgG1WZM (ORCPT ); Tue, 28 Jul 2020 18:25:12 -0400 Received: from mail-pj1-x104a.google.com (mail-pj1-x104a.google.com [IPv6:2607:f8b0:4864:20::104a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 9BF08C0619D2 for ; Tue, 28 Jul 2020 15:25:12 -0700 (PDT) Received: by mail-pj1-x104a.google.com with SMTP id j11so636466pje.0 for ; Tue, 28 Jul 2020 15:25:12 -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=hE/wVrBcrLU6WTTzcQ1lC2cg0JGbTP43UeGVUpoFtZc=; b=uu+h6A/glA0271OO9MrnIfMzMsUDVWrXak7BZiAS+T5VsY1lwwpF6ceIFWyzRxQcuB 5Nsiov5B73Z3AFzty9FuaG1svRQ0lfqTAPVHUlCATakVZ0770SwL9u7ThNPeNeGcZvWL 5iMIaNSS0JdF+YuAtr6NBm8kpOC9oQ4fNxNVPvISl6vKYmo9NWmmTuaMN/50X82r1ulk a/Ukv/W48nkRk5IMHiRa7bddI1JZ3MKCAAgtwqvz0r5DuMqz2iDpMvdV/PKwOLjDeY0+ S1XM8Qd+sTNk7V0J/7dIRzTpzn5CiVWBnUOu7HB882MO68W5jAYrFRaKhtPlZYMKReo3 bpzQ== 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=hE/wVrBcrLU6WTTzcQ1lC2cg0JGbTP43UeGVUpoFtZc=; b=ggGK3eUScTHsg89ZnmcFoNljHO8Yshwkd92p8RnxL05yBXOFTTL2HitR+4b//Pbovm BWAGbS7z6JlszuQ8K3JWVABz4tSdgSGrHghg6vSikyHkYY3+JQWNsD00ZEJ+hhtORM8e snhSlTGigKAOuQuHU3P0qHVe546S/kmGfOLxW9LuWAk7P+ci1Fa1qvwJ0iddTeTv1hm9 icy38p93IrUQwwf2vziRkqpCiKZ6GodW/heGzRAqTxWKkQhjltjAoOLxyPTJpWvkGi48 x0gRodAnwrMzehw4POXodytiumx+yyytDdRYoxgRPZDqVvwGlT0InjqXqb1mW9R0vUjb A5gQ== X-Gm-Message-State: AOAM533LG+7f7qd3p1va3rawAkiNOKYz4L4rlmRKFNx8SFciYS6LR8ET vFSBBQ+SQsu6aX0mEQSfmxPlXLPx4SaIUuvJbo04rr+hHPhNX+hYoKxpTLtHuHhXCc34vhmhvYw zR6SKmy2xafC0rq8jShQ9mkoC87nM484r3N9CrUw/hhLkgJmZlapIiph387z3yz3CsxhswRbPNA == X-Google-Smtp-Source: ABdhPJwyro/YxCMmbpZBUiUPOb0E1DXaZhAOcB2qoKDMYaTplB7VWvM8Q4coVdIWyIAM/Oq+GrDYSxHqn56e6SzOoH8= X-Received: by 2002:a17:90b:3807:: with SMTP id mq7mr6054903pjb.221.1595975111917; Tue, 28 Jul 2020 15:25:11 -0700 (PDT) Date: Tue, 28 Jul 2020 15:24:54 -0700 In-Reply-To: <20200728222455.3023400-1-emilyshaffer@google.com> Message-Id: <20200728222455.3023400-6-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200728222455.3023400-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.28.0.rc0.142.g3c755180ce-goog Subject: [RFC PATCH v3 5/6] parse-options: parse into argv_array 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 parse-options already knows how to read into a string_list, and it knows how to read into an argv_array as a passthrough (that is, including the argument as well as its value). string_list and argv_array serve similar purposes but are somewhat painful to convert between; so, let's teach parse-options to read values of string arguments directly into an argv_array without preserving the argument name. This is useful if collecting generic arguments to pass through to another command, for example, 'git hook run --arg "--quiet" --arg "--format=pretty" some-hook'. The resulting argv_array would contain { "--quiet", "--format=pretty" }. The implementation is based on that of OPT_STRING_LIST. Signed-off-by: Emily Shaffer --- Documentation/technical/api-parse-options.txt | 5 +++++ parse-options-cb.c | 16 ++++++++++++++++ parse-options.h | 4 ++++ 3 files changed, 25 insertions(+) diff --git a/Documentation/technical/api-parse-options.txt b/Documentation/technical/api-parse-options.txt index 2e2e7c10c6..1e97343338 100644 --- a/Documentation/technical/api-parse-options.txt +++ b/Documentation/technical/api-parse-options.txt @@ -173,6 +173,11 @@ There are some macros to easily define options: The string argument is stored as an element in `string_list`. Use of `--no-option` will clear the list of preceding values. +`OPT_ARGV_ARRAY(short, long, &struct argv_array, arg_str, description)`:: + Introduce an option with a string argument. + The string argument is stored as an element in `argv_array`. + Use of `--no-option` will clear the list of preceding values. + `OPT_INTEGER(short, long, &int_var, description)`:: Introduce an option with integer argument. The integer is put into `int_var`. diff --git a/parse-options-cb.c b/parse-options-cb.c index 86cd393013..94c2dd397a 100644 --- a/parse-options-cb.c +++ b/parse-options-cb.c @@ -205,6 +205,22 @@ int parse_opt_string_list(const struct option *opt, const char *arg, int unset) return 0; } +int parse_opt_argv_array(const struct option *opt, const char *arg, int unset) +{ + struct argv_array *v = opt->value; + + if (unset) { + argv_array_clear(v); + return 0; + } + + if (!arg) + return -1; + + argv_array_push(v, arg); + return 0; +} + int parse_opt_noop_cb(const struct option *opt, const char *arg, int unset) { return 0; diff --git a/parse-options.h b/parse-options.h index 46af942093..e2e2de75c8 100644 --- a/parse-options.h +++ b/parse-options.h @@ -177,6 +177,9 @@ struct option { #define OPT_STRING_LIST(s, l, v, a, h) \ { OPTION_CALLBACK, (s), (l), (v), (a), \ (h), 0, &parse_opt_string_list } +#define OPT_ARGV_ARRAY(s, l, v, a, h) \ + { OPTION_CALLBACK, (s), (l), (v), (a), \ + (h), 0, &parse_opt_argv_array } #define OPT_UYN(s, l, v, h) { OPTION_CALLBACK, (s), (l), (v), NULL, \ (h), PARSE_OPT_NOARG, &parse_opt_tertiary } #define OPT_EXPIRY_DATE(s, l, v, h) \ @@ -296,6 +299,7 @@ int parse_opt_commits(const struct option *, const char *, int); int parse_opt_commit(const struct option *, const char *, int); int parse_opt_tertiary(const struct option *, const char *, int); int parse_opt_string_list(const struct option *, const char *, int); +int parse_opt_argv_array(const struct option *, const char *, int); int parse_opt_noop_cb(const struct option *, const char *, int); enum parse_opt_result parse_opt_unknown_cb(struct parse_opt_ctx_t *ctx, const struct option *, From patchwork Tue Jul 28 22:24:55 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Emily Shaffer X-Patchwork-Id: 11690005 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 3017814DD for ; Tue, 28 Jul 2020 22:25:16 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 1AADF2074F for ; Tue, 28 Jul 2020 22:25:16 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="oNQgVfMS" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1729826AbgG1WZP (ORCPT ); Tue, 28 Jul 2020 18:25:15 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:53064 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1729808AbgG1WZO (ORCPT ); Tue, 28 Jul 2020 18:25:14 -0400 Received: from mail-pl1-x64a.google.com (mail-pl1-x64a.google.com [IPv6:2607:f8b0:4864:20::64a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 5AF4AC061794 for ; Tue, 28 Jul 2020 15:25:14 -0700 (PDT) Received: by mail-pl1-x64a.google.com with SMTP id q5so12890013plr.14 for ; Tue, 28 Jul 2020 15:25:14 -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=v0BLwiA/9uAYqtO/hRYI3SGgVidfr0QrVZQpy9WB9FE=; b=oNQgVfMS4+pSu1NBhplAo7THQeMBlgkFqbjnf99AnU+56DINSCZzigkEYDEOhx+7AB ENGy1a1DTKZZnSMbyYM+cTC3XpkXZhQghC5RyRjzLq9XluskHTz6A9OPl9rSRUbp+K5j fwY9amZaj1zu+Z2lMi5fiHA9GXj8nPZZPubSbtJCyS+3exXatLhsQGcOu7MaZv/IgwXg sN9INJyELOcHAHhHUgBYnqfHbXc74RKVeDERQuUX5n5kOqrG8d2aZaS2WPiWaB7KgYOf dvRxAj13n2ecx+1eISe5ZYJjEh4/2O5ZscqJeCcOrz7JUkgcAJyTe3zHTEEGd5KgpYCE VbAA== 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=v0BLwiA/9uAYqtO/hRYI3SGgVidfr0QrVZQpy9WB9FE=; b=f+NKHfz7TF/KGJIrER/V5ETl3eFmfd31yQ6TtBkKwxOEec07yfX4a6v4PyNGq75kkO yXt6DqgfKi2OmL+7i+CeBy78eKeT2Jvt4fkjtbCsLnh+Nm0qnbbpPPBN85l/tBAWuQuW 7JkF212K8jXfykELQ2RaxaqjmZdawpDy+qheHMHTY1AFiNp8o1MEeWwRBHp02MX/D/sl 5kiDVNCgqngC0na7aUOBlVa9upjnMIx28cKhXs7LPgyt0HAa4vYgiqA1B/66YD2w7B5g Cf9a01uNeVRlRlHwPuB9MOMcbTLP8GIutpC8qAk/kBrsVqy4mJ9UbaRulHfZ0joEv5hy 3pUA== X-Gm-Message-State: AOAM532OALlw7WnwCFUxf4GoMbfwYyaiSVxV68xldz7nJ9nKUo0lyPJT y/Az8WFOkpRB54hyQm32/pRZFWvDlx1B5juGmROtCUAauhQAH80m9f28ZnoZyBydK4TAdQ2FGFd sdULxdj+Thhvg6FD2oAIWH6KEsOgoCLeq1OmbHDaW/kS1zTVehXeM8XUtzP6VA8Ovy6QSzaeBrw == X-Google-Smtp-Source: ABdhPJz4ARA2RLYZXEmAcSEx3P0TQTlHH6C0aH6iBUcNCnI7wwLXdOslMKsp3N4JlPXBsEwojLtda4oyk7545FZznM8= X-Received: by 2002:a17:90b:349:: with SMTP id fh9mr6391507pjb.73.1595975113758; Tue, 28 Jul 2020 15:25:13 -0700 (PDT) Date: Tue, 28 Jul 2020 15:24:55 -0700 In-Reply-To: <20200728222455.3023400-1-emilyshaffer@google.com> Message-Id: <20200728222455.3023400-7-emilyshaffer@google.com> Mime-Version: 1.0 References: <20200728222455.3023400-1-emilyshaffer@google.com> X-Mailer: git-send-email 2.28.0.rc0.142.g3c755180ce-goog Subject: [RFC PATCH v3 6/6] hook: add 'run' 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 In order to enable hooks to be run as an external process, by a standalone Git command, or by tools which wrap Git, provide an external means to run all configured hook commands for a given hook event. For now, the hook commands will in config order, in series. As alternate ordering or parallelism is supported in the future, we should add knobs to use those to the command line as well. As with the legacy hook implementation, all stdout generated by hook commands is redirected to stderr. Piping from stdin is not yet supported. Legacy hooks (those present in $GITDIR/hooks) are run at the end of the execution list. For now, there is no way to disable them. Users may wish to provide hook commands like 'git config hook.pre-commit.command "~/linter.sh --pre-commit"'. To enable this, the contents of the 'hook.*.command' and 'hookcmd.*.command' strings are first split by space or quotes into an argv_array, then expanded with 'expand_user_path()'. Signed-off-by: Emily Shaffer --- builtin/hook.c | 30 +++++++++++++++++++++++++ hook.c | 42 +++++++++++++++++++++++++++++++++++ hook.h | 3 +++ t/t1360-config-based-hooks.sh | 28 +++++++++++++++++++++++ 4 files changed, 103 insertions(+) diff --git a/builtin/hook.c b/builtin/hook.c index 0d92124ca6..cd61fad5fb 100644 --- a/builtin/hook.c +++ b/builtin/hook.c @@ -5,9 +5,11 @@ #include "hook.h" #include "parse-options.h" #include "strbuf.h" +#include "argv-array.h" static const char * const builtin_hook_usage[] = { N_("git hook list "), + N_("git hook run [(-e|--env)=...] [(-a|--arg)=...] "), NULL }; @@ -62,6 +64,32 @@ static int list(int argc, const char **argv, const char *prefix) return 0; } +static int run(int argc, const char **argv, const char *prefix) +{ + struct strbuf hookname = STRBUF_INIT; + struct argv_array env_argv = ARGV_ARRAY_INIT; + struct argv_array arg_argv = ARGV_ARRAY_INIT; + + struct option run_options[] = { + OPT_ARGV_ARRAY('e', "env", &env_argv, N_("var"), + N_("environment variables for hook to use")), + OPT_ARGV_ARRAY('a', "arg", &arg_argv, N_("args"), + N_("argument to pass to hook")), + OPT_END(), + }; + + argc = parse_options(argc, argv, prefix, run_options, + builtin_hook_usage, 0); + + if (argc < 1) + usage_msg_opt(_("a hookname must be provided to operate on."), + builtin_hook_usage, run_options); + + strbuf_addstr(&hookname, argv[0]); + + return run_hooks(env_argv.argv, &hookname, &arg_argv); +} + int cmd_hook(int argc, const char **argv, const char *prefix) { struct option builtin_hook_options[] = { @@ -72,6 +100,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix) if (!strcmp(argv[1], "list")) return list(argc - 1, argv + 1, prefix); + if (!strcmp(argv[1], "run")) + return run(argc - 1, argv + 1, prefix); usage_with_options(builtin_hook_usage, builtin_hook_options); } diff --git a/hook.c b/hook.c index 9dfc1a885e..902e213173 100644 --- a/hook.c +++ b/hook.c @@ -2,6 +2,7 @@ #include "hook.h" #include "config.h" +#include "run-command.h" static LIST_HEAD(hook_head); @@ -78,6 +79,7 @@ static int hook_config_lookup(const char *key, const char *value, void *hook_key struct list_head* hook_list(const struct strbuf* hookname) { struct strbuf hook_key = STRBUF_INIT; + const char *legacy_hook_path = NULL; if (!hookname) return NULL; @@ -86,5 +88,45 @@ struct list_head* hook_list(const struct strbuf* hookname) git_config(hook_config_lookup, (void*)hook_key.buf); + legacy_hook_path = find_hook(hookname->buf); + + /* TODO: check hook.runHookDir */ + if (legacy_hook_path) + emplace_hook(&hook_head, legacy_hook_path); + return &hook_head; } + +int run_hooks(const char *const *env, const struct strbuf *hookname, + const struct argv_array *args) +{ + struct list_head *to_run, *pos = NULL, *tmp = NULL; + int rc = 0; + + to_run = hook_list(hookname); + + list_for_each_safe(pos, tmp, to_run) { + struct child_process hook_proc = CHILD_PROCESS_INIT; + struct hook *hook = list_entry(pos, struct hook, list); + + /* add command */ + argv_array_push(&hook_proc.args, hook->command.buf); + + /* + * add passed-in argv, without expanding - let the user get back + * exactly what they put in + */ + if (args) + argv_array_pushv(&hook_proc.args, args->argv); + + hook_proc.env = env; + hook_proc.no_stdin = 1; + hook_proc.stdout_to_stderr = 1; + hook_proc.trace2_hook_name = hook->command.buf; + hook_proc.use_shell = 1; + + rc |= run_command(&hook_proc); + } + + return rc; +} diff --git a/hook.h b/hook.h index aaf6511cff..cf598d6ccb 100644 --- a/hook.h +++ b/hook.h @@ -1,6 +1,7 @@ #include "config.h" #include "list.h" #include "strbuf.h" +#include "argv-array.h" struct hook { @@ -10,6 +11,8 @@ struct hook }; struct list_head* hook_list(const struct strbuf *hookname); +int run_hooks(const char *const *env, const struct strbuf *hookname, + const struct argv_array *args); 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 ebf8f38d68..ee8114250d 100755 --- a/t/t1360-config-based-hooks.sh +++ b/t/t1360-config-based-hooks.sh @@ -84,4 +84,32 @@ test_expect_success 'git hook list --porcelain prints just the command' ' test_cmp expected actual ' +test_expect_success 'inline hook definitions execute oneliners' ' + test_config hook.pre-commit.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' ' + cat >~/sample-hook.sh <<-EOF && + echo \"Sample Hook\" + EOF + + test_when_finished "rm ~/sample-hook.sh" && + + chmod +x ~/sample-hook.sh && + + test_config hook.pre-commit.command "~/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_done