sha1-lookup: make selection of 'middle' less aggressive
[gitweb.git] / git-rebase--interactive.sh
index ff38a22edfa60a759c5e68d9e6968d11531e72d1..8aa73712ca11f4527f4b8d42bf3a14de2a16c39c 100755 (executable)
@@ -13,6 +13,7 @@
 USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose]
        [--onto <branch>] <upstream> [<branch>])'
 
+OPTIONS_SPEC=
 . git-sh-setup
 require_work_tree
 
@@ -29,6 +30,11 @@ test -d "$REWRITTEN" && PRESERVE_MERGES=t
 test -f "$DOTEST"/strategy && STRATEGY="$(cat "$DOTEST"/strategy)"
 test -f "$DOTEST"/verbose && VERBOSE=t
 
+GIT_CHERRY_PICK_HELP="  After resolving the conflicts,
+mark the corrected paths with 'git add <paths>', and
+run 'git rebase --continue'"
+export GIT_CHERRY_PICK_HELP
+
 warn () {
        echo "$*" >&2
 }
@@ -36,14 +42,14 @@ warn () {
 output () {
        case "$VERBOSE" in
        '')
-               "$@" > "$DOTEST"/output 2>&1
+               output=$("$@" 2>&1 )
                status=$?
-               test $status != 0 &&
-                       cat "$DOTEST"/output
+               test $status != 0 && printf "%s\n" "$output"
                return $status
-       ;;
+               ;;
        *)
                "$@"
+               ;;
        esac
 }
 
@@ -63,30 +69,38 @@ comment_for_reflog () {
        ''|rebase*)
                GIT_REFLOG_ACTION="rebase -i ($1)"
                export GIT_REFLOG_ACTION
+               ;;
        esac
 }
 
+last_count=
 mark_action_done () {
        sed -e 1q < "$TODO" >> "$DONE"
        sed -e 1d < "$TODO" >> "$TODO".new
        mv -f "$TODO".new "$TODO"
-       count=$(($(wc -l < "$DONE")))
-       total=$(($count+$(wc -l < "$TODO")))
-       printf "Rebasing (%d/%d)\r" $count $total
-       test -z "$VERBOSE" || echo
+       count=$(grep -c '^[^#]' < "$DONE")
+       total=$(($count+$(grep -c '^[^#]' < "$TODO")))
+       if test "$last_count" != "$count"
+       then
+               last_count=$count
+               printf "Rebasing (%d/%d)\r" $count $total
+               test -z "$VERBOSE" || echo
+       fi
 }
 
 make_patch () {
-       parent_sha1=$(git rev-parse --verify "$1"^ 2> /dev/null)
+       parent_sha1=$(git rev-parse --verify "$1"^) ||
+               die "Cannot get patch for $1^"
        git diff-tree -p "$parent_sha1".."$1" > "$DOTEST"/patch
+       test -f "$DOTEST"/message ||
+               git cat-file commit "$1" | sed "1,/^$/d" > "$DOTEST"/message
+       test -f "$DOTEST"/author-script ||
+               get_author_ident_from_commit "$1" > "$DOTEST"/author-script
 }
 
 die_with_patch () {
-       test -f "$DOTEST"/message ||
-               git cat-file commit $sha1 | sed "1,/^$/d" > "$DOTEST"/message
-       test -f "$DOTEST"/author-script ||
-               get_author_ident_from_commit $sha1 > "$DOTEST"/author-script
        make_patch "$1"
+       git rerere
        die "$2"
 }
 
@@ -95,15 +109,20 @@ die_abort () {
        die "$1"
 }
 
+has_action () {
+       grep '^[^#]' "$1" >/dev/null
+}
+
 pick_one () {
        no_ff=
        case "$1" in -n) sha1=$2; no_ff=t ;; *) sha1=$1 ;; esac
        output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
        test -d "$REWRITTEN" &&
                pick_one_preserving_merges "$@" && return
-       parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null)
+       parent_sha1=$(git rev-parse --verify $sha1^) ||
+               die "Could not get the parent of $sha1"
        current_sha1=$(git rev-parse --verify HEAD)
