Skip to content

Commit

Permalink
Merge branch 'js/pre-merge-commit-hook'
Browse files Browse the repository at this point in the history
A new "pre-merge-commit" hook has been introduced.

* js/pre-merge-commit-hook:
  merge: --no-verify to bypass pre-merge-commit hook
  git-merge: honor pre-merge-commit hook
  merge: do no-verify like commit
  t7503: verify proper hook execution
  • Loading branch information
gitster committed Sep 18, 2019
2 parents a2e524e + bc40ce4 commit f76bd8c
Show file tree
Hide file tree
Showing 7 changed files with 336 additions and 143 deletions.
2 changes: 1 addition & 1 deletion Documentation/git-merge.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ SYNOPSIS
--------
[verse]
'git merge' [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]
[-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]
[--no-verify] [-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]
[--[no-]allow-unrelated-histories]
[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] [<commit>...]
'git merge' (--continue | --abort | --quit)
Expand Down
22 changes: 22 additions & 0 deletions Documentation/githooks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ The default 'pre-commit' hook, when enabled--and with the
`hooks.allownonascii` config option unset or set to false--prevents
the use of non-ASCII filenames.

pre-merge-commit
~~~~~~~~~~~~~~~~

This hook is invoked by linkgit:git-merge[1], and can be bypassed
with the `--no-verify` option. It takes no parameters, and is
invoked after the merge has been carried out successfully and before
obtaining the proposed commit log message to
make a commit. Exiting with a non-zero status from this script
causes the `git merge` command to abort before creating a commit.

The default 'pre-merge-commit' hook, when enabled, runs the
'pre-commit' hook, if the latter is enabled.

This hook is invoked with the environment variable
`GIT_EDITOR=:` if the command will not bring up an editor
to modify the commit message.

If the merge cannot be carried out automatically, the conflicts
need to be resolved and the result committed separately (see
linkgit:git-merge[1]). At that point, this hook will not be executed,
but the 'pre-commit' hook will, if it is enabled.

prepare-commit-msg
~~~~~~~~~~~~~~~~~~

Expand Down
4 changes: 4 additions & 0 deletions Documentation/merge-options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ option can be used to override --squash.
+
With --squash, --commit is not allowed, and will fail.

--no-verify::
This option bypasses the pre-merge and commit-msg hooks.
See also linkgit:githooks[5].

-s <strategy>::
--strategy=<strategy>::
Use the given merge strategy; can be supplied more than
Expand Down
18 changes: 15 additions & 3 deletions builtin/merge.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ static int show_progress = -1;
static int default_to_upstream = 1;
static int signoff;
static const char *sign_commit;
static int verify_msg = 1;
static int no_verify;

