aboutsummaryrefslogtreecommitdiff
path: root/git-rebase--interactive.sh
diff options
context:
space:
mode:
authorJohannes Schindelin <Johannes.Schindelin@gmx.de>2007-06-25 18:59:43 +0100
committerJunio C Hamano <gitster@pobox.com>2007-06-26 18:58:59 -0700
commitf09c9b8c5ff9d8a15499b09ccd6c3e7b3c76af77 (patch)
tree39662b301bd6abe55717934d567f72cd9b54dea7 /git-rebase--interactive.sh
parent68a163c9b483ae352fcfee8c4505d113213daa73 (diff)
downloadgit-f09c9b8c5ff9d8a15499b09ccd6c3e7b3c76af77.tar.gz
git-f09c9b8c5ff9d8a15499b09ccd6c3e7b3c76af77.tar.xz
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>
Diffstat (limited to 'git-rebase--interactive.sh')
-rwxr-xr-xgit-rebase--interactive.sh110
1 files changed, 107 insertions, 3 deletions
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index f0f245797..0c2a9697c 100755
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -10,7 +10,8 @@
# The original idea comes from Eric W. Biederman, in
# 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
require_work_tree
@@ -18,6 +19,8 @@ require_work_tree
DOTEST="$GIT_DIR/.dotest-merge"
TODO="$DOTEST"/todo
DONE="$DOTEST"/done
+REWRITTEN="$DOTEST"/rewritten
+PRESERVE_MERGES=
STRATEGY=
VERBOSE=
@@ -68,6 +71,8 @@ die_abort () {
pick_one () {
case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
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 [ $current_sha1 = $parent_sha1 ]; then
@@ -79,6 +84,75 @@ pick_one () {
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 () {
read command sha1 rest < "$TODO"
case "$command" in
@@ -155,7 +229,15 @@ do_next () {
HEADNAME=$(cat "$DOTEST"/head-name) &&
OLDHEAD=$(cat "$DOTEST"/head) &&
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)" &&
git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD &&
git symbolic-ref HEAD $HEADNAME &&
@@ -226,6 +308,9 @@ do
-v|--verbose)
VERBOSE=t
;;
+ -p|--preserve-merges)
+ PRESERVE_MERGES=t
+ ;;
-i|--interactive)
# yeah, we know
;;
@@ -274,6 +359,25 @@ do
echo $UPSTREAM > "$DOTEST"/upstream
echo $ONTO > "$DOTEST"/onto
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)
SHORTHEAD=$(git rev-parse --short $HEAD)
@@ -286,7 +390,7 @@ do
# edit = use commit, but stop for amending
# squash = use commit, but meld into previous commit
EOF
- git rev-list --no-merges --pretty=oneline --abbrev-commit \
+ git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
--abbrev=7 --reverse $UPSTREAM..$HEAD | \
sed "s/^/pick /" >> "$TODO"