diff options
Diffstat (limited to 'git-rebase--interactive.sh')
-rw-r--r-- | git-rebase--interactive.sh | 263 |
1 files changed, 205 insertions, 58 deletions
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index 65690af893..44901d53c4 100644 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -9,9 +9,7 @@ # # The original idea comes from Eric W. Biederman, in # http://article.gmane.org/gmane.comp.version-control.git/22407 - -. git-sh-setup - +# # 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 @@ -117,6 +115,23 @@ mark_action_done () { fi } +append_todo_help () { + cat >> "$todo" << EOF +# +# 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 +# +# These lines can be re-ordered; they are executed from top to bottom. +# +# If you remove a line here THAT COMMIT WILL BE LOST. +EOF +} + make_patch () { sha1_and_parents="$(git rev-list --parents -1 "$1")" case "$sha1_and_parents" in @@ -143,6 +158,21 @@ die_with_patch () { die "$2" } +exit_with_patch () { + echo "$1" > "$state_dir"/stopped-sha + make_patch $1 + git rev-parse --verify HEAD > "$amend" + warn "You can amend the commit now, with" + warn + warn " git commit --amend" + warn + warn "Once you are satisfied with your changes, run" + warn + warn " git rebase --continue" + warn + exit $2 +} + die_abort () { rm -rf "$state_dir" die "$1" @@ -152,6 +182,14 @@ has_action () { sane_grep '^[^#]' "$1" >/dev/null } +is_empty_commit() { + tree=$(git rev-parse -q --verify "$1"^{tree} 2>/dev/null || + die "$1: not a commit that can be picked") + ptree=$(git rev-parse -q --verify "$1"^^{tree} 2>/dev/null || + ptree=4b825dc642cb6eb9a060e54bf8d69288fbee4904) + test "$tree" = "$ptree" +} + # Run command with GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and # GIT_AUTHOR_DATE exported from the current environment. do_with_author () { @@ -161,14 +199,34 @@ do_with_author () { ) } +git_sequence_editor () { + if test -z "$GIT_SEQUENCE_EDITOR" + then + GIT_SEQUENCE_EDITOR="$(git config sequence.editor)" + if [ -z "$GIT_SEQUENCE_EDITOR" ] + then + GIT_SEQUENCE_EDITOR="$(git var GIT_EDITOR)" || return $? + fi + fi + + eval "$GIT_SEQUENCE_EDITOR" '"$@"' +} + pick_one () { ff=--ff + case "$1" in -n) sha1=$2; ff= ;; *) sha1=$1 ;; esac case "$force_rebase" in '') ;; ?*) ff= ;; esac output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" + + if is_empty_commit "$sha1" + then + empty_args="--allow-empty" + fi + test -d "$rewritten" && pick_one_preserving_merges "$@" && return - output git cherry-pick $ff "$@" + output git cherry-pick $empty_args $ff "$@" } pick_one_preserving_merges () { @@ -374,6 +432,29 @@ record_in_rewritten() { esac } +do_pick () { + if test "$(git rev-parse HEAD)" = "$squash_onto" + then + # 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 $1 && + pick_one -n $1 && + git commit --allow-empty --allow-empty-message \ + --amend --no-post-rewrite -n -q -C $1 || + die_with_patch $1 "Could not apply $1... $2" + else + pick_one $1 || + die_with_patch $1 "Could not apply $1... $2" + fi +} + do_next () { rm -f "$msg" "$author_script" "$amend" || exit read -r command sha1 rest < "$todo" @@ -385,38 +466,30 @@ do_next () { comment_for_reflog pick mark_action_done - pick_one $sha1 || - die_with_patch $sha1 "Could not apply $sha1... $rest" + do_pick $sha1 "$rest" record_in_rewritten $sha1 ;; reword|r) comment_for_reflog reword mark_action_done - pick_one $sha1 || - die_with_patch $sha1 "Could not apply $sha1... $rest" - git commit --amend --no-post-rewrite + do_pick $sha1 "$rest" + git commit --amend --no-post-rewrite || { + 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" + warn "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 - pick_one $sha1 || - die_with_patch $sha1 "Could not apply $sha1... $rest" - echo "$sha1" > "$state_dir"/stopped-sha - make_patch $sha1 - git rev-parse --verify HEAD > "$amend" + do_pick $sha1 "$rest" warn "Stopped at $sha1... $rest" - warn "You can amend the commit now, with" - warn - warn " git commit --amend" - warn - warn "Once you are satisfied with your changes, run" - warn - warn " git rebase --continue" - warn - exit 0 + exit_with_patch $sha1 0 ;; squash|s|fixup|f) case "$command" in @@ -437,25 +510,28 @@ do_next () { author_script_content=$(get_author_ident_from_commit HEAD) echo "$author_script_content" > "$author_script" eval "$author_script_content" - output git reset --soft HEAD^ - pick_one -n $sha1 || die_failed_squash $sha1 "$rest" + if ! pick_one -n $sha1 + then + git rev-parse --verify HEAD >"$amend" + die_failed_squash $sha1 "$rest" + fi case "$(peek_next_command)" in 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 --no-verify -F "$squash_msg" || + do_with_author output git commit --amend --no-verify -F "$squash_msg" || 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 --no-verify -F "$fixup_msg" || + do_with_author git commit --amend --no-verify -F "$fixup_msg" || 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 --no-verify -e || + do_with_author git commit --amend --no-verify -F "$GIT_DIR"/SQUASH_MSG -e || die_failed_squash $sha1 "$rest" fi rm -f "$squash_msg" "$fixup_msg" @@ -472,18 +548,28 @@ do_next () { git rev-parse --verify HEAD > "$state_dir"/stopped-sha ${SHELL:-@SHELL_PATH@} -c "$rest" # Actual execution status=$? + # Run in subshell because require_clean_work_tree can die. + dirty=f + (require_clean_work_tree "rebase" 2>/dev/null) || dirty=t if test "$status" -ne 0 then warn "Execution failed: $rest" + test "$dirty" = f || + warn "and made changes to the index and/or the working tree" + warn "You can fix the problem, and then run" warn warn " git rebase --continue" warn + if test $status -eq 127 # command not found + then + status=1 + fi exit "$status" - fi - # Run in subshell because require_clean_work_tree can die. - if ! (require_clean_work_tree "rebase") + elif test "$dirty" = t then + warn "Execution succeeded: $rest" + warn "but left changes to the index and/or the working tree" warn "Commit or stash your changes, and then run" warn warn " git rebase --continue" @@ -493,22 +579,22 @@ do_next () { ;; *) warn "Unknown command: $command $sha1 $rest" + fixtodo="Please fix this using 'git rebase --edit-todo'." if git rev-parse --verify -q "$sha1" >/dev/null then - die_with_patch $sha1 "Please fix this in the file $todo." + die_with_patch $sha1 "$fixtodo" else - die "Please fix this in the file $todo." + die "$fixtodo" fi ;; esac test -s "$todo" && return comment_for_reflog finish && - shortonto=$(git rev-parse --short $onto) && newhead=$(git rev-parse HEAD) && case $head_name in refs/*) - message="$GIT_REFLOG_ACTION: $head_name onto $shortonto" && + 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" \ @@ -640,16 +726,52 @@ rearrange_squash () { rm -f "$1.sq" "$1.rearranged" } +# Add commands after a pick or after a squash/fixup serie +# in the todo list. +add_exec_commands () { + { + first=t + while read -r insn rest + do + case $insn in + pick) + test -n "$first" || + printf "%s" "$cmd" + ;; + esac + printf "%s %s\n" "$insn" "$rest" + first= + done + printf "%s" "$cmd" + } <"$1" >"$1.new" && + mv "$1.new" "$1" +} + case "$action" in continue) # do we have anything to commit? - if git diff-index --cached --quiet --ignore-submodules HEAD -- + if git diff-index --cached --quiet HEAD -- then : Nothing to commit -- skip this else + if ! test -f "$author_script" + then + 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 + +If they are meant to go into a new commit, run: + + git commit + +In both case, once you're done, continue with: + + git rebase --continue +" + fi . "$author_script" || - die "Cannot find the author identity" - current_head= + die "Error trying to find the author identity to amend commit" if test -f "$amend" then current_head=$(git rev-parse --verify HEAD) @@ -657,13 +779,12 @@ continue) die "\ You have uncommitted changes in your working tree. Please, commit them first and then run 'git rebase --continue' again." - git reset --soft HEAD^ || - die "Cannot rewind the HEAD" + do_with_author git commit --amend --no-verify -F "$msg" -e || + die "Could not commit staged changes." + else + do_with_author git commit --no-verify -F "$msg" -e || + die "Could not commit staged changes." fi - do_with_author git commit --no-verify -F "$msg" -e || { - test -n "$current_head" && git reset --soft $current_head - die "Could not commit staged changes." - } fi record_in_rewritten "$(cat "$state_dir"/stopped-sha)" @@ -676,6 +797,23 @@ skip) do_rest ;; +edit-todo) + sed -e '/^#/d' < "$todo" > "$todo".new + mv -f "$todo".new "$todo" + append_todo_help + cat >> "$todo" << EOF +# +# You are editing the todo file of an ongoing interactive rebase. +# To continue rebase after editing, run: +# git rebase --continue +# +EOF + + git_sequence_editor "$todo" || + die "Could not execute editor" + + exit + ;; esac git var GIT_COMMITTER_IDENT >/dev/null || @@ -713,7 +851,6 @@ then # parents to rewrite and skipping dropped commits would # prematurely end our probe merges_option= - first_after_upstream="$(git rev-list --reverse --first-parent $upstream..$orig_head | head -n 1)" else merges_option="--no-merges --cherry-pick" fi @@ -736,9 +873,17 @@ git rev-list $merges_option --pretty=oneline --abbrev-commit \ sed -n "s/^>//p" | while read -r shortsha1 rest do + + if test -z "$keep_empty" && is_empty_commit $shortsha1 + then + comment_out="# " + else + comment_out= + fi + if test t != "$preserve_merges" then - printf '%s\n' "pick $shortsha1 $rest" >> "$todo" + printf '%s\n' "${comment_out}pick $shortsha1 $rest" >>"$todo" else sha1=$(git rev-parse $shortsha1) if test -z "$rebase_root" @@ -746,7 +891,7 @@ do preserve=t for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -s -f2-) do - if test -f "$rewritten"/$p -a \( $p != $onto -o $sha1 = $first_after_upstream \) + if test -f "$rewritten"/$p then preserve=f fi @@ -757,7 +902,7 @@ do if test f = "$preserve" then touch "$rewritten"/$sha1 - printf '%s\n' "pick $shortsha1 $rest" >> "$todo" + printf '%s\n' "${comment_out}pick $shortsha1 $rest" >>"$todo" fi fi done @@ -790,28 +935,30 @@ fi test -s "$todo" || echo noop >> "$todo" test -n "$autosquash" && rearrange_squash "$todo" +test -n "$cmd" && add_exec_commands "$todo" + cat >> "$todo" << EOF # Rebase $shortrevisions onto $shortonto +EOF +append_todo_help +cat >> "$todo" << EOF # -# 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 -# -# If you remove a line here THAT COMMIT WILL BE LOST. # However, if you remove everything, the rebase will be aborted. # EOF +if test -z "$keep_empty" +then + echo "# Note that empty commits are commented out" >>"$todo" +fi + + has_action "$todo" || die_abort "Nothing to do" cp "$todo" "$todo".backup -git_editor "$todo" || +git_sequence_editor "$todo" || die_abort "Could not execute editor" has_action "$todo" || |