git-rebase--interactive.shon commit Teach rebase an interactive mode (1b1dce4)
   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
  63pick_one () {
  64        case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
  65        git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
  66        parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null)
  67        current_sha1=$(git rev-parse --verify HEAD)
  68        if [ $current_sha1 = $parent_sha1 ]; then
  69                git reset --hard $sha1
  70                sha1=$(git rev-parse --short $sha1)
  71                warn Fast forward to $sha1
  72        else
  73                git cherry-pick $STRATEGY "$@"
  74        fi
  75}
  76
  77do_next () {
  78        read command sha1 rest < "$TODO"
  79        case "$command" in
  80        \#|'')
  81                mark_action_done
  82                continue
  83                ;;
  84        pick)
  85                comment_for_reflog pick
  86
  87                mark_action_done
  88                pick_one $sha1 ||
  89                        die_with_patch $sha1 "Could not apply $sha1... $rest"
  90                ;;
  91        edit)
  92                comment_for_reflog edit
  93
  94                mark_action_done
  95                pick_one $sha1 ||
  96                        die_with_patch $sha1 "Could not apply $sha1... $rest"
  97                make_patch $sha1
  98                warn
  99                warn "You can amend the commit now, with"
 100                warn
 101                warn "  git commit --amend"
 102                warn
 103                exit 0
 104                ;;
 105        squash)
 106                comment_for_reflog squash
 107
 108                test -z "$(grep -ve '^$' -e '^#' < $DONE)" &&
 109                        die "Cannot 'squash' without a previous commit"
 110
 111                mark_action_done
 112                failed=f
 113                pick_one -n $sha1 || failed=t
 114                MSG="$DOTEST"/message
 115                echo "# This is a combination of two commits." > "$MSG"
 116                echo "# The first commit's message is:" >> "$MSG"
 117                echo >> "$MSG"
 118                git cat-file commit HEAD | sed -e '1,/^$/d' >> "$MSG"
 119                echo >> "$MSG"
 120                echo "# And this is the 2nd commit message:" >> "$MSG"
 121                echo >> "$MSG"
 122                git cat-file commit $sha1 | sed -e '1,/^$/d' >> "$MSG"
 123                git reset --soft HEAD^
 124                author_script=$(get_author_ident_from_commit $sha1)
 125                case $failed in
 126                f)
 127                        # This is like --amend, but with a different message
 128                        eval "$author_script"
 129                        export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
 130                        git commit -F "$MSG" -e
 131                        ;;
 132                t)
 133                        cp "$MSG" "$GIT_DIR"/MERGE_MSG
 134                        warn
 135                        warn "Could not apply $sha1... $rest"
 136                        warn "After you fixed that, commit the result with"
 137                        warn
 138                        warn "  $(echo $author_script | tr '\012' ' ') \\"
 139                        warn "    git commit -F \"$GIT_DIR\"/MERGE_MSG -e"
 140                        die_with_patch $sha1 ""
 141                esac
 142                ;;
 143        *)
 144                warn "Unknown command: $command $sha1 $rest"
 145                die_with_patch $sha1 "Please fix this in the file $TODO."
 146        esac
 147        test -s "$TODO" && return
 148
 149        HEAD=$(git rev-parse HEAD)
 150        HEADNAME=$(cat "$DOTEST"/head-name)
 151        rm -rf "$DOTEST" &&
 152        warn "Successfully rebased and updated $HEADNAME."
 153
 154        exit
 155}
 156
 157do_rest () {
 158        while :
 159        do
 160                do_next
 161        done
 162        test -f "$DOTEST"/verbose &&
 163                git diff --stat $(cat "$DOTEST"/head)..HEAD
 164        exit
 165}
 166
 167while case $# in 0) break ;; esac
 168do
 169        case "$1" in
 170        --continue)
 171                comment_for_reflog continue
 172
 173                test -d "$DOTEST" || die "No interactive rebase running"
 174
 175                require_clean_work_tree
 176                do_rest
 177                ;;
 178        --abort)
 179                comment_for_reflog abort
 180
 181                test -d "$DOTEST" || die "No interactive rebase running"
 182
 183                HEADNAME=$(cat "$DOTEST"/head-name)
 184                HEAD=$(cat "$DOTEST"/head)
 185                git symbolic-ref HEAD $HEADNAME &&
 186                git reset --hard $HEAD &&
 187                rm -rf "$DOTEST"
 188                exit
 189                ;;
 190        --skip)
 191                comment_for_reflog skip
 192
 193                test -d "$DOTEST" || die "No interactive rebase running"
 194
 195                git reset --hard && do_rest
 196                ;;
 197        -s|--strategy)
 198                shift
 199                case "$#,$1" in
 200                *,*=*)
 201                        STRATEGY="-s `expr "z$1" : 'z-[^=]*=\(.*\)'`" ;;
 202                1,*)
 203                        usage ;;
 204                *)
 205                        STRATEGY="-s $2"
 206                        shift ;;
 207                esac
 208                ;;
 209        --merge)
 210                # we use merge anyway
 211                ;;
 212        -C*)
 213                die "Interactive rebase uses merge, so $1 does not make sense"
 214                ;;
 215        -v)
 216                VERBOSE=t
 217                ;;
 218        -i|--interactive)
 219                # yeah, we know
 220                ;;
 221        ''|-h)
 222                usage
 223                ;;
 224        *)
 225                test -d "$DOTEST" &&
 226                        die "Interactive rebase already started"
 227
 228                git var GIT_COMMITTER_IDENT >/dev/null ||
 229                        die "You need to set your committer info first"
 230
 231                comment_for_reflog start
 232
 233                ONTO=
 234                case "$1" in
 235                --onto)
 236                        ONTO=$(git rev-parse --verify "$2") ||
 237                                die "Does not point to a valid commit: $2"
 238                        shift; shift
 239                        ;;
 240                esac
 241
 242                require_clean_work_tree
 243
 244                if [ ! -z "$2"]
 245                then
 246                        git show-ref --verify --quiet "refs/heads/$2" ||
 247                                die "Invalid branchname: $2"
 248                        git checkout "$2" ||
 249                                die "Could not checkout $2"
 250                fi
 251
 252                HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?"
 253                UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base"
 254
 255                test -z "$ONTO" && ONTO=$UPSTREAM
 256
 257                mkdir "$DOTEST" || die "Could not create temporary $DOTEST"
 258                : > "$DOTEST"/interactive || die "Could not mark as interactive"
 259                git symbolic-ref HEAD > "$DOTEST"/head-name ||
 260                        die "Could not get HEAD"
 261
 262                echo $HEAD > "$DOTEST"/head
 263                echo $UPSTREAM > "$DOTEST"/upstream
 264                echo $ONTO > "$DOTEST"/onto
 265                test t = "$VERBOSE" && : > "$DOTEST"/verbose
 266
 267                cat > "$TODO" << EOF
 268# Rebasing $UPSTREAM..$HEAD onto $ONTO
 269#
 270# Commands:
 271#  pick = use commit
 272#  edit = use commit, but stop for amending
 273#  squash = use commit, but meld into previous commit
 274EOF
 275                git rev-list --no-merges --pretty=oneline --abbrev-commit \
 276                        --abbrev=7 --reverse $UPSTREAM..$HEAD | \
 277                        sed "s/^/pick /" >> "$TODO"
 278
 279                test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
 280                        die "Nothing to do"
 281
 282                cp "$TODO" "$TODO".backup
 283                ${VISUAL:-${EDITOR:-vi}} "$TODO" ||
 284                        die "Could not execute editor"
 285
 286                git reset --hard $ONTO && do_rest
 287        esac
 288        shift
 289done