Teach the "git hook install all|<hook-name>" command, that can install one or all remote-suggested hooks. If a configuration option hook.promptRemoteSuggested is set, inform the user of the aforementioned command: - when cloning, and refs/remotes/origin/suggested-hooks is present in the newly cloned repo - when fetching, and refs/remotes/origin/suggested-hooks is updated - when committing, there is a remote-suggested commit-msg hook, and there is currently no commit-msg hook configured NEEDSWORK: Write a more detailed commit message once the design is finalized. Signed-off-by: Jonathan Tan <jonathantanmy@xxxxxxxxxx> --- builtin/clone.c | 12 +++ builtin/fetch.c | 17 ++++ builtin/hook.c | 17 +++- hook.c | 151 +++++++++++++++++++++++++++++- hook.h | 3 + t/t1361-remote-suggested-hooks.sh | 105 +++++++++++++++++++++ 6 files changed, 300 insertions(+), 5 deletions(-) create mode 100755 t/t1361-remote-suggested-hooks.sh diff --git a/builtin/clone.c b/builtin/clone.c index 2a2a03bf76..c2c8596aa9 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1393,6 +1393,18 @@ int cmd_clone(int argc, const char **argv, const char *prefix) branch_top.buf, reflog_msg.buf, transport, !is_local); + if (hook_should_prompt_suggestions()) { + for (ref = mapped_refs; ref; ref = ref->next) { + if (ref->peer_ref && + !strcmp(ref->peer_ref->name, + "refs/remotes/origin/suggested-hooks")) { + fprintf(stderr, _("The remote has suggested hooks in refs/remotes/origin/suggested-hooks.\n" + "Run `git hook install all` to install them.\n")); + break; + } + } + } + update_head(our_head_points_at, remote_head, reflog_msg.buf); /* diff --git a/builtin/fetch.c b/builtin/fetch.c index 769af53ca4..e86c312473 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -28,6 +28,7 @@ #include "promisor-remote.h" #include "commit-graph.h" #include "shallow.h" +#include "hook.h" #define FORCED_UPDATES_DELAY_WARNING_IN_MS (10 * 1000) @@ -1313,6 +1314,22 @@ static int consume_refs(struct transport *transport, struct ref *ref_map) ref_map); transport_unlock_pack(transport); trace2_region_leave("fetch", "consume_refs", the_repository); + + if (hook_should_prompt_suggestions()) { + struct ref *ref; + + for (ref = ref_map; ref; ref = ref->next) { + if (ref->peer_ref && + !strcmp(ref->peer_ref->name, + "refs/remotes/origin/suggested-hooks") && + oidcmp(&ref->old_oid, &ref->peer_ref->old_oid)) { + fprintf(stderr, _("The remote has updated its suggested hooks.\n")); + fprintf(stderr, _("Run 'git hook install all' to update.\n")); + break; + } + } + } + return ret; } diff --git a/builtin/hook.c b/builtin/hook.c index c79a961e80..0334fee967 100644 --- a/builtin/hook.c +++ b/builtin/hook.c @@ -139,6 +139,17 @@ static int run(int argc, const char **argv, const char *prefix) return rc; } +static int install(int argc, const char **argv, const char *prefix) +{ + if (argc < 1) + die(_("You must specify a hook event to install.")); + if (!strcmp(argv[1], "all")) + hook_update_suggested(NULL); + else + hook_update_suggested(argv[1]); + return 0; +} + int cmd_hook(int argc, const char **argv, const char *prefix) { const char *run_hookdir = NULL; @@ -152,10 +163,6 @@ int cmd_hook(int argc, const char **argv, const char *prefix) argc = parse_options(argc, argv, prefix, builtin_hook_options, builtin_hook_usage, PARSE_OPT_KEEP_UNKNOWN); - /* after the parse, we should have "<command> <hookname> <args...>" */ - if (argc < 2) - usage_with_options(builtin_hook_usage, builtin_hook_options); - git_config(git_default_config, NULL); @@ -185,6 +192,8 @@ int cmd_hook(int argc, const char **argv, const char *prefix) return list(argc, argv, prefix); if (!strcmp(argv[0], "run")) return run(argc, argv, prefix); + if (!strcmp(argv[0], "install")) + return install(argc, argv, prefix); usage_with_options(builtin_hook_usage, builtin_hook_options); } diff --git a/hook.c b/hook.c index 3ccacb72fa..32eee9abb6 100644 --- a/hook.c +++ b/hook.c @@ -4,6 +4,12 @@ #include "config.h" #include "run-command.h" #include "prompt.h" +#include "commit.h" +#include "object.h" +#include "refs.h" +#include "tree-walk.h" +#include "tree.h" +#include "streaming.h" /* * NEEDSWORK: Doesn't look like there is a list of all possible hooks; @@ -476,6 +482,60 @@ static int notify_hook_finished(int result, return 0; } +static struct tree *remote_suggested_hook_tree(int *warning_printed) +{ + struct object_id oid; + struct object *obj; + struct tree *tree; + + if (read_ref("refs/remotes/origin/suggested-hooks", &oid)) + return NULL; + + obj = parse_object(the_repository, &oid); + if (obj == NULL) { + warning(_("object pointed to by refs/remotes/origin/suggested-hooks '%s' does not exist"), + oid_to_hex(&oid)); + if (warning_printed) + *warning_printed = 1; + return NULL; + } + if (obj->type != OBJ_COMMIT) { + warning(_("object pointed to by refs/remotes/origin/suggested-hooks '%s' is not a commit"), + oid_to_hex(&oid)); + if (warning_printed) + *warning_printed = 1; + return NULL; + } + + tree = get_commit_tree((struct commit *) obj); + if (parse_tree(tree)) { + warning(_("could not parse tree")); + if (warning_printed) + *warning_printed = 1; + return NULL; + } + + return tree; +} + +static int has_suggested_hook(const char *hookname) +{ + struct tree *tree; + struct tree_desc desc; + struct name_entry entry; + + tree = remote_suggested_hook_tree(NULL); + if (!tree) + return 0; + + init_tree_desc(&desc, tree->buffer, tree->size); + while (tree_entry(&desc, &entry)) { + if (!strcmp(hookname, entry.path)) + return 1; + } + return 0; +} + int run_hooks(const char *hookname, struct run_hooks_opt *options) { struct list_head *to_run, *pos = NULL, *tmp = NULL; @@ -497,8 +557,16 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options) list_del(pos); } - if (list_empty(to_run)) + if (list_empty(to_run)) { + if (!strcmp("commit-msg", hookname)) { + if (has_suggested_hook(hookname) && + !hook_exists(hookname, HOOKDIR_USE_CONFIG)) { + fprintf(stderr, _("No commit-msg hook has been configured, but one is suggested by the remote.\n")); + fprintf(stderr, _("Run 'git hook install commit-msg' to install it.\n")); + } + } return 0; + } cb_data.head = to_run; cb_data.run_me = list_entry(to_run->next, struct hook, list); @@ -515,3 +583,84 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options) return cb_data.rc; } + +static int is_hook(const char *filename) +{ + int i; + + for (i = 0; i < hook_name_count; i++) { + if (!strcmp(filename, hook_name[i])) + return 1; + } + return 0; +} + +void hook_update_suggested(const char *hook_to_update) +{ + struct tree *tree; + int warning_printed = 0; + struct tree_desc desc; + struct name_entry entry; + struct strbuf path = STRBUF_INIT; + int hook_found = 0; + + tree = remote_suggested_hook_tree(&warning_printed); + if (!tree) { + if (!warning_printed) + warning(_("no such ref refs/remotes/origin/suggested-hooks, not updating hooks")); + return; + } + + init_tree_desc(&desc, tree->buffer, tree->size); + while (tree_entry(&desc, &entry)) { + int fd; + + if (hook_to_update && strcmp(hook_to_update, entry.path)) + /* + * We only need to update one hook, and this is not the + * hook we're looking for + */ + continue; + + if (!hook_to_update && !is_hook(entry.path)) { + warning(_("file '%s' is not a hook; ignoring"), + entry.path); + continue; + } + hook_found = 1; + if (S_ISDIR(entry.mode) || S_ISGITLINK(entry.mode)) { + warning(_("file '%s' is not an ordinary file; ignoring"), + entry.path); + continue; + } + + strbuf_reset(&path); + strbuf_git_path(&path, "hooks/%s", entry.path); + fd = open(path.buf, O_WRONLY | O_CREAT, 0755); + if (fd < 0) { + warning_errno(_("could not create file '%s'; skipping this hook"), + path.buf); + continue; + } + if (stream_blob_to_fd(fd, &entry.oid, NULL, 1)) { + warning(_("could not write to file '%s'; skipping this hook"), + path.buf); + continue; + } + close(fd); + } + strbuf_release(&path); + + if (hook_to_update && !hook_found) + warning(_("hook '%s' not found"), hook_to_update); + + return; +} + +int hook_should_prompt_suggestions(void) +{ + int dest = 0; + + return !git_config_get_bool("hook.promptremotesuggested", &dest) && + dest; +} diff --git a/hook.h b/hook.h index d902166408..438bf7122e 100644 --- a/hook.h +++ b/hook.h @@ -140,3 +140,6 @@ int run_hooks(const char *hookname, struct run_hooks_opt *options); void free_hook(struct hook *ptr); /* Empties the list at 'head', calling 'free_hook()' on each entry */ void clear_hook_list(struct list_head *head); + +void hook_update_suggested(const char *hook_to_update); +int hook_should_prompt_suggestions(void); diff --git a/t/t1361-remote-suggested-hooks.sh b/t/t1361-remote-suggested-hooks.sh new file mode 100755 index 0000000000..223c65ac99 --- /dev/null +++ b/t/t1361-remote-suggested-hooks.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +test_description='remote-suggested hooks' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +setup_server () { + git init server && + test_when_finished "rm -rf server" && + cat >server/commit-msg <<-'EOF' && + echo "overwrite the commit message" >$1 +EOF + git -C server add commit-msg && + test_commit -C server commit-with-hook && + git -C server checkout -b suggested-hooks +} + +add_hook_to_server () { + >server/pre-commit && + git -C server add pre-commit && + test_commit -C server additional-hook +} + +test_expect_success 'suggestion upon clone' ' + setup_server && + test_when_finished "rm -rf client" && + git -c hook.promptRemoteSuggested=1 clone server client 2>err && + grep "The remote has suggested hooks" err +' + +test_expect_success 'no suggestion upon clone if not configured' ' + setup_server && + test_when_finished "rm -rf client" && + git clone server client 2>err && + ! grep "The remote has suggested hooks" err +' + +test_expect_success 'suggestion upon fetch if server has updated hooks' ' + setup_server && + git clone server client && + test_when_finished "rm -rf client" && + add_hook_to_server && + + git -C client -c hook.promptRemoteSuggested=1 fetch 2>err && + grep "The remote has updated its suggested hooks" err +' + +test_expect_success 'no suggestion upon fetch if not configured' ' + setup_server && + git clone server client && + test_when_finished "rm -rf client" && + add_hook_to_server && + + git -C client fetch 2>err && + ! grep "The remote has updated its suggested hooks" err +' + +test_expect_success 'no suggestion upon fetch if server has not updated hooks' ' + setup_server && + git clone server client && + test_when_finished "rm -rf client" && + git -C server checkout main && + test_commit -C server not-a-hook-update && + + git -C client -c hook.promptRemoteSuggested=1 fetch 2>err && + ! grep "The remote has updated its suggested hooks" err +' + +test_expect_success 'suggest commit-msg hook upon commit' ' + setup_server && + git clone server client && + test_when_finished "rm -rf client" && + + test_commit -C client foo 2>err && + grep "Run .git hook install commit-msg" err +' + +test_expect_success 'install one suggested hook' ' + setup_server && + git clone server client && + test_when_finished "rm -rf client" && + + git -C client hook install commit-msg && + + # Check that the hook was written by making a commit + test_commit -C client bar && + git -C client show >commit && + grep "overwrite the commit message" commit +' + +test_expect_success 'install all suggested hooks' ' + setup_server && + add_hook_to_server && + git clone server client && + test_when_finished "rm -rf client" && + + git -C client hook install all && + test -f client/.git/hooks/pre-commit && + test -f client/.git/hooks/commit-msg +' + +test_done -- 2.32.0.402.g57bb445576-goog