Merge branch 'mh/rebase-fixup'
authorJunio C Hamano <gitster@pobox.com>
Wed, 20 Jan 2010 22:42:07 +0000 (14:42 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 20 Jan 2010 22:42:07 +0000 (14:42 -0800)
* mh/rebase-fixup:
rebase -i: Retain user-edited commit messages after squash/fixup conflicts
t3404: Set up more of the test repo in the "setup" step
rebase -i: For fixup commands without squashes, do not start editor
rebase -i: Change function make_squash_message into update_squash_message
rebase -i: Extract function do_with_author
rebase -i: Handle the author script all in one place in do_next
rebase -i: Extract a function "commit_message"
rebase -i: Simplify commit counting for generated commit messages
rebase -i: Improve consistency of commit count in generated commit messages
t3404: Test the commit count in commit messages generated by "rebase -i"
rebase -i: Introduce a constant AMEND
rebase -i: Introduce a constant AUTHOR_SCRIPT
rebase -i: Document how temporary files are used
rebase -i: Use symbolic constant $MSG consistently
rebase -i: Use "test -n" instead of "test ! -z"
rebase -i: Inline expression
rebase -i: Remove dead code
rebase -i: Make the condition for an "if" more transparent

1  2 
git-rebase--interactive.sh
index 2e56e64a179bb719e680fbefad1c7fc84fe13199,b835a2975987861cdea59c271aee1b3dea63cee6..e551906ecd2a72fa56fa6ef5dba8f12708acb337
@@@ -28,26 -28,79 +28,81 @@@ abort              abort rebasing proce
  skip               skip current patch and continue rebasing process
  no-verify          override pre-rebase hook from stopping the operation
  root               rebase all reachable commmits up to the root(s)
 +autosquash         move commits that begin with squash!/fixup! under -i
  "
  
  . git-sh-setup
  require_work_tree
  
  DOTEST="$GIT_DIR/rebase-merge"
+ # 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.
  TODO="$DOTEST"/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.
  DONE="$DOTEST"/done
+ # The commit message that is planned to be used for any changes that
+ # need to be committed following a user interaction.
  MSG="$DOTEST"/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.
  SQUASH_MSG="$DOTEST"/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.)
+ FIXUP_MSG="$DOTEST"/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.)
  REWRITTEN="$DOTEST"/rewritten
  DROPPED="$DOTEST"/dropped
+ # 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.
+ AUTHOR_SCRIPT="$DOTEST"/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.
+ AMEND="$DOTEST"/amend
  PRESERVE_MERGES=
  STRATEGY=
  ONTO=
  VERBOSE=
  OK_TO_SKIP_PRE_REBASE=
  REBASE_ROOT=
 +AUTOSQUASH=
  
  GIT_CHERRY_PICK_HELP="  After resolving the conflicts,
  mark the corrected paths with 'git add <paths>', and
@@@ -72,6 -125,11 +127,11 @@@ output () 
        esac
  }
  
+ # Output the commit message for the specified commit.
+ commit_message () {
+       git cat-file commit "$1" | sed "1,/^$/d"
+ }
  run_pre_rebase_hook () {
        if test -z "$OK_TO_SKIP_PRE_REBASE" &&
           test -x "$GIT_DIR/hooks/pre-rebase"
@@@ -131,10 -189,10 +191,10 @@@ make_patch () 
                echo "Root commit"
                ;;
        esac > "$DOTEST"/patch
-       test -f "$DOTEST"/message ||
-               git cat-file commit "$1" | sed "1,/^$/d" > "$DOTEST"/message
-       test -f "$DOTEST"/author-script ||
-               get_author_ident_from_commit "$1" > "$DOTEST"/author-script
+       test -f "$MSG" ||
+               commit_message "$1" > "$MSG"
+       test -f "$AUTHOR_SCRIPT" ||
+               get_author_ident_from_commit "$1" > "$AUTHOR_SCRIPT"
  }
  
  die_with_patch () {
@@@ -152,13 -210,22 +212,22 @@@ has_action () 
        sane_grep '^[^#]' "$1" >/dev/null
  }
  
