Browse Source

rebase -i: learn to rebase root commit

Teach git-rebase -i a new option --root, which instructs it to rebase
the entire history leading up to <branch>.  This is mainly for
symmetry with ordinary git-rebase; it cannot be used to edit the root
commit in-place (it requires --onto <newbase>).  Commits that already
exist in <newbase> are skipped.

In the normal mode of operation, this is fairly straightforward.  We
run cherry-pick in a loop, and cherry-pick has supported picking the
root commit since f95ebf7 (Allow cherry-picking root commits,
2008-07-04).

In --preserve-merges mode, we track the mapping from old to rewritten
commits and use it to update the parent list of each commit.  In this
case, we define 'rebase -i -p --root --onto $onto $branch' to rewrite
the parent list of all root commit(s) on $branch to contain $onto
instead.

Signed-off-by: Thomas Rast <trast@student.ethz.ch>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
maint
Thomas Rast 16 years ago committed by Junio C Hamano
parent
commit
d911d1465d
  1. 109
      git-rebase--interactive.sh
  2. 101
      t/t3412-rebase-root.sh

109
git-rebase--interactive.sh

@ -27,6 +27,7 @@ continue continue rebasing process
abort abort rebasing process and restore original branch abort abort rebasing process and restore original branch
skip skip current patch and continue rebasing process skip skip current patch and continue rebasing process
no-verify override pre-rebase hook from stopping the operation no-verify override pre-rebase hook from stopping the operation
root rebase all reachable commmits up to the root(s)
" "


. git-sh-setup . git-sh-setup
@ -44,6 +45,7 @@ STRATEGY=
ONTO= ONTO=
VERBOSE= VERBOSE=
OK_TO_SKIP_PRE_REBASE= OK_TO_SKIP_PRE_REBASE=
REBASE_ROOT=


GIT_CHERRY_PICK_HELP=" After resolving the conflicts, GIT_CHERRY_PICK_HELP=" After resolving the conflicts,
mark the corrected paths with 'git add <paths>', and mark the corrected paths with 'git add <paths>', and
@ -154,6 +156,11 @@ pick_one () {
output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
test -d "$REWRITTEN" && test -d "$REWRITTEN" &&
pick_one_preserving_merges "$@" && return pick_one_preserving_merges "$@" && return
if test ! -z "$REBASE_ROOT"
then
output git cherry-pick "$@"
return
fi
parent_sha1=$(git rev-parse --verify $sha1^) || parent_sha1=$(git rev-parse --verify $sha1^) ||
die "Could not get the parent of $sha1" die "Could not get the parent of $sha1"
current_sha1=$(git rev-parse --verify HEAD) current_sha1=$(git rev-parse --verify HEAD)
@ -197,7 +204,11 @@ pick_one_preserving_merges () {


# rewrite parents; if none were rewritten, we can fast-forward. # rewrite parents; if none were rewritten, we can fast-forward.
new_parents= new_parents=
pend=" $(git rev-list --parents -1 $sha1 | cut -d' ' -f2-)" pend=" $(git rev-list --parents -1 $sha1 | cut -d' ' -s -f2-)"
if test "$pend" = " "
then
pend=" root"
fi
while [ "$pend" != "" ] while [ "$pend" != "" ]
do do
p=$(expr "$pend" : ' \([^ ]*\)') p=$(expr "$pend" : ' \([^ ]*\)')
@ -227,7 +238,9 @@ pick_one_preserving_merges () {
if test -f "$DROPPED"/$p if test -f "$DROPPED"/$p
then then
fast_forward=f fast_forward=f
pend=" $(cat "$DROPPED"/$p)$pend" replacement="$(cat "$DROPPED"/$p)"
test -z "$replacement" && replacement=root
pend=" $replacement$pend"
else else
new_parents="$new_parents $p" new_parents="$new_parents $p"
fi fi
@ -443,6 +456,7 @@ get_saved_options () {
test -d "$REWRITTEN" && PRESERVE_MERGES=t test -d "$REWRITTEN" && PRESERVE_MERGES=t
test -f "$DOTEST"/strategy && STRATEGY="$(cat "$DOTEST"/strategy)" test -f "$DOTEST"/strategy && STRATEGY="$(cat "$DOTEST"/strategy)"
test -f "$DOTEST"/verbose && VERBOSE=t test -f "$DOTEST"/verbose && VERBOSE=t
test ! -s "$DOTEST"/upstream && REBASE_ROOT=t
} }


while test $# != 0 while test $# != 0
@ -547,6 +561,9 @@ first and then run 'git rebase --continue' again."
-i) -i)
# yeah, we know # yeah, we know
;; ;;
--root)
REBASE_ROOT=t
;;
--onto) --onto)
shift shift
ONTO=$(git rev-parse --verify "$1") || ONTO=$(git rev-parse --verify "$1") ||
@ -554,28 +571,36 @@ first and then run 'git rebase --continue' again."
;; ;;
--) --)
shift shift
test $# -eq 1 -o $# -eq 2 || usage test ! -z "$REBASE_ROOT" -o $# -eq 1 -o $# -eq 2 || usage
test -d "$DOTEST" && test -d "$DOTEST" &&
die "Interactive rebase already started" die "Interactive rebase already started"


