Multi-backend merge driver.
authorJunio C Hamano <junkio@cox.net>
Thu, 8 Sep 2005 20:47:12 +0000 (13:47 -0700)
committerJunio C Hamano <junkio@cox.net>
Sun, 11 Sep 2005 01:30:41 +0000 (18:30 -0700)
The new command 'git merge' takes the current head and one or more
remote heads, with the commit log message for the automated case.

If the heads being merged are simple fast-forwards, it acts the
same way as the current 'git resolve'.  Otherwise, it tries
different merge strategies and takes the result from the one that
succeeded auto-merging, if there is any.

If no merge strategy succeeds auto-merging, their results are
evaluated for number of paths needed for hand resolving, and the
one with the least number of such paths is left in the working
tree.  The user is asked to resolve them by hand and make a
commit manually.

The calling convention from the 'git merge' driver to merge
strategy programs is very simple:

 - A strategy program is to be called 'git-merge-<strategy>'.

 - They take input of this form:

<common1> <common2> ... '--' <head> <remote1> <remote2>...

   That is, one or more the common ancestors, double dash, the
   current head, and one or more remote heads being merged into
   the current branch.

 - Before a strategy program is called, the working tree is
   matched to the current <head>.

 - The strategy program exits with status code 0 when it
   successfully auto-merges the given heads.  It should do
   update-cache for all the merged paths when it does so -- the
   index file will be used to record the merge result as a
   commit by the driver.

 - The strategy program exits with status code 1 when it leaves
   conflicts behind.  It should do update-cache for all the
   merged paths that it successfully auto-merged, and leave the
   cache entry in the index file as the same as <head> for paths
   it could not auto-merge, and leave its best-effort result
   with conflict markers in the working tree when it does so.

 - The strategy program exists with status code other than 0 or
   1 if it does not handle the given merge at all.

As examples, this commit comes with merge strategies based on
'git resolve' and 'git octopus'.

Signed-off-by: Junio C Hamano <junkio@cox.net>
Makefile
git-commit.sh
git-merge-octopus.sh [new file with mode: 0755]
git-merge-resolve.sh [new file with mode: 0755]
git-merge.sh [new file with mode: 0755]

index 38f7054..7ef9861 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -73,7 +73,8 @@ SCRIPT_SH = \
        git-repack.sh git-request-pull.sh git-reset.sh \
        git-resolve.sh git-revert.sh git-sh-setup.sh git-status.sh \
        git-tag.sh git-verify-tag.sh git-whatchanged.sh git.sh \
-       git-applymbox.sh git-applypatch.sh
+       git-applymbox.sh git-applypatch.sh \
+       git-merge.sh git-merge-resolve.sh git-merge-octopus.sh
 
 SCRIPT_PERL = \
        git-archimport.perl git-cvsimport.perl git-relink.perl \
index 741444a..d8bfc3c 100755 (executable)
@@ -158,7 +158,7 @@ if [ ! -r "$GIT_DIR/HEAD" ]; then
        PARENTS=""
 else
        if [ -f "$GIT_DIR/MERGE_HEAD" ]; then
-               PARENTS="-p HEAD -p MERGE_HEAD"
+               PARENTS="-p HEAD "`sed -e 's/^/-p /' "$GIT_DIR/MERGE_HEAD"`
        fi
        if test "$use_commit" != ""
        then
