git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [RFC/PATCH Second draft] Fast forward strategies allow, never, and only

From
Sverre Hvammen Johansen <hvammen@gmail.com>
Date
Mar 26, 2008, 03:50 UTC
Message-ID
<402c10cd0803252050u582111cag18674e0257ac2884@mail.gmail.com>
In-Reply-To
<7vbq56ilnj.fsf@gitster.siamese.dyndns.org>

I have attached four patches to this email (to avoid line-wrapping issues if someone needs them for testing). They are also posted inlined for comments.

On Sat, Mar 22, 2008 at 11:49 AM, Junio C Hamano <gitster@pobox.com> wrote:
>  In a series of patches, restructuring without changing
>  semantics should come first to make existing logic cleaner and later
>  enhancements on top of it easier to follow.
The patch series consists of the following four patches:
   0001-Introduce-ff-fast-forward-option.patch
   0002-Restructuring-git-merge.sh.patch
   0003-Head-reduction-before-selecting-merge-strategy.patch
   0004-Introduce-fast-forward-option-only.patch

The first and the last one is trivial. Head reduction is more complicated. I have split this one in two as you suggested except that the one for restructuring does not come as the first on in this series of patches.

-- 
Sverre Hvammen Johansen


From 89fbd87b93017d8a65afd6fd27796c0e8f204c22 Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sun, 23 Mar 2008 19:15:52 -0800
Subject: [PATCH 1/4] Introduce -ff=<fast forward option>

--ff now takes an argument allowing --ff to be written
as --ff=allow and -no-ff to be written as --ff=never.
This change allow other fast forward options to be
introduced later.

See the documentation for a further explanation of these options.

Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
 Documentation/fast-forward-options.txt |   44 +++
 Documentation/git-merge.txt            |    6 +-
 Documentation/git-pull.txt             |    2 +
 Documentation/merge-options.txt        |    9 +-
 git-merge.sh                           |   47 ++-
 git-pull.sh                            |    4 +-
 t/t7601-merge-ff-options.sh            |  639 ++++++++++++++++++++++++++++++++
 7 files changed, 728 insertions(+), 23 deletions(-)
 create mode 100644 Documentation/fast-forward-options.txt
 create mode 100755 t/t7601-merge-ff-options.sh

diff --git a/Documentation/fast-forward-options.txt b/Documentation/fast-forward-options.txt
new file mode 100644
index 0000000..95d0e6f
--- /dev/null
+++ b/Documentation/fast-forward-options.txt
@@ -0,0 +1,44 @@
+FAST FORWARD OPTIONS
+--------------------
+
+allow::
+
+	Do not generate a merge commit if the merge resolves as a
+	fast-forward, only update the branch pointer.  This option is
+	equivalent of '--ff' without any argument.  This is the
+	default behavior.
+
+never::
+	Generate a merge commit even if the merge resolves as a
+	fast-forward.  This option is equivalent of '--no-ff'.
+
+If your workflow is always to branch from the special branch
+("master") when working on a topic and merge that back to "master", if
+you happen to have worked only on a single topic and the "master" was
+never advanced during the time you worked on that topic, merging the
+topic back to "master" will result in a fast-forward.  When you look
+back that history, you will not be able to tell where the topic
+started and ended by following the ancestry chain of the "master"
+branch.
+
+Using "never fast forward" policy on such a special branch will be a
+way to make sure that all commits on the first-parent ancestry of that
+special branch will be merges from something else.  From the history
+you can determine where the topic started and ended.
+
+The following shows two branches forked off from "master".  The branch
+"master" have merged in changes from branch "topicA" twice and
+"topicB" once:
+
+------------
+         o---o---o---o---o  topicA
+        /     \           \
+    ---*-------*-------*---*  master
+      /         \     /
+                 o---o  topicB
+------------
+
+The first merge of topicA or the only merge of topicB would have
+resulted in a fast forward without '--ff=never'.  Topic A consist of
+those commits that can be reached from master^2 without passing
+through any of the first-parent ancestries of master.
diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt
index c136b10..2af33d8 100644
--- a/Documentation/git-merge.txt
+++ b/Documentation/git-merge.txt
@@ -9,7 +9,8 @@ git-merge - Join two or more development histories together
 SYNOPSIS
 --------
 [verse]
-'git-merge' [-n] [--summary] [--no-commit] [--squash] [-s <strategy>]...
+'git-merge' [-n] [--summary] [--no-commit] [--squash]
+	[-s <strategy>]... [--ff[=<fast forward option>]]
 	[-m <msg>] <remote> <remote>...
 'git-merge' <msg> HEAD <remote>...
 
@@ -37,6 +38,9 @@ include::merge-options.txt[]
 	least one <remote>.  Specifying more than one <remote>
 	obviously means you are trying an Octopus.
 
+
+include::fast-forward-options.txt[]
+
 include::merge-strategies.txt[]
 
 
diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt
index 3405ca0..e4e013c 100644
--- a/Documentation/git-pull.txt
+++ b/Documentation/git-pull.txt
@@ -52,6 +52,8 @@ include::pull-fetch-param.txt[]
 
 include::urls-remotes.txt[]
 
+include::fast-forward-options.txt[]
+
 include::merge-strategies.txt[]
 
 DEFAULT BEHAVIOUR
diff --git a/Documentation/merge-options.txt b/Documentation/merge-options.txt
index 9f1fc82..cf4881b 100644
--- a/Documentation/merge-options.txt
+++ b/Documentation/merge-options.txt
@@ -29,12 +29,11 @@
 
 --no-ff::
 	Generate a merge commit even if the merge resolved as a
-	fast-forward.
+	fast-forward.  --no-ff is an alias for --ff=never.
 
---ff::
-	Do not generate a merge commit if the merge resolved as
-	a fast-forward, only update the branch pointer. This is
-	the default behavior of git-merge.
+--ff[=<fast forward option>]::
+	Select fast forward option.  --ff without any argument
+	is an alias for --ff=allow which is the default behavior.
 
 -s <strategy>, \--strategy=<strategy>::
 	Use the given merge strategy; can be supplied more than
diff --git a/git-merge.sh b/git-merge.sh
index 7dbbb1d..17f40f2 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -12,7 +12,7 @@ summary              show a diffstat at the end of the merge
 n,no-summary         don't show a diffstat at the end of the merge
 squash               create a single commit instead of doing a merge
 commit               perform a commit if the merge sucesses (default)
-ff                   allow fast forward (default)
+ff?                  fast forward options
 s,strategy=          merge strategy to use
 m,message=           message to be used for the merge commit (if any)
 "
@@ -35,7 +35,7 @@ no_fast_forward_strategies='subtree ours'
 no_trivial_strategies='recursive recur subtree ours'
 use_strategies=
 
-allow_fast_forward=t
+fast_forward=allow
 allow_trivial_merge=t
 squash= no_commit=
 
@@ -153,8 +153,6 @@ parse_config () {
 		--summary)
 			show_diffstat=t ;;
 		--squash)
-			test "$allow_fast_forward" = t ||
-				die "You cannot combine --squash with --no-ff."
 			squash=t no_commit=t ;;
 		--no-squash)
 			squash= no_commit= ;;
@@ -163,11 +161,26 @@ parse_config () {
 		--no-commit)
 			no_commit=t ;;
 		--ff)
-			allow_fast_forward=t ;;
+			case "$2" in
+			allow|never)
+				fast_forward=$2; shift ;;
+			-*)
+				fast_forward=allow ;;
+			*)
+				die "Available fast-forward options are: allow and newer" ;;
+			esac
+			;;
+		--ff=*)
+			fast_forward=${1#--ff=}
+			case "$fast_forward" in
+			allow|never) 
+				;;
+			*)
+				die "Available fast-forward options are: allow and newer" ;;
+			esac
+			;;
 		--no-ff)
-			test "$squash" != t ||
-				die "You cannot combine --squash with --no-ff."
-			allow_fast_forward=f ;;
+			fast_forward=never ;;
 		-s|--strategy)
 			shift
 			case " $all_strategies " in
@@ -189,6 +202,8 @@ parse_config () {
 		esac
 		shift
 	done
+	test "$fast_forward" = allow -o "$squash" = "" ||
+		die "You cannot combine --squash with --ff=never"
 	args_left=$#
 }
 
@@ -308,7 +323,7 @@ do
 	do
 		case " $s " in
 		*" $ss "*)
-			allow_fast_forward=f
+			fast_forward=never
 			break
 			;;
 		esac
@@ -334,17 +349,17 @@ case "$#" in
 esac
 echo "$head" >"$GIT_DIR/ORIG_HEAD"
 
-case "$allow_fast_forward,$#,$common,$no_commit" in
-?,*,'',*)
+case "$fast_forward,$#,$common,$no_commit" in
+*,*,'',*)
 	# No common ancestors found. We need a real merge.
 	;;
-?,1,"$1",*)
+*,1,"$1",*)
 	# If head can reach all the merge then we are up to date.
 	# but first the most common case of merging one remote.
 	finish_up_to_date "Already up-to-date."
 	exit 0
 	;;
-t,1,"$head",*)
+allow,1,"$head",*)
 	# Again the most common case of merging one remote.
 	echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $1)"
 	git update-index --refresh 2>/dev/null
@@ -359,11 +374,11 @@ t,1,"$head",*)
 	dropsave
 	exit 0
 	;;
-?,1,?*"$LF"?*,*)
+*,1,?*"$LF"?*,*)
 	# We are not doing octopus and not fast forward.  Need a
 	# real merge.
 	;;
-?,1,*,)
+*,1,*,)
 	# We are not doing octopus, not fast forward, and have only
 	# one common.
 	git update-index --refresh 2>/dev/null
@@ -481,7 +496,7 @@ done
 # auto resolved the merge cleanly.
 if test '' != "$result_tree"
 then
-    if test "$allow_fast_forward" = "t"
+    if test $fast_forward = allow
     then
         parents=$(git show-branch --independent "$head" "$@")
     else
diff --git a/git-pull.sh b/git-pull.sh
index 3ce32b5..2d7293a 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -4,7 +4,7 @@
 #
 # Fetch one or more remote refs and merge it/them into the current HEAD.
 
-USAGE='[-n | --no-summary] [--[no-]commit] [--[no-]squash] [--[no-]ff] [-s strategy]... [<fetch-options>] <repo> <head>...'
+USAGE='[-n | --no-summary] [--[no-]commit] [--[no-]squash] [--ff=<ff-strategy>] [-s strategy]... [<fetch-options>] <repo> <head>...'
 LONG_USAGE='Fetch one or more remote refs and merge it/them into the current HEAD.'
 SUBDIRECTORY_OK=Yes
 OPTIONS_SPEC=
@@ -41,6 +41,8 @@ do
 		no_ff=--ff ;;
 	--no-ff)
 		no_ff=--no-ff ;;
+	--ff=allow|--ff=never)
+		no_ff=$1 ;;
 	-s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\
 		--strateg=*|--strategy=*|\
 	-s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy)
diff --git a/t/t7601-merge-ff-options.sh b/t/t7601-merge-ff-options.sh
new file mode 100755
index 0000000..636e71e
--- /dev/null
+++ b/t/t7601-merge-ff-options.sh
@@ -0,0 +1,639 @@
+#!/bin/sh
+#
+# Copyright (c) 2008 Sverre Hvammen Johansen, based on t7600 by Lars Hjemli
+#
+
+test_description='git-merge
+
+Testing basic merge operations/option parsing.'
+
+. ./test-lib.sh
+
+cat >file <<EOF
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+EOF
+
+cat >file.1 <<EOF
+1 X
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+EOF
+
+cat >file.5 <<EOF
+1
+2
+3
+4
+5 X
+6
+7
+8
+9
+10
+11
+12
+EOF
+
+cat >file.9 <<EOF
+1
+2
+3
+4
+5
+6
+7
+8
+9 X
+10
+11
+12
+EOF
+
+cat  >result.0 <<EOF
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+EOF
+
+cat  >result.1 <<EOF
+1 X
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+EOF
+
+cat >result.1-5 <<EOF
+1 X
+2
+3
+4
+5 X
+6
+7
+8
+9
+10
+11
+12
+EOF
+
+cat >result.9 <<EOF
+1
+2
+3
+4
+5
+6
+7
+8
+9 X
+10
+11
+12
+EOF
+
+cat >result.1-5-9 <<EOF
+1 X
+2
+3
+4
+5 X
+6
+7
+8
+9 X
+10
+11
+12
+EOF
+
+cat >result.1-5-9-13 <<EOF
+1 X
+2
+3
+4
+5 X
+6
+7
+8
+9 X
+10
+11
+12
+13 x
+EOF
+
+cat >result.1-5-13 <<EOF
+1 X
+2
+3
+4
+5 X
+6
+7
+8
+9
+10
+11
+12
+13 x
+EOF
+
+cat >result.5-13 <<EOF
+1
+2
+3
+4
+5 X
+6
+7
+8
+9
+10
+11
+12
+13 x
+EOF
+
+cat >result.1-13 <<EOF
+1 X
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13 x
+EOF
+
+cat >extend <<EOF
+13 x
+EOF
+
+
+create_merge_msgs() {
+	echo "Merge commit 'c2'" >msg.1-5 &&
+	echo "Merge commit 'c2'; commit 'c3'" >msg.1-5-9 &&
+	echo "Squashed commit of the following:" >squash.1 &&
+	echo >>squash.1 &&
+	git log --no-merges ^HEAD c1 >>squash.1 &&
+	echo "Squashed commit of the following:" >squash.1-5 &&
+	echo >>squash.1-5 &&
+	git log --no-merges ^HEAD c2 >>squash.1-5 &&
+	echo "Squashed commit of the following:" >squash.1-5-9 &&
+	echo >>squash.1-5-9 &&
+	git log --no-merges ^HEAD c2 c3 >>squash.1-5-9
+}
+
+verify_diff() {
+	if ! diff -u "$1" "$2"
+	then
+		echo "$3"
+		false
+	fi
+}
+
+verify_merge() {
+	verify_diff "$2" "$1" "[OOPS] bad merge result" &&
+	if test $(git ls-files -u | wc -l) -gt 0
+	then
+		echo "[OOPS] unmerged files"
+		false
+	fi &&
+	if ! git diff --exit-code
+	then
+		echo "[OOPS] working tree != index"
+		false
+	fi &&
+	if test -n "$3"
+	then
+		git show -s --pretty=format:%s HEAD >msg.act &&
+		verify_diff "$3" msg.act "[OOPS] bad merge message"
+	fi
+}
+
+verify_head() {
+	if test "$1" != "$(git rev-parse HEAD)"
+	then
+		echo "[OOPS] HEAD != $1"
+		false
+	fi
+}
+
+verify_parents() {
+	i=1
+	while test $# -gt 0
+	do
+		if test "$1" != "$(git rev-parse HEAD^$i)"
+		then
+			echo "[OOPS] HEAD^$i != $1"
+			return 1
+		fi
+		i=$(expr $i + 1)
+		shift
+	done
+}
+
+verify_mergeheads() {
+	i=1
+	if ! test -f .git/MERGE_HEAD
+	then
+		echo "[OOPS] MERGE_HEAD is missing"
+		false
+	fi &&
+	while test $# -gt 0
+	do
+		head=$(head -n $i .git/MERGE_HEAD | tail -n 1)
+		if test "$1" != "$head"
+		then
+			echo "[OOPS] MERGE_HEAD $i != $1"
+			return 1
+		fi
+		i=$(expr $i + 1)
+		shift
+	done
+}
+
+verify_no_mergehead() {
+	if test -f .git/MERGE_HEAD
+	then
+		echo "[OOPS] MERGE_HEAD exists"
+		false
+	fi
+}
+
+
+test_expect_success 'setup' '
+	git add file &&
+	test_tick &&
+	git commit -m "commit 0" &&
+	git tag c0 &&
+	c0=$(git rev-parse HEAD) &&
+
+	cp file.1 file &&
+	git add file &&
+	test_tick &&
+	git commit -m "commit 1" &&
+	git tag c1 &&
+	c1=$(git rev-parse HEAD) &&
+	test_tick &&
+
+	git reset --hard "$c0" &&
+	cp file.5 file &&
+	git add file &&
+	git commit -m "commit 2" &&
+	test_tick &&
+	git tag c2 &&
+	c2=$(git rev-parse HEAD) &&
+
+	git reset --hard "$c0" &&
+	cp file.9 file &&
+	git add file &&
+	test_tick &&
+	git commit -m "commit 3" &&
+	git tag c3 &&
+	c3=$(git rev-parse HEAD) &&
+	test_tick &&
+
+	git reset --hard "$c1" &&
+	cat extend >>file &&
+	git add file &&
+	git commit -m "commit 4" &&
+	git tag x1 &&
+	x1=$(git rev-parse HEAD) &&
+	test_tick &&
+
+	git reset --hard "$c1" &&
+	git merge "$c2" &&
+	git tag x0 &&
+	x0=$(git rev-parse HEAD) &&
+	test_tick &&
+
+	git reset --hard "$c2" &&
+	cat extend >>file &&
+	git add file &&
+	git commit -m "commit 5" &&
+	git tag x2 &&
+	x2=$(git rev-parse HEAD) &&
+	test_tick &&
+
+	git reset --hard "$x1" &&
+	git merge "$x0" &&
+	git tag y1 &&
+	y1=$(git rev-parse HEAD) &&
+	test_tick &&
+
+	git reset --hard "$x0" &&
+	git merge "$x2" &&
+	git tag y2 &&
+	y2=$(git rev-parse HEAD) &&
+	test_tick &&
+
+	git reset --hard "$y1" &&
+	git merge "$y2" &&
+	git tag y3 &&
+	y3=$(git rev-parse HEAD) &&
+	test_tick &&
+	git reset --hard "$c0" &&
+	create_merge_msgs &&
+
+	git reset --hard x1 &&
+	git clone .git clone &&
+	git config remote.clone.url clone &&
+	git config remote.clone.fetch "+refs/heads/*:refs/remotes/clone/*" &&
+
+	(mkdir new && cd new && git init && cp ../file.9 file2 && git add file2 && test_tick && git commit -m "commit new") &&
+	git config remote.new.url new &&
+	git config remote.new.fetch "+refs/heads/*:refs/remotes/new/*"
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c0 with c1 and c2' '
+	git reset --hard c0 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c1 c2 &&
+	verify_merge file result.1-5 &&
+	verify_parents $c1 $c2
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c0, c2, c0, and c1' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c0 c2 c0 c1 &&
+	verify_merge file result.1-5 &&
+	verify_parents $c1 $c2
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge y2 with x0, c3, and c0' '
+	git reset --hard y2 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge x0 c3 c0 &&
+	verify_merge file result.1-5-9-13 &&
+	verify_parents $y2 $c3
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge x0 with y2, c3, and c0' '
+	git reset --hard x0 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge y2 c3 c0 &&
+	verify_merge file result.1-5-9-13 &&
+	verify_parents $y2 $c3
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with c2 and x1' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c2 x1 &&
+	verify_merge file result.1-5-13 &&
+	verify_parents $c2 $x1
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge x0 with c1 (--squash combined with --ff=allow)' '
+	git reset --hard x0 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c1 --squash --ff=allow &&
+	verify_merge file result.1-5 &&
+	verify_head $x0
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with c2 (--squash combined with --ff=allow)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c2 --squash --ff=allow &&
+	verify_merge file result.1-5 &&
+	verify_head $c1 &&
+	git commit &&
+	verify_parents $c1
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with x0 (--no-commit combined with --ff=allow)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge x0 --no-commit --ff=allow &&
+	verify_merge file result.1-5 &&
+	verify_parents $c1
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge x0 with c1 (--no-commit combined with --ff=allow)' '
+	git reset --hard x0 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c1 --no-commit --ff=allow &&
+	verify_merge file result.1-5 &&
+	verify_head $x0
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with c2 (--no-commit combined with --ff=allow)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c2 --no-commit --ff=allow &&
+	verify_merge file result.1-5 &&
+	verify_head $c1 &&
+	git commit &&
+	verify_parents $c1
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with x1 (pull --ff=allow)' '
+	git reset --hard c1 &&
+	test_tick &&
+	git pull --ff=allow clone refs/heads/master &&
+	verify_merge file result.1-13 &&
+	verify_head $x1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge x2 with x1 (pull --ff=allow)' '
+	git reset --hard x2 &&
+	test_tick &&
+	git pull --ff=allow clone refs/heads/master &&
+	verify_merge file result.1-5-13 &&
+	verify_parents $x2 $x1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with new repository (pull --ff=allow)' '
+	git reset --hard c1 &&
+	test_tick &&
+	git pull --ff=allow new refs/heads/master &&
+	verify_merge file result.1 &&
+	verify_merge file2 result.9
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge x0 with c1 (--squash combined with --ff=never)' '
+	git reset --hard x0 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	test_must_fail git merge c1 --squash --ff=never &&
+	verify_merge file result.1-5 &&
+	verify_head $x0
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with c2 (--squash combined with --ff=never)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	test_must_fail git merge c2 --squash --ff=never &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with x0 (--no-commit combined with --ff=never)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge x0 --no-commit --ff=never &&
+	verify_merge file result.1-5 &&
+	verify_head $c1 &&
+	git commit &&
+	verify_parents $c1
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge x0 with c1 (--no-commit combined with --ff=never)' '
+	git reset --hard x0 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c1 --no-commit --ff=never &&
+	verify_merge file result.1-5 &&
+	verify_head $x0
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with c2 (--no-commit combined with --ff=never)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c2 --no-commit --ff=never &&
+	verify_merge file result.1-5 &&
+	verify_head $c1 &&
+	git commit &&
+	verify_parents $c1
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with x1 (pull --ff=never)' '
+	git reset --hard c1 &&
+	test_tick &&
+	git pull --ff=never clone refs/heads/master &&
+	verify_merge file result.1-13 &&
+	verify_parents $c1 $x1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge x2 with x1 (pull --ff=never)' '
+	git reset --hard x2 &&
+	test_tick &&
+	git pull --ff=never clone refs/heads/master &&
+	verify_merge file result.1-5-13 &&
+	verify_parents $x2 $x1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with new repository (pull --ff=never)' '
+	git reset --hard c1 &&
+	test_tick &&
+	git pull --ff=never new refs/heads/master &&
+	verify_merge file result.1 &&
+	verify_merge file2 result.9
+'
+
+test_debug 'gitk --all'
+
+test_done
-- 
1.5.3.3



