56e6f7f08a9003bd2ad463847fe345016808ae8d
   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 | [--preserve-merges] [--verbose]
  14        [--onto <branch>] <upstream> [<branch>])'
  15
  16. git-sh-setup
  17require_work_tree
  18
  19DOTEST="$GIT_DIR/.dotest-merge"
  20TODO="$DOTEST"/git-rebase-todo
  21DONE="$DOTEST"/done
  22MSG="$DOTEST"/message
  23SQUASH_MSG="$DOTEST"/message-squash
  24REWRITTEN="$DOTEST"/rewritten
  25PRESERVE_MERGES=
  26STRATEGY=
  27VERBOSE=
  28test -d "$REWRITTEN" && PRESERVE_MERGES=t
  29test -f "$DOTEST"/strategy && STRATEGY="$(cat "$DOTEST"/strategy)"
  30test -f "$DOTEST"/verbose && VERBOSE=t
  31
  32warn () {
  33        echo "$*" >&2
  34}
  35
  36output () {
  37        case "$VERBOSE" in
  38        '')
  39                "$@" > "$DOTEST"/output 2>&1
  40                status=$?
  41                test $status != 0 &&
  42                        cat "$DOTEST"/output
  43                return $status
  44        ;;
  45        *)
  46                "$@"
  47        esac
  48}
  49
  50require_clean_work_tree () {
  51        # test if working tree is dirty
  52        git rev-parse --verify HEAD > /dev/null &&
  53        git update-index --refresh &&
  54        git diff-files --quiet &&
  55        git diff-index --cached --quiet HEAD ||
  56        die "Working tree is dirty"
  57}
  58
  59ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION"
  60
  61comment_for_reflog () {
  62        case "$ORIG_REFLOG_ACTION" in
  63        ''|rebase*)
  64                GIT_REFLOG_ACTION="rebase -i ($1)"
  65                export GIT_REFLOG_ACTION
  66        esac
  67}
  68
  69mark_action_done () {
  70        sed -e 1q < "$TODO" >> "$DONE"
  71        sed -e 1d < "$TODO" >> "$TODO".new
  72        mv -f "$TODO".new "$TODO"
  73        count=$(($(wc -l < "$DONE")))
  74        total=$(($count+$(wc -l < "$TODO")))
  75        printf "Rebasing (%d/%d)\r" $count $total
  76        test -z "$VERBOSE" || echo
  77}
  78
  79make_patch () {
  80        parent_sha1=$(git rev-parse --verify "$1"^) ||
  81                die "Cannot get patch for $1^"
  82        git diff "$parent_sha1".."$1" > "$DOTEST"/patch
  83        test -f "$DOTEST"/message ||
  84                git cat-file commit "$1" | sed "1,/^$/d" > "$DOTEST"/message
  85        test -f "$DOTEST"/author-script ||
  86                get_author_ident_from_commit "$1" > "$DOTEST"/author-script
  87}
  88
  89die_with_patch () {
  90        make_patch "$1"
  91        die "$2"
  92}
  93
  94die_abort () {
  95        rm -rf "$DOTEST"
  96        die "$1"
  97}
  98
  99pick_one () {
 100        no_ff=
 101        case "$1" in -n) sha1=$2; no_ff=t ;; *) sha1=$1 ;; esac
 102        output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
 103        test -d "$REWRITTEN" &&
 104                pick_one_preserving_merges "$@" && return
 105        parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null)
 106        current_sha1=$(git rev-parse --verify HEAD)
 107        if test $no_ff$current_sha1 = $parent_sha1; then
 108                output git reset --hard $sha1
 109                test "a$1" = a-n && output git reset --soft $current_sha1
 110                sha1=$(git rev-parse --short $sha1)
 111                output warn Fast forward to $sha1
 112        else
 113                output git cherry-pick $STRATEGY "$@"
 114        fi
 115}
 116
 117pick_one_preserving_merges () {
 118        case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
 119        sha1=$(git rev-parse $sha1)
 120
 121        if test -f "$DOTEST"/current-commit
 122        then
 123                current_commit=$(cat "$DOTEST"/current-commit) &&
 124                git rev-parse HEAD > "$REWRITTEN"/$current_commit &&
 125                rm "$DOTEST"/current-commit ||
 126                die "Cannot write current commit's replacement sha1"
 127        fi
 128
 129        # rewrite parents; if none were rewritten, we can fast-forward.
 130        fast_forward=t
 131        preserve=t
 132        new_parents=
 133        for p in $(git rev-list --parents -1 $sha1 | cut -d\  -f2-)
 134        do
 135                if test -f "$REWRITTEN"/$p
 136                then
 137                        preserve=f
 138                        new_p=$(cat "$REWRITTEN"/$p)
 139                        test $p != $new_p && fast_forward=f
 140                        case "$new_parents" in
 141                        *$new_p*)
 142                                ;; # do nothing; that parent is already there
 143                        *)
 144                                new_parents="$new_parents $new_p"
 145                        esac
 146                fi
 147        done
 148        case $fast_forward in
 149        t)
 150                output warn "Fast forward to $sha1"
 151                test $preserve=f && echo $sha1 > "$REWRITTEN"/$sha1
 152                ;;
 153        f)
 154                test "a$1" = a-n && die "Refusing to squash a merge: $sha1"
 155
 156                first_parent=$(expr "$new_parents" : " \([^ ]*\)")
 157                # detach HEAD to current parent
 158                output git checkout $first_parent 2> /dev/null ||
 159                        die "Cannot move HEAD to $first_parent"
 160
 161                echo $sha1 > "$DOTEST"/current-commit
 162                case "$new_parents" in
 163                \ *\ *)
 164                        # redo merge
 165                        author_script=$(get_author_ident_from_commit $sha1)
 166                        eval "$author_script"
 167                        msg="$(git cat-file commit $sha1 | \
 168                                sed -e '1,/^$/d' -e "s/[\"\\]/\\\\&/g")"
 169                        # NEEDSWORK: give rerere a chance
 170                        if ! output git merge $STRATEGY -m "$msg" $new_parents
 171                        then
 172                                echo "$msg" > "$GIT_DIR"/MERGE_MSG
 173                                die Error redoing merge $sha1
 174                        fi
 175                        ;;
 176                *)
 177                        output git cherry-pick $STRATEGY "$@" ||
 178                                die_with_patch $sha1 "Could not pick $sha1"
 179                esac
 180        esac
 181}
 182
 183nth_string () {
 184        case "$1" in
 185        *1[0-9]|*[04-9]) echo "$1"th;;
 186        *1) echo "$1"st;;
 187        *2) echo "$1"nd;;
 188        *3) echo "$1"rd;;
 189        esac
 190}
 191
 192make_squash_message () {
 193        if test -f "$SQUASH_MSG"; then
 194                COUNT=$(($(sed -n "s/^# This is [^0-9]*\([1-9][0-9]*\).*/\1/p" \
 195                        < "$SQUASH_MSG" | tail -n 1)+1))
 196                echo "# This is a combination of $COUNT commits."
 197                sed -n "2,\$p" < "$SQUASH_MSG"
 198        else
 199                COUNT=2
 200                echo "# This is a combination of two commits."
 201                echo "# The first commit's message is:"
 202                echo
 203                git cat-file commit HEAD | sed -e '1,/^$/d'
 204                echo
 205        fi
 206        echo "# This is the $(nth_string $COUNT) commit message:"
 207        echo
 208        git cat-file commit $1 | sed -e '1,/^$/d'
 209}
 210
 211peek_next_command () {
 212        sed -n "1s/ .*$//p" < "$TODO"
 213}
 214
 215do_next () {
 216        test -f "$DOTEST"/message && rm "$DOTEST"/message
 217        test -f "$DOTEST"/author-script && rm "$DOTEST"/author-script
 218        test -f "$DOTEST"/amend && rm "$DOTEST"/amend
 219        read command sha1 rest < "$TODO"
 220        case "$command" in
 221        \#|'')
 222                mark_action_done
 223                ;;
 224        pick)
 225                comment_for_reflog pick
 226
 227                mark_action_done
 228                pick_one $sha1 ||
 229                        die_with_patch $sha1 "Could not apply $sha1... $rest"
 230                ;;
 231        edit)
 232                comment_for_reflog edit
 233
 234                mark_action_done
 235                pick_one $sha1 ||
 236                        die_with_patch $sha1 "Could not apply $sha1... $rest"
 237                make_patch $sha1
 238                : > "$DOTEST"/amend
 239                warn
 240                warn "You can amend the commit now, with"
 241                warn
 242                warn "  git commit --amend"
 243                warn
 244                exit 0
 245                ;;
 246        squash)
 247                comment_for_reflog squash
 248
 249                test -z "$(grep -ve '^$' -e '^#' < $DONE)" &&
 250                        die "Cannot 'squash' without a previous commit"
 251
 252                mark_action_done
 253                make_squash_message $sha1 > "$MSG"
 254                case "$(peek_next_command)" in
 255                squash)
 256                        EDIT_COMMIT=
 257                        USE_OUTPUT=output
 258                        cp "$MSG" "$SQUASH_MSG"
 259                ;;
 260                *)
 261                        EDIT_COMMIT=-e
 262                        USE_OUTPUT=
 263                        test -f "$SQUASH_MSG" && rm "$SQUASH_MSG"
 264                esac
 265
 266                failed=f
 267                output git reset --soft HEAD^
 268                pick_one -n $sha1 || failed=t
 269                author_script=$(get_author_ident_from_commit $sha1)
 270                echo "$author_script" > "$DOTEST"/author-script
 271                case $failed in
 272                f)
 273                        # This is like --amend, but with a different message
 274                        eval "$author_script"
 275                        export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
 276                        $USE_OUTPUT git commit -F "$MSG" $EDIT_COMMIT
 277                        ;;
 278                t)
 279                        cp "$MSG" "$GIT_DIR"/MERGE_MSG
 280                        warn
 281                        warn "Could not apply $sha1... $rest"
 282                        die_with_patch $sha1 ""
 283                esac
 284                ;;
 285        *)
 286                warn "Unknown command: $command $sha1 $rest"
 287                die_with_patch $sha1 "Please fix this in the file $TODO."
 288        esac
 289        test -s "$TODO" && return
 290
 291        comment_for_reflog finish &&
 292        HEADNAME=$(cat "$DOTEST"/head-name) &&
 293        OLDHEAD=$(cat "$DOTEST"/head) &&
 294        SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) &&
 295        if test -d "$REWRITTEN"
 296        then
 297                test -f "$DOTEST"/current-commit &&
 298                        current_commit=$(cat "$DOTEST"/current-commit) &&
 299                        git rev-parse HEAD > "$REWRITTEN"/$current_commit
 300                NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD)
 301        else
 302                NEWHEAD=$(git rev-parse HEAD)
 303        fi &&
 304        message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" &&
 305        git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD &&
 306        git symbolic-ref HEAD $HEADNAME && {
 307                test ! -f "$DOTEST"/verbose ||
 308                        git diff --stat $(cat "$DOTEST"/head)..HEAD
 309        } &&
 310        rm -rf "$DOTEST" &&
 311        warn "Successfully rebased and updated $HEADNAME."
 312
 313        exit
 314}
 315
 316do_rest () {
 317        while :
 318        do
 319                do_next
 320        done
 321}
 322
 323while test $# != 0
 324do
 325        case "$1" in
 326        --continue)
 327                comment_for_reflog continue
 328
 329                test -d "$DOTEST" || die "No interactive rebase running"
 330
 331                # commit if necessary
 332                git rev-parse --verify HEAD > /dev/null &&
 333                git update-index --refresh &&
 334                git diff-files --quiet &&
 335                ! git diff-index --cached --quiet HEAD &&
 336                . "$DOTEST"/author-script && {
 337                        test ! -f "$DOTEST"/amend || git reset --soft HEAD^
 338                } &&
 339                export GIT_AUTHOR_NAME GIT_AUTHOR_NAME GIT_AUTHOR_DATE &&
 340                git commit -F "$DOTEST"/message -e
 341
 342                require_clean_work_tree
 343                do_rest
 344                ;;
 345        --abort)
 346                comment_for_reflog abort
 347
 348                test -d "$DOTEST" || die "No interactive rebase running"
 349
 350                HEADNAME=$(cat "$DOTEST"/head-name)
 351                HEAD=$(cat "$DOTEST"/head)
 352                git symbolic-ref HEAD $HEADNAME &&
 353                output git reset --hard $HEAD &&
 354                rm -rf "$DOTEST"
 355                exit
 356                ;;
 357        --skip)
 358                comment_for_reflog skip
 359
 360                test -d "$DOTEST" || die "No interactive rebase running"
 361
 362                output git reset --hard && do_rest
 363                ;;
 364        -s|--strategy)
 365                shift
 366                case "$#,$1" in
 367                *,*=*)
 368                        STRATEGY="-s `expr "z$1" : 'z-[^=]*=\(.*\)'`" ;;
 369                1,*)
 370                        usage ;;
 371                *)
 372                        STRATEGY="-s $2"
 373                        shift ;;
 374                esac
 375                ;;
 376        --merge)
 377                # we use merge anyway
 378                ;;
 379        -C*)
 380                die "Interactive rebase uses merge, so $1 does not make sense"
 381                ;;
 382        -v|--verbose)
 383                VERBOSE=t
 384                ;;
 385        -p|--preserve-merges)
 386                PRESERVE_MERGES=t
 387                ;;
 388        -i|--interactive)
 389                # yeah, we know
 390                ;;
 391        ''|-h)
 392                usage
 393                ;;
 394        *)
 395                test -d "$DOTEST" &&
 396                        die "Interactive rebase already started"
 397
 398                git var GIT_COMMITTER_IDENT >/dev/null ||
 399                        die "You need to set your committer info first"
 400
 401                comment_for_reflog start
 402
 403                ONTO=
 404                case "$1" in
 405                --onto)
 406                        ONTO=$(git rev-parse --verify "$2") ||
 407                                die "Does not point to a valid commit: $2"
 408                        shift; shift
 409                        ;;
 410                esac
 411
 412                require_clean_work_tree
 413
 414                mkdir "$DOTEST" || die "Could not create temporary $DOTEST"
 415                if test ! -z "$2"
 416                then
 417                        output git show-ref --verify --quiet "refs/heads/$2" ||
 418                                die "Invalid branchname: $2"
 419                        output git checkout "$2" ||
 420                                die "Could not checkout $2"
 421                fi
 422
 423                HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?"
 424                UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base"
 425
 426                test -z "$ONTO" && ONTO=$UPSTREAM
 427
 428                : > "$DOTEST"/interactive || die "Could not mark as interactive"
 429                git symbolic-ref HEAD > "$DOTEST"/head-name ||
 430                        die "Could not get HEAD"
 431
 432                echo $HEAD > "$DOTEST"/head
 433                echo $UPSTREAM > "$DOTEST"/upstream
 434                echo $ONTO > "$DOTEST"/onto
 435                test -z "$STRATEGY" || echo "$STRATEGY" > "$DOTEST"/strategy
 436                test t = "$VERBOSE" && : > "$DOTEST"/verbose
 437                if test t = "$PRESERVE_MERGES"
 438                then
 439                        # $REWRITTEN contains files for each commit that is
 440                        # reachable by at least one merge base of $HEAD and
 441                        # $UPSTREAM. They are not necessarily rewritten, but
 442                        # their children might be.
 443                        # This ensures that commits on merged, but otherwise
 444                        # unrelated side branches are left alone. (Think "X"
 445                        # in the man page's example.)
 446                        mkdir "$REWRITTEN" &&
 447                        for c in $(git merge-base --all $HEAD $UPSTREAM)
 448                        do
 449                                echo $ONTO > "$REWRITTEN"/$c ||
 450                                        die "Could not init rewritten commits"
 451                        done
 452                        MERGES_OPTION=
 453                else
 454                        MERGES_OPTION=--no-merges
 455                fi
 456
 457                SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
 458                SHORTHEAD=$(git rev-parse --short $HEAD)
 459                SHORTONTO=$(git rev-parse --short $ONTO)
 460                cat > "$TODO" << EOF
 461# Rebasing $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO
 462#
 463# Commands:
 464#  pick = use commit
 465#  edit = use commit, but stop for amending
 466#  squash = use commit, but meld into previous commit
 467#
 468# If you remove a line here THAT COMMIT WILL BE LOST.
 469#
 470EOF
 471                git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
 472                        --abbrev=7 --reverse --left-right --cherry-pick \
 473                        $UPSTREAM...$HEAD | \
 474                        sed -n "s/^>/pick /p" >> "$TODO"
 475
 476                test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
 477                        die_abort "Nothing to do"
 478
 479                cp "$TODO" "$TODO".backup
 480                git_editor "$TODO" ||
 481                        die "Could not execute editor"
 482
 483                test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
 484                        die_abort "Nothing to do"
 485
 486                output git checkout $ONTO && do_rest
 487        esac
 488        shift
 489done