git-merge: honor pre-merge-commit hook

git-merge does not honor the pre-commit hook when doing automatic merge commits, and for compatibility reasons this is going to stay. Introduce a pre-merge-commit hook which is called for an automatic merge commit just like pre-commit is called for a non-automatic merge commit (or any other commit). [js: * renamed hook from "pre-merge" to "pre-merge-commit" * only discard the index if the hook is actually present * expanded githooks documentation entry * clarified that hook should write messages to stderr * squashed test changes from the original series' patch 4/4 * modified tests to follow new pattern from this series' patch 1/4 * added a test case for non-executable merge hooks * added a test case for failed merges * when testing that the merge hook did not run, make sure we actually have a merge to perform (by resetting the "side" branch to its original state). * reworded commit message ] Improved-by: Martin Ågren <martin.agren@gmail.com> Signed-off-by: Michael J Gruber <git@grubix.eu> Signed-off-by: Martin Ågren <martin.agren@gmail.com> Signed-off-by: Josh Steadmon <steadmon@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michael J Gruber committed Aug 7, 2019 at 11:57 UTC 6098817fd7f64209664c701df30096dc0f4fb876
4 files changed +129 -1
Documentation/githooks.txt
+21
@@ -103,6 +103,27 @@ The default 'pre-commit' hook, when enabled--and with the
103 `hooks.allownonascii` config option unset or set to false--prevents
104 the use of non-ASCII filenames.
105
106 +pre-merge-commit
107 +~~~~~~~~~~~~~~~~
108 +
109 +This hook is invoked by linkgit:git-merge[1]. It takes no parameters, and is
110 +invoked after the merge has been carried out successfully and before
111 +obtaining the proposed commit log message to
112 +make a commit. Exiting with a non-zero status from this script
113 +causes the `git merge` command to abort before creating a commit.
114 +
115 +The default 'pre-merge-commit' hook, when enabled, runs the
116 +'pre-commit' hook, if the latter is enabled.
117 +
118 +This hook is invoked with the environment variable
119 +`GIT_EDITOR=:` if the command will not bring up an editor
120 +to modify the commit message.
121 +
122 +If the merge cannot be carried out automatically, the conflicts
123 +need to be resolved and the result committed separately (see
124 +linkgit:git-merge[1]). At that point, this hook will not be executed,
125 +but the 'pre-commit' hook will, if it is enabled.
126 +
127 prepare-commit-msg
128 ~~~~~~~~~~~~~~~~~~
129
builtin/merge.c
+12
@@ -816,6 +816,18 @@ static void write_merge_heads(struct commit_list *);
816 static void prepare_to_commit(struct commit_list *remoteheads)
817 {
818 struct strbuf msg = STRBUF_INIT;
819 + const char *index_file = get_index_file();
820 +
821 + if (run_commit_hook(0 < option_edit, index_file, "pre-merge-commit", NULL))
822 + abort_commit(remoteheads, NULL);
823 + /*
824 + * Re-read the index as pre-merge-commit hook could have updated it,
825 + * and write it out as a tree. We must do this before we invoke
826 + * the editor and after we invoke run_status above.
827 + */
828 + if (find_hook("pre-merge-commit"))
829 + discard_cache();
830 + read_cache_from(index_file);
831 strbuf_addbuf(&msg, &merge_msg);
832 if (squash)
833 BUG("the control must not reach here under --squash");
t/t7503-pre-commit-and-pre-merge-commit-hooks.sh renamed
+83 -1
@@ -1,11 +1,12 @@
1 #!/bin/sh
2
3 -test_description='pre-commit hook'
3 +test_description='pre-commit and pre-merge-commit hooks'
4
5 . ./test-lib.sh
6
7 HOOKDIR="$(git rev-parse --git-dir)/hooks"
8 PRECOMMIT="$HOOKDIR/pre-commit"
9 +PREMERGE="$HOOKDIR/pre-merge-commit"
10
11 # Prepare sample scripts that write their $0 to actual_hooks
12 test_expect_success 'sample script setup' '
@@ -34,6 +35,30 @@ test_expect_success 'sample script setup' '
35 EOF
36 '
37
38 +test_expect_success 'root commit' '
39 + echo "root" >file &&
40 + git add file &&
41 + git commit -m "zeroth" &&
42 + git checkout -b side &&
43 + echo "foo" >foo &&
44 + git add foo &&
45 + git commit -m "make it non-ff" &&
46 + git branch side-orig side &&
47 + git checkout master
48 +'
49 +
50 +test_expect_success 'setup conflicting branches' '
51 + test_when_finished "git checkout master" &&
52 + git checkout -b conflicting-a master &&
53 + echo a >conflicting &&
54 + git add conflicting &&
55 + git commit -m conflicting-a &&
56 + git checkout -b conflicting-b master &&
57 + echo b >conflicting &&
58 + git add conflicting &&
59 + git commit -m conflicting-b
60 +'
61 +
62 test_expect_success 'with no hook' '
63 test_when_finished "rm -f actual_hooks" &&
64 echo "foo" >file &&
@@ -42,6 +67,15 @@ test_expect_success 'with no hook' '
67 test_path_is_missing actual_hooks
68 '
69
70 +test_expect_success 'with no hook (merge)' '
71 + test_when_finished "rm -f actual_hooks" &&
72 + git branch -f side side-orig &&
73 + git checkout side &&
74 + git merge -m "merge master" master &&
75 + git checkout master &&
76 + test_path_is_missing actual_hooks
77 +'
78 +
79 test_expect_success '--no-verify with no hook' '
80 test_when_finished "rm -f actual_hooks" &&
81 echo "bar" >file &&
@@ -60,6 +94,34 @@ test_expect_success 'with succeeding hook' '
94 test_cmp expected_hooks actual_hooks
95 '
96
97 +test_expect_success 'with succeeding hook (merge)' '
98 + test_when_finished "rm -f \"$PREMERGE\" expected_hooks actual_hooks" &&
99 + cp "$HOOKDIR/success.sample" "$PREMERGE" &&
100 + echo "$PREMERGE" >expected_hooks &&
101 + git checkout side &&
102 + git merge -m "merge master" master &&
103 + git checkout master &&
104 + test_cmp expected_hooks actual_hooks
105 +'
106 +
107 +test_expect_success 'automatic merge fails; both hooks are available' '
108 + test_when_finished "rm -f \"$PREMERGE\" \"$PRECOMMIT\"" &&
109 + test_when_finished "rm -f expected_hooks actual_hooks" &&
110 + test_when_finished "git checkout master" &&
111 + cp "$HOOKDIR/success.sample" "$PREMERGE" &&
112 + cp "$HOOKDIR/success.sample" "$PRECOMMIT" &&
113 +
114 + git checkout conflicting-a &&
115 + test_must_fail git merge -m "merge conflicting-b" conflicting-b &&
116 + test_path_is_missing actual_hooks &&
117 +
118 + echo "$PRECOMMIT" >expected_hooks &&
119 + echo a+b >conflicting &&
120 + git add conflicting &&
121 + git commit -m "resolve conflict" &&
122 + test_cmp expected_hooks actual_hooks
123 +'
124 +
125 test_expect_success '--no-verify with succeeding hook' '
126 test_when_finished "rm -f \"$PRECOMMIT\" actual_hooks" &&
127 cp "$HOOKDIR/success.sample" "$PRECOMMIT" &&
@@ -88,6 +150,16 @@ test_expect_success '--no-verify with failing hook' '
150 test_path_is_missing actual_hooks
151 '
152
153 +test_expect_success 'with failing hook (merge)' '
154 + test_when_finished "rm -f \"$PREMERGE\" expected_hooks actual_hooks" &&
155 + cp "$HOOKDIR/fail.sample" "$PREMERGE" &&
156 + echo "$PREMERGE" >expected_hooks &&
157 + git checkout side &&
158 + test_must_fail git merge -m "merge master" master &&
159 + git checkout master &&
160 + test_cmp expected_hooks actual_hooks
161 +'
162 +
163 test_expect_success POSIXPERM 'with non-executable hook' '
164 test_when_finished "rm -f \"$PRECOMMIT\" actual_hooks" &&
165 cp "$HOOKDIR/non-exec.sample" "$PRECOMMIT" &&
@@ -106,6 +178,16 @@ test_expect_success POSIXPERM '--no-verify with non-executable hook' '
178 test_path_is_missing actual_hooks
179 '
180
181 +test_expect_success POSIXPERM 'with non-executable hook (merge)' '
182 + test_when_finished "rm -f \"$PREMERGE\" actual_hooks" &&
183 + cp "$HOOKDIR/non-exec.sample" "$PREMERGE" &&
184 + git branch -f side side-orig &&
185 + git checkout side &&
186 + git merge -m "merge master" master &&
187 + git checkout master &&
188 + test_path_is_missing actual_hooks
189 +'
190 +
191 test_expect_success 'with hook requiring GIT_PREFIX' '
192 test_when_finished "rm -rf \"$PRECOMMIT\" expected_hooks actual_hooks success" &&
193 cp "$HOOKDIR/require-prefix.sample" "$PRECOMMIT" &&
templates/hooks--pre-merge-commit.sample new
+13
@@ -0,0 +1,13 @@
1 +#!/bin/sh
2 +#
3 +# An example hook script to verify what is about to be committed.
4 +# Called by "git merge" with no arguments. The hook should
5 +# exit with non-zero status after issuing an appropriate message to
6 +# stderr if it wants to stop the merge commit.
7 +#
8 +# To enable this hook, rename this file to "pre-merge-commit".
9 +
10 +. git-sh-setup
11 +test -x "$GIT_DIR/hooks/pre-commit" &&
12 + exec "$GIT_DIR/hooks/pre-commit"
13 +: