Merge branch 'tr/rebase-root'

* tr/rebase-root:
  rebase: update documentation for --root
  rebase -i: learn to rebase root commit
  rebase: learn to rebase root commit
  rebase -i: execute hook only after argument checking
This commit is contained in:
Junio C Hamano 2009-01-17 23:06:38 -08:00
commit 90abc19b5a
4 changed files with 322 additions and 52 deletions

View File

@ -8,10 +8,11 @@ git-rebase - Forward-port local commits to the updated upstream head
SYNOPSIS SYNOPSIS
-------- --------
[verse] [verse]
'git rebase' [-i | --interactive] [-v | --verbose] [-m | --merge] 'git rebase' [-i | --interactive] [options] [--onto <newbase>]
[-s <strategy> | --strategy=<strategy>] [--no-verify] <upstream> [<branch>]
[-C<n>] [ --whitespace=<option>] [-p | --preserve-merges] 'git rebase' [-i | --interactive] [options] --onto <newbase>
[--onto <newbase>] <upstream> [<branch>] --root [<branch>]
'git rebase' --continue | --skip | --abort 'git rebase' --continue | --skip | --abort
DESCRIPTION DESCRIPTION
@ -22,7 +23,8 @@ it remains on the current branch.
All changes made by commits in the current branch but that are not All changes made by commits in the current branch but that are not
in <upstream> are saved to a temporary area. This is the same set in <upstream> are saved to a temporary area. This is the same set
of commits that would be shown by `git log <upstream>..HEAD`. of commits that would be shown by `git log <upstream>..HEAD` (or
`git log HEAD`, if --root is specified).
The current branch is reset to <upstream>, or <newbase> if the The current branch is reset to <upstream>, or <newbase> if the
--onto option was supplied. This has the exact same effect as --onto option was supplied. This has the exact same effect as
@ -255,6 +257,15 @@ OPTIONS
--preserve-merges:: --preserve-merges::
Instead of ignoring merges, try to recreate them. Instead of ignoring merges, try to recreate them.
--root::
Rebase all commits reachable from <branch>, instead of
limiting them with an <upstream>. This allows you to rebase
the root commit(s) on a branch. Must be used with --onto, and
will skip changes already contained in <newbase> (instead of
<upstream>). When used together with --preserve-merges, 'all'
root commits will be rewritten to have <newbase> as parent
instead.
include::merge-strategies.txt[] include::merge-strategies.txt[]
NOTES NOTES

View File

@ -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,27 +571,36 @@ first and then run 'git rebase --continue' again."
;; ;;
--) --)
shift shift
run_pre_rebase_hook ${1+"$@"} test ! -z "$REBASE_ROOT" -o $# -eq 1 -o $# -eq 2 || usage
test $# -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"
if test -z "$REBASE_ROOT"
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
UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base" if test ! -z "$1"
test -z "$ONTO" && ONTO=$UPSTREAM
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?"
@ -598,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
@ -613,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"
@ -626,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
@ -647,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)" = ""
@ -660,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

View File