From 55d0664258c1053309514b192effd33d1db4c7a0 Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sun, 23 Mar 2008 23:19:37 -0800
Subject: [PATCH 2/4] Restructuring git-merge.sh

for preparation of new feature:

   Head reduction before selecting merge strategy

Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
 git-merge.sh |  166 ++++++++++++++++++++++++++++++----------------------------
 1 files changed, 85 insertions(+), 81 deletions(-)

diff --git a/git-merge.sh b/git-merge.sh
index 17f40f2..2acd2cc 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -207,6 +207,29 @@ parse_config () {
 	args_left=$#
 }
 
+# Find real parents
+# Set the following variables as followd:
+#   real_parents: The parents specified on the command line
+#   common:       All common ancestors or not_queried
+#   ff_head:      Fast forward of head
+find_real_parents () {
+	real_parents=$(git rev-parse "$@")
+	real_parents=${real_parents#$LF}
+	if test $# = 1
+	then
+		common=$(git merge-base --all $head "$@")
+		if test "$common" = $head
+		then
+			ff_head=$1
+		else
+			ff_head=$head
+		fi
+	else
+		common=not_queried
+		ff_head=$head
+	fi
+}
+
 test $# != 0 || usage
 
 have_message=
@@ -294,24 +317,26 @@ do
 done
 set x $remoteheads ; shift
 
+find_real_parents "$@"
+
 case "$use_strategies" in
 '')
