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