# 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 # # The original idea comes from Eric W. Biederman, in # http://article.gmane.org/gmane.comp.version-control.git/22407 # # The file containing rebase commands, comments, and empty lines. # This file is created by "git rebase -i" then edited by the user. As # the lines are processed, they are removed from the front of this # file and written to the tail of $done. setvar todo = ""$state_dir"/git-rebase-todo" # The rebase command lines that have already been processed. A line # is moved here when it is first handled, before any associated user # actions. setvar done = ""$state_dir"/done" # The commit message that is planned to be used for any changes that # need to be committed following a user interaction. setvar msg = ""$state_dir"/message" # The file into which is accumulated the suggested commit message for # squash/fixup commands. When the first of a series of squash/fixups # is seen, the file is created and the commit message from the # previous commit and from the first squash/fixup commit are written # to it. The commit message for each subsequent squash/fixup commit # is appended to the file as it is processed. # # The first line of the file is of the form # # This is a combination of $count commits. # where $count is the number of commits whose messages have been # written to the file so far (including the initial "pick" commit). # Each time that a commit message is processed, this line is read and # updated. It is deleted just before the combined commit is made. setvar squash_msg = ""$state_dir"/message-squash" # If the current series of squash/fixups has not yet included a squash # command, then this file exists and holds the commit message of the # original "pick" commit. (If the series ends without a "squash" # command, then this can be used as the commit message of the combined # commit without opening the editor.) setvar fixup_msg = ""$state_dir"/message-fixup" # $rewritten is the name of a directory containing files for each # commit that is reachable by at least one merge base of $head and # $upstream. They are not necessarily rewritten, but their children # might be. This ensures that commits on merged, but otherwise # unrelated side branches are left alone. (Think "X" in the man page's # example.) setvar rewritten = ""$state_dir"/rewritten" setvar dropped = ""$state_dir"/dropped" setvar end = ""$state_dir"/end" setvar msgnum = ""$state_dir"/msgnum" # A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and # GIT_AUTHOR_DATE that will be used for the commit that is currently # being rebased. setvar author_script = ""$state_dir"/author-script" # When an "edit" rebase command is being processed, the SHA1 of the # commit to be edited is recorded in this file. When "git rebase # --continue" is executed, if there are any staged changes then they # will be amended to the HEAD commit, but only provided the HEAD # commit is still the commit to be edited. When any other rebase # command is processed, this file is deleted. setvar amend = ""$state_dir"/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. setvar rewritten_list = ""$state_dir"/rewritten-list" setvar 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. setvar cr = $(printf "\015") setvar 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 ' setvar GIT_CHERRY_PICK_HELP = "$resolvemsg" export GIT_CHERRY_PICK_HELP setvar comment_char = $(git config --get core.commentchar 2>/dev/null | cut -c1) : ${comment_char:=#} proc warn { printf '%s\n' "$[join(ARGV)]" >&2 } # Output the commit message for the specified commit. proc commit_message { git cat-file commit $1 | sed "1,/^$/d" } setvar orig_reflog_action = "$GIT_REFLOG_ACTION" proc comment_for_reflog { case (orig_reflog_action) { ''|rebase* { setvar GIT_REFLOG_ACTION = ""rebase -i ($1)"" export GIT_REFLOG_ACTION } } } setvar last_count = '' proc mark_action_done { sed -e 1q < "$todo" >> "$done" sed -e 1d < "$todo" >> "$todo".new mv -f "$todo".new $todo setvar new_count = $(( $(git stripspace --strip-comments <"$done" | wc -l) )) echo $new_count >"$msgnum" setvar total = $(($new_count + $(git stripspace --strip-comments <"$todo" | wc -l))) echo $total >"$end" if test $last_count != $new_count { setvar last_count = "$new_count" eval_gettext "Rebasing (\$new_count/\$total)"; printf "\r" test -z $verbose || echo } } # 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. proc 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 } proc append_todo_help { gettext " Commands: p, pick = use commit r, reword = use commit, but edit the commit message e, edit = use commit, but stop for amending 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. " | git stripspace --comment-lines >>"$todo" if test $(get_missing_commit_check_level) = error { gettext " Do not remove any line. Use 'drop' explicitly to remove a commit. " | git stripspace --comment-lines >>"$todo" } else { gettext " If you remove a line here THAT COMMIT WILL BE LOST. " | git stripspace --comment-lines >>"$todo" } } proc make_patch { setvar sha1_and_parents = "$(git rev-list --parents -1 "$1")" case (sha1_and_parents) { ?*' '?*' '?* { git diff --cc $sha1_and_parents } ?*' '?* { git diff-tree -p "$1^!" } * { echo "Root commit" } } > "$state_dir"/patch test -f $msg || commit_message $1 > "$msg" test -f $author_script || get_author_ident_from_commit $1 > "$author_script" } proc die_with_patch { echo $1 > "$state_dir"/stopped-sha make_patch $1 die $2 } proc exit_with_patch { echo $1 > "$state_dir"/stopped-sha make_patch $1 git rev-parse --verify HEAD > "$amend" setvar gpg_sign_opt_quoted = ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} warn $(eval_gettext "\ You can amend the commit now, with git commit --amend \$gpg_sign_opt_quoted Once you are satisfied with your changes, run git rebase --continue") warn exit $2 } proc die_abort { apply_autostash rm -rf $state_dir die $1 } proc has_action { test -n $(git stripspace --strip-comments <"$1") } proc is_empty_commit { setvar tree = $(git rev-parse -q --verify "$1"^{tree} 2>/dev/null) || do { setvar sha1 = "$1" die $(eval_gettext "\$sha1: not a commit that can be picked") } setvar ptree = $(git rev-parse -q --verify "$1"^^{tree} 2>/dev/null) || setvar ptree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904' test $tree = $ptree } proc is_merge_commit { git rev-parse --verify --quiet "$1"^2 >/dev/null 2>&1 } # Run command with GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and # GIT_AUTHOR_DATE exported from the current environment. proc do_with_author { shell { export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE @ARGV } } proc git_sequence_editor { if test -z $GIT_SEQUENCE_EDITOR { setvar GIT_SEQUENCE_EDITOR = "$(git config sequence.editor)" if test -z $GIT_SEQUENCE_EDITOR { setvar GIT_SEQUENCE_EDITOR = "$(git var GIT_EDITOR)" || return $? } } eval $GIT_SEQUENCE_EDITOR '"$@"' } proc pick_one { setvar ff = '--ff' case (1) { -n { setvar sha1 = "$2"; setvar ff = '' } * { setvar sha1 = "$1" } } case (force_rebase) { '' { } ?* { setvar ff = '' } } output git rev-parse --verify $sha1 || die $(eval_gettext "Invalid commit name: \$sha1") if is_empty_commit $sha1 { setvar empty_args = ""--allow-empty"" } test -d $rewritten && pick_one_preserving_merges @ARGV && return output eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ $strategy_args $empty_args $ff @ARGV # If cherry-pick dies it leaves the to-be-picked commit unrecorded. Reschedule # previous task so this commit is not lost. setvar ret = ""$? case (ret) { [01] { } * { reschedule_last_action } } return $ret } proc pick_one_preserving_merges { setvar fast_forward = 't' case (1) { -n { setvar fast_forward = 'f' setvar sha1 = "$2" } * { setvar sha1 = "$1" } } setvar sha1 = $(git rev-parse $sha1) if test -f "$state_dir"/current-commit { if test $fast_forward = t { while read current_commit { git rev-parse HEAD > "$rewritten"/$current_commit } <"$state_dir"/current-commit rm "$state_dir"/current-commit || die $(gettext "Cannot write current commit's replacement sha1") } } echo $sha1 >> "$state_dir"/current-commit # rewrite parents; if none were rewritten, we can fast-forward. setvar new_parents = '' setvar pend = "" $(git rev-list --parents -1 $sha1 | cut -d' ' -s -f2-)"" if test $pend = " " { setvar pend = "" root"" } while [ "$pend" != "" ] { setvar p = $(expr "$pend" : ' \([^ ]*\)') setvar pend = "${pend# $p}" if test -f "$rewritten"/$p { setvar 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 { setvar new_p = $(git rev-parse HEAD) } test $p != $new_p && setvar fast_forward = 'f' case (new_parents) { *$new_p* { } # do nothing; that parent is already there * { setvar new_parents = ""$new_parents $new_p"" } } } else { if test -f "$dropped"/$p { setvar fast_forward = 'f' setvar replacement = "$(cat "$dropped"/$p)" test -z $replacement && setvar replacement = 'root' setvar pend = "" $replacement$pend"" } else { setvar new_parents = ""$new_parents $p"" } } } case (fast_forward) { t { output warn $(eval_gettext "Fast-forward to \$sha1") output git reset --hard $sha1 || die $(eval_gettext "Cannot fast-forward to \$sha1") } f { setvar first_parent = $(expr "$new_parents" : ' \([^ ]*\)') if test $1 != "-n" { # detach HEAD to current parent output git checkout $first_parent 2> /dev/null || die $(eval_gettext "Cannot move HEAD to \$first_parent") } case (new_parents) { ' '*' '* { test "a$1" = a-n && die $(eval_gettext "Refusing to squash a merge: \$sha1") # redo merge setvar author_script_content = $(get_author_ident_from_commit $sha1) eval $author_script_content setvar msg_content = "$(commit_message $sha1)" # No point in merging the first parent, that's HEAD setvar new_parents = ${new_parents# $first_parent} setvar 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' { printf "%s\n" $msg_content > "$GIT_DIR"/MERGE_MSG die_with_patch $sha1 $(eval_gettext "Error redoing merge \$sha1") } echo "$sha1 $(git rev-parse HEAD^0)" >> "$rewritten_list" } * { output eval git cherry-pick \ ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} \ $strategy_args @ARGV || die_with_patch $sha1 $(eval_gettext "Could not pick \$sha1") } } } } } proc this_nth_commit_message { setvar n = "$1" eval_gettext "This is the commit message #\${n}:" } proc skip_nth_commit_message { setvar n = "$1" eval_gettext "The commit message #\${n} will be skipped:" } proc update_squash_messages { if test -f $squash_msg { mv $squash_msg "$squash_msg".bak || exit setvar count = $(($(sed -n \ -e "1s/^$comment_char.*\([0-9][0-9]*\).*/\1/p" \ -e "q" < "$squash_msg".bak)+1)) do { printf '%s\n' "$comment_char $(eval_ngettext \ "This is a combination of \$count commit." \ "This is a combination of \$count commits." \ $count)" sed -e 1d -e '2,/^./{ /^$/d }' <"$squash_msg".bak } >"$squash_msg" } else { commit_message HEAD > "$fixup_msg" || die $(gettext "Cannot write \$fixup_msg") setvar count = '2' do { printf '%s\n' "$comment_char $(gettext "This is a combination of 2 commits.")" printf '%s\n' "$comment_char $(gettext "This is the 1st commit message:")" echo cat $fixup_msg } >"$squash_msg" } case (1) { squash { rm -f $fixup_msg echo printf '%s\n' "$comment_char $(this_nth_commit_message $count)" echo commit_message $2 } fixup { echo printf '%s\n' "$comment_char $(skip_nth_commit_message $count)" echo # Change the space after the comment character to TAB: commit_message $2 | git stripspace --comment-lines | sed -e 's/ / /' } } >>"$squash_msg" } proc peek_next_command { git stripspace --strip-comments <"$todo" | sed -n -e 's/ .*//p' -e q } # A squash/fixup has failed. Prepare the long version of the squash # commit message, then die_with_patch. This code path requires the # user to edit the combined commit message for all commits that have # been squashed/fixedup so far. So also erase the old squash # messages, effectively causing the combined commit to be used as the # new basis for any further squash/fixups. Args: sha1 rest proc die_failed_squash { setvar sha1 = "$1" setvar rest = "$2" mv $squash_msg $msg || exit rm -f $fixup_msg cp $msg "$GIT_DIR"/MERGE_MSG || exit warn warn $(eval_gettext "Could not apply \$sha1... \$rest") die_with_patch $sha1 "" } proc flush_rewritten_pending { test -s $rewritten_pending || return setvar newsha1 = "$(git rev-parse HEAD^0)" sed "s/$/ $newsha1/" < "$rewritten_pending" >> "$rewritten_list" rm -f $rewritten_pending } proc record_in_rewritten { setvar oldsha1 = "$(git rev-parse $1)" echo $oldsha1 >> "$rewritten_pending" case{ squash|s|fixup|f { } * { flush_rewritten_pending } } } proc do_pick { setvar sha1 = "$1" setvar rest = "$2" if test $(git rev-parse HEAD) = $squash_onto { # Set the correct commit message and author info on the # sentinel root before cherry-picking the original changes # without committing (-n). Finally, update the sentinel again # to include these changes. If the cherry-pick results in a # conflict, this means our behaviour is similar to a standard # failed cherry-pick during rebase, with a dirty index to # resolve before manually running git commit --amend then git # rebase --continue. git commit --allow-empty --allow-empty-message --amend \ --no-post-rewrite -n -q -C $sha1 && pick_one -n $sha1 && git commit --allow-empty --allow-empty-message \ --amend --no-post-rewrite -n -q -C $sha1 \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die_with_patch $sha1 $(eval_gettext "Could not apply \$sha1... \$rest") } else { pick_one $sha1 || die_with_patch $sha1 $(eval_gettext "Could not apply \$sha1... \$rest") } } proc do_next { rm -f $msg $author_script $amend "$state_dir"/stopped-sha || exit read -r command sha1 rest < "$todo" case (command) { "$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 { comment_for_reflog pick mark_action_done do_pick $sha1 $rest record_in_rewritten $sha1 } reword|r { comment_for_reflog reword mark_action_done do_pick $sha1 $rest git commit --amend --no-post-rewrite ${gpg_sign_opt:+"$gpg_sign_opt"} || do { warn $(eval_gettext "\ Could not amend commit after successfully picking \$sha1... \$rest This is most likely due to an empty commit message, or the pre-commit hook failed. If the pre-commit hook failed, you may need to resolve the issue before you are able to reword the commit.") exit_with_patch $sha1 1 } record_in_rewritten $sha1 } edit|e { comment_for_reflog edit mark_action_done do_pick $sha1 $rest setvar sha1_abbrev = $(git rev-parse --short $sha1) warn $(eval_gettext "Stopped at \$sha1_abbrev... \$rest") exit_with_patch $sha1 0 } squash|s|fixup|f { case (command) { squash|s { setvar squash_style = 'squash' } fixup|f { setvar squash_style = 'fixup' } } comment_for_reflog $squash_style test -f $done && has_action $done || die $(eval_gettext "Cannot '\$squash_style' without a previous commit") mark_action_done update_squash_messages $squash_style $sha1 setvar author_script_content = $(get_author_ident_from_commit HEAD) echo $author_script_content > "$author_script" eval $author_script_content if ! pick_one -n $sha1 { git rev-parse --verify HEAD >"$amend" die_failed_squash $sha1 $rest } case{ 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 \ ${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 { 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 \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die_failed_squash $sha1 $rest } rm -f $squash_msg $fixup_msg } } record_in_rewritten $sha1 } x|"exec" { read -r command rest < "$todo" mark_action_done eval_gettextln "Executing: \$rest" ${SHELL:-@SHELL_PATH@} -c $rest # Actual execution setvar status = ""$? # Run in subshell because require_clean_work_tree can die. setvar dirty = 'f' shell {require_clean_work_tree "rebase" 2>/dev/null} || setvar dirty = 't' if test $status -ne 0 { warn $(eval_gettext "Execution failed: \$rest") test $dirty = f || warn $(gettext "and made changes to the index and/or the working tree") warn $(gettext "\ You can fix the problem, and then run git rebase --continue") warn if test $status -eq 127 # command not found { setvar status = '1' } exit "$status" } elif test $dirty = t { # TRANSLATORS: after these lines is a command to be issued by the user warn $(eval_gettext "\ Execution succeeded: \$rest but left changes to the index and/or the working tree Commit or stash your changes, and then run git rebase --continue") warn exit 1 } } * { warn $(eval_gettext "Unknown command: \$command \$sha1 \$rest") setvar fixtodo = "$(gettext "Please fix this using 'git rebase --edit-todo'.")" if git rev-parse --verify -q $sha1 >/dev/null { die_with_patch $sha1 $fixtodo } else { die $fixtodo } } } test -s $todo && return comment_for_reflog finish && setvar newhead = $(git rev-parse HEAD) && case (head_name) { refs/* { setvar message = ""$GIT_REFLOG_ACTION: $head_name onto $onto"" && git update-ref -m $message $head_name $newhead $orig_head && git symbolic-ref \ -m "$GIT_REFLOG_ACTION: returning to $head_name" \ HEAD $head_name } } && do { test ! -f "$state_dir"/verbose || git diff-tree --stat $orig_head..HEAD } && do { test -s $rewritten_list && git notes copy --for-rewrite=rebase < "$rewritten_list" || true # we don't care if this copying failed } && setvar hook = "$(git rev-parse --git-path hooks/post-rewrite)" if test -x $hook && test -s $rewritten_list { $hook rebase < "$rewritten_list" true # we don't care if this hook failed } && warn $(eval_gettext "Successfully rebased and updated \$head_name.") return 1 # not failure; just to break the do_rest loop } # can only return 0, when the infinite loop breaks proc do_rest { while : { do_next || break } } # skip picking commits whose parents are unchanged proc skip_unnecessary_picks { setvar fd = '3' while read -r command rest { # fd=3 means we skip the command case{ 3,pick|3,p { # pick a commit whose parent is current $onto -> skip setvar sha1 = ${rest%% *} case{ "$onto"* { setvar onto = "$sha1" } * { setvar fd = '1' } } } 3,"$comment_char"*|3, { # copy comments } * { setvar fd = '1' } } printf '%s\n' "$command${rest:+ }$rest" >&$fd } <"$todo" >"$todo.new" 3>>"$done" && mv -f "$todo".new $todo && case{ squash|s|fixup|f { record_in_rewritten $onto } } || die $(gettext "Could not skip unnecessary pick commands") } proc transform_todo_ids { while read -r command rest { case (command) { "$comment_char"* | exec { # Be careful for oddball commands like 'exec' # that do not have a SHA-1 at the beginning of $rest. } * { setvar sha1 = $(git rev-parse --verify --quiet "$@" ${rest%%[ ]*}) && setvar rest = ""$sha1 ${rest#*[ ]}"" } } printf '%s\n' "$command${rest:+ }$rest" } <"$todo" >"$todo.new" && mv -f "$todo.new" $todo } proc expand_todo_ids { transform_todo_ids } proc 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 proc rearrange_squash { # extract fixup!/squash! lines and resolve any referenced sha1's while read -r pick sha1 message { test -z ${format} || setvar message = $(git log -n 1 --format="%s" ${sha1}) case (message) { "squash! "*|"fixup! "* { setvar action = "${message%%!*}" setvar rest = "$message" setvar prefix = '' # skip all squash! or fixup! (but save for later) while : { case (rest) { "squash! "*|"fixup! "* { setvar prefix = ""$prefix${rest%%!*},"" setvar rest = "${rest#*! }" } * { break } } } 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 if test ${rest#* } = $rest { setvar fullsha = "$(git rev-parse -q --verify "$rest" 2>/dev/null)" if test -n $fullsha { # prefix the action to uniquely identify this line as # intended for full sha1 match echo "$sha1 +$action $prefix $fullsha" } } } } } >"$1.sq" <"$1" test -s "$1.sq" || return setvar used = '' while read -r pick sha1 message { case{ *" $sha1 "* { continue } } printf '%s\n' "$pick $sha1 $message" test -z ${format} || setvar message = $(git log -n 1 --format="%s" ${sha1}) setvar used = ""$used$sha1 "" while read -r squash action msg_prefix msg_content { case{ *" $squash "* { continue } } setvar emit = '0' case (action) { +* { setvar action = "${action#+}" # full sha1 prefix test case (msg_content) { "$sha1"* { setvar emit = '1'} } } * { # message prefix test case (message) { "$msg_content"* { setvar emit = '1'} } } } if test $emit = 1 { if test -n ${format} { setvar msg_content = $(git log -n 1 --format="${format}" ${squash}) } else { setvar msg_content = ""$(echo "$msg_prefix" | sed "s/,/! /g")$msg_content"" } printf '%s\n' "$action $squash $msg_content" setvar used = ""$used$squash "" } } <"$1.sq" } >"$1.rearranged" <"$1" cat "$1.rearranged" >"$1" rm -f "$1.sq" "$1.rearranged" } # Add commands after a pick or after a squash/fixup serie # in the todo list. proc add_exec_commands { do { setvar first = 't' while read -r insn rest { case (insn) { pick { test -n $first || printf "%s" $cmd } } printf "%s %s\n" $insn $rest setvar first = '' } printf "%s" $cmd } <"$1" >"$1.new" && 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 proc check_commit_sha { setvar badsha = '0' if test -z $1 { setvar badsha = '1' } else { setvar sha1_verif = "$(git rev-parse --verify --quiet $1^{commit})" if test -z $sha1_verif { setvar badsha = '1' } } if test $badsha -ne 0 { setvar line = "$(sed -n -e "${2}p" "$3")" warn $(eval_gettext "\ Warning: the SHA-1 is missing or isn't a commit in the following line: - \$line") warn } return $badsha } # prints the bad commits and bad commands # from the todolist in stdin proc check_bad_cmd_and_sha { setvar retval = '0' setvar lineno = '0' while read -r command rest { setvar lineno = $(( $lineno + 1 )) case (command) { "$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 { setvar retval = '1' } } * { setvar line = "$(sed -n -e "${lineno}p" "$1")" warn $(eval_gettext "\ Warning: the command isn't recognized in the following line: - \$line") warn setvar retval = '1' } } } <"$1" return $retval } # Print the list of the SHA-1 of the commits # from stdin to stdout proc todo_list_to_sha_list { git stripspace --strip-comments | while read -r command sha1 rest { case (command) { "$comment_char"*|''|noop|x|"exec" { } * { setvar long_sha = $(git rev-list --no-walk "$sha1" 2>/dev/null) printf "%s\n" $long_sha } } } } # Use warn for each line in stdin proc warn_lines { while read -r line { warn " - $line" } } # Switch to the branch in $into and notify it in the reflog proc checkout_onto { setvar GIT_REFLOG_ACTION = ""$GIT_REFLOG_ACTION: checkout $onto_name"" output git checkout $onto || die_abort $(gettext "could not detach HEAD") git update-ref ORIG_HEAD $orig_head } proc get_missing_commit_check_level { setvar check_level = $(git config --get rebase.missingCommitsCheck) setvar 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. proc check_todo_list { setvar raise_error = 'f' setvar check_level = $(get_missing_commit_check_level) case (check_level) { 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 { test $check_level = error && setvar raise_error = 't' warn $(gettext "\ Warning: some commits may have been dropped accidentally. Dropped commits (newer to older):") # Make the list user-friendly and display setvar opt = ""--no-walk=sorted --format=oneline --abbrev-commit --stdin"" git rev-list $opt <"$todo".miss | warn_lines warn $(gettext "\ To avoid this message, use \"drop\" to explicitly remove a commit. Use 'git config rebase.missingCommitsCheck' to change the level of warnings. The possible behaviours are: ignore, warn, error.") warn } } ignore { } * { warn $(eval_gettext "Unrecognized setting \$check_level for option rebase.missingCommitsCheck. Ignoring.") } } if ! check_bad_cmd_and_sha $todo { setvar raise_error = 't' } if test $raise_error = t { # 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 $(gettext "You can fix this with 'git rebase --edit-todo' and then run 'git rebase --continue'.") die $(gettext "Or you can abort the rebase with 'git rebase --abort'.") } } # 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, older (9.x) versions of FreeBSD /bin/sh misbehave on such a # construct and continue 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. proc git_rebase__interactive { case (action) { continue { # do we have anything to commit? if git diff-index --cached --quiet HEAD -- { # Nothing to commit -- skip this commit test ! -f "$GIT_DIR"/CHERRY_PICK_HEAD || rm "$GIT_DIR"/CHERRY_PICK_HEAD || die $(gettext "Could not remove CHERRY_PICK_HEAD") } else { if ! test -f $author_script { setvar gpg_sign_opt_quoted = ${gpg_sign_opt:+$(git rev-parse --sq-quote "$gpg_sign_opt")} die $(eval_gettext "\ You have staged changes in your working tree. If these changes are meant to be squashed into the previous commit, run: git commit --amend \$gpg_sign_opt_quoted If they are meant to go into a new commit, run: git commit \$gpg_sign_opt_quoted In both cases, once you're done, continue with: git rebase --continue ") } source "$author_script" || die $(gettext "Error trying to find the author identity to amend commit") if test -f $amend { setvar current_head = $(git rev-parse --verify HEAD) test $current_head = $(cat "$amend") || die $(gettext "\ 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 \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die $(gettext "Could not commit staged changes.") } else { do_with_author git commit --no-verify -F $msg -e \ ${gpg_sign_opt:+"$gpg_sign_opt"} || die $(gettext "Could not commit staged changes.") } } if test -r "$state_dir"/stopped-sha { record_in_rewritten $(cat "$state_dir"/stopped-sha) } require_clean_work_tree "rebase" do_rest return 0 } skip { git rerere clear do_rest return 0 } edit-todo { git stripspace --strip-comments <"$todo" >"$todo".new mv -f "$todo".new $todo collapse_todo_ids append_todo_help gettext " You are editing the todo file of an ongoing interactive rebase. To continue rebase after editing, run: git rebase --continue " | git stripspace --comment-lines >>"$todo" git_sequence_editor $todo || die $(gettext "Could not execute editor") expand_todo_ids exit } } comment_for_reflog start if test ! -z $switch_to { setvar GIT_REFLOG_ACTION = ""$GIT_REFLOG_ACTION: checkout $switch_to"" output git checkout $switch_to -- || die $(eval_gettext "Could not checkout \$switch_to") comment_for_reflog start } setvar orig_head = $(git rev-parse --verify HEAD) || die $(gettext "No HEAD?") mkdir -p $state_dir || die $(eval_gettext "Could not create temporary \$state_dir") : > "$state_dir"/interactive || die $(gettext "Could not mark as interactive") write_basic_state if test t = $preserve_merges { if test -z $rebase_root { mkdir $rewritten && for c in $(git merge-base --all $orig_head $upstream) { echo $onto > "$rewritten"/$c || die $(gettext "Could not init rewritten commits") } } else { mkdir $rewritten && echo $onto > "$rewritten"/root || die $(gettext "Could not init rewritten commits") } # No cherry-pick because our first pass is to determine # parents to rewrite and skipping dropped commits would # prematurely end our probe setvar merges_option = '' } else { setvar merges_option = ""--no-merges --cherry-pick"" } setvar shorthead = $(git rev-parse --short $orig_head) setvar shortonto = $(git rev-parse --short $onto) if test -z $rebase_root # this is now equivalent to ! -z "$upstream" { setvar shortupstream = $(git rev-parse --short $upstream) setvar revisions = "$upstream...$orig_head" setvar shortrevisions = "$shortupstream..$shorthead" } else { setvar revisions = "$onto...$orig_head" setvar shortrevisions = "$shorthead" } setvar 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 { if test -z $keep_empty && is_empty_commit $sha1 && ! is_merge_commit $sha1 { setvar comment_out = ""$comment_char "" } else { setvar comment_out = '' } if test t != $preserve_merges { printf '%s\n' "${comment_out}pick $sha1 $rest" >>"$todo" } else { if test -z $rebase_root { setvar preserve = 't' for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -s -f2-) { if test -f "$rewritten"/$p { setvar preserve = 'f' } } } else { setvar preserve = 'f' } if test f = $preserve { touch "$rewritten"/$sha1 printf '%s\n' "${comment_out}pick $sha1 $rest" >>"$todo" } } } # Watch for commits that been dropped by --cherry-pick if test t = $preserve_merges { mkdir $dropped # Save all non-cherry-picked changes git rev-list $revisions --left-right --cherry-pick | \ sed -n "s/^>//p" > "$state_dir"/not-cherry-picks # Now all commits and note which ones are missing in # not-cherry-picks and hence being dropped git rev-list $revisions | while read rev { if test -f "$rewritten"/$rev && ! sane_grep $rev "$state_dir"/not-cherry-picks >/dev/null { # 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 setvar sha1 = $(git rev-list -1 $rev) sane_grep -v "^[a-z][a-z]* $sha1" <"$todo" > "${todo}2" ; mv "${todo}2" $todo rm "$rewritten"/$rev } } } test -s $todo || echo noop >> "$todo" test -n $autosquash && rearrange_squash $todo test -n $cmd && add_exec_commands $todo setvar todocount = $(git stripspace --strip-comments <"$todo" | wc -l) setvar todocount = ${todocount##* } cat >>"$todo" <<< """ $comment_char $(eval_ngettext \ "Rebase \$shortrevisions onto \$shortonto (\$todocount command)" \ "Rebase \$shortrevisions onto \$shortonto (\$todocount commands)" \ "$todocount") """ append_todo_help gettext " However, if you remove everything, the rebase will be aborted. " | git stripspace --comment-lines >>"$todo" if test -z $keep_empty { printf '%s\n' "$comment_char $(gettext "Note that empty commits are commented out")" >>"$todo" } has_action $todo || return 2 cp $todo "$todo".backup collapse_todo_ids git_sequence_editor $todo || die_abort $(gettext "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 checkout_onto do_rest } # ... and then we call the whole thing. git_rebase__interactive