-	case "$#" in
-	1)
-		var="`git config --get pull.twohead`"
+	case "$real_parents" in
+	?*"$LF"?*)
+		var="`git config --get pull.octopus`"
 		if test -n "$var"
 		then
 			use_strategies="$var"
 		else
-			use_strategies="$default_twohead_strategies"
+			use_strategies="$default_octopus_strategies"
 		fi ;;
 	*)
-		var="`git config --get pull.octopus`"
+		var="`git config --get pull.twohead`"
 		if test -n "$var"
 		then
 			use_strategies="$var"
 		else
-			use_strategies="$default_octopus_strategies"
+			use_strategies="$default_twohead_strategies"
 		fi ;;
 	esac
 	;;
@@ -339,87 +364,66 @@ do
 	done
 done
 
-case "$#" in
-1)
-	common=$(git merge-base --all $head "$@")
-	;;
-*)
-	common=$(git show-branch --merge-base $head "$@")
-	;;
-esac
 echo "$head" >"$GIT_DIR/ORIG_HEAD"
 
-case "$fast_forward,$#,$common,$no_commit" in
-*,*,'',*)
-	# No common ancestors found. We need a real merge.
-	;;
-*,1,"$1",*)
-	# If head can reach all the merge then we are up to date.
-	# but first the most common case of merging one remote.
-	finish_up_to_date "Already up-to-date."
-	exit 0
-	;;
-allow,1,"$head",*)
-	# Again the most common case of merging one remote.
-	echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $1)"
-	git update-index --refresh 2>/dev/null
-	msg="Fast forward"
-	if test -n "$have_message"
+if true
+then
+	if test $head = $ff_head -a "$common" = "$real_parents"
 	then
-		msg="$msg (no commit created; -m option ignored)"
-	fi
-	new_head=$(git rev-parse --verify "$1^0") &&
-	git read-tree -v -m -u --exclude-per-directory=.gitignore $head "$new_head" &&
-	finish "$new_head" "$msg" || exit
-	dropsave
-	exit 0
-	;;
-*,1,?*"$LF"?*,*)
-	# We are not doing octopus and not fast forward.  Need a
-	# real merge.
-	;;
-*,1,*,)
-	# We are not doing octopus, not fast forward, and have only
-	# one common.
-	git update-index --refresh 2>/dev/null
-	case "$allow_trivial_merge" in
-	t)
-		# See if it is really trivial.
-		git var GIT_COMMITTER_IDENT >/dev/null || exit
-		echo "Trying really trivial in-index merge..."
-		if git read-tree --trivial -m -u -v $common $head "$1" &&
-		   result_tree=$(git write-tree)
-		then
-			echo "Wonderful."
-			result_commit=$(
-				printf '%s\n' "$merge_msg" |
-				git commit-tree $result_tree -p HEAD -p "$1"
-			) || exit
-			finish "$result_commit" "In-index merge"
-			dropsave
-			exit 0
-		fi
-		echo "Nope."
-	esac
-	;;
-*)
-	# An octopus.  If we can reach all the remote we are up to date.
-	up_to_date=t
-	for remote
-	do
-		common_one=$(git merge-base --all $head $remote)
-		if test "$common_one" != "$remote"
+		finish_up_to_date "Already up-to-date."
+		exit 0
+	elif test $fast_forward != never -a $ff_head = "$real_parents"
+	then
+		echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $ff_head)"
+		git update-index --refresh 2>/dev/null
+		msg="Fast forward"
+		if test -n "$have_message"
 		then
-			up_to_date=f
-			break
+			msg="$msg (no commit created; -m option ignored)"
 		fi
-	done
-	if test "$up_to_date" = t
-	then
-		finish_up_to_date "Already up-to-date. Yeeah!"
+		new_head=$(git rev-parse --verify "$ff_head^0") &&
+		git read-tree -v -m -u --exclude-per-directory=.gitignore $head "$new_head" &&
+		finish "$new_head" "$msg" || exit
+		dropsave
 		exit 0
 	fi
+fi
+
+case "$real_parents" in
+?*"$LF"?*)
+	# We have more than one parent
+	common=$(git show-branch --merge-base $head $real_parents)
 	;;
