i18n: unpack-trees: avoid substituting only a verb in sentences
[gitweb.git] / git-rebase--interactive.sh
index 85f0e93eee1400ba19d36caa75904435297a3f04..4cde685b43a4463bac5f96bcb4f8ed8b3e9347be 100644 (file)
@@ -1,11 +1,8 @@
-#!/bin/sh
+# This shell script fragment is sourced by git-rebase to implement
+# its interactive mode.  "git rebase --interactive" makes it easy
+# to fix up commits in the middle of a series and rearrange commits.
 #
 # Copyright (c) 2006 Johannes E. Schindelin
-
-# SHORT DESCRIPTION
-#
-# This script makes it easy to fix up commits in the middle of a series,
-# and rearrange commits.
 #
 # The original idea comes from Eric W. Biederman, in
 # http://article.gmane.org/gmane.comp.version-control.git/22407
@@ -80,6 +77,18 @@ amend="$state_dir"/amend
 rewritten_list="$state_dir"/rewritten-list
 rewritten_pending="$state_dir"/rewritten-pending
 
+# Work around Git for Windows' Bash whose "read" does not strip CRLF
+# and leaves CR at the end instead.
+cr=$(printf "\015")
+
+strategy_args=${strategy:+--strategy=$strategy}
+eval '
+       for strategy_opt in '"$strategy_opts"'
+       do
+               strategy_args="$strategy_args -X$(git rev-parse --sq-quote "${strategy_opt#--}")"
+       done
+'
+
 GIT_CHERRY_PICK_HELP="$resolvemsg"
 export GIT_CHERRY_PICK_HELP
 
@@ -123,6 +132,16 @@ mark_action_done () {
        fi
 }
 
+# Put the last action marked done at the beginning of the todo list
+# again. If there has not been an action marked done yet, leave the list of
+# items on the todo list unchanged.
+reschedule_last_action () {
+       tail -n 1 "$done" | cat - "$todo" >"$todo".new
+       sed -e \$d <"$done" >"$done".new
+       mv -f "$todo".new "$todo"
+       mv -f "$done".new "$done"
+}
+
 append_todo_help () {
        git stripspace --comment-lines >>"$todo" <<\EOF
 
@@ -133,11 +152,21 @@ Commands:
  s, squash = use commit, but meld into previous commit
  f, fixup = like "squash", but discard this commit's log message
  x, exec = run command (the rest of the line) using shell
+ d, drop = remove commit
 
 These lines can be re-ordered; they are executed from top to bottom.
 
+EOF
+       if test $(get_missing_commit_check_level) = error
+       then
+               git stripspace --comment-lines >>"$todo" <<\EOF
+Do not remove any line. Use 'drop' explicitly to remove a commit.
+EOF
+       else
+               git stripspace --comment-lines >>"$todo" <<\EOF
 If you remove a line here THAT COMMIT WILL BE LOST.
 EOF
+       fi
 }
 
 make_patch () {
@@ -170,9 +199,10 @@ exit_with_patch () {
        echo "$1" > "$state_dir"/stopped-sha
        make_patch $1
        git rev-parse --verify HEAD > "$amend"
+       gpg_sign_opt_quoted=${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")}
        warn "You can amend the commit now, with"
        warn
-       warn "  git commit --amend"
+       warn "  git commit --amend $gpg_sign_opt_quoted"
        warn
        warn "Once you are satisfied with your changes, run"
        warn
@@ -239,7 +269,15 @@ pick_one () {
 
        test -d "$rewritten" &&
                pick_one_preserving_merges "$@" && return
-       output git cherry-pick $empty_args $ff "$@"
+       output eval git cherry-pick \
+                       ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \
+                       "$strategy_args" $empty_args $ff "$@"
+
+       # If cherry-pick dies it leaves the to-be-picked commit unrecorded. Reschedule
+       # previous task so this commit is not lost.
+       ret=$?
+       case "$ret" in [01]) ;; *) reschedule_last_action ;; esac
+       return $ret
 }
 
 pick_one_preserving_merges () {
@@ -340,9 +378,10 @@ pick_one_preserving_merges () {
                        msg_content="$(commit_message $sha1)"
                        # No point in merging the first parent, that's HEAD
                        new_parents=${new_parents# $first_parent}
-                       if ! do_with_author output \
-                               git merge --no-ff ${strategy:+-s $strategy} -m \
-                                       "$msg_content" $new_parents
+                       merge_args="--no-log --no-ff"
+                       if ! do_with_author output eval \
+                       'git merge ${gpg_sign_opt:+"$gpg_sign_opt"} \
+                               $merge_args $strategy_args -m "$msg_content" $new_parents'
                        then
                                printf "%s\n" "$msg_content" > "$GIT_DIR"/MERGE_MSG
                                die_with_patch $sha1 "Error redoing merge $sha1"
@@ -350,7 +389,9 @@ pick_one_preserving_merges () {
                        echo "$sha1 $(git rev-parse HEAD^0)" >> "$rewritten_list"
                        ;;
                *)
-                       output git cherry-pick "$@" ||
+                       output eval git cherry-pick \
+                               ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \
+                               "$strategy_args" "$@" ||
                                die_with_patch $sha1 "Could not pick $sha1"
                        ;;
                esac
@@ -461,7 +502,8 @@ do_pick () {
                           --no-post-rewrite -n -q -C $1 &&
                        pick_one -n $1 &&
                        git commit --allow-empty --allow-empty-message \
-                                  --amend --no-post-rewrite -n -q -C $1 ||
+                                  --amend --no-post-rewrite -n -q -C $1 \
+                                  ${gpg_sign_opt:+"$gpg_sign_opt"} ||
                        die_with_patch $1 "Could not apply $1... $2"
        else
                pick_one $1 ||
@@ -470,10 +512,14 @@ do_pick () {
 }
 
 do_next () {
-       rm -f "$msg" "$author_script" "$amend" || exit
+       rm -f "$msg" "$author_script" "$amend" "$state_dir"/stopped-sha || exit
        read -r command sha1 rest < "$todo"
        case "$command" in
-       "$comment_char"*|''|noop)
+       "$comment_char"*|''|noop|drop|d)
+               mark_action_done
+               ;;
+       "$cr")
+               # Work around CR left by "read" (e.g. with Git for Windows' Bash).
                mark_action_done
                ;;
        pick|p)
@@ -488,7 +534,7 @@ do_next () {
 
                mark_action_done
                do_pick $sha1 "$rest"
-               git commit --amend --no-post-rewrite || {
+               git commit --amend --no-post-rewrite ${gpg_sign_opt:+"$gpg_sign_opt"} || {
                        warn "Could not amend commit after successfully picking $sha1... $rest"
                        warn "This is most likely due to an empty commit message, or the pre-commit hook"
                        warn "failed. If the pre-commit hook failed, you may need to resolve the issue before"
@@ -533,19 +579,22 @@ do_next () {
                squash|s|fixup|f)
                        # This is an intermediate commit; its message will only be
                        # used in case of trouble.  So use the long version:
-                       do_with_author output git commit --amend --no-verify -F "$squash_msg" ||
+                       do_with_author output git commit --amend --no-verify -F "$squash_msg" \
+                               ${gpg_sign_opt:+"$gpg_sign_opt"} ||
                                die_failed_squash $sha1 "$rest"
                        ;;
                *)
                        # This is the final command of this squash/fixup group
                        if test -f "$fixup_msg"
                        then
-                               do_with_author git commit --amend --no-verify -F "$fixup_msg" ||
+                               do_with_author git commit --amend --no-verify -F "$fixup_msg" \
+                                       ${gpg_sign_opt:+"$gpg_sign_opt"} ||
                                        die_failed_squash $sha1 "$rest"
                        else
                                cp "$squash_msg" "$GIT_DIR"/SQUASH_MSG || exit
                                rm -f "$GIT_DIR"/MERGE_MSG
-                               do_with_author git commit --amend --no-verify -F "$GIT_DIR"/SQUASH_MSG -e ||
+                               do_with_author git commit --amend --no-verify -F "$GIT_DIR"/SQUASH_MSG -e \
+                                       ${gpg_sign_opt:+"$gpg_sign_opt"} ||
                                        die_failed_squash $sha1 "$rest"
                        fi
                        rm -f "$squash_msg" "$fixup_msg"
@@ -557,10 +606,7 @@ do_next () {
                read -r command rest < "$todo"
                mark_action_done
                printf 'Executing: %s\n' "$rest"
-               # "exec" command doesn't take a sha1 in the todo-list.
-               # => can't just use $sha1 here.
-               git rev-parse --verify HEAD > "$state_dir"/stopped-sha
-               ${SHELL:-@SHELL_PATH@} -c "$rest" # Actual execution
+               "${SHELL:-@SHELL_PATH@}" -c "$rest" # Actual execution
                status=$?
                # Run in subshell because require_clean_work_tree can die.
                dirty=f
@@ -623,9 +669,9 @@ do_next () {
                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"
+       hook="$(git rev-parse --git-path hooks/post-rewrite)"
+       if test -x "$hook" && test -s "$rewritten_list"; then
+               "$hook" rebase < "$rewritten_list"
                true # we don't care if this hook failed
        fi &&
        warn "Successfully rebased and updated $head_name."
@@ -660,7 +706,7 @@ skip_unnecessary_picks () {
                                ;;
                        esac
                        ;;
-               3,#*|3,)
+               3,"$comment_char"*|3,)
                        # copy comments
                        ;;
                *)
@@ -678,19 +724,64 @@ skip_unnecessary_picks () {
        die "Could not skip unnecessary pick commands"
 }
 
+transform_todo_ids () {
+       while read -r command rest
+       do
+               case "$command" in
+               "$comment_char"* | exec)
+                       # Be careful for oddball commands like 'exec'
+                       # that do not have a SHA-1 at the beginning of $rest.
+                       ;;
+               *)
+                       sha1=$(git rev-parse --verify --quiet "$@" ${rest%%[     ]*}) &&
+                       rest="$sha1 ${rest#*[    ]}"
+                       ;;
+               esac
+               printf '%s\n' "$command${rest:+ }$rest"
+       done <"$todo" >"$todo.new" &&
+       mv -f "$todo.new" "$todo"
+}
+
+expand_todo_ids() {
+       transform_todo_ids
+}
+
+collapse_todo_ids() {
+       transform_todo_ids --short
+}
+
 # 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".
+#
+# Note that if the config has specified a custom instruction format
+# each log message will be re-retrieved in order to normalize the
+# autosquash arrangement
 rearrange_squash () {
        # extract fixup!/squash! lines and resolve any referenced sha1's
        while read -r pick sha1 message
        do
+               test -z "${format}" || message=$(git log -n 1 --format="%s" ${sha1})
                case "$message" in
                "squash! "*|"fixup! "*)
                        action="${message%%!*}"
-                       rest="${message#*! }"
-                       echo "$sha1 $action $rest"
+                       rest=$message
+                       prefix=
+                       # skip all squash! or fixup! (but save for later)
+                       while :
+                       do
+                               case "$rest" in
+                               "squash! "*|"fixup! "*)
+                                       prefix="$prefix${rest%%!*},"
+                                       rest="${rest#*! }"
+                                       ;;
+                               *)
+                                       break
+                                       ;;
+                               esac
+                       done
+                       printf '%s %s %s %s\n' "$sha1" "$action" "$prefix" "$rest"
                        # if it's a single word, try to resolve to a full sha1 and
                        # emit a second copy. This allows us to match on both message
                        # and on sha1 prefix
@@ -699,7 +790,7 @@ rearrange_squash () {
                                if test -n "$fullsha"; then
                                        # prefix the action to uniquely identify this line as
                                        # intended for full sha1 match
-                                       echo "$sha1 +$action $fullsha"
+                                       echo "$sha1 +$action $prefix $fullsha"
                                fi
                        fi
                esac
@@ -713,8 +804,9 @@ rearrange_squash () {
                *" $sha1 "*) continue ;;
                esac
                printf '%s\n' "$pick $sha1 $message"
+               test -z "${format}" || message=$(git log -n 1 --format="%s" ${sha1})
                used="$used$sha1 "
-               while read -r squash action msg_content
+               while read -r squash action msg_prefix msg_content
                do
                        case " $used" in
                        *" $squash "*) continue ;;
@@ -730,7 +822,13 @@ rearrange_squash () {
                                case "$message" in "$msg_content"*) emit=1;; esac ;;
                        esac
                        if test $emit = 1; then
-                               printf '%s\n' "$action $squash $action! $msg_content"
+                               if test -n "${format}"
+                               then
+                                       msg_content=$(git log -n 1 --format="${format}" ${squash})
+                               else
+                                       msg_content="$(echo "$msg_prefix" | sed "s/,/! /g")$msg_content"
+                               fi
+                               printf '%s\n' "$action $squash $msg_content"
                                used="$used$squash "
                        fi
                done <"$1.sq"
@@ -760,23 +858,213 @@ add_exec_commands () {
        mv "$1.new" "$1"
 }
 
+# Check if the SHA-1 passed as an argument is a
+# correct one, if not then print $2 in "$todo".badsha
+# $1: the SHA-1 to test
+# $2: the line number of the input
+# $3: the input filename
+check_commit_sha () {
+       badsha=0
+       if test -z $1
+       then
+               badsha=1
+       else
+               sha1_verif="$(git rev-parse --verify --quiet $1^{commit})"
+               if test -z $sha1_verif
+               then
+                       badsha=1
+               fi
+       fi
+
+       if test $badsha -ne 0
+       then
+               line="$(sed -n -e "${2}p" "$3")"
+               warn "Warning: the SHA-1 is missing or isn't" \
+                       "a commit in the following line:"
+               warn " - $line"
+               warn
+       fi
+
+       return $badsha
+}
+
+# prints the bad commits and bad commands
+# from the todolist in stdin
+check_bad_cmd_and_sha () {
+       retval=0
+       lineno=0
+       while read -r command rest
+       do
+               lineno=$(( $lineno + 1 ))
+               case $command in
+               "$comment_char"*|''|noop|x|exec)
+                       # Doesn't expect a SHA-1
+                       ;;
+               "$cr")
+                       # Work around CR left by "read" (e.g. with Git for
+                       # Windows' Bash).
+                       ;;
+               pick|p|drop|d|reword|r|edit|e|squash|s|fixup|f)
+                       if ! check_commit_sha "${rest%%[        ]*}" "$lineno" "$1"
+                       then
+                               retval=1
+                       fi
+                       ;;
+               *)
+                       line="$(sed -n -e "${lineno}p" "$1")"
+                       warn "Warning: the command isn't recognized" \
+                               "in the following line:"
+                       warn " - $line"
+                       warn
+                       retval=1
+                       ;;
+               esac
+       done <"$1"
+       return $retval
+}
+
+# Print the list of the SHA-1 of the commits
+# from stdin to stdout
+todo_list_to_sha_list () {
+       git stripspace --strip-comments |
+       while read -r command sha1 rest
+       do
+               case $command in
+               "$comment_char"*|''|noop|x|"exec")
+                       ;;
+               *)
+                       long_sha=$(git rev-list --no-walk "$sha1" 2>/dev/null)
+                       printf "%s\n" "$long_sha"
+                       ;;
+               esac
+       done
+}
+
+# Use warn for each line in stdin
+warn_lines () {
+       while read -r line
+       do
+               warn " - $line"
+       done
+}
+
+# Switch to the branch in $into and notify it in the reflog
+checkout_onto () {
+       GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION: checkout $onto_name"
+       output git checkout $onto || die_abort "could not detach HEAD"
+       git update-ref ORIG_HEAD $orig_head
+}
+
+get_missing_commit_check_level () {
+       check_level=$(git config --get rebase.missingCommitsCheck)
+       check_level=${check_level:-ignore}
+       # Don't be case sensitive
+       printf '%s' "$check_level" | tr 'A-Z' 'a-z'
+}
+
+# Check if the user dropped some commits by mistake
+# Behaviour determined by rebase.missingCommitsCheck.
+# Check if there is an unrecognized command or a
+# bad SHA-1 in a command.
+check_todo_list () {
+       raise_error=f
+
+       check_level=$(get_missing_commit_check_level)
+
+       case "$check_level" in
+       warn|error)
+               # Get the SHA-1 of the commits
+               todo_list_to_sha_list <"$todo".backup >"$todo".oldsha1
+               todo_list_to_sha_list <"$todo" >"$todo".newsha1
+
+               # Sort the SHA-1 and compare them
+               sort -u "$todo".oldsha1 >"$todo".oldsha1+
+               mv "$todo".oldsha1+ "$todo".oldsha1
+               sort -u "$todo".newsha1 >"$todo".newsha1+
+               mv "$todo".newsha1+ "$todo".newsha1
+               comm -2 -3 "$todo".oldsha1 "$todo".newsha1 >"$todo".miss
+
+               # Warn about missing commits
+               if test -s "$todo".miss
+               then
+                       test "$check_level" = error && raise_error=t
+
+                       warn "Warning: some commits may have been dropped" \
+                               "accidentally."
+                       warn "Dropped commits (newer to older):"
+
+                       # Make the list user-friendly and display
+                       opt="--no-walk=sorted --format=oneline --abbrev-commit --stdin"
+                       git rev-list $opt <"$todo".miss | warn_lines
+
+                       warn "To avoid this message, use \"drop\" to" \
+                               "explicitly remove a commit."
+                       warn
+                       warn "Use 'git config rebase.missingCommitsCheck' to change" \
+                               "the level of warnings."
+                       warn "The possible behaviours are: ignore, warn, error."
+                       warn
+               fi
+               ;;
+       ignore)
+               ;;
+       *)
+               warn "Unrecognized setting $check_level for option" \
+                       "rebase.missingCommitsCheck. Ignoring."
+               ;;
+       esac
+
+       if ! check_bad_cmd_and_sha "$todo"
+       then
+               raise_error=t
+       fi
+
+       if test $raise_error = t
+       then
+               # Checkout before the first commit of the
+               # rebase: this way git rebase --continue
+               # will work correctly as it expects HEAD to be
+               # placed before the commit of the next action
+               checkout_onto
+
+               warn "You can fix this with 'git rebase --edit-todo'."
+               die "Or you can abort the rebase with 'git rebase --abort'."
+       fi
+}
+
+# The whole contents of this file is run by dot-sourcing it from
+# inside a shell function.  It used to be that "return"s we see
+# below were not inside any function, and expected to return
+# to the function that dot-sourced us.
+#
+# However, FreeBSD /bin/sh misbehaves on such a construct and
+# continues to run the statements that follow such a "return".
+# As a work-around, we introduce an extra layer of a function
+# here, and immediately call it after defining it.
+git_rebase__interactive () {
+
 case "$action" in
 continue)
        # do we have anything to commit?
        if git diff-index --cached --quiet HEAD --
        then
