X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=git-rebase--interactive.sh;h=6d140928890fccc0b2ac8fd640a236ffbad21793;hb=5410a02ab9e6a1987147724f8ea65e6a077b3832;hp=f3950767ea6256cd9c2dceabbe7c91fa62e3f3aa;hpb=793ad04198a79c14094bd1311a9c42f1293c6183;p=git.git diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index f3950767e..6d1409288 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -13,12 +13,15 @@ USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose] [--onto ] [])' +OPTIONS_SPEC= . git-sh-setup require_work_tree DOTEST="$GIT_DIR/.dotest-merge" -TODO="$DOTEST"/todo +TODO="$DOTEST"/git-rebase-todo DONE="$DOTEST"/done +MSG="$DOTEST"/message +SQUASH_MSG="$DOTEST"/message-squash REWRITTEN="$DOTEST"/rewritten PRESERVE_MERGES= STRATEGY= @@ -31,6 +34,20 @@ warn () { echo "$*" >&2 } +output () { + case "$VERBOSE" in + '') + output=$("$@" 2>&1 ) + status=$? + test $status != 0 && printf "%s\n" "$output" + return $status + ;; + *) + "$@" + ;; + esac +} + require_clean_work_tree () { # test if working tree is dirty git rev-parse --verify HEAD > /dev/null && @@ -47,6 +64,7 @@ comment_for_reflog () { ''|rebase*) GIT_REFLOG_ACTION="rebase -i ($1)" export GIT_REFLOG_ACTION + ;; esac } @@ -54,18 +72,23 @@ mark_action_done () { sed -e 1q < "$TODO" >> "$DONE" sed -e 1d < "$TODO" >> "$TODO".new mv -f "$TODO".new "$TODO" + count=$(($(grep -ve '^$' -e '^#' < "$DONE" | wc -l))) + total=$(($count+$(grep -ve '^$' -e '^#' < "$TODO" | wc -l))) + printf "Rebasing (%d/%d)\r" $count $total + test -z "$VERBOSE" || echo } make_patch () { - parent_sha1=$(git rev-parse --verify "$1"^ 2> /dev/null) - git diff "$parent_sha1".."$1" > "$DOTEST"/patch + parent_sha1=$(git rev-parse --verify "$1"^) || + die "Cannot get patch for $1^" + git diff-tree -p "$parent_sha1".."$1" > "$DOTEST"/patch + test -f "$DOTEST"/message || + git cat-file commit "$1" | sed "1,/^$/d" > "$DOTEST"/message + test -f "$DOTEST"/author-script || + get_author_ident_from_commit "$1" > "$DOTEST"/author-script } die_with_patch () { - test -f "$DOTEST"/message || - git cat-file commit $sha1 | sed "1,/^$/d" > "$DOTEST"/message - test -f "$DOTEST"/author-script || - get_author_ident_from_commit $sha1 > "$DOTEST"/author-script make_patch "$1" die "$2" } @@ -75,20 +98,26 @@ die_abort () { die "$1" } +has_action () { + grep -vqe '^$' -e '^#' "$1" +} + pick_one () { - case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac - git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" + no_ff= + case "$1" in -n) sha1=$2; no_ff=t ;; *) sha1=$1 ;; esac + output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" test -d "$REWRITTEN" && pick_one_preserving_merges "$@" && return - parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null) + parent_sha1=$(git rev-parse --verify $sha1^) || + die "Could not get the parent of $sha1" current_sha1=$(git rev-parse --verify HEAD) - if [ $current_sha1 = $parent_sha1 ]; then - git reset --hard $sha1 - test "a$1" = a-n && git reset --soft $current_sha1 + if test "$no_ff$current_sha1" = "$parent_sha1"; then + output git reset --hard $sha1 + test "a$1" = a-n && output git reset --soft $current_sha1 sha1=$(git rev-parse --short $sha1) - warn Fast forward to $sha1 + output warn Fast forward to $sha1 else - git cherry-pick $STRATEGY "$@" + output git cherry-pick "$@" fi } @@ -96,7 +125,7 @@ pick_one_preserving_merges () { case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac sha1=$(git rev-parse $sha1) - if [ -f "$DOTEST"/current-commit ] + if test -f "$DOTEST"/current-commit then current_commit=$(cat "$DOTEST"/current-commit) && git rev-parse HEAD > "$REWRITTEN"/$current_commit && @@ -108,9 +137,9 @@ pick_one_preserving_merges () { fast_forward=t preserve=t new_parents= - for p in $(git rev-list --parents -1 $sha1 | cut -d\ -f2-) + for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -f2-) do - if [ -f "$REWRITTEN"/$p ] + if test -f "$REWRITTEN"/$p then preserve=f new_p=$(cat "$REWRITTEN"/$p) @@ -120,66 +149,107 @@ pick_one_preserving_merges () { ;; # do nothing; that parent is already there *) new_parents="$new_parents $new_p" + ;; esac fi done case $fast_forward in t) - echo "Fast forward to $sha1" - test $preserve=f && echo $sha1 > "$REWRITTEN"/$sha1 + output warn "Fast forward to $sha1" + test $preserve = f || echo $sha1 > "$REWRITTEN"/$sha1 ;; f) test "a$1" = a-n && die "Refusing to squash a merge: $sha1" - first_parent=$(expr "$new_parents" : " \([^ ]*\)") + first_parent=$(expr "$new_parents" : ' \([^ ]*\)') # detach HEAD to current parent - git checkout $first_parent 2> /dev/null || + output git checkout $first_parent 2> /dev/null || die "Cannot move HEAD to $first_parent" echo $sha1 > "$DOTEST"/current-commit case "$new_parents" in - \ *\ *) + ' '*' '*) # redo merge author_script=$(get_author_ident_from_commit $sha1) eval "$author_script" - msg="$(git cat-file commit $sha1 | \ - sed -e '1,/^$/d' -e "s/[\"\\]/\\\\&/g")" + msg="$(git cat-file commit $sha1 | sed -e '1,/^$/d')" + # No point in merging the first parent, that's HEAD + new_parents=${new_parents# $first_parent} # NEEDSWORK: give rerere a chance - if ! git merge $STRATEGY -m "$msg" $new_parents + if ! GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \ + GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \ + GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \ + output git merge $STRATEGY -m "$msg" \ + $new_parents then - echo "$msg" > "$GIT_DIR"/MERGE_MSG + printf "%s\n" "$msg" > "$GIT_DIR"/MERGE_MSG die Error redoing merge $sha1 fi ;; *) - git cherry-pick $STRATEGY "$@" || + output git cherry-pick "$@" || die_with_patch $sha1 "Could not pick $sha1" + ;; esac + ;; + esac +} + +nth_string () { + case "$1" in + *1[0-9]|*[04-9]) echo "$1"th;; + *1) echo "$1"st;; + *2) echo "$1"nd;; + *3) echo "$1"rd;; esac } +make_squash_message () { + if test -f "$SQUASH_MSG"; then + COUNT=$(($(sed -n "s/^# This is [^0-9]*\([1-9][0-9]*\).*/\1/p" \ + < "$SQUASH_MSG" | tail -n 1)+1)) + echo "# This is a combination of $COUNT commits." + sed -n "2,\$p" < "$SQUASH_MSG" + else + COUNT=2 + echo "# This is a combination of two commits." + echo "# The first commit's message is:" + echo + git cat-file commit HEAD | sed -e '1,/^$/d' + echo + fi + echo "# This is the $(nth_string $COUNT) commit message:" + echo + git cat-file commit $1 | sed -e '1,/^$/d' +} + +peek_next_command () { + sed -n "1s/ .*$//p" < "$TODO" +} + do_next () { - test -f "$DOTEST"/message && rm "$DOTEST"/message - test -f "$DOTEST"/author-script && rm "$DOTEST"/author-script + rm -f "$DOTEST"/message "$DOTEST"/author-script \ + "$DOTEST"/amend || exit read command sha1 rest < "$TODO" case "$command" in - \#|'') + '#'*|'') mark_action_done ;; - pick) + pick|p) comment_for_reflog pick mark_action_done pick_one $sha1 || die_with_patch $sha1 "Could not apply $sha1... $rest" ;; - edit) + edit|e) comment_for_reflog edit mark_action_done pick_one $sha1 || die_with_patch $sha1 "Could not apply $sha1... $rest" make_patch $sha1 + : > "$DOTEST"/amend warn warn "You can amend the commit now, with" warn @@ -187,44 +257,53 @@ do_next () { warn exit 0 ;; - squash) + squash|s) comment_for_reflog squash - test -z "$(grep -ve '^$' -e '^#' < $DONE)" && + has_action "$DONE" || die "Cannot 'squash' without a previous commit" mark_action_done - MSG="$DOTEST"/message - echo "# This is a combination of two commits." > "$MSG" - echo "# The first commit's message is:" >> "$MSG" - echo >> "$MSG" - git cat-file commit HEAD | sed -e '1,/^$/d' >> "$MSG" - echo >> "$MSG" + make_squash_message $sha1 > "$MSG" + case "$(peek_next_command)" in + squash|s) + EDIT_COMMIT= + USE_OUTPUT=output + cp "$MSG" "$SQUASH_MSG" + ;; + *) + EDIT_COMMIT=-e + USE_OUTPUT= + rm -f "$SQUASH_MSG" || exit + ;; + esac + failed=f + author_script=$(get_author_ident_from_commit HEAD) + output git reset --soft HEAD^ pick_one -n $sha1 || failed=t - echo "# And this is the 2nd commit message:" >> "$MSG" - echo >> "$MSG" - git cat-file commit $sha1 | sed -e '1,/^$/d' >> "$MSG" - git reset --soft HEAD^ - author_script=$(get_author_ident_from_commit $sha1) echo "$author_script" > "$DOTEST"/author-script case $failed in f) # This is like --amend, but with a different message eval "$author_script" - export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE - git commit -F "$MSG" -e + GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \ + GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \ + GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \ + $USE_OUTPUT git commit -F "$MSG" $EDIT_COMMIT ;; t) cp "$MSG" "$GIT_DIR"/MERGE_MSG warn warn "Could not apply $sha1... $rest" die_with_patch $sha1 "" + ;; esac ;; *) warn "Unknown command: $command $sha1 $rest" die_with_patch $sha1 "Please fix this in the file $TODO." + ;; esac test -s "$TODO" && return @@ -232,7 +311,7 @@ do_next () { HEADNAME=$(cat "$DOTEST"/head-name) && OLDHEAD=$(cat "$DOTEST"/head) && SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) && - if [ -d "$REWRITTEN" ] + if test -d "$REWRITTEN" then test -f "$DOTEST"/current-commit && current_commit=$(cat "$DOTEST"/current-commit) && @@ -241,13 +320,18 @@ do_next () { else NEWHEAD=$(git rev-parse HEAD) fi && - message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" && - git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD && - git symbolic-ref HEAD $HEADNAME && { + case $HEADNAME in + refs/*) + message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" && + git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD && + git symbolic-ref HEAD $HEADNAME + ;; + esac && { test ! -f "$DOTEST"/verbose || - git diff --stat $(cat "$DOTEST"/head)..HEAD + git diff-tree --stat $(cat "$DOTEST"/head)..HEAD } && rm -rf "$DOTEST" && + git gc --auto && warn "Successfully rebased and updated $HEADNAME." exit @@ -260,7 +344,7 @@ do_rest () { done } -while case $# in 0) break ;; esac +while test $# != 0 do case "$1" in --continue) @@ -273,7 +357,9 @@ do git update-index --refresh && git diff-files --quiet && ! git diff-index --cached --quiet HEAD && - . "$DOTEST"/author-script && + . "$DOTEST"/author-script && { + test ! -f "$DOTEST"/amend || git reset --soft HEAD^ + } && export GIT_AUTHOR_NAME GIT_AUTHOR_NAME GIT_AUTHOR_DATE && git commit -F "$DOTEST"/message -e @@ -287,8 +373,12 @@ do HEADNAME=$(cat "$DOTEST"/head-name) HEAD=$(cat "$DOTEST"/head) - git symbolic-ref HEAD $HEADNAME && - git reset --hard $HEAD && + case $HEADNAME in + refs/*) + git symbolic-ref HEAD $HEADNAME + ;; + esac && + output git reset --hard $HEAD && rm -rf "$DOTEST" exit ;; @@ -297,10 +387,9 @@ do test -d "$DOTEST" || die "No interactive rebase running" - git reset --hard && do_rest + output git reset --hard && do_rest ;; -s|--strategy) - shift case "$#,$1" in *,*=*) STRATEGY="-s `expr "z$1" : 'z-[^=]*=\(.*\)'`" ;; @@ -349,30 +438,31 @@ do require_clean_work_tree - if [ ! -z "$2"] + if test ! -z "$2" then - git show-ref --verify --quiet "refs/heads/$2" || + output git show-ref --verify --quiet "refs/heads/$2" || die "Invalid branchname: $2" - git checkout "$2" || + output git checkout "$2" || die "Could not checkout $2" fi HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?" UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base" + mkdir "$DOTEST" || die "Could not create temporary $DOTEST" + test -z "$ONTO" && ONTO=$UPSTREAM - mkdir "$DOTEST" || die "Could not create temporary $DOTEST" : > "$DOTEST"/interactive || die "Could not mark as interactive" - git symbolic-ref HEAD > "$DOTEST"/head-name || - die "Could not get HEAD" + git symbolic-ref HEAD > "$DOTEST"/head-name 2> /dev/null || + echo "detached HEAD" > "$DOTEST"/head-name echo $HEAD > "$DOTEST"/head echo $UPSTREAM > "$DOTEST"/upstream echo $ONTO > "$DOTEST"/onto test -z "$STRATEGY" || echo "$STRATEGY" > "$DOTEST"/strategy test t = "$VERBOSE" && : > "$DOTEST"/verbose - if [ t = "$PRESERVE_MERGES" ] + if test t = "$PRESERVE_MERGES" then # $REWRITTEN contains files for each commit that is # reachable by at least one merge base of $HEAD and @@ -407,20 +497,22 @@ do # EOF git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \ - --abbrev=7 --reverse $UPSTREAM..$HEAD | \ - sed "s/^/pick /" >> "$TODO" + --abbrev=7 --reverse --left-right --cherry-pick \ + $UPSTREAM...$HEAD | \ + sed -n "s/^>/pick /p" >> "$TODO" - test -z "$(grep -ve '^$' -e '^#' < $TODO)" && + has_action "$TODO" || die_abort "Nothing to do" cp "$TODO" "$TODO".backup - ${VISUAL:-${EDITOR:-vi}} "$TODO" || + git_editor "$TODO" || die "Could not execute editor" - test -z "$(grep -ve '^$' -e '^#' < $TODO)" && + has_action "$TODO" || die_abort "Nothing to do" - git checkout $ONTO && do_rest + output git checkout $ONTO && do_rest + ;; esac shift done