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