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@xxxxxxxxx> wrote: > In a series of patches, restructuring without changing > semantics should come first to make existing logic cleaner and later > enhancements on top of it easier to follow. The patch series consists of the following four patches: 0001-Introduce-ff-fast-forward-option.patch 0002-Restructuring-git-merge.sh.patch 0003-Head-reduction-before-selecting-merge-strategy.patch 0004-Introduce-fast-forward-option-only.patch The first and the last one is trivial. Head reduction is more complicated. I have split this one in two as you suggested except that the one for restructuring does not come as the first on in this series of patches. -- Sverre Hvammen Johansen
From 89fbd87b93017d8a65afd6fd27796c0e8f204c22 Mon Sep 17 00:00:00 2001 From: Sverre Hvammen Johansen <hvammen@xxxxxxxxx> 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@xxxxxxxxx> --- Documentation/fast-forward-options.txt | 44 +++ Documentation/git-merge.txt | 6 +- Documentation/git-pull.txt | 2 + Documentation/merge-options.txt | 9 +- git-merge.sh | 47 ++- git-pull.sh | 4 +- t/t7601-merge-ff-options.sh | 639 ++++++++++++++++++++++++++++++++ 7 files changed, 728 insertions(+), 23 deletions(-) create mode 100644 Documentation/fast-forward-options.txt create mode 100755 t/t7601-merge-ff-options.sh diff --git a/Documentation/fast-forward-options.txt b/Documentation/fast-forward-options.txt new file mode 100644 index 0000000..95d0e6f --- /dev/null +++ b/Documentation/fast-forward-options.txt @@ -0,0 +1,44 @@ +FAST FORWARD OPTIONS +-------------------- + +allow:: + + Do not generate a merge commit if the merge resolves as a + fast-forward, only update the branch pointer. This option is + equivalent of '--ff' without any argument. This is the + default behavior. + +never:: + Generate a merge commit even if the merge resolves as a + fast-forward. This option is equivalent of '--no-ff'. + +If your workflow is always to branch from the special branch +("master") when working on a topic and merge that back to "master", if +you happen to have worked only on a single topic and the "master" was +never advanced during the time you worked on that topic, merging the +topic back to "master" will result in a fast-forward. When you look +back that history, you will not be able to tell where the topic +started and ended by following the ancestry chain of the "master" +branch. + +Using "never fast forward" policy on such a special branch will be a +way to make sure that all commits on the first-parent ancestry of that +special branch will be merges from something else. From the history +you can determine where the topic started and ended. + +The following shows two branches forked off from "master". The branch +"master" have merged in changes from branch "topicA" twice and +"topicB" once: + +------------ + o---o---o---o---o topicA + / \ \ + ---*-------*-------*---* master + / \ / + o---o topicB +------------ + +The first merge of topicA or the only merge of topicB would have +resulted in a fast forward without '--ff=never'. Topic A consist of +those commits that can be reached from master^2 without passing +through any of the first-parent ancestries of master. diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt index c136b10..2af33d8 100644 --- a/Documentation/git-merge.txt +++ b/Documentation/git-merge.txt @@ -9,7 +9,8 @@ git-merge - Join two or more development histories together SYNOPSIS -------- [verse] -'git-merge' [-n] [--summary] [--no-commit] [--squash] [-s <strategy>]... +'git-merge' [-n] [--summary] [--no-commit] [--squash] + [-s <strategy>]... [--ff[=<fast forward option>]] [-m <msg>] <remote> <remote>... 'git-merge' <msg> HEAD <remote>... @@ -37,6 +38,9 @@ include::merge-options.txt[] least one <remote>. Specifying more than one <remote> obviously means you are trying an Octopus. + +include::fast-forward-options.txt[] + include::merge-strategies.txt[] diff --git a/Documentation/git-pull.txt b/Documentation/git-pull.txt index 3405ca0..e4e013c 100644 --- a/Documentation/git-pull.txt +++ b/Documentation/git-pull.txt @@ -52,6 +52,8 @@ include::pull-fetch-param.txt[] include::urls-remotes.txt[] +include::fast-forward-options.txt[] + include::merge-strategies.txt[] DEFAULT BEHAVIOUR diff --git a/Documentation/merge-options.txt b/Documentation/merge-options.txt index 9f1fc82..cf4881b 100644 --- a/Documentation/merge-options.txt +++ b/Documentation/merge-options.txt @@ -29,12 +29,11 @@ --no-ff:: Generate a merge commit even if the merge resolved as a - fast-forward. + fast-forward. --no-ff is an alias for --ff=never. ---ff:: - Do not generate a merge commit if the merge resolved as - a fast-forward, only update the branch pointer. This is - the default behavior of git-merge. +--ff[=<fast forward option>]:: + Select fast forward option. --ff without any argument + is an alias for --ff=allow which is the default behavior. -s <strategy>, \--strategy=<strategy>:: Use the given merge strategy; can be supplied more than diff --git a/git-merge.sh b/git-merge.sh index 7dbbb1d..17f40f2 100755 --- a/git-merge.sh +++ b/git-merge.sh @@ -12,7 +12,7 @@ summary show a diffstat at the end of the merge n,no-summary don't show a diffstat at the end of the merge squash create a single commit instead of doing a merge commit perform a commit if the merge sucesses (default) -ff allow fast forward (default) +ff? fast forward options s,strategy= merge strategy to use m,message= message to be used for the merge commit (if any) " @@ -35,7 +35,7 @@ no_fast_forward_strategies='subtree ours' no_trivial_strategies='recursive recur subtree ours' use_strategies= -allow_fast_forward=t +fast_forward=allow allow_trivial_merge=t squash= no_commit= @@ -153,8 +153,6 @@ parse_config () { --summary) show_diffstat=t ;; --squash) - test "$allow_fast_forward" = t || - die "You cannot combine --squash with --no-ff." squash=t no_commit=t ;; --no-squash) squash= no_commit= ;; @@ -163,11 +161,26 @@ parse_config () { --no-commit) no_commit=t ;; --ff) - allow_fast_forward=t ;; + case "$2" in + allow|never) + fast_forward=$2; shift ;; + -*) + fast_forward=allow ;; + *) + die "Available fast-forward options are: allow and newer" ;; + esac + ;; + --ff=*) + fast_forward=${1#--ff=} + case "$fast_forward" in + allow|never) + ;; + *) + die "Available fast-forward options are: allow and newer" ;; + esac + ;; --no-ff) - test "$squash" != t || - die "You cannot combine --squash with --no-ff." - allow_fast_forward=f ;; + fast_forward=never ;; -s|--strategy) shift case " $all_strategies " in @@ -189,6 +202,8 @@ parse_config () { esac shift done + test "$fast_forward" = allow -o "$squash" = "" || + die "You cannot combine --squash with --ff=never" args_left=$# } @@ -308,7 +323,7 @@ do do case " $s " in *" $ss "*) - allow_fast_forward=f + fast_forward=never break ;; esac @@ -334,17 +349,17 @@ case "$#" in esac echo "$head" >"$GIT_DIR/ORIG_HEAD" -case "$allow_fast_forward,$#,$common,$no_commit" in -?,*,'',*) +case "$fast_forward,$#,$common,$no_commit" in +*,*,'',*) # No common ancestors found. We need a real merge. ;; -?,1,"$1",*) +*,1,"$1",*) # If head can reach all the merge then we are up to date. # but first the most common case of merging one remote. finish_up_to_date "Already up-to-date." exit 0 ;; -t,1,"$head",*) +allow,1,"$head",*) # Again the most common case of merging one remote. echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $1)" git update-index --refresh 2>/dev/null @@ -359,11 +374,11 @@ t,1,"$head",*) dropsave exit 0 ;; -?,1,?*"$LF"?*,*) +*,1,?*"$LF"?*,*) # We are not doing octopus and not fast forward. Need a # real merge. ;; -?,1,*,) +*,1,*,) # We are not doing octopus, not fast forward, and have only # one common. git update-index --refresh 2>/dev/null @@ -481,7 +496,7 @@ done # auto resolved the merge cleanly. if test '' != "$result_tree" then - if test "$allow_fast_forward" = "t" + if test $fast_forward = allow then parents=$(git show-branch --independent "$head" "$@") else diff --git a/git-pull.sh b/git-pull.sh index 3ce32b5..2d7293a 100755 --- a/git-pull.sh +++ b/git-pull.sh @@ -4,7 +4,7 @@ # # Fetch one or more remote refs and merge it/them into the current HEAD. -USAGE='[-n | --no-summary] [--[no-]commit] [--[no-]squash] [--[no-]ff] [-s strategy]... [<fetch-options>] <repo> <head>...' +USAGE='[-n | --no-summary] [--[no-]commit] [--[no-]squash] [--ff=<ff-strategy>] [-s strategy]... [<fetch-options>] <repo> <head>...' LONG_USAGE='Fetch one or more remote refs and merge it/them into the current HEAD.' SUBDIRECTORY_OK=Yes OPTIONS_SPEC= @@ -41,6 +41,8 @@ do no_ff=--ff ;; --no-ff) no_ff=--no-ff ;; + --ff=allow|--ff=never) + no_ff=$1 ;; -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\ --strateg=*|--strategy=*|\ -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy) diff --git a/t/t7601-merge-ff-options.sh b/t/t7601-merge-ff-options.sh new file mode 100755 index 0000000..636e71e --- /dev/null +++ b/t/t7601-merge-ff-options.sh @@ -0,0 +1,639 @@ +#!/bin/sh +# +# Copyright (c) 2008 Sverre Hvammen Johansen, based on t7600 by Lars Hjemli +# + +test_description='git-merge + +Testing basic merge operations/option parsing.' + +. ./test-lib.sh + +cat >file <<EOF +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +EOF + +cat >file.1 <<EOF +1 X +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +EOF + +cat >file.5 <<EOF +1 +2 +3 +4 +5 X +6 +7 +8 +9 +10 +11 +12 +EOF + +cat >file.9 <<EOF +1 +2 +3 +4 +5 +6 +7 +8 +9 X +10 +11 +12 +EOF + +cat >result.0 <<EOF +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +EOF + +cat >result.1 <<EOF +1 X +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +EOF + +cat >result.1-5 <<EOF +1 X +2 +3 +4 +5 X +6 +7 +8 +9 +10 +11 +12 +EOF + +cat >result.9 <<EOF +1 +2 +3 +4 +5 +6 +7 +8 +9 X +10 +11 +12 +EOF + +cat >result.1-5-9 <<EOF +1 X +2 +3 +4 +5 X +6 +7 +8 +9 X +10 +11 +12 +EOF + +cat >result.1-5-9-13 <<EOF +1 X +2 +3 +4 +5 X +6 +7 +8 +9 X +10 +11 +12 +13 x +EOF + +cat >result.1-5-13 <<EOF +1 X +2 +3 +4 +5 X +6 +7 +8 +9 +10 +11 +12 +13 x +EOF + +cat >result.5-13 <<EOF +1 +2 +3 +4 +5 X +6 +7 +8 +9 +10 +11 +12 +13 x +EOF + +cat >result.1-13 <<EOF +1 X +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 x +EOF + +cat >extend <<EOF +13 x +EOF + + +create_merge_msgs() { + echo "Merge commit 'c2'" >msg.1-5 && + echo "Merge commit 'c2'; commit 'c3'" >msg.1-5-9 && + echo "Squashed commit of the following:" >squash.1 && + echo >>squash.1 && + git log --no-merges ^HEAD c1 >>squash.1 && + echo "Squashed commit of the following:" >squash.1-5 && + echo >>squash.1-5 && + git log --no-merges ^HEAD c2 >>squash.1-5 && + echo "Squashed commit of the following:" >squash.1-5-9 && + echo >>squash.1-5-9 && + git log --no-merges ^HEAD c2 c3 >>squash.1-5-9 +} + +verify_diff() { + if ! diff -u "$1" "$2" + then + echo "$3" + false + fi +} + +verify_merge() { + verify_diff "$2" "$1" "[OOPS] bad merge result" && + if test $(git ls-files -u | wc -l) -gt 0 + then + echo "[OOPS] unmerged files" + false + fi && + if ! git diff --exit-code + then + echo "[OOPS] working tree != index" + false + fi && + if test -n "$3" + then + git show -s --pretty=format:%s HEAD >msg.act && + verify_diff "$3" msg.act "[OOPS] bad merge message" + fi +} + +verify_head() { + if test "$1" != "$(git rev-parse HEAD)" + then + echo "[OOPS] HEAD != $1" + false + fi +} + +verify_parents() { + i=1 + while test $# -gt 0 + do + if test "$1" != "$(git rev-parse HEAD^$i)" + then + echo "[OOPS] HEAD^$i != $1" + return 1 + fi + i=$(expr $i + 1) + shift + done +} + +verify_mergeheads() { + i=1 + if ! test -f .git/MERGE_HEAD + then + echo "[OOPS] MERGE_HEAD is missing" + false + fi && + while test $# -gt 0 + do + head=$(head -n $i .git/MERGE_HEAD | tail -n 1) + if test "$1" != "$head" + then + echo "[OOPS] MERGE_HEAD $i != $1" + return 1 + fi + i=$(expr $i + 1) + shift + done +} + +verify_no_mergehead() { + if test -f .git/MERGE_HEAD + then + echo "[OOPS] MERGE_HEAD exists" + false + fi +} + + +test_expect_success 'setup' ' + git add file && + test_tick && + git commit -m "commit 0" && + git tag c0 && + c0=$(git rev-parse HEAD) && + + cp file.1 file && + git add file && + test_tick && + git commit -m "commit 1" && + git tag c1 && + c1=$(git rev-parse HEAD) && + test_tick && + + git reset --hard "$c0" && + cp file.5 file && + git add file && + git commit -m "commit 2" && + test_tick && + git tag c2 && + c2=$(git rev-parse HEAD) && + + git reset --hard "$c0" && + cp file.9 file && + git add file && + test_tick && + git commit -m "commit 3" && + git tag c3 && + c3=$(git rev-parse HEAD) && + test_tick && + + git reset --hard "$c1" && + cat extend >>file && + git add file && + git commit -m "commit 4" && + git tag x1 && + x1=$(git rev-parse HEAD) && + test_tick && + + git reset --hard "$c1" && + git merge "$c2" && + git tag x0 && + x0=$(git rev-parse HEAD) && + test_tick && + + git reset --hard "$c2" && + cat extend >>file && + git add file && + git commit -m "commit 5" && + git tag x2 && + x2=$(git rev-parse HEAD) && + test_tick && + + git reset --hard "$x1" && + git merge "$x0" && + git tag y1 && + y1=$(git rev-parse HEAD) && + test_tick && + + git reset --hard "$x0" && + git merge "$x2" && + git tag y2 && + y2=$(git rev-parse HEAD) && + test_tick && + + git reset --hard "$y1" && + git merge "$y2" && + git tag y3 && + y3=$(git rev-parse HEAD) && + test_tick && + git reset --hard "$c0" && + create_merge_msgs && + + git reset --hard x1 && + git clone .git clone && + git config remote.clone.url clone && + git config remote.clone.fetch "+refs/heads/*:refs/remotes/clone/*" && + + (mkdir new && cd new && git init && cp ../file.9 file2 && git add file2 && test_tick && git commit -m "commit new") && + git config remote.new.url new && + git config remote.new.fetch "+refs/heads/*:refs/remotes/new/*" +' + +test_debug 'gitk --all' + +test_expect_success 'merge c0 with c1 and c2' ' + git reset --hard c0 && + git config branch.master.mergeoptions "" && + test_tick && + git merge c1 c2 && + verify_merge file result.1-5 && + verify_parents $c1 $c2 +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with c0, c2, c0, and c1' ' + git reset --hard c1 && + git config branch.master.mergeoptions "" && + test_tick && + git merge c0 c2 c0 c1 && + verify_merge file result.1-5 && + verify_parents $c1 $c2 +' + +test_debug 'gitk --all' + +test_expect_success 'merge y2 with x0, c3, and c0' ' + git reset --hard y2 && + git config branch.master.mergeoptions "" && + test_tick && + git merge x0 c3 c0 && + verify_merge file result.1-5-9-13 && + verify_parents $y2 $c3 +' + +test_debug 'gitk --all' + +test_expect_success 'merge x0 with y2, c3, and c0' ' + git reset --hard x0 && + git config branch.master.mergeoptions "" && + test_tick && + git merge y2 c3 c0 && + verify_merge file result.1-5-9-13 && + verify_parents $y2 $c3 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge c1 with c2 and x1' ' + git reset --hard c1 && + git config branch.master.mergeoptions "" && + test_tick && + git merge c2 x1 && + verify_merge file result.1-5-13 && + verify_parents $c2 $x1 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge x0 with c1 (--squash combined with --ff=allow)' ' + git reset --hard x0 && + git config branch.master.mergeoptions "" && + test_tick && + git merge c1 --squash --ff=allow && + verify_merge file result.1-5 && + verify_head $x0 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge c1 with c2 (--squash combined with --ff=allow)' ' + git reset --hard c1 && + git config branch.master.mergeoptions "" && + test_tick && + git merge c2 --squash --ff=allow && + verify_merge file result.1-5 && + verify_head $c1 && + git commit && + verify_parents $c1 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge c1 with x0 (--no-commit combined with --ff=allow)' ' + git reset --hard c1 && + git config branch.master.mergeoptions "" && + test_tick && + git merge x0 --no-commit --ff=allow && + verify_merge file result.1-5 && + verify_parents $c1 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge x0 with c1 (--no-commit combined with --ff=allow)' ' + git reset --hard x0 && + git config branch.master.mergeoptions "" && + test_tick && + git merge c1 --no-commit --ff=allow && + verify_merge file result.1-5 && + verify_head $x0 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge c1 with c2 (--no-commit combined with --ff=allow)' ' + git reset --hard c1 && + git config branch.master.mergeoptions "" && + test_tick && + git merge c2 --no-commit --ff=allow && + verify_merge file result.1-5 && + verify_head $c1 && + git commit && + verify_parents $c1 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge c1 with x1 (pull --ff=allow)' ' + git reset --hard c1 && + test_tick && + git pull --ff=allow clone refs/heads/master && + verify_merge file result.1-13 && + verify_head $x1 +' + +test_debug 'gitk --all' + +test_expect_success 'merge x2 with x1 (pull --ff=allow)' ' + git reset --hard x2 && + test_tick && + git pull --ff=allow clone refs/heads/master && + verify_merge file result.1-5-13 && + verify_parents $x2 $x1 +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with new repository (pull --ff=allow)' ' + git reset --hard c1 && + test_tick && + git pull --ff=allow new refs/heads/master && + verify_merge file result.1 && + verify_merge file2 result.9 +' + +test_debug 'gitk --all' + +test_expect_success 'merge x0 with c1 (--squash combined with --ff=never)' ' + git reset --hard x0 && + git config branch.master.mergeoptions "" && + test_tick && + test_must_fail git merge c1 --squash --ff=never && + verify_merge file result.1-5 && + verify_head $x0 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge c1 with c2 (--squash combined with --ff=never)' ' + git reset --hard c1 && + git config branch.master.mergeoptions "" && + test_tick && + test_must_fail git merge c2 --squash --ff=never && + verify_merge file result.1 && + verify_head $c1 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge c1 with x0 (--no-commit combined with --ff=never)' ' + git reset --hard c1 && + git config branch.master.mergeoptions "" && + test_tick && + git merge x0 --no-commit --ff=never && + verify_merge file result.1-5 && + verify_head $c1 && + git commit && + verify_parents $c1 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge x0 with c1 (--no-commit combined with --ff=never)' ' + git reset --hard x0 && + git config branch.master.mergeoptions "" && + test_tick && + git merge c1 --no-commit --ff=never && + verify_merge file result.1-5 && + verify_head $x0 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge c1 with c2 (--no-commit combined with --ff=never)' ' + git reset --hard c1 && + git config branch.master.mergeoptions "" && + test_tick && + git merge c2 --no-commit --ff=never && + verify_merge file result.1-5 && + verify_head $c1 && + git commit && + verify_parents $c1 +' + +test_debug 'gitk --all' + + +test_expect_success 'merge c1 with x1 (pull --ff=never)' ' + git reset --hard c1 && + test_tick && + git pull --ff=never clone refs/heads/master && + verify_merge file result.1-13 && + verify_parents $c1 $x1 +' + +test_debug 'gitk --all' + +test_expect_success 'merge x2 with x1 (pull --ff=never)' ' + git reset --hard x2 && + test_tick && + git pull --ff=never clone refs/heads/master && + verify_merge file result.1-5-13 && + verify_parents $x2 $x1 +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with new repository (pull --ff=never)' ' + git reset --hard c1 && + test_tick && + git pull --ff=never new refs/heads/master && + verify_merge file result.1 && + verify_merge file2 result.9 +' + +test_debug 'gitk --all' + +test_done -- 1.5.3.3
From 55d0664258c1053309514b192effd33d1db4c7a0 Mon Sep 17 00:00:00 2001 From: Sverre Hvammen Johansen <hvammen@xxxxxxxxx> 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@xxxxxxxxx> --- git-merge.sh | 166 ++++++++++++++++++++++++++++++---------------------------- 1 files changed, 85 insertions(+), 81 deletions(-) diff --git a/git-merge.sh b/git-merge.sh index 17f40f2..2acd2cc 100755 --- a/git-merge.sh +++ b/git-merge.sh @@ -207,6 +207,29 @@ parse_config () { args_left=$# } +# Find real parents +# Set the following variables as followd: +# real_parents: The parents specified on the command line +# common: All common ancestors or not_queried +# ff_head: Fast forward of head +find_real_parents () { + real_parents=$(git rev-parse "$@") + real_parents=${real_parents#$LF} + if test $# = 1 + then + common=$(git merge-base --all $head "$@") + if test "$common" = $head + then + ff_head=$1 + else + ff_head=$head + fi + else + common=not_queried + ff_head=$head + fi +} + test $# != 0 || usage have_message= @@ -294,24 +317,26 @@ do done set x $remoteheads ; shift +find_real_parents "$@" + case "$use_strategies" in '') - case "$#" in - 1) - var="`git config --get pull.twohead`" + case "$real_parents" in + ?*"$LF"?*) + var="`git config --get pull.octopus`" if test -n "$var" then use_strategies="$var" else - use_strategies="$default_twohead_strategies" + use_strategies="$default_octopus_strategies" fi ;; *) - var="`git config --get pull.octopus`" + var="`git config --get pull.twohead`" if test -n "$var" then use_strategies="$var" else - use_strategies="$default_octopus_strategies" + use_strategies="$default_twohead_strategies" fi ;; esac ;; @@ -339,87 +364,66 @@ do done done -case "$#" in -1) - common=$(git merge-base --all $head "$@") - ;; -*) - common=$(git show-branch --merge-base $head "$@") - ;; -esac echo "$head" >"$GIT_DIR/ORIG_HEAD" -case "$fast_forward,$#,$common,$no_commit" in -*,*,'',*) - # No common ancestors found. We need a real merge. - ;; -*,1,"$1",*) - # If head can reach all the merge then we are up to date. - # but first the most common case of merging one remote. - finish_up_to_date "Already up-to-date." - exit 0 - ;; -allow,1,"$head",*) - # Again the most common case of merging one remote. - echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $1)" - git update-index --refresh 2>/dev/null - msg="Fast forward" - if test -n "$have_message" +if true +then + if test $head = $ff_head -a "$common" = "$real_parents" then - msg="$msg (no commit created; -m option ignored)" - fi - new_head=$(git rev-parse --verify "$1^0") && - git read-tree -v -m -u --exclude-per-directory=.gitignore $head "$new_head" && - finish "$new_head" "$msg" || exit - dropsave - exit 0 - ;; -*,1,?*"$LF"?*,*) - # We are not doing octopus and not fast forward. Need a - # real merge. - ;; -*,1,*,) - # We are not doing octopus, not fast forward, and have only - # one common. - git update-index --refresh 2>/dev/null - case "$allow_trivial_merge" in - t) - # See if it is really trivial. - git var GIT_COMMITTER_IDENT >/dev/null || exit - echo "Trying really trivial in-index merge..." - if git read-tree --trivial -m -u -v $common $head "$1" && - result_tree=$(git write-tree) - then - echo "Wonderful." - result_commit=$( - printf '%s\n' "$merge_msg" | - git commit-tree $result_tree -p HEAD -p "$1" - ) || exit - finish "$result_commit" "In-index merge" - dropsave - exit 0 - fi - echo "Nope." - esac - ;; -*) - # An octopus. If we can reach all the remote we are up to date. - up_to_date=t - for remote - do - common_one=$(git merge-base --all $head $remote) - if test "$common_one" != "$remote" + finish_up_to_date "Already up-to-date." + exit 0 + elif test $fast_forward != never -a $ff_head = "$real_parents" + then + echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $ff_head)" + git update-index --refresh 2>/dev/null + msg="Fast forward" + if test -n "$have_message" then - up_to_date=f - break + msg="$msg (no commit created; -m option ignored)" fi - done - if test "$up_to_date" = t - then - finish_up_to_date "Already up-to-date. Yeeah!" + new_head=$(git rev-parse --verify "$ff_head^0") && + git read-tree -v -m -u --exclude-per-directory=.gitignore $head "$new_head" && + finish "$new_head" "$msg" || exit + dropsave exit 0 fi +fi + +case "$real_parents" in +?*"$LF"?*) + # We have more than one parent + common=$(git show-branch --merge-base $head $real_parents) ;; +*) + # We have exactly one parent + test "$common" != not_queried || common=$(git merge-base --all $head $real_parents) + case "$common" in + ?*"$LF"?*) + # We are not doing octopus and not fast forward. Need a + # real merge. + ;; + *) + git update-index --refresh 2>/dev/null + if test "$allow_trivial_merge" = t + then + # See if it is really trivial. + git var GIT_COMMITTER_IDENT >/dev/null || exit + echo "Trying really trivial in-index merge..." + if git read-tree --trivial -m -u -v $common $head $real_parents && + result_tree=$(git write-tree) + then + echo "Wonderful." + result_commit=$( + printf '%s\n' "$merge_msg" | + git commit-tree $result_tree -p HEAD -p $real_parents + ) || exit + finish "$result_commit" "In-index merge" + dropsave + exit 0 + fi + echo "Nope." + fi ;; + esac ;; esac # We are going to make a new commit. @@ -460,7 +464,7 @@ do # Remember which strategy left the state in the working tree wt_strategy=$strategy - git-merge-$strategy $common -- "$head_arg" "$@" + git-merge-$strategy $common -- "$head_arg" $real_parents exit=$? if test "$no_commit" = t && test "$exit" = 0 then @@ -530,7 +534,7 @@ case "$best_strategy" in echo "Rewinding the tree to pristine..." restorestate echo "Using the $best_strategy to prepare resolving by hand." - git-merge-$best_strategy $common -- "$head_arg" "$@" + git-merge-$best_strategy $common -- "$head_arg" $real_parents ;; esac -- 1.5.3.3
From 2227b803ecfd47b2d5586ec923cb887f017f3b67 Mon Sep 17 00:00:00 2001 From: Sverre Hvammen Johansen <hvammen@xxxxxxxxx> 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@xxxxxxxxx> --- Documentation/git-merge.txt | 43 +++++++++++++++++++++++- git-merge.sh | 76 +++++++++++++++++++++++++++++-------------- 2 files changed, 93 insertions(+), 26 deletions(-) diff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt index 2af33d8..e94d26b 100644 --- a/Documentation/git-merge.txt +++ b/Documentation/git-merge.txt @@ -36,7 +36,7 @@ include::merge-options.txt[] <remote>:: Other branch head merged into our branch. You need at least one <remote>. Specifying more than one <remote> - obviously means you are trying an Octopus. + usually means you are trying an Octopus. include::fast-forward-options.txt[] @@ -133,6 +133,47 @@ merge (which is typically a fraction of the whole tree), you can have local modifications in your working tree as long as they do not overlap with what the merge updates. +If more than one commit are specified for the merge, git will try to +reduce the number of commits (real parents) by eliminating commits +than can be reached from other commits. The commit message will +reflect the actual commits specified but the merge strategy will be +selected based on the real parents, but always including `HEAD`. The +real parents (only including `HEAD` if it is real) are the parents +recorded in the merge commit object. + +The following shows master and three topic branches. topicB is based +on topicA, topicA is previously branched off from master, and topicC +is based on the current `HEAD` of master: + +------------ + o---o---o topicB + / + o---o---o topicA + / + o---o---o---o---o---o master + \ + o---o topicC +------------ + +A merger of master with topicA, topicB, and topicC will select the +merge strategy based on the three branches master, topicB, and topicC +(topicA is eliminated since it can be reached from topicB). topicB +and topicC are the only real parents and are therefore the only +parents recorded in the merge commit object: + +------------ + % git checkout master + % git merge topicA topicB topicC + + o---o---o topicB + / \ + o---o---o topicA \ + / \ + o---o---o---o---o---o o master + \ / + o---o topicC +------------ + When there are conflicts, these things happen: 1. `HEAD` stays the same. diff --git a/git-merge.sh b/git-merge.sh index 2acd2cc..5398606 100755 --- a/git-merge.sh +++ b/git-merge.sh @@ -209,24 +209,41 @@ parse_config () { # Find real parents # Set the following variables as followd: -# real_parents: The parents specified on the command line +# real_parents: The real parents except fast forward of head # common: All common ancestors or not_queried # ff_head: Fast forward of head find_real_parents () { - real_parents=$(git rev-parse "$@") - real_parents=${real_parents#$LF} - if test $# = 1 + if test $fast_forward = never then - common=$(git merge-base --all $head "$@") - if test "$common" = $head + real_parents=$(git rev-parse "$@") + ff_head=$head + common=not_queried + else + if test $# = 1 then - ff_head=$1 + common=$(git merge-base --all $head "$1") + if test "$common" = $head + then + real_parents= + ff_head=$1 + elif test "$common" = "$1" + then + real_parents= + ff_head=$head + else + real_parents=$1 + ff_head=$head + + fi else - ff_head=$head + real_parents=$(git show-branch --independent $head "$@") + # Here we may actually lie about which bransh is ff of head. + # This will preserve the order the user gave. + ff_head=${real_parents%%$LF*} + real_parents=${real_parents#$ff_head} + real_parents=${real_parents#$LF} + common=not_queried fi - else - common=not_queried - ff_head=$head fi } @@ -319,6 +336,12 @@ set x $remoteheads ; shift find_real_parents "$@" +if test -n "$real_parents" +then + test $head = $ff_head || + real_parents="$ff_head$LF$real_parents" +fi + case "$use_strategies" in '') case "$real_parents" in @@ -366,13 +389,13 @@ done echo "$head" >"$GIT_DIR/ORIG_HEAD" -if true +if test -z "$real_parents" then - if test $head = $ff_head -a "$common" = "$real_parents" + if test $head = $ff_head then finish_up_to_date "Already up-to-date." exit 0 - elif test $fast_forward != never -a $ff_head = "$real_parents" + elif test $fast_forward != never then echo "Updating $(git rev-parse --short $head)..$(git rev-parse --short $ff_head)" git update-index --refresh 2>/dev/null @@ -386,6 +409,14 @@ then finish "$new_head" "$msg" || exit dropsave exit 0 + else + real_parents="$ff_head" + ff_head=$head + fi +else + if test $head != $ff_head -a $fast_forward = never + then + real_parents="$ff_head$LF$real_parents" fi fi @@ -500,17 +531,12 @@ done # auto resolved the merge cleanly. if test '' != "$result_tree" then - if test $fast_forward = allow - then - parents=$(git show-branch --independent "$head" "$@") - else - parents=$(git rev-parse "$head" "$@") - fi - parents=$(echo "$parents" | sed -e 's/^/-p /') - result_commit=$(printf '%s\n' "$merge_msg" | git commit-tree $result_tree $parents) || exit - finish "$result_commit" "Merge made by $wt_strategy." - dropsave - exit 0 + test $head = $ff_head && real_parents="$head$LF$real_parents" + parents=$(echo "$real_parents" | sed -e 's/^/-p /') + result_commit=$(printf '%s\n' "$merge_msg" | git commit-tree $result_tree $parents) || exit + finish "$result_commit" "Merge made by $wt_strategy." + dropsave + exit 0 fi # Pick the result from the best strategy and have the user fix it up. -- 1.5.3.3
From df159d4275d25a57898b757489f3d675e715efa3 Mon Sep 17 00:00:00 2001 From: Sverre Hvammen Johansen <hvammen@xxxxxxxxx> 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@xxxxxxxxx> --- 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