Merge branch 'do/rebase-i-arbitrary'
[gitweb.git] / git-rebase--interactive.sh
index b835a2975987861cdea59c271aee1b3dea63cee6..2ff211cbaa928275857577535f02ae92cb407f2f 100755 (executable)
@@ -28,6 +28,7 @@ abort              abort rebasing process and restore original branch
 skip               skip current patch and continue rebasing process
 no-verify          override pre-rebase hook from stopping the operation
 root               rebase all reachable commmits up to the root(s)
+autosquash         move commits that begin with squash!/fixup! under -i
 "
 
 . git-sh-setup
@@ -95,12 +96,20 @@ AUTHOR_SCRIPT="$DOTEST"/author-script
 # command is processed, this file is deleted.
 AMEND="$DOTEST"/amend
 
+# For the post-rewrite hook, we make a list of rewritten commits and
+# their new sha1s.  The rewritten-pending list keeps the sha1s of
+# commits that have been processed, but not committed yet,
+# e.g. because they are waiting for a 'squash' command.
+REWRITTEN_LIST="$DOTEST"/rewritten-list
+REWRITTEN_PENDING="$DOTEST"/rewritten-pending
+
 PRESERVE_MERGES=
 STRATEGY=
 ONTO=
 VERBOSE=
 OK_TO_SKIP_PRE_REBASE=
 REBASE_ROOT=
+AUTOSQUASH=
 
 GIT_CHERRY_PICK_HELP="  After resolving the conflicts,
 mark the corrected paths with 'git add <paths>', and
@@ -196,6 +205,7 @@ make_patch () {
 }
 
 die_with_patch () {
+       echo "$1" > "$DOTEST"/stopped-sha
        make_patch "$1"
        git rerere
        die "$2"
@@ -213,15 +223,15 @@ has_action () {
 # Run command with GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
 # GIT_AUTHOR_DATE exported from the current environment.
 do_with_author () {
-       GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
-       GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
-       GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
-       "$@"
+       (
+               export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
+               "$@"
+       )
 }
 
 pick_one () {
-       no_ff=
-       case "$1" in -n) sha1=$2; no_ff=t ;; *) sha1=$1 ;; esac
+       ff=--ff
+       case "$1" in -n) sha1=$2; ff= ;; *) sha1=$1 ;; esac
        output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
        test -d "$REWRITTEN" &&
                pick_one_preserving_merges "$@" && return
@@ -230,16 +240,7 @@ pick_one () {
                output git cherry-pick "$@"
                return
        fi
-       parent_sha1=$(git rev-parse --verify $sha1^) ||
-               die "Could not get the parent of $sha1"
-       current_sha1=$(git rev-parse --verify HEAD)
-       if test -z "$no_ff" -a "$current_sha1" = "$parent_sha1"
-       then
-               output git reset --hard $sha1
-               output warn Fast-forward to $(git rev-parse --short $sha1)
-       else
-               output git cherry-pick "$@"
-       fi
+       output git cherry-pick $ff "$@"
 }
 
 pick_one_preserving_merges () {
@@ -346,6 +347,7 @@ pick_one_preserving_merges () {
                                printf "%s\n" "$msg" > "$GIT_DIR"/MERGE_MSG
                                die_with_patch $sha1 "Error redoing merge $sha1"
                        fi
+                       echo "$sha1 $(git rev-parse HEAD^0)" >> "$REWRITTEN_LIST"
                        ;;
                *)
                        output git cherry-pick "$@" ||
@@ -376,7 +378,7 @@ update_squash_messages () {
                        sed -e 1d -e '2,/^./{
                                /^$/d
                        }' <"$SQUASH_MSG".bak
-               } >$SQUASH_MSG
+               } >"$SQUASH_MSG"
        else
                commit_message HEAD > "$FIXUP_MSG" || die "Cannot write $FIXUP_MSG"
                COUNT=2