+*)
+	# We have exactly one parent
+	test "$common" != not_queried || common=$(git merge-base --all $head $real_parents)
+	case "$common" in
+	?*"$LF"?*)
+		# We are not doing octopus and not fast forward.  Need a
+		# real merge.
+		;;
+	*)
+		git update-index --refresh 2>/dev/null
+		if test "$allow_trivial_merge" = t
+		then
+			# See if it is really trivial.
+			git var GIT_COMMITTER_IDENT >/dev/null || exit
+			echo "Trying really trivial in-index merge..."
+			if git read-tree --trivial -m -u -v $common $head $real_parents &&
+				result_tree=$(git write-tree)
+			then
+				echo "Wonderful."
+				result_commit=$(
+					printf '%s\n' "$merge_msg" |
+					git commit-tree $result_tree -p HEAD -p $real_parents
+				) || exit
+				finish "$result_commit" "In-index merge"
+				dropsave
+				exit 0
+			fi
+			echo "Nope."
+		fi ;;
+	esac ;;
 esac
 
 # We are going to make a new commit.
@@ -460,7 +464,7 @@ do
     # Remember which strategy left the state in the working tree
     wt_strategy=$strategy
 
-    git-merge-$strategy $common -- "$head_arg" "$@"
+    git-merge-$strategy $common -- "$head_arg" $real_parents
     exit=$?
     if test "$no_commit" = t && test "$exit" = 0
     then
@@ -530,7 +534,7 @@ case "$best_strategy" in
 	echo "Rewinding the tree to pristine..."
 	restorestate
 	echo "Using the $best_strategy to prepare resolving by hand."
-	git-merge-$best_strategy $common -- "$head_arg" "$@"
+	git-merge-$best_strategy $common -- "$head_arg" $real_parents
 	;;
 esac
 
-- 
1.5.3.3



From 2227b803ecfd47b2d5586ec923cb887f017f3b67 Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sun, 23 Mar 2008 23:23:52 -0800
Subject: [PATCH 3/4] Head reduction before selecting merge strategy

See the documentation for an explanation of this feature.

Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
 Documentation/git-merge.txt |   43 +++++++++++++++++++++++-
 git-merge.sh                |   76 +++++++++++++++++++++++++++++--------------
 2 files changed, 93 insertions(+), 26 deletions(-)

diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt
index 2af33d8..e94d26b 100644
--- a/Documentation/git-merge.txt
+++ b/Documentation/git-merge.txt
@@ -36,7 +36,7 @@ include::merge-options.txt[]
 <remote>::
 	Other branch head merged into our branch.  You need at
 	least one <remote>.  Specifying more than one <remote>
-	obviously means you are trying an Octopus.
+	usually means you are trying an Octopus.
 
 
 include::fast-forward-options.txt[]
@@ -133,6 +133,47 @@ merge (which is typically a fraction of the whole tree), you can
 have local modifications in your working tree as long as they do
 not overlap with what the merge updates.
 
+If more than one commit are specified for the merge, git will try to
+reduce the number of commits (real parents) by eliminating commits
+than can be reached from other commits.  The commit message will
+reflect the actual commits specified but the merge strategy will be
+selected based on the real parents, but always including `HEAD`.  The
+real parents (only including `HEAD` if it is real) are the parents
+recorded in the merge commit object.
+
+The following shows master and three topic branches.  topicB is based
+on topicA, topicA is previously branched off from master, and topicC
+is based on the current `HEAD` of master:
+
+------------
+                    o---o---o  topicB
+                   /
+          o---o---o  topicA
+         /
+    o---o---o---o---o---o  master
+                         \
+                          o---o  topicC
+------------
+
+A merger of master with topicA, topicB, and topicC will select the
+merge strategy based on the three branches master, topicB, and topicC
+(topicA is eliminated since it can be reached from topicB).  topicB
+and topicC are the only real parents and are therefore the only
+parents recorded in the merge commit object:
+
+------------
+         % git checkout master
+         % git merge topicA topicB topicC
+
+                    o---o---o  topicB
+                   /         \
+          o---o---o  topicA   \
+         /                     \
+    o---o---o---o---o---o       o  master
+                         \     /
+                          o---o  topicC
+------------
+
 When there are conflicts, these things happen:
 
 1. `HEAD` stays the same.