@ -3,7 +3,7 @@
# Copyright (c) 2005 Junio C Hamano. # Copyright (c) 2005 Junio C Hamano.
# #
USAGE='[--interactive | -i] [-v] [--onto <newbase>] <upstream> [<branch>]' USAGE='[--interactive | -i] [-v] [--onto <newbase>] [<upstream>|--root] [<branch>]'
LONG_USAGE='git-rebase replaces <branch> with a new branch of the LONG_USAGE='git-rebase replaces <branch> with a new branch of the
same name. When the --onto option is provided the new branch starts same name. When the --onto option is provided the new branch starts
out with a HEAD equal to <newbase>, otherwise it is equal to <upstream> out with a HEAD equal to <newbase>, otherwise it is equal to <upstream>
@ -47,6 +47,7 @@ dotest="$GIT_DIR"/rebase-merge
prec=4 prec=4
verbose= verbose=
git_am_opt= git_am_opt=
rebase_root=
continue_merge () { continue_merge () {
test -n "$prev_head" || die "prev_head must be defined" test -n "$prev_head" || die "prev_head must be defined"
@ -297,6 +298,9 @@ do
-C*) -C*)
git_am_opt="$git_am_opt $1" git_am_opt="$git_am_opt $1"
;; ;;
--root)
rebase_root=t
;;
-*) -*)
usage usage
;; ;;
@ -344,17 +348,29 @@ case "$diff" in
;; ;;
esac esac
# The upstream head must be given. Make sure it is valid. if test -z "$rebase_root"
upstream_name="$1" then
upstream=`git rev-parse --verify "${upstream_name}^0"` || # The upstream head must be given. Make sure it is valid.
die "invalid upstream $upstream_name" upstream_name="$1"
shift
upstream=`git rev-parse --verify "${upstream_name}^0"` ||
die "invalid upstream $upstream_name"
unset root_flag
upstream_arg="$upstream_name"
else
test -z "$newbase" && die "--root must be used with --onto"
unset upstream_name
unset upstream
root_flag="--root"
upstream_arg="$root_flag"
fi
# Make sure the branch to rebase onto is valid. # Make sure the branch to rebase onto is valid.
onto_name=${newbase-"$upstream_name"} onto_name=${newbase-"$upstream_name"}
onto=$(git rev-parse --verify "${onto_name}^0") || exit onto=$(git rev-parse --verify "${onto_name}^0") || exit
# If a hook exists, give it a chance to interrupt # If a hook exists, give it a chance to interrupt
run_pre_rebase_hook ${1+"$@"} run_pre_rebase_hook "$upstream_arg" "$@"
# If the branch to rebase is given, that is the branch we will rebase # If the branch to rebase is given, that is the branch we will rebase
# $branch_name -- branch being rebased, or HEAD (already detached) # $branch_name -- branch being rebased, or HEAD (already detached)
@ -362,16 +378,16 @@ run_pre_rebase_hook ${1+"$@"}
# $head_name -- refs/heads/<that-branch> or "detached HEAD" # $head_name -- refs/heads/<that-branch> or "detached HEAD"
switch_to= switch_to=
case "$#" in case "$#" in
2) 1)
# Is it "rebase other $branchname" or "rebase other $commit"? # Is it "rebase other $branchname" or "rebase other $commit"?
branch_name="$2" branch_name="$1"
switch_to="$2" switch_to="$1"
if git show-ref --verify --quiet -- "refs/heads/$2" && if git show-ref --verify --quiet -- "refs/heads/$1" &&
branch=$(git rev-parse -q --verify "refs/heads/$2") branch=$(git rev-parse -q --verify "refs/heads/$1")
then then
head_name="refs/heads/$2" head_name="refs/heads/$1"
elif branch=$(git rev-parse -q --verify "$2") elif branch=$(git rev-parse -q --verify "$1")
then then
head_name="detached HEAD" head_name="detached HEAD"
else else
@ -393,7 +409,8 @@ case "$#" in
esac esac
orig_head=$branch orig_head=$branch
# Now we are rebasing commits $upstream..$branch on top of $onto # Now we are rebasing commits $upstream..$branch (or with --root,
# everything leading up to $branch) on top of $onto
# Check if we are already based on $onto with linear history, # Check if we are already based on $onto with linear history,
# but this should be done only when upstream and onto are the same. # but this should be done only when upstream and onto are the same.
@ -429,10 +446,17 @@ then
exit 0 exit 0
fi fi
if test -n "$rebase_root"
then
revisions="$onto..$orig_head"
else
revisions="$upstream..$orig_head"
fi
if test -z "$do_merge" if test -z "$do_merge"
then then
git format-patch -k --stdout --full-index --ignore-if-in-upstream \ git format-patch -k --stdout --full-index --ignore-if-in-upstream \
"$upstream..$orig_head" | $root_flag "$revisions" |
git am $git_am_opt --rebasing --resolvemsg="$RESOLVEMSG" && git am $git_am_opt --rebasing --resolvemsg="$RESOLVEMSG" &&
move_to_original_branch move_to_original_branch
ret=$? ret=$?
@ -455,7 +479,7 @@ echo "$orig_head" > "$dotest/orig-head"
echo "$head_name" > "$dotest/head-name" echo "$head_name" > "$dotest/head-name"
msgnum=0 msgnum=0
for cmt in `git rev-list --reverse --no-merges "$upstream..$orig_head"` for cmt in `git rev-list --reverse --no-merges "$revisions"`
do do
msgnum=$(($msgnum + 1)) msgnum=$(($msgnum + 1))
echo "$cmt" > "$dotest/cmt.$msgnum" echo "$cmt" > "$dotest/cmt.$msgnum"

187
t/t3412-rebase-root.sh Executable file
View File

@ -0,0 +1,187 @@
#!/bin/sh
test_description='git rebase --root
Tests if git rebase --root --onto <newparent> can rebase the root commit.
'
. ./test-lib.sh
test_expect_success 'prepare repository' '
echo 1 > A &&
git add A &&
git commit -m 1 &&
echo 2 > A &&
git add A &&
git commit -m 2 &&
git symbolic-ref HEAD refs/heads/other &&
rm .git/index &&
echo 3 > B &&
git add B &&
git commit -m 3 &&
echo 1 > A &&
git add A &&
git commit -m 1b &&
echo 4 > B &&
git add B &&
git commit -m 4
'
test_expect_success 'rebase --root expects --onto' '
test_must_fail git rebase --root
'
test_expect_success 'setup pre-rebase hook' '
mkdir -p .git/hooks &&
cat >.git/hooks/pre-rebase <<EOF &&
#!$SHELL_PATH
echo "\$1,\$2" >.git/PRE-REBASE-INPUT
EOF
chmod +x .git/hooks/pre-rebase
'
cat > expect <<EOF
4
3
2
1
EOF
test_expect_success 'rebase --root --onto <newbase>' '
git checkout -b work &&
git rebase --root --onto master &&
git log --pretty=tformat:"%s" > rebased &&
test_cmp expect rebased
'
test_expect_success 'pre-rebase got correct input (1)' '
test "z$(cat .git/PRE-REBASE-INPUT)" = z--root,
'
test_expect_success 'rebase --root --onto <newbase> <branch>' '
git branch work2 other &&
git rebase --root --onto master work2 &&
git log --pretty=tformat:"%s" > rebased2 &&
test_cmp expect rebased2
'
test_expect_success 'pre-rebase got correct input (2)' '
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' '
mkdir -p .git/hooks &&
cat >.git/hooks/pre-rebase <<EOF &&
#!$SHELL_PATH
false
EOF
chmod +x .git/hooks/pre-rebase
'
test_expect_success 'pre-rebase hook stops rebase' '
git checkout -b stops1 other &&
GIT_EDITOR=: test_must_fail git rebase --root --onto master &&
test "z$(git symbolic-ref HEAD)" = zrefs/heads/stops1
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