diff mbox series

[2/7] merge-ort: maintain expected invariant for priv member

Message ID 17c97301baa829a993cf8838deb9271add5bd1cd.1718310307.git.gitgitgadget@gmail.com (mailing list archive)
State Superseded
Headers show
Series Fix and improve some error codepaths in merge-ort | expand

Commit Message

Elijah Newren June 13, 2024, 8:25 p.m. UTC
From: Elijah Newren <newren@gmail.com>

The calling convention for the merge machinery is
   One call to          init_merge_options()
   One or more calls to merge_incore_[non]recursive()
   One call to          merge_finalize()
      (possibly indirectly via merge_switch_to_result())
Both merge_switch_to_result() and merge_finalize() expect
   opt->priv == NULL && result->priv != NULL
which is supposed to be set up by our move_opt_priv_to_result_priv()
function.  However, two codepath dealing with error cases did not
execute this necessary logic, which could result in assertion failures
(or, if assertions were compiled out, could result in segfaults).  Fix
the oversight and add a test that would have caught one of these
problems.

While at it, also tighten an existing test for a non-recursive merge
to verify that it fails correctly, i.e. with the expected exit code
rather than with an assertion failure.

Reported-by: Matt Cree <matt.cree@gearset.com>
Signed-off-by: Elijah Newren <newren@gmail.com>
---
 merge-ort.c           |  3 ++-
 t/t6406-merge-attr.sh | 42 +++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 43 insertions(+), 2 deletions(-)

Comments

Taylor Blau June 13, 2024, 10:59 p.m. UTC | #1
On Thu, Jun 13, 2024 at 08:25:02PM +0000, Elijah Newren via GitGitGadget wrote:
> From: Elijah Newren <newren@gmail.com>
>
> The calling convention for the merge machinery is
>    One call to          init_merge_options()
>    One or more calls to merge_incore_[non]recursive()
>    One call to          merge_finalize()
>       (possibly indirectly via merge_switch_to_result())
> Both merge_switch_to_result() and merge_finalize() expect
>    opt->priv == NULL && result->priv != NULL
> which is supposed to be set up by our move_opt_priv_to_result_priv()
> function.  However, two codepath dealing with error cases did not

s/codepath/&s/ ?

> execute this necessary logic, which could result in assertion failures
> (or, if assertions were compiled out, could result in segfaults).  Fix
> the oversight and add a test that would have caught one of these
> problems.
>
> While at it, also tighten an existing test for a non-recursive merge
> to verify that it fails correctly, i.e. with the expected exit code
> rather than with an assertion failure.

I suspect that this test was flaky, too, since if the assertion errors
were compiled out and it died via a segfault, the test would have failed
outright as test_must_fail does not allow for segfaults typically.

So I'm glad to see us tightening up that area of the test suite.

