git/t/t6426-merge-skip-unneeded-updates.sh
Patrick Steinhardt 52a7dab439 builtin/mv: refactor to use struct strvec
Memory allocation patterns in git-mv(1) are extremely hard to follow:
We copy around string pointers into manually-managed arrays, some of
which alias each other, but only sometimes, while we also drop some of
those strings at other times without ever daring to free them.

While this may be my own subjective feeling, it seems like others have
given up as the code has multiple calls to `UNLEAK()`. These are not
sufficient though, and git-mv(1) is still leaking all over the place
even with them.

Refactor the code to instead track strings in `struct strvec`. While
this has the effect of effectively duplicating some of the strings
without an actual need, it is way easier to reason about and fixes all
of the aliasing of memory that has been going on. It allows us to get
rid of the `UNLEAK()` calls and also fixes leaks that those calls did
not paper over.

Mark tests which are now leak-free accordingly.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-05-27 11:20:02 -07:00

769 lines
17 KiB
Bash
Executable File

#!/bin/sh
test_description="merge cases"
# The setup for all of them, pictorially, is:
#
# A
# o
# / \
# O o ?
# \ /
# o
# B
#
# To help make it easier to follow the flow of tests, they have been
# divided into sections and each test will start with a quick explanation
# of what commits O, A, and B contain.
#
# Notation:
# z/{b,c} means files z/b and z/c both exist
# x/d_1 means file x/d exists with content d1. (Purpose of the
# underscore notation is to differentiate different
# files that might be renamed into each other's paths.)
TEST_PASSES_SANITIZE_LEAK=true
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-merge.sh
###########################################################################
# SECTION 1: Cases involving no renames (one side has subset of changes of
# the other side)
###########################################################################
# Testcase 1a, Changes on A, subset of changes on B
# Commit O: b_1
# Commit A: b_2
# Commit B: b_3
# Expected: b_2
test_setup_1a () {
git init 1a_$1 &&
(
cd 1a_$1 &&
test_write_lines 1 2 3 4 5 6 7 8 9 10 >b &&
git add b &&
test_tick &&
git commit -m "O" &&
git branch O &&
git branch A &&
git branch B &&
git checkout A &&
test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 10.5 >b &&
git add b &&
test_tick &&
git commit -m "A" &&
git checkout B &&
test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 >b &&
git add b &&
test_tick &&
git commit -m "B"
)
}
test_expect_success '1a-L: Modify(A)/Modify(B), change on B subset of A' '
test_setup_1a L &&
(
cd 1a_L &&
git checkout A^0 &&
test-tool chmtime --get -3600 b >old-mtime &&
GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
test_must_be_empty err &&
# Make sure b was NOT updated
test-tool chmtime --get b >new-mtime &&
test_cmp old-mtime new-mtime &&
git ls-files -s >index_files &&
test_line_count = 1 index_files &&
git rev-parse >actual HEAD:b &&
git rev-parse >expect A:b &&
test_cmp expect actual &&
git hash-object b >actual &&
git rev-parse A:b >expect &&
test_cmp expect actual
)
'
test_expect_success '1a-R: Modify(A)/Modify(B), change on B subset of A' '
test_setup_1a R &&
(
cd 1a_R &&
git checkout B^0 &&
test-tool chmtime --get -3600 b >old-mtime &&
GIT_MERGE_VERBOSITY=3 git merge -s recursive A^0 >out 2>err &&
# Make sure b WAS updated
test-tool chmtime --get b >new-mtime &&
test $(cat old-mtime) -lt $(cat new-mtime) &&
test_must_be_empty err &&
git ls-files -s >index_files &&
test_line_count = 1 index_files &&
git rev-parse >actual HEAD:b &&
git rev-parse >expect A:b &&
test_cmp expect actual &&
git hash-object b >actual &&
git rev-parse A:b >expect &&
test_cmp expect actual
)
'
###########################################################################
# SECTION 2: Cases involving basic renames
###########################################################################
# Testcase 2a, Changes on A, rename on B
# Commit O: b_1
# Commit A: b_2
# Commit B: c_1
# Expected: c_2
test_setup_2a () {
git init 2a_$1 &&
(
cd 2a_$1 &&
test_seq 1 10 >b &&
git add b &&
test_tick &&
git commit -m "O" &&
git branch O &&
git branch A &&
git branch B &&
git checkout A &&
test_seq 1 11 >b &&
git add b &&
test_tick &&
git commit -m "A" &&
git checkout B &&
git mv b c &&
test_tick &&
git commit -m "B"
)
}
test_expect_success '2a-L: Modify/rename, merge into modify side' '
test_setup_2a L &&
(
cd 2a_L &&
git checkout A^0 &&
test_path_is_missing c &&
GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
test_path_is_file c &&
git ls-files -s >index_files &&
test_line_count = 1 index_files &&
git rev-parse >actual HEAD:c &&
git rev-parse >expect A:b &&
test_cmp expect actual &&
git hash-object c >actual &&
git rev-parse A:b >expect &&
test_cmp expect actual &&
test_must_fail git rev-parse HEAD:b &&
test_path_is_missing b
)
'
test_expect_success '2a-R: Modify/rename, merge into rename side' '
test_setup_2a R &&
(
cd 2a_R &&
git checkout B^0 &&
test-tool chmtime --get -3600 c >old-mtime &&
GIT_MERGE_VERBOSITY=3 git merge -s recursive A^0 >out 2>err &&
# Make sure c WAS updated
test-tool chmtime --get c >new-mtime &&
test $(cat old-mtime) -lt $(cat new-mtime) &&
test_must_be_empty err &&
git ls-files -s >index_files &&
test_line_count = 1 index_files &&
git rev-parse >actual HEAD:c &&
git rev-parse >expect A:b &&
test_cmp expect actual &&
git hash-object c >actual &&
git rev-parse A:b >expect &&
test_cmp expect actual &&
test_must_fail git rev-parse HEAD:b &&
test_path_is_missing b
)
'
# Testcase 2b, Changed and renamed on A, subset of changes on B
# Commit O: b_1
# Commit A: c_2
# Commit B: b_3
# Expected: c_2
test_setup_2b () {
git init 2b_$1 &&
(
cd 2b_$1 &&
test_write_lines 1 2 3 4 5 6 7 8 9 10 >b &&
git add b &&
test_tick &&
git commit -m "O" &&
git branch O &&
git branch A &&
git branch B &&
git checkout A &&
test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 10.5 >b &&
git add b &&
git mv b c &&
test_tick &&
git commit -m "A" &&
git checkout B &&
test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 >b &&
git add b &&
test_tick &&
git commit -m "B"
)
}
test_expect_success '2b-L: Rename+Mod(A)/Mod(B), B mods subset of A' '
test_setup_2b L &&
(
cd 2b_L &&
git checkout A^0 &&
test-tool chmtime --get -3600 c >old-mtime &&
GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
test_must_be_empty err &&
# Make sure c WAS updated
test-tool chmtime --get c >new-mtime &&
test_cmp old-mtime new-mtime &&
git ls-files -s >index_files &&
test_line_count = 1 index_files &&
git rev-parse >actual HEAD:c &&
git rev-parse >expect A:c &&
test_cmp expect actual &&
git hash-object c >actual &&
git rev-parse A:c >expect &&
test_cmp expect actual &&
test_must_fail git rev-parse HEAD:b &&
test_path_is_missing b
)
'
test_expect_success '2b-R: Rename+Mod(A)/Mod(B), B mods subset of A' '
test_setup_2b R &&
(
cd 2b_R &&
git checkout B^0 &&
test_path_is_missing c &&
GIT_MERGE_VERBOSITY=3 git merge -s recursive A^0 >out 2>err &&
# Make sure c now present (and thus was updated)
test_path_is_file c &&
test_must_be_empty err &&
git ls-files -s >index_files &&
test_line_count = 1 index_files &&
git rev-parse >actual HEAD:c &&
git rev-parse >expect A:c &&
test_cmp expect actual &&
git hash-object c >actual &&
git rev-parse A:c >expect &&
test_cmp expect actual &&
test_must_fail git rev-parse HEAD:b &&
test_path_is_missing b
)
'
# Testcase 2c, Changes on A, rename on B
# Commit O: b_1
# Commit A: b_2, c_3
# Commit B: c_1
# Expected: rename/add conflict c_2 vs c_3
#
# NOTE: Since A modified b_1->b_2, and B renamed b_1->c_1, the threeway
# merge of those files should result in c_2. We then should have a
# rename/add conflict between c_2 and c_3. However, if we note in
# merge_content() that A had the right contents (b_2 has same
# contents as c_2, just at a different name), and that A had the
# right path present (c_3 existed) and thus decides that it can
# skip the update, then we're in trouble. This test verifies we do
# not make that particular mistake.
test_setup_2c () {
git init 2c &&
(
cd 2c &&
test_seq 1 10 >b &&
git add b &&
test_tick &&
git commit -m "O" &&
git branch O &&
git branch A &&
git branch B &&
git checkout A &&
test_seq 1 11 >b &&
echo whatever >c &&
git add b c &&
test_tick &&
git commit -m "A" &&
git checkout B &&
git mv b c &&
test_tick &&
git commit -m "B"
)
}
test_expect_success '2c: Modify b & add c VS rename b->c' '
test_setup_2c &&
(
cd 2c &&
git checkout A^0 &&
test-tool chmtime --get -3600 c >old-mtime &&
GIT_MERGE_VERBOSITY=3 &&
export GIT_MERGE_VERBOSITY &&
test_must_fail git merge -s recursive B^0 >out 2>err &&
test_grep "CONFLICT (.*/add):" out &&
test_must_be_empty err &&
git ls-files -s >index_files &&
test_line_count = 2 index_files &&
# Ensure b was removed
test_path_is_missing b &&
# Make sure c WAS updated...
test-tool chmtime --get c >new-mtime &&
test $(cat old-mtime) -lt $(cat new-mtime) &&
# ...and has correct index entries and working tree contents
git rev-parse >actual :2:c :3:c &&
git rev-parse >expect A:c A:b &&
test_cmp expect actual &&
git cat-file -p A:b >>merge-me &&
git cat-file -p A:c >>merged &&
>empty &&
test_must_fail git merge-file \
-L "HEAD" \
-L "" \
-L "B^0" \
merged empty merge-me &&
test_cmp merged c
)
'
###########################################################################
# SECTION 3: Cases involving directory renames
#
# NOTE:
# Directory renames only apply when one side renames a directory, and the
# other side adds or renames a path into that directory. Applying the
# directory rename to that new path creates a new pathname that didn't
# exist on either side of history. Thus, it is impossible for the
# merge contents to already be at the right path, so all of these checks
# exist just to make sure that updates are not skipped.
###########################################################################
# Testcase 3a, Change + rename into dir foo on A, dir rename foo->bar on B
# Commit O: bq_1, foo/whatever
# Commit A: foo/{bq_2, whatever}
# Commit B: bq_1, bar/whatever
# Expected: bar/{bq_2, whatever}
test_setup_3a () {
git init 3a_$1 &&
(
cd 3a_$1 &&
mkdir foo &&
test_seq 1 10 >bq &&
test_write_lines a b c d e f g h i j k >foo/whatever &&
git add bq foo/whatever &&
test_tick &&
git commit -m "O" &&
git branch O &&
git branch A &&
git branch B &&
git checkout A &&
test_seq 1 11 >bq &&
git add bq &&
git mv bq foo/ &&
test_tick &&
git commit -m "A" &&
git checkout B &&
git mv foo/ bar/ &&
test_tick &&
git commit -m "B"
)
}
test_expect_success '3a-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
test_setup_3a L &&
(
cd 3a_L &&
git checkout A^0 &&
test_path_is_missing bar/bq &&
GIT_MERGE_VERBOSITY=3 git -c merge.directoryRenames=true merge -s recursive B^0 >out 2>err &&
test_must_be_empty err &&
test_path_is_file bar/bq &&
git ls-files -s >index_files &&
test_line_count = 2 index_files &&
git rev-parse >actual HEAD:bar/bq HEAD:bar/whatever &&
git rev-parse >expect A:foo/bq A:foo/whatever &&
test_cmp expect actual &&
git hash-object bar/bq bar/whatever >actual &&
git rev-parse A:foo/bq A:foo/whatever >expect &&
test_cmp expect actual &&
test_must_fail git rev-parse HEAD:bq HEAD:foo/bq &&
test_path_is_missing bq &&
test_path_is_missing foo/bq &&
test_path_is_missing foo/whatever
)
'
test_expect_success '3a-R: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
test_setup_3a R &&
(
cd 3a_R &&
git checkout B^0 &&
test_path_is_missing bar/bq &&
GIT_MERGE_VERBOSITY=3 git -c merge.directoryRenames=true merge -s recursive A^0 >out 2>err &&
test_must_be_empty err &&
test_path_is_file bar/bq &&
git ls-files -s >index_files &&
test_line_count = 2 index_files &&
git rev-parse >actual HEAD:bar/bq HEAD:bar/whatever &&
git rev-parse >expect A:foo/bq A:foo/whatever &&
test_cmp expect actual &&
git hash-object bar/bq bar/whatever >actual &&
git rev-parse A:foo/bq A:foo/whatever >expect &&
test_cmp expect actual &&
test_must_fail git rev-parse HEAD:bq HEAD:foo/bq &&
test_path_is_missing bq &&
test_path_is_missing foo/bq &&
test_path_is_missing foo/whatever
)
'
# Testcase 3b, rename into dir foo on A, dir rename foo->bar + change on B
# Commit O: bq_1, foo/whatever
# Commit A: foo/{bq_1, whatever}
# Commit B: bq_2, bar/whatever
# Expected: bar/{bq_2, whatever}
test_setup_3b () {
git init 3b_$1 &&
(
cd 3b_$1 &&
mkdir foo &&
test_seq 1 10 >bq &&
test_write_lines a b c d e f g h i j k >foo/whatever &&
git add bq foo/whatever &&
test_tick &&
git commit -m "O" &&
git branch O &&
git branch A &&
git branch B &&
git checkout A &&
git mv bq foo/ &&
test_tick &&
git commit -m "A" &&
git checkout B &&
test_seq 1 11 >bq &&
git add bq &&
git mv foo/ bar/ &&
test_tick &&
git commit -m "B"
)
}
test_expect_success '3b-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
test_setup_3b L &&
(
cd 3b_L &&
git checkout A^0 &&
test_path_is_missing bar/bq &&
GIT_MERGE_VERBOSITY=3 git -c merge.directoryRenames=true merge -s recursive B^0 >out 2>err &&
test_must_be_empty err &&
test_path_is_file bar/bq &&
git ls-files -s >index_files &&
test_line_count = 2 index_files &&
git rev-parse >actual HEAD:bar/bq HEAD:bar/whatever &&
git rev-parse >expect B:bq A:foo/whatever &&
test_cmp expect actual &&
git hash-object bar/bq bar/whatever >actual &&
git rev-parse B:bq A:foo/whatever >expect &&
test_cmp expect actual &&
test_must_fail git rev-parse HEAD:bq HEAD:foo/bq &&
test_path_is_missing bq &&
test_path_is_missing foo/bq &&
test_path_is_missing foo/whatever
)
'
test_expect_success '3b-R: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
test_setup_3b R &&
(
cd 3b_R &&
git checkout B^0 &&
test_path_is_missing bar/bq &&
GIT_MERGE_VERBOSITY=3 git -c merge.directoryRenames=true merge -s recursive A^0 >out 2>err &&
test_must_be_empty err &&
test_path_is_file bar/bq &&
git ls-files -s >index_files &&
test_line_count = 2 index_files &&
git rev-parse >actual HEAD:bar/bq HEAD:bar/whatever &&
git rev-parse >expect B:bq A:foo/whatever &&
test_cmp expect actual &&
git hash-object bar/bq bar/whatever >actual &&
git rev-parse B:bq A:foo/whatever >expect &&
test_cmp expect actual &&
test_must_fail git rev-parse HEAD:bq HEAD:foo/bq &&
test_path_is_missing bq &&
test_path_is_missing foo/bq &&
test_path_is_missing foo/whatever
)
'
###########################################################################
# SECTION 4: Cases involving dirty changes
###########################################################################
# Testcase 4a, Changed on A, subset of changes on B, locally modified
# Commit O: b_1
# Commit A: b_2
# Commit B: b_3
# Working copy: b_4
# Expected: b_2 for merge, b_4 in working copy
test_setup_4a () {
git init 4a &&
(
cd 4a &&
test_write_lines 1 2 3 4 5 6 7 8 9 10 >b &&
git add b &&
test_tick &&
git commit -m "O" &&
git branch O &&
git branch A &&
git branch B &&
git checkout A &&
test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 10.5 >b &&
git add b &&
test_tick &&
git commit -m "A" &&
git checkout B &&
test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 >b &&
git add b &&
test_tick &&
git commit -m "B"
)
}
# NOTE: For as long as we continue using unpack_trees() without index_only
# set to true, it will error out on a case like this claiming that the locally
# modified file would be overwritten by the merge. Getting this testcase
# correct requires doing the merge in-memory first, then realizing that no
# updates to the file are necessary, and thus that we can just leave the path
# alone.
test_expect_merge_algorithm failure success '4a: Change on A, change on B subset of A, dirty mods present' '
test_setup_4a &&
(
cd 4a &&
git checkout A^0 &&
echo "File rewritten" >b &&
test-tool chmtime --get -3600 b >old-mtime &&
GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
test_must_be_empty err &&
# Make sure b was NOT updated
test-tool chmtime --get b >new-mtime &&
test_cmp old-mtime new-mtime &&
git ls-files -s >index_files &&
test_line_count = 1 index_files &&
git rev-parse >actual :0:b &&
git rev-parse >expect A:b &&
test_cmp expect actual &&
git hash-object b >actual &&
echo "File rewritten" | git hash-object --stdin >expect &&
test_cmp expect actual
)
'
# Testcase 4b, Changed+renamed on A, subset of changes on B, locally modified
# Commit O: b_1
# Commit A: c_2
# Commit B: b_3
# Working copy: c_4
# Expected: c_2
test_setup_4b () {
git init 4b &&
(
cd 4b &&
test_write_lines 1 2 3 4 5 6 7 8 9 10 >b &&
git add b &&
test_tick &&
git commit -m "O" &&
git branch O &&
git branch A &&
git branch B &&
git checkout A &&
test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 10.5 >b &&
git add b &&
git mv b c &&
test_tick &&
git commit -m "A" &&
git checkout B &&
test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 >b &&
git add b &&
test_tick &&
git commit -m "B"
)
}
test_expect_success '4b: Rename+Mod(A)/Mod(B), change on B subset of A, dirty mods present' '
test_setup_4b &&
(
cd 4b &&
git checkout A^0 &&
echo "File rewritten" >c &&
test-tool chmtime --get -3600 c >old-mtime &&
GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
test_must_be_empty err &&
# Make sure c was NOT updated
test-tool chmtime --get c >new-mtime &&
test_cmp old-mtime new-mtime &&
git ls-files -s >index_files &&
test_line_count = 1 index_files &&
git rev-parse >actual :0:c &&
git rev-parse >expect A:c &&
test_cmp expect actual &&
git hash-object c >actual &&
echo "File rewritten" | git hash-object --stdin >expect &&
test_cmp expect actual &&
test_must_fail git rev-parse HEAD:b &&
test_path_is_missing b
)
'
test_done