diff options
Diffstat (limited to 't/t3430-rebase-merges.sh')
-rwxr-xr-x | t/t3430-rebase-merges.sh | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/t/t3430-rebase-merges.sh b/t/t3430-rebase-merges.sh new file mode 100755 index 0000000..fa2a06c --- /dev/null +++ b/t/t3430-rebase-merges.sh @@ -0,0 +1,537 @@ +#!/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 && + grep "^merge -C .* G$" .git/rebase-merge/done && + grep "^merge -C .* G$" .git/rebase-merge/git-rebase-todo && + test_path_is_file .git/rebase-merge/patch && + + : fail because of merge conflict && + rm G.t .git/rebase-merge/patch && + 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 '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 '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 |