static struct strategy all_strategy[] = {
{ "recursive", DEFAULT_TWOHEAD | NO_TRIVIAL },
Expand Down Expand Up @@ -287,7 +287,7 @@ static struct option builtin_merge_options[] = {
N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
OPT_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")),
OPT_BOOL(0, "signoff", &signoff, N_("add Signed-off-by:")),
OPT_BOOL(0, "verify", &verify_msg, N_("verify commit-msg hook")),
OPT_BOOL(0, "no-verify", &no_verify, N_("bypass pre-merge-commit and commit-msg hooks")),
OPT_END()
};

Expand Down Expand Up @@ -816,6 +816,18 @@ static void write_merge_heads(struct commit_list *);
static void prepare_to_commit(struct commit_list *remoteheads)
{
struct strbuf msg = STRBUF_INIT;
const char *index_file = get_index_file();

if (!no_verify && run_commit_hook(0 < option_edit, index_file, "pre-merge-commit", NULL))
abort_commit(remoteheads, NULL);
/*
* Re-read the index as pre-merge-commit hook could have updated it,
* and write it out as a tree. We must do this before we invoke
* the editor and after we invoke run_status above.
*/
if (find_hook("pre-merge-commit"))
discard_cache();
read_cache_from(index_file);
strbuf_addbuf(&msg, &merge_msg);
if (squash)
BUG("the control must not reach here under --squash");
Expand All @@ -842,7 +854,7 @@ static void prepare_to_commit(struct commit_list *remoteheads)
abort_commit(remoteheads, NULL);
}

if (verify_msg && run_commit_hook(0 < option_edit, get_index_file(),
if (!no_verify && run_commit_hook(0 < option_edit, get_index_file(),
"commit-msg",
git_path_merge_msg(the_repository), NULL))
abort_commit(remoteheads, NULL);
Expand Down
281 changes: 281 additions & 0 deletions t/t7503-pre-commit-and-pre-merge-commit-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
#!/bin/sh

test_description='pre-commit and pre-merge-commit hooks'

. ./test-lib.sh

HOOKDIR="$(git rev-parse --git-dir)/hooks"
PRECOMMIT="$HOOKDIR/pre-commit"
PREMERGE="$HOOKDIR/pre-merge-commit"

# Prepare sample scripts that write their $0 to actual_hooks
test_expect_success 'sample script setup' '
mkdir -p "$HOOKDIR" &&
write_script "$HOOKDIR/success.sample" <<-\EOF &&
echo $0 >>actual_hooks
exit 0
EOF
write_script "$HOOKDIR/fail.sample" <<-\EOF &&
echo $0 >>actual_hooks
exit 1
EOF
write_script "$HOOKDIR/non-exec.sample" <<-\EOF &&
echo $0 >>actual_hooks
exit 1
EOF
chmod -x "$HOOKDIR/non-exec.sample" &&
write_script "$HOOKDIR/require-prefix.sample" <<-\EOF &&
echo $0 >>actual_hooks
test $GIT_PREFIX = "success/"
EOF
write_script "$HOOKDIR/check-author.sample" <<-\EOF
echo $0 >>actual_hooks
test "$GIT_AUTHOR_NAME" = "New Author" &&
test "$GIT_AUTHOR_EMAIL" = "newauthor@example.com"
EOF
'

test_expect_success 'root commit' '
echo "root" >file &&
git add file &&
git commit -m "zeroth" &&
git checkout -b side &&
echo "foo" >foo &&
git add foo &&
git commit -m "make it non-ff" &&
git branch side-orig side &&
git checkout master
'

test_expect_success 'setup conflicting branches' '
test_when_finished "git checkout master" &&
git checkout -b conflicting-a master &&
echo a >conflicting &&
git add conflicting &&
git commit -m conflicting-a &&
git checkout -b conflicting-b master &&
echo b >conflicting &&
git add conflicting &&
git commit -m conflicting-b
'

test_expect_success 'with no hook' '
test_when_finished "rm -f actual_hooks" &&
echo "foo" >file &&
git add file &&
git commit -m "first" &&
test_path_is_missing actual_hooks
'

test_expect_success 'with no hook (merge)' '
test_when_finished "rm -f actual_hooks" &&
git branch -f side side-orig &&
git checkout side &&
git merge -m "merge master" master &&
git checkout master &&
test_path_is_missing actual_hooks
'

test_expect_success '--no-verify with no hook' '
test_when_finished "rm -f actual_hooks" &&
echo "bar" >file &&
git add file &&
git commit --no-verify -m "bar" &&
test_path_is_missing actual_hooks
'

test_expect_success '--no-verify with no hook (merge)' '
test_when_finished "rm -f actual_hooks" &&
git branch -f side side-orig &&
git checkout side &&
git merge --no-verify -m "merge master" master &&
git checkout master &&
test_path_is_missing actual_hooks
'

test_expect_success 'with succeeding hook' '
test_when_finished "rm -f \"$PRECOMMIT\" expected_hooks actual_hooks" &&
cp "$HOOKDIR/success.sample" "$PRECOMMIT" &&
echo "$PRECOMMIT" >expected_hooks &&
echo "more" >>file &&
git add file &&
git commit -m "more" &&
test_cmp expected_hooks actual_hooks
'

test_expect_success 'with succeeding hook (merge)' '
test_when_finished "rm -f \"$PREMERGE\" expected_hooks actual_hooks" &&
cp "$HOOKDIR/success.sample" "$PREMERGE" &&
echo "$PREMERGE" >expected_hooks &&
git checkout side &&
git merge -m "merge master" master &&
git checkout master &&
test_cmp expected_hooks actual_hooks
'

test_expect_success 'automatic merge fails; both hooks are available' '
test_when_finished "rm -f \"$PREMERGE\" \"$PRECOMMIT\"" &&
test_when_finished "rm -f expected_hooks actual_hooks" &&
test_when_finished "git checkout master" &&
cp "$HOOKDIR/success.sample" "$PREMERGE" &&
cp "$HOOKDIR/success.sample" "$PRECOMMIT" &&
git checkout conflicting-a &&
test_must_fail git merge -m "merge conflicting-b" conflicting-b &&
test_path_is_missing actual_hooks &&
echo "$PRECOMMIT" >expected_hooks &&
echo a+b >conflicting &&
git add conflicting &&
git commit -m "resolve conflict" &&
test_cmp expected_hooks actual_hooks
'

test_expect_success '--no-verify with succeeding hook' '
test_when_finished "rm -f \"$PRECOMMIT\" actual_hooks" &&
cp "$HOOKDIR/success.sample" "$PRECOMMIT" &&
echo "even more" >>file &&
git add file &&
git commit --no-verify -m "even more" &&
test_path_is_missing actual_hooks
'

test_expect_success '--no-verify with succeeding hook (merge)' '
test_when_finished "rm -f \"$PREMERGE\" actual_hooks" &&
cp "$HOOKDIR/success.sample" "$PREMERGE" &&
git branch -f side side-orig &&
git checkout side &&
git merge --no-verify -m "merge master" master &&
git checkout master &&
test_path_is_missing actual_hooks
'

test_expect_success 'with failing hook' '
test_when_finished "rm -f \"$PRECOMMIT\" expected_hooks actual_hooks" &&
cp "$HOOKDIR/fail.sample" "$PRECOMMIT" &&
echo "$PRECOMMIT" >expected_hooks &&
echo "another" >>file &&
git add file &&
test_must_fail git commit -m "another" &&
test_cmp expected_hooks actual_hooks
'

test_expect_success '--no-verify with failing hook' '
test_when_finished "rm -f \"$PRECOMMIT\" actual_hooks" &&
cp "$HOOKDIR/fail.sample" "$PRECOMMIT" &&
echo "stuff" >>file &&
git add file &&
git commit --no-verify -m "stuff" &&
test_path_is_missing actual_hooks
'

test_expect_success 'with failing hook (merge)' '
test_when_finished "rm -f \"$PREMERGE\" expected_hooks actual_hooks" &&
cp "$HOOKDIR/fail.sample" "$PREMERGE" &&
echo "$PREMERGE" >expected_hooks &&
git checkout side &&
test_must_fail git merge -m "merge master" master &&
git checkout master &&
test_cmp expected_hooks actual_hooks
'

test_expect_success '--no-verify with failing hook (merge)' '
test_when_finished "rm -f \"$PREMERGE\" actual_hooks" &&
cp "$HOOKDIR/fail.sample" "$PREMERGE" &&
git branch -f side side-orig &&
git checkout side &&
git merge --no-verify -m "merge master" master &&
git checkout master &&
test_path_is_missing actual_hooks
'

test_expect_success POSIXPERM 'with non-executable hook' '
test_when_finished "rm -f \"$PRECOMMIT\" actual_hooks" &&
cp "$HOOKDIR/non-exec.sample" "$PRECOMMIT" &&
echo "content" >>file &&
git add file &&
git commit -m "content" &&
test_path_is_missing actual_hooks
'

test_expect_success POSIXPERM '--no-verify with non-executable hook' '
test_when_finished "rm -f \"$PRECOMMIT\" actual_hooks" &&
cp "$HOOKDIR/non-exec.sample" "$PRECOMMIT" &&
echo "more content" >>file &&
git add file &&
git commit --no-verify -m "more content" &&
test_path_is_missing actual_hooks
'

test_expect_success POSIXPERM 'with non-executable hook (merge)' '
test_when_finished "rm -f \"$PREMERGE\" actual_hooks" &&
cp "$HOOKDIR/non-exec.sample" "$PREMERGE" &&
git branch -f side side-orig &&
git checkout side &&
git merge -m "merge master" master &&
git checkout master &&
test_path_is_missing actual_hooks
'

test_expect_success POSIXPERM '--no-verify with non-executable hook (merge)' '
test_when_finished "rm -f \"$PREMERGE\" actual_hooks" &&
cp "$HOOKDIR/non-exec.sample" "$PREMERGE" &&
git branch -f side side-orig &&
git checkout side &&
git merge --no-verify -m "merge master" master &&
git checkout master &&
test_path_is_missing actual_hooks
'

test_expect_success 'with hook requiring GIT_PREFIX' '
test_when_finished "rm -rf \"$PRECOMMIT\" expected_hooks actual_hooks success" &&
cp "$HOOKDIR/require-prefix.sample" "$PRECOMMIT" &&
echo "$PRECOMMIT" >expected_hooks &&
echo "more content" >>file &&
git add file &&
mkdir success &&
(
cd success &&
git commit -m "hook requires GIT_PREFIX = success/"
) &&
test_cmp expected_hooks actual_hooks
'

test_expect_success 'with failing hook requiring GIT_PREFIX' '
test_when_finished "rm -rf \"$PRECOMMIT\" expected_hooks actual_hooks fail" &&
cp "$HOOKDIR/require-prefix.sample" "$PRECOMMIT" &&
echo "$PRECOMMIT" >expected_hooks &&
echo "more content" >>file &&
git add file &&
mkdir fail &&
(
cd fail &&
test_must_fail git commit -m "hook must fail"
) &&
git checkout -- file &&
test_cmp expected_hooks actual_hooks
'

test_expect_success 'check the author in hook' '
test_when_finished "rm -f \"$PRECOMMIT\" expected_hooks actual_hooks" &&
cp "$HOOKDIR/check-author.sample" "$PRECOMMIT" &&
cat >expected_hooks <<-EOF &&
$PRECOMMIT
$PRECOMMIT
$PRECOMMIT
EOF
test_must_fail git commit --allow-empty -m "by a.u.thor" &&
(
GIT_AUTHOR_NAME="New Author" &&
GIT_AUTHOR_EMAIL="newauthor@example.com" &&
export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL &&
git commit --allow-empty -m "by new.author via env" &&
git show -s
) &&
git commit --author="New Author <newauthor@example.com>" \
--allow-empty -m "by new.author via command line" &&
git show -s &&
test_cmp expected_hooks actual_hooks
'

test_done
Loading

0 comments on commit f76bd8c

Please sign in to comment.