git/t/t3430-rebase-merges.sh
Patrick Steinhardt 44ec7c575f merge: fix leaking merge bases
When calling either the recursive or the ORT merge machineries we need
to provide a list of merge bases. The ownership of that parameter is
then implicitly transferred to the callee, which is somewhat fishy.
Furthermore, that list may leak in some cases where the merge machinery
runs into an error, thus causing a memory leak.

Refactor the code such that we stop transferring ownership. Instead, the
merge machinery will now create its own local copies of the passed in
list as required if they need to modify the list. Free the list at the
callsites as required.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-06-11 13:15:08 -07:00

616 lines
16 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_PASSES_SANITIZE_LEAK=true
. ./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 &&
test_commit dont-overwrite-untracked &&
cat >script-from-scratch <<-EOF &&
exec >dont-overwrite-untracked.t
pick $(git rev-parse B) B
reset refs/tags/dont-overwrite-untracked
pick $(git rev-parse C) C
exec cat .git/rebase-merge/done >actual
EOF
test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
test_must_fail git rebase -ir A &&
test_cmp_rev HEAD B &&
head -n3 script-from-scratch >expect &&
test_cmp expect .git/rebase-merge/done &&
rm dont-overwrite-untracked.t &&
git rebase --continue &&
tail -n3 script-from-scratch >>expect &&
test_cmp expect actual
'
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 &&
echo changed >file1 &&
git add file1 &&
test_must_fail git rebase --continue 2>err &&
grep "error: you have staged changes in your working tree" err &&
: 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_expect_success 'progress shows the correct total' '
git checkout -b progress H &&
git rebase --rebase-merges --force-rebase --verbose A 2> err &&
# Expecting "Rebasing (N/14)" here, no bogus total number
grep "^Rebasing.*/14.$" err >progress &&
test_line_count = 14 progress
'
test_expect_success 'truncate label names' '
commit=$(git commit-tree -p HEAD^ -p HEAD -m "0123456789 我 123" HEAD^{tree}) &&
git merge --ff-only $commit &&
done="$(git rev-parse --git-path rebase-merge/done)" &&
git -c rebase.maxLabelLength=14 rebase --rebase-merges -x "cp \"$done\" out" --root &&
grep "label 0123456789-我$" out &&
git -c rebase.maxLabelLength=13 rebase --rebase-merges -x "cp \"$done\" out" --root &&
grep "label 0123456789-$" out
'
test_done