A patch for this feature is attached. I intend to write more tests for this patch next weekend. Any comments are welcome. -- Sverre Hvammen Johansen
From 1283f5ea17b493a9224fa7a65bb233962bc9ea41 Mon Sep 17 00:00:00 2001 From: Sverre Hvammen Johansen <sj@xxxxxxxxxxx> Date: Tue, 22 Jan 2008 00:29:37 -0800 Subject: [PATCH] Merge strategy single A new merge strategy, single is introduces. This merge strategy 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. Known issues: More tests are needed. Add documentation. Better handling of fast forward of HEAD when doing real merge. Signed-off-by: Sverre Hvammen Johansen <hvammen@xxxxxxxxx> --- git-merge.sh | 238 ++++++++++++++++++++++++++++++++++++------------------ t/t7600-merge.sh | 172 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 332 insertions(+), 78 deletions(-) diff --git a/git-merge.sh b/git-merge.sh index 1c123a3..57f2f00 100755 --- a/git-merge.sh +++ b/git-merge.sh @@ -28,7 +28,7 @@ test -z "$(git ls-files -u)" || LF=' ' -all_strategies='recur recursive octopus resolve stupid ours subtree' +all_strategies='single recur recursive octopus resolve stupid ours subtree' default_twohead_strategies='recursive' default_octopus_strategies='octopus' no_fast_forward_strategies='subtree ours' @@ -167,7 +167,12 @@ parse_config () { shift case " $all_strategies " in *" $1 "*) - use_strategies="$use_strategies$1 " ;; + if test "$use_strategies" = single -o "$1" = single -o -z "$use_strategies" + then + use_strategies="$1" + else + use_strategies="$use_strategies $1" + fi ;; *) die "available strategies are: $all_strategies" ;; esac @@ -274,24 +279,113 @@ 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)" + 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)." + 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)." + 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 $first_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 independt + # 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 +} + +find_real_parents "$@" + +if test -n "$real_parents" -a "$use_strategies" != single -a $head != $ff_head +then + # We currently don't handle ff_head + if test -n "$real_parents" + then + real_parents="$ff_head $real_parents" + else + real_parents=$ff_head + fi + ff_head=$head +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 ;; @@ -319,87 +413,75 @@ 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 "$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. +if test -n "$real_parents" +then + if test "$use_strategies" = single + then + die "Merge strategy single can not handle more than one real parent" + fi +elif test $head = $ff_head +then 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)" +elif test $allow_fast_forward = t +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 msg="$msg (no commit created; -m option ignored)" fi - new_head=$(git rev-parse --verify "$1^0") && + 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 - ;; -?,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" - then - up_to_date=f - break - fi - done - if test "$up_to_date" = t +else + if test "$use_strategies" = "single" then - finish_up_to_date "Already up-to-date. Yeeah!" - exit 0 + die "Merge strategy single can not fast forward when --no-ff is specified" + else + real_parents=$ff_head + 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. @@ -440,7 +522,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 @@ -478,9 +560,9 @@ if test '' != "$result_tree" then if test "$allow_fast_forward" = "t" 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 @@ -510,7 +592,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/t/t7600-merge.sh b/t/t7600-merge.sh index 50c51c8..fe01941 100755 --- a/t/t7600-merge.sh +++ b/t/t7600-merge.sh @@ -57,6 +57,18 @@ cat >file.9 <<EOF 9 X EOF +cat >result.0 <<EOF +1 +2 +3 +4 +5 +6 +7 +8 +9 +EOF + cat >result.1 <<EOF 1 X 2 @@ -437,4 +449,164 @@ test_expect_success 'merge c0 with c1 (ff overrides no-ff)' ' test_debug 'gitk --all' +test_expect_success 'merge c0 with c1 (-s single in config)' ' + git reset --hard c0 && + git config branch.master.mergeoptions "-s single" && + git merge c1 && + test_tick && + verify_merge file result.1 && + verify_head $c1 +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with c0 (--strategy=single in config)' ' + git reset --hard c1 && + git config branch.master.mergeoptions "--strategy single" && + git merge c0 && + verify_merge file result.1 && + verify_head $c1 +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with c2 (--strategy=single in config)' ' + git reset --hard c1 && + test_tick && + git config branch.master.mergeoptions "--strategy single" && + 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 (strategy=single)' ' + git reset --hard c0 && + test_tick && + git merge c1 --strategy=single && + verify_merge file result.1 && + verify_head $c1 +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with c0 (strategy single)' ' + git reset --hard c1 && + test_tick && + git merge c0 --strategy single && + verify_merge file result.1 && + verify_head $c1 +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with c2 (-s single)' ' + git reset --hard c1 && + test_tick && + if git merge c2 -s single + then + false + else + verify_merge file result.1 && + verify_head $c1 + fi +' + +test_debug 'gitk --all' + +test_expect_success 'merge c0 with c1 and c2 (-s single)' ' + git reset --hard c0 && + if git merge c1 c2 -s single + then + false + else + verify_merge file result.0 && + verify_head $c0 + fi +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with c0 (-s single and no-ff)' ' + git reset --hard c1 && + test_tick && + git merge -s single --no-ff c0 && + verify_merge file result.1 && + verify_head $c1 +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with c2 (--strategy=single and no-ff)' ' + git reset --hard c1 && + git config branch.master.mergeoptions "--no-ff" && + test_tick && + if git merge c2 --strategy=single + 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 and -s single)' ' + git reset --hard c0 && + git config branch.master.mergeoptions "-s single" && + test_tick && + if git merge --no-ff c1 + then + false + else + verify_merge file result.0 && + verify_head $c0 + fi +' + +test_debug 'gitk --all' + +test_expect_success 'merge c1 with c2 (ff and -s single)' ' + git reset --hard c1 && + git config branch.master.mergeoptions "-s single" && + test_tick && + if git merge --ff c2 + then + false + else + verify_merge file result.1 && + verify_head $c1 + fi +' + +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_done -- 1.5.3.3