# 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. setglobal 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. setglobal 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. setglobal 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. setglobal 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.) setglobal 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.) setglobal rewritten = ""$state_dir"/rewritten" setglobal dropped = ""$state_dir"/dropped" setglobal end = ""$state_dir"/end" setglobal 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. setglobal 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. setglobal 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. setglobal rewritten_list = ""$state_dir"/rewritten-list" setglobal 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. setglobal cr = $[printf "\015] setglobal 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 ' setglobal GIT_CHERRY_PICK_HELP = $resolvemsg export GIT_CHERRY_PICK_HELP setglobal comment_char = $[git config --get core.commentchar !2 >/dev/null | cut -c1] : $(comment_char:=#) proc warn { printf '%s\n' "$ifsjoin(Argv)" > !2 } # Output the commit message for the specified commit. proc commit_message { git cat-file commit $1 | sed "1,/^$/d" } setglobal orig_reflog_action = $GIT_REFLOG_ACTION proc comment_for_reflog { match $orig_reflog_action { with ''|rebase* setglobal GIT_REFLOG_ACTION = ""rebase -i ($1)"" export GIT_REFLOG_ACTION } } setglobal last_count = '' proc mark_action_done { sed -e 1q < $todo >> $done sed -e 1d < $todo >> "$todo".new mv -f "$todo".new $todo setglobal new_count = $shExpr(' $(git stripspace --strip-comments <"$done" | wc -l) ') echo $new_count >$msgnum setglobal total = $shExpr('$new_count + $(git stripspace --strip-comments <"$todo" | wc -l)') echo $total >$end if test $last_count != $new_count { setglobal 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 { setglobal sha1_and_parents = $[git rev-list --parents -1 $1] match $sha1_and_parents { with ?*' '?*' '?* git diff --cc $sha1_and_parents with ?*' '?* git diff-tree -p "$1^!" with * 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 setglobal 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 { setglobal tree = $[git rev-parse -q --verify "$1"^{tree} !2 >/dev/null] || do { setglobal sha1 = $1 die $[eval_gettext "\$sha1: not a commit that can be picked] } setglobal ptree = $[git rev-parse -q --verify "$1"^^{tree} !2 >/dev/null] || setglobal 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 { setglobal GIT_SEQUENCE_EDITOR = $[git config sequence.editor] if test -z $GIT_SEQUENCE_EDITOR { setglobal GIT_SEQUENCE_EDITOR = $[git var GIT_EDITOR] || return $? } } eval $GIT_SEQUENCE_EDITOR '"$@"' } proc pick_one { setglobal ff = '--ff' match $1 { with -n setglobal sha1 = $2; setglobal ff = '' with * setglobal sha1 = $1 } match $force_rebase { with '' with ?* setglobal ff = '' } output git rev-parse --verify $sha1 || die $[eval_gettext "Invalid commit name: \$sha1] if is_empty_commit $sha1 { setglobal 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. setglobal ret = $Status match $ret { with [01] with * reschedule_last_action } return $ret } proc pick_one_preserving_merges { setglobal fast_forward = 't' match $1 { with -n setglobal fast_forward = 'f' setglobal sha1 = $2 with * setglobal sha1 = $1 } setglobal 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. setglobal new_parents = '' setglobal pend = "" $[git rev-list --parents -1 $sha1 | cut -d' ' -s -f2-]"" if test $pend = " " { setglobal pend = '" root'" } while [ "$pend" != "" ] { setglobal p = $[expr $pend : ' \([^ ]*\)] setglobal pend = $(pend# $p) if test -f "$rewritten"/$p { setglobal 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 { setglobal new_p = $[git rev-parse HEAD] } test $p != $new_p && setglobal fast_forward = 'f' match $new_parents { with *$new_p* # do nothing; that parent is already there with * setglobal new_parents = ""$new_parents $new_p"" } } else { if test -f "$dropped"/$p { setglobal fast_forward = 'f' setglobal replacement = $[cat "$dropped"/$p] test -z $replacement && setglobal replacement = 'root' setglobal pend = "" $replacement$pend"" } else { setglobal new_parents = ""$new_parents $p"" } } } match $fast_forward { with t output warn $[eval_gettext "Fast-forward to \$sha1] output git reset --hard $sha1 || die $[eval_gettext "Cannot fast-forward to \$sha1] with f setglobal 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] } match $new_parents { with ' '*' '* test "a$1" = a-n && die $[eval_gettext "Refusing to squash a merge: \$sha1] # redo merge setglobal author_script_content = $[get_author_ident_from_commit $sha1] eval $author_script_content setglobal msg_content = $[commit_message $sha1] # No point in merging the first parent, that's HEAD setglobal new_parents = $(new_parents# $first_parent) setglobal 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 with * 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 { setglobal n = $1 eval_gettext "This is the commit message #\${n}:" } proc skip_nth_commit_message { setglobal 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 setglobal count = $shExpr('$(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] setglobal 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" } match $1 { with squash rm -f $fixup_msg echo printf '%s\n' "$comment_char $[this_nth_commit_message $count]" echo commit_message $2 with 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 { setglobal sha1 = $1 setglobal 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 setglobal newsha1 = $[git rev-parse HEAD^0] sed "s/$/ $newsha1/" < $rewritten_pending >> $rewritten_list rm -f $rewritten_pending } proc record_in_rewritten { setglobal oldsha1 = $[git rev-parse $1] echo $oldsha1 >> $rewritten_pending match $[peek_next_command] { with squash|s|fixup|f with * flush_rewritten_pending } } proc do_pick { setglobal sha1 = $1 setglobal 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 match $command { with "$comment_char"*|''|noop|drop|d mark_action_done with "$cr" # Work around CR left by "read" (e.g. with Git for Windows' Bash). mark_action_done with pick|p comment_for_reflog pick mark_action_done do_pick $sha1 $rest record_in_rewritten $sha1 with 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 with edit|e comment_for_reflog edit mark_action_done do_pick $sha1 $rest setglobal sha1_abbrev = $[git rev-parse --short $sha1] warn $[eval_gettext "Stopped at \$sha1_abbrev... \$rest] exit_with_patch $sha1 0 with squash|s|fixup|f match $command { with squash|s setglobal squash_style = 'squash' with fixup|f setglobal 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 setglobal 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 } match $[peek_next_command] { with 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 with * # 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 with x|"exec" read -r command rest < $todo mark_action_done eval_gettextln "Executing: \$rest" $(SHELL:-@SHELL_PATH@) -c $rest # Actual execution setglobal status = $Status # Run in subshell because require_clean_work_tree can die. setglobal dirty = 'f' shell {require_clean_work_tree "rebase" !2 >/dev/null} || setglobal 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 { setglobal 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 } with * warn $[eval_gettext "Unknown command: \$command \$sha1 \$rest] setglobal 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 && setglobal newhead = $[git rev-parse HEAD] && match $head_name { with refs/* setglobal 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 } && setglobal 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 { setglobal fd = '3' while read -r command rest { # fd=3 means we skip the command match "$fd,$command" { with 3,pick|3,p # pick a commit whose parent is current $onto -> skip setglobal sha1 = $(rest%% *) match $[git rev-parse --verify --quiet "$sha1"^] { with "$onto"* setglobal onto = $sha1 with * setglobal fd = '1' } with 3,"$comment_char"*|3, # copy comments with * setglobal fd = '1' } printf '%s\n' "$command$(rest:+ )$rest" > !$fd } <"$todo" >"$todo.new" 3>>"$done" && mv -f "$todo".new $todo && match $[peek_next_command] { with 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 { match $command { with "$comment_char"* | exec # Be careful for oddball commands like 'exec' # that do not have a SHA-1 at the beginning of $rest. with * setglobal sha1 = $[git rev-parse --verify --quiet @Argv $(rest%%[ ]*)] && setglobal 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) || setglobal message = $[git log -n 1 --format="%s" $(sha1)] match $message { with "squash! "*|"fixup! "* setglobal action = $(message%%!*) setglobal rest = $message setglobal prefix = '' # skip all squash! or fixup! (but save for later) while : { match $rest { with "squash! "*|"fixup! "* setglobal prefix = ""$prefix$(rest%%!*),"" setglobal rest = $(rest#*! ) with * 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 { setglobal 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 setglobal used = '' while read -r pick sha1 message { match " $used" { with *" $sha1 "* continue } printf '%s\n' "$pick $sha1 $message" test -z $(format) || setglobal message = $[git log -n 1 --format="%s" $(sha1)] setglobal used = ""$used$sha1 "" while read -r squash action msg_prefix msg_content { match " $used" { with *" $squash "* continue } setglobal emit = '0' match $action { with +* setglobal action = $(action#+) # full sha1 prefix test match $msg_content { with "$sha1"* setglobal emit = '1' } with * # message prefix test match $message { with "$msg_content"* setglobal emit = '1' } } if test $emit = 1 { if test -n $(format) { setglobal msg_content = $[git log -n 1 --format="$(format)" $(squash)] } else { setglobal msg_content = ""$[echo $msg_prefix | sed "s/,/! /g]$msg_content"" } printf '%s\n' "$action $squash $msg_content" setglobal 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 { setglobal first = 't' while read -r insn rest { match $insn { with pick test -n $first || printf "%s" $cmd } printf "%s %s\n" $insn $rest setglobal 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 { setglobal badsha = '0' if test -z $1 { setglobal badsha = '1' } else { setglobal sha1_verif = $[git rev-parse --verify --quiet $1^{commit}] if test -z $sha1_verif { setglobal badsha = '1' } } if test $badsha -ne 0 { setglobal 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 { setglobal retval = '0' setglobal lineno = '0' while read -r command rest { setglobal lineno = $shExpr(' $lineno + 1 ') match $command { with "$comment_char"*|''|noop|x|exec # Doesn't expect a SHA-1 with "$cr" # Work around CR left by "read" (e.g. with Git for # Windows' Bash). with pick|p|drop|d|reword|r|edit|e|squash|s|fixup|f if ! check_commit_sha $(rest%%[ ]*) $lineno $1 { setglobal retval = '1' } with * setglobal line = $[sed -n -e "$(lineno)p" $1] warn $[eval_gettext "\ Warning: the command isn't recognized in the following line: - \$line] warn setglobal 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 { match $command { with "$comment_char"*|''|noop|x|"exec" with * setglobal 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 { setglobal 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 { setglobal check_level = $[git config --get rebase.missingCommitsCheck] setglobal 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 { setglobal raise_error = 'f' setglobal check_level = $[get_missing_commit_check_level] match $check_level { with 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 && setglobal 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 setglobal 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 } with ignore with * warn $[eval_gettext "Unrecognized setting \$check_level for option rebase.missingCommitsCheck. Ignoring.] } if ! check_bad_cmd_and_sha $todo { setglobal 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 { match $action { with 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 { setglobal 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 { setglobal 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 with skip git rerere clear do_rest return 0 with 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 { setglobal 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 } setglobal 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 setglobal merges_option = '' } else { setglobal merges_option = '"--no-merges --cherry-pick'" } setglobal shorthead = $[git rev-parse --short $orig_head] setglobal shortonto = $[git rev-parse --short $onto] if test -z $rebase_root # this is now equivalent to ! -z "$upstream" { setglobal shortupstream = $[git rev-parse --short $upstream] setglobal revisions = "$upstream...$orig_head" setglobal shortrevisions = "$shortupstream..$shorthead" } else { setglobal revisions = "$onto...$orig_head" setglobal shortrevisions = $shorthead } setglobal 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 { setglobal comment_out = ""$comment_char "" } else { setglobal comment_out = '' } if test t != $preserve_merges { printf '%s\n' "$(comment_out)pick $sha1 $rest" >>$todo } else { if test -z $rebase_root { setglobal preserve = 't' for p in [$[git rev-list --parents -1 $sha1 | cut -d' ' -s -f2-]] { if test -f "$rewritten"/$p { setglobal preserve = 'f' } } } else { setglobal 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 setglobal 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 setglobal todocount = $[git stripspace --strip-comments <$todo | wc -l] setglobal 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