diff --git a/git-merge-octopus.sh b/git-merge-octopus.sh
new file mode 100755 (executable)
index 0000000..aa1cd2f
--- /dev/null
@@ -0,0 +1,86 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+# Resolve two or more trees.
+#
+
+# The first parameters up to -- are merge bases; the rest are heads.
+bases= head= remotes= sep_seen=
+for arg
+do
+       case ",$sep_seen,$head,$arg," in
+       *,--,)
+               sep_seen=yes
+               ;;
+       ,yes,,*)
+               head=$arg
+               ;;
+       ,yes,*)
+               remotes="$remotes$arg "
+               ;;
+       *)
+               bases="$bases$arg "
+               ;;
+       esac
+done
+
+# Reject if this is not an Octopus -- resolve should be used instead.
+case "$remotes" in
+?*' '?*)
+       ;;
+*)
+       exit 2 ;;
+esac
+
+# MRC is the current "merge reference commit"
+# MRT is the current "merge result tree"
+
+MRC=$head MSG= PARENT="-p $head"
+MRT=$(git-write-tree)
+CNT=1 ;# counting our head
+NON_FF_MERGE=0
+for SHA1 in $remotes
+do
+       common=$(git-merge-base $MRC $SHA1) ||
+               die "Unable to find common commit with $SHA1"
+
+       if test "$common" = $SHA1
+       then
+               echo "Already up-to-date with $SHA1"
+               continue
+       fi
+
+       CNT=`expr $CNT + 1`
+       PARENT="$PARENT -p $SHA1"
+
+       if test "$common,$NON_FF_MERGE" = "$MRC,0"
+       then
+               # The first head being merged was a fast-forward.
+               # Advance MRC to the head being merged, and use that
+               # tree as the intermediate result of the merge.
+               # We still need to count this as part of the parent set.
+
+               echo "Fast forwarding to: $SHA1"
+               git-read-tree -u -m $head $SHA1 || exit
+               MRC=$SHA1 MRT=$(git-write-tree)
+               continue
+       fi
+
+       NON_FF_MERGE=1
+
+       echo "Trying simple merge with $SHA1"
+       git-read-tree -u -m $common $MRT $SHA1 || exit 2
+       next=$(git-write-tree 2>/dev/null)
+       if test $? -ne 0
+       then
+               echo "Simple merge did not work, trying automatic merge."
+               git-merge-index -o git-merge-one-file -a ||
+               exit 2 ; # Automatic merge failed; should not be doing Octopus
+               next=$(git-write-tree 2>/dev/null)
+       fi
+       MRC=$common
+       MRT=$next
+done
+
+exit 0
diff --git a/git-merge-resolve.sh b/git-merge-resolve.sh
new file mode 100755 (executable)
index 0000000..f0a19b4
--- /dev/null
@@ -0,0 +1,80 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Linus Torvalds
+#
+# Resolve two trees.
+
+# The first parameters up to -- are merge bases; the rest are heads.
+bases= head= remotes= sep_seen=
+for arg
+do
+       case ",$sep_seen,$head,$arg," in
+       *,--,)
+               sep_seen=yes
+               ;;
+       ,yes,,*)
+               head=$arg
+               ;;
+       ,yes,*)
+               remotes="$remotes$arg "
+               ;;
+       *)
+               bases="$bases$arg "
+               ;;
+       esac
+done
+
+# Give up if we are given more than two remotes -- not handling octopus.
+case "$remotes" in
+?*' '?*)
+       exit 2 ;;
+esac
+
+# Find an optimum merge base if there are more than one candidates.
+case "$bases" in
+?*' '?*)
+       echo "Trying to find the optimum merge base."
+       G=.tmp-index$$
+       best=
+       best_cnt=-1
+       for c in $bases
+       do
+               rm -f $G
+               GIT_INDEX_FILE=$G git-read-tree -m $c $head $remotes \
+                        2>/dev/null || continue
+               # Count the paths that are unmerged.
+               cnt=`GIT_INDEX_FILE=$G git-ls-files --unmerged | wc -l`
+               if test $best_cnt -le 0 -o $cnt -le $best_cnt
+               then
+                       best=$c
+                       best_cnt=$cnt
+                       if test "$best_cnt" -eq 0
+                       then
+                               # Cannot do any better than all trivial merge.
+                               break
+                       fi
+               fi
+       done
+       rm -f $G
+       common="$best"
+       ;;
+*)
+       common="$bases"
+       ;;
+esac
+
+git-update-index --refresh 2>/dev/null
+git-read-tree -u -m $common $head $remotes || exit 2
+echo "Trying simple merge."
+if result_tree=$(git-write-tree  2>/dev/null)
+then
+       exit 0
+else
+       echo "Simple merge failed, trying Automatic merge."
+       if git-merge-index -o git-merge-one-file -a
+       then
+               exit 0
+       else
+               exit 1
+       fi
+fi
diff --git a/git-merge.sh b/git-merge.sh
new file mode 100755 (executable)
index 0000000..9926e45
--- /dev/null
@@ -0,0 +1,212 @@
+#!/bin/sh
+#
+# Copyright (c) 2005 Junio C Hamano
+#
+
+. git-sh-setup || die "Not a git archive"
+
+LF='
+'
+
+usage () {
+    die "git-merge [-n] [-s <strategy>]... <merge-message> <head> <remote>+"
+}
+
+# all_strategies='resolve multibase fredrik octopus'
+
+all_strategies='resolve multibase octopus'
+default_strategies='resolve multibase octopus'
+use_strategies=
+
+dropheads() {
+       rm -f -- "$GIT_DIR/MERGE_HEAD" || exit 1
+}
+
+summary() {
+       case "$no_summary" in
+       '')
+               git-diff-tree -p -M $head "$1" |
+               git-apply --stat --summary
+               ;;
+       esac
+}
+
+while case "$#" in 0) break ;; esac
+do
+       case "$1" in
+       -n|--n|--no|--no-|--no-s|--no-su|--no-sum|--no-summ|\
+               --no-summa|--no-summar|--no-summary)
+               no_summary=t ;;
+       -s=*|--s=*|--st=*|--str=*|--stra=*|--strat=*|--strate=*|\
+               --strateg=*|--strategy=*|\
+       -s|--s|--st|--str|--stra|--strat|--strate|--strateg|--strategy)
+               case "$#,$1" in
+               *,*=*)
+                       strategy=`expr "$1" : '-[^=]*=\(.*\)'` ;;
+               0,*)
+                       usage ;;
+               *)
+                       strategy="$2"
+                       shift ;;
+               esac
+               case " $all_strategies " in
+               *" $strategy "*)
+                       use_strategies="$use_strategies$strategy " ;;
+               *)
+                       die "available strategies are: $all_strategies" ;;
+               esac
+               ;;
+       -*)     usage ;;
+       *)      break ;;
+       esac
+       shift
+done
+
+case "$use_strategies" in
+'')
+       use_strategies=$default_strategies
+       ;;
+esac
+test "$#" -le 2 && usage ;# we need at least two heads.
+
+merge_msg="$1"
+shift
+head=$(git-rev-parse --verify "$1"^0) || usage
+shift
+
+# All the rest are remote heads
+for remote
+do
+       git-rev-parse --verify "$remote"^0 >/dev/null ||
+           die "$remote - not something we can merge"
+done
+
+common=$(git-show-branch --merge-base $head "$@")
+echo "$head" >"$GIT_DIR/ORIG_HEAD"
+
+case "$#,$common" in
+*,'')
+       die "Unable to find common commit between $head and $*"
+       ;;
+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
+       echo "Already up-to-date. Yeeah!"
+       dropheads
+       exit 0
+       ;;
+1,"$head")
+       # Again the most common case of merging one remote.
+       echo "Updating from $head to $1."
+       git-update-index --refresh 2>/dev/null
+       git-read-tree -u -m $head "$1" || exit 1
+       git-rev-parse --verify "$1^0" > "$GIT_DIR/HEAD"
+       summary "$1"
+       dropheads
+       exit 0
+       ;;
+1,*)
+       # We are not doing octopus and not fast forward.  Need a
+       # real merge.
+       ;;
+*)
+       # 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 $head $remote)
+               if test "$common_one" != "$remote"
+               then
+                       up_to_date=f
+                       break
+               fi
+       done
+       if test "$up_to_date" = t
+       then
+               echo "Already up-to-date. Yeeah!"
+               dropheads
+               exit 0
+       fi
+       ;;
+esac
+
+# At this point we need a real merge.  Require that the tree matches
+# exactly our head.
+
+git-update-index --refresh &&
+test '' = "`git-diff-index --cached --name-only $head`" || {
+       die "Need real merge but the working tree has local changes."
+}
+
+result_tree= best_cnt=-1 best_strategy= wt_strategy=
+for strategy in $use_strategies
+do
+    test "$wt_strategy" = '' || {
+       echo "Rewinding the tree to pristine..."
+       git reset --hard $head
+    }
+    echo "Trying merge strategy $strategy..."
+    wt_strategy=$strategy
+    git-merge-$strategy $common -- $head "$@" || {
+
+       # The backend exits with 1 when conflicts are left to be resolved,
+       # with 2 when it does not handle the given merge at all.
+
+       exit=$?
+       if test "$exit" -eq 1
+       then
+           cnt=`{
+               git-diff-files --name-only
+               git-ls-files --unmerged
+           } | wc -l`
+           if test $best_cnt -le 0 -o $cnt -le $best_cnt
+           then
+               best_strategy=$strategy
+               best_cnt=$cnt
+           fi
+       fi
+       continue
+    }
+
+    # Automerge succeeded.
+    result_tree=$(git-write-tree) && break
+done
+
+# If we have a resulting tree, that means the strategy module
+# auto resolved the merge cleanly.
+if test '' != "$result_tree"
+then
+    parents="-p $head"
+    for remote
+    do
+        parents="$parents -p $remote"
+    done
+    result_commit=$(echo "$merge_msg" | git-commit-tree $result_tree $parents)
+    echo "Committed merge $result_commit, made by $wt_strategy."
+    echo $result_commit >"$GIT_DIR/HEAD"
+    summary $result_commit
+    dropheads
+    exit 0
+fi
+
+# Pick the result from the best strategy and have the user fix it up.
+case "$best_strategy" in
+'')
+       git reset --hard $head
+       die "No merge strategy handled the merge."
+       ;;
+"$wt_strategy")
+       # We already have its result in the working tree.
+       ;;
+*)
+       echo "Rewinding the tree to pristine..."
+       git reset --hard $head
+       echo "Using the $best_strategy to prepare resolving by hand."
+       git-merge-$best_strategy $common -- $head "$@"
+       ;;
+esac
+for remote
+do
+       echo $remote
+done >"$GIT_DIR/MERGE_HEAD"
+die "Automatic merge failed; fix up by hand"