-               : Nothing to commit -- skip this
+               # Nothing to commit -- skip this commit
+
+               test ! -f "$GIT_DIR"/CHERRY_PICK_HEAD ||
+               rm "$GIT_DIR"/CHERRY_PICK_HEAD ||
+               die "Could not remove CHERRY_PICK_HEAD"
        else
                if ! test -f "$author_script"
                then
+                       gpg_sign_opt_quoted=${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")}
                        die "You have staged changes in your working tree. If these changes are meant to be
 squashed into the previous commit, run:
 
-  git commit --amend
+  git commit --amend $gpg_sign_opt_quoted
 
 If they are meant to go into a new commit, run:
 
-  git commit
+  git commit $gpg_sign_opt_quoted
 
 In both case, once you're done, continue with:
 
@@ -792,15 +1080,20 @@ In both case, once you're done, continue with:
                        die "\
 You have uncommitted changes in your working tree. Please, commit them
 first and then run 'git rebase --continue' again."
-                       do_with_author git commit --amend --no-verify -F "$msg" -e ||
+                       do_with_author git commit --amend --no-verify -F "$msg" -e \
+                               ${gpg_sign_opt:+"$gpg_sign_opt"} ||
                                die "Could not commit staged changes."
                else
-                       do_with_author git commit --no-verify -F "$msg" -e ||
+                       do_with_author git commit --no-verify -F "$msg" -e \
+                               ${gpg_sign_opt:+"$gpg_sign_opt"} ||
                                die "Could not commit staged changes."
                fi
        fi
 
-       record_in_rewritten "$(cat "$state_dir"/stopped-sha)"
+       if test -r "$state_dir"/stopped-sha
+       then
+               record_in_rewritten "$(cat "$state_dir"/stopped-sha)"
+       fi
 
        require_clean_work_tree "rebase"
        do_rest
@@ -815,6 +1108,7 @@ skip)
 edit-todo)
        git stripspace --strip-comments <"$todo" >"$todo".new
        mv -f "$todo".new "$todo"
