From: Orgad Shaneh <orgads@xxxxxxxxx> Let hooks receive user input if applicable. Closing stdin originates in f5bbc3225 (Port git commit to C, 2007). Looks like the original shell implementation did have stdin open. This allows for example prompting the user to choose an issue in prepare-commit-msg, and add "Fixes #123" to the commit message. Another possible use-case is running sanity test on pre-commit, and having a prompt like "This and that issue were found in your changes. Are you sure you want to commit? [Y/N]". Allow stdin only for commit-related hooks. Some of the other hooks pass their own input to the hook, so don't change them. Note: If pre-commit reads from stdin, and git commit is executed with -F - (read message from stdin), the message is not read correctly. This is fixed in the follow-up commit. Signed-off-by: Orgad Shaneh <orgads@xxxxxxxxx> --- commit.c | 2 +- run-command.c | 6 +-- run-command.h | 17 +++++-- ...3-pre-commit-and-pre-merge-commit-hooks.sh | 46 ++++++++++++++++++- t/t7504-commit-msg-hook.sh | 15 ++++++ t/t7505-prepare-commit-msg-hook.sh | 14 ++++++ 6 files changed, 90 insertions(+), 10 deletions(-) diff --git a/commit.c b/commit.c index fe1fa3dc41..775019ec9d 100644 --- a/commit.c +++ b/commit.c @@ -1646,7 +1646,7 @@ int run_commit_hook(int editor_is_used, const char *index_file, strvec_push(&hook_env, "GIT_EDITOR=:"); va_start(args, name); - ret = run_hook_ve(hook_env.v, name, args); + ret = run_hook_ve(hook_env.v, RUN_HOOK_ALLOW_STDIN, name, args); va_end(args); strvec_clear(&hook_env); diff --git a/run-command.c b/run-command.c index 2ee59acdc8..38ce53bee5 100644 --- a/run-command.c +++ b/run-command.c @@ -1343,7 +1343,7 @@ const char *find_hook(const char *name) return path.buf; } -int run_hook_ve(const char *const *env, const char *name, va_list args) +int run_hook_ve(const char *const *env, unsigned flags, const char *name, va_list args) { struct child_process hook = CHILD_PROCESS_INIT; const char *p; @@ -1356,7 +1356,7 @@ int run_hook_ve(const char *const *env, const char *name, va_list args) while ((p = va_arg(args, const char *))) strvec_push(&hook.args, p); hook.env = env; - hook.no_stdin = 1; + hook.no_stdin = !(flags & RUN_HOOK_ALLOW_STDIN); hook.stdout_to_stderr = 1; hook.trace2_hook_name = name; @@ -1369,7 +1369,7 @@ int run_hook_le(const char *const *env, const char *name, ...) int ret; va_start(args, name); - ret = run_hook_ve(env, name, args); + ret = run_hook_ve(env, 0, name, args); va_end(args); return ret; diff --git a/run-command.h b/run-command.h index 6472b38bde..e613e5e3f9 100644 --- a/run-command.h +++ b/run-command.h @@ -201,22 +201,29 @@ int run_command(struct child_process *); */ const char *find_hook(const char *name); +#define RUN_HOOK_ALLOW_STDIN 1 + /** * Run a hook. - * The first argument is a pathname to an index file, or NULL - * if the hook uses the default index file or no index is needed. - * The second argument is the name of the hook. + * The env argument is an array of environment variables, or NULL + * if the hook uses the default environment and doesn't require + * additional variables. + * The flags argument is an OR'ed collection of feature bits like + * RUN_HOOK_ALLOW_STDIN defined above, which enables + * stdin for the child process (the default is no_stdin). + * The name argument is the name of the hook. * The further arguments correspond to the hook arguments. * The last argument has to be NULL to terminate the arguments list. * If the hook does not exist or is not executable, the return * value will be zero. * If it is executable, the hook will be executed and the exit * status of the hook is returned. - * On execution, .stdout_to_stderr and .no_stdin will be set. + * On execution, .stdout_to_stderr will be set, and .no_stdin will be + * set unless RUN_HOOK_ALLOW_STDIN flag is requested. */ LAST_ARG_MUST_BE_NULL int run_hook_le(const char *const *env, const char *name, ...); -int run_hook_ve(const char *const *env, const char *name, va_list args); +int run_hook_ve(const char *const *env, unsigned flags, const char *name, va_list args); /* * Trigger an auto-gc diff --git a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh index b3485450a2..7bfb7435c6 100755 --- a/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh +++ b/t/t7503-pre-commit-and-pre-merge-commit-hooks.sh @@ -7,6 +7,7 @@ test_description='pre-commit and pre-merge-commit hooks' HOOKDIR="$(git rev-parse --git-dir)/hooks" PRECOMMIT="$HOOKDIR/pre-commit" PREMERGE="$HOOKDIR/pre-merge-commit" +POSTCOMMIT="$HOOKDIR/post-commit" # Prepare sample scripts that write their $0 to actual_hooks test_expect_success 'sample script setup' ' @@ -28,11 +29,14 @@ test_expect_success 'sample script setup' ' echo $0 >>actual_hooks test $GIT_PREFIX = "success/" EOF - write_script "$HOOKDIR/check-author.sample" <<-\EOF + write_script "$HOOKDIR/check-author.sample" <<-\EOF && echo $0 >>actual_hooks test "$GIT_AUTHOR_NAME" = "New Author" && test "$GIT_AUTHOR_EMAIL" = "newauthor@xxxxxxxxxxx" EOF + write_script "$HOOKDIR/user-input.sample" <<-\EOF + ! read -r line || echo "$line" >hook_input + EOF ' test_expect_success 'root commit' ' @@ -278,4 +282,44 @@ test_expect_success 'check the author in hook' ' test_cmp expected_hooks actual_hooks ' +test_expect_success 'with user input' ' + test_when_finished "rm -f \"$PRECOMMIT\" user_input hook_input" && + cp "$HOOKDIR/user-input.sample" "$PRECOMMIT" && + echo "user input" >user_input && + echo "more" >>file && + git add file && + git commit -m "more" <user_input && + test_cmp user_input hook_input +' + +test_expect_failure 'with user input combined with -F -' ' + test_when_finished "rm -f \"$PRECOMMIT\" user_input hook_input" && + cp "$HOOKDIR/user-input.sample" "$PRECOMMIT" && + echo "user input" >user_input && + echo "more" >>file && + git add file && + git commit -F - <user_input && + ! test_path_is_file hook_input +' + +test_expect_success 'post-commit with user input' ' + test_when_finished "rm -f \"$POSTCOMMIT\" user_input hook_input" && + cp "$HOOKDIR/user-input.sample" "$POSTCOMMIT" && + echo "user input" >user_input && + echo "more" >>file && + git add file && + git commit -m "more" <user_input && + test_cmp user_input hook_input +' + +test_expect_success 'with user input (merge)' ' + test_when_finished "rm -f \"$PREMERGE\" user_input hook_input" && + cp "$HOOKDIR/user-input.sample" "$PREMERGE" && + echo "user input" >user_input && + git checkout side && + git merge -m "merge master" master <user_input && + git checkout master && + test_cmp user_input hook_input +' + test_done diff --git a/t/t7504-commit-msg-hook.sh b/t/t7504-commit-msg-hook.sh index 31b9c6a2c1..aa76eb7e1f 100755 --- a/t/t7504-commit-msg-hook.sh +++ b/t/t7504-commit-msg-hook.sh @@ -294,5 +294,20 @@ test_expect_success 'hook is called for reword during `rebase -i`' ' ' +# now a hook that accepts input and writes it as the commit message +cat >"$HOOK" <<'EOF' +#!/bin/sh +! read -r line || echo "$line" >"$1" +EOF +chmod +x "$HOOK" + +test_expect_success 'hook with user input' ' + + echo "additional" >>file && + git add file && + echo "user input" | git commit -m "additional" && + commit_msg_is "user input" + +' test_done diff --git a/t/t7505-prepare-commit-msg-hook.sh b/t/t7505-prepare-commit-msg-hook.sh index 94f85cdf83..aa9c9375e6 100755 --- a/t/t7505-prepare-commit-msg-hook.sh +++ b/t/t7505-prepare-commit-msg-hook.sh @@ -91,6 +91,11 @@ else fi test "$GIT_EDITOR" = : && source="$source (no editor)" +if read -r line +then + source="$source $line" +fi + if test $rebasing = 1 then echo "$source $(get_last_cmd)" >"$1" @@ -113,6 +118,15 @@ test_expect_success 'with hook (-m)' ' ' +test_expect_success 'with hook (-m and input)' ' + + echo "more" >>file && + git add file && + echo "user input" | git commit -m "more" && + test "$(git log -1 --pretty=format:%s)" = "message (no editor) user input" + +' + test_expect_success 'with hook (-m editor)' ' echo "more" >> file && -- gitgitgadget