-       if test $no_ff$current_sha1 = $parent_sha1; then
+       if test "$no_ff$current_sha1" = "$parent_sha1"; then
                output git reset --hard $sha1
                test "a$1" = a-n && output git reset --soft $current_sha1
                sha1=$(git rev-parse --short $sha1)
@@ -129,7 +148,7 @@ pick_one_preserving_merges () {
        fast_forward=t
        preserve=t
        new_parents=
-       for p in $(git rev-list --parents -1 $sha1 | cut -d -f2-)
+       for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -f2-)
        do
                if test -f "$REWRITTEN"/$p
                then
@@ -141,43 +160,49 @@ pick_one_preserving_merges () {
                                ;; # do nothing; that parent is already there
                        *)
                                new_parents="$new_parents $new_p"
+                               ;;
                        esac
                fi
        done
        case $fast_forward in
        t)
                output warn "Fast forward to $sha1"
-               test $preserve=f && echo $sha1 > "$REWRITTEN"/$sha1
+               test $preserve = f || echo $sha1 > "$REWRITTEN"/$sha1
                ;;
        f)
                test "a$1" = a-n && die "Refusing to squash a merge: $sha1"
 
-               first_parent=$(expr "$new_parents" : " \([^ ]*\)")
+               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"
 
                echo $sha1 > "$DOTEST"/current-commit
                case "$new_parents" in
-               \ *\ *)
+               ' '*' '*)
                        # redo merge
                        author_script=$(get_author_ident_from_commit $sha1)
                        eval "$author_script"
-                       msg="$(git cat-file commit $sha1 | \
-                               sed -e '1,/^$/d' -e "s/[\"\\]/\\\\&/g")"
+                       msg="$(git cat-file commit $sha1 | sed -e '1,/^$/d')"
                        # No point in merging the first parent, that's HEAD
                        new_parents=${new_parents# $first_parent}
-                       # NEEDSWORK: give rerere a chance
-                       if ! output git merge $STRATEGY -m "$msg" $new_parents
+                       if ! GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
+                               GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
+                               GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
+                               output git merge $STRATEGY -m "$msg" \
+                                       $new_parents
                        then
-                               echo "$msg" > "$GIT_DIR"/MERGE_MSG
+                               git rerere
+                               printf "%s\n" "$msg" > "$GIT_DIR"/MERGE_MSG
                                die Error redoing merge $sha1
                        fi
                        ;;
                *)
                        output git cherry-pick "$@" ||
                                die_with_patch $sha1 "Could not pick $sha1"
+                       ;;
                esac
+               ;;
        esac
 }
 
@@ -193,17 +218,19 @@ nth_string () {
 make_squash_message () {
        if test -f "$SQUASH_MSG"; then
                COUNT=$(($(sed -n "s/^# This is [^0-9]*\([1-9][0-9]*\).*/\1/p" \
-                       < "$SQUASH_MSG" | tail -n 1)+1))
+                       < "$SQUASH_MSG" | sed -ne '$p')+1))
                echo "# This is a combination of $COUNT commits."
-               sed -n "2,\$p" < "$SQUASH_MSG"
+               sed -e 1d -e '2,/^./{
+                       /^$/d
+               }' <"$SQUASH_MSG"
        else
                COUNT=2
                echo "# This is a combination of two commits."
                echo "# The first commit's message is:"
                echo
                git cat-file commit HEAD | sed -e '1,/^$/d'
-               echo
        fi
+       echo
        echo "# This is the $(nth_string $COUNT) commit message:"
        echo
        git cat-file commit $1 | sed -e '1,/^$/d'
@@ -214,52 +241,58 @@ peek_next_command () {
 }
 
 do_next () {
-       test -f "$DOTEST"/message && rm "$DOTEST"/message
-       test -f "$DOTEST"/author-script && rm "$DOTEST"/author-script
+       rm -f "$DOTEST"/message "$DOTEST"/author-script \
+               "$DOTEST"/amend || exit
        read command sha1 rest < "$TODO"
        case "$command" in
-       \#|'')
+       '#'*|'')
                mark_action_done
                ;;
-       pick)
+       pick|p)
                comment_for_reflog pick
 
                mark_action_done
                pick_one $sha1 ||
                        die_with_patch $sha1 "Could not apply $sha1... $rest"
                ;;
