From 0cae23467ada9b94210a0e770064841efea8ad40 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 25 Jun 2007 01:04:11 +0100 Subject: Move the pick_author code to git-sh-setup At the moment, only git-commit uses that code, to pick the author name, email and date from a given commit. This code will be reused in git rebase --interactive. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- git-commit.sh | 30 ++---------------------------- git-sh-setup.sh | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/git-commit.sh b/git-commit.sh index 5547a02954..d43bdd87c0 100755 --- a/git-commit.sh +++ b/git-commit.sh @@ -483,34 +483,8 @@ fi >>"$GIT_DIR"/COMMIT_EDITMSG # Author if test '' != "$use_commit" then - pick_author_script=' - /^author /{ - s/'\''/'\''\\'\'\''/g - h - s/^author \([^<]*\) <[^>]*> .*$/\1/ - s/'\''/'\''\'\'\''/g - s/.*/GIT_AUTHOR_NAME='\''&'\''/p - - g - s/^author [^<]* <\([^>]*\)> .*$/\1/ - s/'\''/'\''\'\'\''/g - s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p - - g - s/^author [^<]* <[^>]*> \(.*\)$/\1/ - s/'\''/'\''\'\'\''/g - s/.*/GIT_AUTHOR_DATE='\''&'\''/p - - q - } - ' - encoding=$(git config i18n.commitencoding || echo UTF-8) - set_author_env=`git show -s --pretty=raw --encoding="$encoding" "$use_commit" | - LANG=C LC_ALL=C sed -ne "$pick_author_script"` - eval "$set_author_env" - export GIT_AUTHOR_NAME - export GIT_AUTHOR_EMAIL - export GIT_AUTHOR_DATE + eval "$(get_author_ident_from_commit "$use_commit")" + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE fi if test '' != "$force_author" then diff --git a/git-sh-setup.sh b/git-sh-setup.sh index f24c7f2d23..d861db3b28 100755 --- a/git-sh-setup.sh +++ b/git-sh-setup.sh @@ -53,6 +53,33 @@ require_work_tree () { die "fatal: $0 cannot be used without a working tree." } +get_author_ident_from_commit () { + pick_author_script=' + /^author /{ + s/'\''/'\''\\'\'\''/g + h + s/^author \([^<]*\) <[^>]*> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_NAME='\''&'\''/p + + g + s/^author [^<]* <\([^>]*\)> .*$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p + + g + s/^author [^<]* <[^>]*> \(.*\)$/\1/ + s/'\''/'\''\'\'\''/g + s/.*/GIT_AUTHOR_DATE='\''&'\''/p + + q + } + ' + encoding=$(git config i18n.commitencoding || echo UTF-8) + git show -s --pretty=raw --encoding="$encoding" "$1" | + LANG=C LC_ALL=C sed -ne "$pick_author_script" +} + if [ -z "$LONG_USAGE" ] then LONG_USAGE="Usage: $0 $USAGE" -- cgit v1.2.3 From 1b1dce4bae760248a1fc3e29548a72c446e77270 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 25 Jun 2007 01:11:14 +0100 Subject: Teach rebase an interactive mode Don't you just hate the fact sometimes, that git-rebase just applies the patches, without any possibility to edit them, or rearrange them? With "--interactive", git-rebase now lets you edit the list of patches, so that you can reorder, edit and delete patches. Such a list will typically look like this: pick deadbee The oneline of this commit pick fa1afe1 The oneline of the next commit ... By replacing the command "pick" with the command "edit", you can amend that patch and/or its commit message, and by replacing it with "squash" you can tell rebase to fold that patch into the patch before that. It is derived from the script sent to the list in Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- Documentation/git-rebase.txt | 85 ++++++++++++- Makefile | 2 +- git-rebase--interactive.sh | 289 ++++++++++++++++++++++++++++++++++++++++++ git-rebase.sh | 12 +- t/t3404-rebase-interactive.sh | 163 ++++++++++++++++++++++++ 5 files changed, 546 insertions(+), 5 deletions(-) create mode 100755 git-rebase--interactive.sh create mode 100755 t/t3404-rebase-interactive.sh diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 0c00090a6b..2e3363a617 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -8,7 +8,8 @@ git-rebase - Forward-port local commits to the updated upstream head SYNOPSIS -------- [verse] -'git-rebase' [-v] [--merge] [-C] [--onto ] [] +'git-rebase' [-i | --interactive] [-v | --verbose] [--merge] [-C] + [--onto ] [] 'git-rebase' --continue | --skip | --abort DESCRIPTION @@ -208,6 +209,10 @@ OPTIONS context exist they all must match. By default no context is ever ignored. +-i, \--interactive:: + Make a list of the commits which are about to be rebased. Let the + user edit that list before rebasing. + include::merge-strategies.txt[] NOTES @@ -226,9 +231,83 @@ pre-rebase hook script for an example. You must be in the top directory of your project to start (or continue) a rebase. Upon completion, will be the current branch. -Author +INTERACTIVE MODE +---------------- + +Rebasing interactively means that you have a chance to edit the commits +which are rebased. You can reorder the commits, and you can +remove them (weeding out bad or otherwise unwanted patches). + +The interactive mode is meant for this type of workflow: + +1. have a wonderful idea +2. hack on the code +3. prepare a series for submission +4. submit + +where point 2. consists of several instances of + +a. regular use + 1. finish something worthy of a commit + 2. commit +b. independent fixup + 1. realize that something does not work + 2. fix that + 3. commit it + +Sometimes the thing fixed in b.2. cannot be amended to the not-quite +perfect commit it fixes, because that commit is buried deeply in a +patch series. That is exactly what interactive rebase is for: use it +after plenty of "a"s and "b"s, by rearranging and editing +commits, and squashing multiple commits into one. + +Start it with the last commit you want to retain as-is: + + git rebase -i + +An editor will be fired up with all the commits in your current branch +(ignoring merge commits), which come after the given commit. You can +reorder the commits in this list to your heart's content, and you can +remove them. The list looks more or less like this: + +------------------------------------------- +pick deadbee The oneline of this commit +pick fa1afe1 The oneline of the next commit +... +------------------------------------------- + +The oneline descriptions are purely for your pleasure; `git-rebase` will +not look at them but at the commit names ("deadbee" and "fa1afe1" in this +example), so do not delete or edit the names. + +By replacing the command "pick" with the command "edit", you can tell +`git-rebase` to stop after applying that commit, so that you can edit +the files and/or the commit message, amend the commit, and continue +rebasing. + +If you want to fold two or more commits into one, replace the command +"pick" with "squash" for the second and subsequent commit. If the +commits had different authors, it will attribute the squashed commit to +the author of the last commit. + +In both cases, or when a "pick" does not succeed (because of merge +errors), the loop will stop to let you fix things, and you can continue +the loop with `git rebase --continue`. + +For example, if you want to reorder the last 5 commits, such that what +was HEAD~4 becomes the new HEAD. To achieve that, you would call +`git-rebase` like this: + +---------------------- +$ git rebase -i HEAD~5 +---------------------- + +And move the first patch to the end of the list. + +Authors ------ -Written by Junio C Hamano +Written by Junio C Hamano and +Johannes E. Schindelin Documentation -------------- diff --git a/Makefile b/Makefile index a98e27aa7e..4ea5e450bd 100644 --- a/Makefile +++ b/Makefile @@ -204,7 +204,7 @@ SCRIPT_SH = \ git-fetch.sh \ git-ls-remote.sh \ git-merge-one-file.sh git-mergetool.sh git-parse-remote.sh \ - git-pull.sh git-rebase.sh \ + git-pull.sh git-rebase.sh git-rebase--interactive.sh \ git-repack.sh git-request-pull.sh git-reset.sh \ git-sh-setup.sh \ git-tag.sh git-verify-tag.sh \ diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh new file mode 100755 index 0000000000..ab3657250e --- /dev/null +++ b/git-rebase--interactive.sh @@ -0,0 +1,289 @@ +#!/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 | [--onto ] [])' + +. git-sh-setup +require_work_tree + +DOTEST="$GIT_DIR/.dotest-merge" +TODO="$DOTEST"/todo +DONE="$DOTEST"/done +STRATEGY= +VERBOSE= + +warn () { + echo "$*" >&2 +} + +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" +} + +make_patch () { + parent_sha1=$(git rev-parse --verify "$1"^ 2> /dev/null) + git diff "$parent_sha1".."$1" > "$DOTEST"/patch +} + +die_with_patch () { + make_patch "$1" + die "$2" +} + +pick_one () { + case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac + git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" + parent_sha1=$(git rev-parse --verify $sha1^ 2>/dev/null) + current_sha1=$(git rev-parse --verify HEAD) + if [ $current_sha1 = $parent_sha1 ]; then + git reset --hard $sha1 + sha1=$(git rev-parse --short $sha1) + warn Fast forward to $sha1 + else + git cherry-pick $STRATEGY "$@" + fi +} + +do_next () { + read command sha1 rest < "$TODO" + case "$command" in + \#|'') + mark_action_done + continue + ;; + 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 + failed=f + pick_one -n $sha1 || failed=t + MSG="$DOTEST"/message + echo "# This is a combination of two commits." > "$MSG" + echo "# The first commit's message is:" >> "$MSG" + echo >> "$MSG" + git cat-file commit HEAD | sed -e '1,/^$/d' >> "$MSG" + echo >> "$MSG" + echo "# And this is the 2nd commit message:" >> "$MSG" + echo >> "$MSG" + git cat-file commit $sha1 | sed -e '1,/^$/d' >> "$MSG" + git reset --soft HEAD^ + author_script=$(get_author_ident_from_commit $sha1) + 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 + git commit -F "$MSG" -e + ;; + t) + cp "$MSG" "$GIT_DIR"/MERGE_MSG + warn + warn "Could not apply $sha1... $rest" + warn "After you fixed that, commit the result with" + warn + warn " $(echo $author_script | tr '\012' ' ') \\" + warn " git commit -F \"$GIT_DIR\"/MERGE_MSG -e" + 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 + + HEAD=$(git rev-parse HEAD) + HEADNAME=$(cat "$DOTEST"/head-name) + rm -rf "$DOTEST" && + warn "Successfully rebased and updated $HEADNAME." + + exit +} + +do_rest () { + while : + do + do_next + done + test -f "$DOTEST"/verbose && + git diff --stat $(cat "$DOTEST"/head)..HEAD + exit +} + +while case $# in 0) break ;; esac +do + case "$1" in + --continue) + comment_for_reflog continue + + test -d "$DOTEST" || die "No interactive rebase running" + + 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 && + git reset --hard $HEAD && + rm -rf "$DOTEST" + exit + ;; + --skip) + comment_for_reflog skip + + test -d "$DOTEST" || die "No interactive rebase running" + + 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=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 + + if [ ! -z "$2"] + then + git show-ref --verify --quiet "refs/heads/$2" || + die "Invalid branchname: $2" + 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 + + mkdir "$DOTEST" || die "Could not create temporary $DOTEST" + : > "$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 t = "$VERBOSE" && : > "$DOTEST"/verbose + + cat > "$TODO" << EOF +# Rebasing $UPSTREAM..$HEAD onto $ONTO +# +# Commands: +# pick = use commit +# 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 \ + --abbrev=7 --reverse $UPSTREAM..$HEAD | \ + sed "s/^/pick /" >> "$TODO" + + test -z "$(grep -ve '^$' -e '^#' < $TODO)" && + die "Nothing to do" + + cp "$TODO" "$TODO".backup + ${VISUAL:-${EDITOR:-vi}} "$TODO" || + die "Could not execute editor" + + git reset --hard $ONTO && do_rest + esac + shift +done diff --git a/git-rebase.sh b/git-rebase.sh index 2aa3a011db..388752661f 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -3,7 +3,7 @@ # Copyright (c) 2005 Junio C Hamano. # -USAGE='[-v] [--onto ] []' +USAGE='[--interactive | -i] [-v] [--onto ] []' LONG_USAGE='git-rebase replaces with a new branch of the same name. When the --onto option is provided the new branch starts out with a HEAD equal to , otherwise it is equal to @@ -120,6 +120,16 @@ finish_rb_merge () { echo "All done." } +is_interactive () { + test -f "$dotest"/interactive || + while case $#,"$1" in 0,|*,-i|*,--interactive) break ;; esac + do + shift + done && test -n "$1" +} + +is_interactive "$@" && exec git-rebase--interactive "$@" + while case "$#" in 0) break ;; esac do case "$1" in diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh new file mode 100755 index 0000000000..48aa8ea814 --- /dev/null +++ b/t/t3404-rebase-interactive.sh @@ -0,0 +1,163 @@ +#!/bin/sh +# +# Copyright (c) 2007 Johannes E. Schindelin +# + +test_description='git rebase interactive + +This test runs git rebase "interactively", by faking an edit, and verifies +that the result still makes sense. +' +. ./test-lib.sh + +# set up two branches like this: +# +# A - B - C - D - E +# \ +# F - G - H +# \ +# I +# +# where B, D and G touch the same file. + +test_expect_success 'setup' ' + : > file1 && + git add file1 && + test_tick && + git commit -m A && + git tag A && + echo 1 > file1 && + test_tick && + git commit -m B file1 && + : > file2 && + git add file2 && + test_tick && + git commit -m C && + echo 2 > file1 && + test_tick && + git commit -m D file1 && + : > file3 && + git add file3 && + test_tick && + git commit -m E && + git checkout -b branch1 A && + : > file4 && + git add file4 && + test_tick && + git commit -m F && + git tag F && + echo 3 > file1 && + test_tick && + git commit -m G file1 && + : > file5 && + git add file5 && + test_tick && + git commit -m H && + git checkout -b branch2 F && + : > file6 && + git add file6 && + test_tick && + git commit -m I && + git tag I +' + +cat > fake-editor.sh << EOF +#!/bin/sh +test "\$1" = .git/COMMIT_EDITMSG && exit +test -z "\$FAKE_LINES" && exit +grep -v "^#" < "\$1" > "\$1".tmp +rm "\$1" +cat "\$1".tmp +action=pick +for line in \$FAKE_LINES; do + case \$line in + squash) + action="\$line";; + *) + echo sed -n "\${line}s/^pick/\$action/p" + sed -n "\${line}p" < "\$1".tmp + sed -n "\${line}s/^pick/\$action/p" < "\$1".tmp >> "\$1" + action=pick;; + esac +done +EOF + +chmod a+x fake-editor.sh +VISUAL="$(pwd)/fake-editor.sh" +export VISUAL + +test_expect_success 'no changes are a nop' ' + git rebase -i F && + test $(git rev-parse I) = $(git rev-parse HEAD) +' + +test_expect_success 'rebase on top of a non-conflicting commit' ' + git checkout branch1 && + git tag original-branch1 && + git rebase -i branch2 && + test file6 = $(git diff --name-only original-branch1) && + test $(git rev-parse I) = $(git rev-parse HEAD~2) +' + +test_expect_success 'exchange two commits' ' + FAKE_LINES="2 1" git rebase -i HEAD~2 && + test H = $(git cat-file commit HEAD^ | tail -n 1) && + test G = $(git cat-file commit HEAD | tail -n 1) +' + +cat > expect << EOF +diff --git a/file1 b/file1 +index e69de29..00750ed 100644 +--- a/file1 ++++ b/file1 +@@ -0,0 +1 @@ ++3 +EOF + +cat > expect2 << EOF +<<<<<<< HEAD:file1 +2 +======= +3 +>>>>>>> b7ca976... G:file1 +EOF + +test_expect_success 'stop on conflicting pick' ' + git tag new-branch1 && + ! git rebase -i master && + diff -u expect .git/.dotest-merge/patch && + diff -u expect2 file1 && + test 4 = $(grep -v "^#" < .git/.dotest-merge/done | wc -l) && + test 0 = $(grep -v "^#" < .git/.dotest-merge/todo | wc -l) +' + +test_expect_success 'abort' ' + git rebase --abort && + test $(git rev-parse new-branch1) = $(git rev-parse HEAD) && + ! test -d .git/.dotest-merge +' + +test_expect_success 'retain authorship' ' + echo A > file7 && + git add file7 && + GIT_AUTHOR_NAME="Twerp Snog" git commit -m "different author" && + git tag twerp && + git rebase -i --onto master HEAD^ && + git show HEAD | grep "^Author: Twerp Snog" +' + +test_expect_success 'squash' ' + git reset --hard twerp && + echo B > file7 && + GIT_AUTHOR_NAME="Nitfol" git commit -m "nitfol" file7 && + echo "******************************" && + FAKE_LINES="1 squash 2" git rebase -i --onto master HEAD~2 && + test B = $(cat file7) && + test $(git rev-parse HEAD^) = $(git rev-parse master) +' + +test_expect_success 'retain authorship when squashing' ' + git show HEAD | grep "^Author: Nitfol" +' + +test_done -- cgit v1.2.3 From ad562a817256adff4faadc17900b4aba67ca471a Mon Sep 17 00:00:00 2001 From: Matthias Lederhofer Date: Tue, 26 Jun 2007 15:38:42 +0200 Subject: ignore git-rebase--interactive Signed-off-by: Matthias Lederhofer Signed-off-by: Junio C Hamano --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e8b060cbe4..a2a617dc62 100644 --- a/.gitignore +++ b/.gitignore @@ -96,6 +96,7 @@ git-push git-quiltimport git-read-tree git-rebase +git-rebase--interactive git-receive-pack git-reflog git-relink -- cgit v1.2.3 From c54b7817f4f6bf422ea532d81217c28f63c259c5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 25 Jun 2007 18:56:55 +0100 Subject: rebase -i: several cleanups Support "--verbose" in addition to "-v", show short names in the list comment, clean up if there is nothing to do, and add several "test_ticks" in the test script. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- git-rebase--interactive.sh | 19 +++++++++++++++---- t/t3404-rebase-interactive.sh | 2 ++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index ab3657250e..a81432c0a5 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -60,6 +60,11 @@ die_with_patch () { die "$2" } +die_abort () { + rm -rf "$DOTEST" + die "$1" +} + pick_one () { case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac git rev-parse --verify $sha1 || die "Invalid commit name: $sha1" @@ -212,7 +217,7 @@ do -C*) die "Interactive rebase uses merge, so $1 does not make sense" ;; - -v) + -v|--verbose) VERBOSE=t ;; -i|--interactive) @@ -264,8 +269,11 @@ do echo $ONTO > "$DOTEST"/onto test t = "$VERBOSE" && : > "$DOTEST"/verbose + SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM) + SHORTHEAD=$(git rev-parse --short $HEAD) + SHORTONTO=$(git rev-parse --short $ONTO) cat > "$TODO" << EOF -# Rebasing $UPSTREAM..$HEAD onto $ONTO +# Rebasing $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO # # Commands: # pick = use commit @@ -277,13 +285,16 @@ EOF sed "s/^/pick /" >> "$TODO" test -z "$(grep -ve '^$' -e '^#' < $TODO)" && - die "Nothing to do" + die_abort "Nothing to do" cp "$TODO" "$TODO".backup ${VISUAL:-${EDITOR:-vi}} "$TODO" || die "Could not execute editor" - git reset --hard $ONTO && do_rest + test -z "$(grep -ve '^$' -e '^#' < $TODO)" && + die_abort "Nothing to do" + + git checkout $ONTO && do_rest esac shift done diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 48aa8ea814..19a3a8e813 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -140,6 +140,7 @@ test_expect_success 'abort' ' test_expect_success 'retain authorship' ' echo A > file7 && git add file7 && + test_tick && GIT_AUTHOR_NAME="Twerp Snog" git commit -m "different author" && git tag twerp && git rebase -i --onto master HEAD^ && @@ -149,6 +150,7 @@ test_expect_success 'retain authorship' ' test_expect_success 'squash' ' git reset --hard twerp && echo B > file7 && + test_tick && GIT_AUTHOR_NAME="Nitfol" git commit -m "nitfol" file7 && echo "******************************" && FAKE_LINES="1 squash 2" git rebase -i --onto master HEAD~2 && -- cgit v1.2.3 From 68a163c9b483ae352fcfee8c4505d113213daa73 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 25 Jun 2007 18:58:28 +0100 Subject: rebase -i: provide reasonable reflog for the rebased branch If your rebase succeeded, the HEAD's reflog will still show the whole mess, but "@{1}" now shows the state _before_ the rebase, so that you can reset (or compare) the original and the rebased revisions more easily. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- git-rebase--interactive.sh | 10 ++++++++-- t/t3404-rebase-interactive.sh | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index a81432c0a5..f0f2457975 100755 --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@ -151,8 +151,14 @@ do_next () { esac test -s "$TODO" && return - HEAD=$(git rev-parse HEAD) - HEADNAME=$(cat "$DOTEST"/head-name) + comment_for_reflog finish && + HEADNAME=$(cat "$DOTEST"/head-name) && + OLDHEAD=$(cat "$DOTEST"/head) && + SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) && + NEWHEAD=$(git rev-parse HEAD) && + message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" && + git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD && + git symbolic-ref HEAD $HEADNAME && rm -rf "$DOTEST" && warn "Successfully rebased and updated $HEADNAME." diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 19a3a8e813..9f12bb9321 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -99,6 +99,10 @@ test_expect_success 'rebase on top of a non-conflicting commit' ' test $(git rev-parse I) = $(git rev-parse HEAD~2) ' +test_expect_success 'reflog for the branch shows state before rebase' ' + test $(git rev-parse branch1@{1}) = $(git rev-parse original-branch1) +' + test_expect_success 'exchange two commits' ' FAKE_LINES="2 1" git rebase -i HEAD~2 && test H = $(git cat-file commit HEAD^ | tail -n 1) && -- cgit v1.2.3 From f09c9b8c5ff9d8a15499b09ccd6c3e7b3c76af77 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 25 Jun 2007 18:59:43 +0100 Subject: 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 " 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 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 Signed-off-by: Junio C Hamano --- Documentation/git-rebase.txt | 23 ++++++++- git-rebase--interactive.sh | 110 ++++++++++++++++++++++++++++++++++++++++-- t/t3404-rebase-interactive.sh | 22 +++++++++ 3 files changed, 151 insertions(+), 4 deletions(-) diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 2e3363a617..96907d4863 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git-rebase' [-i | --interactive] [-v | --verbose] [--merge] [-C] - [--onto ] [] + [-p | --preserve-merges] [--onto ] [] 'git-rebase' --continue | --skip | --abort DESCRIPTION @@ -213,6 +213,10 @@ OPTIONS Make a list of the commits which are about to be rebased. Let the 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[] NOTES @@ -304,6 +308,23 @@ $ git rebase -i HEAD~5 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 ------ Written by Junio C Hamano and diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh index f0f2457975..0c2a9697c4 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 ] [])' +USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose] + [--onto ] [])' . 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" diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 9f12bb9321..883cf29595 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -166,4 +166,26 @@ test_expect_success 'retain authorship when squashing' ' 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 -- cgit v1.2.3