This hook provides a way to look at what kind of merge is invoked, and stop it. The type of merge is passed to the hook. The patch provides a sample implementation that implements a `branch.<branch-name>.allowmerges` key. It can be useful for project whose policy is "only fast forwards on the integrator's repository". Signed-off-by: Paolo Bonzini <bonzini@xxxxxxx> --- Documentation/githooks.txt | 18 +++++ builtin-merge.c | 65 +++++++++++++++---- t/t5407-pre-merge-hook.sh | 130 +++++++++++++++++++++++++++++++++++++ templates/hooks--pre-merge.sample | 49 ++++++++++++++ 4 files changed, 250 insertions(+), 12 deletions(-) create mode 100755 t/t5407-pre-merge-hook.sh create mode 100755 templates/hooks--pre-merge.sample The included testcases demonstrate that trivial merges are currently broken. The failing test is equivalent to: git init echo a > a git add a git commit -ma git checkout -b branch echo b > b git add b git commit -mb git checkout master git merge --no-ff -s resolve branch Running this in 1.5.5 shows: Trying really trivial in-index merge... Wonderful. In-index merge while `next' gives Trying really trivial in-index merge... error: Untracked working tree file 'a' would be overwritten by merge. Nope. Trying simple merge. Merge made by resolve. diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt index 046a2a7..c45602c 100644 --- a/Documentation/githooks.txt +++ b/Documentation/githooks.txt @@ -187,6 +187,24 @@ Both standard output and standard error output are forwarded to 'git-send-pack' on the other end, so you can simply `echo` messages for the user. +pre-merge +--------- + +This hook is invoked before a merge is attempted. The command +line passed to the hook can have multiple parameters. The +first parameter is the type of merge, which can be one of +"squash" (if --squash was given on the command line), "ff" +(fast-forward), "trivial" (trivial in-index merge), "merge" +(non-trivial 2-head merge), "octopus" (any other merge). After this +there is a "--" argument, followed by the commit SHA1 values for the +heads being merged. + +The hook can interrupt the merge by returning a non-zero +status. The default hook checks for a boolean configuration +key `branch.<branch-name>.allowmerges`, where `<branch-name>` +is the current branch. If the key is false, only squash or +fast-forward merges are allowed. + [[update]] update ------ diff --git a/builtin-merge.c b/builtin-merge.c index a3b9b10..e7f3ece 100644 --- a/builtin-merge.c +++ b/builtin-merge.c @@ -298,12 +298,16 @@ static void squash_message(void) strbuf_release(&out); } -static int run_hook(const char *name) +static int run_hook(const char *name, struct commit_list *heads, ...) { struct child_process hook; - const char *argv[3], *env[2]; + char **argv, *env[2]; char index[PATH_MAX]; + va_list args; + int first_remote_head, i, rc; + va_start(args, heads); + argv = xmalloc((10 + commit_list_count(remoteheads)) * sizeof(char *)); argv[0] = git_path("hooks/%s", name); if (access(argv[0], X_OK) < 0) return 0; @@ -312,19 +316,43 @@ static int run_hook(const char *name) env[0] = index; env[1] = NULL; - if (squash) - argv[1] = "1"; - else - argv[1] = "0"; - argv[2] = NULL; + i = 0; + do { + /* "--" ovewrites the last string returned by va_arg, but we + still have to reserve a slot for the final NULL in argv. */ + if (++i >= 9) + die("run_hook(): too many arguments"); + argv[i] = va_arg(args, char *); + } while (argv[i]); + va_end(args); + + if (heads) { + argv[i] = "--"; + first_remote_head = i + 1; + for (; heads; heads = heads->next) + argv[++i] = xstrdup(sha1_to_hex(heads->item->object.sha1)); + argv[++i] = NULL; + } else + first_remote_head = i; memset(&hook, 0, sizeof(hook)); - hook.argv = argv; + hook.argv = (const char **) argv; hook.no_stdin = 1; hook.stdout_to_stderr = 1; - hook.env = env; + hook.env = (const char **) env; + + rc = run_command(&hook); - return run_command(&hook); + for (i = first_remote_head; argv[i]; i++) + free (argv[i]); + free (argv); + return rc; +} + +static int run_pre_merge_hook(const char *kind) +{ + return run_hook("pre-merge", remoteheads, + squash ? "squash" : kind, "--", NULL); } static void finish(const unsigned char *new_head, const char *msg) @@ -372,7 +400,7 @@ static void finish(const unsigned char *new_head, const char *msg) } /* Run a post-merge hook */ - run_hook("post-merge"); + run_hook("post-merge", NULL, squash ? "1" : "0", NULL); strbuf_release(&reflog_message); } @@ -877,6 +905,10 @@ int cmd_merge(int argc, const char **argv, const char *prefix) remote_head = peel_to_type(argv[0], 0, NULL, OBJ_COMMIT); if (!remote_head) die("%s - not something we can merge", argv[0]); + if (run_hook("pre-merge", NULL, "ff", "--", + remote_head->sha1, NULL)) + return 1; + update_ref("initial pull", "HEAD", remote_head->sha1, NULL, 0, DIE_ON_ERR); reset_hard(remote_head->sha1, 0); @@ -974,6 +1006,9 @@ int cmd_merge(int argc, const char **argv, const char *prefix) strcpy(hex, find_unique_abbrev(head, DEFAULT_ABBREV)); + if (run_pre_merge_hook("ff")) + return 1; + printf("Updating %s..%s\n", hex, find_unique_abbrev(remoteheads->item->object.sha1, @@ -1011,8 +1046,11 @@ int cmd_merge(int argc, const char **argv, const char *prefix) git_committer_info(IDENT_ERROR_ON_NO_NAME); printf("Trying really trivial in-index merge...\n"); if (!read_tree_trivial(common->item->object.sha1, - head, remoteheads->item->object.sha1)) + head, remoteheads->item->object.sha1)) { + if (run_pre_merge_hook("trivial")) + return 1; return merge_trivial(); + } printf("Nope.\n"); } } else { @@ -1045,6 +1083,9 @@ int cmd_merge(int argc, const char **argv, const char *prefix) } } + if (run_pre_merge_hook(remoteheads->next ? "octopus" : "merge")) + return 1; + /* We are going to make a new commit. */ git_committer_info(IDENT_ERROR_ON_NO_NAME); diff --git a/t/t5407-pre-merge-hook.sh b/t/t5407-pre-merge-hook.sh new file mode 100755 index 0000000..b7029ce --- /dev/null +++ b/t/t5407-pre-merge-hook.sh @@ -0,0 +1,130 @@ +#!/bin/sh +# +# Copyright (c) 2008 Paolo Bonzini +# + +test_description='Test the pre-merge hook.' +. ./test-lib.sh + +test_expect_success setup ' + echo Data for commit0. >a && + echo a >>a && + echo b >>a && + echo c >>a && + git add a && + git commit -m"setup" && + commit0=$(git rev-parse HEAD) && + + git checkout -b branch1 && + echo Data for branch1. >b && + git add b && + git commit -m"setup branch1" && + git checkout master && + + git checkout -b branch2 && + echo Data for branch2. >c && + git add c && + git commit -m"setup branch2" && + git checkout master && + + git checkout -b branch3 && + echo Changed data for branch3. >a && + echo a >>a && + echo b >>a && + echo c >>a && + git commit -m"setup branch3" a && + git checkout master && + + git checkout -b branch4 && + echo Data for commit0. >a && + echo a >>a && + echo b >>a && + echo c for branch4 >>a && + git commit -m"setup branch4" a && + git checkout master +' + +mkdir .git/hooks +cat >.git/hooks/pre-merge <<'EOF' +#!/bin/sh +echo $# $1 $2 >> $GIT_DIR/pre-merge.args +EOF +chmod u+x .git/hooks/pre-merge + +test_expect_success 'pre-merge runs as expected ' ' + rm -f .git/pre-merge.args && + git reset --hard $commit0 && + git merge branch1 && + test -e .git/pre-merge.args +' + +test_expect_success 'pre-merge from fast-forward merge receives the right argument ' ' + test "`cat .git/pre-merge.args`" = "4 ff --" +' + +test_expect_success 'pre-merge from squash merge receives the right argument ' ' + rm -f .git/pre-merge.args && + git reset --hard $commit0 && + git merge --squash branch1 && + test "`cat .git/pre-merge.args`" = "4 squash --" +' + +test_expect_failure 'pre-merge from trivial merge receives the right argument ' ' + rm -f .git/pre-merge.args && + git checkout $(git rev-parse branch1) && + git merge --no-ff -s resolve branch1 && + test "`cat .git/pre-merge.args`" = "4 trivial --" +' + +test_expect_success 'pre-merge from real merge receives the right argument ' ' + rm -f .git/pre-merge.args && + git reset --hard branch3 && + git merge --no-ff branch4 && + test "`cat .git/pre-merge.args`" = "4 merge --" +' + +test_expect_success 'pre-merge from octopus merge receives the right argument ' ' + rm -f .git/pre-merge.args && + git reset --hard $commit0 && + git merge branch1 branch2 branch3 && + test "`cat .git/pre-merge.args`" = "6 octopus --" +' + +test_expect_success 'pre-merge into empty head receives the right argument ' ' + # Here we use a subshell so that the next tests have the right + # cwd if this test fails. + mkdir second && + (cd second && + git init && + cp ../.git/hooks/pre-merge .git/hooks/pre-merge && + chmod u+x .git/hooks/pre-merge && + git fetch .. && + git merge $commit0 && + test "`cat .git/pre-merge.args`" = "3 ff --" && + cd .. && + rm -rf second) +' + +test_expect_success 'pre-merge does not run for up-to-date ' ' + rm -f .git/pre-merge.args && + git merge $commit0 && + ! test -f .git/pre-merge.args +' + +test_expect_success 'pre-merge does not run for up-to-date octopus ' ' + rm -f .git/pre-merge.args && + git merge branch1 branch2 branch3 && + ! test -f .git/pre-merge.args +' + +cat >.git/hooks/pre-merge <<'EOF' +#!/bin/sh +exit 1 +EOF +chmod u+x .git/hooks/pre-merge + +test_expect_success 'pre-merge can stop merge ' ' + ! git merge --no-ff branch4 +' + +test_done diff --git a/templates/hooks--pre-merge.sample b/templates/hooks--pre-merge.sample new file mode 100755 index 0000000..bf2e82c --- /dev/null +++ b/templates/hooks--pre-merge.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to block merges on same branches. +# Called by git-merge with arguments: type -- head remote1 remote2... +# +# To enable this hook, rename this file to "pre-merge". +# +# Config +# ------ +# branch.<branch-name>.allowmerges +# This boolean sets whether merges will be allowed for the +# given branch in the repository. By default they will be. + +# --- Command line +type="$1" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 <type> -- <head>...)" >&2 + exit 1 +fi + +if [ -z "$type" ]; then + echo "Usage: $0 <type> -- <head>..." >&2 + exit 1 +fi + +# --- Examine repository state +head=`git symbolic-ref HEAD 2>/dev/null || echo detached` + +# --- Do the actual check +case "$head,$type" in + detached,* | *,squash | *,ff) + ;; + refs/heads/*) + allowmerges=$(git config --bool branch.${head#refs/heads/}.allowmerges) + test "$allowmerges" = false && { + echo Merge commits not allowed on this branch. + exit 1 + } + ;; + *) + ;; +esac + +# --- Finished +exit 0 -- 1.5.5 -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html