-       edit)
+       edit|e)
                comment_for_reflog edit
 
                mark_action_done
                pick_one $sha1 ||
                        die_with_patch $sha1 "Could not apply $sha1... $rest"
                make_patch $sha1
+               : > "$DOTEST"/amend
                warn
                warn "You can amend the commit now, with"
                warn
                warn "  git commit --amend"
                warn
+               warn "Once you are satisfied with your changes, run"
+               warn
+               warn "  git rebase --continue"
+               warn
                exit 0
                ;;
-       squash)
+       squash|s)
                comment_for_reflog squash
 
-               test -z "$(grep -ve '^$' -e '^#' < $DONE)" &&
+               has_action "$DONE" ||
                        die "Cannot 'squash' without a previous commit"
 
                mark_action_done
                make_squash_message $sha1 > "$MSG"
                case "$(peek_next_command)" in
-               squash)
+               squash|s)
                        EDIT_COMMIT=
                        USE_OUTPUT=output
                        cp "$MSG" "$SQUASH_MSG"
-               ;;
+                       ;;
                *)
                        EDIT_COMMIT=-e
                        USE_OUTPUT=
-                       test -f "$SQUASH_MSG" && rm "$SQUASH_MSG"
+                       rm -f "$SQUASH_MSG" || exit
+                       ;;
                esac
 
                failed=f
@@ -267,23 +300,27 @@ do_next () {
                output git reset --soft HEAD^
                pick_one -n $sha1 || failed=t
                echo "$author_script" > "$DOTEST"/author-script
-               case $failed in
-               f)
+               if test $failed = f
+               then
                        # This is like --amend, but with a different message
                        eval "$author_script"
-                       export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
-                       $USE_OUTPUT git commit -F "$MSG" $EDIT_COMMIT
-                       ;;
-               t)
+                       GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
+                       GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
+                       GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
+                       $USE_OUTPUT git commit --no-verify -F "$MSG" $EDIT_COMMIT || failed=t
+               fi
+               if test $failed = t
+               then
                        cp "$MSG" "$GIT_DIR"/MERGE_MSG
                        warn
                        warn "Could not apply $sha1... $rest"
                        die_with_patch $sha1 ""
-               esac
+               fi
                ;;
        *)
                warn "Unknown command: $command $sha1 $rest"
                die_with_patch $sha1 "Please fix this in the file $TODO."
+               ;;
        esac
        test -s "$TODO" && return
 
@@ -296,17 +333,27 @@ do_next () {
                test -f "$DOTEST"/current-commit &&
                        current_commit=$(cat "$DOTEST"/current-commit) &&
                        git rev-parse HEAD > "$REWRITTEN"/$current_commit
-               NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD)
+               if test -f "$REWRITTEN"/$OLDHEAD
+               then
+                       NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD)
+               else
+                       NEWHEAD=$OLDHEAD
+               fi
        else
                NEWHEAD=$(git rev-parse HEAD)
        fi &&