@@ -385,7 +387,7 @@ update_squash_messages () {
                        echo "# The first commit's message is:"
                        echo
                        cat "$FIXUP_MSG"
-               } >$SQUASH_MSG
+               } >"$SQUASH_MSG"
        fi
        case $1 in
        squash)
@@ -401,11 +403,11 @@ update_squash_messages () {
                echo
                commit_message $2 | sed -e 's/^/#       /'
                ;;
-       esac >>$SQUASH_MSG
+       esac >>"$SQUASH_MSG"
 }
 
 peek_next_command () {
-       sed -n -e "/^#/d" -e "/^$/d" -e "s/ .*//p" -e "q" < "$TODO"
+       sed -n -e "/^#/d" -e '/^$/d' -e "s/ .*//p" -e "q" < "$TODO"
 }
 
 # A squash/fixup has failed.  Prepare the long version of the squash
@@ -423,6 +425,26 @@ die_failed_squash() {
        die_with_patch $1 ""
 }
 
+flush_rewritten_pending() {
+       test -s "$REWRITTEN_PENDING" || return
+       newsha1="$(git rev-parse HEAD^0)"
+       sed "s/$/ $newsha1/" < "$REWRITTEN_PENDING" >> "$REWRITTEN_LIST"
+       rm -f "$REWRITTEN_PENDING"
+}
+
+record_in_rewritten() {
+       oldsha1="$(git rev-parse $1)"
+       echo "$oldsha1" >> "$REWRITTEN_PENDING"
+
+       case "$(peek_next_command)" in
+           squash|s|fixup|f)
+               ;;
+           *)
+               flush_rewritten_pending
+               ;;
+       esac
+}
+
 do_next () {
        rm -f "$MSG" "$AUTHOR_SCRIPT" "$AMEND" || exit
        read command sha1 rest < "$TODO"
@@ -436,6 +458,7 @@ do_next () {
                mark_action_done
                pick_one $sha1 ||
                        die_with_patch $sha1 "Could not apply $sha1... $rest"
+               record_in_rewritten $sha1
                ;;
        reword|r)
                comment_for_reflog reword
@@ -443,7 +466,8 @@ do_next () {
                mark_action_done
                pick_one $sha1 ||
                        die_with_patch $sha1 "Could not apply $sha1... $rest"
-               git commit --amend
+               git commit --amend --no-post-rewrite
+               record_in_rewritten $sha1
                ;;
        edit|e)
                comment_for_reflog edit
@@ -451,6 +475,7 @@ do_next () {
                mark_action_done
                pick_one $sha1 ||
                        die_with_patch $sha1 "Could not apply $sha1... $rest"
+               echo "$sha1" > "$DOTEST"/stopped-sha
                make_patch $sha1
                git rev-parse --verify HEAD > "$AMEND"
                warn "Stopped at $sha1... $rest"
@@ -507,6 +532,7 @@ do_next () {
                        rm -f "$SQUASH_MSG" "$FIXUP_MSG"
                        ;;
                esac
+               record_in_rewritten $sha1
                ;;
        *)
                warn "Unknown command: $command $sha1 $rest"
@@ -535,6 +561,15 @@ do_next () {
                test ! -f "$DOTEST"/verbose ||
                        git diff-tree --stat $(cat "$DOTEST"/head)..HEAD
        } &&
+       {
+               git notes copy --for-rewrite=rebase < "$REWRITTEN_LIST" ||
+               true # we don't care if this copying failed
+       } &&
+       if test -x "$GIT_DIR"/hooks/post-rewrite &&
+               test -s "$REWRITTEN_LIST"; then
+               "$GIT_DIR"/hooks/post-rewrite rebase < "$REWRITTEN_LIST"
+               true # we don't care if this hook failed
+       fi &&
        rm -rf "$DOTEST" &&
        git gc --auto &&
        warn "Successfully rebased and updated $HEADNAME."
@@ -569,7 +604,12 @@ skip_unnecessary_picks () {
                esac
                echo "$command${sha1:+ }$sha1${rest:+ }$rest" >&$fd
        done <"$TODO" >"$TODO.new" 3>>"$DONE" &&
-       mv -f "$TODO".new "$TODO" ||
+       mv -f "$TODO".new "$TODO" &&
+       case "$(peek_next_command)" in
+       squash|s|fixup|f)
+               record_in_rewritten "$ONTO"
+               ;;
+       esac ||
        die "Could not skip unnecessary pick commands"
 }
 
@@ -589,6 +629,56 @@ get_saved_options () {
        test -f "$DOTEST"/rebase-root && REBASE_ROOT=t
 }
 
+# Rearrange the todo list that has both "pick sha1 msg" and
+# "pick sha1 fixup!/squash! msg" appears in it so that the latter
+# comes immediately after the former, and change "pick" to
+# "fixup"/"squash".
+rearrange_squash () {
+       sed -n -e 's/^pick \([0-9a-f]*\) \(squash\)! /\1 \2 /p' \
+               -e 's/^pick \([0-9a-f]*\) \(fixup\)! /\1 \2 /p' \
+               "$1" >"$1.sq"
+       test -s "$1.sq" || return
+
+       used=
+       while read pick sha1 message
+       do
+               case " $used" in
+               *" $sha1 "*) continue ;;
+               esac
+               echo "$pick $sha1 $message"
+               while read squash action msg
+               do
+                       case "$message" in
+                       "$msg"*)
+                               echo "$action $squash $action! $msg"
+                               used="$used$squash "
+                               ;;
+                       esac
+               done <"$1.sq"
+       done >"$1.rearranged" <"$1"
+       cat "$1.rearranged" >"$1"
+       rm -f "$1.sq" "$1.rearranged"
+}
+
+LF='
+'
+parse_onto () {
+       case "$1" in
+       *...*)
+               if      left=${1%...*} right=${1#*...} &&
+                       onto=$(git merge-base --all ${left:-HEAD} ${right:-HEAD})
+               then
+                       case "$onto" in
+                       ?*"$LF"?* | '')
+                               exit 1 ;;
+                       esac
+                       echo "$onto"
+                       exit 0
+               fi
+       esac
+       git rev-parse --verify "$1^0"
+}
+
 while test $# != 0
 do
        case "$1" in