+       collapse_todo_ids
        append_todo_help
        git stripspace --comment-lines >>"$todo" <<\EOF
 
@@ -826,6 +1120,7 @@ EOF
 
        git_sequence_editor "$todo" ||
                die "Could not execute editor"
+       expand_todo_ids
 
        exit
        ;;
@@ -838,8 +1133,11 @@ comment_for_reflog start
 
 if test ! -z "$switch_to"
 then
+       GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION: checkout $switch_to"
        output git checkout "$switch_to" -- ||
-               die "Could not checkout $switch_to"
+       die "Could not checkout $switch_to"
+
+       comment_for_reflog start
 fi
 
 orig_head=$(git rev-parse --verify HEAD) || die "No HEAD?"
@@ -882,14 +1180,16 @@ else
        revisions=$onto...$orig_head
        shortrevisions=$shorthead
 fi
-git rev-list $merges_option --pretty=oneline --abbrev-commit \
-       --abbrev=7 --reverse --left-right --topo-order \
-       $revisions | \
+format=$(git config --get rebase.instructionFormat)
+# the 'rev-list .. | sed' requires %m to parse; the instruction requires %H to parse
+git rev-list $merges_option --format="%m%H ${format:-%s}" \
+       --reverse --left-right --topo-order \
+       $revisions ${restrict_revision+^$restrict_revision} | \
        sed -n "s/^>//p" |
