Teach rebase -i about --preserve-merges

The option "-p" (or long "--preserve-merges") makes it possible to
rebase side branches including merges, without straightening the
history.

Example:

           X
            \
         A---M---B
        /
---o---O---P---Q

When the current HEAD is "B", "git rebase -i -p --onto Q O" will yield

               X
                 \
---o---O---P---Q---A'---M'---B'

Note that this will

- _not_ touch X [*1*], it does

- _not_ work without the --interactive flag [*2*], it does

- _not_ guess the type of the merge, but blindly uses recursive or
  whatever strategy you provided with "-s <strategy>" for all merges it
  has to redo, and it does

- _not_ make use of the original merge commit via git-rerere.

*1*: only commits which reach a merge base between <upstream> and HEAD
     are reapplied. The others are kept as-are.

*2*: git-rebase without --interactive is inherently patch based (at
     least at the moment), and therefore merges cannot be preserved.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Johannes Schindelin 2007-06-25 18:59:43 +01:00 коммит произвёл Junio C Hamano
Родитель 68a163c9b4
Коммит f09c9b8c5f
3 изменённых файлов: 151 добавлений и 4 удалений

Просмотреть файл

@ -9,7 +9,7 @@ SYNOPSIS
-------- --------
[verse] [verse]
'git-rebase' [-i | --interactive] [-v | --verbose] [--merge] [-C<n>] 'git-rebase' [-i | --interactive] [-v | --verbose] [--merge] [-C<n>]
[--onto <newbase>] <upstream> [<branch>] [-p | --preserve-merges] [--onto <newbase>] <upstream> [<branch>]
'git-rebase' --continue | --skip | --abort 'git-rebase' --continue | --skip | --abort
DESCRIPTION DESCRIPTION
@ -213,6 +213,10 @@ OPTIONS
Make a list of the commits which are about to be rebased. Let the Make a list of the commits which are about to be rebased. Let the
user edit that list before rebasing. user edit that list before rebasing.
-p, \--preserve-merges::
Instead of ignoring merges, try to recreate them. This option
only works in interactive mode.
include::merge-strategies.txt[] include::merge-strategies.txt[]
NOTES NOTES
@ -304,6 +308,23 @@ $ git rebase -i HEAD~5
And move the first patch to the end of the list. And move the first patch to the end of the list.
You might want to preserve merges, if you have a history like this:
------------------
X
\
A---M---B
/
---o---O---P---Q
------------------
Suppose you want to rebase the side branch starting at "A" to "Q". Make
sure that the current HEAD is "B", and call
-----------------------------
$ git rebase -i -p --onto Q O
-----------------------------
Authors Authors
------ ------
Written by Junio C Hamano <junkio@cox.net> and Written by Junio C Hamano <junkio@cox.net> and

Просмотреть файл