-       message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" &&
-       git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD &&
-       git symbolic-ref HEAD $HEADNAME && {
+       case $HEADNAME in
+       refs/*)
+               message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" &&
+               git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD &&
+               git symbolic-ref HEAD $HEADNAME
+               ;;
+       esac && {
                test ! -f "$DOTEST"/verbose ||
                        git diff-tree --stat $(cat "$DOTEST"/head)..HEAD
        } &&
        rm -rf "$DOTEST" &&
+       git gc --auto &&
        warn "Successfully rebased and updated $HEADNAME."
 
        exit
@@ -327,14 +374,28 @@ do
 
                test -d "$DOTEST" || die "No interactive rebase running"
 
-               # commit if necessary
-               git rev-parse --verify HEAD > /dev/null &&
-               git update-index --refresh &&
-               git diff-files --quiet &&
-               ! git diff-index --cached --quiet HEAD -- &&
-               . "$DOTEST"/author-script &&
-               export GIT_AUTHOR_NAME GIT_AUTHOR_NAME GIT_AUTHOR_DATE &&
-               git commit -F "$DOTEST"/message -e
+               # Sanity check
+               git rev-parse --verify HEAD >/dev/null ||
+                       die "Cannot read HEAD"
+               git update-index --refresh && git diff-files --quiet ||
+                       die "Working tree is dirty"
+
+               # do we have anything to commit?
+               if git diff-index --cached --quiet HEAD --
+               then
+                       : Nothing to commit -- skip this
+               else
+                       . "$DOTEST"/author-script ||
+                               die "Cannot find the author identity"
+                       if test -f "$DOTEST"/amend
+                       then
+                               git reset --soft HEAD^ ||
+                               die "Cannot rewind the HEAD"
+                       fi
+                       export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE &&
+                       git commit --no-verify -F "$DOTEST"/message -e ||
+                       die "Could not commit staged changes."
+               fi
 
                require_clean_work_tree
                do_rest
@@ -342,11 +403,16 @@ do
        --abort)
                comment_for_reflog abort
 
+               git rerere clear
                test -d "$DOTEST" || die "No interactive rebase running"
 
                HEADNAME=$(cat "$DOTEST"/head-name)
                HEAD=$(cat "$DOTEST"/head)
-               git symbolic-ref HEAD $HEADNAME &&
+               case $HEADNAME in
+               refs/*)
+                       git symbolic-ref HEAD $HEADNAME
+                       ;;
+               esac &&
                output git reset --hard $HEAD &&
                rm -rf "$DOTEST"
                exit
@@ -354,6 +420,7 @@ do
        --skip)
                comment_for_reflog skip
 
+               git rerere clear
                test -d "$DOTEST" || die "No interactive rebase running"
 
                output git reset --hard && do_rest
@@ -369,7 +436,7 @@ do
                        shift ;;
                esac
                ;;
-       --merge)
+       -m|--merge)
                # we use merge anyway
                ;;
        -C*)
@@ -407,7 +474,6 @@ do
 
                require_clean_work_tree
 
-               mkdir "$DOTEST" || die "Could not create temporary $DOTEST"
                if test ! -z "$2"
                then
                        output git show-ref --verify --quiet "refs/heads/$2" ||
@@ -419,11 +485,13 @@ do
                HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?"
                UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base"
 
+               mkdir "$DOTEST" || die "Could not create temporary $DOTEST"
+
                test -z "$ONTO" && ONTO=$UPSTREAM
 
                : > "$DOTEST"/interactive || die "Could not mark as interactive"
-               git symbolic-ref HEAD > "$DOTEST"/head-name ||
-                       die "Could not get HEAD"
+               git symbolic-ref HEAD > "$DOTEST"/head-name 2> /dev/null ||
+                       echo "detached HEAD" > "$DOTEST"/head-name
 
                echo $HEAD > "$DOTEST"/head
                echo $UPSTREAM > "$DOTEST"/upstream
@@ -453,8 +521,13 @@ do
                SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
                SHORTHEAD=$(git rev-parse --short $HEAD)
                SHORTONTO=$(git rev-parse --short $ONTO)
-               cat > "$TODO" << EOF
-# Rebasing $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO
+               git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
+                       --abbrev=7 --reverse --left-right --cherry-pick \
+                       $UPSTREAM...$HEAD | \
+                       sed -n "s/^>/pick /p" > "$TODO"
+               cat >> "$TODO" << EOF
+
+# Rebase $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO
 #
 # Commands:
 #  pick = use commit
@@ -462,24 +535,22 @@ do
 #  squash = use commit, but meld into previous commit
 #
 # If you remove a line here THAT COMMIT WILL BE LOST.
+# However, if you remove everything, the rebase will be aborted.
 #
 EOF
-               git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
-                       --abbrev=7 --reverse --left-right --cherry-pick \
-                       $UPSTREAM...$HEAD | \
-                       sed -n "s/^>/pick /p" >> "$TODO"
 
-               test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
+               has_action "$TODO" ||
                        die_abort "Nothing to do"
 
                cp "$TODO" "$TODO".backup
                git_editor "$TODO" ||
                        die "Could not execute editor"
 
-               test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
+               has_action "$TODO" ||
                        die_abort "Nothing to do"
 
                output git checkout $ONTO && do_rest
+               ;;
        esac
        shift
 done