Merge branch 'sh/rebase-i-p'
authorJunio C Hamano <gitster@pobox.com>
Mon, 3 Nov 2008 00:36:33 +0000 (16:36 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 3 Nov 2008 00:36:33 +0000 (16:36 -0800)
* sh/rebase-i-p:
git-rebase--interactive.sh: comparision with == is bashism
rebase-i-p: minimum fix to obvious issues
rebase-i-p: if todo was reordered use HEAD as the rewritten parent
rebase-i-p: do not include non-first-parent commits touching UPSTREAM
rebase-i-p: only list commits that require rewriting in todo
rebase-i-p: fix 'no squashing merges' tripping up non-merges
rebase-i-p: delay saving current-commit to REWRITTEN if squashing
rebase-i-p: use HEAD for updating the ref instead of mapping OLDHEAD
rebase-i-p: test to exclude commits from todo based on its parents

git-rebase--interactive.sh
t/t3411-rebase-preserve-around-merges.sh [new file with mode: 0644]
index 39f8d73dfa4377922fd8ebc406643d73338916d0..1172e47571dfe1d6dd088381d63045fd5eae3db3 100755 (executable)
@@ -173,13 +173,18 @@ pick_one_preserving_merges () {
 
        if test -f "$DOTEST"/current-commit
        then
-               current_commit=$(cat "$DOTEST"/current-commit) &&
-               git rev-parse HEAD > "$REWRITTEN"/$current_commit &&
-               rm "$DOTEST"/current-commit ||
-               die "Cannot write current commit's replacement sha1"
+               if test "$fast_forward" = t
+               then
+                       cat "$DOTEST"/current-commit | while read current_commit
+                       do
+                               git rev-parse HEAD > "$REWRITTEN"/$current_commit
+                       done
+                       rm "$DOTEST"/current-commit ||
+                       die "Cannot write current commit's replacement sha1"
+               fi
        fi
 
-       echo $sha1 > "$DOTEST"/current-commit
+       echo $sha1 >> "$DOTEST"/current-commit
 
        # rewrite parents; if none were rewritten, we can fast-forward.
        new_parents=
@@ -192,6 +197,15 @@ pick_one_preserving_merges () {
                if test -f "$REWRITTEN"/$p
                then
                        new_p=$(cat "$REWRITTEN"/$p)
+
+                       # If the todo reordered commits, and our parent is marked for
+                       # rewriting, but hasn't been gotten to yet, assume the user meant to
+                       # drop it on top of the current HEAD
+                       if test -z "$new_p"
+                       then
+                               new_p=$(git rev-parse HEAD)
+                       fi
+
                        test $p != $new_p && fast_forward=f
                        case "$new_parents" in
                        *$new_p*)
@@ -217,15 +231,19 @@ pick_one_preserving_merges () {
                        die "Cannot fast forward to $sha1"
                ;;
        f)
-               test "a$1" = a-n && die "Refusing to squash a merge: $sha1"
-
                first_parent=$(expr "$new_parents" : ' \([^ ]*\)')
-               # detach HEAD to current parent
-               output git checkout $first_parent 2> /dev/null ||
-                       die "Cannot move HEAD to $first_parent"
+
+               if [ "$1" != "-n" ]
+               then
+                       # detach HEAD to current parent
+                       output git checkout $first_parent 2> /dev/null ||
+                               die "Cannot move HEAD to $first_parent"
+               fi
 
                case "$new_parents" in
                ' '*' '*)
+                       test "a$1" = a-n && die "Refusing to squash a merge: $sha1"
+
                        # redo merge
                        author_script=$(get_author_ident_from_commit $sha1)
                        eval "$author_script"
@@ -379,20 +397,7 @@ do_next () {
        HEADNAME=$(cat "$DOTEST"/head-name) &&
        OLDHEAD=$(cat "$DOTEST"/head) &&
        SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) &&
-       if test -d "$REWRITTEN"
-       then
-               test -f "$DOTEST"/current-commit &&
-                       current_commit=$(cat "$DOTEST"/current-commit) &&
-                       git rev-parse HEAD > "$REWRITTEN"/$current_commit
-               if test -f "$REWRITTEN"/$OLDHEAD
-               then
-                       NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD)
-               else
-                       NEWHEAD=$OLDHEAD
-               fi
-       else
-               NEWHEAD=$(git rev-parse HEAD)
-       fi &&
+       NEWHEAD=$(git rev-parse HEAD) &&
        case $HEADNAME in
        refs/*)
                message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" &&
@@ -591,18 +596,69 @@ first and then run 'git rebase --continue' again."
                                echo $ONTO > "$REWRITTEN"/$c ||
                                        die "Could not init rewritten commits"
                        done
+                       # No cherry-pick because our first pass is to determine
+                       # parents to rewrite and skipping dropped commits would
+                       # prematurely end our probe
                        MERGES_OPTION=
+                       first_after_upstream="$(git rev-list --reverse --first-parent $UPSTREAM..$HEAD | head -n 1)"
                else
-                       MERGES_OPTION=--no-merges
+                       MERGES_OPTION="--no-merges --cherry-pick"
                fi
 
                SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
                SHORTHEAD=$(git rev-parse --short $HEAD)
                SHORTONTO=$(git rev-parse --short $ONTO)
                git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
-                       --abbrev=7 --reverse --left-right --cherry-pick \
+                       --abbrev=7 --reverse --left-right --topo-order \
                        $UPSTREAM...$HEAD | \
-                       sed -n "s/^>/pick /p" > "$TODO"
+                       sed -n "s/^>//p" | while read shortsha1 rest
+               do
+                       if test t != "$PRESERVE_MERGES"
+                       then
+                               echo "pick $shortsha1 $rest" >> "$TODO"
+                       else
+                               sha1=$(git rev-parse $shortsha1)
+                               preserve=t
+                               for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -f2-)
+                               do
+                                       if test -f "$REWRITTEN"/$p -a \( $p != $UPSTREAM -o $sha1 = $first_after_upstream \)
+                                       then
+                                               preserve=f
+                                       fi
+                               done
+                               if test f = "$preserve"
+                               then
+                                       touch "$REWRITTEN"/$sha1
+                                       echo "pick $shortsha1 $rest" >> "$TODO"
+                               fi
+                       fi
+               done
+
+               # Watch for commits that been dropped by --cherry-pick
+               if test t = "$PRESERVE_MERGES"
+               then
+                       mkdir "$DROPPED"
+                       # Save all non-cherry-picked changes
+                       git rev-list $UPSTREAM...$HEAD --left-right --cherry-pick | \
+                               sed -n "s/^>//p" > "$DOTEST"/not-cherry-picks
+                       # Now all commits and note which ones are missing in
+                       # not-cherry-picks and hence being dropped
+                       git rev-list $UPSTREAM..$HEAD |
+                       while read rev
+                       do
+                               if test -f "$REWRITTEN"/$rev -a "$(grep "$rev" "$DOTEST"/not-cherry-picks)" = ""
+                               then
+                                       # Use -f2 because if rev-list is telling us this commit is
+                                       # not worthwhile, we don't want to track its multiple heads,
+                                       # just the history of its first-parent for others that will
+                                       # be rebasing on top of it
+                                       git rev-list --parents -1 $rev | cut -d' ' -f2 > "$DROPPED"/$rev
+                                       short=$(git rev-list -1 --abbrev-commit --abbrev=7 $rev)
+                                       grep -v "^[a-z][a-z]* $short" <"$TODO" > "${TODO}2" ; mv "${TODO}2" "$TODO"
+                                       rm "$REWRITTEN"/$rev
+                               fi
+                       done
+               fi
                test -s "$TODO" || echo noop >> "$TODO"
                cat >> "$TODO" << EOF
 
@@ -618,28 +674,6 @@ first and then run 'git rebase --continue' again."
 #
 EOF
 
-               # Watch for commits that been dropped by --cherry-pick
-               if test t = "$PRESERVE_MERGES"
-               then
-                       mkdir "$DROPPED"
-                       # drop the --cherry-pick parameter this time
-                       git rev-list $MERGES_OPTION --abbrev-commit \
-                               --abbrev=7 $UPSTREAM...$HEAD --left-right | \
-                               sed -n "s/^>//p" | while read rev
-                       do
-                               grep --quiet "$rev" "$TODO"
-                               if [ $? -ne 0 ]
-                               then
-                                       # Use -f2 because if rev-list is telling this commit is not
-                                       # worthwhile, we don't want to track its multiple heads,
-                                       # just the history of its first-parent for others that will
-                                       # be rebasing on top of us
-                                       full=$(git rev-parse $rev)
-                                       git rev-list --parents -1 $rev | cut -d' ' -f2 > "$DROPPED"/$full
-                               fi
-                       done
-               fi
-
                has_action "$TODO" ||
                        die_abort "Nothing to do"
 
diff --git a/t/t3411-rebase-preserve-around-merges.sh b/t/t3411-rebase-preserve-around-merges.sh
new file mode 100644 (file)
index 0000000..aacfaae
--- /dev/null
@@ -0,0 +1,135 @@
+#!/bin/sh
+#
+# Copyright (c) 2008 Stephen Haberman
+#
+
+test_description='git rebase preserve merges
+
+This test runs git rebase with and tries to squash a commit from after a merge
+to before the merge.
+'
+. ./test-lib.sh
+
+# Copy/paste from t3404-rebase-interactive.sh
+echo "#!$SHELL_PATH" >fake-editor.sh
+cat >> fake-editor.sh <<\EOF
+case "$1" in
+*/COMMIT_EDITMSG)
+       test -z "$FAKE_COMMIT_MESSAGE" || echo "$FAKE_COMMIT_MESSAGE" > "$1"
+       test -z "$FAKE_COMMIT_AMEND" || echo "$FAKE_COMMIT_AMEND" >> "$1"
+       exit
+       ;;
+esac
+test -z "$EXPECT_COUNT" ||
+       test "$EXPECT_COUNT" = $(sed -e '/^#/d' -e '/^$/d' < "$1" | wc -l) ||
+       exit
+test -z "$FAKE_LINES" && exit
+grep -v '^#' < "$1" > "$1".tmp
+rm -f "$1"
+cat "$1".tmp
+action=pick
+for line in $FAKE_LINES; do
+       case $line in
+       squash|edit)
+               action="$line";;
+       *)
+               echo sed -n "${line}s/^pick/$action/p"
+               sed -n "${line}p" < "$1".tmp
+               sed -n "${line}s/^pick/$action/p" < "$1".tmp >> "$1"
+               action=pick;;
+       esac
+done
+EOF
+
+test_set_editor "$(pwd)/fake-editor.sh"
+chmod a+x fake-editor.sh
+
+# set up two branches like this:
+#
+# A1 - B1 - D1 - E1 - F1
+#       \        /
+#        -- C1 --
+
+test_expect_success 'setup' '
+       touch a &&
+       touch b &&
+       git add a &&
+       git commit -m A1 &&
+       git tag A1
+       git add b &&
+       git commit -m B1 &&
+       git tag B1 &&
+       git checkout -b branch &&
+       touch c &&
+       git add c &&
+       git commit -m C1 &&
+       git checkout master &&
+       touch d &&
+       git add d &&
+       git commit -m D1 &&
+       git merge branch &&
+       touch f &&
+       git add f &&
+       git commit -m F1 &&
+       git tag F1
+'
+
+# Should result in:
+#
+# A1 - B1 - D2 - E2
+#       \        /
+#        -- C1 --
+#
+test_expect_success 'squash F1 into D1' '
+       FAKE_LINES="1 squash 3 2" git rebase -i -p B1 &&
+       test "$(git rev-parse HEAD^2)" = "$(git rev-parse branch)" &&
+       test "$(git rev-parse HEAD~2)" = "$(git rev-parse B1)" &&
+       git tag E2
+'
+
+# Start with:
+#
+# A1 - B1 - D2 - E2
+#  \
+#   G1 ---- L1 ---- M1
+#    \             /
+#     H1 -- J1 -- K1
+#      \         /
+#        -- I1 --
+#
+# And rebase G1..M1 onto E2
+
+test_expect_success 'rebase two levels of merge' '
+       git checkout -b branch2 A1 &&
+       touch g &&
+       git add g &&
+       git commit -m G1 &&
+       git checkout -b branch3 &&
+       touch h
+       git add h &&
+       git commit -m H1 &&
+       git checkout -b branch4 &&
+       touch i &&
+       git add i &&
+       git commit -m I1 &&
+       git tag I1 &&
+       git checkout branch3 &&
+       touch j &&
+       git add j &&
+       git commit -m J1 &&
+       git merge I1 --no-commit &&
+       git commit -m K1 &&
+       git tag K1 &&
+       git checkout branch2 &&
+       touch l &&
+       git add l &&
+       git commit -m L1 &&
+       git merge K1 --no-commit &&
+       git commit -m M1 &&
+       GIT_EDITOR=: git rebase -i -p E2 &&
+       test "$(git rev-parse HEAD~3)" = "$(git rev-parse E2)" &&
+       test "$(git rev-parse HEAD~2)" = "$(git rev-parse HEAD^2^2~2)" &&
+       test "$(git rev-parse HEAD^2^1^1)" = "$(git rev-parse HEAD^2^2^1)"
+'
+
+test_done