@ -10,7 +10,8 @@
# The original idea comes from Eric W. Biederman, in # The original idea comes from Eric W. Biederman, in
# http://article.gmane.org/gmane.comp.version-control.git/22407 # http://article.gmane.org/gmane.comp.version-control.git/22407
USAGE='(--continue | --abort | --skip | [--onto <branch>] <upstream> [<branch>])' USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose]
[--onto <branch>] <upstream> [<branch>])'
. git-sh-setup . git-sh-setup
require_work_tree require_work_tree
@ -18,6 +19,8 @@ require_work_tree
DOTEST="$GIT_DIR/.dotest-merge" DOTEST="$GIT_DIR/.dotest-merge"
TODO="$DOTEST"/todo TODO="$DOTEST"/todo
DONE="$DOTEST"/done DONE="$DOTEST"/done
REWRITTEN="$DOTEST"/rewritten
PRESERVE_MERGES=
STRATEGY= STRATEGY=
VERBOSE= VERBOSE=
@ -68,6 +71,8 @@ die_abort () {
pick_one () { pick_one () {
case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" 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^ 2>/dev/null)
current_sha1=$(git rev-parse --verify HEAD) current_sha1=$(git rev-parse --verify HEAD)
if [ $current_sha1 = $parent_sha1 ]; then if [ $current_sha1 = $parent_sha1 ]; then
@ -79,6 +84,75 @@ pick_one () {
fi fi
} }
pick_one_preserving_merges () {
case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
sha1=$(git rev-parse $sha1)
if [ -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 [ -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)
echo "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
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 ! git merge $STRATEGY -m "$msg" $new_parents
then
echo "$msg" > "$GIT_DIR"/MERGE_MSG
warn Error redoing merge $sha1
warn
warn After fixup, please use
die "$author_script git commit"
fi
;;
*)
git cherry-pick $STRATEGY "$@" ||
die_with_patch $sha1 "Could not pick $sha1"
esac
esac
}
do_next () { do_next () {
read command sha1 rest < "$TODO" read command sha1 rest < "$TODO"
case "$command" in case "$command" in
@ -155,7 +229,15 @@ do_next () {
HEADNAME=$(cat "$DOTEST"/head-name) && HEADNAME=$(cat "$DOTEST"/head-name) &&
OLDHEAD=$(cat "$DOTEST"/head) && OLDHEAD=$(cat "$DOTEST"/head) &&
SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) && SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) &&
NEWHEAD=$(git rev-parse HEAD) && if [ -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)" && message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" &&
git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD && git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD &&
git symbolic-ref HEAD $HEADNAME && git symbolic-ref HEAD $HEADNAME &&
@ -226,6 +308,9 @@ do
-v|--verbose) -v|--verbose)
VERBOSE=t VERBOSE=t
;; ;;
-p|--preserve-merges)
PRESERVE_MERGES=t
;;
-i|--interactive) -i|--interactive)
# yeah, we know # yeah, we know
;; ;;
@ -274,6 +359,25 @@ do
echo $UPSTREAM > "$DOTEST"/upstream echo $UPSTREAM > "$DOTEST"/upstream
echo $ONTO > "$DOTEST"/onto echo $ONTO > "$DOTEST"/onto
test t = "$VERBOSE" && : > "$DOTEST"/verbose test t = "$VERBOSE" && : > "$DOTEST"/verbose
if [ 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) SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
SHORTHEAD=$(git rev-parse --short $HEAD) SHORTHEAD=$(git rev-parse --short $HEAD)
@ -286,7 +390,7 @@ do
# edit = use commit, but stop for amending # edit = use commit, but stop for amending
# squash = use commit, but meld into previous commit # squash = use commit, but meld into previous commit
EOF EOF
git rev-list --no-merges --pretty=oneline --abbrev-commit \ git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
--abbrev=7 --reverse $UPSTREAM..$HEAD | \ --abbrev=7 --reverse $UPSTREAM..$HEAD | \
sed "s/^/pick /" >> "$TODO" sed "s/^/pick /" >> "$TODO"

Просмотреть файл

@ -166,4 +166,26 @@ test_expect_success 'retain authorship when squashing' '
git show HEAD | grep "^Author: Nitfol" git show HEAD | grep "^Author: Nitfol"
' '
test_expect_success 'preserve merges with -p' '
git checkout -b to-be-preserved master^ &&
: > unrelated-file &&
git add unrelated-file &&
test_tick &&
git commit -m "unrelated" &&
git checkout -b to-be-rebased master &&
echo B > file1 &&
test_tick &&
git commit -m J file1 &&
test_tick &&
git merge to-be-preserved &&
echo C > file1 &&
test_tick &&
git commit -m K file1 &&
git rebase -i -p --onto branch1 master &&
test $(git rev-parse HEAD^^2) = $(git rev-parse to-be-preserved) &&
test $(git rev-parse HEAD~3) = $(git rev-parse branch1) &&
test $(git show HEAD:file1) = C &&
test $(git show HEAD~2:file1) = B
'
test_done test_done