{"thread":{"id":"65472","subject":"[PATCH 00/12] t: detect errors outside of test cases","startedAt":"2026-04-13T09:49:30Z","lastAt":"2026-04-21T07:34:50Z","messageCount":95,"participants":["Patrick Steinhardt","Junio C Hamano","Jeff King","SZEDER Gábor"],"isPatch":true,"patchVersion":1,"patchTotal":12},"messages":[{"id":"541452","messageId":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":null,"subject":"[PATCH 00/12] t: detect errors outside of test cases","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:21Z","receivedAt":"2026-04-13T09:49:30Z","isPatch":true,"body":"Hi,\n\nthis is a follow-up to the recent discussion we had around `set -e` to\nmake our tests more robust and basically supersedes Junio's [1].\n\nI've tested the patches with both Bash and Dash, and all tests are\npassing on my machine with both of them. CI seems to be happy, as\nwell. But I would expect that this change probably has some fallout,\neven though I hope that it's generally going to be small and contained.\n\nThis series is based on 8c9303b1ff (Merge branch\n'jc/no-writev-does-not-work', 2026-04-10).\n\nThanks!\n\nPatrick\n\n[1]: <20260325062114.2067946-1-gitster@pobox.com>\n\n---\nPatrick Steinhardt (12):\n      t: prepare `test_match_signal ()` calls for `set -e`\n      t: prepare `test_must_fail ()` for `set -e`\n      t: prepare `stop_git_daemon ()` for `set -e`\n      t: prepare `git config --unset` calls for `set -e`\n      t: prepare conditional test execution for `set -e`\n      t: prepare execution of potentially failing commands for `set -e`\n      t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`\n      t0008: silence error in subshell when using `grep -v`\n      t1301: don't fail in case setfacl(1) doesn't exist or fails\n      t6002: fix use of `expr` with `set -e`\n      t9902: fix use of `read` with `set -e`\n      t: detect errors outside of test cases\n\n t/lib-git-daemon.sh                | 13 ++++++++++---\n t/lib-git-svn.sh                   |  3 +--\n t/lib-httpd.sh                     |  3 +--\n t/t0005-signals.sh                 |  4 ++--\n t/t0008-ignores.sh                 |  4 ++--\n t/t1301-shared-repo.sh             |  2 +-\n t/t3600-rm.sh                      |  2 +-\n t/t4032-diff-inter-hunk-context.sh | 14 ++++++++------\n t/t6002-rev-list-bisect.sh         | 17 ++++++++++-------\n t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n t/t7508-status.sh                  |  4 ++--\n t/t9138-git-svn-authors-prog.sh    |  4 ++--\n t/t9200-git-cvsexportcommit.sh     |  3 +--\n t/t9400-git-cvsserver-server.sh    |  5 +++--\n t/t9401-git-cvsserver-crlf.sh      |  4 ++--\n t/t9402-git-cvsserver-refs.sh      |  4 ++--\n t/t9902-completion.sh              |  2 +-\n t/test-lib-functions.sh            | 12 ++++++++----\n t/test-lib.sh                      |  8 ++++++--\n 19 files changed, 76 insertions(+), 56 deletions(-)\n\n\n---\nbase-commit: 8c9303b1ffae5b745d1b0a1f98330cf7944d8db0\nchange-id: 20260410-b4-pks-tests-with-set-e-3ae479b24b51\n\n"},{"id":"541453","messageId":"20260413-b4-pks-tests-with-set-e-v1-1-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 01/12] t: prepare `test_match_signal ()` calls for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:22Z","receivedAt":"2026-04-13T09:49:31Z","isPatch":true,"body":"We have a couple of calls to `test_match_signal ()` where we execute a\nGit command and expect it to die with a specific signal. These calls\nwill essentially execute the process in a subshell via `foo; echo $?`,\nbut as we expect `foo` to fail this will cause the overall subshell to\nfail once we `set -e`.\n\nFix this issue by using `foo || echo $?` instead.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0005-signals.sh | 4 ++--\n t/t3600-rm.sh      | 2 +-\n 2 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t0005-signals.sh b/t/t0005-signals.sh\nindex afba0fc3fc..74c2a27972 100755\n--- a/t/t0005-signals.sh\n+++ b/t/t0005-signals.sh\n@@ -42,12 +42,12 @@ test_expect_success 'create blob' '\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE' '\n-\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((large_git || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE even if parent ignores it' '\n-\tOUT=$( ((trap \"\" PIPE && large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && large_git || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 1f16e6b522..8e68a00dcb 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -260,7 +260,7 @@ test_expect_success 'choking \"git rm\" should not let it die with cruft (induce S\n \n test_expect_success !MINGW 'choking \"git rm\" should not let it die with cruft (induce and check SIGPIPE)' '\n \tchoke_git_rm_setup &&\n-\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\"; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\" || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\" &&\n \ttest_path_is_missing .git/index.lock\n '\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541454","messageId":"20260413-b4-pks-tests-with-set-e-v1-2-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:23Z","receivedAt":"2026-04-13T09:49:34Z","isPatch":true,"body":"The helper function `test_must_fail ()` executes a specific Git command\nthat may or may not fail in a specific way. This is done by executing\nthe command in question and then comparing its exit code against a set\nof conditions.\n\nThis works, but once we run our test suite with `set -e` we may bail out\nof `test_must_fail ()` early in case the command actually fails, even\nthough we expect it to fail. Prepare for this change by marking the\ncommand part of a condition.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/test-lib-functions.sh | 8 ++++++--\n 1 file changed, 6 insertions(+), 2 deletions(-)\n\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex f3af10fb7e..f8bc77619b 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1195,8 +1195,12 @@ test_must_fail () {\n \t\techo >&7 \"test_must_fail: only 'git' is allowed: $*\"\n \t\treturn 1\n \tfi\n-\t\"$@\" 2>&7\n-\texit_code=$?\n+\tif \"$@\" 2>&7\n+\tthen\n+\t\texit_code=0\n+\telse\n+\t\texit_code=$?\n+\tfi\n \tif test $exit_code -eq 0 && ! list_contains \"$_test_ok\" success\n \tthen\n \t\techo >&4 \"test_must_fail: command succeeded: $*\"\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541455","messageId":"20260413-b4-pks-tests-with-set-e-v1-3-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 03/12] t: prepare `stop_git_daemon ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:24Z","receivedAt":"2026-04-13T09:49:36Z","isPatch":true,"body":"We have a couple of calls to `stop_git_daemon ()` outside of specific\ntest cases that will kill a backgrounded git-daemon(1) process and\nexpect the process with a specific error code. While these function\ncalls do end up killing git-daemon(1), the error handling we have in\nthose contexts is basically ineffective. So while we expect the process\nto exit with a specific error code, we will just continue with any error\nin case it doesn't.\n\nThis will change once we enable `set -e` in a subsequent commit. There's\ntwo issues though that will make this _always_ fail:\n\n  - Our call to `wait` is expected to fail, but because it's not part of\n    a condition it will cause us to bail out immediately with `set -e`.\n\n  - We try to kill git-daemon(1) a second time via the pidfile. We can\n    generally expect that this is the same PID though as we had in the\n    \"GIT_DAEMON_PID\" environment variable, and thus it's more likely\n    than not that we have already killed it, and the call to kill will\n    fail.\n\nPrepare for this change by making the call to `wait` part of a condition\nand by silencing failures of the second call to `kill`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/lib-git-daemon.sh | 13 ++++++++++---\n 1 file changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh\nindex e62569222b..91bcdd0bf7 100644\n--- a/t/lib-git-daemon.sh\n+++ b/t/lib-git-daemon.sh\n@@ -85,14 +85,21 @@ stop_git_daemon() {\n \n \t# kill git-daemon child of git\n \tsay >&3 \"Stopping git daemon ...\"\n+\n \tkill \"$GIT_DAEMON_PID\"\n-\twait \"$GIT_DAEMON_PID\" >&3 2>&4\n-\tret=$?\n+\tif wait \"$GIT_DAEMON_PID\" >&3 2>&4\n+\tthen\n+\t\tret=0\n+\telse\n+\t\tret=$?\n+\tfi\n+\n \tif ! test_match_signal 15 $ret\n \tthen\n \t\terror \"git daemon exited with status: $ret\"\n \tfi\n-\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null\n+\n+\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null || :\n \tGIT_DAEMON_PID=\n \trm -f git_daemon_output \"$GIT_DAEMON_PIDFILE\"\n }\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541456","messageId":"20260413-b4-pks-tests-with-set-e-v1-4-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 04/12] t: prepare `git config --unset` calls for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:25Z","receivedAt":"2026-04-13T09:49:38Z","isPatch":true,"body":"We have a couple of calls to `git config --unset` that ultimately end up\nas no-ops as the configuration variables aren't set (anymore) in the\nfirst place. These calls are mostly intended to recover unconditionally\nfrom tests that may have executed only partially, but they'll ultimately\nfail during a normal test run.\n\nThis hasn't been a problem until now as we aren't running tests with\n`set -e`. This is about to change though, so let's silence the case\nwhere we cannot unset the config keys.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t4032-diff-inter-hunk-context.sh | 2 +-\n t/t7508-status.sh                  | 4 ++--\n t/t9138-git-svn-authors-prog.sh    | 4 ++--\n 3 files changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\nindex bada0cbd32..c98eb6abb2 100755\n--- a/t/t4032-diff-inter-hunk-context.sh\n+++ b/t/t4032-diff-inter-hunk-context.sh\n@@ -17,7 +17,7 @@ f() {\n \n t() {\n \tuse_config=\n-\tgit config --unset diff.interHunkContext\n+\tgit config --unset diff.interHunkContext || :\n \n \tcase $# in\n \t4) hunks=$4; cmd=\"diff -U$3\";;\ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex a5e21bf8bf..1167b835a4 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -773,8 +773,8 @@ test_expect_success TTY 'status --porcelain ignores color.status' '\n '\n \n # recover unconditionally from color tests\n-git config --unset color.status\n-git config --unset color.ui\n+git config --unset color.status || :\n+git config --unset color.ui || :\n \n test_expect_success 'status --porcelain respects -b' '\n \ndiff --git a/t/t9138-git-svn-authors-prog.sh b/t/t9138-git-svn-authors-prog.sh\nindex 784ec7fc2d..5bb38cb23a 100755\n--- a/t/t9138-git-svn-authors-prog.sh\n+++ b/t/t9138-git-svn-authors-prog.sh\n@@ -68,8 +68,8 @@ test_expect_success 'authors-file overrode authors-prog' '\n \t)\n '\n \n-git --git-dir=x/.git config --unset svn.authorsfile\n-git --git-dir=x/.git config --unset svn.authorsprog\n+git --git-dir=x/.git config --unset svn.authorsfile || :\n+git --git-dir=x/.git config --unset svn.authorsprog || :\n \n test_expect_success 'authors-prog imported user without email' '\n \tsvn mkdir -m gg --username gg-hermit \"$svnrepo\"/gg &&\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541457","messageId":"20260413-b4-pks-tests-with-set-e-v1-5-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 05/12] t: prepare conditional test execution for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:26Z","receivedAt":"2026-04-13T09:49:41Z","isPatch":true,"body":"We have some test in our test suite where we use the pattern of\n`test ... && test_expect_succeess` to conditionally execute a test. The\nproblem is that when we decide to not execute the test, we'll indeed\nskip the test, but the overall statement will also be unsuccessful. This\nwill become a problem once we enable `set -e`.\n\nPrepare for this future by turning this into a proper conditional, which\nis also a bit easier to read overall.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t4032-diff-inter-hunk-context.sh | 12 +++++++-----\n t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n 2 files changed, 20 insertions(+), 16 deletions(-)\n\ndiff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\nindex c98eb6abb2..2d216fb70f 100755\n--- a/t/t4032-diff-inter-hunk-context.sh\n+++ b/t/t4032-diff-inter-hunk-context.sh\n@@ -40,11 +40,13 @@ t() {\n \t\ttest $(git $cmd $file | grep '^@@ ' | wc -l) = $hunks\n \t\"\n \n-\ttest -f $expected &&\n-\ttest_expect_success \"$label: check output\" \"\n-\t\tgit $cmd $file | grep -v '^index ' >actual &&\n-\t\ttest_cmp $expected actual\n-\t\"\n+\tif test -f $expected\n+\tthen\n+\t\ttest_expect_success \"$label: check output\" \"\n+\t\t\tgit $cmd $file | grep -v '^index ' >actual &&\n+\t\t\ttest_cmp $expected actual\n+\t\t\"\n+\tfi\n }\n \n cat <<EOF >expected.f1.0.1 || exit 1\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex f512eed278..8cc86522b2 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -220,17 +220,19 @@ check_dotx_symlink () {\n \t\t)\n \t'\n \n-\ttest -n \"$refuse_index\" &&\n-\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n-\t\ttest_must_fail \\\n-\t\t\tgit -C $dir \\\n-\t\t\t    -c core.protectntfs \\\n-\t\t\t    -c core.protecthfs \\\n-\t\t\t    read-tree $tree 2>err &&\n-\t\tgrep \"invalid path.*$name\" err &&\n-\t\tgit -C $dir ls-files -s >out &&\n-\t\ttest_must_be_empty out\n-\t'\n+\tif test -n \"$refuse_index\"\n+\tthen\n+\t\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n+\t\t\ttest_must_fail \\\n+\t\t\t\tgit -C $dir \\\n+\t\t\t\t    -c core.protectntfs \\\n+\t\t\t\t    -c core.protecthfs \\\n+\t\t\t\t    read-tree $tree 2>err &&\n+\t\t\tgrep \"invalid path.*$name\" err &&\n+\t\t\tgit -C $dir ls-files -s >out &&\n+\t\t\ttest_must_be_empty out\n+\t\t'\n+\tfi\n }\n \n check_dotx_symlink gitmodules vanilla .gitmodules\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541458","messageId":"20260413-b4-pks-tests-with-set-e-v1-6-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:27Z","receivedAt":"2026-04-13T09:49:43Z","isPatch":true,"body":"Several of our tests verify whether a certain binary can be executed,\npotentially skipping tests in case we cannot, for example because the\nbinary doesn't exist. In those cases we often run the binary outside of\nany conditionally.\n\nThis will start to fail once we enable `set -e`, as that will cause us\nto bail out the test immediately. Improve these tests by executing them\ninside of a conditional instead.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/lib-git-svn.sh                | 3 +--\n t/lib-httpd.sh                  | 3 +--\n t/t9200-git-cvsexportcommit.sh  | 3 +--\n t/t9400-git-cvsserver-server.sh | 5 +++--\n t/t9401-git-cvsserver-crlf.sh   | 4 ++--\n t/t9402-git-cvsserver-refs.sh   | 4 ++--\n t/test-lib.sh                   | 4 ++--\n 7 files changed, 12 insertions(+), 14 deletions(-)\n\ndiff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh\nindex 2fde2353fd..07d86ea244 100644\n--- a/t/lib-git-svn.sh\n+++ b/t/lib-git-svn.sh\n@@ -15,8 +15,7 @@ GIT_SVN_DIR=$GIT_DIR/svn/refs/remotes/git-svn\n SVN_TREE=$GIT_SVN_DIR/svn-tree\n test_set_port SVNSERVE_PORT\n \n-svn >/dev/null 2>&1\n-if test $? -ne 1\n+if ! svn help >/dev/null 2>&1\n then\n \tskip_all='skipping git svn tests, svn not found'\n \ttest_done\ndiff --git a/t/lib-httpd.sh b/t/lib-httpd.sh\nindex 4c76e813e3..fc646447d5 100644\n--- a/t/lib-httpd.sh\n+++ b/t/lib-httpd.sh\n@@ -235,11 +235,10 @@ start_httpd() {\n \n \ttest_atexit stop_httpd\n \n-\t\"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n+\tif ! \"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n \t\t-f \"$TEST_PATH/apache.conf\" $HTTPD_PARA \\\n \t\t-c \"Listen 127.0.0.1:$LIB_HTTPD_PORT\" -k start \\\n \t\t>&3 2>&4\n-\tif test $? -ne 0\n \tthen\n \t\tcat \"$HTTPD_ROOT_PATH\"/error.log >&4 2>/dev/null\n \t\ttest_skip_or_die GIT_TEST_HTTPD \"web server setup failed\"\ndiff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh\nindex 14cbe96527..581cf3d28f 100755\n--- a/t/t9200-git-cvsexportcommit.sh\n+++ b/t/t9200-git-cvsexportcommit.sh\n@@ -11,8 +11,7 @@ if ! test_have_prereq PERL; then\n \ttest_done\n fi\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git cvsexportcommit tests, cvs not found'\n     test_done\ndiff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh\nindex e499c7f955..4b45398bab 100755\n--- a/t/t9400-git-cvsserver-server.sh\n+++ b/t/t9400-git-cvsserver-server.sh\n@@ -17,12 +17,13 @@ if ! test_have_prereq PERL; then\n \tskip_all='skipping git cvsserver tests, perl not available'\n \ttest_done\n fi\n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git-cvsserver tests, cvs not found'\n     test_done\n fi\n+\n perl -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {\n     skip_all='skipping git-cvsserver tests, Perl SQLite interface unavailable'\n     test_done\ndiff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh\nindex a34805acdc..6b4cbb1651 100755\n--- a/t/t9401-git-cvsserver-crlf.sh\n+++ b/t/t9401-git-cvsserver-crlf.sh\n@@ -60,12 +60,12 @@ check_status_options() {\n     return $stat\n }\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git-cvsserver tests, cvs not found'\n     test_done\n fi\n+\n if ! test_have_prereq PERL\n then\n     skip_all='skipping git-cvsserver tests, perl not available'\ndiff --git a/t/t9402-git-cvsserver-refs.sh b/t/t9402-git-cvsserver-refs.sh\nindex 2ee41f9443..65f2ceedec 100755\n--- a/t/t9402-git-cvsserver-refs.sh\n+++ b/t/t9402-git-cvsserver-refs.sh\n@@ -68,12 +68,12 @@ check_diff() {\n \n #########\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n \tskip_all='skipping git-cvsserver tests, cvs not found'\n \ttest_done\n fi\n+\n if ! test_have_prereq PERL\n then\n \tskip_all='skipping git-cvsserver tests, perl not available'\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 70fd3e9baf..81380fe978 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -143,8 +143,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n ################################################################\n # It appears that people try to run tests without building...\n GIT_BINARY=\"${GIT_TEST_INSTALLED:-$GIT_BUILD_DIR}/git$X\"\n-\"$GIT_BINARY\" >/dev/null\n-if test $? != 1\n+\n+if ! \"$GIT_BINARY\" version >/dev/null\n then\n \tif test -n \"$GIT_TEST_INSTALLED\"\n \tthen\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541459","messageId":"20260413-b4-pks-tests-with-set-e-v1-7-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 07/12] t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:28Z","receivedAt":"2026-04-13T09:49:46Z","isPatch":true,"body":"Both `test_when_finished ()` and `test_atexit ()` build up a chain of\ncleanup commands by prepending each new command to the existing cleanup\nstring. To preserve the exit code of the test body across cleanup\nexecution, we append the following logic:\n\n    } && (exit \"$eval_ret\"); eval_ret=$?; ...\n\nThe intent of this is to run the cleanup block and then unconditionally\nrestore `eval_ret`. The original behaviour of this is is:\n\n   +------------------+---------+------------------------------------+\n   |test body         │ cleanup │ old behaviour                      │\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | pass    │ && taken -> (exit 0) -> eval_ret=0 |\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | fail    │ && not taken -> eval_ret=$?        |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | pass    │ && taken -> (exit 1) -> eval_ret=1 |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | fail    | && not taken -> eval_ret=$?        |\n   +------------------+---------+------------------------------------+\n\nThis logic will start to fail once we enable `set -e`. When `$eval_ret`\nis non-zero, the subshell we create will fail, and with `set -e` we'll\nthus bail out without evaluating the logic after the semicolon.\n\nFix this issue by instead using `|| eval_ret=\\$?; ...`. Besides being\na bit simpler, it also retains the original behaviour:\n\n   +------------------+---------+------------------------------------+\n   |test body         │ cleanup │ old behaviour                      │\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | pass    │ || not taken -> eval_ret unchanged |\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | fail    │ || taken -> eval_ret=$?            |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | pass    │ || not taken -> eval_ret unchanged |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | fail    | || taken -> eval_ret=$?            |\n   +------------------+---------+------------------------------------+\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/test-lib-functions.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex f8bc77619b..0eac676109 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1516,7 +1516,7 @@ test_when_finished () {\n \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n \tBUG \"test_when_finished does nothing in a subshell\"\n \ttest_cleanup=\"{ $*\n-\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_cleanup\"\n+\t\t} || eval_ret=\\$?; $test_cleanup\"\n }\n \n # This function can be used to schedule some commands to be run\n@@ -1544,7 +1544,7 @@ test_atexit () {\n \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n \tBUG \"test_atexit does nothing in a subshell\"\n \ttest_atexit_cleanup=\"{ $*\n-\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_atexit_cleanup\"\n+\t\t} || eval_ret=\\$?; $test_atexit_cleanup\"\n }\n \n # Deprecated wrapper for \"git init\", use \"git init\" directly instead\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541460","messageId":"20260413-b4-pks-tests-with-set-e-v1-8-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 08/12] t0008: silence error in subshell when using `grep -v`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:29Z","receivedAt":"2026-04-13T09:49:49Z","isPatch":true,"body":"In t0008 we use `grep -v` in a subshell, but expect that this command\nwill sometimes not match anything. This would cause grep(1) to return an\nerror code, but given that we don't run with `set -e` we swallow this\nerror.\n\nWe're about to enable `set -e`. Prepare for this by ignoring any errors.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0008-ignores.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh\nindex e716b5cdfa..1218005b54 100755\n--- a/t/t0008-ignores.sh\n+++ b/t/t0008-ignores.sh\n@@ -122,8 +122,8 @@ test_expect_success_multiple () {\n \tfi\n \ttestname=\"$1\" expect_all=\"$2\" code=\"$3\"\n \n-\texpect_verbose=$( echo \"$expect_all\" | grep -v '^::\t' )\n-\texpect=$( echo \"$expect_verbose\" | sed -e 's/.*\t//' )\n+\texpect_verbose=$(echo \"$expect_all\" | grep -v '^::\t' || true)\n+\texpect=$(echo \"$expect_verbose\" | sed -e 's/.*\t//')\n \n \ttest_expect_success $prereq \"$testname${no_index_opt:+ with $no_index_opt}\" '\n \t\texpect \"$expect\" &&\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541461","messageId":"20260413-b4-pks-tests-with-set-e-v1-9-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 09/12] t1301: don't fail in case setfacl(1) doesn't exist or fails","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:30Z","receivedAt":"2026-04-13T09:49:51Z","isPatch":true,"body":"In t1301 we're trying to remove any potentially-existing default ACLs\nthat might exist on the transh directory by executing setfacl(1).\nAccording to 8ed0a740dd (t1301-shared-repo.sh: don't let a default ACL\ninterfere with the test, 2008-10-16), this is done because we play\naround with permissions and umasks in this test suite.\n\nThe setfacl(1) binary may not exist on some systems though, even though\ntests ultimately still pass. This doesn't matter currently, but will\ncause the test to fail once we start running with `set -e`. Silence such\nfailures by ignoring failures here.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t1301-shared-repo.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/t1301-shared-repo.sh b/t/t1301-shared-repo.sh\nindex 630a47af21..7f920d7b9e 100755\n--- a/t/t1301-shared-repo.sh\n+++ b/t/t1301-shared-repo.sh\n@@ -12,7 +12,7 @@ TEST_CREATE_REPO_NO_TEMPLATE=1\n . ./test-lib.sh\n \n # Remove a default ACL from the test dir if possible.\n-setfacl -k . 2>/dev/null\n+setfacl -k . 2>/dev/null || true\n \n # User must have read permissions to the repo -> failure on --shared=0400\n test_expect_success 'shared = 0400 (faulty permission u-w)' '\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541462","messageId":"20260413-b4-pks-tests-with-set-e-v1-10-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 10/12] t6002: fix use of `expr` with `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:31Z","receivedAt":"2026-04-13T09:49:54Z","isPatch":true,"body":"In `test_bisection_diff ()` we use `expr` to perform some math. This\ncommand has some gotchas though in that it will only return success when\nthe result is neither null nor zero. In some of our cases though it\nactually _is_ zero, and that will cause the expressions to fail once we\nenable `set -e`.\n\nPrepare for this change by instead using `$(( ))`, which doesn't have\nthe same issue. While at it, modernize the function a tiny bit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t6002-rev-list-bisect.sh | 17 ++++++++++-------\n 1 file changed, 10 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t6002-rev-list-bisect.sh b/t/t6002-rev-list-bisect.sh\nindex daa009c9a1..f2de40b5ed 100755\n--- a/t/t6002-rev-list-bisect.sh\n+++ b/t/t6002-rev-list-bisect.sh\n@@ -27,13 +27,16 @@ test_bisection_diff()\n \t# Test if bisection size is close to half of list size within\n \t# tolerance.\n \t#\n-\t_bisect_err=$(expr $_list_size - $_bisection_size \\* 2)\n-\ttest \"$_bisect_err\" -lt 0 && _bisect_err=$(expr 0 - $_bisect_err)\n-\t_bisect_err=$(expr $_bisect_err / 2) ; # floor\n-\n-\ttest_expect_success \\\n-\t\"bisection diff $_bisect_option $_head $* <= $_max_diff\" \\\n-\t'test $_bisect_err -le $_max_diff'\n+\t_bisect_err=$(($_list_size - $_bisection_size * 2))\n+\tif test \"$_bisect_err\" -lt 0\n+\tthen\n+\t\t_bisect_err=$((0 - $_bisect_err))\n+\tfi\n+\t_bisect_err=$(($_bisect_err / 2)) ; # floor\n+\n+\ttest_expect_success \"bisection diff $_bisect_option $_head $* <= $_max_diff\" '\n+\t\ttest $_bisect_err -le $_max_diff\n+\t'\n }\n \n date >path0\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541463","messageId":"20260413-b4-pks-tests-with-set-e-v1-11-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 11/12] t9902: fix use of `read` with `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:32Z","receivedAt":"2026-04-13T09:49:56Z","isPatch":true,"body":"In t9902 we're using the `read` builtin to read some values into a\nvariable. This is done by using `-d \"\"`, which cause us to read until\nthe end of the heredoc. There is a gotcha though: when the delimiter\nisn't found at all, then the read builtin will return an error. This\nhasn't been an issue until now as we didn't run with `set -e`, but\nthat'll change in a subsequent commit.\n\nPrepare for this change by silencing the error.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t9902-completion.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 2f9a597ec7..e3a7df7691 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -590,7 +590,7 @@ test_expect_success '__gitcomp - doesnt fail because of invalid variable name' '\n \t__gitcomp \"$invalid_variable_name\"\n '\n \n-read -r -d \"\" refs <<-\\EOF\n+read -r -d \"\" refs <<-\\EOF || :\n main\n maint\n next\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541464","messageId":"20260413-b4-pks-tests-with-set-e-v1-12-5b83763a0e84@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH 12/12] t: detect errors outside of test cases","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-13T09:49:33Z","receivedAt":"2026-04-13T09:49:58Z","isPatch":true,"body":"We have recently merged a patch series that had a simple misspelling of\n`test_expect_success`. Instead of making our tests fail though, this\ntypo went completely undetected and all of our tests passed, which is of\ncourse unfortunate. This is a more general issue with our test suite:\nall commands that run outside of a specific test case can fail, and if\nwe don't explicitly check for such failure then this failure will be\nsilently ignored.\n\nImprove the status quo by enabling the errexit option so that any such\nunchecked failures will cause us to abort immediately.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/test-lib.sh | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 81380fe978..c493e3c768 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -15,6 +15,10 @@\n # You should have received a copy of the GNU General Public License\n # along with this program.  If not, see https://www.gnu.org/licenses/ .\n \n+# Enable the use of errexit so that any unexpected failures will cause us to\n+# abort tests, even when outside of a specific test case.\n+set -e\n+\n # Test the binaries we have just built.  The tests are kept in\n # t/ subdirectory and are run in 'trash directory' subdirectory.\n if test -z \"$TEST_DIRECTORY\"\n\n-- \n2.54.0.rc0.707.g0fbf48f4d6.dirty\n\n"},{"id":"541483","messageId":"xmqqldeqkesa.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-1-5b83763a0e84@pks.im","subject":"Re: [PATCH 01/12] t: prepare `test_match_signal ()` calls for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T16:26:13Z","receivedAt":"2026-04-13T16:26:16Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We have a couple of calls to `test_match_signal ()` where we execute a\n> Git command and expect it to die with a specific signal. These calls\n> will essentially execute the process in a subshell via `foo; echo $?`,\n> but as we expect `foo` to fail this will cause the overall subshell to\n> fail once we `set -e`.\n>\n> Fix this issue by using `foo || echo $?` instead.\n\nHmph, if the 'foo' command that is expected to fail succeeds by a\nbug, we won't see 0 on the standard output anymore.  Comparing 13\nwith 0 or 13 with an empty string \"\" would fail either way, so that\nmay not give us a practical difference, but this somehow leaves a\nfoul small in my nose.\n\nThe technique used in <20260325062114.2067946-2-gitster@pobox.com>\nwould give us something ugly like\n\n-\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((st=0; large_git || st=$?; echo $st 1>&3) | :) 3>&1 ) &&\n\nand it ensures that we give 0 when we succeed, but I am not sure if\nthis is worth doing.\n\nOr perhaps\n\n-\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((large_git || echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=${OUT:-0} &&\n\nI dunno.\n\nIf \"large_git\" somehow writes into file descriptor #3, then OUT will\nbe contaminated with something other than $?, so the additional \"if\nempty substitute with 0\" would not work without doing something like\n\n-\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((large_git 3>/dev/null || echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=${OUT:-0} &&\n\nI guess.  For that matter, if large_git spits out \"13\" to its file\ndescriptor #3, I do not know what would have happened ;-)\n\n>  test_expect_success !MINGW 'a constipated git dies with SIGPIPE' '\n> -\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n> +\tOUT=$( ((large_git || echo $? 1>&3) | :) 3>&1 ) &&\n>  \ttest_match_signal 13 \"$OUT\"\n>  '\n>  \n>  test_expect_success !MINGW 'a constipated git dies with SIGPIPE even if parent ignores it' '\n> -\tOUT=$( ((trap \"\" PIPE && large_git; echo $? 1>&3) | :) 3>&1 ) &&\n> +\tOUT=$( ((trap \"\" PIPE && large_git || echo $? 1>&3) | :) 3>&1 ) &&\n>  \ttest_match_signal 13 \"$OUT\"\n>  '\n>  \n> diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\n> index 1f16e6b522..8e68a00dcb 100755\n> --- a/t/t3600-rm.sh\n> +++ b/t/t3600-rm.sh\n> @@ -260,7 +260,7 @@ test_expect_success 'choking \"git rm\" should not let it die with cruft (induce S\n>  \n>  test_expect_success !MINGW 'choking \"git rm\" should not let it die with cruft (induce and check SIGPIPE)' '\n>  \tchoke_git_rm_setup &&\n> -\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\"; echo $? 1>&3) | :) 3>&1 ) &&\n> +\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\" || echo $? 1>&3) | :) 3>&1 ) &&\n>  \ttest_match_signal 13 \"$OUT\" &&\n>  \ttest_path_is_missing .git/index.lock\n>  '\n"},{"id":"541484","messageId":"xmqqcy02kefc.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-2-5b83763a0e84@pks.im","subject":"Re: [PATCH 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T16:33:59Z","receivedAt":"2026-04-13T16:34:02Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The helper function `test_must_fail ()` executes a specific Git command\n> that may or may not fail in a specific way. This is done by executing\n> the command in question and then comparing its exit code against a set\n> of conditions.\n>\n> This works, but once we run our test suite with `set -e` we may bail out\n> of `test_must_fail ()` early in case the command actually fails, even\n> though we expect it to fail. Prepare for this change by marking the\n> command part of a condition.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/test-lib-functions.sh | 8 ++++++--\n>  1 file changed, 6 insertions(+), 2 deletions(-)\n>\n> diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\n> index f3af10fb7e..f8bc77619b 100644\n> --- a/t/test-lib-functions.sh\n> +++ b/t/test-lib-functions.sh\n> @@ -1195,8 +1195,12 @@ test_must_fail () {\n>  \t\techo >&7 \"test_must_fail: only 'git' is allowed: $*\"\n>  \t\treturn 1\n>  \tfi\n> -\t\"$@\" 2>&7\n> -\texit_code=$?\n> +\tif \"$@\" 2>&7\n> +\tthen\n> +\t\texit_code=0\n> +\telse\n> +\t\texit_code=$?\n> +\tfi\n\nThis is obvious and clear.  Alternatively\n\n\texit_code=0; \"$@\" 2>&7 || exit_code=$?\n\nwould be more assuring to readers who (with less fluency in Bourne\nshells) wonder, upon seeing the else clause, how far \"$?\"  traveled\nand it still holds the status of \"$@\".\n\n>  \tif test $exit_code -eq 0 && ! list_contains \"$_test_ok\" success\n>  \tthen\n>  \t\techo >&4 \"test_must_fail: command succeeded: $*\"\n"},{"id":"541485","messageId":"xmqq8qaqkdjc.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-3-5b83763a0e84@pks.im","subject":"Re: [PATCH 03/12] t: prepare `stop_git_daemon ()` for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T16:53:11Z","receivedAt":"2026-04-13T16:53:14Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We have a couple of calls to `stop_git_daemon ()` outside of specific\n> test cases that will kill a backgrounded git-daemon(1) process and\n> expect the process with a specific error code. While these function\n> calls do end up killing git-daemon(1), the error handling we have in\n> those contexts is basically ineffective. So while we expect the process\n> to exit with a specific error code, we will just continue with any error\n> in case it doesn't.\n>\n> This will change once we enable `set -e` in a subsequent commit. There's\n> two issues though that will make this _always_ fail:\n>\n>   - Our call to `wait` is expected to fail, but because it's not part of\n>     a condition it will cause us to bail out immediately with `set -e`.\n>\n>   - We try to kill git-daemon(1) a second time via the pidfile. We can\n>     generally expect that this is the same PID though as we had in the\n>     \"GIT_DAEMON_PID\" environment variable, and thus it's more likely\n>     than not that we have already killed it, and the call to kill will\n>     fail.\n>\n> Prepare for this change by making the call to `wait` part of a condition\n> and by silencing failures of the second call to `kill`.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/lib-git-daemon.sh | 13 ++++++++++---\n>  1 file changed, 10 insertions(+), 3 deletions(-)\n\nThe same comment as [02/12] applies to the early part.\n\nIgnoring the exit status from \"kill\" in case that the daemon has\nalready exited with \" || :\" totally makes sense.\n\n> diff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh\n> index e62569222b..91bcdd0bf7 100644\n> --- a/t/lib-git-daemon.sh\n> +++ b/t/lib-git-daemon.sh\n> @@ -85,14 +85,21 @@ stop_git_daemon() {\n>  \n>  \t# kill git-daemon child of git\n>  \tsay >&3 \"Stopping git daemon ...\"\n> +\n>  \tkill \"$GIT_DAEMON_PID\"\n> -\twait \"$GIT_DAEMON_PID\" >&3 2>&4\n> -\tret=$?\n> +\tif wait \"$GIT_DAEMON_PID\" >&3 2>&4\n> +\tthen\n> +\t\tret=0\n> +\telse\n> +\t\tret=$?\n> +\tfi\n> +\n>  \tif ! test_match_signal 15 $ret\n>  \tthen\n>  \t\terror \"git daemon exited with status: $ret\"\n>  \tfi\n> -\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null\n> +\n> +\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null || :\n>  \tGIT_DAEMON_PID=\n>  \trm -f git_daemon_output \"$GIT_DAEMON_PIDFILE\"\n>  }\n"},{"id":"541486","messageId":"xmqq4ilekd9j.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-4-5b83763a0e84@pks.im","subject":"Re: [PATCH 04/12] t: prepare `git config --unset` calls for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T16:59:04Z","receivedAt":"2026-04-13T16:59:06Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We have a couple of calls to `git config --unset` that ultimately end up\n> as no-ops as the configuration variables aren't set (anymore) in the\n> first place. These calls are mostly intended to recover unconditionally\n> from tests that may have executed only partially, but they'll ultimately\n> fail during a normal test run.\n>\n> This hasn't been a problem until now as we aren't running tests with\n> `set -e`. This is about to change though, so let's silence the case\n> where we cannot unset the config keys.\n\nThey all look good.  A bare \"unset\" in the shell also can fail, and\nthat is where our sane_unset comes from.  It is where the \"it is an\nerror to try unsetting a config\" comes from, but given that only\nsmall number of places we need these changes to, it wouldn't not be\nworth adding \"git config --sane-unset\" that does not fail ;-)\n\nLooking good.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/t4032-diff-inter-hunk-context.sh | 2 +-\n>  t/t7508-status.sh                  | 4 ++--\n>  t/t9138-git-svn-authors-prog.sh    | 4 ++--\n>  3 files changed, 5 insertions(+), 5 deletions(-)\n>\n> diff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\n> index bada0cbd32..c98eb6abb2 100755\n> --- a/t/t4032-diff-inter-hunk-context.sh\n> +++ b/t/t4032-diff-inter-hunk-context.sh\n> @@ -17,7 +17,7 @@ f() {\n>  \n>  t() {\n>  \tuse_config=\n> -\tgit config --unset diff.interHunkContext\n> +\tgit config --unset diff.interHunkContext || :\n>  \n>  \tcase $# in\n>  \t4) hunks=$4; cmd=\"diff -U$3\";;\n> diff --git a/t/t7508-status.sh b/t/t7508-status.sh\n> index a5e21bf8bf..1167b835a4 100755\n> --- a/t/t7508-status.sh\n> +++ b/t/t7508-status.sh\n> @@ -773,8 +773,8 @@ test_expect_success TTY 'status --porcelain ignores color.status' '\n>  '\n>  \n>  # recover unconditionally from color tests\n> -git config --unset color.status\n> -git config --unset color.ui\n> +git config --unset color.status || :\n> +git config --unset color.ui || :\n>  \n>  test_expect_success 'status --porcelain respects -b' '\n>  \n> diff --git a/t/t9138-git-svn-authors-prog.sh b/t/t9138-git-svn-authors-prog.sh\n> index 784ec7fc2d..5bb38cb23a 100755\n> --- a/t/t9138-git-svn-authors-prog.sh\n> +++ b/t/t9138-git-svn-authors-prog.sh\n> @@ -68,8 +68,8 @@ test_expect_success 'authors-file overrode authors-prog' '\n>  \t)\n>  '\n>  \n> -git --git-dir=x/.git config --unset svn.authorsfile\n> -git --git-dir=x/.git config --unset svn.authorsprog\n> +git --git-dir=x/.git config --unset svn.authorsfile || :\n> +git --git-dir=x/.git config --unset svn.authorsprog || :\n>  \n>  test_expect_success 'authors-prog imported user without email' '\n>  \tsvn mkdir -m gg --username gg-hermit \"$svnrepo\"/gg &&\n"},{"id":"541487","messageId":"xmqqy0iqiyfp.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-5-5b83763a0e84@pks.im","subject":"Re: [PATCH 05/12] t: prepare conditional test execution for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T17:04:42Z","receivedAt":"2026-04-13T17:04:44Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We have some test in our test suite where we use the pattern of\n> `test ... && test_expect_succeess` to conditionally execute a test. The\n> problem is that when we decide to not execute the test, we'll indeed\n> skip the test, but the overall statement will also be unsuccessful. This\n> will become a problem once we enable `set -e`.\n>\n> Prepare for this future by turning this into a proper conditional, which\n> is also a bit easier to read overall.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/t4032-diff-inter-hunk-context.sh | 12 +++++++-----\n>  t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n>  2 files changed, 20 insertions(+), 16 deletions(-)\n\nUnlike the ones that involve $?, I very much prefer the explicit\nuses of if/then/else in these changes.  Compared to a potential\nalternative to rewrite the existing\n\n\tcheck &&\n\ttest_expect_success ...\n\ninto\n\n\t! check ||\n\ttest_expect_success ...\n\nit is much more clear to explicitly say\n\n\tif check\n\tthen\n\t\ttest_expect_success ...\n\tfi\n\n> diff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\n> index c98eb6abb2..2d216fb70f 100755\n> --- a/t/t4032-diff-inter-hunk-context.sh\n> +++ b/t/t4032-diff-inter-hunk-context.sh\n> @@ -40,11 +40,13 @@ t() {\n>  \t\ttest $(git $cmd $file | grep '^@@ ' | wc -l) = $hunks\n>  \t\"\n>  \n> -\ttest -f $expected &&\n> -\ttest_expect_success \"$label: check output\" \"\n> -\t\tgit $cmd $file | grep -v '^index ' >actual &&\n> -\t\ttest_cmp $expected actual\n> -\t\"\n> +\tif test -f $expected\n> +\tthen\n> +\t\ttest_expect_success \"$label: check output\" \"\n> +\t\t\tgit $cmd $file | grep -v '^index ' >actual &&\n> +\t\t\ttest_cmp $expected actual\n> +\t\t\"\n> +\tfi\n>  }\n>  \n>  cat <<EOF >expected.f1.0.1 || exit 1\n> diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\n> index f512eed278..8cc86522b2 100755\n> --- a/t/t7450-bad-git-dotfiles.sh\n> +++ b/t/t7450-bad-git-dotfiles.sh\n> @@ -220,17 +220,19 @@ check_dotx_symlink () {\n>  \t\t)\n>  \t'\n>  \n> -\ttest -n \"$refuse_index\" &&\n> -\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n> -\t\ttest_must_fail \\\n> -\t\t\tgit -C $dir \\\n> -\t\t\t    -c core.protectntfs \\\n> -\t\t\t    -c core.protecthfs \\\n> -\t\t\t    read-tree $tree 2>err &&\n> -\t\tgrep \"invalid path.*$name\" err &&\n> -\t\tgit -C $dir ls-files -s >out &&\n> -\t\ttest_must_be_empty out\n> -\t'\n> +\tif test -n \"$refuse_index\"\n> +\tthen\n> +\t\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n> +\t\t\ttest_must_fail \\\n> +\t\t\t\tgit -C $dir \\\n> +\t\t\t\t    -c core.protectntfs \\\n> +\t\t\t\t    -c core.protecthfs \\\n> +\t\t\t\t    read-tree $tree 2>err &&\n> +\t\t\tgrep \"invalid path.*$name\" err &&\n> +\t\t\tgit -C $dir ls-files -s >out &&\n> +\t\t\ttest_must_be_empty out\n> +\t\t'\n> +\tfi\n>  }\n>  \n>  check_dotx_symlink gitmodules vanilla .gitmodules\n"},{"id":"541488","messageId":"xmqqtsteiy7s.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-6-5b83763a0e84@pks.im","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T17:09:27Z","receivedAt":"2026-04-13T17:09:30Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Several of our tests verify whether a certain binary can be executed,\n> potentially skipping tests in case we cannot, for example because the\n> binary doesn't exist. In those cases we often run the binary outside of\n> any conditionally.\n>\n> This will start to fail once we enable `set -e`, as that will cause us\n> to bail out the test immediately. Improve these tests by executing them\n> inside of a conditional instead.\n\nOK.  \"svn help\" and \"cvs version\" do exit with status 0 and the\nrewrites that use them make sense.  I wonder if we can do something\nsimilar to \"git\" instead of relying on \"git<RETURN>\" to exit with 1,\nperhaps ...\n\n    $ git version >/dev/null; echo $?\n    0\n\n... by using \"git version\" in the test-lib.sh change?\n\nOther than that looking very good.\n\n\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/lib-git-svn.sh                | 3 +--\n>  t/lib-httpd.sh                  | 3 +--\n>  t/t9200-git-cvsexportcommit.sh  | 3 +--\n>  t/t9400-git-cvsserver-server.sh | 5 +++--\n>  t/t9401-git-cvsserver-crlf.sh   | 4 ++--\n>  t/t9402-git-cvsserver-refs.sh   | 4 ++--\n>  t/test-lib.sh                   | 4 ++--\n>  7 files changed, 12 insertions(+), 14 deletions(-)\n>\n> diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh\n> index 2fde2353fd..07d86ea244 100644\n> --- a/t/lib-git-svn.sh\n> +++ b/t/lib-git-svn.sh\n> @@ -15,8 +15,7 @@ GIT_SVN_DIR=$GIT_DIR/svn/refs/remotes/git-svn\n>  SVN_TREE=$GIT_SVN_DIR/svn-tree\n>  test_set_port SVNSERVE_PORT\n>  \n> -svn >/dev/null 2>&1\n> -if test $? -ne 1\n> +if ! svn help >/dev/null 2>&1\n>  then\n>  \tskip_all='skipping git svn tests, svn not found'\n>  \ttest_done\n> diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh\n> index 4c76e813e3..fc646447d5 100644\n> --- a/t/lib-httpd.sh\n> +++ b/t/lib-httpd.sh\n> @@ -235,11 +235,10 @@ start_httpd() {\n>  \n>  \ttest_atexit stop_httpd\n>  \n> -\t\"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n> +\tif ! \"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n>  \t\t-f \"$TEST_PATH/apache.conf\" $HTTPD_PARA \\\n>  \t\t-c \"Listen 127.0.0.1:$LIB_HTTPD_PORT\" -k start \\\n>  \t\t>&3 2>&4\n> -\tif test $? -ne 0\n>  \tthen\n>  \t\tcat \"$HTTPD_ROOT_PATH\"/error.log >&4 2>/dev/null\n>  \t\ttest_skip_or_die GIT_TEST_HTTPD \"web server setup failed\"\n> diff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh\n> index 14cbe96527..581cf3d28f 100755\n> --- a/t/t9200-git-cvsexportcommit.sh\n> +++ b/t/t9200-git-cvsexportcommit.sh\n> @@ -11,8 +11,7 @@ if ! test_have_prereq PERL; then\n>  \ttest_done\n>  fi\n>  \n> -cvs >/dev/null 2>&1\n> -if test $? -ne 1\n> +if ! cvs version >/dev/null 2>&1\n>  then\n>      skip_all='skipping git cvsexportcommit tests, cvs not found'\n>      test_done\n> diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh\n> index e499c7f955..4b45398bab 100755\n> --- a/t/t9400-git-cvsserver-server.sh\n> +++ b/t/t9400-git-cvsserver-server.sh\n> @@ -17,12 +17,13 @@ if ! test_have_prereq PERL; then\n>  \tskip_all='skipping git cvsserver tests, perl not available'\n>  \ttest_done\n>  fi\n> -cvs >/dev/null 2>&1\n> -if test $? -ne 1\n> +\n> +if ! cvs version >/dev/null 2>&1\n>  then\n>      skip_all='skipping git-cvsserver tests, cvs not found'\n>      test_done\n>  fi\n> +\n>  perl -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {\n>      skip_all='skipping git-cvsserver tests, Perl SQLite interface unavailable'\n>      test_done\n> diff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh\n> index a34805acdc..6b4cbb1651 100755\n> --- a/t/t9401-git-cvsserver-crlf.sh\n> +++ b/t/t9401-git-cvsserver-crlf.sh\n> @@ -60,12 +60,12 @@ check_status_options() {\n>      return $stat\n>  }\n>  \n> -cvs >/dev/null 2>&1\n> -if test $? -ne 1\n> +if ! cvs version >/dev/null 2>&1\n>  then\n>      skip_all='skipping git-cvsserver tests, cvs not found'\n>      test_done\n>  fi\n> +\n>  if ! test_have_prereq PERL\n>  then\n>      skip_all='skipping git-cvsserver tests, perl not available'\n> diff --git a/t/t9402-git-cvsserver-refs.sh b/t/t9402-git-cvsserver-refs.sh\n> index 2ee41f9443..65f2ceedec 100755\n> --- a/t/t9402-git-cvsserver-refs.sh\n> +++ b/t/t9402-git-cvsserver-refs.sh\n> @@ -68,12 +68,12 @@ check_diff() {\n>  \n>  #########\n>  \n> -cvs >/dev/null 2>&1\n> -if test $? -ne 1\n> +if ! cvs version >/dev/null 2>&1\n>  then\n>  \tskip_all='skipping git-cvsserver tests, cvs not found'\n>  \ttest_done\n>  fi\n> +\n>  if ! test_have_prereq PERL\n>  then\n>  \tskip_all='skipping git-cvsserver tests, perl not available'\n> diff --git a/t/test-lib.sh b/t/test-lib.sh\n> index 70fd3e9baf..81380fe978 100644\n> --- a/t/test-lib.sh\n> +++ b/t/test-lib.sh\n> @@ -143,8 +143,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>  ################################################################\n>  # It appears that people try to run tests without building...\n>  GIT_BINARY=\"${GIT_TEST_INSTALLED:-$GIT_BUILD_DIR}/git$X\"\n> -\"$GIT_BINARY\" >/dev/null\n> -if test $? != 1\n> +\n> +if ! \"$GIT_BINARY\" version >/dev/null\n>  then\n>  \tif test -n \"$GIT_TEST_INSTALLED\"\n>  \tthen\n"},{"id":"541489","messageId":"xmqqpl42ixk6.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-7-5b83763a0e84@pks.im","subject":"Re: [PATCH 07/12] t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T17:23:37Z","receivedAt":"2026-04-13T17:23:41Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Both `test_when_finished ()` and `test_atexit ()` build up a chain of\n> cleanup commands by prepending each new command to the existing cleanup\n> string. To preserve the exit code of the test body across cleanup\n> execution, we append the following logic:\n>\n>     } && (exit \"$eval_ret\"); eval_ret=$?; ...\n>\n> The intent of this is to run the cleanup block and then unconditionally\n> restore `eval_ret`. The original behaviour of this is is:\n>\n>    +------------------+---------+------------------------------------+\n>    |test body         │ cleanup │ old behaviour                      │\n>    +------------------+---------+------------------------------------+\n>    │pass (eval_ret=0) | pass    │ && taken -> (exit 0) -> eval_ret=0 |\n>    +------------------+---------+------------------------------------+\n>    │pass (eval_ret=0) | fail    │ && not taken -> eval_ret=$?        |\n>    +------------------+---------+------------------------------------+\n>    │fail (eval_ret=1) | pass    │ && taken -> (exit 1) -> eval_ret=1 |\n>    +------------------+---------+------------------------------------+\n>    │fail (eval_ret=1) | fail    | && not taken -> eval_ret=$?        |\n>    +------------------+---------+------------------------------------+\n>\n> This logic will start to fail once we enable `set -e`. When `$eval_ret`\n> is non-zero, the subshell we create will fail, and with `set -e` we'll\n> thus bail out without evaluating the logic after the semicolon.\n>\n> Fix this issue by instead using `|| eval_ret=\\$?; ...`. Besides being\n> a bit simpler, it also retains the original behaviour:\n>\n>    +------------------+---------+------------------------------------+\n>    |test body         │ cleanup │ old behaviour                      │\n>    +------------------+---------+------------------------------------+\n>    │pass (eval_ret=0) | pass    │ || not taken -> eval_ret unchanged |\n>    +------------------+---------+------------------------------------+\n>    │pass (eval_ret=0) | fail    │ || taken -> eval_ret=$?            |\n>    +------------------+---------+------------------------------------+\n>    │fail (eval_ret=1) | pass    │ || not taken -> eval_ret unchanged |\n>    +------------------+---------+------------------------------------+\n>    │fail (eval_ret=1) | fail    | || taken -> eval_ret=$?            |\n>    +------------------+---------+------------------------------------+\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/test-lib-functions.sh | 4 ++--\n>  1 file changed, 2 insertions(+), 2 deletions(-)\n>\n> diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\n> index f8bc77619b..0eac676109 100644\n> --- a/t/test-lib-functions.sh\n> +++ b/t/test-lib-functions.sh\n> @@ -1516,7 +1516,7 @@ test_when_finished () {\n>  \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n>  \tBUG \"test_when_finished does nothing in a subshell\"\n>  \ttest_cleanup=\"{ $*\n> -\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_cleanup\"\n> +\t\t} || eval_ret=\\$?; $test_cleanup\"\n\nHmph, it seems this \"&& (exit $eval_ret)\" pattern has been with us\nforever since it was introduced at 3bf78867 (test-lib: Let tests\nspecify commands to be run at end of test, 2010-05-02), and survived\na slight modification to work around issues on FreeBSD done in\nb6b0afdc (test-lib: some shells do not let $? propagate into an\neval, 2010-05-06).  When a major part of test-lib.sh was split into\ntest-lib-functions.s at 12a29b1a (Move the user-facing test library\nto test-lib-functions.sh, 2012-02-17), this part was copied intact.\n\nDoes this interact well with the glitch b6b0afdc (test-lib: some\nshells do not let $? propagate into an eval, 2010-05-06) tried to\nwork around, by the way?\n\nThanks.\n\n>  }\n>  \n>  # This function can be used to schedule some commands to be run\n> @@ -1544,7 +1544,7 @@ test_atexit () {\n>  \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n>  \tBUG \"test_atexit does nothing in a subshell\"\n>  \ttest_atexit_cleanup=\"{ $*\n> -\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_atexit_cleanup\"\n> +\t\t} || eval_ret=\\$?; $test_atexit_cleanup\"\n>  }\n>  \n>  # Deprecated wrapper for \"git init\", use \"git init\" directly instead\n"},{"id":"541490","messageId":"xmqqjyuaixcv.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-8-5b83763a0e84@pks.im","subject":"Re: [PATCH 08/12] t0008: silence error in subshell when using `grep -v`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T17:28:00Z","receivedAt":"2026-04-13T17:28:03Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> In t0008 we use `grep -v` in a subshell, but expect that this command\n> will sometimes not match anything. This would cause grep(1) to return an\n> error code, but given that we don't run with `set -e` we swallow this\n> error.\n>\n> We're about to enable `set -e`. Prepare for this by ignoring any errors.\n\nIt is curious that true is explicitly spelled out, unlike the\nearlier steps in the series that used \":\" in \"|| :\".\n\nLoss of extra spaces around the statement inside $() is a good\ntouch.\n\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/t0008-ignores.sh | 4 ++--\n>  1 file changed, 2 insertions(+), 2 deletions(-)\n>\n> diff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh\n> index e716b5cdfa..1218005b54 100755\n> --- a/t/t0008-ignores.sh\n> +++ b/t/t0008-ignores.sh\n> @@ -122,8 +122,8 @@ test_expect_success_multiple () {\n>  \tfi\n>  \ttestname=\"$1\" expect_all=\"$2\" code=\"$3\"\n>  \n> -\texpect_verbose=$( echo \"$expect_all\" | grep -v '^::\t' )\n> -\texpect=$( echo \"$expect_verbose\" | sed -e 's/.*\t//' )\n> +\texpect_verbose=$(echo \"$expect_all\" | grep -v '^::\t' || true)\n> +\texpect=$(echo \"$expect_verbose\" | sed -e 's/.*\t//')\n>  \n>  \ttest_expect_success $prereq \"$testname${no_index_opt:+ with $no_index_opt}\" '\n>  \t\texpect \"$expect\" &&\n"},{"id":"541491","messageId":"xmqqfr4yixah.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-12-5b83763a0e84@pks.im","subject":"Re: [PATCH 12/12] t: detect errors outside of test cases","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T17:29:26Z","receivedAt":"2026-04-13T17:29:28Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We have recently merged a patch series that had a simple misspelling of\n> `test_expect_success`. Instead of making our tests fail though, this\n> typo went completely undetected and all of our tests passed, which is of\n> course unfortunate. This is a more general issue with our test suite:\n> all commands that run outside of a specific test case can fail, and if\n> we don't explicitly check for such failure then this failure will be\n> silently ignored.\n>\n> Improve the status quo by enabling the errexit option so that any such\n> unchecked failures will cause us to abort immediately.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/test-lib.sh | 4 ++++\n>  1 file changed, 4 insertions(+)\n>\n> diff --git a/t/test-lib.sh b/t/test-lib.sh\n> index 81380fe978..c493e3c768 100644\n> --- a/t/test-lib.sh\n> +++ b/t/test-lib.sh\n> @@ -15,6 +15,10 @@\n>  # You should have received a copy of the GNU General Public License\n>  # along with this program.  If not, see https://www.gnu.org/licenses/ .\n>  \n> +# Enable the use of errexit so that any unexpected failures will cause us to\n> +# abort tests, even when outside of a specific test case.\n> +set -e\n\nYay.  Very nice.\n\n>  # Test the binaries we have just built.  The tests are kept in\n>  # t/ subdirectory and are run in 'trash directory' subdirectory.\n>  if test -z \"$TEST_DIRECTORY\"\n"},{"id":"541500","messageId":"xmqqy0iqfsuu.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"Re: [PATCH 00/12] t: detect errors outside of test cases","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T21:33:29Z","receivedAt":"2026-04-13T21:33:32Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> this is a follow-up to the recent discussion we had around `set -e` to\n> make our tests more robust and basically supersedes Junio's [1].\n>\n> I've tested the patches with both Bash and Dash, and all tests are\n> passing on my machine with both of them. CI seems to be happy, as\n> well. But I would expect that this change probably has some fallout,\n> even though I hope that it's generally going to be small and contained.\n>\n> This series is based on 8c9303b1ff (Merge branch\n> 'jc/no-writev-does-not-work', 2026-04-10).\n\nThis unfortunately breaks svn related tests big time for me, as I\ndeliberately do not install Perl modules that are needed for git-svn.\n\n\n    $ cd t && sh t9152-svn-empty-dirs-after-gc.sh -i -v\n    Initialized empty Git repository in /home/gitster/git/t/trash directory.t9152-svn-empty-dirs-after-gc/.git/\n    Can't locate SVN/Core.pm in @INC (you may need to install the SVN::Core module) (@INC entries checked: /etc/perl /usr/local/lib/x86_64-linux-gnu/perl/5.40.1 /usr/local/share/perl/5.40.1 /usr/lib/x86_64-linux-gnu/perl5/5.40 /usr/share/perl5 /usr/lib/x86_64-linux-gnu/perl-base /usr/lib/x86_64-linux-gnu/perl/5.40 /usr/share/perl/5.40 /usr/local/lib/site_perl) at -e line 2.\n    BEGIN failed--compilation aborted at -e line 2.\n    FATAL: Unexpected exit with code 2\n\n\n    $ cd t && ls -d trash\\ directory.t*\n    trash directory.t9100-git-svn-basic\n    trash directory.t9101-git-svn-props\n    trash directory.t9102-git-svn-deep-rmdir\n    trash directory.t9103-git-svn-tracked-directory-removed\n    trash directory.t9104-git-svn-follow-parent\n    trash directory.t9105-git-svn-commit-diff\n    trash directory.t9106-git-svn-commit-diff-clobber\n    trash directory.t9107-git-svn-migrate\n    trash directory.t9108-git-svn-glob\n    trash directory.t9109-git-svn-multi-glob\n    trash directory.t9110-git-svn-use-svm-props\n    trash directory.t9111-git-svn-use-svnsync-props\n    trash directory.t9112-git-svn-md5less-file\n    trash directory.t9113-git-svn-dcommit-new-file\n    trash directory.t9114-git-svn-dcommit-merge\n    trash directory.t9115-git-svn-dcommit-funky-renames\n    trash directory.t9116-git-svn-log\n    trash directory.t9117-git-svn-init-clone\n    trash directory.t9118-git-svn-funky-branch-names\n    trash directory.t9119-git-svn-info\n    trash directory.t9120-git-svn-clone-with-percent-escapes\n    trash directory.t9121-git-svn-fetch-renamed-dir\n    trash directory.t9122-git-svn-author\n    trash directory.t9123-git-svn-rebuild-with-rewriteroot\n    trash directory.t9124-git-svn-dcommit-auto-props\n    trash directory.t9125-git-svn-multi-glob-branch-names\n    trash directory.t9126-git-svn-follow-deleted-readded-directory\n    trash directory.t9127-git-svn-partial-rebuild\n    trash directory.t9128-git-svn-cmd-branch\n    trash directory.t9129-git-svn-i18n-commitencoding\n    trash directory.t9130-git-svn-authors-file\n    trash directory.t9131-git-svn-empty-symlink\n    trash directory.t9132-git-svn-broken-symlink\n    trash directory.t9133-git-svn-nested-git-repo\n    trash directory.t9134-git-svn-ignore-paths\n    trash directory.t9135-git-svn-moved-branch-empty-file\n    trash directory.t9136-git-svn-recreated-branch-empty-file\n    trash directory.t9137-git-svn-dcommit-clobber-series\n    trash directory.t9138-git-svn-authors-prog\n    trash directory.t9139-git-svn-non-utf8-commitencoding\n    trash directory.t9140-git-svn-reset\n    trash directory.t9141-git-svn-multiple-branches\n    trash directory.t9142-git-svn-shallow-clone\n    trash directory.t9143-git-svn-gc\n    trash directory.t9144-git-svn-old-rev_map\n    trash directory.t9145-git-svn-master-branch\n    trash directory.t9146-git-svn-empty-dirs\n    trash directory.t9147-git-svn-include-paths\n    trash directory.t9148-git-svn-propset\n    trash directory.t9150-svk-mergetickets\n    trash directory.t9151-svn-mergeinfo\n    trash directory.t9152-svn-empty-dirs-after-gc\n    trash directory.t9153-git-svn-rewrite-uuid\n    trash directory.t9154-git-svn-fancy-glob\n    trash directory.t9155-git-svn-fetch-deleted-tag\n    trash directory.t9156-git-svn-fetch-deleted-tag-2\n    trash directory.t9157-git-svn-fetch-merge\n    trash directory.t9158-git-svn-mergeinfo\n    trash directory.t9159-git-svn-no-parent-mergeinfo\n    trash directory.t9160-git-svn-preserve-empty-dirs\n    trash directory.t9161-git-svn-mergeinfo-push\n    trash directory.t9162-git-svn-dcommit-interactive\n    trash directory.t9163-git-svn-reset-clears-caches\n    trash directory.t9164-git-svn-dcommit-concurrent\n    trash directory.t9165-git-svn-fetch-merge-branch-of-branch\n    trash directory.t9166-git-svn-fetch-merge-branch-of-branch2\n    trash directory.t9167-git-svn-cmd-branch-subproject\n    trash directory.t9168-git-svn-partially-globbed-names\n    trash directory.t9169-git-svn-dcommit-crlf\n"},{"id":"541501","messageId":"xmqqqzoifs8y.fsf@gitster.g","threadId":"65472","inReplyTo":"xmqqy0iqfsuu.fsf@gitster.g","subject":"Re: [PATCH 00/12] t: detect errors outside of test cases","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T21:46:37Z","receivedAt":"2026-04-13T21:46:39Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> this is a follow-up to the recent discussion we had around `set -e` to\n>> make our tests more robust and basically supersedes Junio's [1].\n>>\n>> I've tested the patches with both Bash and Dash, and all tests are\n>> passing on my machine with both of them. CI seems to be happy, as\n>> well. But I would expect that this change probably has some fallout,\n>> even though I hope that it's generally going to be small and contained.\n>>\n>> This series is based on 8c9303b1ff (Merge branch\n>> 'jc/no-writev-does-not-work', 2026-04-10).\n>\n> This unfortunately breaks svn related tests big time for me, as I\n> deliberately do not install Perl modules that are needed for git-svn.\n>\n>\n>     $ cd t && sh t9152-svn-empty-dirs-after-gc.sh -i -v\n>     Initialized empty Git repository in /home/gitster/git/t/trash directory.t9152-svn-empty-dirs-after-gc/.git/\n>     Can't locate SVN/Core.pm in @INC (you may need to install the SVN::Core module) (@INC entries checked: /etc/perl /usr/local/lib/x86_64-linux-gnu/perl/5.40.1 /usr/local/share/perl/5.40.1 /usr/lib/x86_64-linux-gnu/perl5/5.40 /usr/share/perl5 /usr/lib/x86_64-linux-gnu/perl-base /usr/lib/x86_64-linux-gnu/perl/5.40 /usr/share/perl/5.40 /usr/local/lib/site_perl) at -e line 2.\n>     BEGIN failed--compilation aborted at -e line 2.\n>     FATAL: Unexpected exit with code 2\n\nWith the merge of this topic reverted out of 'seen', here is how it\nused to end:\n\n    $ cd t && sh t9152-svn-empty-dirs-after-gc.sh -i -v; echo $?\n    Initialized empty Git repository in /home/gitster/git/t/trash directory.t9152-svn-empty-dirs-after-gc/.git/\n    Can't locate SVN/Core.pm in @INC (you may need to install the SVN::Core module) (@INC entries checked: /etc/perl /usr/local/lib/x86_64-linux-gnu/perl/5.40.1 /usr/local/share/perl/5.40.1 /usr/lib/x86_64-linux-gnu/perl5/5.40 /usr/share/perl5 /usr/lib/x86_64-linux-gnu/perl-base /usr/lib/x86_64-linux-gnu/perl/5.40 /usr/share/perl/5.40 /usr/local/lib/site_perl) at -e line 2.\n    BEGIN failed--compilation aborted at -e line 2.\n    1..0 # SKIP Perl SVN libraries not found or unusable\n    0\n\nThe largest difference being that we used to exit with 0 so the\noverall \"make test\" passed, but it no longer is the case.\n"},{"id":"541503","messageId":"xmqqeckifq59.fsf@gitster.g","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-6-5b83763a0e84@pks.im","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-13T22:32:02Z","receivedAt":"2026-04-13T22:32:04Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh\n> index 2fde2353fd..07d86ea244 100644\n> --- a/t/lib-git-svn.sh\n> +++ b/t/lib-git-svn.sh\n> @@ -15,8 +15,7 @@ GIT_SVN_DIR=$GIT_DIR/svn/refs/remotes/git-svn\n>  SVN_TREE=$GIT_SVN_DIR/svn-tree\n>  test_set_port SVNSERVE_PORT\n>  \n> -svn >/dev/null 2>&1\n> -if test $? -ne 1\n> +if ! svn help >/dev/null 2>&1\n>  then\n>  \tskip_all='skipping git svn tests, svn not found'\n>  \ttest_done\n\nI think I know what is lacking in this patch.  Following the above\nsection (which is a good conversion), there is this bit that needs a\nsimilar handling.\n\n t/lib-git-svn.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git c/t/lib-git-svn.sh w/t/lib-git-svn.sh\nindex 2fde2353fd..24c15d17eb 100644\n--- c/t/lib-git-svn.sh\n+++ w/t/lib-git-svn.sh\n@@ -27,13 +27,13 @@ export svnrepo\n svnconf=$PWD/svnconf\n export svnconf\n \n+x=0\n perl -w -e \"\n use SVN::Core;\n use SVN::Repos;\n \\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n system(qw/svnadmin create --fs-type fsfs/, \\$ENV{svnrepo}) == 0 or exit(41);\n-\" >&3 2>&4\n-x=$?\n+\" >&3 2>&4 || x=$?\n if test $x -ne 0\n then\n \tif test $x -eq 42; then\n"},{"id":"541515","messageId":"xmqq340yfivf.fsf@gitster.g","threadId":"65472","inReplyTo":"xmqqeckifq59.fsf@gitster.g","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-14T01:09:08Z","receivedAt":"2026-04-14T01:09:12Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I think I know what is lacking in this patch.  Following the above\n> section (which is a good conversion), there is this bit that needs a\n> similar handling.\n>\n>  t/lib-git-svn.sh | 4 ++--\n>  1 file changed, 2 insertions(+), 2 deletions(-)\n>\n> diff --git c/t/lib-git-svn.sh w/t/lib-git-svn.sh\n> index 2fde2353fd..24c15d17eb 100644\n> --- c/t/lib-git-svn.sh\n> +++ w/t/lib-git-svn.sh\n> @@ -27,13 +27,13 @@ export svnrepo\n>  svnconf=$PWD/svnconf\n>  export svnconf\n>  \n> +x=0\n>  perl -w -e \"\n>  use SVN::Core;\n>  use SVN::Repos;\n>  \\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n>  system(qw/svnadmin create --fs-type fsfs/, \\$ENV{svnrepo}) == 0 or exit(41);\n> -\" >&3 2>&4\n> -x=$?\n> +\" >&3 2>&4 || x=$?\n>  if test $x -ne 0\n>  then\n>  \tif test $x -eq 42; then\n\nThe above is queued as a squash fix-up on top of the topic, but with\nthe topic merged to 'seen', we seem to be getting a CI failure that\nappears specific to macOS.  Compare the failing\n\n  https://github.com/git/git/actions/runs/24371204585 (aa13593)\n\nwith the same tree without the topic\n\n  https://github.com/git/git/actions/runs/24369661492 (ad8b884)\n\nThe only differences between the commits are\n\n$ git diff --compact-summary ad8b884 aa13593\n t/lib-git-daemon.sh                | 13 ++++++++++---\n t/lib-git-svn.sh                   |  7 +++----\n t/lib-httpd.sh                     |  3 +--\n t/t0005-signals.sh                 |  4 ++--\n t/t0008-ignores.sh                 |  4 ++--\n t/t1301-shared-repo.sh             |  2 +-\n t/t3600-rm.sh                      |  2 +-\n t/t4032-diff-inter-hunk-context.sh | 14 ++++++++------\n t/t6002-rev-list-bisect.sh         | 17 ++++++++++-------\n t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n t/t7508-status.sh                  |  4 ++--\n t/t9138-git-svn-authors-prog.sh    |  4 ++--\n t/t9200-git-cvsexportcommit.sh     |  3 +--\n t/t9400-git-cvsserver-server.sh    |  5 +++--\n t/t9401-git-cvsserver-crlf.sh      |  4 ++--\n t/t9402-git-cvsserver-refs.sh      |  4 ++--\n t/t9902-completion.sh              |  2 +-\n t/test-lib-functions.sh            | 12 ++++++++----\n t/test-lib.sh                      |  8 ++++++--\n 19 files changed, 78 insertions(+), 58 deletions(-)\n\nwhich does match what is contained in this topic.\n\n\n"},{"id":"541521","messageId":"20260414062345.GA2902286@coredump.intra.peff.net","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-2-5b83763a0e84@pks.im","subject":"Re: [PATCH 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-04-14T06:23:45Z","receivedAt":"2026-04-14T06:23:47Z","isPatch":true,"body":"On Mon, Apr 13, 2026 at 11:49:23AM +0200, Patrick Steinhardt wrote:\n\n> diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\n> index f3af10fb7e..f8bc77619b 100644\n> --- a/t/test-lib-functions.sh\n> +++ b/t/test-lib-functions.sh\n> @@ -1195,8 +1195,12 @@ test_must_fail () {\n>  \t\techo >&7 \"test_must_fail: only 'git' is allowed: $*\"\n>  \t\treturn 1\n>  \tfi\n> -\t\"$@\" 2>&7\n> -\texit_code=$?\n> +\tif \"$@\" 2>&7\n> +\tthen\n> +\t\texit_code=0\n> +\telse\n> +\t\texit_code=$?\n> +\tfi\n\nOne subtle interaction here is that the command in \"$@\" will be run with\n\"set -e\" suppressed if it's a shell function (even if the body\nexplicitly says \"set -e\").\n\nI think it's mostly academic since we don't tend to use \"set -e\" in the\ntests at all, but it may be an eventual gotcha.\n\n-Peff\n"},{"id":"541522","messageId":"ad3rW2aWD6WrGUiG@pks.im","threadId":"65472","inReplyTo":"xmqqcy02kefc.fsf@gitster.g","subject":"Re: [PATCH 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-14T07:23:07Z","receivedAt":"2026-04-14T07:23:18Z","isPatch":true,"body":"On Mon, Apr 13, 2026 at 09:33:59AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > The helper function `test_must_fail ()` executes a specific Git command\n> > that may or may not fail in a specific way. This is done by executing\n> > the command in question and then comparing its exit code against a set\n> > of conditions.\n> >\n> > This works, but once we run our test suite with `set -e` we may bail out\n> > of `test_must_fail ()` early in case the command actually fails, even\n> > though we expect it to fail. Prepare for this change by marking the\n> > command part of a condition.\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  t/test-lib-functions.sh | 8 ++++++--\n> >  1 file changed, 6 insertions(+), 2 deletions(-)\n> >\n> > diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\n> > index f3af10fb7e..f8bc77619b 100644\n> > --- a/t/test-lib-functions.sh\n> > +++ b/t/test-lib-functions.sh\n> > @@ -1195,8 +1195,12 @@ test_must_fail () {\n> >  \t\techo >&7 \"test_must_fail: only 'git' is allowed: $*\"\n> >  \t\treturn 1\n> >  \tfi\n> > -\t\"$@\" 2>&7\n> > -\texit_code=$?\n> > +\tif \"$@\" 2>&7\n> > +\tthen\n> > +\t\texit_code=0\n> > +\telse\n> > +\t\texit_code=$?\n> > +\tfi\n> \n> This is obvious and clear.  Alternatively\n> \n> \texit_code=0; \"$@\" 2>&7 || exit_code=$?\n> \n> would be more assuring to readers who (with less fluency in Bourne\n> shells) wonder, upon seeing the else clause, how far \"$?\"  traveled\n> and it still holds the status of \"$@\".\n\nFair enough, will change.\n\nPatrick\n"},{"id":"541523","messageId":"ad3rZoZ0VaOcAAzS@pks.im","threadId":"65472","inReplyTo":"xmqqjyuaixcv.fsf@gitster.g","subject":"Re: [PATCH 08/12] t0008: silence error in subshell when using `grep -v`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-14T07:23:18Z","receivedAt":"2026-04-14T07:23:23Z","isPatch":true,"body":"On Mon, Apr 13, 2026 at 10:28:00AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > In t0008 we use `grep -v` in a subshell, but expect that this command\n> > will sometimes not match anything. This would cause grep(1) to return an\n> > error code, but given that we don't run with `set -e` we swallow this\n> > error.\n> >\n> > We're about to enable `set -e`. Prepare for this by ignoring any errors.\n> \n> It is curious that true is explicitly spelled out, unlike the\n> earlier steps in the series that used \":\" in \"|| :\".\n\nThat's fair. Will adapt.\n\nPatrick\n"},{"id":"541524","messageId":"ad3rbjDYad8rTybG@pks.im","threadId":"65472","inReplyTo":"xmqqldeqkesa.fsf@gitster.g","subject":"Re: [PATCH 01/12] t: prepare `test_match_signal ()` calls for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-14T07:23:26Z","receivedAt":"2026-04-14T07:23:31Z","isPatch":true,"body":"On Mon, Apr 13, 2026 at 09:26:13AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > We have a couple of calls to `test_match_signal ()` where we execute a\n> > Git command and expect it to die with a specific signal. These calls\n> > will essentially execute the process in a subshell via `foo; echo $?`,\n> > but as we expect `foo` to fail this will cause the overall subshell to\n> > fail once we `set -e`.\n> >\n> > Fix this issue by using `foo || echo $?` instead.\n> \n> Hmph, if the 'foo' command that is expected to fail succeeds by a\n> bug, we won't see 0 on the standard output anymore.  Comparing 13\n> with 0 or 13 with an empty string \"\" would fail either way, so that\n> may not give us a practical difference, but this somehow leaves a\n> foul small in my nose.\n\nHm, true, it is a wee bit ugly.\n\n> The technique used in <20260325062114.2067946-2-gitster@pobox.com>\n> would give us something ugly like\n> \n> -\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n> +\tOUT=$( ((st=0; large_git || st=$?; echo $st 1>&3) | :) 3>&1 ) &&\n> \n> and it ensures that we give 0 when we succeed, but I am not sure if\n> this is worth doing.\n> \n> Or perhaps\n> \n> -\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n> +\tOUT=$( ((large_git || echo $? 1>&3) | :) 3>&1 ) &&\n> +\tOUT=${OUT:-0} &&\n> \n> I dunno.\n> \n> If \"large_git\" somehow writes into file descriptor #3, then OUT will\n> be contaminated with something other than $?, so the additional \"if\n> empty substitute with 0\" would not work without doing something like\n> \n> -\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n> +\tOUT=$( ((large_git 3>/dev/null || echo $? 1>&3) | :) 3>&1 ) &&\n> +\tOUT=${OUT:-0} &&\n> \n> I guess.  For that matter, if large_git spits out \"13\" to its file\n> descriptor #3, I do not know what would have happened ;-)\n\nHow about the below patch? It's a mouthful, but the intent is quite\nexplicit and it's overall not too bad, if you ask me.\n\nPatrick\n\ndiff --git a/t/t0005-signals.sh b/t/t0005-signals.sh\nindex 74c2a27972..84319cf169 100755\n--- a/t/t0005-signals.sh\n+++ b/t/t0005-signals.sh\n@@ -42,12 +42,12 @@ test_expect_success 'create blob' '\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE' '\n-\tOUT=$( ((large_git || echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE even if parent ignores it' '\n-\tOUT=$( ((trap \"\" PIPE && large_git || echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 8e68a00dcb..a371ea690e 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -260,7 +260,7 @@ test_expect_success 'choking \"git rm\" should not let it die with cruft (induce S\n \n test_expect_success !MINGW 'choking \"git rm\" should not let it die with cruft (induce and check SIGPIPE)' '\n \tchoke_git_rm_setup &&\n-\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\" || echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\" && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\" &&\n \ttest_path_is_missing .git/index.lock\n '\n"},{"id":"541525","messageId":"ad3rdxVSrV9U4a9D@pks.im","threadId":"65472","inReplyTo":"xmqq340yfivf.fsf@gitster.g","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-14T07:23:35Z","receivedAt":"2026-04-14T07:23:41Z","isPatch":true,"body":"On Mon, Apr 13, 2026 at 06:09:08PM -0700, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > I think I know what is lacking in this patch.  Following the above\n> > section (which is a good conversion), there is this bit that needs a\n> > similar handling.\n> >\n> >  t/lib-git-svn.sh | 4 ++--\n> >  1 file changed, 2 insertions(+), 2 deletions(-)\n> >\n> > diff --git c/t/lib-git-svn.sh w/t/lib-git-svn.sh\n> > index 2fde2353fd..24c15d17eb 100644\n> > --- c/t/lib-git-svn.sh\n> > +++ w/t/lib-git-svn.sh\n> > @@ -27,13 +27,13 @@ export svnrepo\n> >  svnconf=$PWD/svnconf\n> >  export svnconf\n> >  \n> > +x=0\n> >  perl -w -e \"\n> >  use SVN::Core;\n> >  use SVN::Repos;\n> >  \\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n> >  system(qw/svnadmin create --fs-type fsfs/, \\$ENV{svnrepo}) == 0 or exit(41);\n> > -\" >&3 2>&4\n> > -x=$?\n> > +\" >&3 2>&4 || x=$?\n> >  if test $x -ne 0\n> >  then\n> >  \tif test $x -eq 42; then\n\nThanks, I will squash this fix-up into my branch.\n\n> The above is queued as a squash fix-up on top of the topic, but with\n> the topic merged to 'seen', we seem to be getting a CI failure that\n> appears specific to macOS.  Compare the failing\n> \n>   https://github.com/git/git/actions/runs/24371204585 (aa13593)\n> \n> with the same tree without the topic\n> \n>   https://github.com/git/git/actions/runs/24369661492 (ad8b884)\n> \n> The only differences between the commits are\n> \n> $ git diff --compact-summary ad8b884 aa13593\n>  t/lib-git-daemon.sh                | 13 ++++++++++---\n>  t/lib-git-svn.sh                   |  7 +++----\n>  t/lib-httpd.sh                     |  3 +--\n>  t/t0005-signals.sh                 |  4 ++--\n>  t/t0008-ignores.sh                 |  4 ++--\n>  t/t1301-shared-repo.sh             |  2 +-\n>  t/t3600-rm.sh                      |  2 +-\n>  t/t4032-diff-inter-hunk-context.sh | 14 ++++++++------\n>  t/t6002-rev-list-bisect.sh         | 17 ++++++++++-------\n>  t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n>  t/t7508-status.sh                  |  4 ++--\n>  t/t9138-git-svn-authors-prog.sh    |  4 ++--\n>  t/t9200-git-cvsexportcommit.sh     |  3 +--\n>  t/t9400-git-cvsserver-server.sh    |  5 +++--\n>  t/t9401-git-cvsserver-crlf.sh      |  4 ++--\n>  t/t9402-git-cvsserver-refs.sh      |  4 ++--\n>  t/t9902-completion.sh              |  2 +-\n>  t/test-lib-functions.sh            | 12 ++++++++----\n>  t/test-lib.sh                      |  8 ++++++--\n>  19 files changed, 78 insertions(+), 58 deletions(-)\n\nI'll investigate, thanks for the hint!\n\nPatrick\n"},{"id":"541526","messageId":"ad3rgbgadjIZRgaz@pks.im","threadId":"65472","inReplyTo":"xmqq340yfivf.fsf@gitster.g","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-14T07:23:45Z","receivedAt":"2026-04-14T07:23:50Z","isPatch":true,"body":"On Mon, Apr 13, 2026 at 06:09:08PM -0700, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > I think I know what is lacking in this patch.  Following the above\n> > section (which is a good conversion), there is this bit that needs a\n> > similar handling.\n> >\n> >  t/lib-git-svn.sh | 4 ++--\n> >  1 file changed, 2 insertions(+), 2 deletions(-)\n> >\n> > diff --git c/t/lib-git-svn.sh w/t/lib-git-svn.sh\n> > index 2fde2353fd..24c15d17eb 100644\n> > --- c/t/lib-git-svn.sh\n> > +++ w/t/lib-git-svn.sh\n> > @@ -27,13 +27,13 @@ export svnrepo\n> >  svnconf=$PWD/svnconf\n> >  export svnconf\n> >  \n> > +x=0\n> >  perl -w -e \"\n> >  use SVN::Core;\n> >  use SVN::Repos;\n> >  \\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n> >  system(qw/svnadmin create --fs-type fsfs/, \\$ENV{svnrepo}) == 0 or exit(41);\n> > -\" >&3 2>&4\n> > -x=$?\n> > +\" >&3 2>&4 || x=$?\n> >  if test $x -ne 0\n> >  then\n> >  \tif test $x -eq 42; then\n\nThanks, I've queued that change locally.\n\n> The above is queued as a squash fix-up on top of the topic, but with\n> the topic merged to 'seen', we seem to be getting a CI failure that\n> appears specific to macOS.  Compare the failing\n> \n>   https://github.com/git/git/actions/runs/24371204585 (aa13593)\n> \n> with the same tree without the topic\n> \n>   https://github.com/git/git/actions/runs/24369661492 (ad8b884)\n> \n> The only differences between the commits are\n> \n> $ git diff --compact-summary ad8b884 aa13593\n>  t/lib-git-daemon.sh                | 13 ++++++++++---\n>  t/lib-git-svn.sh                   |  7 +++----\n>  t/lib-httpd.sh                     |  3 +--\n>  t/t0005-signals.sh                 |  4 ++--\n>  t/t0008-ignores.sh                 |  4 ++--\n>  t/t1301-shared-repo.sh             |  2 +-\n>  t/t3600-rm.sh                      |  2 +-\n>  t/t4032-diff-inter-hunk-context.sh | 14 ++++++++------\n>  t/t6002-rev-list-bisect.sh         | 17 ++++++++++-------\n>  t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n>  t/t7508-status.sh                  |  4 ++--\n>  t/t9138-git-svn-authors-prog.sh    |  4 ++--\n>  t/t9200-git-cvsexportcommit.sh     |  3 +--\n>  t/t9400-git-cvsserver-server.sh    |  5 +++--\n>  t/t9401-git-cvsserver-crlf.sh      |  4 ++--\n>  t/t9402-git-cvsserver-refs.sh      |  4 ++--\n>  t/t9902-completion.sh              |  2 +-\n>  t/test-lib-functions.sh            | 12 ++++++++----\n>  t/test-lib.sh                      |  8 ++++++--\n>  19 files changed, 78 insertions(+), 58 deletions(-)\n> \n> which does match what is contained in this topic.\n\nI knew it was a bad idea to not also run tests on GitHub Actions :) I'll\ndo that before the next reroll.\n\nAnyway, looking at the failing test t9501, I assume that Perl isn't able\nto enable the DATE_PARSER prerequisite. So something like the below\npatch might hopefully fix it.\n\nThanks!\n\nPatrick\n\ndiff --git a/t/t9501-gitweb-standalone-http-status.sh b/t/t9501-gitweb-standalone-http-status.sh\nindex 32814e75df..3acb58125b 100755\n--- a/t/t9501-gitweb-standalone-http-status.sh\n+++ b/t/t9501-gitweb-standalone-http-status.sh\n@@ -15,12 +15,12 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./lib-gitweb.sh\n \n-#\n # Gitweb only provides the functionality tested by the 'modification times'\n # tests if it can access a date parser from one of these modules:\n-#\n-perl -MHTTP::Date -e 0 >/dev/null 2>&1 && test_set_prereq DATE_PARSER\n-perl -MTime::ParseDate -e 0 >/dev/null 2>&1 && test_set_prereq DATE_PARSER\n+test_lazy_prereq DATE_PARSER '\n+\tperl -MHTTP::Date -e 0 ||\n+\tperl -MTime::ParseDate -e 0\n+'\n \n # ----------------------------------------------------------------------\n # snapshot settings\n"},{"id":"541527","messageId":"ad3riiF5QWuMmUeT@pks.im","threadId":"65472","inReplyTo":"xmqqtsteiy7s.fsf@gitster.g","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-14T07:23:54Z","receivedAt":"2026-04-14T07:23:59Z","isPatch":true,"body":"On Mon, Apr 13, 2026 at 10:09:27AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Several of our tests verify whether a certain binary can be executed,\n> > potentially skipping tests in case we cannot, for example because the\n> > binary doesn't exist. In those cases we often run the binary outside of\n> > any conditionally.\n> >\n> > This will start to fail once we enable `set -e`, as that will cause us\n> > to bail out the test immediately. Improve these tests by executing them\n> > inside of a conditional instead.\n> \n> OK.  \"svn help\" and \"cvs version\" do exit with status 0 and the\n> rewrites that use them make sense.  I wonder if we can do something\n> similar to \"git\" instead of relying on \"git<RETURN>\" to exit with 1,\n> perhaps ...\n> \n>     $ git version >/dev/null; echo $?\n>     0\n> \n> ... by using \"git version\" in the test-lib.sh change?\n> \n> Other than that looking very good.\n\nI'm a bit confused. We do exactly that in \"test-lib.sh\" now, see the\nbelow hunk that is part of this patch. Am I missing something?\n\nPatrick\n\n> > diff --git a/t/test-lib.sh b/t/test-lib.sh\n> > index 70fd3e9baf..81380fe978 100644\n> > --- a/t/test-lib.sh\n> > +++ b/t/test-lib.sh\n> > @@ -143,8 +143,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n> >  ################################################################\n> >  # It appears that people try to run tests without building...\n> >  GIT_BINARY=\"${GIT_TEST_INSTALLED:-$GIT_BUILD_DIR}/git$X\"\n> > -\"$GIT_BINARY\" >/dev/null\n> > -if test $? != 1\n> > +\n> > +if ! \"$GIT_BINARY\" version >/dev/null\n> >  then\n> >  \tif test -n \"$GIT_TEST_INSTALLED\"\n> >  \tthen\n"},{"id":"541528","messageId":"ad3rkmf2227AWrVD@pks.im","threadId":"65472","inReplyTo":"xmqqpl42ixk6.fsf@gitster.g","subject":"Re: [PATCH 07/12] t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-14T07:24:02Z","receivedAt":"2026-04-14T07:24:08Z","isPatch":true,"body":"On Mon, Apr 13, 2026 at 10:23:37AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Both `test_when_finished ()` and `test_atexit ()` build up a chain of\n> > cleanup commands by prepending each new command to the existing cleanup\n> > string. To preserve the exit code of the test body across cleanup\n> > execution, we append the following logic:\n> >\n> >     } && (exit \"$eval_ret\"); eval_ret=$?; ...\n> >\n> > The intent of this is to run the cleanup block and then unconditionally\n> > restore `eval_ret`. The original behaviour of this is is:\n> >\n> >    +------------------+---------+------------------------------------+\n> >    |test body         │ cleanup │ old behaviour                      │\n> >    +------------------+---------+------------------------------------+\n> >    │pass (eval_ret=0) | pass    │ && taken -> (exit 0) -> eval_ret=0 |\n> >    +------------------+---------+------------------------------------+\n> >    │pass (eval_ret=0) | fail    │ && not taken -> eval_ret=$?        |\n> >    +------------------+---------+------------------------------------+\n> >    │fail (eval_ret=1) | pass    │ && taken -> (exit 1) -> eval_ret=1 |\n> >    +------------------+---------+------------------------------------+\n> >    │fail (eval_ret=1) | fail    | && not taken -> eval_ret=$?        |\n> >    +------------------+---------+------------------------------------+\n> >\n> > This logic will start to fail once we enable `set -e`. When `$eval_ret`\n> > is non-zero, the subshell we create will fail, and with `set -e` we'll\n> > thus bail out without evaluating the logic after the semicolon.\n> >\n> > Fix this issue by instead using `|| eval_ret=\\$?; ...`. Besides being\n> > a bit simpler, it also retains the original behaviour:\n> >\n> >    +------------------+---------+------------------------------------+\n> >    |test body         │ cleanup │ old behaviour                      │\n> >    +------------------+---------+------------------------------------+\n> >    │pass (eval_ret=0) | pass    │ || not taken -> eval_ret unchanged |\n> >    +------------------+---------+------------------------------------+\n> >    │pass (eval_ret=0) | fail    │ || taken -> eval_ret=$?            |\n> >    +------------------+---------+------------------------------------+\n> >    │fail (eval_ret=1) | pass    │ || not taken -> eval_ret unchanged |\n> >    +------------------+---------+------------------------------------+\n> >    │fail (eval_ret=1) | fail    | || taken -> eval_ret=$?            |\n> >    +------------------+---------+------------------------------------+\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  t/test-lib-functions.sh | 4 ++--\n> >  1 file changed, 2 insertions(+), 2 deletions(-)\n> >\n> > diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\n> > index f8bc77619b..0eac676109 100644\n> > --- a/t/test-lib-functions.sh\n> > +++ b/t/test-lib-functions.sh\n> > @@ -1516,7 +1516,7 @@ test_when_finished () {\n> >  \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n> >  \tBUG \"test_when_finished does nothing in a subshell\"\n> >  \ttest_cleanup=\"{ $*\n> > -\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_cleanup\"\n> > +\t\t} || eval_ret=\\$?; $test_cleanup\"\n> \n> Hmph, it seems this \"&& (exit $eval_ret)\" pattern has been with us\n> forever since it was introduced at 3bf78867 (test-lib: Let tests\n> specify commands to be run at end of test, 2010-05-02), and survived\n> a slight modification to work around issues on FreeBSD done in\n> b6b0afdc (test-lib: some shells do not let $? propagate into an\n> eval, 2010-05-06).  When a major part of test-lib.sh was split into\n> test-lib-functions.s at 12a29b1a (Move the user-facing test library\n> to test-lib-functions.sh, 2012-02-17), this part was copied intact.\n> \n> Does this interact well with the glitch b6b0afdc (test-lib: some\n> shells do not let $? propagate into an eval, 2010-05-06) tried to\n> work around, by the way?\n> \n> Thanks.\n\nAh, that's a very good question. I'll spin up a FreeBSD VM to give this\na test, thanks!\n\nPatrick\n"},{"id":"541552","messageId":"xmqqik9tek3g.fsf@gitster.g","threadId":"65472","inReplyTo":"ad3rgbgadjIZRgaz@pks.im","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-14T13:40:19Z","receivedAt":"2026-04-14T13:40:22Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> ...\n> Anyway, looking at the failing test t9501, I assume that Perl isn't able\n> to enable the DATE_PARSER prerequisite. So something like the below\n> patch might hopefully fix it.\n\nAhhhh.  This is exactly the kind of test breakage we _were_ hoping\nto find and fix with \"set -e\" series.  It has been happily failing\nto set DATE_PARSER on macOS, but now with \"set -e\", it is unhappy.\n\nThanks.\n"},{"id":"541553","messageId":"xmqqeckhek18.fsf@gitster.g","threadId":"65472","inReplyTo":"ad3riiF5QWuMmUeT@pks.im","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-14T13:41:39Z","receivedAt":"2026-04-14T13:41:41Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> I'm a bit confused. We do exactly that in \"test-lib.sh\" now, see the\n> below hunk that is part of this patch. Am I missing something?\n\nNo, I was misreading the patch and \"$GIT_BINARY\" >/dev/null in *preimage*\nwas pulling my attention away from the fixed one with \"version\".\n\nThanks for correcting me.\n\n>\n> Patrick\n>\n>> > diff --git a/t/test-lib.sh b/t/test-lib.sh\n>> > index 70fd3e9baf..81380fe978 100644\n>> > --- a/t/test-lib.sh\n>> > +++ b/t/test-lib.sh\n>> > @@ -143,8 +143,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>> >  ################################################################\n>> >  # It appears that people try to run tests without building...\n>> >  GIT_BINARY=\"${GIT_TEST_INSTALLED:-$GIT_BUILD_DIR}/git$X\"\n>> > -\"$GIT_BINARY\" >/dev/null\n>> > -if test $? != 1\n>> > +\n>> > +if ! \"$GIT_BINARY\" version >/dev/null\n>> >  then\n>> >  \tif test -n \"$GIT_TEST_INSTALLED\"\n>> >  \tthen\n"},{"id":"541574","messageId":"xmqq5x5tbfsm.fsf@gitster.g","threadId":"65472","inReplyTo":"20260414062345.GA2902286@coredump.intra.peff.net","subject":"Re: [PATCH 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-14T17:41:29Z","receivedAt":"2026-04-14T17:41:32Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> On Mon, Apr 13, 2026 at 11:49:23AM +0200, Patrick Steinhardt wrote:\n>\n>> diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\n>> index f3af10fb7e..f8bc77619b 100644\n>> --- a/t/test-lib-functions.sh\n>> +++ b/t/test-lib-functions.sh\n>> @@ -1195,8 +1195,12 @@ test_must_fail () {\n>>  \t\techo >&7 \"test_must_fail: only 'git' is allowed: $*\"\n>>  \t\treturn 1\n>>  \tfi\n>> -\t\"$@\" 2>&7\n>> -\texit_code=$?\n>> +\tif \"$@\" 2>&7\n>> +\tthen\n>> +\t\texit_code=0\n>> +\telse\n>> +\t\texit_code=$?\n>> +\tfi\n>\n> One subtle interaction here is that the command in \"$@\" will be run with\n> \"set -e\" suppressed if it's a shell function (even if the body\n> explicitly says \"set -e\").\n>\n> I think it's mostly academic since we don't tend to use \"set -e\" in the\n> tests at all, but it may be an eventual gotcha.\n\nYeah, ugly, but this is tricky that they felt the need to explain it\nwith an example in the informative section X-<.\n\n\"set\" is described in\n\n  https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_26\n\nand the description for \"-e\" does not even talk about any function.\nThe application usage notes associated with the \"set\" gives this\n\n    Application writers should avoid relying on set -e within\n    functions. For example, in the following script:\n\n    set -e\n    start() {\n        some_server\n        echo some_server started successfully\n    }\n    start || echo >&2 some_server failed\n\n    the -e setting is ignored within the function body (because the\n    function is a command in an AND-OR list other than the\n    last). Therefore, if some_server fails, the function carries on to\n    echo \"some_server started successfully\", and the exit status of the\n    function is zero (which means \"some_server failed\" is not output).\n\nwhich greatly helps.\n"},{"id":"541576","messageId":"xmqqtstda0vn.fsf@gitster.g","threadId":"65472","inReplyTo":"ad3rbjDYad8rTybG@pks.im","subject":"Re: [PATCH 01/12] t: prepare `test_match_signal ()` calls for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-14T17:49:00Z","receivedAt":"2026-04-14T17:49:03Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> How about the below patch? It's a mouthful, but the intent is quite\n> explicit and it's overall not too bad, if you ask me.\n>\n>  test_expect_success !MINGW 'a constipated git dies with SIGPIPE' '\n> -\tOUT=$( ((large_git || echo $? 1>&3) | :) 3>&1 ) &&\n> +\tOUT=$( ((large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n\nAh, I like it slightly better than \"st=0; large_git || st=$?\"\npattern, but yes, it is mouthful.\n"},{"id":"541597","messageId":"20260414220347.GA3475127@coredump.intra.peff.net","threadId":"65472","inReplyTo":"ad3rgbgadjIZRgaz@pks.im","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-04-14T22:03:47Z","receivedAt":"2026-04-14T22:03:49Z","isPatch":true,"body":"On Tue, Apr 14, 2026 at 09:23:45AM +0200, Patrick Steinhardt wrote:\n\n> diff --git a/t/t9501-gitweb-standalone-http-status.sh b/t/t9501-gitweb-standalone-http-status.sh\n> index 32814e75df..3acb58125b 100755\n> --- a/t/t9501-gitweb-standalone-http-status.sh\n> +++ b/t/t9501-gitweb-standalone-http-status.sh\n> @@ -15,12 +15,12 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>  \n>  . ./lib-gitweb.sh\n>  \n> -#\n>  # Gitweb only provides the functionality tested by the 'modification times'\n>  # tests if it can access a date parser from one of these modules:\n> -#\n> -perl -MHTTP::Date -e 0 >/dev/null 2>&1 && test_set_prereq DATE_PARSER\n> -perl -MTime::ParseDate -e 0 >/dev/null 2>&1 && test_set_prereq DATE_PARSER\n> +test_lazy_prereq DATE_PARSER '\n> +\tperl -MHTTP::Date -e 0 ||\n> +\tperl -MTime::ParseDate -e 0\n> +'\n\nMaybe I am being dense, but I don't see how the original would have\nproblems with \"set -e\". The perl command is on the left-hand side of an\n&&, so \"set -e\" will be suppressed.\n\n-Peff\n"},{"id":"541605","messageId":"20260414225206.GA3486072@coredump.intra.peff.net","threadId":"65472","inReplyTo":"20260414220347.GA3475127@coredump.intra.peff.net","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-04-14T22:52:06Z","receivedAt":"2026-04-14T22:52:08Z","isPatch":true,"body":"On Tue, Apr 14, 2026 at 06:03:47PM -0400, Jeff King wrote:\n\n> > -perl -MHTTP::Date -e 0 >/dev/null 2>&1 && test_set_prereq DATE_PARSER\n> > -perl -MTime::ParseDate -e 0 >/dev/null 2>&1 && test_set_prereq DATE_PARSER\n> > +test_lazy_prereq DATE_PARSER '\n> > +\tperl -MHTTP::Date -e 0 ||\n> > +\tperl -MTime::ParseDate -e 0\n> > +'\n> \n> Maybe I am being dense, but I don't see how the original would have\n> problems with \"set -e\". The perl command is on the left-hand side of an\n> &&, so \"set -e\" will be suppressed.\n\nJoy of joys, it looks like a bash bug. Using \"command\" re-enables \"set\n-e\", even on the left-hand side of an &&.\n\nWith bash 5.3.9 on my Debian system:\n\n  $ bash -ec 'command false && echo one; echo two'\n  two\n\nOn bash 3.2.57, tmate'd into the GitHub Actions macOS image:\n\n  $ bash -ec 'command false && echo one; echo two'\n  [no output, we exited after command failed]\n\nIt triggers in this case because perl in our test suite is a shell\nfunction which runs \"command $PERL_PATH\".\n\nBut I don't think switching to an if/then conditional helps. Doing:\n\n  bash -ec 'if command false; then echo one; fi; echo two'\n\nlikewise exits early (but prints \"two\" on modern versions).\n\nSadly I do not think we'll see a fixed version anytime soon. Apple is\nsticking with ancient bash because of licensing, IIRC. I don't know if\nthey're backporting any fixes (and even if they wanted to, there are\nprobably license complications).\n\nShort of requiring a third-party shell, the only workaround I can think\nof is to manually \"set +e\" before using \"command\", and then restore it\nwith \"set -e\". Gross.\n\n-Peff\n"},{"id":"541606","messageId":"20260414230810.GA3528448@coredump.intra.peff.net","threadId":"65472","inReplyTo":"20260414225206.GA3486072@coredump.intra.peff.net","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-04-14T23:08:10Z","receivedAt":"2026-04-14T23:08:12Z","isPatch":true,"body":"On Tue, Apr 14, 2026 at 06:52:07PM -0400, Jeff King wrote:\n\n> Short of requiring a third-party shell, the only workaround I can think\n> of is to manually \"set +e\" before using \"command\", and then restore it\n> with \"set -e\". Gross.\n\nI guess one other option is to avoid turning on \"set -e\" at all for\nknown-buggy shells. We are not relying on it working everywhere, but\nrather hoping that if at least one platform uses it, it will find\nprogramming errors in the test script.\n\nPersonally, I am still skeptical that all of this is worth it versus\njust checking stderr.\n\n-Peff\n"},{"id":"541612","messageId":"ad80r_oY-uAV2zVt@pks.im","threadId":"65472","inReplyTo":"20260414230810.GA3528448@coredump.intra.peff.net","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-15T06:48:15Z","receivedAt":"2026-04-15T06:48:21Z","isPatch":true,"body":"On Tue, Apr 14, 2026 at 07:08:10PM -0400, Jeff King wrote:\n> On Tue, Apr 14, 2026 at 06:52:07PM -0400, Jeff King wrote:\n> \n> > Short of requiring a third-party shell, the only workaround I can think\n> > of is to manually \"set +e\" before using \"command\", and then restore it\n> > with \"set -e\". Gross.\n> \n> I guess one other option is to avoid turning on \"set -e\" at all for\n> known-buggy shells. We are not relying on it working everywhere, but\n> rather hoping that if at least one platform uses it, it will find\n> programming errors in the test script.\n> \n> Personally, I am still skeptical that all of this is worth it versus\n> just checking stderr.\n\nYeah, the more I dive into this topic the more sceptical I get, as well,\nas shells behave wildly different around `set -e`. So I'm starting to\nfeel somewhat uncomfortable with the idea of blanket-enabling it for all\nshells, as that will for sure lead to lots of fallout on platforms that\nwe're not testing.\n\nMaybe we should really only do this for an allow-listed set of shells.\nStarting with Bash 5 and newer might be good enough, and given that we\nuse Bash for some of our CI jobs we can assume that this would weed out\nfailures anyway.\n\nPatrick\n"},{"id":"541614","messageId":"ad83IHKLtO4-gsG-@pks.im","threadId":"65472","inReplyTo":"xmqq5x5tbfsm.fsf@gitster.g","subject":"Re: [PATCH 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-15T06:58:40Z","receivedAt":"2026-04-15T06:58:47Z","isPatch":true,"body":"On Tue, Apr 14, 2026 at 10:41:29AM -0700, Junio C Hamano wrote:\n> Jeff King <peff@peff.net> writes:\n> \n> > On Mon, Apr 13, 2026 at 11:49:23AM +0200, Patrick Steinhardt wrote:\n> >\n> >> diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\n> >> index f3af10fb7e..f8bc77619b 100644\n> >> --- a/t/test-lib-functions.sh\n> >> +++ b/t/test-lib-functions.sh\n> >> @@ -1195,8 +1195,12 @@ test_must_fail () {\n> >>  \t\techo >&7 \"test_must_fail: only 'git' is allowed: $*\"\n> >>  \t\treturn 1\n> >>  \tfi\n> >> -\t\"$@\" 2>&7\n> >> -\texit_code=$?\n> >> +\tif \"$@\" 2>&7\n> >> +\tthen\n> >> +\t\texit_code=0\n> >> +\telse\n> >> +\t\texit_code=$?\n> >> +\tfi\n> >\n> > One subtle interaction here is that the command in \"$@\" will be run with\n> > \"set -e\" suppressed if it's a shell function (even if the body\n> > explicitly says \"set -e\").\n> >\n> > I think it's mostly academic since we don't tend to use \"set -e\" in the\n> > tests at all, but it may be an eventual gotcha.\n> \n> Yeah, ugly, but this is tricky that they felt the need to explain it\n> with an example in the informative section X-<.\n> \n> \"set\" is described in\n> \n>   https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_26\n> \n> and the description for \"-e\" does not even talk about any function.\n> The application usage notes associated with the \"set\" gives this\n> \n>     Application writers should avoid relying on set -e within\n>     functions. For example, in the following script:\n> \n>     set -e\n>     start() {\n>         some_server\n>         echo some_server started successfully\n>     }\n>     start || echo >&2 some_server failed\n> \n>     the -e setting is ignored within the function body (because the\n>     function is a command in an AND-OR list other than the\n>     last). Therefore, if some_server fails, the function carries on to\n>     echo \"some_server started successfully\", and the exit status of the\n>     function is zero (which means \"some_server failed\" is not output).\n> \n> which greatly helps.\n\nThe funny thing is that I discovered shells where this is not the case,\nand `set -e` _does_ extend into functions. Anyway, I think I'll follow\nPeff's suggestion and just enable `set -e` for Bash 5 and newer. This\nshould ensure that the behaviour is a lot more uniform without having\nnegative fallout on other platforms that don't use Bash, but we'd still\nget a failing CI as we use Bash in some of the jobs.\n\nPatrick\n"},{"id":"541670","messageId":"xmqqa4v48ckm.fsf@gitster.g","threadId":"65472","inReplyTo":"20260414230810.GA3528448@coredump.intra.peff.net","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-15T15:31:37Z","receivedAt":"2026-04-15T15:31:39Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> I guess one other option is to avoid turning on \"set -e\" at all for\n> known-buggy shells. We are not relying on it working everywhere, but\n> rather hoping that if at least one platform uses it, it will find\n> programming errors in the test script.\n>\n> Personally, I am still skeptical that all of this is worth it versus\n> just checking stderr.\n\nNot having to \"check\" stderr is powerful, when you generally just\nrun your tests with the output sent to your terminal.\n\nI agree that it is a good workaround to use 'set -e' only where we\nknow it works.\n"},{"id":"541712","messageId":"20260416054038.GA646814@coredump.intra.peff.net","threadId":"65472","inReplyTo":"ad83IHKLtO4-gsG-@pks.im","subject":"Re: [PATCH 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-04-16T05:40:38Z","receivedAt":"2026-04-16T05:40:40Z","isPatch":true,"body":"On Wed, Apr 15, 2026 at 08:58:40AM +0200, Patrick Steinhardt wrote:\n\n> > \"set\" is described in\n> > \n> >   https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_26\n> > \n> > and the description for \"-e\" does not even talk about any function.\n> > The application usage notes associated with the \"set\" gives this\n> > \n> >     Application writers should avoid relying on set -e within\n> >     functions. For example, in the following script:\n> > \n> >     set -e\n> >     start() {\n> >         some_server\n> >         echo some_server started successfully\n> >     }\n> >     start || echo >&2 some_server failed\n> > \n> >     the -e setting is ignored within the function body (because the\n> >     function is a command in an AND-OR list other than the\n> >     last). Therefore, if some_server fails, the function carries on to\n> >     echo \"some_server started successfully\", and the exit status of the\n> >     function is zero (which means \"some_server failed\" is not output).\n> > \n> > which greatly helps.\n> \n> The funny thing is that I discovered shells where this is not the case,\n> and `set -e` _does_ extend into functions.\n\nIt should extend into functions for all shells. It is just that the\nsuppression via \"if\" (or \"&&\", \"||\", etc) is counter-intuitive because\nit applies to individual commands within the function, not just the\nreturn value of the function as a whole.\n\nSo if you have seen shells where \"set -e\" is respected within a function\neven when that function is part of a conditional, that would be\nnoteworthy. But mostly only for our own curiosity at this point, I\nthink, as we'd be restricting the set of shells we'd even try to use\n\"set -e\" with.\n\n-Peff\n"},{"id":"541713","messageId":"20260416054924.GB646814@coredump.intra.peff.net","threadId":"65472","inReplyTo":"ad80r_oY-uAV2zVt@pks.im","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-04-16T05:49:24Z","receivedAt":"2026-04-16T05:49:26Z","isPatch":true,"body":"On Wed, Apr 15, 2026 at 08:48:15AM +0200, Patrick Steinhardt wrote:\n\n> > Personally, I am still skeptical that all of this is worth it versus\n> > just checking stderr.\n> \n> Yeah, the more I dive into this topic the more sceptical I get, as well,\n> as shells behave wildly different around `set -e`. So I'm starting to\n> feel somewhat uncomfortable with the idea of blanket-enabling it for all\n> shells, as that will for sure lead to lots of fallout on platforms that\n> we're not testing.\n> \n> Maybe we should really only do this for an allow-listed set of shells.\n> Starting with Bash 5 and newer might be good enough, and given that we\n> use Bash for some of our CI jobs we can assume that this would weed out\n> failures anyway.\n\nYeah, an allow-list is probably much better than trying to come up with\na list of buggy shells. But that only helps with portability.\n\nI'm still concerned that this approach is going to create extra friction\nfor test writers down the road. This series needed to clean up several\nspots to avoid false positives, and some of the spots were non-trivial.\n\nNow that was the accumulated cruft of 20 years of test-writing, so it's\nnot clear to me how often new test-writers will run into this. But when\nthey do, I worry that it may be hard to even figure out what is going\non.\n\nBut I've said as much in earlier rounds, and I'm not sure Junio agrees.\nSo we can note my dissent in the captain's log, and I can reserve the\nright to told-you-so later if need be. ;)\n\n-Peff\n"},{"id":"541715","messageId":"aeCXxdru6GqfA4lR@pks.im","threadId":"65472","inReplyTo":"20260416054924.GB646814@coredump.intra.peff.net","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T08:03:17Z","receivedAt":"2026-04-16T08:03:23Z","isPatch":true,"body":"On Thu, Apr 16, 2026 at 01:49:24AM -0400, Jeff King wrote:\n> On Wed, Apr 15, 2026 at 08:48:15AM +0200, Patrick Steinhardt wrote:\n> \n> > > Personally, I am still skeptical that all of this is worth it versus\n> > > just checking stderr.\n> > \n> > Yeah, the more I dive into this topic the more sceptical I get, as well,\n> > as shells behave wildly different around `set -e`. So I'm starting to\n> > feel somewhat uncomfortable with the idea of blanket-enabling it for all\n> > shells, as that will for sure lead to lots of fallout on platforms that\n> > we're not testing.\n> > \n> > Maybe we should really only do this for an allow-listed set of shells.\n> > Starting with Bash 5 and newer might be good enough, and given that we\n> > use Bash for some of our CI jobs we can assume that this would weed out\n> > failures anyway.\n> \n> Yeah, an allow-list is probably much better than trying to come up with\n> a list of buggy shells. But that only helps with portability.\n> \n> I'm still concerned that this approach is going to create extra friction\n> for test writers down the road. This series needed to clean up several\n> spots to avoid false positives, and some of the spots were non-trivial.\n> \n> Now that was the accumulated cruft of 20 years of test-writing, so it's\n> not clear to me how often new test-writers will run into this. But when\n> they do, I worry that it may be hard to even figure out what is going\n> on.\n> \n> But I've said as much in earlier rounds, and I'm not sure Junio agrees.\n> So we can note my dissent in the captain's log, and I can reserve the\n> right to told-you-so later if need be. ;)\n\nI don't necessarily disagree with your take, I do think this has the\npotential to cause some pain. I guess the question is _how_ painful it\nwill get, and whether that additional pain is worth it to help us avoid\nsilent breakage like we recently had.\n\nSo I'd take a \"merge and see\" approach here, and if we ever notice that\nit's too annoying we simply revert the last commit that introduced `set\n-e`. It doesn't have to be a one-way decision.\n\nPatrick\n"},{"id":"541722","messageId":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH v3 00/12] t: detect errors outside of test cases","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:17Z","receivedAt":"2026-04-16T11:19:27Z","isPatch":true,"body":"Hi,\n\nthis is a follow-up to the recent discussion we had around `set -e` to\nmake our tests more robust and basically supersedes Junio's [1].\n\nI've tested the patches with both Bash and Dash, and all tests are\npassing on my machine with both of them. CI seems to be happy, as\nwell. But I would expect that this change probably has some fallout,\neven though I hope that it's generally going to be small and contained.\n\nThis series is based on 8c9303b1ff (Merge branch\n'jc/no-writev-does-not-work', 2026-04-10).\n\nI've created an MR with GitLab [2] and a PR with GitHub [3] to verify\nthat these changes work on both platforms.\n\nChanges in v3:\n  - Adapt `linux-TEST-vars` job to use Bash instead of Dash. Ubuntu\n    packet mirrors seem to be having problems, so I wasn't able to get\n    past installing dependencies in any jobs. All to say that I couldn't\n    verify that this works as expected :/\n  - Link to v2: https://patch.msgid.link/20260415-b4-pks-tests-with-set-e-v2-0-4e4904a96f15@pks.im\n\nChanges in v2:\n  - Use `ret=0; $command || ret=$?` pattern.\n  - Restore `echo 0` in SIGPIPE tests.\n  - Fix \"lib-git-svn.sh\" to gracefully handle the case where SVN Perl\n    modules aren't installed.\n  - Use `|| :` consistently instead of `|| true`.\n  - Fix up a couple of tests that fail on FreeBSD 15. The test suite is\n    now passing on this system, too.\n  - Only enable `set -e` on Bash 5 and newer.\n  - Link to v1: https://patch.msgid.link/20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <20260325062114.2067946-1-gitster@pobox.com>\n[2]: https://gitlab.com/gitlab-org/git/-/merge_requests/541\n[3]: https://github.com/git/git/pull/2270\n\n---\nPatrick Steinhardt (12):\n      t: prepare `test_match_signal ()` calls for `set -e`\n      t: prepare `test_must_fail ()` for `set -e`\n      t: prepare `stop_git_daemon ()` for `set -e`\n      t: prepare `git config --unset` calls for `set -e`\n      t: prepare conditional test execution for `set -e`\n      t: prepare execution of potentially failing commands for `set -e`\n      t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`\n      t0008: silence error in subshell when using `grep -v`\n      t1301: don't fail in case setfacl(1) doesn't exist or fails\n      t6002: fix use of `expr` with `set -e`\n      t9902: fix use of `read` with `set -e`\n      t: detect errors outside of test cases\n\n ci/run-build-and-tests.sh          |  5 +++++\n t/lib-git-daemon.sh                |  8 +++++---\n t/lib-git-svn.sh                   |  7 +++----\n t/lib-httpd.sh                     |  3 +--\n t/t0005-signals.sh                 |  4 ++--\n t/t0008-ignores.sh                 |  4 ++--\n t/t1301-shared-repo.sh             |  2 +-\n t/t3600-rm.sh                      |  2 +-\n t/t3901-i18n-patch.sh              |  3 ++-\n t/t4032-diff-inter-hunk-context.sh | 14 ++++++++------\n t/t5000-tar-tree.sh                |  4 ++--\n t/t6002-rev-list-bisect.sh         | 17 ++++++++++-------\n t/t7422-submodule-output.sh        |  2 +-\n t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n t/t7508-status.sh                  |  4 ++--\n t/t9138-git-svn-authors-prog.sh    |  4 ++--\n t/t9200-git-cvsexportcommit.sh     |  3 +--\n t/t9400-git-cvsserver-server.sh    |  5 +++--\n t/t9401-git-cvsserver-crlf.sh      |  4 ++--\n t/t9402-git-cvsserver-refs.sh      |  4 ++--\n t/t9902-completion.sh              |  2 +-\n t/test-lib-functions.sh            | 12 ++++++------\n t/test-lib.sh                      | 19 +++++++++++++++----\n 23 files changed, 90 insertions(+), 66 deletions(-)\n\nRange-diff versus v2:\n\n 1:  5685131f51 =  1:  97c51d03f7 t: prepare `test_match_signal ()` calls for `set -e`\n 2:  7459f569ee =  2:  e1e2f83d1d t: prepare `test_must_fail ()` for `set -e`\n 3:  4077ee4b7f =  3:  441444895e t: prepare `stop_git_daemon ()` for `set -e`\n 4:  095c9b5b38 =  4:  5084b4627a t: prepare `git config --unset` calls for `set -e`\n 5:  0cdf15d405 =  5:  4608aef6ee t: prepare conditional test execution for `set -e`\n 6:  12f85b2c18 =  6:  9f10b21410 t: prepare execution of potentially failing commands for `set -e`\n 7:  50dc57081d =  7:  c9584e9b56 t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`\n 8:  e223383e82 =  8:  778a555bb2 t0008: silence error in subshell when using `grep -v`\n 9:  ad084a9d37 =  9:  ba4b513722 t1301: don't fail in case setfacl(1) doesn't exist or fails\n10:  39a01a5e06 = 10:  5f5db73fdb t6002: fix use of `expr` with `set -e`\n11:  060d094d3f = 11:  03d0e9c089 t9902: fix use of `read` with `set -e`\n12:  73a21590b1 ! 12:  bd300ab2f8 t: detect errors outside of test cases\n    @@ Commit message\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    + ## ci/run-build-and-tests.sh ##\n    +@@ ci/run-build-and-tests.sh: fedora-breaking-changes-musl|linux-breaking-changes)\n    + \tMESONFLAGS=\"$MESONFLAGS -Drust=enabled\"\n    + \t;;\n    + linux-TEST-vars)\n    ++\t# Ubuntu uses Dash by default, but we only enable use of `set -e`\n    ++\t# when using Bash 5+. Ensure that we have at least one CI job that uses\n    ++\t# it.\n    ++\texport TEST_SHELL_PATH=/usr/bin/bash\n    ++\n    + \texport OPENSSL_SHA1_UNSAFE=YesPlease\n    + \texport GIT_TEST_SPLIT_INDEX=yes\n    + \texport GIT_TEST_FULL_IN_PACK_ARRAY=true\n    +\n      ## t/test-lib.sh ##\n     @@\n      # You should have received a copy of the GNU General Public License\n\n---\nbase-commit: 8c9303b1ffae5b745d1b0a1f98330cf7944d8db0\nchange-id: 20260410-b4-pks-tests-with-set-e-3ae479b24b51\n\n"},{"id":"541723","messageId":"20260416-b4-pks-tests-with-set-e-v3-1-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 01/12] t: prepare `test_match_signal ()` calls for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:18Z","receivedAt":"2026-04-16T11:19:29Z","isPatch":true,"body":"We have a couple of calls to `test_match_signal ()` where we execute a\nGit command and expect it to die with a specific signal. These calls\nwill essentially execute the process in a subshell via `foo; echo $?`,\nbut as we expect `foo` to fail this will cause the overall subshell to\nfail once we `set -e`.\n\nFix this issue by using `foo && echo 0 || echo $?` instead.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0005-signals.sh | 4 ++--\n t/t3600-rm.sh      | 2 +-\n 2 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t0005-signals.sh b/t/t0005-signals.sh\nindex afba0fc3fc..84319cf169 100755\n--- a/t/t0005-signals.sh\n+++ b/t/t0005-signals.sh\n@@ -42,12 +42,12 @@ test_expect_success 'create blob' '\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE' '\n-\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE even if parent ignores it' '\n-\tOUT=$( ((trap \"\" PIPE && large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 1f16e6b522..a371ea690e 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -260,7 +260,7 @@ test_expect_success 'choking \"git rm\" should not let it die with cruft (induce S\n \n test_expect_success !MINGW 'choking \"git rm\" should not let it die with cruft (induce and check SIGPIPE)' '\n \tchoke_git_rm_setup &&\n-\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\"; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\" && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\" &&\n \ttest_path_is_missing .git/index.lock\n '\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541724","messageId":"20260416-b4-pks-tests-with-set-e-v3-2-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:19Z","receivedAt":"2026-04-16T11:19:33Z","isPatch":true,"body":"The helper function `test_must_fail ()` executes a specific Git command\nthat may or may not fail in a specific way. This is done by executing\nthe command in question and then comparing its exit code against a set\nof conditions.\n\nThis works, but once we run our test suite with `set -e` we may bail out\nof `test_must_fail ()` early in case the command actually fails, even\nthough we expect it to fail. Prepare for this change by handling the\nfailed case with `||`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/test-lib-functions.sh | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex f3af10fb7e..5fd5494ef1 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1195,8 +1195,9 @@ test_must_fail () {\n \t\techo >&7 \"test_must_fail: only 'git' is allowed: $*\"\n \t\treturn 1\n \tfi\n-\t\"$@\" 2>&7\n-\texit_code=$?\n+\n+\texit_code=0; \"$@\" 2>&7 || exit_code=$?\n+\n \tif test $exit_code -eq 0 && ! list_contains \"$_test_ok\" success\n \tthen\n \t\techo >&4 \"test_must_fail: command succeeded: $*\"\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541725","messageId":"20260416-b4-pks-tests-with-set-e-v3-3-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 03/12] t: prepare `stop_git_daemon ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:20Z","receivedAt":"2026-04-16T11:19:37Z","isPatch":true,"body":"We have a couple of calls to `stop_git_daemon ()` outside of specific\ntest cases that will kill a backgrounded git-daemon(1) process and\nexpect the process with a specific error code. While these function\ncalls do end up killing git-daemon(1), the error handling we have in\nthose contexts is basically ineffective. So while we expect the process\nto exit with a specific error code, we will just continue with any error\nin case it doesn't.\n\nThis will change once we enable `set -e` in a subsequent commit. There's\ntwo issues though that will make this _always_ fail:\n\n  - Our call to `wait` is expected to fail, but because it's not part of\n    a condition it will cause us to bail out immediately with `set -e`.\n\n  - We try to kill git-daemon(1) a second time via the pidfile. We can\n    generally expect that this is the same PID though as we had in the\n    \"GIT_DAEMON_PID\" environment variable, and thus it's more likely\n    than not that we have already killed it, and the call to kill will\n    fail.\n\nPrepare for this change by handling the failure of `wait` with `||` and\nby silencing failures of the second call to `kill`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/lib-git-daemon.sh | 8 +++++---\n 1 file changed, 5 insertions(+), 3 deletions(-)\n\ndiff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh\nindex e62569222b..d172aa51f0 100644\n--- a/t/lib-git-daemon.sh\n+++ b/t/lib-git-daemon.sh\n@@ -85,14 +85,16 @@ stop_git_daemon() {\n \n \t# kill git-daemon child of git\n \tsay >&3 \"Stopping git daemon ...\"\n+\n \tkill \"$GIT_DAEMON_PID\"\n-\twait \"$GIT_DAEMON_PID\" >&3 2>&4\n-\tret=$?\n+\tret=0; wait \"$GIT_DAEMON_PID\" >&3 2>&4 || ret=$?\n+\n \tif ! test_match_signal 15 $ret\n \tthen\n \t\terror \"git daemon exited with status: $ret\"\n \tfi\n-\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null\n+\n+\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null || :\n \tGIT_DAEMON_PID=\n \trm -f git_daemon_output \"$GIT_DAEMON_PIDFILE\"\n }\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541726","messageId":"20260416-b4-pks-tests-with-set-e-v3-4-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 04/12] t: prepare `git config --unset` calls for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:21Z","receivedAt":"2026-04-16T11:19:41Z","isPatch":true,"body":"We have a couple of calls to `git config --unset` that ultimately end up\nas no-ops as the configuration variables aren't set (anymore) in the\nfirst place. These calls are mostly intended to recover unconditionally\nfrom tests that may have executed only partially, but they'll ultimately\nfail during a normal test run.\n\nThis hasn't been a problem until now as we aren't running tests with\n`set -e`. This is about to change though, so let's silence the case\nwhere we cannot unset the config keys.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t4032-diff-inter-hunk-context.sh | 2 +-\n t/t7508-status.sh                  | 4 ++--\n t/t9138-git-svn-authors-prog.sh    | 4 ++--\n 3 files changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\nindex bada0cbd32..c98eb6abb2 100755\n--- a/t/t4032-diff-inter-hunk-context.sh\n+++ b/t/t4032-diff-inter-hunk-context.sh\n@@ -17,7 +17,7 @@ f() {\n \n t() {\n \tuse_config=\n-\tgit config --unset diff.interHunkContext\n+\tgit config --unset diff.interHunkContext || :\n \n \tcase $# in\n \t4) hunks=$4; cmd=\"diff -U$3\";;\ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex a5e21bf8bf..1167b835a4 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -773,8 +773,8 @@ test_expect_success TTY 'status --porcelain ignores color.status' '\n '\n \n # recover unconditionally from color tests\n-git config --unset color.status\n-git config --unset color.ui\n+git config --unset color.status || :\n+git config --unset color.ui || :\n \n test_expect_success 'status --porcelain respects -b' '\n \ndiff --git a/t/t9138-git-svn-authors-prog.sh b/t/t9138-git-svn-authors-prog.sh\nindex 784ec7fc2d..5bb38cb23a 100755\n--- a/t/t9138-git-svn-authors-prog.sh\n+++ b/t/t9138-git-svn-authors-prog.sh\n@@ -68,8 +68,8 @@ test_expect_success 'authors-file overrode authors-prog' '\n \t)\n '\n \n-git --git-dir=x/.git config --unset svn.authorsfile\n-git --git-dir=x/.git config --unset svn.authorsprog\n+git --git-dir=x/.git config --unset svn.authorsfile || :\n+git --git-dir=x/.git config --unset svn.authorsprog || :\n \n test_expect_success 'authors-prog imported user without email' '\n \tsvn mkdir -m gg --username gg-hermit \"$svnrepo\"/gg &&\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541727","messageId":"20260416-b4-pks-tests-with-set-e-v3-5-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 05/12] t: prepare conditional test execution for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:22Z","receivedAt":"2026-04-16T11:19:45Z","isPatch":true,"body":"We have some test in our test suite where we use the pattern of\n`test ... && test_expect_succeess` to conditionally execute a test. The\nproblem is that when we decide to not execute the test, we'll indeed\nskip the test, but the overall statement will also be unsuccessful. This\nwill become a problem once we enable `set -e`.\n\nPrepare for this future by turning this into a proper conditional, which\nis also a bit easier to read overall.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t4032-diff-inter-hunk-context.sh | 12 +++++++-----\n t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n 2 files changed, 20 insertions(+), 16 deletions(-)\n\ndiff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\nindex c98eb6abb2..2d216fb70f 100755\n--- a/t/t4032-diff-inter-hunk-context.sh\n+++ b/t/t4032-diff-inter-hunk-context.sh\n@@ -40,11 +40,13 @@ t() {\n \t\ttest $(git $cmd $file | grep '^@@ ' | wc -l) = $hunks\n \t\"\n \n-\ttest -f $expected &&\n-\ttest_expect_success \"$label: check output\" \"\n-\t\tgit $cmd $file | grep -v '^index ' >actual &&\n-\t\ttest_cmp $expected actual\n-\t\"\n+\tif test -f $expected\n+\tthen\n+\t\ttest_expect_success \"$label: check output\" \"\n+\t\t\tgit $cmd $file | grep -v '^index ' >actual &&\n+\t\t\ttest_cmp $expected actual\n+\t\t\"\n+\tfi\n }\n \n cat <<EOF >expected.f1.0.1 || exit 1\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex f512eed278..8cc86522b2 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -220,17 +220,19 @@ check_dotx_symlink () {\n \t\t)\n \t'\n \n-\ttest -n \"$refuse_index\" &&\n-\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n-\t\ttest_must_fail \\\n-\t\t\tgit -C $dir \\\n-\t\t\t    -c core.protectntfs \\\n-\t\t\t    -c core.protecthfs \\\n-\t\t\t    read-tree $tree 2>err &&\n-\t\tgrep \"invalid path.*$name\" err &&\n-\t\tgit -C $dir ls-files -s >out &&\n-\t\ttest_must_be_empty out\n-\t'\n+\tif test -n \"$refuse_index\"\n+\tthen\n+\t\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n+\t\t\ttest_must_fail \\\n+\t\t\t\tgit -C $dir \\\n+\t\t\t\t    -c core.protectntfs \\\n+\t\t\t\t    -c core.protecthfs \\\n+\t\t\t\t    read-tree $tree 2>err &&\n+\t\t\tgrep \"invalid path.*$name\" err &&\n+\t\t\tgit -C $dir ls-files -s >out &&\n+\t\t\ttest_must_be_empty out\n+\t\t'\n+\tfi\n }\n \n check_dotx_symlink gitmodules vanilla .gitmodules\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541728","messageId":"20260416-b4-pks-tests-with-set-e-v3-6-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:23Z","receivedAt":"2026-04-16T11:19:49Z","isPatch":true,"body":"Several of our tests verify whether a certain binary can be executed,\npotentially skipping tests in case we cannot, for example because the\nbinary doesn't exist. In those cases we often run the binary outside of\nany conditionally.\n\nThis will start to fail once we enable `set -e`, as that will cause us\nto bail out the test immediately. Improve these tests by executing them\ninside of a conditional instead.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/lib-git-svn.sh                |  7 +++----\n t/lib-httpd.sh                  |  3 +--\n t/t3901-i18n-patch.sh           |  3 ++-\n t/t5000-tar-tree.sh             |  4 ++--\n t/t7422-submodule-output.sh     |  2 +-\n t/t9200-git-cvsexportcommit.sh  |  3 +--\n t/t9400-git-cvsserver-server.sh |  5 +++--\n t/t9401-git-cvsserver-crlf.sh   |  4 ++--\n t/t9402-git-cvsserver-refs.sh   |  4 ++--\n t/test-lib-functions.sh         |  3 +--\n t/test-lib.sh                   | 10 ++++++----\n 11 files changed, 24 insertions(+), 24 deletions(-)\n\ndiff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh\nindex 2fde2353fd..52843f667d 100644\n--- a/t/lib-git-svn.sh\n+++ b/t/lib-git-svn.sh\n@@ -15,8 +15,7 @@ GIT_SVN_DIR=$GIT_DIR/svn/refs/remotes/git-svn\n SVN_TREE=$GIT_SVN_DIR/svn-tree\n test_set_port SVNSERVE_PORT\n \n-svn >/dev/null 2>&1\n-if test $? -ne 1\n+if ! svn help >/dev/null 2>&1\n then\n \tskip_all='skipping git svn tests, svn not found'\n \ttest_done\n@@ -27,13 +26,13 @@ export svnrepo\n svnconf=$PWD/svnconf\n export svnconf\n \n+x=0\n perl -w -e \"\n use SVN::Core;\n use SVN::Repos;\n \\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n system(qw/svnadmin create --fs-type fsfs/, \\$ENV{svnrepo}) == 0 or exit(41);\n-\" >&3 2>&4\n-x=$?\n+\" >&3 2>&4 || x=$?\n if test $x -ne 0\n then\n \tif test $x -eq 42; then\ndiff --git a/t/lib-httpd.sh b/t/lib-httpd.sh\nindex 4c76e813e3..fc646447d5 100644\n--- a/t/lib-httpd.sh\n+++ b/t/lib-httpd.sh\n@@ -235,11 +235,10 @@ start_httpd() {\n \n \ttest_atexit stop_httpd\n \n-\t\"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n+\tif ! \"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n \t\t-f \"$TEST_PATH/apache.conf\" $HTTPD_PARA \\\n \t\t-c \"Listen 127.0.0.1:$LIB_HTTPD_PORT\" -k start \\\n \t\t>&3 2>&4\n-\tif test $? -ne 0\n \tthen\n \t\tcat \"$HTTPD_ROOT_PATH\"/error.log >&4 2>/dev/null\n \t\ttest_skip_or_die GIT_TEST_HTTPD \"web server setup failed\"\ndiff --git a/t/t3901-i18n-patch.sh b/t/t3901-i18n-patch.sh\nindex f03601b49a..ef7d7e1edc 100755\n--- a/t/t3901-i18n-patch.sh\n+++ b/t/t3901-i18n-patch.sh\n@@ -28,7 +28,8 @@ check_encoding () {\n \t\t8859)\n \t\t\tgrep \"^encoding ISO8859-1\" ;;\n \t\t*)\n-\t\t\tgrep \"^encoding ISO8859-1\"; test \"$?\" != 0 ;;\n+\t\t\tret=0; grep \"^encoding ISO8859-1\" || ret=$?\n+\t\t\ttest \"$ret\" != 0 ;;\n \t\tesac || return 1\n \t\tj=$i\n \t\ti=$(($i+1))\ndiff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh\nindex 5465054f17..a8c28533dc 100755\n--- a/t/t5000-tar-tree.sh\n+++ b/t/t5000-tar-tree.sh\n@@ -503,8 +503,8 @@ test_expect_success LONG_IS_64BIT 'set up repository with huge blob' '\n # would generate the whole 64GB).\n test_expect_success LONG_IS_64BIT 'generate tar with huge size' '\n \t{\n-\t\tgit archive HEAD\n-\t\techo $? >exit-code\n+\t\t{ ret=0 && git archive HEAD || ret=$?; } &&\n+\t\techo \"$ret\" >exit-code\n \t} | test_copy_bytes 4096 >huge.tar &&\n \techo 141 >expect &&\n \ttest_cmp expect exit-code\ndiff --git a/t/t7422-submodule-output.sh b/t/t7422-submodule-output.sh\nindex aea1ddf117..852136fdfd 100755\n--- a/t/t7422-submodule-output.sh\n+++ b/t/t7422-submodule-output.sh\n@@ -198,7 +198,7 @@ test_expect_success !MINGW 'git submodule status --recursive propagates SIGPIPE'\n \t(\n \t\tcd repo &&\n \t\tGIT_ALLOW_PROTOCOL=file git submodule add \"$(pwd)\"/../submodule &&\n-\t\t{ git submodule status --recursive 2>err; echo $?>status; } |\n+\t\t{ { ret=0 && git submodule status --recursive 2>err || ret=$?; } && echo $ret >status; } |\n \t\t\tgrep -q recursive-submodule-path-1 &&\n \t\ttest_must_be_empty err &&\n \t\ttest_match_signal 13 \"$(cat status)\"\ndiff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh\nindex 14cbe96527..581cf3d28f 100755\n--- a/t/t9200-git-cvsexportcommit.sh\n+++ b/t/t9200-git-cvsexportcommit.sh\n@@ -11,8 +11,7 @@ if ! test_have_prereq PERL; then\n \ttest_done\n fi\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git cvsexportcommit tests, cvs not found'\n     test_done\ndiff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh\nindex e499c7f955..4b45398bab 100755\n--- a/t/t9400-git-cvsserver-server.sh\n+++ b/t/t9400-git-cvsserver-server.sh\n@@ -17,12 +17,13 @@ if ! test_have_prereq PERL; then\n \tskip_all='skipping git cvsserver tests, perl not available'\n \ttest_done\n fi\n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git-cvsserver tests, cvs not found'\n     test_done\n fi\n+\n perl -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {\n     skip_all='skipping git-cvsserver tests, Perl SQLite interface unavailable'\n     test_done\ndiff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh\nindex a34805acdc..6b4cbb1651 100755\n--- a/t/t9401-git-cvsserver-crlf.sh\n+++ b/t/t9401-git-cvsserver-crlf.sh\n@@ -60,12 +60,12 @@ check_status_options() {\n     return $stat\n }\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git-cvsserver tests, cvs not found'\n     test_done\n fi\n+\n if ! test_have_prereq PERL\n then\n     skip_all='skipping git-cvsserver tests, perl not available'\ndiff --git a/t/t9402-git-cvsserver-refs.sh b/t/t9402-git-cvsserver-refs.sh\nindex 2ee41f9443..65f2ceedec 100755\n--- a/t/t9402-git-cvsserver-refs.sh\n+++ b/t/t9402-git-cvsserver-refs.sh\n@@ -68,12 +68,12 @@ check_diff() {\n \n #########\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n \tskip_all='skipping git-cvsserver tests, cvs not found'\n \ttest_done\n fi\n+\n if ! test_have_prereq PERL\n then\n \tskip_all='skipping git-cvsserver tests, perl not available'\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex 5fd5494ef1..879ee1ee59 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1248,8 +1248,7 @@ test_might_fail () {\n test_expect_code () {\n \twant_code=$1\n \tshift\n-\t\"$@\" 2>&7\n-\texit_code=$?\n+\texit_code=0; \"$@\" 2>&7 || exit_code=$?\n \tif test $exit_code = $want_code\n \tthen\n \t\treturn 0\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 70fd3e9baf..de7d9e7b92 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -143,8 +143,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n ################################################################\n # It appears that people try to run tests without building...\n GIT_BINARY=\"${GIT_TEST_INSTALLED:-$GIT_BUILD_DIR}/git$X\"\n-\"$GIT_BINARY\" >/dev/null\n-if test $? != 1\n+\n+if ! \"$GIT_BINARY\" version >/dev/null\n then\n \tif test -n \"$GIT_TEST_INSTALLED\"\n \tthen\n@@ -454,8 +454,10 @@ then\n \t# from any previous runs.\n \t>\"$GIT_TEST_TEE_OUTPUT_FILE\"\n \n-\t(GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} \"$0\" \"$@\" 2>&1;\n-\t echo $? >\"$TEST_RESULTS_BASE.exit\") | tee -a \"$GIT_TEST_TEE_OUTPUT_FILE\"\n+\t(\n+\t\tret=0 && GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} \"$0\" \"$@\" 2>&1 || ret=$?\n+\t\techo \"$ret\" >\"$TEST_RESULTS_BASE.exit\"\n+\t) | tee -a \"$GIT_TEST_TEE_OUTPUT_FILE\"\n \ttest \"$(cat \"$TEST_RESULTS_BASE.exit\")\" = 0\n \texit\n fi\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541729","messageId":"20260416-b4-pks-tests-with-set-e-v3-7-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 07/12] t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:24Z","receivedAt":"2026-04-16T11:19:53Z","isPatch":true,"body":"Both `test_when_finished ()` and `test_atexit ()` build up a chain of\ncleanup commands by prepending each new command to the existing cleanup\nstring. To preserve the exit code of the test body across cleanup\nexecution, we append the following logic:\n\n    } && (exit \"$eval_ret\"); eval_ret=$?; ...\n\nThe intent of this is to run the cleanup block and then unconditionally\nrestore `eval_ret`. The original behaviour of this is is:\n\n   +------------------+---------+------------------------------------+\n   |test body         │ cleanup │ old behaviour                      │\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | pass    │ && taken -> (exit 0) -> eval_ret=0 |\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | fail    │ && not taken -> eval_ret=$?        |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | pass    │ && taken -> (exit 1) -> eval_ret=1 |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | fail    | && not taken -> eval_ret=$?        |\n   +------------------+---------+------------------------------------+\n\nThis logic will start to fail once we enable `set -e`. When `$eval_ret`\nis non-zero, the subshell we create will fail, and with `set -e` we'll\nthus bail out without evaluating the logic after the semicolon.\n\nFix this issue by instead using `|| eval_ret=\\$?; ...`. Besides being\na bit simpler, it also retains the original behaviour:\n\n   +------------------+---------+------------------------------------+\n   |test body         │ cleanup │ old behaviour                      │\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | pass    │ || not taken -> eval_ret unchanged |\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | fail    │ || taken -> eval_ret=$?            |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | pass    │ || not taken -> eval_ret unchanged |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | fail    | || taken -> eval_ret=$?            |\n   +------------------+---------+------------------------------------+\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/test-lib-functions.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex 879ee1ee59..502bb0ddcb 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1512,7 +1512,7 @@ test_when_finished () {\n \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n \tBUG \"test_when_finished does nothing in a subshell\"\n \ttest_cleanup=\"{ $*\n-\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_cleanup\"\n+\t\t} || eval_ret=\\$?; $test_cleanup\"\n }\n \n # This function can be used to schedule some commands to be run\n@@ -1540,7 +1540,7 @@ test_atexit () {\n \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n \tBUG \"test_atexit does nothing in a subshell\"\n \ttest_atexit_cleanup=\"{ $*\n-\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_atexit_cleanup\"\n+\t\t} || eval_ret=\\$?; $test_atexit_cleanup\"\n }\n \n # Deprecated wrapper for \"git init\", use \"git init\" directly instead\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541730","messageId":"20260416-b4-pks-tests-with-set-e-v3-8-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 08/12] t0008: silence error in subshell when using `grep -v`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:25Z","receivedAt":"2026-04-16T11:19:57Z","isPatch":true,"body":"In t0008 we use `grep -v` in a subshell, but expect that this command\nwill sometimes not match anything. This would cause grep(1) to return an\nerror code, but given that we don't run with `set -e` we swallow this\nerror.\n\nWe're about to enable `set -e`. Prepare for this by ignoring any errors.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0008-ignores.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh\nindex e716b5cdfa..d77a179bdd 100755\n--- a/t/t0008-ignores.sh\n+++ b/t/t0008-ignores.sh\n@@ -122,8 +122,8 @@ test_expect_success_multiple () {\n \tfi\n \ttestname=\"$1\" expect_all=\"$2\" code=\"$3\"\n \n-\texpect_verbose=$( echo \"$expect_all\" | grep -v '^::\t' )\n-\texpect=$( echo \"$expect_verbose\" | sed -e 's/.*\t//' )\n+\texpect_verbose=$(echo \"$expect_all\" | grep -v '^::\t' || :)\n+\texpect=$(echo \"$expect_verbose\" | sed -e 's/.*\t//')\n \n \ttest_expect_success $prereq \"$testname${no_index_opt:+ with $no_index_opt}\" '\n \t\texpect \"$expect\" &&\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541731","messageId":"20260416-b4-pks-tests-with-set-e-v3-9-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 09/12] t1301: don't fail in case setfacl(1) doesn't exist or fails","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:26Z","receivedAt":"2026-04-16T11:20:01Z","isPatch":true,"body":"In t1301 we're trying to remove any potentially-existing default ACLs\nthat might exist on the transh directory by executing setfacl(1).\nAccording to 8ed0a740dd (t1301-shared-repo.sh: don't let a default ACL\ninterfere with the test, 2008-10-16), this is done because we play\naround with permissions and umasks in this test suite.\n\nThe setfacl(1) binary may not exist on some systems though, even though\ntests ultimately still pass. This doesn't matter currently, but will\ncause the test to fail once we start running with `set -e`. Silence such\nfailures by ignoring failures here.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t1301-shared-repo.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/t1301-shared-repo.sh b/t/t1301-shared-repo.sh\nindex 630a47af21..0e0d07a1a1 100755\n--- a/t/t1301-shared-repo.sh\n+++ b/t/t1301-shared-repo.sh\n@@ -12,7 +12,7 @@ TEST_CREATE_REPO_NO_TEMPLATE=1\n . ./test-lib.sh\n \n # Remove a default ACL from the test dir if possible.\n-setfacl -k . 2>/dev/null\n+setfacl -k . 2>/dev/null || :\n \n # User must have read permissions to the repo -> failure on --shared=0400\n test_expect_success 'shared = 0400 (faulty permission u-w)' '\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541732","messageId":"20260416-b4-pks-tests-with-set-e-v3-10-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 10/12] t6002: fix use of `expr` with `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:27Z","receivedAt":"2026-04-16T11:20:06Z","isPatch":true,"body":"In `test_bisection_diff ()` we use `expr` to perform some math. This\ncommand has some gotchas though in that it will only return success when\nthe result is neither null nor zero. In some of our cases though it\nactually _is_ zero, and that will cause the expressions to fail once we\nenable `set -e`.\n\nPrepare for this change by instead using `$(( ))`, which doesn't have\nthe same issue. While at it, modernize the function a tiny bit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t6002-rev-list-bisect.sh | 17 ++++++++++-------\n 1 file changed, 10 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t6002-rev-list-bisect.sh b/t/t6002-rev-list-bisect.sh\nindex daa009c9a1..f2de40b5ed 100755\n--- a/t/t6002-rev-list-bisect.sh\n+++ b/t/t6002-rev-list-bisect.sh\n@@ -27,13 +27,16 @@ test_bisection_diff()\n \t# Test if bisection size is close to half of list size within\n \t# tolerance.\n \t#\n-\t_bisect_err=$(expr $_list_size - $_bisection_size \\* 2)\n-\ttest \"$_bisect_err\" -lt 0 && _bisect_err=$(expr 0 - $_bisect_err)\n-\t_bisect_err=$(expr $_bisect_err / 2) ; # floor\n-\n-\ttest_expect_success \\\n-\t\"bisection diff $_bisect_option $_head $* <= $_max_diff\" \\\n-\t'test $_bisect_err -le $_max_diff'\n+\t_bisect_err=$(($_list_size - $_bisection_size * 2))\n+\tif test \"$_bisect_err\" -lt 0\n+\tthen\n+\t\t_bisect_err=$((0 - $_bisect_err))\n+\tfi\n+\t_bisect_err=$(($_bisect_err / 2)) ; # floor\n+\n+\ttest_expect_success \"bisection diff $_bisect_option $_head $* <= $_max_diff\" '\n+\t\ttest $_bisect_err -le $_max_diff\n+\t'\n }\n \n date >path0\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541733","messageId":"20260416-b4-pks-tests-with-set-e-v3-11-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 11/12] t9902: fix use of `read` with `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:28Z","receivedAt":"2026-04-16T11:20:09Z","isPatch":true,"body":"In t9902 we're using the `read` builtin to read some values into a\nvariable. This is done by using `-d \"\"`, which cause us to read until\nthe end of the heredoc. There is a gotcha though: when the delimiter\nisn't found at all, then the read builtin will return an error. This\nhasn't been an issue until now as we didn't run with `set -e`, but\nthat'll change in a subsequent commit.\n\nPrepare for this change by silencing the error.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t9902-completion.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 2f9a597ec7..e3a7df7691 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -590,7 +590,7 @@ test_expect_success '__gitcomp - doesnt fail because of invalid variable name' '\n \t__gitcomp \"$invalid_variable_name\"\n '\n \n-read -r -d \"\" refs <<-\\EOF\n+read -r -d \"\" refs <<-\\EOF || :\n main\n maint\n next\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541734","messageId":"20260416-b4-pks-tests-with-set-e-v3-12-7a90e5dccadd@pks.im","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im","subject":"[PATCH v3 12/12] t: detect errors outside of test cases","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-16T11:19:29Z","receivedAt":"2026-04-16T11:20:13Z","isPatch":true,"body":"We have recently merged a patch series that had a simple misspelling of\n`test_expect_success`. Instead of making our tests fail though, this\ntypo went completely undetected and all of our tests passed, which is of\ncourse unfortunate. This is a more general issue with our test suite:\nall commands that run outside of a specific test case can fail, and if\nwe don't explicitly check for such failure then this failure will be\nsilently ignored.\n\nImprove the status quo by enabling the errexit option so that any such\nunchecked failures will cause us to abort immediately.\n\nNote that for now, we only enable this option for Bash 5 and newer. This\nis because other shells have wildly different behaviour, and older\nversions of Bash (especially on macOS) are buggy. The list of enabled\nshells may be extended going forward.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ci/run-build-and-tests.sh | 5 +++++\n t/test-lib.sh             | 9 +++++++++\n 2 files changed, 14 insertions(+)\n\ndiff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh\nindex 28cfe730ee..f0a3597184 100755\n--- a/ci/run-build-and-tests.sh\n+++ b/ci/run-build-and-tests.sh\n@@ -15,6 +15,11 @@ fedora-breaking-changes-musl|linux-breaking-changes)\n \tMESONFLAGS=\"$MESONFLAGS -Drust=enabled\"\n \t;;\n linux-TEST-vars)\n+\t# Ubuntu uses Dash by default, but we only enable use of `set -e`\n+\t# when using Bash 5+. Ensure that we have at least one CI job that uses\n+\t# it.\n+\texport TEST_SHELL_PATH=/usr/bin/bash\n+\n \texport OPENSSL_SHA1_UNSAFE=YesPlease\n \texport GIT_TEST_SPLIT_INDEX=yes\n \texport GIT_TEST_FULL_IN_PACK_ARRAY=true\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex de7d9e7b92..1f7868c537 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -15,6 +15,15 @@\n # You should have received a copy of the GNU General Public License\n # along with this program.  If not, see https://www.gnu.org/licenses/ .\n \n+# Enable the use of errexit so that any unexpected failures will cause us to\n+# abort tests, even when outside of a specific test case. Note that we only\n+# enable this on Bash 5 and newer, as `set -e` has wildly different behaviour\n+# across shells. The list of allowed shells may be extended going forward.\n+if test \"${BASH_VERSINFO:=0}\" -ge 5\n+then\n+\tset -e\n+fi\n+\n # Test the binaries we have just built.  The tests are kept in\n # t/ subdirectory and are run in 'trash directory' subdirectory.\n if test -z \"$TEST_DIRECTORY\"\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541745","messageId":"xmqqtstb3rf1.fsf@gitster.g","threadId":"65472","inReplyTo":"20260416054924.GB646814@coredump.intra.peff.net","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-16T14:34:26Z","receivedAt":"2026-04-16T14:34:29Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> I'm still concerned that this approach is going to create extra friction\n> for test writers down the road. This series needed to clean up several\n> spots to avoid false positives, and some of the spots were non-trivial.\n>\n> Now that was the accumulated cruft of 20 years of test-writing, so it's\n> not clear to me how often new test-writers will run into this. But when\n> they do, I worry that it may be hard to even figure out what is going\n> on.\n>\n> But I've said as much in earlier rounds, and I'm not sure Junio agrees.\n> So we can note my dissent in the captain's log, and I can reserve the\n> right to told-you-so later if need be. ;)\n\nThe alternatigve to allow us to be sloppy is alluring from the point\nof view of a test writer in me.  But do we have an easy/canned way\nto run tests and see the unexpected failures outside test_expect_foo\nwhile ignoring all the noises from passing tests?  Perhaps running\ntests (with and without prove) while redirecting the standard output\nstream to /dev/null or something?\n\n"},{"id":"541748","messageId":"xmqqpl3y3n60.fsf@gitster.g","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-12-7a90e5dccadd@pks.im","subject":"Re: [PATCH v3 12/12] t: detect errors outside of test cases","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-16T16:06:15Z","receivedAt":"2026-04-16T16:06:19Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>  linux-TEST-vars)\n> +\t# Ubuntu uses Dash by default, but we only enable use of `set -e`\n> +\t# when using Bash 5+. Ensure that we have at least one CI job that uses\n> +\t# it.\n> +\texport TEST_SHELL_PATH=/usr/bin/bash\n> +\n\nOK.\n\n> diff --git a/t/test-lib.sh b/t/test-lib.sh\n> index de7d9e7b92..1f7868c537 100644\n> --- a/t/test-lib.sh\n> +++ b/t/test-lib.sh\n> @@ -15,6 +15,15 @@\n>  # You should have received a copy of the GNU General Public License\n>  # along with this program.  If not, see https://www.gnu.org/licenses/ .\n>  \n> +# Enable the use of errexit so that any unexpected failures will cause us to\n> +# abort tests, even when outside of a specific test case. Note that we only\n> +# enable this on Bash 5 and newer, as `set -e` has wildly different behaviour\n> +# across shells. The list of allowed shells may be extended going forward.\n> +if test \"${BASH_VERSINFO:=0}\" -ge 5\n> +then\n> +\tset -e\n> +fi\n\nA possible alternative would be to actually test \"$TEST_SHELL_PATH\"\nwith the features we want to be working, e.g.,\n\n    out=$(\"$TEST_SHELL_PATH\" -e -c '\n\t# basics - \"set -e\" suppressed when \"tested\"\n\tfalse && echo 0\n\n\t# <20260414225206.GA3486072@coredump.intra.peff.net>\n\t# ancient bash dies after \"command false\"\n\tcommand false && echo 1\n\n\t# <20260416054038.GA646814@coredump.intra.peff.net>\n\t# failure inside a function excempt from \"set -e\"\n\tf () { false ; echo 2; }\n\tf || echo 3\n    ')\n    if test \"$out\" = 2\n    then\n            set -e\n    fi\n\nwhich adds some documentation value, but I think \"this one is\nknown-good, and we know we use it somewhere\" is probably good\nenough.\n"},{"id":"541769","messageId":"aeFCqhGIWQgE9lq6@szeder.dev","threadId":"65472","inReplyTo":"20260416-b4-pks-tests-with-set-e-v3-11-7a90e5dccadd@pks.im","subject":"Re: [PATCH v3 11/12] t9902: fix use of `read` with `set -e`","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-04-16T20:12:26Z","receivedAt":"2026-04-16T20:12:30Z","isPatch":true,"body":"On Thu, Apr 16, 2026 at 01:19:28PM +0200, Patrick Steinhardt wrote:\n> In t9902 we're using the `read` builtin to read some values into a\n> variable. This is done by using `-d \"\"`, which cause us to read until\n> the end of the heredoc. There is a gotcha though: when the delimiter\n> isn't found at all, then the read builtin will return an error.\n\nThe absence of the delimiter doesn't make \"read\" return an error, EOF\ndoes (that's why a \"while read ...\" loop works).\n\n> This\n> hasn't been an issue until now as we didn't run with `set -e`, but\n> that'll change in a subsequent commit.\n> \n> Prepare for this change by silencing the error.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  t/t9902-completion.sh | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n> \n> diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\n> index 2f9a597ec7..e3a7df7691 100755\n> --- a/t/t9902-completion.sh\n> +++ b/t/t9902-completion.sh\n> @@ -590,7 +590,7 @@ test_expect_success '__gitcomp - doesnt fail because of invalid variable name' '\n>  \t__gitcomp \"$invalid_variable_name\"\n>  '\n>  \n> -read -r -d \"\" refs <<-\\EOF\n> +read -r -d \"\" refs <<-\\EOF || :\n>  main\n>  maint\n>  next\n\nSo AFAICT what this \"read\" does is equivalent to:\n\nrefs='main\nmaint\nnext\nseen'\n\nIsn't this much easier to read!?  OK, the first ref is not aligned\nwith the rest...  But I admit I had to look up the docs to see what\nthe empty string as delimiter actually does, and even after that I had\nto add a printf '>%s<\\n' \"$refs\" command to the test script to see\nwhat's exactly going on.\n\n"},{"id":"541774","messageId":"xmqqbjfizlfx.fsf@gitster.g","threadId":"65472","inReplyTo":"aeFCqhGIWQgE9lq6@szeder.dev","subject":"Re: [PATCH v3 11/12] t9902: fix use of `read` with `set -e`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-16T20:42:26Z","receivedAt":"2026-04-16T20:42:29Z","isPatch":true,"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> On Thu, Apr 16, 2026 at 01:19:28PM +0200, Patrick Steinhardt wrote:\n>> In t9902 we're using the `read` builtin to read some values into a\n>> variable. This is done by using `-d \"\"`, which cause us to read until\n>> the end of the heredoc. There is a gotcha though: when the delimiter\n>> isn't found at all, then the read builtin will return an error.\n>\n> The absence of the delimiter doesn't make \"read\" return an error, EOF\n> does (that's why a \"while read ...\" loop works).\n\nVery true.  So for this kind of \"read\", failing is the norm.\n\n> So AFAICT what this \"read\" does is equivalent to:\n>\n> refs='main\n> maint\n> next\n> seen'\n>\n> Isn't this much easier to read!?  OK, the first ref is not aligned\n> with the rest...  But I admit I had to look up the docs to see what\n> the empty string as delimiter actually does, and even after that I had\n> to add a printf '>%s<\\n' \"$refs\" command to the test script to see\n> what's exactly going on.\n\nYes, whether \"set -e\" is used or not, not using \"read\" there makes a\nlot more sense.\n\n"},{"id":"541811","messageId":"aeIA6H2G3D2FoJPj@pks.im","threadId":"65472","inReplyTo":"xmqqbjfizlfx.fsf@gitster.g","subject":"Re: [PATCH v3 11/12] t9902: fix use of `read` with `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-17T09:44:08Z","receivedAt":"2026-04-17T09:44:14Z","isPatch":true,"body":"On Thu, Apr 16, 2026 at 01:42:26PM -0700, Junio C Hamano wrote:\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n> \n> > On Thu, Apr 16, 2026 at 01:19:28PM +0200, Patrick Steinhardt wrote:\n> >> In t9902 we're using the `read` builtin to read some values into a\n> >> variable. This is done by using `-d \"\"`, which cause us to read until\n> >> the end of the heredoc. There is a gotcha though: when the delimiter\n> >> isn't found at all, then the read builtin will return an error.\n> >\n> > The absence of the delimiter doesn't make \"read\" return an error, EOF\n> > does (that's why a \"while read ...\" loop works).\n> \n> Very true.  So for this kind of \"read\", failing is the norm.\n> \n> > So AFAICT what this \"read\" does is equivalent to:\n> >\n> > refs='main\n> > maint\n> > next\n> > seen'\n> >\n> > Isn't this much easier to read!?  OK, the first ref is not aligned\n> > with the rest...  But I admit I had to look up the docs to see what\n> > the empty string as delimiter actually does, and even after that I had\n> > to add a printf '>%s<\\n' \"$refs\" command to the test script to see\n> > what's exactly going on.\n> \n> Yes, whether \"set -e\" is used or not, not using \"read\" there makes a\n> lot more sense.\n\nHm, true indeed, that variant is a lot more straight-forward. Will\nchange, thanks!\n\nPatrick\n"},{"id":"541854","messageId":"20260418080111.GA3187789@coredump.intra.peff.net","threadId":"65472","inReplyTo":"xmqqtstb3rf1.fsf@gitster.g","subject":"Re: [PATCH 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-04-18T08:01:11Z","receivedAt":"2026-04-18T08:01:13Z","isPatch":true,"body":"On Thu, Apr 16, 2026 at 07:34:26AM -0700, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n> \n> > I'm still concerned that this approach is going to create extra friction\n> > for test writers down the road. This series needed to clean up several\n> > spots to avoid false positives, and some of the spots were non-trivial.\n> >\n> > Now that was the accumulated cruft of 20 years of test-writing, so it's\n> > not clear to me how often new test-writers will run into this. But when\n> > they do, I worry that it may be hard to even figure out what is going\n> > on.\n> >\n> > But I've said as much in earlier rounds, and I'm not sure Junio agrees.\n> > So we can note my dissent in the captain's log, and I can reserve the\n> > right to told-you-so later if need be. ;)\n> \n> The alternatigve to allow us to be sloppy is alluring from the point\n> of view of a test writer in me.  But do we have an easy/canned way\n> to run tests and see the unexpected failures outside test_expect_foo\n> while ignoring all the noises from passing tests?  Perhaps running\n> tests (with and without prove) while redirecting the standard output\n> stream to /dev/null or something?\n\nI took a stab at this. It's easy to do it hackily, but it proved a\nlittle trickier than I'd hoped to get something elegant.\n\nMy main goals were that you would still get good output to either the\nterminal or the test-results/*.out files (so we can't just redirect\nstderr for all runs), and that we wouldn't spend too much extra CPU (so\njust running the tests an extra time with stderr redirected is right\nout).\n\nMy first attempt was to teach test-lib.sh a mode where all of the test\nsnippets are noops, stderr goes to a file in test-results/, and at the\nend of the script we complain if our stderr file is non-empty. And then\nwe have a \"test-lint-stderr\" make target that runs each script in the\nspecial mode. That works, but:\n\n  1. Quite a few scripts do stuff inside their test snippets that affect\n     the environment, and then do stuff outside of a test snippet that\n     depends on that. Some of it is questionable, like running git\n     commands outside of test snippets to generate expected output. But\n     there's reasonable stuff like t0012 generating the list of builtin\n     inside a test snippet and expecting:\n\n       while read builtin\n       do\n               test_expect_success \"$builtin...\" ...\n       done <builtins\n\n     So I think the test suite is a little too free-form for this kind\n     of trickery.\n\n  2. It's way too slow, anyway. Even with all of the snippets as noops,\n     it still takes ~13s to run all of the scripts in parallel on my\n     machine. The real test suite only takes ~90 seconds! There's\n     probably low-hanging fruit to fix there, but it's still an order of\n     magnitude off what I'd hope the cost would be.\n\nSo I think that's probably a dead end.\n\nThe next obvious choice is redirecting stderr via tee or similar, which\nis easy-ish. We already do it for --verbose-log, but we combine it with\nstdout there (and we turn on verbose mode for the tests, which is going\nto generate a bunch of extra output).\n\nI think there's probably a way to do this with an extra tee for stderr.\nBut actually...do we even need tee? The idea is that we are not\nexpecting any output here, so if there is any, we'd bail and show it\nalong with an error.\n\nSo I think we might able able to get by with just redirecting descriptor\n2, and then making sure that test snippets stderr goes to the original\nstderr (which we already save as descriptor 7). And we catch only errors\noutside of the snippet.\n\nSomething like this seems to work:\n\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 1f7868c537..6657d56e52 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -728,7 +728,7 @@ then\n \texec 3>>\"$GIT_TEST_TEE_OUTPUT_FILE\" 4>&3\n elif test \"$verbose\" = \"t\"\n then\n-\texec 4>&2 3>&2\n+\texec 4>&7 3>&7\n else\n \texec 4>/dev/null 3>/dev/null\n fi\n@@ -970,7 +970,7 @@ maybe_setup_verbose () {\n \ttest -z \"$verbose_only\" && return\n \tif match_pattern_list $test_count \"$verbose_only\"\n \tthen\n-\t\texec 4>&2 3>&2\n+\t\texec 4>&7 3>&7\n \t\t# Emit a delimiting blank line when going from\n \t\t# non-verbose to verbose.  Within verbose mode the\n \t\t# delimiter is printed by test_expect_*.  The choice\n@@ -1052,7 +1052,7 @@ test_eval_ () {\n \t\t\ttest 1 = $trace_level_ && set +x\n \t\t\ttrace_level_=$(($trace_level_-1))\n \t\tfi\n-\t} 2>/dev/null 4>&2\n+\t} 2>/dev/null 4>&7\n \n \tif test \"$test_eval_ret_\" != 0 && want_trace\n \tthen\n@@ -1234,6 +1234,14 @@ test_done () {\n \t\tEOF\n \tfi\n \n+\tif test -s \"$TEST_RESULTS_BASE.stderr\"\n+\tthen\n+\t\tsay_color >&5 error \"FATAL: Unexpected output on stderr\"\n+\t\tsed >&5 's/^/# /' \"$TEST_RESULTS_BASE.stderr\"\n+\t\tGIT_EXIT_OK=1\n+\t\texit 1\n+\tfi\n+\n \tif test \"$test_fixed\" != 0\n \tthen\n \t\tsay_color error \"# $test_fixed known breakage(s) vanished; please update test(s)\"\n@@ -1982,3 +1990,9 @@ test_lazy_prereq FSMONITOR_DAEMON '\n \tgit version --build-options >output &&\n \tgrep \"feature: fsmonitor--daemon\" output\n '\n+\n+# Now that test-lib setup is done, direct our stderr to a file, as we want to\n+# catch and complain if anything ends up here. We can always access the\n+# original via descriptor 7.\n+mkdir -p \"$TEST_RESULTS_DIR\"\n+exec 2>\"$TEST_RESULTS_BASE.stderr\"\n\n\nIf I stick a \"test_expect_foobar\" into a test script, it is found:\n\n  $ ./t0001-init.sh\n  ok 1 - plain\n  [...]\n  ok 102 - re-init reads matching includeIf.onbranch\n  FATAL: Unexpected output on stderr\n  # ./t0001-init.sh: 983: test_expect_foobar: not found\n\nAnd if I run scripts with \"-v\" or \"-x\", that output still goes to the\nterminal and does not trigger the stderr checker. Likewise with\n--verbose-log, the log gets the verbose bits.\n\nIt _almost_ passes a full \"make test\", but there's one lingering\nproblem: random bits of code which want to output to stderr and bail,\nand use \"echo >&2\" to do so. But with our redirect, that stderr goes to\nthe log file. t0000-basic notices this, because it is testing the test\nsuite itself. And it notices that:\n\n  ./t0000-basic.sh --run=bogus\n\nshould complain to stderr, but now doesn't (it's swallowed by our log\nfile). We can fix that with;\n\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 6657d56e52..aa6415b308 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -902,13 +902,13 @@ match_test_selector_list () {\n \t\t\t\tif expr \"z${selector%%-*}\" : \"z[0-9]*[^0-9]\" >/dev/null\n \t\t\t\tthen\n \t\t\t\t\techo \"error: $operation: invalid non-numeric in range\" \\\n-\t\t\t\t\t\t\"start: '$orig_selector'\" >&2\n+\t\t\t\t\t\t\"start: '$orig_selector'\" >&7\n \t\t\t\t\texit 1\n \t\t\t\tfi\n \t\t\t\tif expr \"z${selector#*-}\" : \"z[0-9]*[^0-9]\" >/dev/null\n \t\t\t\tthen\n \t\t\t\t\techo \"error: $operation: invalid non-numeric in range\" \\\n-\t\t\t\t\t\t\"end: '$orig_selector'\" >&2\n+\t\t\t\t\t\t\"end: '$orig_selector'\" >&7\n \t\t\t\t\texit 1\n \t\t\t\tfi\n \t\t\t\t;;\n\nbut the same problem doubtless exists elsewhere. We could fix that\nglobally like this:\n\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 6657d56e52..72d60d9dac 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -762,6 +762,15 @@ die () {\n \t# test script run with '--immediate' fails, or when the user hits\n \t# ctrl-C, i.e. when 'test_done' is not invoked at all.\n \ttest_atexit_handler || code=$?\n+\n+\t# Dump the stderr log if we saw anything, since otherwise random\n+\t# errors will never make it to the user when we do not have\n+\t# a clean exit.\n+\tif test -s \"$TEST_RESULTS_BASE.stderr\"\n+\tthen\n+\t\tcat >&7 \"$TEST_RESULTS_BASE.stderr\"\n+\tfi\n+\n \tif test -n \"$GIT_EXIT_OK\"\n \tthen\n \t\texit $code\n\nIt's a little weird because the stderr message is delayed until we cat\nit out, but it makes sense. If we are bailing immediately, then the time\nbetween \"echo\" and \"cat\" is small. And if we are not bailing, then that\nis exactly the kind of bug our stderr log is trying to catch (and we\nwill complain during test_done).\n\nI am not quite sure if this is an elegant solution or a terrible hack.\n\n-Peff\n"},{"id":"541911","messageId":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH v5 00/12] t: detect errors outside of test cases","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:19Z","receivedAt":"2026-04-20T07:27:27Z","isPatch":true,"body":"Hi,\n\nthis is a follow-up to the recent discussion we had around `set -e` to\nmake our tests more robust and basically supersedes Junio's [1].\n\nI've tested the patches with both Bash and Dash, and all tests are\npassing on my machine with both of them. CI seems to be happy, as\nwell. But I would expect that this change probably has some fallout,\neven though I hope that it's generally going to be small and contained.\n\nThis series is based on 8c9303b1ff (Merge branch\n'jc/no-writev-does-not-work', 2026-04-10).\n\nI've created an MR with GitLab [2] and a PR with GitHub [3] to verify\nthat these changes work on both platforms.\n\nChanges in v5:\n  - Allow opting in via `GIT_TEST_USE_SET_E=yes` and enable this option\n    for Linux CI jobs.\n  - Another fix for a potentially-failing command.\n  - Link to v4: https://patch.msgid.link/20260417-b4-pks-tests-with-set-e-v4-0-44d43efdafb1@pks.im\n\nChanges in v4:\n  - Simplify how we read a multi-line variable value.\n  - Link to v3: https://patch.msgid.link/20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im\n\nChanges in v3:\n  - Adapt `linux-TEST-vars` job to use Bash instead of Dash. Ubuntu\n    packet mirrors seem to be having problems, so I wasn't able to get\n    past installing dependencies in any jobs. All to say that I couldn't\n    verify that this works as expected :/\n  - Link to v2: https://patch.msgid.link/20260415-b4-pks-tests-with-set-e-v2-0-4e4904a96f15@pks.im\n\nChanges in v2:\n  - Use `ret=0; $command || ret=$?` pattern.\n  - Restore `echo 0` in SIGPIPE tests.\n  - Fix \"lib-git-svn.sh\" to gracefully handle the case where SVN Perl\n    modules aren't installed.\n  - Use `|| :` consistently instead of `|| true`.\n  - Fix up a couple of tests that fail on FreeBSD 15. The test suite is\n    now passing on this system, too.\n  - Only enable `set -e` on Bash 5 and newer.\n  - Link to v1: https://patch.msgid.link/20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <20260325062114.2067946-1-gitster@pobox.com>\n[2]: https://gitlab.com/gitlab-org/git/-/merge_requests/541\n[3]: https://github.com/git/git/pull/2270\n\n---\nPatrick Steinhardt (12):\n      t: prepare `test_match_signal ()` calls for `set -e`\n      t: prepare `test_must_fail ()` for `set -e`\n      t: prepare `stop_git_daemon ()` for `set -e`\n      t: prepare `git config --unset` calls for `set -e`\n      t: prepare conditional test execution for `set -e`\n      t: prepare execution of potentially failing commands for `set -e`\n      t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`\n      t0008: silence error in subshell when using `grep -v`\n      t1301: don't fail in case setfacl(1) doesn't exist or fails\n      t6002: fix use of `expr` with `set -e`\n      t9902: fix use of `read` with `set -e`\n      t: detect errors outside of test cases\n\n ci/run-build-and-tests.sh          |  6 ++++++\n t/lib-git-daemon.sh                |  8 +++++---\n t/lib-git-svn.sh                   |  7 +++----\n t/lib-httpd.sh                     |  3 +--\n t/t0005-signals.sh                 |  4 ++--\n t/t0008-ignores.sh                 |  4 ++--\n t/t1301-shared-repo.sh             |  2 +-\n t/t1410-reflog.sh                  |  2 +-\n t/t3600-rm.sh                      |  2 +-\n t/t3901-i18n-patch.sh              |  3 ++-\n t/t4032-diff-inter-hunk-context.sh | 14 ++++++++------\n t/t5000-tar-tree.sh                |  4 ++--\n t/t6002-rev-list-bisect.sh         | 17 ++++++++++-------\n t/t7422-submodule-output.sh        |  2 +-\n t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n t/t7508-status.sh                  |  4 ++--\n t/t9138-git-svn-authors-prog.sh    |  4 ++--\n t/t9200-git-cvsexportcommit.sh     |  3 +--\n t/t9400-git-cvsserver-server.sh    |  5 +++--\n t/t9401-git-cvsserver-crlf.sh      |  4 ++--\n t/t9402-git-cvsserver-refs.sh      |  4 ++--\n t/t9902-completion.sh              |  6 ++----\n t/test-lib-functions.sh            | 12 ++++++------\n t/test-lib.sh                      | 35 +++++++++++++++++++++++++++++++----\n 24 files changed, 109 insertions(+), 70 deletions(-)\n\nRange-diff versus v4:\n\n 1:  c6503a6ecc =  1:  9f5238f960 t: prepare `test_match_signal ()` calls for `set -e`\n 2:  792e674fe2 =  2:  7c1beb70dc t: prepare `test_must_fail ()` for `set -e`\n 3:  106cf12fa0 =  3:  f209a2ecbb t: prepare `stop_git_daemon ()` for `set -e`\n 4:  1a5ac9b5d8 =  4:  7b6f7091f8 t: prepare `git config --unset` calls for `set -e`\n 5:  ecb6f07131 =  5:  e265d86fa0 t: prepare conditional test execution for `set -e`\n 6:  4468b5ef08 !  6:  04978dda41 t: prepare execution of potentially failing commands for `set -e`\n    @@ t/lib-httpd.sh: start_httpd() {\n      \t\tcat \"$HTTPD_ROOT_PATH\"/error.log >&4 2>/dev/null\n      \t\ttest_skip_or_die GIT_TEST_HTTPD \"web server setup failed\"\n     \n    + ## t/t1410-reflog.sh ##\n    +@@ t/t1410-reflog.sh: check_have () {\n    + }\n    + \n    + check_fsck () {\n    +-\tgit fsck --full >fsck.output\n    ++\tgit fsck --full >fsck.output || true\n    + \tcase \"$1\" in\n    + \t'')\n    + \t\ttest_must_be_empty fsck.output ;;\n    +\n      ## t/t3901-i18n-patch.sh ##\n     @@ t/t3901-i18n-patch.sh: check_encoding () {\n      \t\t8859)\n 7:  a188ac9564 =  7:  3c07c82032 t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`\n 8:  963b9f4a82 =  8:  43d67af0e5 t0008: silence error in subshell when using `grep -v`\n 9:  510d235080 =  9:  822857fd35 t1301: don't fail in case setfacl(1) doesn't exist or fails\n10:  550d9042bc = 10:  ad24432195 t6002: fix use of `expr` with `set -e`\n11:  9b23e69584 = 11:  88c8b888e1 t9902: fix use of `read` with `set -e`\n12:  55fc58c1ec <  -:  ---------- t: detect errors outside of test cases\n -:  ---------- > 12:  0de0c56caa t: detect errors outside of test cases\n\n---\nbase-commit: 8c9303b1ffae5b745d1b0a1f98330cf7944d8db0\nchange-id: 20260410-b4-pks-tests-with-set-e-3ae479b24b51\n\n"},{"id":"541912","messageId":"20260420-b4-pks-tests-with-set-e-v5-1-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 01/12] t: prepare `test_match_signal ()` calls for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:20Z","receivedAt":"2026-04-20T07:27:30Z","isPatch":true,"body":"We have a couple of calls to `test_match_signal ()` where we execute a\nGit command and expect it to die with a specific signal. These calls\nwill essentially execute the process in a subshell via `foo; echo $?`,\nbut as we expect `foo` to fail this will cause the overall subshell to\nfail once we `set -e`.\n\nFix this issue by using `foo && echo 0 || echo $?` instead.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0005-signals.sh | 4 ++--\n t/t3600-rm.sh      | 2 +-\n 2 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t0005-signals.sh b/t/t0005-signals.sh\nindex afba0fc3fc..84319cf169 100755\n--- a/t/t0005-signals.sh\n+++ b/t/t0005-signals.sh\n@@ -42,12 +42,12 @@ test_expect_success 'create blob' '\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE' '\n-\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE even if parent ignores it' '\n-\tOUT=$( ((trap \"\" PIPE && large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 1f16e6b522..a371ea690e 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -260,7 +260,7 @@ test_expect_success 'choking \"git rm\" should not let it die with cruft (induce S\n \n test_expect_success !MINGW 'choking \"git rm\" should not let it die with cruft (induce and check SIGPIPE)' '\n \tchoke_git_rm_setup &&\n-\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\"; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\" && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\" &&\n \ttest_path_is_missing .git/index.lock\n '\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541913","messageId":"20260420-b4-pks-tests-with-set-e-v5-2-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:21Z","receivedAt":"2026-04-20T07:27:33Z","isPatch":true,"body":"The helper function `test_must_fail ()` executes a specific Git command\nthat may or may not fail in a specific way. This is done by executing\nthe command in question and then comparing its exit code against a set\nof conditions.\n\nThis works, but once we run our test suite with `set -e` we may bail out\nof `test_must_fail ()` early in case the command actually fails, even\nthough we expect it to fail. Prepare for this change by handling the\nfailed case with `||`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/test-lib-functions.sh | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex f3af10fb7e..5fd5494ef1 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1195,8 +1195,9 @@ test_must_fail () {\n \t\techo >&7 \"test_must_fail: only 'git' is allowed: $*\"\n \t\treturn 1\n \tfi\n-\t\"$@\" 2>&7\n-\texit_code=$?\n+\n+\texit_code=0; \"$@\" 2>&7 || exit_code=$?\n+\n \tif test $exit_code -eq 0 && ! list_contains \"$_test_ok\" success\n \tthen\n \t\techo >&4 \"test_must_fail: command succeeded: $*\"\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541914","messageId":"20260420-b4-pks-tests-with-set-e-v5-3-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 03/12] t: prepare `stop_git_daemon ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:22Z","receivedAt":"2026-04-20T07:27:36Z","isPatch":true,"body":"We have a couple of calls to `stop_git_daemon ()` outside of specific\ntest cases that will kill a backgrounded git-daemon(1) process and\nexpect the process with a specific error code. While these function\ncalls do end up killing git-daemon(1), the error handling we have in\nthose contexts is basically ineffective. So while we expect the process\nto exit with a specific error code, we will just continue with any error\nin case it doesn't.\n\nThis will change once we enable `set -e` in a subsequent commit. There's\ntwo issues though that will make this _always_ fail:\n\n  - Our call to `wait` is expected to fail, but because it's not part of\n    a condition it will cause us to bail out immediately with `set -e`.\n\n  - We try to kill git-daemon(1) a second time via the pidfile. We can\n    generally expect that this is the same PID though as we had in the\n    \"GIT_DAEMON_PID\" environment variable, and thus it's more likely\n    than not that we have already killed it, and the call to kill will\n    fail.\n\nPrepare for this change by handling the failure of `wait` with `||` and\nby silencing failures of the second call to `kill`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/lib-git-daemon.sh | 8 +++++---\n 1 file changed, 5 insertions(+), 3 deletions(-)\n\ndiff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh\nindex e62569222b..d172aa51f0 100644\n--- a/t/lib-git-daemon.sh\n+++ b/t/lib-git-daemon.sh\n@@ -85,14 +85,16 @@ stop_git_daemon() {\n \n \t# kill git-daemon child of git\n \tsay >&3 \"Stopping git daemon ...\"\n+\n \tkill \"$GIT_DAEMON_PID\"\n-\twait \"$GIT_DAEMON_PID\" >&3 2>&4\n-\tret=$?\n+\tret=0; wait \"$GIT_DAEMON_PID\" >&3 2>&4 || ret=$?\n+\n \tif ! test_match_signal 15 $ret\n \tthen\n \t\terror \"git daemon exited with status: $ret\"\n \tfi\n-\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null\n+\n+\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null || :\n \tGIT_DAEMON_PID=\n \trm -f git_daemon_output \"$GIT_DAEMON_PIDFILE\"\n }\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541915","messageId":"20260420-b4-pks-tests-with-set-e-v5-4-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 04/12] t: prepare `git config --unset` calls for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:23Z","receivedAt":"2026-04-20T07:27:39Z","isPatch":true,"body":"We have a couple of calls to `git config --unset` that ultimately end up\nas no-ops as the configuration variables aren't set (anymore) in the\nfirst place. These calls are mostly intended to recover unconditionally\nfrom tests that may have executed only partially, but they'll ultimately\nfail during a normal test run.\n\nThis hasn't been a problem until now as we aren't running tests with\n`set -e`. This is about to change though, so let's silence the case\nwhere we cannot unset the config keys.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t4032-diff-inter-hunk-context.sh | 2 +-\n t/t7508-status.sh                  | 4 ++--\n t/t9138-git-svn-authors-prog.sh    | 4 ++--\n 3 files changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\nindex bada0cbd32..c98eb6abb2 100755\n--- a/t/t4032-diff-inter-hunk-context.sh\n+++ b/t/t4032-diff-inter-hunk-context.sh\n@@ -17,7 +17,7 @@ f() {\n \n t() {\n \tuse_config=\n-\tgit config --unset diff.interHunkContext\n+\tgit config --unset diff.interHunkContext || :\n \n \tcase $# in\n \t4) hunks=$4; cmd=\"diff -U$3\";;\ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex a5e21bf8bf..1167b835a4 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -773,8 +773,8 @@ test_expect_success TTY 'status --porcelain ignores color.status' '\n '\n \n # recover unconditionally from color tests\n-git config --unset color.status\n-git config --unset color.ui\n+git config --unset color.status || :\n+git config --unset color.ui || :\n \n test_expect_success 'status --porcelain respects -b' '\n \ndiff --git a/t/t9138-git-svn-authors-prog.sh b/t/t9138-git-svn-authors-prog.sh\nindex 784ec7fc2d..5bb38cb23a 100755\n--- a/t/t9138-git-svn-authors-prog.sh\n+++ b/t/t9138-git-svn-authors-prog.sh\n@@ -68,8 +68,8 @@ test_expect_success 'authors-file overrode authors-prog' '\n \t)\n '\n \n-git --git-dir=x/.git config --unset svn.authorsfile\n-git --git-dir=x/.git config --unset svn.authorsprog\n+git --git-dir=x/.git config --unset svn.authorsfile || :\n+git --git-dir=x/.git config --unset svn.authorsprog || :\n \n test_expect_success 'authors-prog imported user without email' '\n \tsvn mkdir -m gg --username gg-hermit \"$svnrepo\"/gg &&\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541916","messageId":"20260420-b4-pks-tests-with-set-e-v5-5-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 05/12] t: prepare conditional test execution for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:24Z","receivedAt":"2026-04-20T07:27:42Z","isPatch":true,"body":"We have some test in our test suite where we use the pattern of\n`test ... && test_expect_succeess` to conditionally execute a test. The\nproblem is that when we decide to not execute the test, we'll indeed\nskip the test, but the overall statement will also be unsuccessful. This\nwill become a problem once we enable `set -e`.\n\nPrepare for this future by turning this into a proper conditional, which\nis also a bit easier to read overall.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t4032-diff-inter-hunk-context.sh | 12 +++++++-----\n t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n 2 files changed, 20 insertions(+), 16 deletions(-)\n\ndiff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\nindex c98eb6abb2..2d216fb70f 100755\n--- a/t/t4032-diff-inter-hunk-context.sh\n+++ b/t/t4032-diff-inter-hunk-context.sh\n@@ -40,11 +40,13 @@ t() {\n \t\ttest $(git $cmd $file | grep '^@@ ' | wc -l) = $hunks\n \t\"\n \n-\ttest -f $expected &&\n-\ttest_expect_success \"$label: check output\" \"\n-\t\tgit $cmd $file | grep -v '^index ' >actual &&\n-\t\ttest_cmp $expected actual\n-\t\"\n+\tif test -f $expected\n+\tthen\n+\t\ttest_expect_success \"$label: check output\" \"\n+\t\t\tgit $cmd $file | grep -v '^index ' >actual &&\n+\t\t\ttest_cmp $expected actual\n+\t\t\"\n+\tfi\n }\n \n cat <<EOF >expected.f1.0.1 || exit 1\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex f512eed278..8cc86522b2 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -220,17 +220,19 @@ check_dotx_symlink () {\n \t\t)\n \t'\n \n-\ttest -n \"$refuse_index\" &&\n-\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n-\t\ttest_must_fail \\\n-\t\t\tgit -C $dir \\\n-\t\t\t    -c core.protectntfs \\\n-\t\t\t    -c core.protecthfs \\\n-\t\t\t    read-tree $tree 2>err &&\n-\t\tgrep \"invalid path.*$name\" err &&\n-\t\tgit -C $dir ls-files -s >out &&\n-\t\ttest_must_be_empty out\n-\t'\n+\tif test -n \"$refuse_index\"\n+\tthen\n+\t\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n+\t\t\ttest_must_fail \\\n+\t\t\t\tgit -C $dir \\\n+\t\t\t\t    -c core.protectntfs \\\n+\t\t\t\t    -c core.protecthfs \\\n+\t\t\t\t    read-tree $tree 2>err &&\n+\t\t\tgrep \"invalid path.*$name\" err &&\n+\t\t\tgit -C $dir ls-files -s >out &&\n+\t\t\ttest_must_be_empty out\n+\t\t'\n+\tfi\n }\n \n check_dotx_symlink gitmodules vanilla .gitmodules\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541917","messageId":"20260420-b4-pks-tests-with-set-e-v5-6-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:25Z","receivedAt":"2026-04-20T07:27:45Z","isPatch":true,"body":"Several of our tests verify whether a certain binary can be executed,\npotentially skipping tests in case we cannot, for example because the\nbinary doesn't exist. In those cases we often run the binary outside of\nany conditionally.\n\nThis will start to fail once we enable `set -e`, as that will cause us\nto bail out the test immediately. Improve these tests by executing them\ninside of a conditional instead.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/lib-git-svn.sh                |  7 +++----\n t/lib-httpd.sh                  |  3 +--\n t/t1410-reflog.sh               |  2 +-\n t/t3901-i18n-patch.sh           |  3 ++-\n t/t5000-tar-tree.sh             |  4 ++--\n t/t7422-submodule-output.sh     |  2 +-\n t/t9200-git-cvsexportcommit.sh  |  3 +--\n t/t9400-git-cvsserver-server.sh |  5 +++--\n t/t9401-git-cvsserver-crlf.sh   |  4 ++--\n t/t9402-git-cvsserver-refs.sh   |  4 ++--\n t/test-lib-functions.sh         |  3 +--\n t/test-lib.sh                   | 10 ++++++----\n 12 files changed, 25 insertions(+), 25 deletions(-)\n\ndiff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh\nindex 2fde2353fd..52843f667d 100644\n--- a/t/lib-git-svn.sh\n+++ b/t/lib-git-svn.sh\n@@ -15,8 +15,7 @@ GIT_SVN_DIR=$GIT_DIR/svn/refs/remotes/git-svn\n SVN_TREE=$GIT_SVN_DIR/svn-tree\n test_set_port SVNSERVE_PORT\n \n-svn >/dev/null 2>&1\n-if test $? -ne 1\n+if ! svn help >/dev/null 2>&1\n then\n \tskip_all='skipping git svn tests, svn not found'\n \ttest_done\n@@ -27,13 +26,13 @@ export svnrepo\n svnconf=$PWD/svnconf\n export svnconf\n \n+x=0\n perl -w -e \"\n use SVN::Core;\n use SVN::Repos;\n \\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n system(qw/svnadmin create --fs-type fsfs/, \\$ENV{svnrepo}) == 0 or exit(41);\n-\" >&3 2>&4\n-x=$?\n+\" >&3 2>&4 || x=$?\n if test $x -ne 0\n then\n \tif test $x -eq 42; then\ndiff --git a/t/lib-httpd.sh b/t/lib-httpd.sh\nindex 4c76e813e3..fc646447d5 100644\n--- a/t/lib-httpd.sh\n+++ b/t/lib-httpd.sh\n@@ -235,11 +235,10 @@ start_httpd() {\n \n \ttest_atexit stop_httpd\n \n-\t\"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n+\tif ! \"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n \t\t-f \"$TEST_PATH/apache.conf\" $HTTPD_PARA \\\n \t\t-c \"Listen 127.0.0.1:$LIB_HTTPD_PORT\" -k start \\\n \t\t>&3 2>&4\n-\tif test $? -ne 0\n \tthen\n \t\tcat \"$HTTPD_ROOT_PATH\"/error.log >&4 2>/dev/null\n \t\ttest_skip_or_die GIT_TEST_HTTPD \"web server setup failed\"\ndiff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh\nindex ce71f9a30a..f289fc11e9 100755\n--- a/t/t1410-reflog.sh\n+++ b/t/t1410-reflog.sh\n@@ -23,7 +23,7 @@ check_have () {\n }\n \n check_fsck () {\n-\tgit fsck --full >fsck.output\n+\tgit fsck --full >fsck.output || true\n \tcase \"$1\" in\n \t'')\n \t\ttest_must_be_empty fsck.output ;;\ndiff --git a/t/t3901-i18n-patch.sh b/t/t3901-i18n-patch.sh\nindex f03601b49a..ef7d7e1edc 100755\n--- a/t/t3901-i18n-patch.sh\n+++ b/t/t3901-i18n-patch.sh\n@@ -28,7 +28,8 @@ check_encoding () {\n \t\t8859)\n \t\t\tgrep \"^encoding ISO8859-1\" ;;\n \t\t*)\n-\t\t\tgrep \"^encoding ISO8859-1\"; test \"$?\" != 0 ;;\n+\t\t\tret=0; grep \"^encoding ISO8859-1\" || ret=$?\n+\t\t\ttest \"$ret\" != 0 ;;\n \t\tesac || return 1\n \t\tj=$i\n \t\ti=$(($i+1))\ndiff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh\nindex 5465054f17..a8c28533dc 100755\n--- a/t/t5000-tar-tree.sh\n+++ b/t/t5000-tar-tree.sh\n@@ -503,8 +503,8 @@ test_expect_success LONG_IS_64BIT 'set up repository with huge blob' '\n # would generate the whole 64GB).\n test_expect_success LONG_IS_64BIT 'generate tar with huge size' '\n \t{\n-\t\tgit archive HEAD\n-\t\techo $? >exit-code\n+\t\t{ ret=0 && git archive HEAD || ret=$?; } &&\n+\t\techo \"$ret\" >exit-code\n \t} | test_copy_bytes 4096 >huge.tar &&\n \techo 141 >expect &&\n \ttest_cmp expect exit-code\ndiff --git a/t/t7422-submodule-output.sh b/t/t7422-submodule-output.sh\nindex aea1ddf117..852136fdfd 100755\n--- a/t/t7422-submodule-output.sh\n+++ b/t/t7422-submodule-output.sh\n@@ -198,7 +198,7 @@ test_expect_success !MINGW 'git submodule status --recursive propagates SIGPIPE'\n \t(\n \t\tcd repo &&\n \t\tGIT_ALLOW_PROTOCOL=file git submodule add \"$(pwd)\"/../submodule &&\n-\t\t{ git submodule status --recursive 2>err; echo $?>status; } |\n+\t\t{ { ret=0 && git submodule status --recursive 2>err || ret=$?; } && echo $ret >status; } |\n \t\t\tgrep -q recursive-submodule-path-1 &&\n \t\ttest_must_be_empty err &&\n \t\ttest_match_signal 13 \"$(cat status)\"\ndiff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh\nindex 14cbe96527..581cf3d28f 100755\n--- a/t/t9200-git-cvsexportcommit.sh\n+++ b/t/t9200-git-cvsexportcommit.sh\n@@ -11,8 +11,7 @@ if ! test_have_prereq PERL; then\n \ttest_done\n fi\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git cvsexportcommit tests, cvs not found'\n     test_done\ndiff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh\nindex e499c7f955..4b45398bab 100755\n--- a/t/t9400-git-cvsserver-server.sh\n+++ b/t/t9400-git-cvsserver-server.sh\n@@ -17,12 +17,13 @@ if ! test_have_prereq PERL; then\n \tskip_all='skipping git cvsserver tests, perl not available'\n \ttest_done\n fi\n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git-cvsserver tests, cvs not found'\n     test_done\n fi\n+\n perl -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {\n     skip_all='skipping git-cvsserver tests, Perl SQLite interface unavailable'\n     test_done\ndiff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh\nindex a34805acdc..6b4cbb1651 100755\n--- a/t/t9401-git-cvsserver-crlf.sh\n+++ b/t/t9401-git-cvsserver-crlf.sh\n@@ -60,12 +60,12 @@ check_status_options() {\n     return $stat\n }\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git-cvsserver tests, cvs not found'\n     test_done\n fi\n+\n if ! test_have_prereq PERL\n then\n     skip_all='skipping git-cvsserver tests, perl not available'\ndiff --git a/t/t9402-git-cvsserver-refs.sh b/t/t9402-git-cvsserver-refs.sh\nindex 2ee41f9443..65f2ceedec 100755\n--- a/t/t9402-git-cvsserver-refs.sh\n+++ b/t/t9402-git-cvsserver-refs.sh\n@@ -68,12 +68,12 @@ check_diff() {\n \n #########\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n \tskip_all='skipping git-cvsserver tests, cvs not found'\n \ttest_done\n fi\n+\n if ! test_have_prereq PERL\n then\n \tskip_all='skipping git-cvsserver tests, perl not available'\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex 5fd5494ef1..879ee1ee59 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1248,8 +1248,7 @@ test_might_fail () {\n test_expect_code () {\n \twant_code=$1\n \tshift\n-\t\"$@\" 2>&7\n-\texit_code=$?\n+\texit_code=0; \"$@\" 2>&7 || exit_code=$?\n \tif test $exit_code = $want_code\n \tthen\n \t\treturn 0\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 70fd3e9baf..de7d9e7b92 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -143,8 +143,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n ################################################################\n # It appears that people try to run tests without building...\n GIT_BINARY=\"${GIT_TEST_INSTALLED:-$GIT_BUILD_DIR}/git$X\"\n-\"$GIT_BINARY\" >/dev/null\n-if test $? != 1\n+\n+if ! \"$GIT_BINARY\" version >/dev/null\n then\n \tif test -n \"$GIT_TEST_INSTALLED\"\n \tthen\n@@ -454,8 +454,10 @@ then\n \t# from any previous runs.\n \t>\"$GIT_TEST_TEE_OUTPUT_FILE\"\n \n-\t(GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} \"$0\" \"$@\" 2>&1;\n-\t echo $? >\"$TEST_RESULTS_BASE.exit\") | tee -a \"$GIT_TEST_TEE_OUTPUT_FILE\"\n+\t(\n+\t\tret=0 && GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} \"$0\" \"$@\" 2>&1 || ret=$?\n+\t\techo \"$ret\" >\"$TEST_RESULTS_BASE.exit\"\n+\t) | tee -a \"$GIT_TEST_TEE_OUTPUT_FILE\"\n \ttest \"$(cat \"$TEST_RESULTS_BASE.exit\")\" = 0\n \texit\n fi\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541918","messageId":"20260420-b4-pks-tests-with-set-e-v5-7-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 07/12] t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:26Z","receivedAt":"2026-04-20T07:27:48Z","isPatch":true,"body":"Both `test_when_finished ()` and `test_atexit ()` build up a chain of\ncleanup commands by prepending each new command to the existing cleanup\nstring. To preserve the exit code of the test body across cleanup\nexecution, we append the following logic:\n\n    } && (exit \"$eval_ret\"); eval_ret=$?; ...\n\nThe intent of this is to run the cleanup block and then unconditionally\nrestore `eval_ret`. The original behaviour of this is is:\n\n   +------------------+---------+------------------------------------+\n   |test body         │ cleanup │ old behaviour                      │\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | pass    │ && taken -> (exit 0) -> eval_ret=0 |\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | fail    │ && not taken -> eval_ret=$?        |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | pass    │ && taken -> (exit 1) -> eval_ret=1 |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | fail    | && not taken -> eval_ret=$?        |\n   +------------------+---------+------------------------------------+\n\nThis logic will start to fail once we enable `set -e`. When `$eval_ret`\nis non-zero, the subshell we create will fail, and with `set -e` we'll\nthus bail out without evaluating the logic after the semicolon.\n\nFix this issue by instead using `|| eval_ret=\\$?; ...`. Besides being\na bit simpler, it also retains the original behaviour:\n\n   +------------------+---------+------------------------------------+\n   |test body         │ cleanup │ old behaviour                      │\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | pass    │ || not taken -> eval_ret unchanged |\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | fail    │ || taken -> eval_ret=$?            |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | pass    │ || not taken -> eval_ret unchanged |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | fail    | || taken -> eval_ret=$?            |\n   +------------------+---------+------------------------------------+\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/test-lib-functions.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex 879ee1ee59..502bb0ddcb 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1512,7 +1512,7 @@ test_when_finished () {\n \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n \tBUG \"test_when_finished does nothing in a subshell\"\n \ttest_cleanup=\"{ $*\n-\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_cleanup\"\n+\t\t} || eval_ret=\\$?; $test_cleanup\"\n }\n \n # This function can be used to schedule some commands to be run\n@@ -1540,7 +1540,7 @@ test_atexit () {\n \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n \tBUG \"test_atexit does nothing in a subshell\"\n \ttest_atexit_cleanup=\"{ $*\n-\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_atexit_cleanup\"\n+\t\t} || eval_ret=\\$?; $test_atexit_cleanup\"\n }\n \n # Deprecated wrapper for \"git init\", use \"git init\" directly instead\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541919","messageId":"20260420-b4-pks-tests-with-set-e-v5-8-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 08/12] t0008: silence error in subshell when using `grep -v`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:27Z","receivedAt":"2026-04-20T07:27:52Z","isPatch":true,"body":"In t0008 we use `grep -v` in a subshell, but expect that this command\nwill sometimes not match anything. This would cause grep(1) to return an\nerror code, but given that we don't run with `set -e` we swallow this\nerror.\n\nWe're about to enable `set -e`. Prepare for this by ignoring any errors.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0008-ignores.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh\nindex e716b5cdfa..d77a179bdd 100755\n--- a/t/t0008-ignores.sh\n+++ b/t/t0008-ignores.sh\n@@ -122,8 +122,8 @@ test_expect_success_multiple () {\n \tfi\n \ttestname=\"$1\" expect_all=\"$2\" code=\"$3\"\n \n-\texpect_verbose=$( echo \"$expect_all\" | grep -v '^::\t' )\n-\texpect=$( echo \"$expect_verbose\" | sed -e 's/.*\t//' )\n+\texpect_verbose=$(echo \"$expect_all\" | grep -v '^::\t' || :)\n+\texpect=$(echo \"$expect_verbose\" | sed -e 's/.*\t//')\n \n \ttest_expect_success $prereq \"$testname${no_index_opt:+ with $no_index_opt}\" '\n \t\texpect \"$expect\" &&\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541920","messageId":"20260420-b4-pks-tests-with-set-e-v5-9-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 09/12] t1301: don't fail in case setfacl(1) doesn't exist or fails","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:28Z","receivedAt":"2026-04-20T07:27:55Z","isPatch":true,"body":"In t1301 we're trying to remove any potentially-existing default ACLs\nthat might exist on the transh directory by executing setfacl(1).\nAccording to 8ed0a740dd (t1301-shared-repo.sh: don't let a default ACL\ninterfere with the test, 2008-10-16), this is done because we play\naround with permissions and umasks in this test suite.\n\nThe setfacl(1) binary may not exist on some systems though, even though\ntests ultimately still pass. This doesn't matter currently, but will\ncause the test to fail once we start running with `set -e`. Silence such\nfailures by ignoring failures here.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t1301-shared-repo.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/t1301-shared-repo.sh b/t/t1301-shared-repo.sh\nindex 630a47af21..0e0d07a1a1 100755\n--- a/t/t1301-shared-repo.sh\n+++ b/t/t1301-shared-repo.sh\n@@ -12,7 +12,7 @@ TEST_CREATE_REPO_NO_TEMPLATE=1\n . ./test-lib.sh\n \n # Remove a default ACL from the test dir if possible.\n-setfacl -k . 2>/dev/null\n+setfacl -k . 2>/dev/null || :\n \n # User must have read permissions to the repo -> failure on --shared=0400\n test_expect_success 'shared = 0400 (faulty permission u-w)' '\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541921","messageId":"20260420-b4-pks-tests-with-set-e-v5-10-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 10/12] t6002: fix use of `expr` with `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:29Z","receivedAt":"2026-04-20T07:27:57Z","isPatch":true,"body":"In `test_bisection_diff ()` we use `expr` to perform some math. This\ncommand has some gotchas though in that it will only return success when\nthe result is neither null nor zero. In some of our cases though it\nactually _is_ zero, and that will cause the expressions to fail once we\nenable `set -e`.\n\nPrepare for this change by instead using `$(( ))`, which doesn't have\nthe same issue. While at it, modernize the function a tiny bit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t6002-rev-list-bisect.sh | 17 ++++++++++-------\n 1 file changed, 10 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t6002-rev-list-bisect.sh b/t/t6002-rev-list-bisect.sh\nindex daa009c9a1..f2de40b5ed 100755\n--- a/t/t6002-rev-list-bisect.sh\n+++ b/t/t6002-rev-list-bisect.sh\n@@ -27,13 +27,16 @@ test_bisection_diff()\n \t# Test if bisection size is close to half of list size within\n \t# tolerance.\n \t#\n-\t_bisect_err=$(expr $_list_size - $_bisection_size \\* 2)\n-\ttest \"$_bisect_err\" -lt 0 && _bisect_err=$(expr 0 - $_bisect_err)\n-\t_bisect_err=$(expr $_bisect_err / 2) ; # floor\n-\n-\ttest_expect_success \\\n-\t\"bisection diff $_bisect_option $_head $* <= $_max_diff\" \\\n-\t'test $_bisect_err -le $_max_diff'\n+\t_bisect_err=$(($_list_size - $_bisection_size * 2))\n+\tif test \"$_bisect_err\" -lt 0\n+\tthen\n+\t\t_bisect_err=$((0 - $_bisect_err))\n+\tfi\n+\t_bisect_err=$(($_bisect_err / 2)) ; # floor\n+\n+\ttest_expect_success \"bisection diff $_bisect_option $_head $* <= $_max_diff\" '\n+\t\ttest $_bisect_err -le $_max_diff\n+\t'\n }\n \n date >path0\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541922","messageId":"20260420-b4-pks-tests-with-set-e-v5-11-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 11/12] t9902: fix use of `read` with `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:30Z","receivedAt":"2026-04-20T07:28:00Z","isPatch":true,"body":"In t9902 we're using the `read` builtin to read some values into a\nvariable. This is done by using `-d \"\"`, which cause us to read until\nthe end of the heredoc. As the read is terminated by EOF, the command\nwill end up returning a non-zero error code. This hasn't been an issue\nuntil now as we didn't run with `set -e`, but that'll change in a\nsubsequent commit.\n\nPrepare for this change by not using read at all, as we can simply store\nthe multi-line value directly.\n\nSuggested-by: SZEDER Gábor <szeder.dev@gmail.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t9902-completion.sh | 6 ++----\n 1 file changed, 2 insertions(+), 4 deletions(-)\n\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 2f9a597ec7..28f61f08fb 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -590,12 +590,10 @@ test_expect_success '__gitcomp - doesnt fail because of invalid variable name' '\n \t__gitcomp \"$invalid_variable_name\"\n '\n \n-read -r -d \"\" refs <<-\\EOF\n-main\n+refs='main\n maint\n next\n-seen\n-EOF\n+seen'\n \n test_expect_success '__gitcomp_nl - trailing space' '\n \ttest_gitcomp_nl \"m\" \"$refs\" <<-EOF\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541923","messageId":"20260420-b4-pks-tests-with-set-e-v5-12-7d3d68292f6b@pks.im","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"[PATCH v5 12/12] t: detect errors outside of test cases","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-20T07:27:31Z","receivedAt":"2026-04-20T07:28:03Z","isPatch":true,"body":"We have recently merged a patch series that had a simple misspelling of\n`test_expect_success`. Instead of making our tests fail though, this\ntypo went completely undetected and all of our tests passed, which is of\ncourse unfortunate. This is a more general issue with our test suite:\nall commands that run outside of a specific test case can fail, and if\nwe don't explicitly check for such failure then this failure will be\nsilently ignored.\n\nImprove the status quo by enabling the errexit option so that any such\nunchecked failures will cause us to abort immediately.\n\nNote that for now, we only enable this option for Bash 5 and newer. This\nis because other shells have wildly different behaviour, and older\nversions of Bash (especially on macOS) are buggy. The list of enabled\nshells may be extended going forward.\n\nHelped-by: Jeff King <peff@peff.net>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ci/run-build-and-tests.sh |  6 ++++++\n t/test-lib.sh             | 25 +++++++++++++++++++++++++\n 2 files changed, 31 insertions(+)\n\ndiff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh\nindex 28cfe730ee..de08a08d59 100755\n--- a/ci/run-build-and-tests.sh\n+++ b/ci/run-build-and-tests.sh\n@@ -7,6 +7,12 @@\n \n export TEST_CONTRIB_TOO=yes\n \n+case \"$jobname\" in\n+almalinux-*|debian-*|fedora-*|linux-*)\n+\texport GIT_TEST_USE_SET_E=yes\n+\t;;\n+esac\n+\n case \"$jobname\" in\n fedora-breaking-changes-musl|linux-breaking-changes)\n \texport WITH_BREAKING_CHANGES=YesPlease\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex de7d9e7b92..cded7bd693 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -15,6 +15,31 @@\n # You should have received a copy of the GNU General Public License\n # along with this program.  If not, see https://www.gnu.org/licenses/ .\n \n+# Enable the use of errexit so that any unexpected failures will cause us to\n+# abort tests, even when outside of a specific test case.\n+#\n+# Note that we only enable this on Bash 5 and newer, or when explicitly\n+# requested by the user via `GIT_TEST_USE_SET_E=true`. This ib secause `set -e`\n+# has wildly different behaviour across shells. The list of default-enabled\n+# shells may be extended going forward.\n+if test -z \"$GIT_TEST_USE_SET_E\" && test \"${BASH_VERSINFO:=0}\" -ge 5\n+then\n+\tGIT_TEST_USE_SET_E=true\n+fi\n+\n+# We cannot use `test-tool env-helper` here, as it's not yet available.\n+case \"${GIT_TEST_USE_SET_E:-false}\" in\n+1|on|true|yes)\n+\tset -e\n+\t;;\n+0|off|false|no)\n+\t;;\n+*)\n+\techo \"GIT_TEST_USE_SET_E requires a boolean\" >&2\n+\texit 1\n+\t;;\n+esac\n+\n # Test the binaries we have just built.  The tests are kept in\n # t/ subdirectory and are run in 'trash directory' subdirectory.\n if test -z \"$TEST_DIRECTORY\"\n\n-- \n2.54.0.rc2.529.gd9106f7525.dirty\n\n"},{"id":"541984","messageId":"xmqqfr4pr4e7.fsf@gitster.g","threadId":"65472","inReplyTo":"20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im","subject":"Re: [PATCH v5 00/12] t: detect errors outside of test cases","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-20T16:19:12Z","receivedAt":"2026-04-20T16:19:14Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> I've created an MR with GitLab [2] and a PR with GitHub [3] to verify\n> that these changes work on both platforms.\n>\n> Changes in v5:\n>   - Allow opting in via `GIT_TEST_USE_SET_E=yes` and enable this option\n>     for Linux CI jobs.\n>   - Another fix for a potentially-failing command.\n>   - Link to v4: https://patch.msgid.link/20260417-b4-pks-tests-with-set-e-v4-0-44d43efdafb1@pks.im\n\nI agree that the explicit GIT_TEST_USE_SET_E option is a good way to\ngo, as it would be clear which ones are (and which ones are not)\nusing it.  I am not sure why we have check_fsck() thing?  Wasn't it\ndetermined that this would fail only with a broken shells, or is it\nfutureproofing just in case the function is used without being\ntested?\n\nAlso you updated in your earlier round to make everybody to use \"|| :\"\nnot \"|| true\".  Either would work, but this sticks out.\n\n>     + ## t/t1410-reflog.sh ##\n>     +@@ t/t1410-reflog.sh: check_have () {\n>     + }\n>     + \n>     + check_fsck () {\n>     +-\tgit fsck --full >fsck.output\n>     ++\tgit fsck --full >fsck.output || true\n>     + \tcase \"$1\" in\n>     + \t'')\n>     + \t\ttest_must_be_empty fsck.output ;;\n>     +\n"},{"id":"542005","messageId":"20260421030045.GA1870557@coredump.intra.peff.net","threadId":"65472","inReplyTo":"xmqqfr4pr4e7.fsf@gitster.g","subject":"Re: [PATCH v5 00/12] t: detect errors outside of test cases","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-04-21T03:00:45Z","receivedAt":"2026-04-21T03:01:00Z","isPatch":true,"body":"On Mon, Apr 20, 2026 at 09:19:12AM -0700, Junio C Hamano wrote:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > I've created an MR with GitLab [2] and a PR with GitHub [3] to verify\n> > that these changes work on both platforms.\n> >\n> > Changes in v5:\n> >   - Allow opting in via `GIT_TEST_USE_SET_E=yes` and enable this option\n> >     for Linux CI jobs.\n> >   - Another fix for a potentially-failing command.\n> >   - Link to v4: https://patch.msgid.link/20260417-b4-pks-tests-with-set-e-v4-0-44d43efdafb1@pks.im\n> \n> I agree that the explicit GIT_TEST_USE_SET_E option is a good way to\n> go, as it would be clear which ones are (and which ones are not)\n> using it.  I am not sure why we have check_fsck() thing?  Wasn't it\n> determined that this would fail only with a broken shells, or is it\n> futureproofing just in case the function is used without being\n> tested?\n\nYes, I think it would only fail on a broken shell. It's not _wrong_ to\nprotect against it, but it's the tip of the iceberg. There are many\nother spots that rely on \"set -e\" being suppressed inside test snippets,\nnot the least of which is every single final command in each snippet\n(because it's at the end of the &&-chain).\n\nSo I think it is better to draw the line at things that actually trigger\nwith working shells.\n\n-Peff\n"},{"id":"542014","messageId":"aecN9A3WFIU2aBpW@pks.im","threadId":"65472","inReplyTo":"20260421030045.GA1870557@coredump.intra.peff.net","subject":"Re: [PATCH v5 00/12] t: detect errors outside of test cases","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T05:41:08Z","receivedAt":"2026-04-21T05:41:13Z","isPatch":true,"body":"On Mon, Apr 20, 2026 at 11:00:45PM -0400, Jeff King wrote:\n> On Mon, Apr 20, 2026 at 09:19:12AM -0700, Junio C Hamano wrote:\n> \n> > Patrick Steinhardt <ps@pks.im> writes:\n> > \n> > > I've created an MR with GitLab [2] and a PR with GitHub [3] to verify\n> > > that these changes work on both platforms.\n> > >\n> > > Changes in v5:\n> > >   - Allow opting in via `GIT_TEST_USE_SET_E=yes` and enable this option\n> > >     for Linux CI jobs.\n> > >   - Another fix for a potentially-failing command.\n> > >   - Link to v4: https://patch.msgid.link/20260417-b4-pks-tests-with-set-e-v4-0-44d43efdafb1@pks.im\n> > \n> > I agree that the explicit GIT_TEST_USE_SET_E option is a good way to\n> > go, as it would be clear which ones are (and which ones are not)\n> > using it.  I am not sure why we have check_fsck() thing?  Wasn't it\n> > determined that this would fail only with a broken shells, or is it\n> > futureproofing just in case the function is used without being\n> > tested?\n> \n> Yes, I think it would only fail on a broken shell. It's not _wrong_ to\n> protect against it, but it's the tip of the iceberg. There are many\n> other spots that rely on \"set -e\" being suppressed inside test snippets,\n> not the least of which is every single final command in each snippet\n> (because it's at the end of the &&-chain).\n> \n> So I think it is better to draw the line at things that actually trigger\n> with working shells.\n\nOkay, that's fair enough. Let me drop that part again and then send out\nanother (hopefully final) version. Thanks!\n\nPatrick\n"},{"id":"542016","messageId":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im","subject":"[PATCH v6 00/12] t: detect errors outside of test cases","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:13Z","receivedAt":"2026-04-21T07:34:21Z","isPatch":true,"body":"Hi,\n\nthis is a follow-up to the recent discussion we had around `set -e` to\nmake our tests more robust and basically supersedes Junio's [1].\n\nI've tested the patches with both Bash and Dash, and all tests are\npassing on my machine with both of them. CI seems to be happy, as\nwell. But I would expect that this change probably has some fallout,\neven though I hope that it's generally going to be small and contained.\n\nThis series is based on 8c9303b1ff (Merge branch\n'jc/no-writev-does-not-work', 2026-04-10).\n\nI've created an MR with GitLab [2] and a PR with GitHub [3] to verify\nthat these changes work on both platforms.\n\nChanges in v6:\n  - Drop the change in t1410, which is only required for broken shells.\n  - Link to v5: https://patch.msgid.link/20260420-b4-pks-tests-with-set-e-v5-0-7d3d68292f6b@pks.im\n\nChanges in v5:\n  - Allow opting in via `GIT_TEST_USE_SET_E=yes` and enable this option\n    for Linux CI jobs.\n  - Another fix for a potentially-failing command.\n  - Link to v4: https://patch.msgid.link/20260417-b4-pks-tests-with-set-e-v4-0-44d43efdafb1@pks.im\n\nChanges in v4:\n  - Simplify how we read a multi-line variable value.\n  - Link to v3: https://patch.msgid.link/20260416-b4-pks-tests-with-set-e-v3-0-7a90e5dccadd@pks.im\n\nChanges in v3:\n  - Adapt `linux-TEST-vars` job to use Bash instead of Dash. Ubuntu\n    packet mirrors seem to be having problems, so I wasn't able to get\n    past installing dependencies in any jobs. All to say that I couldn't\n    verify that this works as expected :/\n  - Link to v2: https://patch.msgid.link/20260415-b4-pks-tests-with-set-e-v2-0-4e4904a96f15@pks.im\n\nChanges in v2:\n  - Use `ret=0; $command || ret=$?` pattern.\n  - Restore `echo 0` in SIGPIPE tests.\n  - Fix \"lib-git-svn.sh\" to gracefully handle the case where SVN Perl\n    modules aren't installed.\n  - Use `|| :` consistently instead of `|| true`.\n  - Fix up a couple of tests that fail on FreeBSD 15. The test suite is\n    now passing on this system, too.\n  - Only enable `set -e` on Bash 5 and newer.\n  - Link to v1: https://patch.msgid.link/20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <20260325062114.2067946-1-gitster@pobox.com>\n[2]: https://gitlab.com/gitlab-org/git/-/merge_requests/541\n[3]: https://github.com/git/git/pull/2270\n\n---\nPatrick Steinhardt (12):\n      t: prepare `test_match_signal ()` calls for `set -e`\n      t: prepare `test_must_fail ()` for `set -e`\n      t: prepare `stop_git_daemon ()` for `set -e`\n      t: prepare `git config --unset` calls for `set -e`\n      t: prepare conditional test execution for `set -e`\n      t: prepare execution of potentially failing commands for `set -e`\n      t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`\n      t0008: silence error in subshell when using `grep -v`\n      t1301: don't fail in case setfacl(1) doesn't exist or fails\n      t6002: fix use of `expr` with `set -e`\n      t9902: fix use of `read` with `set -e`\n      t: detect errors outside of test cases\n\n ci/run-build-and-tests.sh          |  6 ++++++\n t/lib-git-daemon.sh                |  8 +++++---\n t/lib-git-svn.sh                   |  7 +++----\n t/lib-httpd.sh                     |  3 +--\n t/t0005-signals.sh                 |  4 ++--\n t/t0008-ignores.sh                 |  4 ++--\n t/t1301-shared-repo.sh             |  2 +-\n t/t3600-rm.sh                      |  2 +-\n t/t3901-i18n-patch.sh              |  3 ++-\n t/t4032-diff-inter-hunk-context.sh | 14 ++++++++------\n t/t5000-tar-tree.sh                |  4 ++--\n t/t6002-rev-list-bisect.sh         | 17 ++++++++++-------\n t/t7422-submodule-output.sh        |  2 +-\n t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n t/t7508-status.sh                  |  4 ++--\n t/t9138-git-svn-authors-prog.sh    |  4 ++--\n t/t9200-git-cvsexportcommit.sh     |  3 +--\n t/t9400-git-cvsserver-server.sh    |  5 +++--\n t/t9401-git-cvsserver-crlf.sh      |  4 ++--\n t/t9402-git-cvsserver-refs.sh      |  4 ++--\n t/t9902-completion.sh              |  6 ++----\n t/test-lib-functions.sh            | 12 ++++++------\n t/test-lib.sh                      | 35 +++++++++++++++++++++++++++++++----\n 23 files changed, 108 insertions(+), 69 deletions(-)\n\nRange-diff versus v5:\n\n 1:  a7fd95c0f8 =  1:  e619a6f565 t: prepare `test_match_signal ()` calls for `set -e`\n 2:  1edfada458 =  2:  9002a8e77e t: prepare `test_must_fail ()` for `set -e`\n 3:  9c703ea3ec =  3:  b375926fc4 t: prepare `stop_git_daemon ()` for `set -e`\n 4:  855707f648 =  4:  0d46e4212e t: prepare `git config --unset` calls for `set -e`\n 5:  e24c272d06 =  5:  5bdb1ad025 t: prepare conditional test execution for `set -e`\n 6:  dd89a14595 !  6:  7654824960 t: prepare execution of potentially failing commands for `set -e`\n    @@ t/lib-httpd.sh: start_httpd() {\n      \t\tcat \"$HTTPD_ROOT_PATH\"/error.log >&4 2>/dev/null\n      \t\ttest_skip_or_die GIT_TEST_HTTPD \"web server setup failed\"\n     \n    - ## t/t1410-reflog.sh ##\n    -@@ t/t1410-reflog.sh: check_have () {\n    - }\n    - \n    - check_fsck () {\n    --\tgit fsck --full >fsck.output\n    -+\tgit fsck --full >fsck.output || true\n    - \tcase \"$1\" in\n    - \t'')\n    - \t\ttest_must_be_empty fsck.output ;;\n    -\n      ## t/t3901-i18n-patch.sh ##\n     @@ t/t3901-i18n-patch.sh: check_encoding () {\n      \t\t8859)\n 7:  dfcbb1ed20 =  7:  8f5ee3234a t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`\n 8:  cef0ff3bf9 =  8:  46b4ddf1fc t0008: silence error in subshell when using `grep -v`\n 9:  f0e43f0f54 =  9:  6e21a1af73 t1301: don't fail in case setfacl(1) doesn't exist or fails\n10:  b61761cfdb = 10:  c1c907187f t6002: fix use of `expr` with `set -e`\n11:  591e53ab47 = 11:  b6df934d90 t9902: fix use of `read` with `set -e`\n12:  dd7a68c141 = 12:  60681e8ebe t: detect errors outside of test cases\n\n---\nbase-commit: 8c9303b1ffae5b745d1b0a1f98330cf7944d8db0\nchange-id: 20260410-b4-pks-tests-with-set-e-3ae479b24b51\n\n"},{"id":"542017","messageId":"20260421-b4-pks-tests-with-set-e-v6-1-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 01/12] t: prepare `test_match_signal ()` calls for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:14Z","receivedAt":"2026-04-21T07:34:22Z","isPatch":true,"body":"We have a couple of calls to `test_match_signal ()` where we execute a\nGit command and expect it to die with a specific signal. These calls\nwill essentially execute the process in a subshell via `foo; echo $?`,\nbut as we expect `foo` to fail this will cause the overall subshell to\nfail once we `set -e`.\n\nFix this issue by using `foo && echo 0 || echo $?` instead.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0005-signals.sh | 4 ++--\n t/t3600-rm.sh      | 2 +-\n 2 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t0005-signals.sh b/t/t0005-signals.sh\nindex afba0fc3fc..84319cf169 100755\n--- a/t/t0005-signals.sh\n+++ b/t/t0005-signals.sh\n@@ -42,12 +42,12 @@ test_expect_success 'create blob' '\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE' '\n-\tOUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \n test_expect_success !MINGW 'a constipated git dies with SIGPIPE even if parent ignores it' '\n-\tOUT=$( ((trap \"\" PIPE && large_git; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\"\n '\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 1f16e6b522..a371ea690e 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -260,7 +260,7 @@ test_expect_success 'choking \"git rm\" should not let it die with cruft (induce S\n \n test_expect_success !MINGW 'choking \"git rm\" should not let it die with cruft (induce and check SIGPIPE)' '\n \tchoke_git_rm_setup &&\n-\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\"; echo $? 1>&3) | :) 3>&1 ) &&\n+\tOUT=$( ((trap \"\" PIPE && git rm -n \"some-file-*\" && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&\n \ttest_match_signal 13 \"$OUT\" &&\n \ttest_path_is_missing .git/index.lock\n '\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542018","messageId":"20260421-b4-pks-tests-with-set-e-v6-2-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 02/12] t: prepare `test_must_fail ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:15Z","receivedAt":"2026-04-21T07:34:25Z","isPatch":true,"body":"The helper function `test_must_fail ()` executes a specific Git command\nthat may or may not fail in a specific way. This is done by executing\nthe command in question and then comparing its exit code against a set\nof conditions.\n\nThis works, but once we run our test suite with `set -e` we may bail out\nof `test_must_fail ()` early in case the command actually fails, even\nthough we expect it to fail. Prepare for this change by handling the\nfailed case with `||`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/test-lib-functions.sh | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex f3af10fb7e..5fd5494ef1 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1195,8 +1195,9 @@ test_must_fail () {\n \t\techo >&7 \"test_must_fail: only 'git' is allowed: $*\"\n \t\treturn 1\n \tfi\n-\t\"$@\" 2>&7\n-\texit_code=$?\n+\n+\texit_code=0; \"$@\" 2>&7 || exit_code=$?\n+\n \tif test $exit_code -eq 0 && ! list_contains \"$_test_ok\" success\n \tthen\n \t\techo >&4 \"test_must_fail: command succeeded: $*\"\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542019","messageId":"20260421-b4-pks-tests-with-set-e-v6-3-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 03/12] t: prepare `stop_git_daemon ()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:16Z","receivedAt":"2026-04-21T07:34:27Z","isPatch":true,"body":"We have a couple of calls to `stop_git_daemon ()` outside of specific\ntest cases that will kill a backgrounded git-daemon(1) process and\nexpect the process with a specific error code. While these function\ncalls do end up killing git-daemon(1), the error handling we have in\nthose contexts is basically ineffective. So while we expect the process\nto exit with a specific error code, we will just continue with any error\nin case it doesn't.\n\nThis will change once we enable `set -e` in a subsequent commit. There's\ntwo issues though that will make this _always_ fail:\n\n  - Our call to `wait` is expected to fail, but because it's not part of\n    a condition it will cause us to bail out immediately with `set -e`.\n\n  - We try to kill git-daemon(1) a second time via the pidfile. We can\n    generally expect that this is the same PID though as we had in the\n    \"GIT_DAEMON_PID\" environment variable, and thus it's more likely\n    than not that we have already killed it, and the call to kill will\n    fail.\n\nPrepare for this change by handling the failure of `wait` with `||` and\nby silencing failures of the second call to `kill`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/lib-git-daemon.sh | 8 +++++---\n 1 file changed, 5 insertions(+), 3 deletions(-)\n\ndiff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh\nindex e62569222b..d172aa51f0 100644\n--- a/t/lib-git-daemon.sh\n+++ b/t/lib-git-daemon.sh\n@@ -85,14 +85,16 @@ stop_git_daemon() {\n \n \t# kill git-daemon child of git\n \tsay >&3 \"Stopping git daemon ...\"\n+\n \tkill \"$GIT_DAEMON_PID\"\n-\twait \"$GIT_DAEMON_PID\" >&3 2>&4\n-\tret=$?\n+\tret=0; wait \"$GIT_DAEMON_PID\" >&3 2>&4 || ret=$?\n+\n \tif ! test_match_signal 15 $ret\n \tthen\n \t\terror \"git daemon exited with status: $ret\"\n \tfi\n-\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null\n+\n+\tkill \"$(cat \"$GIT_DAEMON_PIDFILE\")\" 2>/dev/null || :\n \tGIT_DAEMON_PID=\n \trm -f git_daemon_output \"$GIT_DAEMON_PIDFILE\"\n }\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542020","messageId":"20260421-b4-pks-tests-with-set-e-v6-4-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 04/12] t: prepare `git config --unset` calls for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:17Z","receivedAt":"2026-04-21T07:34:30Z","isPatch":true,"body":"We have a couple of calls to `git config --unset` that ultimately end up\nas no-ops as the configuration variables aren't set (anymore) in the\nfirst place. These calls are mostly intended to recover unconditionally\nfrom tests that may have executed only partially, but they'll ultimately\nfail during a normal test run.\n\nThis hasn't been a problem until now as we aren't running tests with\n`set -e`. This is about to change though, so let's silence the case\nwhere we cannot unset the config keys.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t4032-diff-inter-hunk-context.sh | 2 +-\n t/t7508-status.sh                  | 4 ++--\n t/t9138-git-svn-authors-prog.sh    | 4 ++--\n 3 files changed, 5 insertions(+), 5 deletions(-)\n\ndiff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\nindex bada0cbd32..c98eb6abb2 100755\n--- a/t/t4032-diff-inter-hunk-context.sh\n+++ b/t/t4032-diff-inter-hunk-context.sh\n@@ -17,7 +17,7 @@ f() {\n \n t() {\n \tuse_config=\n-\tgit config --unset diff.interHunkContext\n+\tgit config --unset diff.interHunkContext || :\n \n \tcase $# in\n \t4) hunks=$4; cmd=\"diff -U$3\";;\ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex a5e21bf8bf..1167b835a4 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -773,8 +773,8 @@ test_expect_success TTY 'status --porcelain ignores color.status' '\n '\n \n # recover unconditionally from color tests\n-git config --unset color.status\n-git config --unset color.ui\n+git config --unset color.status || :\n+git config --unset color.ui || :\n \n test_expect_success 'status --porcelain respects -b' '\n \ndiff --git a/t/t9138-git-svn-authors-prog.sh b/t/t9138-git-svn-authors-prog.sh\nindex 784ec7fc2d..5bb38cb23a 100755\n--- a/t/t9138-git-svn-authors-prog.sh\n+++ b/t/t9138-git-svn-authors-prog.sh\n@@ -68,8 +68,8 @@ test_expect_success 'authors-file overrode authors-prog' '\n \t)\n '\n \n-git --git-dir=x/.git config --unset svn.authorsfile\n-git --git-dir=x/.git config --unset svn.authorsprog\n+git --git-dir=x/.git config --unset svn.authorsfile || :\n+git --git-dir=x/.git config --unset svn.authorsprog || :\n \n test_expect_success 'authors-prog imported user without email' '\n \tsvn mkdir -m gg --username gg-hermit \"$svnrepo\"/gg &&\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542021","messageId":"20260421-b4-pks-tests-with-set-e-v6-5-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 05/12] t: prepare conditional test execution for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:18Z","receivedAt":"2026-04-21T07:34:32Z","isPatch":true,"body":"We have some test in our test suite where we use the pattern of\n`test ... && test_expect_succeess` to conditionally execute a test. The\nproblem is that when we decide to not execute the test, we'll indeed\nskip the test, but the overall statement will also be unsuccessful. This\nwill become a problem once we enable `set -e`.\n\nPrepare for this future by turning this into a proper conditional, which\nis also a bit easier to read overall.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t4032-diff-inter-hunk-context.sh | 12 +++++++-----\n t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------\n 2 files changed, 20 insertions(+), 16 deletions(-)\n\ndiff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh\nindex c98eb6abb2..2d216fb70f 100755\n--- a/t/t4032-diff-inter-hunk-context.sh\n+++ b/t/t4032-diff-inter-hunk-context.sh\n@@ -40,11 +40,13 @@ t() {\n \t\ttest $(git $cmd $file | grep '^@@ ' | wc -l) = $hunks\n \t\"\n \n-\ttest -f $expected &&\n-\ttest_expect_success \"$label: check output\" \"\n-\t\tgit $cmd $file | grep -v '^index ' >actual &&\n-\t\ttest_cmp $expected actual\n-\t\"\n+\tif test -f $expected\n+\tthen\n+\t\ttest_expect_success \"$label: check output\" \"\n+\t\t\tgit $cmd $file | grep -v '^index ' >actual &&\n+\t\t\ttest_cmp $expected actual\n+\t\t\"\n+\tfi\n }\n \n cat <<EOF >expected.f1.0.1 || exit 1\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex f512eed278..8cc86522b2 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -220,17 +220,19 @@ check_dotx_symlink () {\n \t\t)\n \t'\n \n-\ttest -n \"$refuse_index\" &&\n-\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n-\t\ttest_must_fail \\\n-\t\t\tgit -C $dir \\\n-\t\t\t    -c core.protectntfs \\\n-\t\t\t    -c core.protecthfs \\\n-\t\t\t    read-tree $tree 2>err &&\n-\t\tgrep \"invalid path.*$name\" err &&\n-\t\tgit -C $dir ls-files -s >out &&\n-\t\ttest_must_be_empty out\n-\t'\n+\tif test -n \"$refuse_index\"\n+\tthen\n+\t\ttest_expect_success \"refuse to load symlinked $name into index ($type)\" '\n+\t\t\ttest_must_fail \\\n+\t\t\t\tgit -C $dir \\\n+\t\t\t\t    -c core.protectntfs \\\n+\t\t\t\t    -c core.protecthfs \\\n+\t\t\t\t    read-tree $tree 2>err &&\n+\t\t\tgrep \"invalid path.*$name\" err &&\n+\t\t\tgit -C $dir ls-files -s >out &&\n+\t\t\ttest_must_be_empty out\n+\t\t'\n+\tfi\n }\n \n check_dotx_symlink gitmodules vanilla .gitmodules\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542022","messageId":"20260421-b4-pks-tests-with-set-e-v6-6-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 06/12] t: prepare execution of potentially failing commands for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:19Z","receivedAt":"2026-04-21T07:34:36Z","isPatch":true,"body":"Several of our tests verify whether a certain binary can be executed,\npotentially skipping tests in case we cannot, for example because the\nbinary doesn't exist. In those cases we often run the binary outside of\nany conditionally.\n\nThis will start to fail once we enable `set -e`, as that will cause us\nto bail out the test immediately. Improve these tests by executing them\ninside of a conditional instead.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/lib-git-svn.sh                |  7 +++----\n t/lib-httpd.sh                  |  3 +--\n t/t3901-i18n-patch.sh           |  3 ++-\n t/t5000-tar-tree.sh             |  4 ++--\n t/t7422-submodule-output.sh     |  2 +-\n t/t9200-git-cvsexportcommit.sh  |  3 +--\n t/t9400-git-cvsserver-server.sh |  5 +++--\n t/t9401-git-cvsserver-crlf.sh   |  4 ++--\n t/t9402-git-cvsserver-refs.sh   |  4 ++--\n t/test-lib-functions.sh         |  3 +--\n t/test-lib.sh                   | 10 ++++++----\n 11 files changed, 24 insertions(+), 24 deletions(-)\n\ndiff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh\nindex 2fde2353fd..52843f667d 100644\n--- a/t/lib-git-svn.sh\n+++ b/t/lib-git-svn.sh\n@@ -15,8 +15,7 @@ GIT_SVN_DIR=$GIT_DIR/svn/refs/remotes/git-svn\n SVN_TREE=$GIT_SVN_DIR/svn-tree\n test_set_port SVNSERVE_PORT\n \n-svn >/dev/null 2>&1\n-if test $? -ne 1\n+if ! svn help >/dev/null 2>&1\n then\n \tskip_all='skipping git svn tests, svn not found'\n \ttest_done\n@@ -27,13 +26,13 @@ export svnrepo\n svnconf=$PWD/svnconf\n export svnconf\n \n+x=0\n perl -w -e \"\n use SVN::Core;\n use SVN::Repos;\n \\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n system(qw/svnadmin create --fs-type fsfs/, \\$ENV{svnrepo}) == 0 or exit(41);\n-\" >&3 2>&4\n-x=$?\n+\" >&3 2>&4 || x=$?\n if test $x -ne 0\n then\n \tif test $x -eq 42; then\ndiff --git a/t/lib-httpd.sh b/t/lib-httpd.sh\nindex 4c76e813e3..fc646447d5 100644\n--- a/t/lib-httpd.sh\n+++ b/t/lib-httpd.sh\n@@ -235,11 +235,10 @@ start_httpd() {\n \n \ttest_atexit stop_httpd\n \n-\t\"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n+\tif ! \"$LIB_HTTPD_PATH\" -d \"$HTTPD_ROOT_PATH\" \\\n \t\t-f \"$TEST_PATH/apache.conf\" $HTTPD_PARA \\\n \t\t-c \"Listen 127.0.0.1:$LIB_HTTPD_PORT\" -k start \\\n \t\t>&3 2>&4\n-\tif test $? -ne 0\n \tthen\n \t\tcat \"$HTTPD_ROOT_PATH\"/error.log >&4 2>/dev/null\n \t\ttest_skip_or_die GIT_TEST_HTTPD \"web server setup failed\"\ndiff --git a/t/t3901-i18n-patch.sh b/t/t3901-i18n-patch.sh\nindex f03601b49a..ef7d7e1edc 100755\n--- a/t/t3901-i18n-patch.sh\n+++ b/t/t3901-i18n-patch.sh\n@@ -28,7 +28,8 @@ check_encoding () {\n \t\t8859)\n \t\t\tgrep \"^encoding ISO8859-1\" ;;\n \t\t*)\n-\t\t\tgrep \"^encoding ISO8859-1\"; test \"$?\" != 0 ;;\n+\t\t\tret=0; grep \"^encoding ISO8859-1\" || ret=$?\n+\t\t\ttest \"$ret\" != 0 ;;\n \t\tesac || return 1\n \t\tj=$i\n \t\ti=$(($i+1))\ndiff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh\nindex 5465054f17..a8c28533dc 100755\n--- a/t/t5000-tar-tree.sh\n+++ b/t/t5000-tar-tree.sh\n@@ -503,8 +503,8 @@ test_expect_success LONG_IS_64BIT 'set up repository with huge blob' '\n # would generate the whole 64GB).\n test_expect_success LONG_IS_64BIT 'generate tar with huge size' '\n \t{\n-\t\tgit archive HEAD\n-\t\techo $? >exit-code\n+\t\t{ ret=0 && git archive HEAD || ret=$?; } &&\n+\t\techo \"$ret\" >exit-code\n \t} | test_copy_bytes 4096 >huge.tar &&\n \techo 141 >expect &&\n \ttest_cmp expect exit-code\ndiff --git a/t/t7422-submodule-output.sh b/t/t7422-submodule-output.sh\nindex aea1ddf117..852136fdfd 100755\n--- a/t/t7422-submodule-output.sh\n+++ b/t/t7422-submodule-output.sh\n@@ -198,7 +198,7 @@ test_expect_success !MINGW 'git submodule status --recursive propagates SIGPIPE'\n \t(\n \t\tcd repo &&\n \t\tGIT_ALLOW_PROTOCOL=file git submodule add \"$(pwd)\"/../submodule &&\n-\t\t{ git submodule status --recursive 2>err; echo $?>status; } |\n+\t\t{ { ret=0 && git submodule status --recursive 2>err || ret=$?; } && echo $ret >status; } |\n \t\t\tgrep -q recursive-submodule-path-1 &&\n \t\ttest_must_be_empty err &&\n \t\ttest_match_signal 13 \"$(cat status)\"\ndiff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh\nindex 14cbe96527..581cf3d28f 100755\n--- a/t/t9200-git-cvsexportcommit.sh\n+++ b/t/t9200-git-cvsexportcommit.sh\n@@ -11,8 +11,7 @@ if ! test_have_prereq PERL; then\n \ttest_done\n fi\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git cvsexportcommit tests, cvs not found'\n     test_done\ndiff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh\nindex e499c7f955..4b45398bab 100755\n--- a/t/t9400-git-cvsserver-server.sh\n+++ b/t/t9400-git-cvsserver-server.sh\n@@ -17,12 +17,13 @@ if ! test_have_prereq PERL; then\n \tskip_all='skipping git cvsserver tests, perl not available'\n \ttest_done\n fi\n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git-cvsserver tests, cvs not found'\n     test_done\n fi\n+\n perl -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {\n     skip_all='skipping git-cvsserver tests, Perl SQLite interface unavailable'\n     test_done\ndiff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh\nindex a34805acdc..6b4cbb1651 100755\n--- a/t/t9401-git-cvsserver-crlf.sh\n+++ b/t/t9401-git-cvsserver-crlf.sh\n@@ -60,12 +60,12 @@ check_status_options() {\n     return $stat\n }\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n     skip_all='skipping git-cvsserver tests, cvs not found'\n     test_done\n fi\n+\n if ! test_have_prereq PERL\n then\n     skip_all='skipping git-cvsserver tests, perl not available'\ndiff --git a/t/t9402-git-cvsserver-refs.sh b/t/t9402-git-cvsserver-refs.sh\nindex 2ee41f9443..65f2ceedec 100755\n--- a/t/t9402-git-cvsserver-refs.sh\n+++ b/t/t9402-git-cvsserver-refs.sh\n@@ -68,12 +68,12 @@ check_diff() {\n \n #########\n \n-cvs >/dev/null 2>&1\n-if test $? -ne 1\n+if ! cvs version >/dev/null 2>&1\n then\n \tskip_all='skipping git-cvsserver tests, cvs not found'\n \ttest_done\n fi\n+\n if ! test_have_prereq PERL\n then\n \tskip_all='skipping git-cvsserver tests, perl not available'\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex 5fd5494ef1..879ee1ee59 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1248,8 +1248,7 @@ test_might_fail () {\n test_expect_code () {\n \twant_code=$1\n \tshift\n-\t\"$@\" 2>&7\n-\texit_code=$?\n+\texit_code=0; \"$@\" 2>&7 || exit_code=$?\n \tif test $exit_code = $want_code\n \tthen\n \t\treturn 0\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 70fd3e9baf..de7d9e7b92 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -143,8 +143,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n ################################################################\n # It appears that people try to run tests without building...\n GIT_BINARY=\"${GIT_TEST_INSTALLED:-$GIT_BUILD_DIR}/git$X\"\n-\"$GIT_BINARY\" >/dev/null\n-if test $? != 1\n+\n+if ! \"$GIT_BINARY\" version >/dev/null\n then\n \tif test -n \"$GIT_TEST_INSTALLED\"\n \tthen\n@@ -454,8 +454,10 @@ then\n \t# from any previous runs.\n \t>\"$GIT_TEST_TEE_OUTPUT_FILE\"\n \n-\t(GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} \"$0\" \"$@\" 2>&1;\n-\t echo $? >\"$TEST_RESULTS_BASE.exit\") | tee -a \"$GIT_TEST_TEE_OUTPUT_FILE\"\n+\t(\n+\t\tret=0 && GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} \"$0\" \"$@\" 2>&1 || ret=$?\n+\t\techo \"$ret\" >\"$TEST_RESULTS_BASE.exit\"\n+\t) | tee -a \"$GIT_TEST_TEE_OUTPUT_FILE\"\n \ttest \"$(cat \"$TEST_RESULTS_BASE.exit\")\" = 0\n \texit\n fi\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542023","messageId":"20260421-b4-pks-tests-with-set-e-v6-7-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 07/12] t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:20Z","receivedAt":"2026-04-21T07:34:38Z","isPatch":true,"body":"Both `test_when_finished ()` and `test_atexit ()` build up a chain of\ncleanup commands by prepending each new command to the existing cleanup\nstring. To preserve the exit code of the test body across cleanup\nexecution, we append the following logic:\n\n    } && (exit \"$eval_ret\"); eval_ret=$?; ...\n\nThe intent of this is to run the cleanup block and then unconditionally\nrestore `eval_ret`. The original behaviour of this is is:\n\n   +------------------+---------+------------------------------------+\n   |test body         │ cleanup │ old behaviour                      │\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | pass    │ && taken -> (exit 0) -> eval_ret=0 |\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | fail    │ && not taken -> eval_ret=$?        |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | pass    │ && taken -> (exit 1) -> eval_ret=1 |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | fail    | && not taken -> eval_ret=$?        |\n   +------------------+---------+------------------------------------+\n\nThis logic will start to fail once we enable `set -e`. When `$eval_ret`\nis non-zero, the subshell we create will fail, and with `set -e` we'll\nthus bail out without evaluating the logic after the semicolon.\n\nFix this issue by instead using `|| eval_ret=\\$?; ...`. Besides being\na bit simpler, it also retains the original behaviour:\n\n   +------------------+---------+------------------------------------+\n   |test body         │ cleanup │ old behaviour                      │\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | pass    │ || not taken -> eval_ret unchanged |\n   +------------------+---------+------------------------------------+\n   │pass (eval_ret=0) | fail    │ || taken -> eval_ret=$?            |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | pass    │ || not taken -> eval_ret unchanged |\n   +------------------+---------+------------------------------------+\n   │fail (eval_ret=1) | fail    | || taken -> eval_ret=$?            |\n   +------------------+---------+------------------------------------+\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/test-lib-functions.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh\nindex 879ee1ee59..502bb0ddcb 100644\n--- a/t/test-lib-functions.sh\n+++ b/t/test-lib-functions.sh\n@@ -1512,7 +1512,7 @@ test_when_finished () {\n \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n \tBUG \"test_when_finished does nothing in a subshell\"\n \ttest_cleanup=\"{ $*\n-\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_cleanup\"\n+\t\t} || eval_ret=\\$?; $test_cleanup\"\n }\n \n # This function can be used to schedule some commands to be run\n@@ -1540,7 +1540,7 @@ test_atexit () {\n \ttest \"${BASH_SUBSHELL-0}\" = 0 ||\n \tBUG \"test_atexit does nothing in a subshell\"\n \ttest_atexit_cleanup=\"{ $*\n-\t\t} && (exit \\\"\\$eval_ret\\\"); eval_ret=\\$?; $test_atexit_cleanup\"\n+\t\t} || eval_ret=\\$?; $test_atexit_cleanup\"\n }\n \n # Deprecated wrapper for \"git init\", use \"git init\" directly instead\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542024","messageId":"20260421-b4-pks-tests-with-set-e-v6-8-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 08/12] t0008: silence error in subshell when using `grep -v`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:21Z","receivedAt":"2026-04-21T07:34:41Z","isPatch":true,"body":"In t0008 we use `grep -v` in a subshell, but expect that this command\nwill sometimes not match anything. This would cause grep(1) to return an\nerror code, but given that we don't run with `set -e` we swallow this\nerror.\n\nWe're about to enable `set -e`. Prepare for this by ignoring any errors.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t0008-ignores.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh\nindex e716b5cdfa..d77a179bdd 100755\n--- a/t/t0008-ignores.sh\n+++ b/t/t0008-ignores.sh\n@@ -122,8 +122,8 @@ test_expect_success_multiple () {\n \tfi\n \ttestname=\"$1\" expect_all=\"$2\" code=\"$3\"\n \n-\texpect_verbose=$( echo \"$expect_all\" | grep -v '^::\t' )\n-\texpect=$( echo \"$expect_verbose\" | sed -e 's/.*\t//' )\n+\texpect_verbose=$(echo \"$expect_all\" | grep -v '^::\t' || :)\n+\texpect=$(echo \"$expect_verbose\" | sed -e 's/.*\t//')\n \n \ttest_expect_success $prereq \"$testname${no_index_opt:+ with $no_index_opt}\" '\n \t\texpect \"$expect\" &&\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542025","messageId":"20260421-b4-pks-tests-with-set-e-v6-9-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 09/12] t1301: don't fail in case setfacl(1) doesn't exist or fails","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:22Z","receivedAt":"2026-04-21T07:34:43Z","isPatch":true,"body":"In t1301 we're trying to remove any potentially-existing default ACLs\nthat might exist on the transh directory by executing setfacl(1).\nAccording to 8ed0a740dd (t1301-shared-repo.sh: don't let a default ACL\ninterfere with the test, 2008-10-16), this is done because we play\naround with permissions and umasks in this test suite.\n\nThe setfacl(1) binary may not exist on some systems though, even though\ntests ultimately still pass. This doesn't matter currently, but will\ncause the test to fail once we start running with `set -e`. Silence such\nfailures by ignoring failures here.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t1301-shared-repo.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/t1301-shared-repo.sh b/t/t1301-shared-repo.sh\nindex 630a47af21..0e0d07a1a1 100755\n--- a/t/t1301-shared-repo.sh\n+++ b/t/t1301-shared-repo.sh\n@@ -12,7 +12,7 @@ TEST_CREATE_REPO_NO_TEMPLATE=1\n . ./test-lib.sh\n \n # Remove a default ACL from the test dir if possible.\n-setfacl -k . 2>/dev/null\n+setfacl -k . 2>/dev/null || :\n \n # User must have read permissions to the repo -> failure on --shared=0400\n test_expect_success 'shared = 0400 (faulty permission u-w)' '\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542027","messageId":"20260421-b4-pks-tests-with-set-e-v6-10-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 10/12] t6002: fix use of `expr` with `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:23Z","receivedAt":"2026-04-21T07:34:45Z","isPatch":true,"body":"In `test_bisection_diff ()` we use `expr` to perform some math. This\ncommand has some gotchas though in that it will only return success when\nthe result is neither null nor zero. In some of our cases though it\nactually _is_ zero, and that will cause the expressions to fail once we\nenable `set -e`.\n\nPrepare for this change by instead using `$(( ))`, which doesn't have\nthe same issue. While at it, modernize the function a tiny bit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t6002-rev-list-bisect.sh | 17 ++++++++++-------\n 1 file changed, 10 insertions(+), 7 deletions(-)\n\ndiff --git a/t/t6002-rev-list-bisect.sh b/t/t6002-rev-list-bisect.sh\nindex daa009c9a1..f2de40b5ed 100755\n--- a/t/t6002-rev-list-bisect.sh\n+++ b/t/t6002-rev-list-bisect.sh\n@@ -27,13 +27,16 @@ test_bisection_diff()\n \t# Test if bisection size is close to half of list size within\n \t# tolerance.\n \t#\n-\t_bisect_err=$(expr $_list_size - $_bisection_size \\* 2)\n-\ttest \"$_bisect_err\" -lt 0 && _bisect_err=$(expr 0 - $_bisect_err)\n-\t_bisect_err=$(expr $_bisect_err / 2) ; # floor\n-\n-\ttest_expect_success \\\n-\t\"bisection diff $_bisect_option $_head $* <= $_max_diff\" \\\n-\t'test $_bisect_err -le $_max_diff'\n+\t_bisect_err=$(($_list_size - $_bisection_size * 2))\n+\tif test \"$_bisect_err\" -lt 0\n+\tthen\n+\t\t_bisect_err=$((0 - $_bisect_err))\n+\tfi\n+\t_bisect_err=$(($_bisect_err / 2)) ; # floor\n+\n+\ttest_expect_success \"bisection diff $_bisect_option $_head $* <= $_max_diff\" '\n+\t\ttest $_bisect_err -le $_max_diff\n+\t'\n }\n \n date >path0\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542026","messageId":"20260421-b4-pks-tests-with-set-e-v6-11-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 11/12] t9902: fix use of `read` with `set -e`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:24Z","receivedAt":"2026-04-21T07:34:48Z","isPatch":true,"body":"In t9902 we're using the `read` builtin to read some values into a\nvariable. This is done by using `-d \"\"`, which cause us to read until\nthe end of the heredoc. As the read is terminated by EOF, the command\nwill end up returning a non-zero error code. This hasn't been an issue\nuntil now as we didn't run with `set -e`, but that'll change in a\nsubsequent commit.\n\nPrepare for this change by not using read at all, as we can simply store\nthe multi-line value directly.\n\nSuggested-by: SZEDER Gábor <szeder.dev@gmail.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n t/t9902-completion.sh | 6 ++----\n 1 file changed, 2 insertions(+), 4 deletions(-)\n\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 2f9a597ec7..28f61f08fb 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -590,12 +590,10 @@ test_expect_success '__gitcomp - doesnt fail because of invalid variable name' '\n \t__gitcomp \"$invalid_variable_name\"\n '\n \n-read -r -d \"\" refs <<-\\EOF\n-main\n+refs='main\n maint\n next\n-seen\n-EOF\n+seen'\n \n test_expect_success '__gitcomp_nl - trailing space' '\n \ttest_gitcomp_nl \"m\" \"$refs\" <<-EOF\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"},{"id":"542028","messageId":"20260421-b4-pks-tests-with-set-e-v6-12-26330e3061ab@pks.im","threadId":"65472","inReplyTo":"20260421-b4-pks-tests-with-set-e-v6-0-26330e3061ab@pks.im","subject":"[PATCH v6 12/12] t: detect errors outside of test cases","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-04-21T07:34:25Z","receivedAt":"2026-04-21T07:34:50Z","isPatch":true,"body":"We have recently merged a patch series that had a simple misspelling of\n`test_expect_success`. Instead of making our tests fail though, this\ntypo went completely undetected and all of our tests passed, which is of\ncourse unfortunate. This is a more general issue with our test suite:\nall commands that run outside of a specific test case can fail, and if\nwe don't explicitly check for such failure then this failure will be\nsilently ignored.\n\nImprove the status quo by enabling the errexit option so that any such\nunchecked failures will cause us to abort immediately.\n\nNote that for now, we only enable this option for Bash 5 and newer. This\nis because other shells have wildly different behaviour, and older\nversions of Bash (especially on macOS) are buggy. The list of enabled\nshells may be extended going forward.\n\nHelped-by: Jeff King <peff@peff.net>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n ci/run-build-and-tests.sh |  6 ++++++\n t/test-lib.sh             | 25 +++++++++++++++++++++++++\n 2 files changed, 31 insertions(+)\n\ndiff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh\nindex 28cfe730ee..de08a08d59 100755\n--- a/ci/run-build-and-tests.sh\n+++ b/ci/run-build-and-tests.sh\n@@ -7,6 +7,12 @@\n \n export TEST_CONTRIB_TOO=yes\n \n+case \"$jobname\" in\n+almalinux-*|debian-*|fedora-*|linux-*)\n+\texport GIT_TEST_USE_SET_E=yes\n+\t;;\n+esac\n+\n case \"$jobname\" in\n fedora-breaking-changes-musl|linux-breaking-changes)\n \texport WITH_BREAKING_CHANGES=YesPlease\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex de7d9e7b92..cded7bd693 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -15,6 +15,31 @@\n # You should have received a copy of the GNU General Public License\n # along with this program.  If not, see https://www.gnu.org/licenses/ .\n \n+# Enable the use of errexit so that any unexpected failures will cause us to\n+# abort tests, even when outside of a specific test case.\n+#\n+# Note that we only enable this on Bash 5 and newer, or when explicitly\n+# requested by the user via `GIT_TEST_USE_SET_E=true`. This ib secause `set -e`\n+# has wildly different behaviour across shells. The list of default-enabled\n+# shells may be extended going forward.\n+if test -z \"$GIT_TEST_USE_SET_E\" && test \"${BASH_VERSINFO:=0}\" -ge 5\n+then\n+\tGIT_TEST_USE_SET_E=true\n+fi\n+\n+# We cannot use `test-tool env-helper` here, as it's not yet available.\n+case \"${GIT_TEST_USE_SET_E:-false}\" in\n+1|on|true|yes)\n+\tset -e\n+\t;;\n+0|off|false|no)\n+\t;;\n+*)\n+\techo \"GIT_TEST_USE_SET_E requires a boolean\" >&2\n+\texit 1\n+\t;;\n+esac\n+\n # Test the binaries we have just built.  The tests are kept in\n # t/ subdirectory and are run in 'trash directory' subdirectory.\n if test -z \"$TEST_DIRECTORY\"\n\n-- \n2.54.0.545.g6539524ca2.dirty\n\n"}]}