+ # Run command with GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
+ # GIT_AUTHOR_DATE exported from the current environment.
+ do_with_author () {
+       GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
+       GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
+       GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
+       "$@"
+ }
  pick_one () {
        no_ff=
        case "$1" in -n) sha1=$2; no_ff=t ;; *) sha1=$1 ;; esac
        output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
        test -d "$REWRITTEN" &&
                pick_one_preserving_merges "$@" && return
-       if test ! -z "$REBASE_ROOT"
+       if test -n "$REBASE_ROOT"
        then
                output git cherry-pick "$@"
                return
        parent_sha1=$(git rev-parse --verify $sha1^) ||
                die "Could not get the parent of $sha1"
        current_sha1=$(git rev-parse --verify HEAD)
-       if test "$no_ff$current_sha1" = "$parent_sha1"; then
+       if test -z "$no_ff" -a "$current_sha1" = "$parent_sha1"
+       then
                output git reset --hard $sha1
-               test "a$1" = a-n && output git reset --soft $current_sha1
-               sha1=$(git rev-parse --short $sha1)
-               output warn Fast-forward to $sha1
+               output warn Fast-forward to $(git rev-parse --short $sha1)
        else
                output git cherry-pick "$@"
        fi
@@@ -271,14 -337,11 +339,11 @@@ pick_one_preserving_merges () 
                        # redo merge
                        author_script=$(get_author_ident_from_commit $sha1)
                        eval "$author_script"
-                       msg="$(git cat-file commit $sha1 | sed -e '1,/^$/d')"
+                       msg="$(commit_message $sha1)"
                        # No point in merging the first parent, that's HEAD
                        new_parents=${new_parents# $first_parent}
-                       if ! GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
-                               GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
-                               GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
-                               output git merge $STRATEGY -m "$msg" \
-                                       $new_parents
+                       if ! do_with_author output \
+                               git merge $STRATEGY -m "$msg" $new_parents
                        then
                                printf "%s\n" "$msg" > "$GIT_DIR"/MERGE_MSG
                                die_with_patch $sha1 "Error redoing merge $sha1"
@@@ -302,50 -365,66 +367,66 @@@ nth_string () 
        esac
  }
  
make_squash_message () {
update_squash_messages () {
        if test -f "$SQUASH_MSG"; then
-               # We want to be careful about matching only the commit
-               # message comment lines generated by this function.
-               # "[snrt][tdh]" matches the nth_string endings.
-               COUNT=$(($(sed -n "s/^# Th[^0-9]*\([1-9][0-9]*\)[snrt][tdh] commit message.*:/\1/p" \
-                       < "$SQUASH_MSG" | sed -ne '$p')+1))
-               echo "# This is a combination of $COUNT commits."
-               sed -e 1d -e '2,/^./{
-                       /^$/d
-               }' <"$SQUASH_MSG"
+               mv "$SQUASH_MSG" "$SQUASH_MSG".bak || exit
+               COUNT=$(($(sed -n \
+                       -e "1s/^# This is a combination of \(.*\) commits\./\1/p" \
+                       -e "q" < "$SQUASH_MSG".bak)+1))
+               {
+                       echo "# This is a combination of $COUNT commits."
+                       sed -e 1d -e '2,/^./{
+                               /^$/d
+                       }' <"$SQUASH_MSG".bak
+               } >$SQUASH_MSG
        else
+               commit_message HEAD > "$FIXUP_MSG" || die "Cannot write $FIXUP_MSG"
                COUNT=2
-               echo "# This is a combination of two commits."
-               echo "# The first commit's message is:"
-               echo
-               git cat-file commit HEAD | sed -e '1,/^$/d'
+               {
+                       echo "# This is a combination of 2 commits."
+                       echo "# The first commit's message is:"
+                       echo
+                       cat "$FIXUP_MSG"
+               } >$SQUASH_MSG
        fi
        case $1 in
        squash)