git var GIT_COMMITTER_IDENT >/dev/null || git var GIT_COMMITTER_IDENT >/dev/null ||
die "You need to set your committer info first" die "You need to set your committer info first"


UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base" if test -z "$REBASE_ROOT"
run_pre_rebase_hook ${1+"$@"} then
UPSTREAM_ARG="$1"
UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base"
test -z "$ONTO" && ONTO=$UPSTREAM
shift
else
UPSTREAM_ARG=--root
test -z "$ONTO" &&
die "You must specify --onto when using --root"
fi
run_pre_rebase_hook "$UPSTREAM_ARG" "$@"


comment_for_reflog start comment_for_reflog start


require_clean_work_tree require_clean_work_tree


test -z "$ONTO" && ONTO=$UPSTREAM if test ! -z "$1"

if test ! -z "$2"
then then
output git show-ref --verify --quiet "refs/heads/$2" || output git show-ref --verify --quiet "refs/heads/$1" ||
die "Invalid branchname: $2" die "Invalid branchname: $1"
output git checkout "$2" || output git checkout "$1" ||
die "Could not checkout $2" die "Could not checkout $1"
fi fi


HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?" HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?"
@ -599,12 +624,19 @@ first and then run 'git rebase --continue' again."
# This ensures that commits on merged, but otherwise # This ensures that commits on merged, but otherwise
# unrelated side branches are left alone. (Think "X" # unrelated side branches are left alone. (Think "X"
# in the man page's example.) # in the man page's example.)
mkdir "$REWRITTEN" && if test -z "$REBASE_ROOT"
for c in $(git merge-base --all $HEAD $UPSTREAM) then
do mkdir "$REWRITTEN" &&
echo $ONTO > "$REWRITTEN"/$c || for c in $(git merge-base --all $HEAD $UPSTREAM)
do
echo $ONTO > "$REWRITTEN"/$c ||
die "Could not init rewritten commits"
done
else
mkdir "$REWRITTEN" &&
echo $ONTO > "$REWRITTEN"/root ||
die "Could not init rewritten commits" die "Could not init rewritten commits"
done fi
# No cherry-pick because our first pass is to determine # No cherry-pick because our first pass is to determine
# parents to rewrite and skipping dropped commits would # parents to rewrite and skipping dropped commits would
# prematurely end our probe # prematurely end our probe
@ -614,12 +646,21 @@ first and then run 'git rebase --continue' again."
MERGES_OPTION="--no-merges --cherry-pick" MERGES_OPTION="--no-merges --cherry-pick"
fi fi


SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
SHORTHEAD=$(git rev-parse --short $HEAD) SHORTHEAD=$(git rev-parse --short $HEAD)
SHORTONTO=$(git rev-parse --short $ONTO) SHORTONTO=$(git rev-parse --short $ONTO)
if test -z "$REBASE_ROOT"
# this is now equivalent to ! -z "$UPSTREAM"
then
SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
REVISIONS=$UPSTREAM...$HEAD
SHORTREVISIONS=$SHORTUPSTREAM..$SHORTHEAD
else
REVISIONS=$ONTO...$HEAD
SHORTREVISIONS=$SHORTHEAD
fi
git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \ git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
--abbrev=7 --reverse --left-right --topo-order \ --abbrev=7 --reverse --left-right --topo-order \
$UPSTREAM...$HEAD | \ $REVISIONS | \
sed -n "s/^>//p" | while read shortsha1 rest sed -n "s/^>//p" | while read shortsha1 rest
do do
if test t != "$PRESERVE_MERGES" if test t != "$PRESERVE_MERGES"
@ -627,14 +668,19 @@ first and then run 'git rebase --continue' again."
echo "pick $shortsha1 $rest" >> "$TODO" echo "pick $shortsha1 $rest" >> "$TODO"
else else
sha1=$(git rev-parse $shortsha1) sha1=$(git rev-parse $shortsha1)
preserve=t if test -z "$REBASE_ROOT"
for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -f2-) then
do preserve=t
if test -f "$REWRITTEN"/$p -a \( $p != $UPSTREAM -o $sha1 = $first_after_upstream \) for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -s -f2-)
then do
preserve=f if test -f "$REWRITTEN"/$p -a \( $p != $UPSTREAM -o $sha1 = $first_after_upstream \)
fi then
done preserve=f
fi
done
else
preserve=f
fi
if test f = "$preserve" if test f = "$preserve"
then then
touch "$REWRITTEN"/$sha1 touch "$REWRITTEN"/$sha1
@ -648,11 +694,11 @@ first and then run 'git rebase --continue' again."
then then
mkdir "$DROPPED" mkdir "$DROPPED"
# Save all non-cherry-picked changes # Save all non-cherry-picked changes
git rev-list $UPSTREAM...$HEAD --left-right --cherry-pick | \ git rev-list $REVISIONS --left-right --cherry-pick | \
sed -n "s/^>//p" > "$DOTEST"/not-cherry-picks sed -n "s/^>//p" > "$DOTEST"/not-cherry-picks
# Now all commits and note which ones are missing in # Now all commits and note which ones are missing in
# not-cherry-picks and hence being dropped # not-cherry-picks and hence being dropped
git rev-list $UPSTREAM..$HEAD | git rev-list $REVISIONS |
while read rev while read rev
do do
if test -f "$REWRITTEN"/$rev -a "$(grep "$rev" "$DOTEST"/not-cherry-picks)" = "" if test -f "$REWRITTEN"/$rev -a "$(grep "$rev" "$DOTEST"/not-cherry-picks)" = ""
@ -661,17 +707,18 @@ first and then run 'git rebase --continue' again."
# not worthwhile, we don't want to track its multiple heads, # not worthwhile, we don't want to track its multiple heads,
# just the history of its first-parent for others that will # just the history of its first-parent for others that will
# be rebasing on top of it # be rebasing on top of it
git rev-list --parents -1 $rev | cut -d' ' -f2 > "$DROPPED"/$rev git rev-list --parents -1 $rev | cut -d' ' -s -f2 > "$DROPPED"/$rev
short=$(git rev-list -1 --abbrev-commit --abbrev=7 $rev) short=$(git rev-list -1 --abbrev-commit --abbrev=7 $rev)
grep -v "^[a-z][a-z]* $short" <"$TODO" > "${TODO}2" ; mv "${TODO}2" "$TODO" grep -v "^[a-z][a-z]* $short" <"$TODO" > "${TODO}2" ; mv "${TODO}2" "$TODO"
rm "$REWRITTEN"/$rev rm "$REWRITTEN"/$rev
fi fi
done done
fi fi

test -s "$TODO" || echo noop >> "$TODO" test -s "$TODO" || echo noop >> "$TODO"
cat >> "$TODO" << EOF cat >> "$TODO" << EOF