diff --git a/git-merge.sh b/git-merge.sh
index 2acd2cc..5398606 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -209,24 +209,41 @@ parse_config () {
 
 # Find real parents
 # Set the following variables as followd:
-#   real_parents: The parents specified on the command line
+#   real_parents: The real parents except fast forward of head
 #   common:       All common ancestors or not_queried
 #   ff_head:      Fast forward of head
 find_real_parents () {
-	real_parents=$(git rev-parse "$@")
-	real_parents=${real_parents#$LF}
-	if test $# = 1
+	if test $fast_forward = never
 	then
-		common=$(git merge-base --all $head "$@")
-		if test "$common" = $head
+		real_parents=$(git rev-parse "$@")
+		ff_head=$head
+		common=not_queried
+	else
+		if test $# = 1
 		then
-			ff_head=$1
+			common=$(git merge-base --all $head "$1")
+			if test "$common" = $head
+			then
+				real_parents=
+				ff_head=$1
+			elif test "$common" = "$1"
+			then
+				real_parents=
+				ff_head=$head
+			else
+				real_parents=$1
+				ff_head=$head
+			    
+			fi
 		else
-			ff_head=$head
+			real_parents=$(git show-branch --independent $head "$@")
+			# Here we may actually lie about which bransh is ff of head.
+			# This will preserve the order the user gave.
+			ff_head=${real_parents%%$LF*}
+			real_parents=${real_parents#$ff_head}
+			real_parents=${real_parents#$LF}
+			common=not_queried
 		fi
-	else
-		common=not_queried
-		ff_head=$head
 	fi
 }
 
@@ -319,6 +336,12 @@ set x $remoteheads ; shift
 
 find_real_parents "$@"
 
+if test -n "$real_parents"
+then
+	test $head = $ff_head ||
+		real_parents="$ff_head$LF$real_parents"
+fi
+
 case "$use_strategies" in
 '')
 	case "$real_parents" in
@@ -366,13 +389,13 @@ done
 
 echo "$head" >"$GIT_DIR/ORIG_HEAD"
 
-if true
+if test -z "$real_parents"
 then
-	if test $head = $ff_head -a "$common" = "$real_parents"
+	if test $head = $ff_head
 	then
 		finish_up_to_date "Already up-to-date."
 		exit 0
-	elif test $fast_forward != never -a $ff_head = "$real_parents"
+	elif test $fast_forward != never
 	then
 		echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $ff_head)"
 		git update-index --refresh 2>/dev/null
@@ -386,6 +409,14 @@ then
 		finish "$new_head" "$msg" || exit
 		dropsave
 		exit 0
+	else
+		real_parents="$ff_head"
+		ff_head=$head
+	fi
+else
+	if test $head != $ff_head -a $fast_forward = never
+	then
+		real_parents="$ff_head$LF$real_parents"
 	fi
 fi
 
@@ -500,17 +531,12 @@ done
 # auto resolved the merge cleanly.
 if test '' != "$result_tree"
 then
-    if test $fast_forward = allow
-    then
-        parents=$(git show-branch --independent "$head" "$@")
-    else
-        parents=$(git rev-parse "$head" "$@")
-    fi
-    parents=$(echo "$parents" | sed -e 's/^/-p /')
-    result_commit=$(printf '%s\n' "$merge_msg" | git commit-tree $result_tree $parents) || exit
-    finish "$result_commit" "Merge made by $wt_strategy."
-    dropsave
-    exit 0
+	test $head = $ff_head && real_parents="$head$LF$real_parents"
+	parents=$(echo "$real_parents" | sed -e 's/^/-p /')
+	result_commit=$(printf '%s\n' "$merge_msg" | git commit-tree $result_tree $parents) || exit
+	finish "$result_commit" "Merge made by $wt_strategy."
+	dropsave
+	exit 0
 fi
 
 # Pick the result from the best strategy and have the user fix it up.
-- 
1.5.3.3



From df159d4275d25a57898b757489f3d675e715efa3 Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sun, 23 Mar 2008 19:02:39 -0800
Subject: [PATCH 4/4] Introduce fast forward option only

This feature is needed for git integration with accurev.
See the documentation for an explanation of this feature.

Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
 Documentation/fast-forward-options.txt |    9 ++
 git-merge.sh                           |   12 +-
 git-pull.sh                            |    2 +-
 t/t7601-merge-ff-options.sh            |  214 ++++++++++++++++++++++++++++++++
 4 files changed, 231 insertions(+), 6 deletions(-)

diff --git a/Documentation/fast-forward-options.txt b/Documentation/fast-forward-options.txt
index 95d0e6f..4445b0e 100644
--- a/Documentation/fast-forward-options.txt
+++ b/Documentation/fast-forward-options.txt
@@ -12,6 +12,10 @@ never::
 	Generate a merge commit even if the merge resolves as a
 	fast-forward.  This option is equivalent of '--no-ff'.
 
+only::
+	Only allow a fast-forward.  The merge will fail unless HEAD is
+	up to date or the merge resolves as a fast-forward.
+
 If your workflow is always to branch from the special branch
 ("master") when working on a topic and merge that back to "master", if
 you happen to have worked only on a single topic and the "master" was
@@ -42,3 +46,8 @@ The first merge of topicA or the only merge of topicB would have
 resulted in a fast forward without '--ff=never'.  Topic A consist of
 those commits that can be reached from master^2 without passing
 through any of the first-parent ancestries of master.
+
+However, if the workflow require that the branch you are merging with
+is based on the current HEAD you can use "only fast forward" policy to
+enforce fast forward or a failure.  The last merge of topicA in
+the example above would have failed with '--ff=only'.
diff --git a/git-merge.sh b/git-merge.sh
index 5398606..b6c428f 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -162,21 +162,21 @@ parse_config () {
 			no_commit=t ;;
 		--ff)
 			case "$2" in
-			allow|never)
+			allow|never|only)
 				fast_forward=$2; shift ;;
 			-*)
 				fast_forward=allow ;;
 			*)
-				die "Available fast-forward options are: allow and newer" ;;
+				die "Available fast-forward options are: allow, newer, and only" ;;
 			esac
 			;;
 		--ff=*)
 			fast_forward=${1#--ff=}
 			case "$fast_forward" in
-			allow|never) 
+			allow|never|only) 
 				;;
 			*)
-				die "Available fast-forward options are: allow and newer" ;;
+				die "Available fast-forward options are: allow, newer, and only" ;;
 			esac
 			;;
 		--no-ff)
@@ -203,7 +203,7 @@ parse_config () {
 		shift
 	done
 	test "$fast_forward" = allow -o "$squash" = "" ||
-		die "You cannot combine --squash with --ff=never"
+		die "You cannot combine --squash with --ff=never or --ff=only."
 	args_left=$#
 }
 
@@ -338,6 +338,8 @@ find_real_parents "$@"
 
 if test -n "$real_parents"
 then
+	test $fast_forward = only &&
+		die "--ff=only can not handle more than one real parent"
 	test $head = $ff_head ||
 		real_parents="$ff_head$LF$real_parents"
 fi
diff --git a/git-pull.sh b/git-pull.sh
index 2d7293a..5bc84a6 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -41,7 +41,7 @@ do
 		no_ff=--ff ;;
 	--no-ff)
 		no_ff=--no-ff ;;
-	--ff=allow|--ff=never)
+	--ff=allow|--ff=only|--ff=never)
 		no_ff=$1 ;;
 	-s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\
 		--strateg=*|--strategy=*|\
diff --git a/t/t7601-merge-ff-options.sh b/t/t7601-merge-ff-options.sh
index 636e71e..ca4cc67 100755
--- a/t/t7601-merge-ff-options.sh
+++ b/t/t7601-merge-ff-options.sh
@@ -636,4 +636,218 @@ test_expect_success 'merge c1 with new repository (pull --ff=never)' '
 
 test_debug 'gitk --all'
 