@@ -635,6 +725,8 @@ first and then run 'git rebase --continue' again."
                        }
                fi
 
+               record_in_rewritten "$(cat "$DOTEST"/stopped-sha)"
+
                require_clean_work_tree
                do_rest
                ;;
@@ -693,9 +785,12 @@ first and then run 'git rebase --continue' again."
        --root)
                REBASE_ROOT=t
                ;;
+       --autosquash)
+               AUTOSQUASH=t
+               ;;
        --onto)
                shift
-               ONTO=$(git rev-parse --verify "$1") ||
+               ONTO=$(parse_onto "$1") ||
                        die "Does not point to a valid commit: $1"
                ;;
        --)
@@ -728,8 +823,6 @@ first and then run 'git rebase --continue' again."
 
                if test ! -z "$1"
                then
-                       output git show-ref --verify --quiet "refs/heads/$1" ||
-                               die "Invalid branchname: $1"
                        output git checkout "$1" ||
                                die "Could not checkout $1"
                fi
@@ -845,6 +938,7 @@ first and then run 'git rebase --continue' again."
                fi
 
                test -s "$TODO" || echo noop >> "$TODO"
+               test -n "$AUTOSQUASH" && rearrange_squash "$TODO"
                cat >> "$TODO" << EOF
 
 # Rebase $SHORTREVISIONS onto $SHORTONTO
@@ -866,7 +960,7 @@ EOF
 
                cp "$TODO" "$TODO".backup
                git_editor "$TODO" ||
-                       die "Could not execute editor"
+                       die_abort "Could not execute editor"
 
                has_action "$TODO" ||
                        die_abort "Nothing to do"