a81432c0a5b9e5caca47d58665b47c75accacc74
   1#!/bin/sh
   2#
   3# Copyright (c) 2006 Johannes E. Schindelin
   4
   5# SHORT DESCRIPTION
   6#
   7# This script makes it easy to fix up commits in the middle of a series,
   8# and rearrange commits.
   9#
  10# The original idea comes from Eric W. Biederman, in
  11# http://article.gmane.org/gmane.comp.version-control.git/22407
  12
  13USAGE='(--continue | --abort | --skip | [--onto <branch>] <upstream> [<branch>])'
  14
  15. git-sh-setup
  16require_work_tree
  17
  18DOTEST="$GIT_DIR/.dotest-merge"
  19TODO="$DOTEST"/todo
  20DONE="$DOTEST"/done
  21STRATEGY=
  22VERBOSE=
  23
  24warn () {
  25        echo "$*" >&2
  26}
  27
  28require_clean_work_tree () {
  29        # test if working tree is dirty
  30        git rev-parse --verify HEAD > /dev/null &&
  31        git update-index --refresh &&
  32        git diff-files --quiet &&
  33        git diff-index --cached --quiet HEAD ||
  34        die "Working tree is dirty"
  35}
  36
  37ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION"
  38
  39comment_for_reflog () {
  40        case "$ORIG_REFLOG_ACTION" in
  41        ''|rebase*)
  42                GIT_REFLOG_ACTION="rebase -i ($1)"
  43                export GIT_REFLOG_ACTION
  44        esac
  45}
  46
  47mark_action_done () {
  48        sed -e 1q < "$TODO" >> "$DONE"
  49        sed -e 1d < "$TODO" >> "$TODO".new
  50        mv -f "$TODO".new "$TODO"
  51}
  52
  53make_patch () {
  54        parent_sha1=$(git rev-parse --verify "$1"^ 2> /dev/null)
  55        git diff "$parent_sha1".."$1" > "$DOTEST"/patch
  56}
  57
  58die_with_patch () {
  59        make_patch "$1"
  60        die "$2"
  61}
  62
  63die_abort () {
  64        rm -rf "$DOTEST"
  65        die "$1"
  66}
  67
  68pick_one () {
  69        case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
  70        git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
  71        parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null)
  72        current_sha1=$(git rev-parse --verify HEAD)
  73        if [ $current_sha1 = $parent_sha1 ]; then
  74                git reset --hard $sha1
  75                sha1=$(git rev-parse --short $sha1)
  76                warn Fast forward to $sha1
  77        else
  78                git cherry-pick $STRATEGY "$@"
  79        fi
  80}
  81
  82do_next () {
  83        read command sha1 rest < "$TODO"
  84        case "$command" in
  85        \#|'')
  86                mark_action_done
  87                continue
  88                ;;
  89        pick)
  90                comment_for_reflog pick
  91
  92                mark_action_done
  93                pick_one $sha1 ||
  94                        die_with_patch $sha1 "Could not apply $sha1... $rest"
  95                ;;
  96        edit)
  97                comment_for_reflog edit
  98
  99                mark_action_done
 100                pick_one $sha1 ||
 101                        die_with_patch $sha1 "Could not apply $sha1... $rest"
 102                make_patch $sha1
 103                warn
 104                warn "You can amend the commit now, with"
 105                warn
 106                warn "  git commit --amend"
 107                warn
 108                exit 0
 109                ;;
 110        squash)
 111                comment_for_reflog squash
 112
 113                test -z "$(grep -ve '^$' -e '^#' < $DONE)" &&
 114                        die "Cannot 'squash' without a previous commit"
 115
 116                mark_action_done
 117                failed=f
 118                pick_one -n $sha1 || failed=t
 119                MSG="$DOTEST"/message
 120                echo "# This is a combination of two commits." > "$MSG"
 121                echo "# The first commit's message is:" >> "$MSG"
 122                echo >> "$MSG"
 123                git cat-file commit HEAD | sed -e '1,/^$/d' >> "$MSG"
 124                echo >> "$MSG"
 125                echo "# And this is the 2nd commit message:" >> "$MSG"
 126                echo >> "$MSG"
 127                git cat-file commit $sha1 | sed -e '1,/^$/d' >> "$MSG"
 128                git reset --soft HEAD^
 129                author_script=$(get_author_ident_from_commit $sha1)
 130                case $failed in
 131                f)
 132                        # This is like --amend, but with a different message
 133                        eval "$author_script"
 134                        export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
 135                        git commit -F "$MSG" -e
 136                        ;;
 137                t)
 138                        cp "$MSG" "$GIT_DIR"/MERGE_MSG
 139                        warn
 140                        warn "Could not apply $sha1... $rest"
 141                        warn "After you fixed that, commit the result with"
 142                        warn
 143                        warn "  $(echo $author_script | tr '\012' ' ') \\"
 144                        warn "    git commit -F \"$GIT_DIR\"/MERGE_MSG -e"
 145                        die_with_patch $sha1 ""
 146                esac
 147                ;;
 148        *)
 149                warn "Unknown command: $command $sha1 $rest"
 150                die_with_patch $sha1 "Please fix this in the file $TODO."
 151        esac
 152        test -s "$TODO" && return
 153
 154        HEAD=$(git rev-parse HEAD)
 155        HEADNAME=$(cat "$DOTEST"/head-name)
 156        rm -rf "$DOTEST" &&
 157        warn "Successfully rebased and updated $HEADNAME."
 158
 159        exit
 160}
 161
 162do_rest () {
 163        while :
 164        do
 165                do_next
 166        done
 167        test -f "$DOTEST"/verbose &&
 168                git diff --stat $(cat "$DOTEST"/head)..HEAD
 169        exit
 170}
 171
 172while case $# in 0) break ;; esac
 173do
 174        case "$1" in
 175        --continue)
 176                comment_for_reflog continue
 177
 178                test -d "$DOTEST" || die "No interactive rebase running"
 179
 180                require_clean_work_tree
 181                do_rest
 182                ;;
 183        --abort)
 184                comment_for_reflog abort
 185
 186                test -d "$DOTEST" || die "No interactive rebase running"
 187
 188                HEADNAME=$(cat "$DOTEST"/head-name)
 189                HEAD=$(cat "$DOTEST"/head)
 190                git symbolic-ref HEAD $HEADNAME &&
 191                git reset --hard $HEAD &&
 192                rm -rf "$DOTEST"
 193                exit
 194                ;;
 195        --skip)
 196                comment_for_reflog skip
 197
 198                test -d "$DOTEST" || die "No interactive rebase running"
 199
 200                git reset --hard && do_rest
 201                ;;
 202        -s|--strategy)
 203                shift
 204                case "$#,$1" in
 205                *,*=*)
 206                        STRATEGY="-s `expr "z$1" : 'z-[^=]*=\(.*\)'`" ;;
 207                1,*)
 208                        usage ;;
 209                *)
 210                        STRATEGY="-s $2"
 211                        shift ;;
 212                esac
 213                ;;
 214        --merge)
 215                # we use merge anyway
 216                ;;
 217        -C*)
 218                die "Interactive rebase uses merge, so $1 does not make sense"
 219                ;;
 220        -v|--verbose)
 221                VERBOSE=t
 222                ;;
 223        -i|--interactive)
 224                # yeah, we know
 225                ;;
 226        ''|-h)
 227                usage
 228                ;;
 229        *)
 230                test -d "$DOTEST" &&
 231                        die "Interactive rebase already started"
 232
 233                git var GIT_COMMITTER_IDENT >/dev/null ||
 234                        die "You need to set your committer info first"
 235
 236                comment_for_reflog start
 237
 238                ONTO=
 239                case "$1" in
 240                --onto)
 241                        ONTO=$(git rev-parse --verify "$2") ||
 242                                die "Does not point to a valid commit: $2"
 243                        shift; shift
 244                        ;;
 245                esac
 246
 247                require_clean_work_tree
 248
 249                if [ ! -z "$2"]
 250                then
 251                        git show-ref --verify --quiet "refs/heads/$2" ||
 252                                die "Invalid branchname: $2"
 253                        git checkout "$2" ||
 254                                die "Could not checkout $2"
 255                fi
 256
 257                HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?"
 258                UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base"
 259
 260                test -z "$ONTO" && ONTO=$UPSTREAM
 261
 262                mkdir "$DOTEST" || die "Could not create temporary $DOTEST"
 263                : > "$DOTEST"/interactive || die "Could not mark as interactive"
 264                git symbolic-ref HEAD > "$DOTEST"/head-name ||
 265                        die "Could not get HEAD"
 266
 267                echo $HEAD > "$DOTEST"/head
 268                echo $UPSTREAM > "$DOTEST"/upstream
 269                echo $ONTO > "$DOTEST"/onto
 270                test t = "$VERBOSE" && : > "$DOTEST"/verbose
 271
 272                SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
 273                SHORTHEAD=$(git rev-parse --short $HEAD)
 274                SHORTONTO=$(git rev-parse --short $ONTO)
 275                cat > "$TODO" << EOF
 276# Rebasing $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO
 277#
 278# Commands:
 279#  pick = use commit
 280#  edit = use commit, but stop for amending
 281#  squash = use commit, but meld into previous commit
 282EOF
 283                git rev-list --no-merges --pretty=oneline --abbrev-commit \
 284                        --abbrev=7 --reverse $UPSTREAM..$HEAD | \
 285                        sed "s/^/pick /" >> "$TODO"
 286
 287                test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
 288                        die_abort "Nothing to do"
 289
 290                cp "$TODO" "$TODO".backup
 291                ${VISUAL:-${EDITOR:-vi}} "$TODO" ||
 292                        die "Could not execute editor"
 293
 294                test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
 295                        die_abort "Nothing to do"
 296
 297                git checkout $ONTO && do_rest
 298        esac
 299        shift
 300done