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