> Reported-by: Matt Cree <matt.cree@gearset.com>
> Signed-off-by: Elijah Newren <newren@gmail.com>
> ---
>  merge-ort.c           |  3 ++-
>  t/t6406-merge-attr.sh | 42 +++++++++++++++++++++++++++++++++++++++++-
>  2 files changed, 43 insertions(+), 2 deletions(-)
>
> diff --git a/merge-ort.c b/merge-ort.c
> index 10f5a655f29..6ca7b0f9be4 100644
> --- a/merge-ort.c
> +++ b/merge-ort.c
> @@ -5015,7 +5015,7 @@ static void move_opt_priv_to_result_priv(struct merge_options *opt,
>  	 * to move it.
>  	 */
>  	assert(opt->priv && !result->priv);
> -	if (!opt->priv->call_depth) {
> +	if (!opt->priv->call_depth || result->clean < 0) {
>  		result->priv = opt->priv;
>  		result->_properly_initialized = RESULT_INITIALIZED;
>  		opt->priv = NULL;
> @@ -5052,6 +5052,7 @@ static void merge_ort_nonrecursive_internal(struct merge_options *opt,
>  		    oid_to_hex(&side1->object.oid),
>  		    oid_to_hex(&side2->object.oid));
>  		result->clean = -1;
> +		move_opt_priv_to_result_priv(opt, result);
>  		return;
>  	}
>  	trace2_region_leave("merge", "collect_merge_info", opt->repo);
> diff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh
> index 156a1efacfe..b6db5c2cc36 100755
> --- a/t/t6406-merge-attr.sh
> +++ b/t/t6406-merge-attr.sh
> @@ -185,7 +185,7 @@ test_expect_success !WINDOWS 'custom merge driver that is killed with a signal'
>
>  	>./please-abort &&
>  	echo "* merge=custom" >.gitattributes &&
> -	test_must_fail git merge main 2>err &&
> +	test_expect_code 2 git merge main 2>err &&
>  	grep "^error: failed to execute internal merge" err &&
>  	git ls-files -u >output &&
>  	git diff --name-only HEAD >>output &&
> @@ -261,4 +261,44 @@ test_expect_success 'binary files with union attribute' '
>  	grep -i "warning.*cannot merge.*HEAD vs. bin-main" output
>  '
>
> +test_expect_success !WINDOWS 'custom merge driver that is killed with a signal on recursive merge' '
> +	test_when_finished "rm -f output please-abort" &&
> +	test_when_finished "git checkout side" &&
> +
> +	git reset --hard anchor &&
> +
> +	git checkout -b base-a main^ &&
> +	echo base-a >text &&
> +	git commit -m base-a text &&
> +
> +	git checkout -b base-b main^ &&
> +	echo base-b >text &&
> +	git commit -m base-b text &&
> +
> +	git checkout -b recursive-a base-a &&
> +	test_must_fail git merge base-b &&

Here and below, do you care about the particular exit code of merging as
above? IOW, should these be `test_expect_code`'s as well?

Thanks,
Taylor
Elijah Newren June 19, 2024, 2:58 a.m. UTC | #2
On Thu, Jun 13, 2024 at 10:59 PM Taylor Blau <me@ttaylorr.com> wrote:
>
> On Thu, Jun 13, 2024 at 08:25:02PM +0000, Elijah Newren via GitGitGadget wrote:
> > From: Elijah Newren <newren@gmail.com>
> >
> > The calling convention for the merge machinery is
> >    One call to          init_merge_options()
> >    One or more calls to merge_incore_[non]recursive()
> >    One call to          merge_finalize()
> >       (possibly indirectly via merge_switch_to_result())
> > Both merge_switch_to_result() and merge_finalize() expect
> >    opt->priv == NULL && result->priv != NULL
> > which is supposed to be set up by our move_opt_priv_to_result_priv()
> > function.  However, two codepath dealing with error cases did not
>
> s/codepath/&s/ ?

Indeed.

> > execute this necessary logic, which could result in assertion failures
> > (or, if assertions were compiled out, could result in segfaults).  Fix
> > the oversight and add a test that would have caught one of these
> > problems.
> >
> > While at it, also tighten an existing test for a non-recursive merge
> > to verify that it fails correctly, i.e. with the expected exit code
> > rather than with an assertion failure.

It turns out my logic here was faulty; if a
   test_must_fail git ARGS...
command is run and git hits an assertion failure, the test_must_fail
will fail with
   test_must_fail: died by signal 6: git ARGS...
similar to how it would fail if git were to segfault.

However, I still like the idea of testing the exit status here because
of this comment in builtin/merge.c:
    /*
     * The backend exits with 1 when conflicts are
     * left to be resolved, with 2 when it does not
     * handle the given merge at all.
     */
and this is one of the few tests in the testsuite where we are
explicitly testing a case where the merge is neither success nor
conflicts, but failed-to-handle.

> I suspect that this test was flaky, too, since if the assertion errors
> were compiled out and it died via a segfault, the test would have failed
> outright as test_must_fail does not allow for segfaults typically.
>
> So I'm glad to see us tightening up that area of the test suite.
[...]
> > +     test_must_fail git merge base-b &&
>
> Here and below, do you care about the particular exit code of merging as
> above? IOW, should these be `test_expect_code`'s as well?

Both of these comments led me on the path to check something out and
discover my error above, which will lead to slightly different changes
to the patch.
diff mbox series

Patch

diff --git a/merge-ort.c b/merge-ort.c
index 10f5a655f29..6ca7b0f9be4 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -5015,7 +5015,7 @@  static void move_opt_priv_to_result_priv(struct merge_options *opt,
 	 * to move it.
 	 */
 	assert(opt->priv && !result->priv);
-	if (!opt->priv->call_depth) {
+	if (!opt->priv->call_depth || result->clean < 0) {
 		result->priv = opt->priv;
 		result->_properly_initialized = RESULT_INITIALIZED;
 		opt->priv = NULL;
@@ -5052,6 +5052,7 @@  static void merge_ort_nonrecursive_internal(struct merge_options *opt,
 		    oid_to_hex(&side1->object.oid),
 		    oid_to_hex(&side2->object.oid));
 		result->clean = -1;
+		move_opt_priv_to_result_priv(opt, result);
 		return;
 	}
 	trace2_region_leave("merge", "collect_merge_info", opt->repo);
diff --git a/t/t6406-merge-attr.sh b/t/t6406-merge-attr.sh
index 156a1efacfe..b6db5c2cc36 100755
--- a/t/t6406-merge-attr.sh
+++ b/t/t6406-merge-attr.sh
@@ -185,7 +185,7 @@  test_expect_success !WINDOWS 'custom merge driver that is killed with a signal'
 
 	>./please-abort &&
 	echo "* merge=custom" >.gitattributes &&
-	test_must_fail git merge main 2>err &&
+	test_expect_code 2 git merge main 2>err &&
 	grep "^error: failed to execute internal merge" err &&
 	git ls-files -u >output &&
 	git diff --name-only HEAD >>output &&
@@ -261,4 +261,44 @@  test_expect_success 'binary files with union attribute' '
 	grep -i "warning.*cannot merge.*HEAD vs. bin-main" output
 '
 
+test_expect_success !WINDOWS 'custom merge driver that is killed with a signal on recursive merge' '
+	test_when_finished "rm -f output please-abort" &&
+	test_when_finished "git checkout side" &&
+
+	git reset --hard anchor &&
+
+	git checkout -b base-a main^ &&
+	echo base-a >text &&
+	git commit -m base-a text &&
+
+	git checkout -b base-b main^ &&
+	echo base-b >text &&
+	git commit -m base-b text &&
+
+	git checkout -b recursive-a base-a &&
+	test_must_fail git merge base-b &&
+	echo recursive-a >text &&
+	git add text &&
+	git commit -m recursive-a &&
+
+	git checkout -b recursive-b base-b &&
+	test_must_fail git merge base-a &&
+	echo recursive-b >text &&
+	git add text &&
+	git commit -m recursive-b &&
+
+	git config --replace-all \
+	merge.custom.driver "./custom-merge %O %A %B 0 %P %S %X %Y" &&
+	git config --replace-all \
+	merge.custom.name "custom merge driver for testing" &&
+
+	>./please-abort &&
+	echo "* merge=custom" >.gitattributes &&
+	test_expect_code 2 git merge recursive-a 2>err &&
+	grep "^error: failed to execute internal merge" err &&
+	git ls-files -u >output &&
+	git diff --name-only HEAD >>output &&
+	test_must_be_empty output
+'
+
 test_done