+               rm -f "$FIXUP_MSG"
                echo
                echo "# This is the $(nth_string $COUNT) commit message:"
                echo
-               git cat-file commit $2 | sed -e '1,/^$/d'
+               commit_message $2
                ;;
        fixup)
                echo
                echo "# The $(nth_string $COUNT) commit message will be skipped:"
                echo
-               # Comment the lines of the commit message out using
-               # "#    " rather than "# " to make them less likely to
-               # confuse the sed regexp above.
-               git cat-file commit $2 | sed -e '1,/^$/d' -e 's/^/#     /'
+               commit_message $2 | sed -e 's/^/#       /'
                ;;
-       esac
+       esac >>$SQUASH_MSG
  }
  
  peek_next_command () {
        sed -n -e "/^#/d" -e "/^$/d" -e "s/ .*//p" -e "q" < "$TODO"
  }
  
+ # 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
+ die_failed_squash() {
+       mv "$SQUASH_MSG" "$MSG" || exit
+       rm -f "$FIXUP_MSG"
+       cp "$MSG" "$GIT_DIR"/MERGE_MSG || exit
+       warn
+       warn "Could not apply $1... $2"
+       die_with_patch $1 ""
+ }
  do_next () {
-       rm -f "$DOTEST"/message "$DOTEST"/author-script \
-               "$DOTEST"/amend || exit
+       rm -f "$MSG" "$AUTHOR_SCRIPT" "$AMEND" || exit
        read command sha1 rest < "$TODO"
        case "$command" in
        '#'*|''|noop)
                pick_one $sha1 ||
                        die_with_patch $sha1 "Could not apply $sha1... $rest"
                make_patch $sha1
-               git rev-parse --verify HEAD > "$DOTEST"/amend
+               git rev-parse --verify HEAD > "$AMEND"
                warn "Stopped at $sha1... $rest"
                warn "You can amend the commit now, with"
                warn
                        die "Cannot '$squash_style' without a previous commit"
  
                mark_action_done
-               make_squash_message $squash_style $sha1 > "$MSG"
-               failed=f
+               update_squash_messages $squash_style $sha1
                author_script=$(get_author_ident_from_commit HEAD)
+               echo "$author_script" > "$AUTHOR_SCRIPT"
+               eval "$author_script"
                output git reset --soft HEAD^
-               pick_one -n $sha1 || failed=t
+               pick_one -n $sha1 || die_failed_squash $sha1 "$rest"
                case "$(peek_next_command)" in
                squash|s|fixup|f)
-                       USE_OUTPUT=output
-                       MSG_OPT=-F
-                       EDIT_OR_FILE="$MSG"
-                       cp "$MSG" "$SQUASH_MSG"
+                       # 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" ||
+                               die_failed_squash $sha1 "$rest"
                        ;;
                *)
-                       USE_OUTPUT=
-                       MSG_OPT=
-                       EDIT_OR_FILE=-e
-                       rm -f "$SQUASH_MSG" || exit
-                       cp "$MSG" "$GIT_DIR"/SQUASH_MSG
-                       rm -f "$GIT_DIR"/MERGE_MSG || exit
+                       # 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" ||
+                                       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 ||
+                                       die_failed_squash $sha1 "$rest"
+                       fi
+                       rm -f "$SQUASH_MSG" "$FIXUP_MSG"
                        ;;
                esac
-               echo "$author_script" > "$DOTEST"/author-script
-               if test $failed = f
-               then
-                       # This is like --amend, but with a different message
-                       eval "$author_script"
-                       GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
-                       GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
-                       GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
-                       $USE_OUTPUT git commit --no-verify \
-                               $MSG_OPT "$EDIT_OR_FILE" || failed=t
-               fi
-               if test $failed = t
-               then
-                       cp "$MSG" "$GIT_DIR"/MERGE_MSG
-                       warn
-                       warn "Could not apply $sha1... $rest"
-                       die_with_patch $sha1 ""
-               fi
                ;;
        *)
                warn "Unknown command: $command $sha1 $rest"
