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.org \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.