* [PATCH] Fast forward strategies allow, never, and only
@ 2008-05-12 5:17 Sverre Hvammen Johansen
2008-05-12 12:13 ` Miklos Vajna
0 siblings, 1 reply; 4+ messages in thread
From: Sverre Hvammen Johansen @ 2008-05-12 5:17 UTC (permalink / raw)
To: git
[-- Attachment #1: Type: text/plain, Size: 810 bytes --]
I am hereby sending an updated version of these patches as
attachments to this mail. The patch series consists of the
following six patches:
0001-Documentation-for-joining-more-than-two-histories.patch
0002-New-merge-tests.patch
0003-Introduce-ff-fast-forward-option.patch
0004-Restructure-git-merge.sh.patch
0005-Head-reduction-before-selecting-merge-strategy.patch
0006-Introduce-fast-forward-option-only.patch
The first patch add some documentation, for how git currently handles
merges where more than one remote is involved. The second patch add
some tests. The third, fifth, and sixth adds new features and they are all
trivial. The fourth patch computes the reduced parents but uses it only
to determine whether we are up-to-date or doing a fast forward.
--
Sverre Hvammen Johansen
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Documentation-for-joining-more-than-two-histories.patch --]
[-- Type: text/x-patch; name=0001-Documentation-for-joining-more-than-two-histories.patch, Size: 2060 bytes --]
From 781b3c10294db46e91d633c85a15fc2e1e1093b1 Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sun, 11 May 2008 17:19:12 -0700
Subject: [PATCH 1/6] Documentation for joining more than two histories
Added some documentation for how git-merge currently
works when more than two histories are joined.
Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
Documentation/git-merge.txt | 36 ++++++++++++++++++++++++++++++++++++
1 files changed, 36 insertions(+), 0 deletions(-)
diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt
index ef1f055..aa56512 100644
--- a/Documentation/git-merge.txt
+++ b/Documentation/git-merge.txt
@@ -151,6 +151,42 @@ After seeing a conflict, you can do two things:
should be, and run `git-commit` to commit the result.
+JOINING MORE THAN TWO HISTORIES
+-------------------------------
+
+More than one remote may be specified on the command line. Those
+remotes are used for selecting the merge startegy and is also used in
+the merge commit message. However, some of these remotes may not be
+independent. Only remotes with independent heads (reduced parents)
+will be recorded in the merge commit object.
+
+The following shows master and two topic branches. topicB is based
+on topicA, topicA is previously branched off from master:
+
+------------
+ o---o---o topicB
+ /
+ o---o---o topicA
+ /
+ o---o---o---o---o---o---o master
+
+------------
+
+Merging topicA and topicB to the master branch will select the merge
+strategy based on all three branches (an Octopus). master and topicB
+are the reduced parents and are therefore the only parents recorded in
+the merge commit object:
+
+------------
+
+ o---o---o topicB
+ / \
+ o---o---o topicA o master
+ / /
+ o---o---o---o---o---o---o
+
+------------
+
SEE ALSO
--------
linkgit:git-fmt-merge-msg[1], linkgit:git-pull[1],
--
1.5.5.54.gc6550
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-New-merge-tests.patch --]
[-- Type: text/x-patch; name=0002-New-merge-tests.patch, Size: 7510 bytes --]
From 38fe432f824a1c6d225cc0cb17c65a9374fe948c Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sat, 3 May 2008 21:37:15 -0700
Subject: [PATCH 2/6] New merge tests
Introduce new merge tests for preparation of new features:
--ff=<fast forward option>
Head reduction
--ff=only
Set up the framework (based on t7600) and add some test cases
that should succeed independent of the new features.
Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
t/t7601-merge-ff-options.sh | 477 +++++++++++++++++++++++++++++++++++++++++++
1 files changed, 477 insertions(+), 0 deletions(-)
create mode 100755 t/t7601-merge-ff-options.sh
diff --git a/t/t7601-merge-ff-options.sh b/t/t7601-merge-ff-options.sh
new file mode 100755
index 0000000..e499bf5
--- /dev/null
+++ b/t/t7601-merge-ff-options.sh
@@ -0,0 +1,477 @@
+#!/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
+}
+
+# Setup with the following commits:
+#
+# y3
+# / \
+# / \
+# y1 y2
+# | \ / |
+# | \ / |
+# x1 x0 x2
+# | / \ |
+# | / \ |
+# c1 c2
+# \ /
+# \ /
+# c0---c3
+#
+
+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 c1 with c0 and c0' '
+ git reset --hard c1 &&
+ git config branch.master.mergeoptions "" &&
+ test_tick &&
+ git merge c0 c0 &&
+ verify_merge file result.1 &&
+ verify_head $c1
+'
+
+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_done
--
1.5.5.54.gc6550
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-Introduce-ff-fast-forward-option.patch --]
[-- Type: text/x-patch; name=0003-Introduce-ff-fast-forward-option.patch, Size: 13899 bytes --]
From b763dd493e5cea45ffecdeb11561deb6b0732aea Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sat, 3 May 2008 17:02:52 -0700
Subject: [PATCH 3/6] 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 | 188 ++++++++++++++++++++++++++++++++
7 files changed, 277 insertions(+), 23 deletions(-)
create mode 100644 Documentation/fast-forward-options.txt
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 aa56512..7e1cb7e 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] [--stat] [--no-commit] [--squash] [-s <strategy>]...
+'git-merge' [-n] [--stat] [--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 66304f0..eb816bc 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 f37a776..8f87758 100644
--- a/Documentation/merge-options.txt
+++ b/Documentation/merge-options.txt
@@ -42,12 +42,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 69b35d8..91fada7 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -16,7 +16,7 @@ log add list of one-line log to merge commit message
no-log don't add list of one-line log to merge commit message
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)
"
@@ -39,7 +39,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= log_arg=
@@ -159,8 +159,6 @@ parse_config () {
--log|--no-log)
log_arg=$1 ;;
--squash)
- test "$allow_fast_forward" = t ||
- die "You cannot combine --squash with --no-ff."
squash=t no_commit=t ;;
--no-squash)
squash= no_commit= ;;
@@ -169,11 +167,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
@@ -195,6 +208,8 @@ parse_config () {
esac
shift
done
+ test "$fast_forward" = allow -o "$squash" = "" ||
+ die "You cannot combine --squash with --ff=never"
args_left=$#
}
@@ -315,7 +330,7 @@ do
do
case " $s " in
*" $ss "*)
- allow_fast_forward=f
+ fast_forward=never
break
;;
esac
@@ -341,17 +356,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
@@ -366,11 +381,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
@@ -488,7 +503,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 bf0c298..9e91e75 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-stat] [--[no-]commit] [--[no-]squash] [--[no-]ff] [-s strategy]... [<fetch-options>] <repo> <head>...'
+USAGE='[-n | --no-stat] [--[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
index e499bf5..c7c6d14 100755
--- a/t/t7601-merge-ff-options.sh
+++ b/t/t7601-merge-ff-options.sh
@@ -474,4 +474,192 @@ test_expect_success 'merge c1 with c2 and 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.5.54.gc6550
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Restructure-git-merge.sh.patch --]
[-- Type: text/x-patch; name=0004-Restructure-git-merge.sh.patch, Size: 7114 bytes --]
From 265bdb11f363e9c4e23d8129c31dfc506d008303 Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sat, 3 May 2008 16:30:11 -0700
Subject: [PATCH 4/6] Restructure git-merge.sh
Restructure git-merge.sh for preparation of new feature:
Head reduction before selecting merge strategy
Some aspects of this patch does not make much sense without
the next patch in this series.
Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
git-merge.sh | 186 +++++++++++++++++++++++++++++++++-------------------------
1 files changed, 105 insertions(+), 81 deletions(-)
diff --git a/git-merge.sh b/git-merge.sh
index 91fada7..2c9f19f 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -213,6 +213,47 @@ parse_config () {
args_left=$#
}
+# Find reduced parents
+# The following variables are set as follow:
+# reduced_parents: The reduced parents of those specified on the command line.
+# However, the actual parents are included if we never ff.
+# common: All common ancestors or not_queried
+# ff_head: Head or an reduced parent that may be a candidate for fast forward
+find_reduced_parents () {
+ if test $fast_forward = never
+ then
+ reduced_parents=$(git rev-parse "$@")
+ ff_head=$head
+ common=not_queried
+ else
+ if test $# = 1
+ then
+ common=$(git merge-base --all $head "$1")
+ if test "$common" = $head
+ then
+ reduced_parents=
+ ff_head=$1
+ elif test "$common" = "$1"
+ then
+ reduced_parents=
+ ff_head=$head
+ else
+ reduced_parents=$1
+ ff_head=$head
+
+ fi
+ else
+ reduced_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=${reduced_parents%%$LF*}
+ reduced_parents=${reduced_parents#$ff_head}
+ reduced_parents=${reduced_parents#$LF}
+ common=not_queried
+ fi
+ fi
+}
+
test $# != 0 || usage
have_message=
@@ -301,24 +342,28 @@ do
done
set x $remoteheads ; shift
+find_reduced_parents "$@"
+
+actual_parents=$(git rev-parse "$@")
+
case "$use_strategies" in
'')
- case "$#" in
- 1)
- var="`git config --get pull.twohead`"
+ case "$actual_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
;;
@@ -346,87 +391,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 test -z "$reduced_parents"
+then
+ if test $head = $ff_head
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
+ 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 "$actual_parents" in
+?*"$LF"?*)
+ # We have more than one actual parent
+ common=$(git show-branch --merge-base $head $actual_parents)
;;
+*)
+ # We have exactly one actual parent
+ test "$common" != not_queried || common=$(git merge-base --all $head $actual_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 $actual_parents &&
+ result_tree=$(git write-tree)
+ then
+ echo "Wonderful."
+ result_commit=$(
+ printf '%s\n' "$merge_msg" |
+ git commit-tree $result_tree -p HEAD -p $actual_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.
@@ -467,7 +491,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" $actual_parents
exit=$?
if test "$no_commit" = t && test "$exit" = 0
then
@@ -537,7 +561,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" $actual_parents
;;
esac
--
1.5.5.54.gc6550
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-Head-reduction-before-selecting-merge-strategy.patch --]
[-- Type: text/x-patch; name=0005-Head-reduction-before-selecting-merge-strategy.patch, Size: 6474 bytes --]
From 1984eb35a19f2dbbae40d91f36f77d723767c6ab Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sat, 3 May 2008 21:08:59 -0700
Subject: [PATCH 5/6] Head reduction before selecting merge strategy
This commit uses the reduced parents instead of the actual
parents specified for selecting the merge strategy when doing
a merge. The recorded parents and the commit message stays
the same.
Some cases where we before needed an octopus strategy may now be
done using a two head strategy.
See the documentation for further details.
Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
Documentation/git-merge.txt | 20 +++++++++-------
git-merge.sh | 50 ++++++++++++++++++++++++-------------------
2 files changed, 39 insertions(+), 31 deletions(-)
diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt
index 7e1cb7e..11d53e6 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[]
@@ -158,11 +158,12 @@ After seeing a conflict, you can do two things:
JOINING MORE THAN TWO HISTORIES
-------------------------------
-More than one remote may be specified on the command line. Those
-remotes are used for selecting the merge startegy and is also used in
-the merge commit message. However, some of these remotes may not be
-independent. Only remotes with independent heads (reduced parents)
-will be recorded in the merge commit object.
+More than one remote may be specified on the command line. Some of
+these remotes may not have independent heads. Git will only use the
+remotes with independent heads (reduced parents) for selecting the
+merge startegy. The reduced parents are the parents recorded in the
+merge commit object. However, the commit message will reflect the
+remotes specified on the command line.
The following shows master and two topic branches. topicB is based
on topicA, topicA is previously branched off from master:
@@ -177,9 +178,10 @@ on topicA, topicA is previously branched off from master:
------------
Merging topicA and topicB to the master branch will select the merge
-strategy based on all three branches (an Octopus). master and topicB
-are the reduced parents and are therefore the only parents recorded in
-the merge commit object:
+strategy based on the two independent branch heads, tip of the master
+branch and the tip of the topicB branch (a two head strategy). The
+commits are also the reduced parents recorded in the merge commit
+object:
------------
diff --git a/git-merge.sh b/git-merge.sh
index 2c9f19f..f080986 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -344,11 +344,16 @@ set x $remoteheads ; shift
find_reduced_parents "$@"
-actual_parents=$(git rev-parse "$@")
+# ff_head may be included here or later in actual parents
+if test -n "$reduced_parents"
+then
+ test $head = $ff_head ||
+ reduced_parents="$ff_head$LF$reduced_parents"
+fi
case "$use_strategies" in
'')
- case "$actual_parents" in
+ case "$reduced_parents" in
?*"$LF"?*)
var="`git config --get pull.octopus`"
if test -n "$var"
@@ -413,17 +418,23 @@ then
finish "$new_head" "$msg" || exit
dropsave
exit 0
+ else
+ reduced_parents="$ff_head"
+ ff_head=$head
fi
+else
+ test $head != $ff_head -a $fast_forward = never &&
+ reduced_parents="$ff_head$LF$reduced_parents"
fi
-case "$actual_parents" in
+case "$reduced_parents" in
?*"$LF"?*)
- # We have more than one actual parent
- common=$(git show-branch --merge-base $head $actual_parents)
+ # We have more than one reduced parent
+ common=$(git show-branch --merge-base $head $reduced_parents)
;;
*)
- # We have exactly one actual parent
- test "$common" != not_queried || common=$(git merge-base --all $head $actual_parents)
+ # We have exactly one reduced parent
+ test "$common" != not_queried || common=$(git merge-base --all $head $reduced_parents)
case "$common" in
?*"$LF"?*)
# We are not doing octopus and not fast forward. Need a
@@ -436,13 +447,13 @@ case "$actual_parents" in
# 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 $actual_parents &&
+ if git read-tree --trivial -m -u -v $common $head $reduced_parents &&
result_tree=$(git write-tree)
then
echo "Wonderful."
result_commit=$(
printf '%s\n' "$merge_msg" |
- git commit-tree $result_tree -p HEAD -p $actual_parents
+ git commit-tree $result_tree -p HEAD -p $reduced_parents
) || exit
finish "$result_commit" "In-index merge"
dropsave
@@ -491,7 +502,7 @@ do
# Remember which strategy left the state in the working tree
wt_strategy=$strategy
- git-merge-$strategy $common -- "$head_arg" $actual_parents
+ git-merge-$strategy $common -- "$head_arg" $reduced_parents
exit=$?
if test "$no_commit" = t && test "$exit" = 0
then
@@ -527,17 +538,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 && reduced_parents="$head$LF$reduced_parents"
+ parents=$(echo "$reduced_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.
@@ -561,7 +567,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" $actual_parents
+ git-merge-$best_strategy $common -- "$head_arg" $reduced_parents
;;
esac
--
1.5.5.54.gc6550
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-Introduce-fast-forward-option-only.patch --]
[-- Type: text/x-patch; name=0006-Introduce-fast-forward-option-only.patch, Size: 9305 bytes --]
From 6b58d39f1cf364d8b0c91da519aace607172b724 Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sat, 3 May 2008 16:55:47 -0700
Subject: [PATCH 6/6] Introduce fast forward option only
This commit introduces fast forward option 'only'. With --ff=only
merge succeeds only if it resolves to fast-forward merge.
This feature is useful for cases where a rebase is desired
instead of a real merge. This option can then be used to
avoid an accidental merge.
See the documentation for further details.
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 f080986..94ff719 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -168,21 +168,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)
@@ -209,7 +209,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=$#
}
@@ -347,6 +347,8 @@ find_reduced_parents "$@"
# ff_head may be included here or later in actual parents
if test -n "$reduced_parents"
then
+ test $fast_forward = only &&
+ die "--ff=only can not handle more than one real parent"
test $head = $ff_head ||
reduced_parents="$ff_head$LF$reduced_parents"
fi
diff --git a/git-pull.sh b/git-pull.sh
index 9e91e75..c5fa1ee 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 c7c6d14..56e8370 100755
--- a/t/t7601-merge-ff-options.sh
+++ b/t/t7601-merge-ff-options.sh
@@ -662,4 +662,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.5.54.gc6550
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH] Fast forward strategies allow, never, and only
2008-05-12 5:17 [PATCH] Fast forward strategies allow, never, and only Sverre Hvammen Johansen
@ 2008-05-12 12:13 ` Miklos Vajna
0 siblings, 0 replies; 4+ messages in thread
From: Miklos Vajna @ 2008-05-12 12:13 UTC (permalink / raw)
To: Sverre Hvammen Johansen; +Cc: git
[-- Attachment #1: Type: text/plain, Size: 1094 bytes --]
On Sun, May 11, 2008 at 10:17:26PM -0700, Sverre Hvammen Johansen <hvammen@gmail.com> wrote:
> I am hereby sending an updated version of these patches as
> attachments to this mail. The patch series consists of the
> following six patches:
>
> 0001-Documentation-for-joining-more-than-two-histories.patch
> 0002-New-merge-tests.patch
> 0003-Introduce-ff-fast-forward-option.patch
> 0004-Restructure-git-merge.sh.patch
> 0005-Head-reduction-before-selecting-merge-strategy.patch
> 0006-Introduce-fast-forward-option-only.patch
>
> The first patch add some documentation, for how git currently handles
> merges where more than one remote is involved. The second patch add
> some tests. The third, fifth, and sixth adds new features and they are all
> trivial. The fourth patch computes the reduced parents but uses it only
> to determine whether we are up-to-date or doing a fast forward.
According to Documentation/SubmittingPatches, you should not attach
patches, but send one email / patch with the patch inline.
Just use git-send-email to do so. :)
[-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --]
^ permalink raw reply [flat|nested] 4+ messages in thread
* [RFC/PATCH] Fast forward strategies allow, never, and only
@ 2008-03-11 2:59 Sverre Hvammen Johansen
2008-03-18 4:27 ` [RFC/PATCH Second draft] " Sverre Hvammen Johansen
0 siblings, 1 reply; 4+ messages in thread
From: Sverre Hvammen Johansen @ 2008-03-11 2:59 UTC (permalink / raw)
To: git
>From 795bd1b3e70f011b675061ecae322527a9f0695c Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <sj@gmail.com>
Date: Sun, 9 Mar 2008 21:43:56 -0800
Subject: [PATCH] Fast forward strategies allow, never, and only.
New fast forward strategies, only, is introduced. This new fast
forward strategy prevents real merges.
FF strategy "only" fails if the specified heads and HEAD can not
be reduced down to only one real parent. The only allowed
outcome is a fast forward unless HEAD is up to date with
the specified heads.
This patch also uses the real heads found instead of those
specified for real merges. This means that merge startegies
that only take two heads can now accept more than two heads
if they can be reduced down to only two real heads. However,
fast-forward of head in combination with a real merge is
handled as before.
Signed-off-by: Sverre Hvammen Johansen <sj@black.local>
---
Documentation/fast-forward-strategies.txt | 16 +
Documentation/git-merge.txt | 6 +-
Documentation/git-pull.txt | 2 +
Documentation/merge-options.txt | 11 +-
git-merge.sh | 291 +++++++++++-----
git-pull.sh | 4 +-
t/t7601-merge-ff-strategies.sh | 549 +++++++++++++++++++++++++++++
7 files changed, 778 insertions(+), 101 deletions(-)
create mode 100644 Documentation/fast-forward-strategies.txt
create mode 100755 t/t7601-merge-ff-strategies.sh
diff --git a/Documentation/fast-forward-strategies.txt
b/Documentation/fast-forward-strategies.txt
new file mode 100644
index 0000000..1d6da26
--- /dev/null
+++ b/Documentation/fast-forward-strategies.txt
@@ -0,0 +1,16 @@
+FAST FORWARD STRATEGIES
+-----------------------
+
+allow::
+ 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.
+
+never::
+ Generate a merge commit even if the merge resolved
+ as a fast-forward.
+
+only::
+ Only allow a fast-forward. The merge will fail
+ unless HEAD is up to date or the merge resolved as
+ a fast-forward.
diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt
index c136b10..fbf3ebe 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 strategy>]]
[-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-strategies.txt[]
+
include::merge-strategies.txt[]
diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt
index 7378943..99d205c 100644
--- a/Documentation/git-pull.txt
+++ b/Documentation/git-pull.txt
@@ -50,6 +50,8 @@ include::pull-fetch-param.txt[]
include::urls-remotes.txt[]
+include::fast-forward-strategies.txt[]
+
include::merge-strategies.txt[]
DEFAULT BEHAVIOUR
diff --git a/Documentation/merge-options.txt b/Documentation/merge-options.txt
index 9f1fc82..848b786 100644
--- a/Documentation/merge-options.txt
+++ b/Documentation/merge-options.txt
@@ -29,12 +29,13 @@
--no-ff::
Generate a merge commit even if the merge resolved as a
- fast-forward.
+ fast-forward. --on-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 strategy>]::
+ Select fast forward strategy. --ff without any argument
+ is an alias for --ff=allow which is the default behavior
+ of git-merge. It will not generate a merge commit if the
+ merge resolved as a fast-forward,
-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..873e4cb 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? allow fast forward (default)
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,8 @@ parse_config () {
--summary)
show_diffstat=t ;;
--squash)
- test "$allow_fast_forward" = t ||
- die "You cannot combine --squash with --no-ff."
+ test "$fast_forward" = allow ||
+ die "You cannot combine --squash with --ff=never."
squash=t no_commit=t ;;
--no-squash)
squash= no_commit= ;;
@@ -163,11 +163,28 @@ parse_config () {
--no-commit)
no_commit=t ;;
--ff)
- allow_fast_forward=t ;;
+ case "$2" in
+ allow|never|only)
+ fast_forward=$2 squash= no_commit= ; shift ;;
+ -*)
+ fast_forward=allow squash= no_commit= ;;
+ *)
+ die "available fast-forward strategies are: allow, newer, and only" ;;
+ esac
+ ;;
+ --ff=*)
+ fast_forward=$(echo $1 |cut -d = -f 2) squash= no_commit=
+ case $fast_forward in
+ allow|never|only)
+ ;;
+ *)
+ die "available fast-forward strategies are: allow, newer, and only" ;;
+ 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
@@ -279,24 +296,125 @@ do
done
set x $remoteheads ; shift
+echo "$head" >"$GIT_DIR/ORIG_HEAD"
+
+find_one_real_parent () {
+ # The real parent candidate
+ real_parent=$1
+ shift
+
+ # Other parents that are indepent of the real parent candidate
+ other_parents=
+
+ # Parents that need further processing to determine whether
+ # they are independent parents of the parent candidate or not
+ parents_x=
+
+ while test $# -gt 0
+ do
+ if test $real_parent = $1
+ then
+ # Found a parent that is equal to the real
+ # parent candidate
+ echo "Duplicate $(git rev-parse --short $1)"
+ echo "Ignoring $1"
+ else
+ common_b=$(git merge-base --all $real_parent $1)
+
+ if test "$common_b" = $1
+ then
+ # Found a parent that is not
+ # independent of the real parent
+ # candidate
+ echo "Possible ff $(git rev-parse --short $1)..$(git rev-parse
--short $real_parent)."
+ echo "Ignoring $1"
+ elif test "$common_b" = $real_parent
+ then
+ # Found a better real parent candidate
+ echo "Possible ff $(git rev-parse --short $real_parent)..$(git
rev-parse --short $1)."
+ echo "Ignoring $real_parent"
+ real_parent=$1
+ parents_x="$other_parents"
+ other_parents=
+ else
+ # Found a parent that is independent
+ # of the real parent candidate
+ other_parents="$other_parents $1"
+ fi
+ fi
+ shift
+ done
+
+ # We have a real parent, some parents we know is independt of
+ # this real parent, and some parents that need further
+ # processing.
+
+ for b in $parents_x
+ do
+ common_b=$(git merge-base --all $real_parent $b)
+ if test "$common_b" != $b
+ then
+ other_parents="$other_parents $b"
+ fi
+ done
+
+ # We have a real parent and other parents we know is independent
+ # of this real parent
+}
+
+find_real_parents () {
+ find_one_real_parent $head "$@"
+ ff_head=$real_parent
+ real_parents=
+
+ while test -n "$other_parents"
+ do
+ find_one_real_parent $other_parents
+ real_parents="$real_parents $real_parent"
+ done
+}
+
+if test $fast_forward = never -o
+then
+ real_parents="$@"
+ ff_head=$head
+else
+ find_real_parents "$@"
+fi
+
+if test -n "$real_parents"
+then
+ case $fast_forward in
+ only)
+ die "Fast forward strategy only can only handle one real parent" ;;
+ never|allow)
+ if test $head != $ff_head
+ then
+ real_parents="$ff_head $real_parents"
+ ff_head=$head
+ fi
+ ;;
+ esac
+fi
+
case "$use_strategies" in
'')
- case "$#" in
- 1)
- var="`git config --get pull.twohead`"
+ case "$real_parents" in
+ ?*" "?*)
+ 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
;;
@@ -308,7 +426,7 @@ do
do
case " $s " in
*" $ss "*)
- allow_fast_forward=f
+ fast_forward=never
break
;;
esac
@@ -323,88 +441,73 @@ do
esac
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 "$allow_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
- ;;
-t,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 test -z "$real_parents"
+then
+ if test $head = $ff_head
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
+ then
+ real_parents="$ff_head"
+ ff_head=$head
+ else
+ 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
+else
+ if test $head != $ff_head -a $fast_forward = never
+ then
+ real_parents="$ff_head $real_parents"
+ ff_head=$head
+ fi
+fi
+
+case "$real_parents" in
+?*" "?*)
+ # We have more than one parent
+ common=$(git show-branch --merge-base $head $real_parents)
;;
+*)
+ # We have exactly one parent
+ common=$(git merge-base --all $ff_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.
@@ -445,7 +548,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
@@ -481,11 +584,11 @@ done
# auto resolved the merge cleanly.
if test '' != "$result_tree"
then
- if test "$allow_fast_forward" = "t"
+ if test $fast_forward != never
then
- parents=$(git show-branch --independent "$head" "$@")
+ parents=$(git show-branch --independent "$head" $real_parents)
else
- parents=$(git rev-parse "$head" "$@")
+ parents=$(git rev-parse "$head" $real_parents)
fi
parents=$(echo "$parents" | sed -e 's/^/-p /')
result_commit=$(printf '%s\n' "$merge_msg" | git commit-tree
$result_tree $parents) || exit
@@ -515,7 +618,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
diff --git a/git-pull.sh b/git-pull.sh
index 3ce32b5..b78cfdd 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=*)
+ 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-strategies.sh b/t/t7601-merge-ff-strategies.sh
new file mode 100755
index 0000000..6c0a91a
--- /dev/null
+++ b/t/t7601-merge-ff-strategies.sh
@@ -0,0 +1,549 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 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.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 (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" &&
+ if git merge c2
+ then
+ false
+ else
+ verify_merge file result.1 &&
+ verify_head $c1
+ fi
+'
+
+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 &&
+ if git --ff=only merge c1 c2
+ then
+ false
+ else
+ verify_merge file result.0 &&
+ verify_head $c0
+ fi
+'
+
+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 &&
+ if git merge c2 --ff=only
+ then
+ false
+ else
+ verify_merge file result.1 &&
+ verify_head $c1
+ fi
+'
+
+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 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_done
--
1.5.3.3
--
Sverre Hvammen Johansen
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [RFC/PATCH Second draft] Fast forward strategies allow, never, and only
2008-03-11 2:59 [RFC/PATCH] " Sverre Hvammen Johansen
@ 2008-03-18 4:27 ` Sverre Hvammen Johansen
2008-03-19 20:35 ` Junio C Hamano
0 siblings, 1 reply; 4+ messages in thread
From: Sverre Hvammen Johansen @ 2008-03-18 4:27 UTC (permalink / raw)
To: git
New fast forward strategies, only, is introduced. This new fast
forward strategy prevents real merges.
FF strategy "only" fails if the specified heads and HEAD can not
be reduced down to only one real parent. The only allowed
outcome is a fast forward unless HEAD is up to date with
the specified heads.
This patch also uses the real heads found instead of those
specified for real merges. This means that merge startegies
that only take two heads can now accept more than two heads
if they can be reduced down to only two real heads. However,
fast-forward of head in combination with a real merge is
handled as before.
See the documentation for further explanation of this feature.
Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
Documentation/fast-forward-options.txt | 69 +++
Documentation/git-merge.txt | 6 +-
Documentation/git-pull.txt | 2 +
Documentation/merge-options.txt | 9 +-
git-merge.sh | 246 +++++++-----
git-pull.sh | 4 +-
t/t7600-merge.sh | 5 -
t/t7601-merge-ff-options.sh | 722 ++++++++++++++++++++++++++++++++
8 files changed, 946 insertions(+), 117 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..87fd0ae
--- /dev/null
+++ b/Documentation/fast-forward-options.txt
@@ -0,0 +1,69 @@
+FAST FORWARD OPTIONS
+--------------------
+
+allow::
+ Do not generate a merge commit if the merge resolved as a
+ fast-forward, only update the branch pointer. This is the
+ default behavior. This option is equivalent of '--ff' without
+ any argument.
+
+never::
+ Generate a merge commit even if the merge resolved 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 resolved 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
+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'. The last merge of
+topicA would have failed with '--ff=only'. 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 your workflow require a linear history for the special
+branch ("master"), topic branches must be rebased before merging them
+back to "master". A pull or a merge from the "master branch of a
+topic branch may accidentally introduce a merge commit that was not
+already in the topic branch if the topic that were merged was not
+properly rebased. This will creating a none linear history.
+
+Using "only fast forward" policy ensures that whenever a pull or a
+merge is performed it will fail unless the merge can be resolved as a
+fast forward. This will however not guarantee a linear history since
+the topic branches that are merged in may have merge commits recorded.
+You may therefor need to use this policy on the topic branches as
+well.
+
+"Only fast forward" policy on the "master" branch can be enforced by
+setting the mergeoptions for that branch using git config:
+
+------------
+% git config branch.master.mergeoptions --ff=only
+------------
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 7378943..222482f 100644
--- a/Documentation/git-pull.txt
+++ b/Documentation/git-pull.txt
@@ -50,6 +50,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..a98cd77 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,18 +161,33 @@ parse_config () {
--no-commit)
no_commit=t ;;
--ff)
- allow_fast_forward=t ;;
+ case "$2" in
+ allow|never|only)
+ fast_forward=$2; shift ;;
+ -*)
+ fast_forward=allow ;;
+ *)
+ die "Available fast-forward strategies
are: allow, newer, and only" ;;
+ esac
+ ;;
+ --ff=*)
+ fast_forward=${1#--ff=}
+ case "$fast_forward" in
+ allow|never|only)
+ ;;
+ *)
+ die "Available fast-forward strategies
are: allow, newer, and only" ;;
+ 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
*" $1 "*)
use_strategies="$use_strategies$1 " ;;
*)
- die "available strategies are:
$all_strategies" ;;
+ die "Available strategies are:
$all_strategies" ;;
esac
;;
-m|--message)
@@ -279,24 +292,68 @@ do
done
set x $remoteheads ; shift
+echo "$head" >"$GIT_DIR/ORIG_HEAD"
+
+if test $fast_forward = never
+then
+ real_parents=$(git rev-parse "$@")
+ ff_head=$head
+ common=
+else
+ if test $# = 1
+ then
+ 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
+ real_parents=$(git show-branch --independent $head "$@")
+ ff_head=${real_parents%%$LF*}
+ real_parents=${real_parents#$ff_head}
+ real_parents=${real_parents#$LF}
+ common=
+ fi
+fi
+
+if test -n "$real_parents"
+then
+ if test $fast_forward = only
+ then
+ die "--ff=only can not handle more than one real parent"
+ elif test $head != $ff_head
+ then
+ real_parents="$ff_head$LF$real_parents"
+ fi
+fi
+
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
;;
@@ -308,7 +365,7 @@ do
do
case " $s " in
*" $ss "*)
- allow_fast_forward=f
+ fast_forward=never
break
;;
esac
@@ -323,88 +380,72 @@ do
esac
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 "$allow_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
- ;;
-t,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 test -z "$real_parents"
+then
+ if test $head = $ff_head
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
+ then
+ real_parents="$ff_head"
+ ff_head=$head
+ else
+ 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
+else
+ if test $head != $ff_head -a $fast_forward = never
+ then
+ real_parents="$ff_head$LF$real_parents"
+ 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 -n "$common" || 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.
@@ -445,7 +486,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
@@ -481,17 +522,12 @@ done
# auto resolved the merge cleanly.
if test '' != "$result_tree"
then
- if test "$allow_fast_forward" = "t"
- 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.
@@ -515,7 +551,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
diff --git a/git-pull.sh b/git-pull.sh
index 3ce32b5..5bc84a6 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=only|--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/t7600-merge.sh b/t/t7600-merge.sh
index 5d16628..085f55f 100755
--- a/t/t7600-merge.sh
+++ b/t/t7600-merge.sh
@@ -428,11 +428,6 @@ test_expect_success 'merge c0 with c1 (no-ff)' '
test_debug 'gitk --all'
-test_expect_success 'combining --squash and --no-ff is refused' '
- test_must_fail git merge --squash --no-ff c1 &&
- test_must_fail git merge --no-ff --squash c1
-'
-
test_expect_success 'merge c0 with c1 (ff overrides no-ff)' '
git reset --hard c0 &&
git config branch.master.mergeoptions "--no-ff" &&
diff --git a/t/t7601-merge-ff-options.sh b/t/t7601-merge-ff-options.sh
new file mode 100755
index 0000000..e78d32e
--- /dev/null
+++ b/t/t7601-merge-ff-options.sh
@@ -0,0 +1,722 @@
+#!/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.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 (--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 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 x0 (--squash combined with --ff=only)' '
+ git reset --hard c1 &&
+ git config branch.master.mergeoptions "" &&
+ test_tick &&
+ git merge x0 --squash --ff=only &&
+ 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 (--squash combined with --ff=only)' '
+ git reset --hard x0 &&
+ git config branch.master.mergeoptions "" &&
+ test_tick &&
+ 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 (--squash combined with --ff=never)' '
+ git reset --hard c1 &&
+ git config branch.master.mergeoptions "" &&
+ test_tick &&
+ git merge x0 --squash --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 (--squash combined with --ff=never)' '
+ git reset --hard x0 &&
+ git config branch.master.mergeoptions "" &&
+ test_tick &&
+ 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 &&
+ git merge c2 --squash --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 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 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=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
--
Sverre Hvammen Johansen
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [RFC/PATCH Second draft] Fast forward strategies allow, never, and only
2008-03-18 4:27 ` [RFC/PATCH Second draft] " Sverre Hvammen Johansen
@ 2008-03-19 20:35 ` Junio C Hamano
2008-03-20 6:47 ` Sverre Hvammen Johansen
0 siblings, 1 reply; 4+ messages in thread
From: Junio C Hamano @ 2008-03-19 20:35 UTC (permalink / raw)
To: Sverre Hvammen Johansen; +Cc: git
"Sverre Hvammen Johansen" <hvammen@gmail.com> writes:
> New fast forward strategies, only, is introduced. This new fast
> forward strategy prevents real merges.
>
> FF strategy "only" fails if the specified heads and HEAD can not
> be reduced down to only one real parent. The only allowed
> outcome is a fast forward unless HEAD is up to date with
> the specified heads.
>
> This patch also uses the real heads found instead of those
> specified for real merges. This means that merge startegies
> that only take two heads can now accept more than two heads
> if they can be reduced down to only two real heads. However,
> fast-forward of head in combination with a real merge is
> handled as before.
This might be easier to review if split into two parts. Code suffling to
do --ff/--no-ff => ff={allow,never} and documentation updates to improve
the description of these two options in the first patch, and addition of
"only" to code and the updated docuemntation in the second.
> +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'. The last merge of
> +topicA would have failed with '--ff=only'. Topic A consist of those
> +commits that can be reached from master^2 without passing through any
> +of the first-parent ancestries of master.
If you remove one sentence "The last merge ... =only'." from it, this part
is a very good write-up on how "never fast forward" could be useful, and
might even be a worthy addition to the current documentation. However it
lacks a crucial bit of information: it is _not enough_ to just use --no-ff
to maintain the "special status" of "master". You also need to prevent
direct committing to it. So while --no-ff is an ingredient you can
construct such a workflow out of, it by itself is not the whole solution.
You need additional discipline.
> +However, if your workflow require a linear history for the special
> +branch ("master"), topic branches must be rebased before merging them
> +back to "master". A pull or a merge from the "master branch of a
> +topic branch may accidentally introduce a merge commit that was not
> +already in the topic branch if the topic that were merged was not
> +properly rebased. This will creating a none linear history.
This,...
> +Using "only fast forward" policy ensures that whenever a pull or a
> +merge is performed it will fail unless the merge can be resolved as a
> +fast forward. This will however not guarantee a linear history since
> +the topic branches that are merged in may have merge commits recorded.
>
> +You may therefor need to use this policy on the topic branches as
> +well.
combined with the above, would make "only" an incomplete implementation of
the goal you stated earlier, i.e. "to force a completely linear history",
but I think you can trivially fix this by making sure that there is no
merge commit in ORIG_HEAD..MERGE_HEAD and refusing if you find one. And
by fixing the implementation, you do not have to make excuses like the
above two and half paragraphs.
Having said that, I doubt what you are trying to achieve is to force a
totally linear history, as that is quite useless policy unless you are
talking about a one-man project.
When your project has gained meaningful number of developers, there will
always be more than one nontrivial changes outstanding. Forcing a linear
history everywhere means whenever somebody wants his changes accepted, he
needs to fetch + rebase whenever anybody else's change is accepted to the
special "master". The more developers you have, the narrower window
between acceptance of changes to the "master" becomes, and eventually the
window will become shorter than the time needed to rebase and retest any
nontrivial change on top of the tip of "master". At that point, nobody
can get anything but a trivially rebasable and untested series accepted.
Such a policy would encourage people to merge only "early half" of their
series (because they do not have enough time to rebase the full series) in
a poorly tested shape. So in that sense it is not just "practicaly
useless", but can be actively harmful by encouraging a bad workflow.
But your "merge has to fast-forward, but the merged branch can have
nonlinear history itself" semantics is different from "require linear
history". For example, if you want the top-level integrator to do
absolutely _nothing_ (not even a trivial merge), in order to shift the
burden of integration testing to contributors, then use of such semantics
(which is _different_ from "completely linear") by the top-level
integrator could achieve that. Nobody can ask the top-level to pull which
would result in a tree that he built and tested himself. The top-level
integrator would say "Your tree is not a fast-forward from mine, because I
merged with somebody else between the time you prepared your tree and I
learned about it. Please try again", and in response, the contributor can
pull from the top-level integrator and re-test the merge result and ask
again. With luck, the top-level integrator may not have merged with
anybody else and a pull from that contributor would fast-forward.
So if that is what you are trying to achieve, you need to update your
description. If you aim for "Totally linear", I think many people will
find it is practically useless, but if you are aiming for something
different, you should advertise it as such.
I think the scaling issue (iow "narrowing window") is the same either way,
and the documentaion should warn about it to the users. Unclueful people
may think, without really thinking ;-), that it is a good thing in any
occassions to require totally linear history, unless downsides are also
explained.
> diff --git a/git-merge.sh b/git-merge.sh
> index 7dbbb1d..a98cd77 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."
I do not think you defended why it is good idea to drop this sanity check.
> squash=t no_commit=t ;;
> --no-squash)
> squash= no_commit= ;;
> @@ -163,18 +161,33 @@ parse_config () {
> --no-commit)
> no_commit=t ;;
> --ff)
> - allow_fast_forward=t ;;
> + case "$2" in
> + allow|never|only)
> + fast_forward=$2; shift ;;
> + -*)
> + fast_forward=allow ;;
> + *)
> + die "Available fast-forward strategies
> are: allow, newer, and only" ;;
Strategies? "fast forward options" you used in the option description is
a reasonable one, and it would be better to keep it consistent.
I didn't look at the rest of the patch (yet).
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [RFC/PATCH Second draft] Fast forward strategies allow, never, and only
2008-03-19 20:35 ` Junio C Hamano
@ 2008-03-20 6:47 ` Sverre Hvammen Johansen
2008-03-22 19:49 ` Junio C Hamano
0 siblings, 1 reply; 4+ messages in thread
From: Sverre Hvammen Johansen @ 2008-03-20 6:47 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Wed, Mar 19, 2008 at 12:35 PM, Junio C Hamano <gitster@pobox.com> wrote:
> > ...
> This might be easier to review if split into two parts. Code suffling to
> do --ff/--no-ff => ff={allow,never} and documentation updates to improve
> the description of these two options in the first patch, and addition of
> "only" to code and the updated docuemntation in the second.
What I would like to do is to split it in three like this:
1. Head reduction
2. --ff/--no-ff => ff={allow,never} and documentation updates.
3. --ff=only
If you would like me to do this please tell me.
> ...
> might even be a worthy addition to the current documentation. However it
> lacks a crucial bit of information: it is _not enough_ to just use --no-ff
> to maintain the "special status" of "master". You also need to prevent
> direct committing to it.
True, but the special case where you have a topic that only consists
of one commit you might as well apply it directly on master. In any
case, when you commit something directly on the special branch master
you usually know what you are doing. It is perfectly OK to combine the
two. I am not sure we need to explain this.
> > +You may therefor need to use this policy on the topic branches as
> > +well.
>
> combined with the above, would make "only" an incomplete implementation of
> the goal you stated earlier, i.e. "to force a completely linear history",
Actually that is not my goal for this implementation, I just tried to
describe a useful use case, but failed. Let me try again.
I actually need this for the integration between accurev and git I am
using/maintaining/developing (at some point I intend to release it).
At work I am forced to use accurev, but the user interface for accurev
is horrible and it is slow. I therefor have complete history of
accurev streams in git and are doing all my work in git with branches
and everything. The git-accurev integrator creates one merge commit
object in git for each time i check something into accurev, . This
merge commit object ties the content in accurev that was committed
into accurev with the corresponding content in git. It is important
that further work I do is based on this special merge commit object.
It works if I don't, but the history gets really messy, and for this
I need the --ff=only so I don't forget to pull or rebase before the
next commit I make into accurev.
> but I think you can trivially fix this by making sure that there is no
> merge commit in ORIG_HEAD..MERGE_HEAD and refusing if you find one. And
> by fixing the implementation, you do not have to make excuses like the
> above two and half paragraphs.
I don't intend to do that, simply because I don't need it and it would
actually not work for my workflow.
> So if that is what you are trying to achieve, you need to update your
> description. If you aim for "Totally linear", I think many people will
> find it is practically useless, but if you are aiming for something
> different, you should advertise it as such.
You are right, I will try to come up with something better.
> > @@ -153,8 +153,6 @@ parse_config () {
> > --summary)
> > show_diffstat=t ;;
> > --squash)
> > - test "$allow_fast_forward" = t ||
> > - die "You cannot combine --squash with --no-ff."
>
> I do not think you defended why it is good idea to drop this sanity check.
I don't see any good idea for having this check. Nothing bad happens
by allowing to combine these options the way I currently implement it.
--
Sverre Hvammen Johansen
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [RFC/PATCH Second draft] Fast forward strategies allow, never, and only
2008-03-20 6:47 ` Sverre Hvammen Johansen
@ 2008-03-22 19:49 ` Junio C Hamano
2008-03-26 3:50 ` Sverre Hvammen Johansen
0 siblings, 1 reply; 4+ messages in thread
From: Junio C Hamano @ 2008-03-22 19:49 UTC (permalink / raw)
To: Sverre Hvammen Johansen; +Cc: git
"Sverre Hvammen Johansen" <hvammen@gmail.com> writes:
> On Wed, Mar 19, 2008 at 12:35 PM, Junio C Hamano <gitster@pobox.com> wrote:
>> > ...
>> This might be easier to review if split into two parts. Code suffling to
>> do --ff/--no-ff => ff={allow,never} and documentation updates to improve
>> the description of these two options in the first patch, and addition of
>> "only" to code and the updated docuemntation in the second.
>
> What I would like to do is to split it in three like this:
>
> 1. Head reduction
>
> 2. --ff/--no-ff => ff={allow,never} and documentation updates.
>
> 3. --ff=only
>
> If you would like me to do this please tell me.
Yeah, making head reduction into its own separate patch would make things
clearer, I guess.
But if you are going to do that, then the order should be 2/1/3 from the
above list. 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. Then you build new features
and enhancements on top of that solidified base.
Because "head reduction" changes the semantics (making it better or worse
does not matter --- "changes" is what matters), it should come after #2
above, I think.
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [RFC/PATCH Second draft] Fast forward strategies allow, never, and only
2008-03-22 19:49 ` Junio C Hamano
@ 2008-03-26 3:50 ` Sverre Hvammen Johansen
2008-03-31 4:19 ` Sverre Hvammen Johansen
0 siblings, 1 reply; 4+ messages in thread
From: Sverre Hvammen Johansen @ 2008-03-26 3:50 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
[-- Attachment #1: Type: text/plain, Size: 914 bytes --]
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
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Introduce-ff-fast-forward-option.patch --]
[-- Type: text/x-patch; name=0001-Introduce-ff-fast-forward-option.patch, Size: 19899 bytes --]
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
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-Restructuring-git-merge.sh.patch --]
[-- Type: text/x-patch; name=0002-Restructuring-git-merge.sh.patch, Size: 6295 bytes --]
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
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-Head-reduction-before-selecting-merge-strategy.patch --]
[-- Type: text/x-patch; name=0003-Head-reduction-before-selecting-merge-strategy.patch, Size: 6173 bytes --]
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
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Introduce-fast-forward-option-only.patch --]
[-- Type: text/x-patch; name=0004-Introduce-fast-forward-option-only.patch, Size: 9032 bytes --]
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
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [RFC/PATCH Second draft] Fast forward strategies allow, never, and only
2008-03-26 3:50 ` Sverre Hvammen Johansen
@ 2008-03-31 4:19 ` Sverre Hvammen Johansen
2008-04-20 1:06 ` [PATCH] " Sverre Hvammen Johansen
0 siblings, 1 reply; 4+ messages in thread
From: Sverre Hvammen Johansen @ 2008-03-31 4:19 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
[-- Attachment #1: Type: text/plain, Size: 1773 bytes --]
I have attached a new set of patches to this email (to avoid
line-wrapping issues if someone needs them for testing). They are
also posted inlined for comments.
The patch series consists of the following five patches:
0001-New-merge-tests.patch
0002-Introduce-ff-fast-forward-option.patch
0003-Restructure-git-merge.sh.patch
0004-Head-reduction-before-selecting-merge-strategy.patch
0005-Introduce-fast-forward-option-only.patch
The first patch add some tests. The second, fourth, and fifth adds
new features and they are all trivial. I was able to make the fourth
patch trivial as well by actually doing the real work of finding the
reduced parents in the third patch. The third patch computes the
reduced parents but uses it only to determine whether we are
up-to-date or do a fast forward.
There are probably some minor adjustments to the documentation we
should do. The third patch doesn't do much but it is still a pretty
big step for code change for git-merge.sh. I would therefor like more
eyes to look at this. More tests is also needed. I would like the
following tests to be added:
- A test where a recursive merge is required for the merge
to succeed, where an octopus would fail.
- A test where a recursive merge gets more than one commit as the
merge base. A case where "git merge-base --all" returns at least
two commits.
Is there anyone that can give me some tips for such tests? What is
the simplest case where a recursive merge is required?. How can I
construct a history where we have more than one common ancestor for
two commit objects?
I have looked at the current tests and there does not seem to be
anything where "git merge-base --all" return more than one ancestor.
--
Sverre Hvammen Johansen
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-New-merge-tests.patch --]
[-- Type: text/x-patch; name=0001-New-merge-tests.patch, Size: 7071 bytes --]
From 40068ea878fc0561e44e31d002bd0870ff1ac5fa Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sat, 29 Mar 2008 18:37:51 -0800
Subject: [PATCH 1/5] New merge tests
Introduce new merge tests for preparation of new features:
--ff=<fast forward option>
Head reduction
--ff=only
Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
t/t7601-merge-ff-options.sh | 461 +++++++++++++++++++++++++++++++++++++++++++
1 files changed, 461 insertions(+), 0 deletions(-)
create mode 100755 t/t7601-merge-ff-options.sh
diff --git a/t/t7601-merge-ff-options.sh b/t/t7601-merge-ff-options.sh
new file mode 100755
index 0000000..408122e
--- /dev/null
+++ b/t/t7601-merge-ff-options.sh
@@ -0,0 +1,461 @@
+#!/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 c1 with c0 and c0' '
+ git reset --hard c1 &&
+ git config branch.master.mergeoptions "" &&
+ test_tick &&
+ git merge c0 c0 &&
+ verify_merge file result.1 &&
+ verify_head $c1
+'
+
+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_done
--
1.5.3.3
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-Introduce-ff-fast-forward-option.patch --]
[-- Type: text/x-patch; name=0002-Introduce-ff-fast-forward-option.patch, Size: 13878 bytes --]
From 0c8fc6cd4bb097746ecdbc96cecfa774cc5e478c Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sat, 29 Mar 2008 17:27:28 -0800
Subject: [PATCH 2/5] 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 | 188 ++++++++++++++++++++++++++++++++
7 files changed, 277 insertions(+), 23 deletions(-)
create mode 100644 Documentation/fast-forward-options.txt
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
index 408122e..2749f4f 100755
--- a/t/t7601-merge-ff-options.sh
+++ b/t/t7601-merge-ff-options.sh
@@ -458,4 +458,192 @@ test_expect_success 'merge c1 with c2 and 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
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-Restructure-git-merge.sh.patch --]
[-- Type: text/x-patch; name=0003-Restructure-git-merge.sh.patch, Size: 7107 bytes --]
From f20dcfce6dd6af176f4f8b91822bc0a32ddc563e Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sun, 30 Mar 2008 12:55:23 -0800
Subject: [PATCH 3/5] Restructure git-merge.sh
Restructure git-merge.sh for preparation of new feature:
Head reduction before selecting merge strategy
Some aspects of this patch does not make much sense without
the next patch in this series.
Signed-off-by: Sverre Hvammen Johansen <hvammen@gmail.com>
---
git-merge.sh | 186 +++++++++++++++++++++++++++++++++-------------------------
1 files changed, 105 insertions(+), 81 deletions(-)
diff --git a/git-merge.sh b/git-merge.sh
index 17f40f2..7c34b6c 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -207,6 +207,47 @@ parse_config () {
args_left=$#
}
+# Find reduced parents
+# The following variables are set as follow:
+# reduced_parents: The reduced parents of those specified on the command line.
+# However, the actual parents are included if we never ff.
+# common: All common ancestors or not_queried
+# ff_head: Head or an reduced parent that may be a candidate for fast forward
+find_reduced_parents () {
+ if test $fast_forward = never
+ then
+ reduced_parents=$(git rev-parse "$@")
+ ff_head=$head
+ common=not_queried
+ else
+ if test $# = 1
+ then
+ common=$(git merge-base --all $head "$1")
+ if test "$common" = $head
+ then
+ reduced_parents=
+ ff_head=$1
+ elif test "$common" = "$1"
+ then
+ reduced_parents=
+ ff_head=$head
+ else
+ reduced_parents=$1
+ ff_head=$head
+
+ fi
+ else
+ reduced_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=${reduced_parents%%$LF*}
+ reduced_parents=${reduced_parents#$ff_head}
+ reduced_parents=${reduced_parents#$LF}
+ common=not_queried
+ fi
+ fi
+}
+
test $# != 0 || usage
have_message=
@@ -294,24 +335,28 @@ do
done
set x $remoteheads ; shift
+find_reduced_parents "$@"
+
+actual_parents=$(git rev-parse "$@")
+
case "$use_strategies" in
'')
- case "$#" in
- 1)
- var="`git config --get pull.twohead`"
+ case "$actual_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 +384,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 test -z "$reduced_parents"
+then
+ if test $head = $ff_head
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
+ 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 "$actual_parents" in
+?*"$LF"?*)
+ # We have more than one actual parent
+ common=$(git show-branch --merge-base $head $actual_parents)
;;
+*)
+ # We have exactly one actual parent
+ test "$common" != not_queried || common=$(git merge-base --all $head $actual_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 $actual_parents &&
+ result_tree=$(git write-tree)
+ then
+ echo "Wonderful."
+ result_commit=$(
+ printf '%s\n' "$merge_msg" |
+ git commit-tree $result_tree -p HEAD -p $actual_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 +484,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" $actual_parents
exit=$?
if test "$no_commit" = t && test "$exit" = 0
then
@@ -530,7 +554,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" $actual_parents
;;
esac
--
1.5.3.3
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Head-reduction-before-selecting-merge-strategy.patch --]
[-- Type: text/x-patch; name=0004-Head-reduction-before-selecting-merge-strategy.patch, Size: 6475 bytes --]
From 179c59ec8c06e3dbee251fa510267b6f2eb52b6e Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sun, 30 Mar 2008 00:01:33 -0800
Subject: [PATCH 4/5] 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 | 50 ++++++++++++++++++++++++-------------------
2 files changed, 70 insertions(+), 23 deletions(-)
diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt
index 2af33d8..f6bc96f 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 on the command line, git will
+try to reduce the number of commits used (reduced parents) by
+eliminating commits than can be reached from other commits. The
+commit message will reflect the commits specified on the command line
+but the merge strategy will be selected based on the reduced parents
+including `HEAD`. The reduced parents 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 tip of the master branch:
+
+------------
+ o---o---o topicB
+ /
+ o---o---o topicA
+ /
+ o---o---o---o---o---o master
+ \
+ o---o topicC
+------------
+
+Merging topicA, B and C to the master branch 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 reduced 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 7c34b6c..7c70c56 100755
--- a/git-merge.sh
+++ b/git-merge.sh
@@ -337,11 +337,16 @@ set x $remoteheads ; shift
find_reduced_parents "$@"
-actual_parents=$(git rev-parse "$@")
+# ff_head may be included here or later in actual parents
+if test -n "$reduced_parents"
+then
+ test $head = $ff_head ||
+ reduced_parents="$ff_head$LF$reduced_parents"
+fi
case "$use_strategies" in
'')
- case "$actual_parents" in
+ case "$reduced_parents" in
?*"$LF"?*)
var="`git config --get pull.octopus`"
if test -n "$var"
@@ -406,17 +411,23 @@ then
finish "$new_head" "$msg" || exit
dropsave
exit 0
+ else
+ reduced_parents="$ff_head"
+ ff_head=$head
fi
+else
+ test $head != $ff_head -a $fast_forward = never &&
+ reduced_parents="$ff_head$LF$reduced_parents"
fi
-case "$actual_parents" in
+case "$reduced_parents" in
?*"$LF"?*)
- # We have more than one actual parent
- common=$(git show-branch --merge-base $head $actual_parents)
+ # We have more than one reduced parent
+ common=$(git show-branch --merge-base $head $reduced_parents)
;;
*)
- # We have exactly one actual parent
- test "$common" != not_queried || common=$(git merge-base --all $head $actual_parents)
+ # We have exactly one reduced parent
+ test "$common" != not_queried || common=$(git merge-base --all $head $reduced_parents)
case "$common" in
?*"$LF"?*)
# We are not doing octopus and not fast forward. Need a
@@ -429,13 +440,13 @@ case "$actual_parents" in
# 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 $actual_parents &&
+ if git read-tree --trivial -m -u -v $common $head $reduced_parents &&
result_tree=$(git write-tree)
then
echo "Wonderful."
result_commit=$(
printf '%s\n' "$merge_msg" |
- git commit-tree $result_tree -p HEAD -p $actual_parents
+ git commit-tree $result_tree -p HEAD -p $reduced_parents
) || exit
finish "$result_commit" "In-index merge"
dropsave
@@ -484,7 +495,7 @@ do
# Remember which strategy left the state in the working tree
wt_strategy=$strategy
- git-merge-$strategy $common -- "$head_arg" $actual_parents
+ git-merge-$strategy $common -- "$head_arg" $reduced_parents
exit=$?
if test "$no_commit" = t && test "$exit" = 0
then
@@ -520,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 && reduced_parents="$head$LF$reduced_parents"
+ parents=$(echo "$reduced_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.
@@ -554,7 +560,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" $actual_parents
+ git-merge-$best_strategy $common -- "$head_arg" $reduced_parents
;;
esac
--
1.5.3.3
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-Introduce-fast-forward-option-only.patch --]
[-- Type: text/x-patch; name=0005-Introduce-fast-forward-option-only.patch, Size: 9101 bytes --]
From bd97c8c32bbac389eba5bfbb25b4d0219bd3dd04 Mon Sep 17 00:00:00 2001
From: Sverre Hvammen Johansen <hvammen@gmail.com>
Date: Sat, 29 Mar 2008 23:01:30 -0800
Subject: [PATCH 5/5] 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 7c70c56..68f627d 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=$#
}
@@ -340,6 +340,8 @@ find_reduced_parents "$@"
# ff_head may be included here or later in actual parents
if test -n "$reduced_parents"
then
+ test $fast_forward = only &&
+ die "--ff=only can not handle more than one real parent"
test $head = $ff_head ||
reduced_parents="$ff_head$LF$reduced_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 2749f4f..ea36549 100755
--- a/t/t7601-merge-ff-options.sh
+++ b/t/t7601-merge-ff-options.sh
@@ -646,4 +646,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
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH] Fast forward strategies allow, never, and only
2008-03-31 4:19 ` Sverre Hvammen Johansen
@ 2008-04-20 1:06 ` Sverre Hvammen Johansen
2008-04-22 7:48 ` Junio C Hamano
0 siblings, 1 reply; 4+ messages in thread
From: Sverre Hvammen Johansen @ 2008-04-20 1:06 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git
On Sun, Mar 30, 2008 at 9:19 PM, Sverre Hvammen Johansen
<hvammen@gmail.com> wrote:
> I have attached a new set of patches to this email (to avoid
>
> line-wrapping issues if someone needs them for testing). They are
> also posted inlined for comments.
>
> The patch series consists of the following five patches:
>
> 0001-New-merge-tests.patch
> 0002-Introduce-ff-fast-forward-option.patch
> 0003-Restructure-git-merge.sh.patch
> 0004-Head-reduction-before-selecting-merge-strategy.patch
> 0005-Introduce-fast-forward-option-only.patch
I consider myself finished with this and as far as I am concerned it
can be applied to git.git. I am currently using this on top of 1.5.5
in production and assume it to be good. All tests passes.
--
Sverre Hvammen Johansen
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH] Fast forward strategies allow, never, and only
2008-04-20 1:06 ` [PATCH] " Sverre Hvammen Johansen
@ 2008-04-22 7:48 ` Junio C Hamano
0 siblings, 0 replies; 4+ messages in thread
From: Junio C Hamano @ 2008-04-22 7:48 UTC (permalink / raw)
To: Sverre Hvammen Johansen; +Cc: git
"Sverre Hvammen Johansen" <hvammen@gmail.com> writes:
> On Sun, Mar 30, 2008 at 9:19 PM, Sverre Hvammen Johansen
> <hvammen@gmail.com> wrote:
>> I have attached a new set of patches to this email (to avoid
>>
>> line-wrapping issues if someone needs them for testing). They are
>> also posted inlined for comments.
>>
>> The patch series consists of the following five patches:
>>
>> 0001-New-merge-tests.patch
>> 0002-Introduce-ff-fast-forward-option.patch
>> 0003-Restructure-git-merge.sh.patch
>> 0004-Head-reduction-before-selecting-merge-strategy.patch
>> 0005-Introduce-fast-forward-option-only.patch
>
> I consider myself finished with this and as far as I am concerned it
> can be applied to git.git. I am currently using this on top of 1.5.5
> in production and assume it to be good. All tests passes.
Sorry, but I am with a rather big backlog and am reluctant to go back the
archive a looong way to pick up and comment on a series when not many
people are wondering what happened to the wonderful series ;-) Care to
resend and ask for comments from people?
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2008-05-12 12:14 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2008-05-12 5:17 [PATCH] Fast forward strategies allow, never, and only Sverre Hvammen Johansen
2008-05-12 12:13 ` Miklos Vajna
-- strict thread matches above, loose matches on Subject: below --
2008-03-11 2:59 [RFC/PATCH] " Sverre Hvammen Johansen
2008-03-18 4:27 ` [RFC/PATCH Second draft] " Sverre Hvammen Johansen
2008-03-19 20:35 ` Junio C Hamano
2008-03-20 6:47 ` Sverre Hvammen Johansen
2008-03-22 19:49 ` Junio C Hamano
2008-03-26 3:50 ` Sverre Hvammen Johansen
2008-03-31 4:19 ` Sverre Hvammen Johansen
2008-04-20 1:06 ` [PATCH] " Sverre Hvammen Johansen
2008-04-22 7:48 ` Junio C Hamano
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).