+test_expect_success 'merge c0 with c1 (--ff=only overrides --no-ff)' '
+	git reset --hard c0 &&
+	git config branch.master.mergeoptions "--no-ff" &&
+	git merge --ff=only c1 &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c0 with c1 (--ff=only in config)' '
+	git reset --hard c0 &&
+	git config branch.master.mergeoptions "--ff=only" &&
+	git merge c1 &&
+	test_tick &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c0 (--ff=only in config)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "--ff=only" &&
+	git merge c0 &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (--ff=only in config)' '
+	git reset --hard c1 &&
+	test_tick &&
+	git config branch.master.mergeoptions "--ff=only" &&
+	test_must_fail git merge c2 &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c0 with c1 (--ff=only)' '
+	git reset --hard c0 &&
+	test_tick &&
+	git merge --ff=only c1 &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c0 (--ff=only)' '
+	git reset --hard c1 &&
+	test_tick &&
+	git merge --ff=only c0 &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c0 with c1 and c2 (--ff=only)' '
+	git reset --hard c0 &&
+	test_must_fail git merge --ff=only c1 c2 &&
+	verify_merge file result.0 &&
+	verify_head $c0
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c0 (--ff=only)' '
+	git reset --hard c1 &&
+	test_tick &&
+	git merge --ff=only c0 &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (--ff=only overrides --no-ff)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "--no-ff" &&
+	test_tick &&
+	test_must_fail git merge c2 --ff=only &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c0 with c1 (--no-ff overrides --ff=only)' '
+	git reset --hard c0 &&
+	git config branch.master.mergeoptions "--ff=only" &&
+	test_tick &&
+	git merge --no-ff c1 &&
+	verify_merge file result.1 &&
+	verify_parents $c0 $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (--ff owerrides --ff=only)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "--ff=only" &&
+	test_tick &&
+	git merge --ff c2 &&
+	verify_merge file result.1-5 &&
+	verify_parents $c1 $c2
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with x0 (--squash combined with --ff=only)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	test_must_fail git merge x0 --squash --ff=only &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge x0 with c1 (--squash combined with --ff=only)' '
+	git reset --hard x0 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	test_must_fail git merge c1 --squash --ff=only &&
+	verify_merge file result.1-5 &&
+	verify_head $x0
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with c2 (--squash combined with --ff=only)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	test_must_fail git merge c2 --squash --ff=only &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge c1 with x0 (--no-commit combined with --ff=only)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge x0 --no-commit --ff=only &&
+	verify_merge file result.1-5 &&
+	verify_head $x0
+'
+
+test_debug 'gitk --all'
+
+
+test_expect_success 'merge x0 with c1 (--no-commit combined with --ff=only)' '
+	git reset --hard x0 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	git merge c1 --no-commit --ff=only &&
+	verify_merge file result.1-5 &&
+	verify_head $x0
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with c2 (--no-commit combined with --ff=only)' '
+	git reset --hard c1 &&
+	git config branch.master.mergeoptions "" &&
+	test_tick &&
+	test_must_fail git merge c2 --no-commit --ff=only &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with x1 (pull --ff=only)' '
+	git reset --hard c1 &&
+	test_tick &&
+	git pull --ff=only clone refs/heads/master &&
+	verify_merge file result.1-13 &&
+	verify_head $x1
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge x2 with x1 (pull --ff=only)' '
+	git reset --hard x2 &&
+	test_tick &&
+	test_must_fail git pull --ff=only clone refs/heads/master &&
+	verify_merge file result.5-13 &&
+	verify_head $x2
+'
+
+test_debug 'gitk --all'
+
+test_expect_success 'merge c1 with new repository (pull --ff=only)' '
+	git reset --hard c1 &&
+	test_tick &&
+	test_must_fail git pull --ff=only new refs/heads/master &&
+	verify_merge file result.1 &&
+	verify_head $c1
+'
+
+test_debug 'gitk --all'
+
 test_done
-- 
1.5.3.3
Previous: Junio C HamanoNext: Sverre Hvammen Johansen
Message 23 of 27 in “Fast forward strategies allow, never, and only”
  1. Fast forward strategies allow, never, and onlySverre Hvammen Johansen, Mar 11, 2008
  2. Sverre Hvammen JohansenMar 11, 2008
  3. Ping YinMar 11, 2008
  4. Junio C HamanoMar 11, 2008
  5. Sverre Hvammen JohansenMar 12, 2008
  6. Sverre Hvammen JohansenMar 16, 2008
  7. Sverre Hvammen JohansenMar 14, 2008
  8. Jakub NarebskiMar 11, 2008
  9. Sverre Hvammen JohansenMar 12, 2008
  10. Junio C HamanoMar 12, 2008
  11. Sverre Hvammen JohansenMar 12, 2008
  12. Sverre Hvammen JohansenMar 18, 2008
  13. Ping YinMar 18, 2008
  14. Sverre Hvammen JohansenMar 18, 2008
  15. Jon LoeligerMar 18, 2008
  16. Jakub NarebskiMar 18, 2008
  17. Sverre Hvammen JohansenMar 19, 2008
  18. Jakub NarebskiMar 19, 2008
  19. Sverre Hvammen JohansenMar 20, 2008
  20. Junio C HamanoMar 19, 2008
  21. Sverre Hvammen JohansenMar 20, 2008
  22. Junio C HamanoMar 22, 2008
  23. Sverre Hvammen JohansenMar 26, 2008
  24. Sverre Hvammen JohansenMar 31, 2008
  25. Fast forward strategies allow, never, and onlySverre Hvammen Johansen, Apr 20, 2008
  26. Junio C HamanoApr 22, 2008
  27. Sverre Hvammen JohansenApr 24, 2008

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.