f0f2457975edcedaf9cf241efa189ae2fb081de7
   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        comment_for_reflog finish &&
 155        HEADNAME=$(cat "$DOTEST"/head-name) &&
 156        OLDHEAD=$(cat "$DOTEST"/head) &&
 157        SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) &&
 158        NEWHEAD=$(git rev-parse HEAD) &&
 159        message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" &&
 160        git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD &&
 161        git symbolic-ref HEAD $HEADNAME &&
 162        rm -rf "$DOTEST" &&
 163        warn "Successfully rebased and updated $HEADNAME."
 164
 165        exit
 166}
 167
 168do_rest () {
 169        while :
 170        do
 171                do_next
 172        done
 173        test -f "$DOTEST"/verbose &&
 174                git diff --stat $(cat "$DOTEST"/head)..HEAD
 175        exit
 176}
 177
 178while case $# in 0) break ;; esac
 179do
 180        case "$1" in
 181        --continue)
 182                comment_for_reflog continue
 183
 184                test -d "$DOTEST" || die "No interactive rebase running"
 185
 186                require_clean_work_tree
 187                do_rest
 188                ;;
 189        --abort)
 190                comment_for_reflog abort
 191
 192                test -d "$DOTEST" || die "No interactive rebase running"
 193
 194                HEADNAME=$(cat "$DOTEST"/head-name)
 195                HEAD=$(cat "$DOTEST"/head)
 196                git symbolic-ref HEAD $HEADNAME &&
 197                git reset --hard $HEAD &&
 198                rm -rf "$DOTEST"
 199                exit
 200                ;;
 201        --skip)
 202                comment_for_reflog skip
 203
 204                test -d "$DOTEST" || die "No interactive rebase running"
 205
 206                git reset --hard && do_rest
 207                ;;
 208        -s|--strategy)
 209                shift
 210                case "$#,$1" in
 211                *,*=*)
 212                        STRATEGY="-s `expr "z$1" : 'z-[^=]*=\(.*\)'`" ;;
 213                1,*)
 214                        usage ;;
 215                *)
 216                        STRATEGY="-s $2"
 217                        shift ;;
 218                esac
 219                ;;
 220        --merge)
 221                # we use merge anyway
 222                ;;
 223        -C*)
 224                die "Interactive rebase uses merge, so $1 does not make sense"
 225                ;;
 226        -v|--verbose)
 227                VERBOSE=t
 228                ;;
 229        -i|--interactive)
 230                # yeah, we know
 231                ;;
 232        ''|-h)
 233                usage
 234                ;;
 235        *)
 236                test -d "$DOTEST" &&
 237                        die "Interactive rebase already started"
 238
 239                git var GIT_COMMITTER_IDENT >/dev/null ||
 240                        die "You need to set your committer info first"
 241
 242                comment_for_reflog start
 243
 244                ONTO=
 245                case "$1" in
 246                --onto)
 247                        ONTO=$(git rev-parse --verify "$2") ||
 248                                die "Does not point to a valid commit: $2"
 249                        shift; shift
 250                        ;;
 251                esac
 252
 253                require_clean_work_tree
 254
 255                if [ ! -z "$2"]
 256                then
 257                        git show-ref --verify --quiet "refs/heads/$2" ||
 258                                die "Invalid branchname: $2"
 259                        git checkout "$2" ||
 260                                die "Could not checkout $2"
 261                fi
 262
 263                HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?"
 264                UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base"
 265
 266                test -z "$ONTO" && ONTO=$UPSTREAM
 267
 268                mkdir "$DOTEST" || die "Could not create temporary $DOTEST"
 269                : > "$DOTEST"/interactive || die "Could not mark as interactive"
 270                git symbolic-ref HEAD > "$DOTEST"/head-name ||
 271                        die "Could not get HEAD"
 272
 273                echo $HEAD > "$DOTEST"/head
 274                echo $UPSTREAM > "$DOTEST"/upstream
 275                echo $ONTO > "$DOTEST"/onto
 276                test t = "$VERBOSE" && : > "$DOTEST"/verbose
 277
 278                SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
 279                SHORTHEAD=$(git rev-parse --short $HEAD)
 280                SHORTONTO=$(git rev-parse --short $ONTO)
 281                cat > "$TODO" << EOF
 282# Rebasing $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO
 283#
 284# Commands:
 285#  pick = use commit
 286#  edit = use commit, but stop for amending
 287#  squash = use commit, but meld into previous commit
 288EOF
 289                git rev-list --no-merges --pretty=oneline --abbrev-commit \
 290                        --abbrev=7 --reverse $UPSTREAM..$HEAD | \
 291                        sed "s/^/pick /" >> "$TODO"
 292
 293                test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
 294                        die_abort "Nothing to do"
 295
 296                cp "$TODO" "$TODO".backup
 297                ${VISUAL:-${EDITOR:-vi}} "$TODO" ||
 298                        die "Could not execute editor"
 299
 300                test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
 301                        die_abort "Nothing to do"
 302
 303                git checkout $ONTO && do_rest
 304        esac
 305        shift
 306done