@@@ -521,56 -589,6 +591,56 @@@ get_saved_options () 
        test -f "$DOTEST"/rebase-root && REBASE_ROOT=t
  }
  
 +# 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".
 +rearrange_squash () {
 +      sed -n -e 's/^pick \([0-9a-f]*\) \(squash\)! /\1 \2 /p' \
 +              -e 's/^pick \([0-9a-f]*\) \(fixup\)! /\1 \2 /p' \
 +              "$1" >"$1.sq"
 +      test -s "$1.sq" || return
 +
 +      used=
 +      while read pick sha1 message
 +      do
 +              case " $used" in
 +              *" $sha1 "*) continue ;;
 +              esac
 +              echo "$pick $sha1 $message"
 +              while read squash action msg
 +              do
 +                      case "$message" in
 +                      "$msg"*)
 +                              echo "$action $squash $action! $msg"
 +                              used="$used$squash "
 +                              ;;
 +                      esac
 +              done <"$1.sq"
 +      done >"$1.rearranged" <"$1"
 +      cat "$1.rearranged" >"$1"
 +      rm -f "$1.sq" "$1.rearranged"
 +}
 +
 +LF='
 +'
 +parse_onto () {
 +      case "$1" in
 +      *...*)
 +              if      left=${1%...*} right=${1#*...} &&
 +                      onto=$(git merge-base --all ${left:-HEAD} ${right:-HEAD})
 +              then
 +                      case "$onto" in
 +                      ?*"$LF"?* | '')
 +                              exit 1 ;;
 +                      esac
 +                      echo "$onto"
 +                      exit 0
 +              fi
 +      esac
 +      git rev-parse --verify "$1^0"
 +}
 +
  while test $# != 0
  do
        case "$1" in
                then
                        : Nothing to commit -- skip this
                else
-                       . "$DOTEST"/author-script ||
+                       . "$AUTHOR_SCRIPT" ||
                                die "Cannot find the author identity"
                        amend=
-                       if test -f "$DOTEST"/amend
+                       if test -f "$AMEND"
                        then
                                amend=$(git rev-parse --verify HEAD)
-                               test "$amend" = $(cat "$DOTEST"/amend) ||
+                               test "$amend" = $(cat "$AMEND") ||
                                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"
                        fi
-                       export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE &&
-                       git commit --no-verify -F "$DOTEST"/message -e || {
+                       do_with_author git commit --no-verify -F "$MSG" -e || {
                                test -n "$amend" && git reset --soft $amend
                                die "Could not commit staged changes."
                        }
        --root)
                REBASE_ROOT=t
                ;;
 +      --autosquash)
 +              AUTOSQUASH=t
 +              ;;
        --onto)
                shift
 -              ONTO=$(git rev-parse --verify "$1") ||
 +              ONTO=$(parse_onto "$1") ||
                        die "Does not point to a valid commit: $1"
                ;;
        --)
                test t = "$VERBOSE" && : > "$DOTEST"/verbose
                if test t = "$PRESERVE_MERGES"
                then
-                       # $REWRITTEN contains 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.)
                        if test -z "$REBASE_ROOT"
                        then
                                mkdir "$REWRITTEN" &&
                fi
  
                test -s "$TODO" || echo noop >> "$TODO"
 +              test -n "$AUTOSQUASH" && rearrange_squash "$TODO"
                cat >> "$TODO" << EOF
  
  # Rebase $SHORTREVISIONS onto $SHORTONTO
@@@ -860,7 -866,7 +922,7 @@@ EO
  
                cp "$TODO" "$TODO".backup
                git_editor "$TODO" ||
 -                      die "Could not execute editor"
 +                      die_abort "Could not execute editor"
  
                has_action "$TODO" ||
                        die_abort "Nothing to do"