-while read -r shortsha1 rest
+while read -r sha1 rest
 do
 
-       if test -z "$keep_empty" && is_empty_commit $shortsha1 && ! is_merge_commit $shortsha1
+       if test -z "$keep_empty" && is_empty_commit $sha1 && ! is_merge_commit $sha1
        then
                comment_out="$comment_char "
        else
@@ -898,9 +1198,8 @@ do
 
        if test t != "$preserve_merges"
        then
-               printf '%s\n' "${comment_out}pick $shortsha1 $rest" >>"$todo"
+               printf '%s\n' "${comment_out}pick $sha1 $rest" >>"$todo"
        else
-               sha1=$(git rev-parse $shortsha1)
                if test -z "$rebase_root"
                then
                        preserve=t
@@ -917,7 +1216,7 @@ do
                if test f = "$preserve"
                then
                        touch "$rewritten"/$sha1
-                       printf '%s\n' "${comment_out}pick $shortsha1 $rest" >>"$todo"
+                       printf '%s\n' "${comment_out}pick $sha1 $rest" >>"$todo"
                fi
        fi
 done
@@ -934,15 +1233,16 @@ then
        git rev-list $revisions |
        while read rev
        do
-               if test -f "$rewritten"/$rev -a "$(sane_grep "$rev" "$state_dir"/not-cherry-picks)" = ""
+               if test -f "$rewritten"/$rev &&
+                  ! sane_grep "$rev" "$state_dir"/not-cherry-picks >/dev/null
                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' ' -s -f2 > "$dropped"/$rev
