From: Paolo Bonzini <bonzini@gnu•org>
To: <git@vger•kernel.org>
Cc: <gitster@pobox•com>
Subject: [PATCH] add a pre-merge hook
Date: Tue, 27 Aug 2008 14:19:42 +0200 [thread overview]
Message-ID: <E1KYK3t-0000ZX-3b@fencepost.gnu.org> (raw)
In-Reply-To: <E1KW9NC-0003mM-6F@fencepost.gnu.org>
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@gnu•org>
---
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 only difference with respect to the previous version is
that the trivial merge testcase passes.
The follow-up at
http://permalink.gmane.org/gmane.comp.version-control.git/93147
is unchanged.
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_success '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
next prev parent reply other threads:[~2008-08-27 12:26 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2008-08-21 12:09 [PATCH] add a pre-merge hook Paolo Bonzini
2008-08-27 12:19 ` Paolo Bonzini [this message]
2008-08-27 18:02 ` Junio C Hamano
2008-08-27 20:05 ` Paolo Bonzini
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=E1KYK3t-0000ZX-3b@fencepost.gnu.org \
--to=bonzini@gnu$(echo .)org \
--cc=git@vger$(echo .)kernel.org \
--cc=gitster@pobox$(echo .)com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox