git rebase keeps a list that maps the OID of each commit before it was rebased to the OID of the equivalent commit after the rebase. This list is used to drive the "post-rewrite" hook that is called at the end of a successful rebase. When a rebase stops for the user to resolve merge conflicts the OID of the commit being picked is written to ".git/rebase-merge/stopped-sha". Then when the rebase is continued that OID is added to the list of rewritten commits. Unfortunately if a commit cannot be picked because it would overwrite an untracked file we still write the "stopped-sha1" file. This means that when the rebase is continued the commit is added into the list of rewritten commits even though it has not been picked yet. Fix this by not calling error_with_patch() for failed commands. The pick has failed so there is nothing to commit and therefore we do not want to set up the state files for committing staged changes when the rebase continues. This change means we no-longer write a patch for the failed command or display the error message printed by error_with_patch(). As the command has failed the patch isn't really useful and in any case the user can inspect the commit associated with the failed command by inspecting REBASE_HEAD. Unless the user has disabled it we already print an advice message that is more helpful than the message from error_with_patch() which the user will still see. Even if the advice is disabled the user will see the messages from the merge machinery detailing the problem. The code to add a failed command back into the todo list is duplicated between pick_one_commit() and the loop in pick_commits(). Both sites print advice about the command being rescheduled, decrement the current item and save the todo list. To avoid duplicating this code pick_one_commit() is modified to set a flag to indicate that the command should be rescheduled in the main loop. This simplifies things as only the remaining copy of the code needs to be modified to set REBASE_HEAD rather than calling error_with_patch(). Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>
582 lines
14 KiB
Bash
Executable File
582 lines
14 KiB
Bash
Executable File
#!/bin/sh
|
|
#
|
|
# Copyright (c) 2018 Johannes E. Schindelin
|
|
#
|
|
|
|
test_description='git rebase -i --rebase-merges
|
|
|
|
This test runs git rebase "interactively", retaining the branch structure by
|
|
recreating merge commits.
|
|
|
|
Initial setup:
|
|
|
|
-- B -- (first)
|
|
/ \
|
|
A - C - D - E - H (main)
|
|
\ \ /
|
|
\ F - G (second)
|
|
\
|
|
Conflicting-G
|
|
'
|
|
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
|
|
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
|
|
|
|
. ./test-lib.sh
|
|
. "$TEST_DIRECTORY"/lib-rebase.sh
|
|
. "$TEST_DIRECTORY"/lib-log-graph.sh
|
|
|
|
test_cmp_graph () {
|
|
cat >expect &&
|
|
lib_test_cmp_graph --boundary --format=%s "$@"
|
|
}
|
|
|
|
test_expect_success 'setup' '
|
|
write_script replace-editor.sh <<-\EOF &&
|
|
mv "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
|
|
cp script-from-scratch "$1"
|
|
EOF
|
|
|
|
test_commit A &&
|
|
git checkout -b first &&
|
|
test_commit B &&
|
|
b=$(git rev-parse --short HEAD) &&
|
|
git checkout main &&
|
|
test_commit C &&
|
|
c=$(git rev-parse --short HEAD) &&
|
|
test_commit D &&
|
|
d=$(git rev-parse --short HEAD) &&
|
|
git merge --no-commit B &&
|
|
test_tick &&
|
|
git commit -m E &&
|
|
git tag -m E E &&
|
|
e=$(git rev-parse --short HEAD) &&
|
|
git checkout -b second C &&
|
|
test_commit F &&
|
|
f=$(git rev-parse --short HEAD) &&
|
|
test_commit G &&
|
|
g=$(git rev-parse --short HEAD) &&
|
|
git checkout main &&
|
|
git merge --no-commit G &&
|
|
test_tick &&
|
|
git commit -m H &&
|
|
h=$(git rev-parse --short HEAD) &&
|
|
git tag -m H H &&
|
|
git checkout A &&
|
|
test_commit conflicting-G G.t
|
|
'
|
|
|
|
test_expect_success 'create completely different structure' '
|
|
cat >script-from-scratch <<-\EOF &&
|
|
label onto
|
|
|
|
# onebranch
|
|
pick G
|
|
pick D
|
|
label onebranch
|
|
|
|
# second
|
|
reset onto
|
|
pick B
|
|
label second
|
|
|
|
reset onto
|
|
merge -C H second
|
|
merge onebranch # Merge the topic branch '\''onebranch'\''
|
|
EOF
|
|
test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
|
|
test_tick &&
|
|
git rebase -i -r A main &&
|
|
test_cmp_graph <<-\EOF
|
|
* Merge the topic branch '\''onebranch'\''
|
|
|\
|
|
| * D
|
|
| * G
|
|
* | H
|
|
|\ \
|
|
| |/
|
|
|/|
|
|
| * B
|
|
|/
|
|
* A
|
|
EOF
|
|
'
|
|
|
|
test_expect_success 'generate correct todo list' '
|
|
cat >expect <<-EOF &&
|
|
label onto
|
|
|
|
reset onto
|
|
pick $b B
|
|
label E
|
|
|
|
reset onto
|
|
pick $c C
|
|
label branch-point
|
|
pick $f F
|
|
pick $g G
|
|
label H
|
|
|
|
reset branch-point # C
|
|
pick $d D
|
|
merge -C $e E # E
|
|
merge -C $h H # H
|
|
|
|
EOF
|
|
|
|
grep -v "^#" <.git/ORIGINAL-TODO >output &&
|
|
test_cmp expect output
|
|
'
|
|
|
|
test_expect_success '`reset` refuses to overwrite untracked files' '
|
|
git checkout -b refuse-to-reset &&
|
|
test_commit dont-overwrite-untracked &&
|
|
git checkout @{-1} &&
|
|
: >dont-overwrite-untracked.t &&
|
|
echo "reset refs/tags/dont-overwrite-untracked" >script-from-scratch &&
|
|
test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
|
|
test_must_fail git rebase -ir HEAD &&
|
|
git rebase --abort
|
|
'
|
|
|
|
test_expect_success '`reset` rejects trees' '
|
|
test_when_finished "test_might_fail git rebase --abort" &&
|
|
test_must_fail env GIT_SEQUENCE_EDITOR="echo reset A^{tree} >" \
|
|
git rebase -i B C >out 2>err &&
|
|
grep "object .* is a tree" err &&
|
|
test_must_be_empty out
|
|
'
|
|
|
|
test_expect_success '`reset` only looks for labels under refs/rewritten/' '
|
|
test_when_finished "test_might_fail git rebase --abort" &&
|
|
git branch refs/rewritten/my-label A &&
|
|
test_must_fail env GIT_SEQUENCE_EDITOR="echo reset my-label >" \
|
|
git rebase -i B C >out 2>err &&
|
|
grep "could not resolve ${SQ}my-label${SQ}" err &&
|
|
test_must_be_empty out
|
|
'
|
|
|
|
test_expect_success 'failed `merge -C` writes patch (may be rescheduled, too)' '
|
|
test_when_finished "test_might_fail git rebase --abort" &&
|
|
git checkout -b conflicting-merge A &&
|
|
|
|
: fail because of conflicting untracked file &&
|
|
>G.t &&
|
|
echo "merge -C H G" >script-from-scratch &&
|
|
test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
|
|
test_tick &&
|
|
test_must_fail git rebase -ir HEAD &&
|
|
test_cmp_rev REBASE_HEAD H^0 &&
|
|
grep "^merge -C .* G$" .git/rebase-merge/done &&
|
|
grep "^merge -C .* G$" .git/rebase-merge/git-rebase-todo &&
|
|
test_path_is_missing .git/rebase-merge/patch &&
|
|
|
|
: fail because of merge conflict &&
|
|
git reset --hard conflicting-G &&
|
|
test_must_fail git rebase --continue &&
|
|
! grep "^merge -C .* G$" .git/rebase-merge/git-rebase-todo &&
|
|
test_path_is_file .git/rebase-merge/patch
|
|
'
|
|
|
|
test_expect_success 'failed `merge <branch>` does not crash' '
|
|
test_when_finished "test_might_fail git rebase --abort" &&
|
|
git checkout conflicting-G &&
|
|
|
|
echo "merge G" >script-from-scratch &&
|
|
test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
|
|
test_tick &&
|
|
test_must_fail git rebase -ir HEAD &&
|
|
! grep "^merge G$" .git/rebase-merge/git-rebase-todo &&
|
|
grep "^Merge branch ${SQ}G${SQ}$" .git/rebase-merge/message
|
|
'
|
|
|
|
test_expect_success 'merge -c commits before rewording and reloads todo-list' '
|
|
cat >script-from-scratch <<-\EOF &&
|
|
merge -c E B
|
|
merge -c H G
|
|
EOF
|
|
|
|
git checkout -b merge-c H &&
|
|
(
|
|
set_reword_editor &&
|
|
GIT_SEQUENCE_EDITOR="\"$PWD/replace-editor.sh\"" \
|
|
git rebase -i -r D
|
|
) &&
|
|
check_reworded_commits E H
|
|
'
|
|
|
|
test_expect_success 'merge -c rewords when a strategy is given' '
|
|
git checkout -b merge-c-with-strategy H &&
|
|
write_script git-merge-override <<-\EOF &&
|
|
echo overridden$1 >G.t
|
|
git add G.t
|
|
EOF
|
|
|
|
PATH="$PWD:$PATH" \
|
|
GIT_SEQUENCE_EDITOR="echo merge -c H G >" \
|
|
GIT_EDITOR="echo edited >>" \
|
|
git rebase --no-ff -ir -s override -Xxopt E &&
|
|
test_write_lines overridden--xopt >expect &&
|
|
test_cmp expect G.t &&
|
|
test_write_lines H "" edited "" >expect &&
|
|
git log --format=%B -1 >actual &&
|
|
test_cmp expect actual
|
|
|
|
'
|
|
test_expect_success 'with a branch tip that was cherry-picked already' '
|
|
git checkout -b already-upstream main &&
|
|
base="$(git rev-parse --verify HEAD)" &&
|
|
|
|
test_commit A1 &&
|
|
test_commit A2 &&
|
|
git reset --hard $base &&
|
|
test_commit B1 &&
|
|
test_tick &&
|
|
git merge -m "Merge branch A" A2 &&
|
|
|
|
git checkout -b upstream-with-a2 $base &&
|
|
test_tick &&
|
|
git cherry-pick A2 &&
|
|
|
|
git checkout already-upstream &&
|
|
test_tick &&
|
|
git rebase -i -r upstream-with-a2 &&
|
|
test_cmp_graph upstream-with-a2.. <<-\EOF
|
|
* Merge branch A
|
|
|\
|
|
| * A1
|
|
* | B1
|
|
|/
|
|
o A2
|
|
EOF
|
|
'
|
|
|
|
test_expect_success '--no-rebase-merges countermands --rebase-merges' '
|
|
git checkout -b no-rebase-merges E &&
|
|
git rebase --rebase-merges --no-rebase-merges C &&
|
|
test_cmp_graph C.. <<-\EOF
|
|
* B
|
|
* D
|
|
o C
|
|
EOF
|
|
'
|
|
|
|
test_expect_success 'do not rebase cousins unless asked for' '
|
|
git checkout -b cousins main &&
|
|
before="$(git rev-parse --verify HEAD)" &&
|
|
test_tick &&
|
|
git rebase -r HEAD^ &&
|
|
test_cmp_rev HEAD $before &&
|
|
test_tick &&
|
|
git rebase --rebase-merges=rebase-cousins HEAD^ &&
|
|
test_cmp_graph HEAD^.. <<-\EOF
|
|
* Merge the topic branch '\''onebranch'\''
|
|
|\
|
|
| * D
|
|
| * G
|
|
|/
|
|
o H
|
|
EOF
|
|
'
|
|
|
|
test_expect_success 'rebase.rebaseMerges=rebase-cousins is equivalent to --rebase-merges=rebase-cousins' '
|
|
test_config rebase.rebaseMerges rebase-cousins &&
|
|
git checkout -b config-rebase-cousins main &&
|
|
git rebase HEAD^ &&
|
|
test_cmp_graph HEAD^.. <<-\EOF
|
|
* Merge the topic branch '\''onebranch'\''
|
|
|\
|
|
| * D
|
|
| * G
|
|
|/
|
|
o H
|
|
EOF
|
|
'
|
|
|
|
test_expect_success '--no-rebase-merges overrides rebase.rebaseMerges=no-rebase-cousins' '
|
|
test_config rebase.rebaseMerges no-rebase-cousins &&
|
|
git checkout -b override-config-no-rebase-cousins E &&
|
|
git rebase --no-rebase-merges C &&
|
|
test_cmp_graph C.. <<-\EOF
|
|
* B
|
|
* D
|
|
o C
|
|
EOF
|
|
'
|
|
|
|
test_expect_success '--rebase-merges overrides rebase.rebaseMerges=rebase-cousins' '
|
|
test_config rebase.rebaseMerges rebase-cousins &&
|
|
git checkout -b override-config-rebase-cousins E &&
|
|
before="$(git rev-parse --verify HEAD)" &&
|
|
test_tick &&
|
|
git rebase --rebase-merges C &&
|
|
test_cmp_rev HEAD $before
|
|
'
|
|
|
|
test_expect_success 'refs/rewritten/* is worktree-local' '
|
|
git worktree add wt &&
|
|
cat >wt/script-from-scratch <<-\EOF &&
|
|
label xyz
|
|
exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
|
|
exec git rev-parse --verify refs/rewritten/xyz >b
|
|
EOF
|
|
|
|
test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
|
|
git -C wt rebase -i HEAD &&
|
|
test_must_be_empty wt/a &&
|
|
test_cmp_rev HEAD "$(cat wt/b)"
|
|
'
|
|
|
|
test_expect_success '--abort cleans up refs/rewritten' '
|
|
git checkout -b abort-cleans-refs-rewritten H &&
|
|
GIT_SEQUENCE_EDITOR="echo break >>" git rebase -ir @^ &&
|
|
git rev-parse --verify refs/rewritten/onto &&
|
|
git rebase --abort &&
|
|
test_must_fail git rev-parse --verify refs/rewritten/onto
|
|
'
|
|
|
|
test_expect_success '--quit cleans up refs/rewritten' '
|
|
git checkout -b quit-cleans-refs-rewritten H &&
|
|
GIT_SEQUENCE_EDITOR="echo break >>" git rebase -ir @^ &&
|
|
git rev-parse --verify refs/rewritten/onto &&
|
|
git rebase --quit &&
|
|
test_must_fail git rev-parse --verify refs/rewritten/onto
|
|
'
|
|
|
|
test_expect_success 'post-rewrite hook and fixups work for merges' '
|
|
git checkout -b post-rewrite H &&
|
|
test_commit same1 &&
|
|
git reset --hard HEAD^ &&
|
|
test_commit same2 &&
|
|
git merge -m "to fix up" same1 &&
|
|
echo same old same old >same2.t &&
|
|
test_tick &&
|
|
git commit --fixup HEAD same2.t &&
|
|
fixup="$(git rev-parse HEAD)" &&
|
|
|
|
test_hook post-rewrite <<-\EOF &&
|
|
cat >actual
|
|
EOF
|
|
|
|
test_tick &&
|
|
git rebase -i --autosquash -r HEAD^^^ &&
|
|
printf "%s %s\n%s %s\n%s %s\n%s %s\n" >expect $(git rev-parse \
|
|
$fixup^^2 HEAD^2 \
|
|
$fixup^^ HEAD^ \
|
|
$fixup^ HEAD \
|
|
$fixup HEAD) &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
test_expect_success 'refuse to merge ancestors of HEAD' '
|
|
echo "merge HEAD^" >script-from-scratch &&
|
|
test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
|
|
before="$(git rev-parse HEAD)" &&
|
|
git rebase -i HEAD &&
|
|
test_cmp_rev HEAD $before
|
|
'
|
|
|
|
test_expect_success 'root commits' '
|
|
git checkout --orphan unrelated &&
|
|
(GIT_AUTHOR_NAME="Parsnip" GIT_AUTHOR_EMAIL="root@example.com" \
|
|
test_commit second-root) &&
|
|
test_commit third-root &&
|
|
cat >script-from-scratch <<-\EOF &&
|
|
pick third-root
|
|
label first-branch
|
|
reset [new root]
|
|
pick second-root
|
|
merge first-branch # Merge the 3rd root
|
|
EOF
|
|
test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
|
|
test_tick &&
|
|
git rebase -i --force-rebase --root -r &&
|
|
test "Parsnip" = "$(git show -s --format=%an HEAD^)" &&
|
|
test $(git rev-parse second-root^0) != $(git rev-parse HEAD^) &&
|
|
test $(git rev-parse second-root:second-root.t) = \
|
|
$(git rev-parse HEAD^:second-root.t) &&
|
|
test_cmp_graph HEAD <<-\EOF &&
|
|
* Merge the 3rd root
|
|
|\
|
|
| * third-root
|
|
* second-root
|
|
EOF
|
|
|
|
: fast forward if possible &&
|
|
before="$(git rev-parse --verify HEAD)" &&
|
|
test_might_fail git config --unset sequence.editor &&
|
|
test_tick &&
|
|
git rebase -i --root -r &&
|
|
test_cmp_rev HEAD $before
|
|
'
|
|
|
|
test_expect_success 'a "merge" into a root commit is a fast-forward' '
|
|
head=$(git rev-parse HEAD) &&
|
|
cat >script-from-scratch <<-EOF &&
|
|
reset [new root]
|
|
merge $head
|
|
EOF
|
|
test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
|
|
test_tick &&
|
|
git rebase -i -r HEAD^ &&
|
|
test_cmp_rev HEAD $head
|
|
'
|
|
|
|
test_expect_success 'A root commit can be a cousin, treat it that way' '
|
|
git checkout --orphan khnum &&
|
|
test_commit yama &&
|
|
git checkout -b asherah main &&
|
|
test_commit shamkat &&
|
|
git merge --allow-unrelated-histories khnum &&
|
|
test_tick &&
|
|
git rebase -f -r HEAD^ &&
|
|
test_cmp_rev ! HEAD^2 khnum &&
|
|
test_cmp_graph HEAD^.. <<-\EOF &&
|
|
* Merge branch '\''khnum'\'' into asherah
|
|
|\
|
|
| * yama
|
|
o shamkat
|
|
EOF
|
|
test_tick &&
|
|
git rebase --rebase-merges=rebase-cousins HEAD^ &&
|
|
test_cmp_graph HEAD^.. <<-\EOF
|
|
* Merge branch '\''khnum'\'' into asherah
|
|
|\
|
|
| * yama
|
|
|/
|
|
o shamkat
|
|
EOF
|
|
'
|
|
|
|
test_expect_success 'labels that are object IDs are rewritten' '
|
|
git checkout -b third B &&
|
|
test_commit I &&
|
|
third=$(git rev-parse HEAD) &&
|
|
git checkout -b labels main &&
|
|
git merge --no-commit third &&
|
|
test_tick &&
|
|
git commit -m "Merge commit '\''$third'\'' into labels" &&
|
|
echo noop >script-from-scratch &&
|
|
test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
|
|
test_tick &&
|
|
git rebase -i -r A &&
|
|
grep "^label $third-" .git/ORIGINAL-TODO &&
|
|
! grep "^label $third$" .git/ORIGINAL-TODO
|
|
'
|
|
|
|
test_expect_success 'octopus merges' '
|
|
git checkout -b three &&
|
|
test_commit before-octopus &&
|
|
test_commit three &&
|
|
git checkout -b two HEAD^ &&
|
|
test_commit two &&
|
|
git checkout -b one HEAD^ &&
|
|
test_commit one &&
|
|
test_tick &&
|
|
(GIT_AUTHOR_NAME="Hank" GIT_AUTHOR_EMAIL="hank@sea.world" \
|
|
git merge -m "Tüntenfüsch" two three) &&
|
|
|
|
: fast forward if possible &&
|
|
before="$(git rev-parse --verify HEAD)" &&
|
|
test_tick &&
|
|
git rebase -i -r HEAD^^ &&
|
|
test_cmp_rev HEAD $before &&
|
|
|
|
test_tick &&
|
|
git rebase -i --force-rebase -r HEAD^^ &&
|
|
test "Hank" = "$(git show -s --format=%an HEAD)" &&
|
|
test "$before" != $(git rev-parse HEAD) &&
|
|
test_cmp_graph HEAD^^.. <<-\EOF
|
|
*-. Tüntenfüsch
|
|
|\ \
|
|
| | * three
|
|
| * | two
|
|
| |/
|
|
* / one
|
|
|/
|
|
o before-octopus
|
|
EOF
|
|
'
|
|
|
|
test_expect_success 'with --autosquash and --exec' '
|
|
git checkout -b with-exec H &&
|
|
echo Booh >B.t &&
|
|
test_tick &&
|
|
git commit --fixup B B.t &&
|
|
write_script show.sh <<-\EOF &&
|
|
subject="$(git show -s --format=%s HEAD)"
|
|
content="$(git diff HEAD^ HEAD | tail -n 1)"
|
|
echo "$subject: $content"
|
|
EOF
|
|
test_tick &&
|
|
git rebase -ir --autosquash --exec ./show.sh A >actual &&
|
|
grep "B: +Booh" actual &&
|
|
grep "E: +Booh" actual &&
|
|
grep "G: +G" actual
|
|
'
|
|
|
|
test_expect_success '--continue after resolving conflicts after a merge' '
|
|
git checkout -b already-has-g E &&
|
|
git cherry-pick E..G &&
|
|
test_commit H2 &&
|
|
|
|
git checkout -b conflicts-in-merge H &&
|
|
test_commit H2 H2.t conflicts H2-conflict &&
|
|
test_must_fail git rebase -r already-has-g &&
|
|
grep conflicts H2.t &&
|
|
echo resolved >H2.t &&
|
|
git add -u &&
|
|
git rebase --continue &&
|
|
test_must_fail git rev-parse --verify HEAD^2 &&
|
|
test_path_is_missing .git/MERGE_HEAD
|
|
'
|
|
|
|
test_expect_success '--rebase-merges with strategies' '
|
|
git checkout -b with-a-strategy F &&
|
|
test_tick &&
|
|
git merge -m "Merge conflicting-G" conflicting-G &&
|
|
|
|
: first, test with a merge strategy option &&
|
|
git rebase -ir -Xtheirs G &&
|
|
echo conflicting-G >expect &&
|
|
test_cmp expect G.t &&
|
|
|
|
: now, try with a merge strategy other than recursive &&
|
|
git reset --hard @{1} &&
|
|
write_script git-merge-override <<-\EOF &&
|
|
echo overridden$1 >>G.t
|
|
git add G.t
|
|
EOF
|
|
PATH="$PWD:$PATH" git rebase -ir -s override -Xxopt G &&
|
|
test_write_lines G overridden--xopt >expect &&
|
|
test_cmp expect G.t
|
|
'
|
|
|
|
test_expect_success '--rebase-merges with commit that can generate bad characters for filename' '
|
|
git checkout -b colon-in-label E &&
|
|
git merge -m "colon: this should work" G &&
|
|
git rebase --rebase-merges --force-rebase E
|
|
'
|
|
|
|
test_expect_success '--rebase-merges with message matched with onto label' '
|
|
git checkout -b onto-label E &&
|
|
git merge -m onto G &&
|
|
git rebase --rebase-merges --force-rebase E &&
|
|
test_cmp_graph <<-\EOF
|
|
* onto
|
|
|\
|
|
| * G
|
|
| * F
|
|
* | E
|
|
|\ \
|
|
| * | B
|
|
* | | D
|
|
| |/
|
|
|/|
|
|
* | C
|
|
|/
|
|
* A
|
|
EOF
|
|
'
|
|
|
|
test_done
|