Merge branch 'em/newer-freebsd-shells-are-fine-with-returns'
[gitweb.git] / git-rebase--interactive.sh
index 56d99662f4bf62236328011636f4250d8ca1a57f..05f22e43ccfe485b2a676f96ddec1a99f7a84612 100644 (file)
@@ -77,17 +77,18 @@ amend="$state_dir"/amend
 rewritten_list="$state_dir"/rewritten-list
 rewritten_pending="$state_dir"/rewritten-pending
 
-strategy_args=
-if test -n "$do_merge"
-then
-       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
-       '
-fi
+# 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}
+test -n "$strategy_opts" &&
+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
@@ -152,11 +153,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 () {
@@ -181,7 +192,6 @@ make_patch () {
 die_with_patch () {
        echo "$1" > "$state_dir"/stopped-sha
        make_patch "$1"
-       git rerere
        die "$2"
 }
 
@@ -505,7 +515,11 @@ do_next () {
        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)
@@ -534,7 +548,8 @@ do_next () {
 
                mark_action_done
                do_pick $sha1 "$rest"
-               warn "Stopped at $sha1... $rest"
+               sha1_abbrev=$(git rev-parse --short $sha1)
+               warn "Stopped at $sha1_abbrev... $rest"
                exit_with_patch $sha1 0
                ;;
        squash|s|fixup|f)
@@ -592,7 +607,7 @@ do_next () {
                read -r command rest < "$todo"
                mark_action_done
                printf 'Executing: %s\n' "$rest"
-               ${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
@@ -655,9 +670,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."
@@ -719,8 +734,8 @@ transform_todo_ids () {
                        # that do not have a SHA-1 at the beginning of $rest.
                        ;;
                *)
-                       sha1=$(git rev-parse --verify --quiet "$@" ${rest%% *}) &&
-                       rest="$sha1 ${rest#* }"
+                       sha1=$(git rev-parse --verify --quiet "$@" ${rest%%[     ]*}) &&
+                       rest="$sha1 ${rest#*[    ]}"
                        ;;
                esac
                printf '%s\n' "$command${rest:+ }$rest"
@@ -740,10 +755,15 @@ collapse_todo_ids() {
 # "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%%!*}"
@@ -785,6 +805,7 @@ 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_prefix msg_content
                do
@@ -802,8 +823,13 @@ rearrange_squash () {
                                case "$message" in "$msg_content"*) emit=1;; esac ;;
                        esac
                        if test $emit = 1; then
-                               real_prefix=$(echo "$msg_prefix" | sed "s/,/! /g")
-                               printf '%s\n' "$action $squash ${real_prefix}$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"
@@ -833,6 +859,180 @@ 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
@@ -981,7 +1181,10 @@ else
        revisions=$onto...$orig_head
        shortrevisions=$shorthead
 fi
-git rev-list $merges_option --pretty=oneline --reverse --left-right --topo-order \
+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 sha1 rest
@@ -1031,7 +1234,8 @@ then
        git rev-list $revisions |
        while read rev
        do
-               if test -f "$rewritten"/$rev && test "$(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,
@@ -1080,13 +1284,13 @@ git_sequence_editor "$todo" ||
 has_action "$todo" ||
        return 2
 
+check_todo_list
+
 expand_todo_ids
 
 test -d "$rewritten" || test -n "$force_rebase" || skip_unnecessary_picks
 
-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
+checkout_onto
 do_rest
 
 }