git-commit-vandalism/git-rebase--interactive.sh
Johannes Schindelin 1d25c8cf82 rebase -i: fix squashing corner case
When squashing, rebase -i did not prevent fast forwards.  This could
happen when picking some other commit than the first one, and then
squashing the first commit.  So do not allow fast forwards when
squashing.

Noticed by Johannes Sixt.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2007-08-23 02:34:19 -07:00

485 lines
11 KiB
Bash
Executable File

#!/bin/sh
#
# Copyright (c) 2006 Johannes E. Schindelin
# SHORT DESCRIPTION
#
# This script makes it easy to fix up commits in the middle of a series,
# and rearrange commits.
#
# The original idea comes from Eric W. Biederman, in
# http://article.gmane.org/gmane.comp.version-control.git/22407
USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose]
[--onto <branch>] <upstream> [<branch>])'
. git-sh-setup
require_work_tree
DOTEST="$GIT_DIR/.dotest-merge"
TODO="$DOTEST"/git-rebase-todo
DONE="$DOTEST"/done
MSG="$DOTEST"/message
SQUASH_MSG="$DOTEST"/message-squash
REWRITTEN="$DOTEST"/rewritten
PRESERVE_MERGES=
STRATEGY=
VERBOSE=
test -d "$REWRITTEN" && PRESERVE_MERGES=t
test -f "$DOTEST"/strategy && STRATEGY="$(cat "$DOTEST"/strategy)"
test -f "$DOTEST"/verbose && VERBOSE=t
warn () {
echo "$*" >&2
}
output () {
case "$VERBOSE" in
'')
"$@" > "$DOTEST"/output 2>&1
status=$?
test $status != 0 &&
cat "$DOTEST"/output
return $status
;;
*)
"$@"
esac
}
require_clean_work_tree () {
# test if working tree is dirty
git rev-parse --verify HEAD > /dev/null &&
git update-index --refresh &&
git diff-files --quiet &&
git diff-index --cached --quiet HEAD ||
die "Working tree is dirty"
}
ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION"
comment_for_reflog () {
case "$ORIG_REFLOG_ACTION" in
''|rebase*)
GIT_REFLOG_ACTION="rebase -i ($1)"
export GIT_REFLOG_ACTION
esac
}
mark_action_done () {
sed -e 1q < "$TODO" >> "$DONE"
sed -e 1d < "$TODO" >> "$TODO".new
mv -f "$TODO".new "$TODO"
count=$(($(wc -l < "$DONE")))
total=$(($count+$(wc -l < "$TODO")))
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
}
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"
}
die_abort () {
rm -rf "$DOTEST"
die "$1"
}
pick_one () {
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)
current_sha1=$(git rev-parse --verify HEAD)
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)
output warn Fast forward to $sha1
else
output git cherry-pick $STRATEGY "$@"
fi
}
pick_one_preserving_merges () {
case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
sha1=$(git rev-parse $sha1)
if test -f "$DOTEST"/current-commit
then
current_commit=$(cat "$DOTEST"/current-commit) &&
git rev-parse HEAD > "$REWRITTEN"/$current_commit &&
rm "$DOTEST"/current-commit ||
die "Cannot write current commit's replacement sha1"
fi
# rewrite parents; if none were rewritten, we can fast-forward.
fast_forward=t
preserve=t
new_parents=
for p in $(git rev-list --parents -1 $sha1 | cut -d\ -f2-)
do
if test -f "$REWRITTEN"/$p
then
preserve=f
new_p=$(cat "$REWRITTEN"/$p)
test $p != $new_p && fast_forward=f
case "$new_parents" in
*$new_p*)
;; # do nothing; that parent is already there
*)
new_parents="$new_parents $new_p"
esac
fi
done
case $fast_forward in
t)
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" : " \([^ ]*\)")
# detach HEAD to current parent
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")"
# NEEDSWORK: give rerere a chance
if ! output git merge $STRATEGY -m "$msg" $new_parents
then
echo "$msg" > "$GIT_DIR"/MERGE_MSG
die Error redoing merge $sha1
fi
;;
*)
output git cherry-pick $STRATEGY "$@" ||
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]*\([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
read command sha1 rest < "$TODO"
case "$command" in
\#|'')
mark_action_done
;;
pick)
comment_for_reflog pick
mark_action_done
pick_one $sha1 ||
die_with_patch $sha1 "Could not apply $sha1... $rest"
;;
edit)
comment_for_reflog edit
mark_action_done
pick_one $sha1 ||
die_with_patch $sha1 "Could not apply $sha1... $rest"
make_patch $sha1
warn
warn "You can amend the commit now, with"
warn
warn " git commit --amend"
warn
exit 0
;;
squash)
comment_for_reflog squash
test -z "$(grep -ve '^$' -e '^#' < $DONE)" &&
die "Cannot 'squash' without a previous commit"
mark_action_done
make_squash_message $sha1 > "$MSG"
case "$(peek_next_command)" in
squash)
EDIT_COMMIT=
USE_OUTPUT=output
cp "$MSG" "$SQUASH_MSG"
;;
*)
EDIT_COMMIT=-e
USE_OUTPUT=
test -f "$SQUASH_MSG" && rm "$SQUASH_MSG"
esac
failed=f
output git reset --soft HEAD^
pick_one -n $sha1 || failed=t
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
$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
comment_for_reflog finish &&
HEADNAME=$(cat "$DOTEST"/head-name) &&
OLDHEAD=$(cat "$DOTEST"/head) &&
SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) &&
if test -d "$REWRITTEN"
then
test -f "$DOTEST"/current-commit &&
current_commit=$(cat "$DOTEST"/current-commit) &&
git rev-parse HEAD > "$REWRITTEN"/$current_commit
NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD)
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 && {
test ! -f "$DOTEST"/verbose ||
git diff --stat $(cat "$DOTEST"/head)..HEAD
} &&
rm -rf "$DOTEST" &&
warn "Successfully rebased and updated $HEADNAME."
exit
}
do_rest () {
while :
do
do_next
done
}
while case $# in 0) break ;; esac
do
case "$1" in
--continue)
comment_for_reflog continue
test -d "$DOTEST" || die "No interactive rebase running"
# commit if necessary
git rev-parse --verify HEAD > /dev/null &&
git update-index --refresh &&
git diff-files --quiet &&
! git diff-index --cached --quiet HEAD &&
. "$DOTEST"/author-script &&
export GIT_AUTHOR_NAME GIT_AUTHOR_NAME GIT_AUTHOR_DATE &&
git commit -F "$DOTEST"/message -e
require_clean_work_tree
do_rest
;;
--abort)
comment_for_reflog abort
test -d "$DOTEST" || die "No interactive rebase running"
HEADNAME=$(cat "$DOTEST"/head-name)
HEAD=$(cat "$DOTEST"/head)
git symbolic-ref HEAD $HEADNAME &&
output git reset --hard $HEAD &&
rm -rf "$DOTEST"
exit
;;
--skip)
comment_for_reflog skip
test -d "$DOTEST" || die "No interactive rebase running"
output git reset --hard && do_rest
;;
-s|--strategy)
shift
case "$#,$1" in
*,*=*)
STRATEGY="-s `expr "z$1" : 'z-[^=]*=\(.*\)'`" ;;
1,*)
usage ;;
*)
STRATEGY="-s $2"
shift ;;
esac
;;
--merge)
# we use merge anyway
;;
-C*)
die "Interactive rebase uses merge, so $1 does not make sense"
;;
-v|--verbose)
VERBOSE=t
;;
-p|--preserve-merges)
PRESERVE_MERGES=t
;;
-i|--interactive)
# yeah, we know
;;
''|-h)
usage
;;
*)
test -d "$DOTEST" &&
die "Interactive rebase already started"
git var GIT_COMMITTER_IDENT >/dev/null ||
die "You need to set your committer info first"
comment_for_reflog start
ONTO=
case "$1" in
--onto)
ONTO=$(git rev-parse --verify "$2") ||
die "Does not point to a valid commit: $2"
shift; shift
;;
esac
require_clean_work_tree
mkdir "$DOTEST" || die "Could not create temporary $DOTEST"
if test ! -z "$2"
then
output git show-ref --verify --quiet "refs/heads/$2" ||
die "Invalid branchname: $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"
test -z "$ONTO" && ONTO=$UPSTREAM
: > "$DOTEST"/interactive || die "Could not mark as interactive"
git symbolic-ref HEAD > "$DOTEST"/head-name ||
die "Could not get HEAD"
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 test t = "$PRESERVE_MERGES"
then
# $REWRITTEN contains files for each commit that is
# reachable by at least one merge base of $HEAD and
# $UPSTREAM. They are not necessarily rewritten, but
# their children might be.
# This ensures that commits on merged, but otherwise
# unrelated side branches are left alone. (Think "X"
# in the man page's example.)
mkdir "$REWRITTEN" &&
for c in $(git merge-base --all $HEAD $UPSTREAM)
do
echo $ONTO > "$REWRITTEN"/$c ||
die "Could not init rewritten commits"
done
MERGES_OPTION=
else
MERGES_OPTION=--no-merges
fi
SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
SHORTHEAD=$(git rev-parse --short $HEAD)
SHORTONTO=$(git rev-parse --short $ONTO)
cat > "$TODO" << EOF
# Rebasing $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO
#
# Commands:
# pick = use commit
# edit = use commit, but stop for amending
# squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
EOF
git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
--abbrev=7 --reverse --left-right --cherry-pick \
$UPSTREAM...$HEAD | \
sed -n "s/^>/pick /p" >> "$TODO"
test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
die_abort "Nothing to do"
cp "$TODO" "$TODO".backup
git_editor "$TODO" ||
die "Could not execute editor"
test -z "$(grep -ve '^$' -e '^#' < $TODO)" &&
die_abort "Nothing to do"
output git checkout $ONTO && do_rest
esac
shift
done