# Rebase $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO # Rebase $SHORTREVISIONS onto $SHORTONTO
# #
# Commands: # Commands:
# p, pick = use commit # p, pick = use commit

101
t/t3412-rebase-root.sh

@ -67,6 +67,100 @@ test_expect_success 'pre-rebase got correct input (2)' '
test "z$(cat .git/PRE-REBASE-INPUT)" = z--root,work2 test "z$(cat .git/PRE-REBASE-INPUT)" = z--root,work2
' '


test_expect_success 'rebase -i --root --onto <newbase>' '
git checkout -b work3 other &&
GIT_EDITOR=: git rebase -i --root --onto master &&
git log --pretty=tformat:"%s" > rebased3 &&
test_cmp expect rebased3
'

test_expect_success 'pre-rebase got correct input (3)' '
test "z$(cat .git/PRE-REBASE-INPUT)" = z--root,
'

test_expect_success 'rebase -i --root --onto <newbase> <branch>' '
git branch work4 other &&
GIT_EDITOR=: git rebase -i --root --onto master work4 &&
git log --pretty=tformat:"%s" > rebased4 &&
test_cmp expect rebased4
'

test_expect_success 'pre-rebase got correct input (4)' '
test "z$(cat .git/PRE-REBASE-INPUT)" = z--root,work4
'

test_expect_success 'rebase -i -p with linear history' '
git checkout -b work5 other &&
GIT_EDITOR=: git rebase -i -p --root --onto master &&
git log --pretty=tformat:"%s" > rebased5 &&
test_cmp expect rebased5
'

test_expect_success 'pre-rebase got correct input (5)' '
test "z$(cat .git/PRE-REBASE-INPUT)" = z--root,
'

test_expect_success 'set up merge history' '
git checkout other^ &&
git checkout -b side &&
echo 5 > C &&
git add C &&
git commit -m 5 &&
git checkout other &&
git merge side
'

sed 's/#/ /g' > expect-side <<'EOF'
* Merge branch 'side' into other
|\##
| * 5
* | 4
|/##
* 3
* 2
* 1
EOF

test_expect_success 'rebase -i -p with merge' '
git checkout -b work6 other &&
GIT_EDITOR=: git rebase -i -p --root --onto master &&
git log --graph --topo-order --pretty=tformat:"%s" > rebased6 &&
test_cmp expect-side rebased6
'

test_expect_success 'set up second root and merge' '
git symbolic-ref HEAD refs/heads/third &&
rm .git/index &&
rm A B C &&
echo 6 > D &&
git add D &&
git commit -m 6 &&
git checkout other &&
git merge third
'

sed 's/#/ /g' > expect-third <<'EOF'
* Merge branch 'third' into other
|\##
| * 6
* | Merge branch 'side' into other
|\ \##
| * | 5
* | | 4
|/ /##
* | 3
|/##
* 2
* 1
EOF

test_expect_success 'rebase -i -p with two roots' '
git checkout -b work7 other &&
GIT_EDITOR=: git rebase -i -p --root --onto master &&
git log --graph --topo-order --pretty=tformat:"%s" > rebased7 &&
test_cmp expect-third rebased7
'

test_expect_success 'setup pre-rebase hook that fails' ' test_expect_success 'setup pre-rebase hook that fails' '
mkdir -p .git/hooks && mkdir -p .git/hooks &&
cat >.git/hooks/pre-rebase <<EOF && cat >.git/hooks/pre-rebase <<EOF &&
@ -83,4 +177,11 @@ test_expect_success 'pre-rebase hook stops rebase' '
test 0 = $(git rev-list other...stops1 | wc -l) test 0 = $(git rev-list other...stops1 | wc -l)
' '


test_expect_success 'pre-rebase hook stops rebase -i' '
git checkout -b stops2 other &&
GIT_EDITOR=: test_must_fail git rebase --root --onto master &&
test "z$(git symbolic-ref HEAD)" = zrefs/heads/stops2
test 0 = $(git rev-list other...stops2 | wc -l)
'

test_done test_done

Loading…
Cancel
Save