-                       short=$(git rev-list -1 --abbrev-commit --abbrev=7 $rev)
-                       sane_grep -v "^[a-z][a-z]* $short" <"$todo" > "${todo}2" ; mv "${todo}2" "$todo"
+                       sha1=$(git rev-list -1 $rev)
+                       sane_grep -v "^[a-z][a-z]* $sha1" <"$todo" > "${todo}2" ; mv "${todo}2" "$todo"
                        rm "$rewritten"/$rev
                fi
        done
@@ -952,9 +1252,12 @@ test -s "$todo" || echo noop >> "$todo"
 test -n "$autosquash" && rearrange_squash "$todo"
 test -n "$cmd" && add_exec_commands "$todo"
 
+todocount=$(git stripspace --strip-comments <"$todo" | wc -l)
+todocount=${todocount##* }
+
 cat >>"$todo" <<EOF
 
-$comment_char Rebase $shortrevisions onto $shortonto
+$comment_char Rebase $shortrevisions onto $shortonto ($todocount command(s))
 EOF
 append_todo_help
 git stripspace --comment-lines >>"$todo" <<\EOF
@@ -973,14 +1276,22 @@ has_action "$todo" ||
        return 2
 
 cp "$todo" "$todo".backup
+collapse_todo_ids
 git_sequence_editor "$todo" ||
        die_abort "Could not execute editor"
 
 has_action "$todo" ||
        return 2
 
+check_todo_list
+
+expand_todo_ids
+
 test -d "$rewritten" || test -n "$force_rebase" || skip_unnecessary_picks
 
-output git checkout $onto || die_abort "could not detach HEAD"
-git update-ref ORIG_HEAD $orig_head
+checkout_onto
 do_rest
+
+}
+# ... and then we call the whole thing.
+git_rebase__interactive