{"thread":{"id":"65249","subject":"Unexpected exit code for --help with rev-parse --parseopt","startedAt":"2026-03-15T00:52:29Z","lastAt":"2026-07-08T20:32:06Z","messageCount":39,"participants":["brian m. carlson","Jeff King","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"539003","messageId":"abYCxrEEPaI21g3H@fruit.crustytoothpaste.net","threadId":"65249","inReplyTo":null,"subject":"Unexpected exit code for --help with rev-parse --parseopt","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-03-15T00:52:22Z","receivedAt":"2026-03-15T00:52:29Z","isPatch":false,"body":"I use git rev-parse --parseopt to parse command-line options in various\nscript.  I've noticed that it exits 129 if the options are invalid,\nwhich is fine, but that it also exits 129 if --help or -h are given,\nwhich is not.\n\nThe standard philosophy is that if the user explicitly asked for help,\nthen help output should be printed to standard output (since that's what\nthe user asked for) and it should exit 0, since the program fulfilled\nthe user's request successfully.  If the help output is provided because\nthe user provided an invalid option or argument, then the output should\nGo to standard error (since it's an error message) and the program\nshould exit unsuccessfully (since it did not fulfill the user's request\nsuccessfully).\n\nGit gets the location of the output just fine, but currently there's no\nway for a program to detect the help output and exit successfully.  Note\nthat Git subcommands don't have this problem because Git itself\nintercepts the help output and sends it to man (or whatever the user has\nconfigured), which then exits successfully.\n\nI'd like to fix this in git rev-parse --parseopt (that is, I am willing\nto send a patch), but I'm unsure about the best way to go about this.\nDoes anyone have ideas about how they'd like this to be fixed?  I could\nsimply change the exit code in this case or I could add an option to\ncontrol this behaviour for backwards compatibility.\n\nExample:\n\n----\n#!/bin/sh\n\nOPTS_SPEC=\"\\\nfoo-cmd [<options>] <args>...\n\nDo stuff.\n--\nh,help!         show this help\n\no,output=       output to this file\nd,dir=          specify dependencies relative to this directory\n\"\n\nmain () {\n    eval \"$(echo \"$OPTS_SPEC\" | git rev-parse --parseopt -- \"$@\" || echo exit $?)\"\n\n    # Do something with the options here.\n}\n\nmain \"$@\"\n----\n\nOutput:\n\n----\n% ./foo-cmd --help >/dev/null; echo $?\n129\n% ./foo-cmd -h >/dev/null; echo $?\n129\n% ./foo-cmd --bassoon >/dev/null; echo $?\nerror: unknown option `bassoon'\nusage: foo-cmd [<options>] <args>...\n\n    Do stuff.\n\n    -h, --help            show this help\n    -o, --[no-]output ... output to this file\n    -d, --[no-]dir ...    specify dependencies relative to this directory\n\n129\n----\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"539008","messageId":"20260315031447.GB926820@coredump.intra.peff.net","threadId":"65249","inReplyTo":"abYCxrEEPaI21g3H@fruit.crustytoothpaste.net","subject":"Re: Unexpected exit code for --help with rev-parse --parseopt","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-15T03:14:47Z","receivedAt":"2026-03-15T03:14:50Z","isPatch":false,"body":"On Sun, Mar 15, 2026 at 12:52:22AM +0000, brian m. carlson wrote:\n\n> I use git rev-parse --parseopt to parse command-line options in various\n> script.  I've noticed that it exits 129 if the options are invalid,\n> which is fine, but that it also exits 129 if --help or -h are given,\n> which is not.\n> \n> The standard philosophy is that if the user explicitly asked for help,\n> then help output should be printed to standard output (since that's what\n> the user asked for) and it should exit 0, since the program fulfilled\n> the user's request successfully.  If the help output is provided because\n> the user provided an invalid option or argument, then the output should\n> Go to standard error (since it's an error message) and the program\n> should exit unsuccessfully (since it did not fulfill the user's request\n> successfully).\n\nI agree with this philosophy in general, but there's catch here with\n\"rev-parse --parseopt\", because it's running as a separate program. It\nneeds to signal to the calling program that \"-h\" was seen, the program\nshould not proceed normally (because rev-parse already dumped help\noutput).\n\nSo if your proposal is to just exit with code 0 from rev-parse, I don't\nthink that works.\n\nMy first instinct is that it would have to exit with some other\nwell-known code, the callers would recognize that, and then exit\nthemselves (with code 0).\n\nBut there's a little more to the story. The output of --parseopt is meant\nto be eval'd by the shell. And so when it works, we get a \"set\" command:\n\n  $ git rev-parse --parseopt <input -- --output=foo\n  set -- -o 'foo' --\n\nAnd when there's an error, it dumps the usage text straight to stderr\nand exits 129:\n\n  $ git rev-parse --parseopt <input -- --foo\n  error: unknown option `foo'\n  usage: foo-cmd [<options>] <args>...\n  \n      Do stuff.\n  \n      -h, --help            show this help\n      -o, --[no-]output ... output to this file\n      -d, --[no-]dir ...    specify dependencies relative to this directory\n\nBut when the user asks for \"-h\", we get a here-doc! Like this:\n\n  $ git rev-parse --parseopt <input -- -h\n  cat <<\\EOF\n  usage: foo-cmd [<options>] <args>...\n  \n      Do stuff.\n  \n      -h, --help            show this help\n      -o, --[no-]output ... output to this file\n      -d, --[no-]dir ...    specify dependencies relative to this directory\n  \n  EOF\n\nSo the calling program runs that cat command. And I think what you\nreally want is to tack \"exit 0\" onto the end of that output, which would\ntell the callers to exit. Something like this:\n\ndiff --git a/parse-options.c b/parse-options.c\nindex a676da86f5..b990f38419 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1473,8 +1473,10 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \t}\n \tfputc('\\n', outfile);\n \n-\tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n+\tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL) {\n \t\tfputs(\"EOF\\n\", outfile);\n+\t\tfputs(\"exit 0\\n\", outfile);\n+\t}\n \n \treturn PARSE_OPT_HELP;\n }\n\nAnd then you don't even need to change the exit code of rev-parse itself\n(since we'd never hit the \"exit $?\" that the caller tacks on in case of\nfailure). Though I think it might be reasonable to switch it to 0\nanyway.\n\n-Peff\n"},{"id":"539041","messageId":"abblhCBTgamxPOhD@fruit.crustytoothpaste.net","threadId":"65249","inReplyTo":"20260315031447.GB926820@coredump.intra.peff.net","subject":"Re: Unexpected exit code for --help with rev-parse --parseopt","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-03-15T16:59:48Z","receivedAt":"2026-03-15T16:59:51Z","isPatch":false,"body":"On 2026-03-15 at 03:14:47, Jeff King wrote:\n> So the calling program runs that cat command. And I think what you\n> really want is to tack \"exit 0\" onto the end of that output, which would\n> tell the callers to exit. Something like this:\n> \n> diff --git a/parse-options.c b/parse-options.c\n> index a676da86f5..b990f38419 100644\n> --- a/parse-options.c\n> +++ b/parse-options.c\n> @@ -1473,8 +1473,10 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n>  \t}\n>  \tfputc('\\n', outfile);\n>  \n> -\tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n> +\tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL) {\n>  \t\tfputs(\"EOF\\n\", outfile);\n> +\t\tfputs(\"exit 0\\n\", outfile);\n> +\t}\n>  \n>  \treturn PARSE_OPT_HELP;\n>  }\n\nYeah, I did some poking around, saw the heredoc, and came to the same\nconclusion that this was a viable solution.  Since we both seem to agree\nthat this is a good solution and it also has the advantage of being\nbackward compatible (in that nobody has to change their code to get the\nnew behaviour[0]), this seems like the best option.\n\n> And then you don't even need to change the exit code of rev-parse itself\n> (since we'd never hit the \"exit $?\" that the caller tacks on in case of\n> failure). Though I think it might be reasonable to switch it to 0\n> anyway.\n\nI can do that as well, even though I think that is actually\nsubstantially more complicated than the first part.  I'll write up a\nbunch of new tests for these cases in addition.\n\nThanks for a sober second opinion.\n\n[0] As we all know, sometimes one has to use older systems and having\nscripts break on older systems needlessly is quite inconvenient.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"539044","messageId":"20260315181628.GA953713@coredump.intra.peff.net","threadId":"65249","inReplyTo":"abblhCBTgamxPOhD@fruit.crustytoothpaste.net","subject":"Re: Unexpected exit code for --help with rev-parse --parseopt","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-15T18:16:28Z","receivedAt":"2026-03-15T18:16:35Z","isPatch":false,"body":"On Sun, Mar 15, 2026 at 04:59:48PM +0000, brian m. carlson wrote:\n\n> > And then you don't even need to change the exit code of rev-parse itself\n> > (since we'd never hit the \"exit $?\" that the caller tacks on in case of\n> > failure). Though I think it might be reasonable to switch it to 0\n> > anyway.\n> \n> I can do that as well, even though I think that is actually\n> substantially more complicated than the first part.  I'll write up a\n> bunch of new tests for these cases in addition.\n\nI would be perfectly happy to leave it as-is. The exit code _shouldn't_\nmatter anymore if the stdout result (with \"exit 0\") is being eval'd. So\nin that case, maybe leaving it as-is might be the more conservative\nchoice, if the caller has somehow screwed up the eval and we want to\nmake sure they still exit.\n\n-Peff\n"},{"id":"539171","messageId":"20260316220742.1286157-1-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"abYCxrEEPaI21g3H@fruit.crustytoothpaste.net","subject":"[PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-03-16T22:07:42Z","receivedAt":"2026-03-16T22:07:59Z","isPatch":true,"body":"The standard philosophy for Unix software when a help option (such as\n--help) is specified is that the software should exit 0, printing the\nhelp output to standard output, since the standard output is for\nuser-requested output and the program performed the requested task\nsuccessfully.  If the user specifies an incorrect option, then the help\noutput should be printed to standard error (since the user has made a\nmistake) and it should exit unsuccessfully.\n\ngit rev-parse --parseopt properly directs the output in both of these\ncases, but it currently exits 129 when it receives a --help or -h option\non the command line, which causes its invoking script to do the same.\nThis is not in line with the usual behavior and it causes scripts using\nthis command to exit unsuccessfully on --help as well.\n\nNote that Git subcommands implemented using scripts, such as git\nsubmodule, don't have this problem because Git itself intercepts the\n--help option and runs man (or a similar tool), which then exits 0.\nHowever, this still affects the myriad scripts that use this\nfunctionality because Git is widespread and the --parseopt functionality\nis a good way to get sensible option parsing across shells in a portable\nway.\n\nBecause git rev-parse --parseopt is intended to be eval'd by the shell,\nwhen help output is to be printed to standard output, Git actually\nprints a cat command with a heredoc since the standard output is being\nevaluated by the shell.  Thus, to do the right thing, simply add an\n\"exit 0\" right after the end of the heredoc, which will cause the\ninvoking program to exit successfully.\n\nThe usual invocation recommended by the manual page is this:\n\n    eval \"$(echo \"$OPTS_SPEC\" | git rev-parse --parseopt -- \"$@\" || echo exit $?)\"\n\nThus, the fact that git rev-parse --parseopt still exits 129 in this\ncase is irrelevant, since the \"echo exit $?\" will print \"exit 129\", but\nthat will be after the \"exit 0\" printed by Git—and thus ignored, since\nthe shell will have already exited successfully.\n\nUpdate the tests for this case.  Note that we no longer need to delete\nonly the first and last lines in some tests, so add a command to delete\nthe end of the heredoc as well.  We could do something clever with sed\nto delete all but the last two lines or switch to head and tail, but\nthose would be more complicated and less readable, so just stick with\nthe simple approach.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n parse-options.c               | 2 +-\n t/t1502-rev-parse-parseopt.sh | 9 +++++++--\n t/t1502/optionspec-neg.help   | 1 +\n t/t1502/optionspec.help       | 1 +\n 4 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex a676da86f5..85e2f0ea7c 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1474,7 +1474,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \tfputc('\\n', outfile);\n \n \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n-\t\tfputs(\"EOF\\n\", outfile);\n+\t\tfputs(\"EOF\\nexit 0\\n\", outfile);\n \n \treturn PARSE_OPT_HELP;\n }\ndiff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh\nindex 3962f1d288..455608c429 100755\n--- a/t/t1502-rev-parse-parseopt.sh\n+++ b/t/t1502-rev-parse-parseopt.sh\n@@ -12,7 +12,7 @@ check_invalid_long_option () {\n \t\t\tcat <<-\\EOF &&\n \t\t\terror: unknown option `'${opt#--}\\''\n \t\t\tEOF\n-\t\t\tsed -e 1d -e \\$d <\"$TEST_DIRECTORY/t1502/$spec.help\"\n+\t\t\tsed -e 1d -e /EOF/d -e \\$d <\"$TEST_DIRECTORY/t1502/$spec.help\"\n \t\t} >expect &&\n \t\ttest_expect_code 129 git rev-parse --parseopt -- $opt \\\n \t\t\t2>output <\"$TEST_DIRECTORY/t1502/$spec\" &&\n@@ -87,6 +87,7 @@ test_expect_success 'test --parseopt help output no switches' '\n |    some-command does foo and bar!\n |\n |EOF\n+|exit 0\n END_EXPECT\n \ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_no_switches &&\n \ttest_cmp expect output\n@@ -100,6 +101,7 @@ test_expect_success 'test --parseopt help output hidden switches' '\n |    some-command does foo and bar!\n |\n |EOF\n+|exit 0\n END_EXPECT\n \ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n@@ -115,6 +117,7 @@ test_expect_success 'test --parseopt help-all output hidden switches' '\n |    --[no-]hidden1        A hidden switch\n |\n |EOF\n+|exit 0\n END_EXPECT\n \ttest_expect_code 129 git rev-parse --parseopt -- --help-all > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n@@ -125,7 +128,7 @@ test_expect_success 'test --parseopt invalid switch help output' '\n \t\tcat <<-\\EOF &&\n \t\terror: unknown option `does-not-exist'\\''\n \t\tEOF\n-\t\tsed -e 1d -e \\$d <\"$TEST_DIRECTORY/t1502/optionspec.help\"\n+\t\tsed -e 1d -e /EOF/d -e \\$d <\"$TEST_DIRECTORY/t1502/optionspec.help\"\n \t} >expect &&\n \ttest_expect_code 129 git rev-parse --parseopt -- --does-not-exist 1>/dev/null 2>output < optionspec &&\n \ttest_cmp expect output\n@@ -252,6 +255,7 @@ test_expect_success 'test --parseopt help output: \"wrapped\" options normal \"or:\"\n \t|    -h, --help            show the help\n \t|\n \t|EOF\n+\t|exit 0\n \tEND_EXPECT\n \n \ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\n@@ -289,6 +293,7 @@ test_expect_success 'test --parseopt help output: multi-line blurb after empty l\n \t|    -h, --help            show the help\n \t|\n \t|EOF\n+\t|exit 0\n \tEND_EXPECT\n \n \ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\ndiff --git a/t/t1502/optionspec-neg.help b/t/t1502/optionspec-neg.help\nindex 7a29f8cb03..f85be7b8fd 100644\n--- a/t/t1502/optionspec-neg.help\n+++ b/t/t1502/optionspec-neg.help\n@@ -10,3 +10,4 @@ usage: some-command [options] <args>...\n     --no-negative         cannot be positivated\n \n EOF\n+exit 0\ndiff --git a/t/t1502/optionspec.help b/t/t1502/optionspec.help\nindex cbdd54d41b..ded35ebc82 100755\n--- a/t/t1502/optionspec.help\n+++ b/t/t1502/optionspec.help\n@@ -34,3 +34,4 @@ Extras\n     --[no-]extra1         line above used to cause a segfault but no longer does\n \n EOF\n+exit 0\n"},{"id":"539191","messageId":"xmqqcy13mgdk.fsf@gitster.g","threadId":"65249","inReplyTo":"20260316220742.1286157-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-17T00:47:19Z","receivedAt":"2026-03-17T00:47:21Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n> ---\n>  parse-options.c               | 2 +-\n>  t/t1502-rev-parse-parseopt.sh | 9 +++++++--\n>  t/t1502/optionspec-neg.help   | 1 +\n>  t/t1502/optionspec.help       | 1 +\n>  4 files changed, 10 insertions(+), 3 deletions(-)\n\nHas t1517 passed for you?\n\nQueued directly on top of v2.53.0, I am seeing:\n\n>>>>>\nexpecting success of 1517.169 ''git instaweb -h' outside a repository':\n                test_expect_code 129 nongit git $cmd -h >usage &&\n                test_grep \"[Uu]sage: git $cmd \" usage\n\ntest_expect_code: command exited with 0, we wanted 129 nongit git instaweb -h\nnot ok 169 - 'git instaweb -h' outside a repository\n<<<<<\n\n"},{"id":"539213","messageId":"ablCBkmOdoourCnO@fruit.crustytoothpaste.net","threadId":"65249","inReplyTo":"xmqqcy13mgdk.fsf@gitster.g","subject":"Re: [PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-03-17T11:59:02Z","receivedAt":"2026-03-17T11:59:04Z","isPatch":true,"body":"On 2026-03-17 at 00:47:19, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> \n> > Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n> > ---\n> >  parse-options.c               | 2 +-\n> >  t/t1502-rev-parse-parseopt.sh | 9 +++++++--\n> >  t/t1502/optionspec-neg.help   | 1 +\n> >  t/t1502/optionspec.help       | 1 +\n> >  4 files changed, 10 insertions(+), 3 deletions(-)\n> \n> Has t1517 passed for you?\n> \n> Queued directly on top of v2.53.0, I am seeing:\n> \n> >>>>>\n> expecting success of 1517.169 ''git instaweb -h' outside a repository':\n>                 test_expect_code 129 nongit git $cmd -h >usage &&\n>                 test_grep \"[Uu]sage: git $cmd \" usage\n> \n> test_expect_code: command exited with 0, we wanted 129 nongit git instaweb -h\n> not ok 169 - 'git instaweb -h' outside a repository\n> <<<<<\n\nI thought the tests passed, but I may have neglected to run them on the\nlatest revision.  Go ahead and drop this for now and I'll send out a v2\neither tonight or later this week.  My apologies.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"539223","messageId":"20260317145543.GA1828@coredump.intra.peff.net","threadId":"65249","inReplyTo":"ablCBkmOdoourCnO@fruit.crustytoothpaste.net","subject":"Re: [PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-17T14:55:43Z","receivedAt":"2026-03-17T14:55:45Z","isPatch":true,"body":"On Tue, Mar 17, 2026 at 11:59:02AM +0000, brian m. carlson wrote:\n\n> On 2026-03-17 at 00:47:19, Junio C Hamano wrote:\n> > \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> > \n> > > Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n> > > ---\n> > >  parse-options.c               | 2 +-\n> > >  t/t1502-rev-parse-parseopt.sh | 9 +++++++--\n> > >  t/t1502/optionspec-neg.help   | 1 +\n> > >  t/t1502/optionspec.help       | 1 +\n> > >  4 files changed, 10 insertions(+), 3 deletions(-)\n> > \n> > Has t1517 passed for you?\n> > \n> > Queued directly on top of v2.53.0, I am seeing:\n> > \n> > >>>>>\n> > expecting success of 1517.169 ''git instaweb -h' outside a repository':\n> >                 test_expect_code 129 nongit git $cmd -h >usage &&\n> >                 test_grep \"[Uu]sage: git $cmd \" usage\n> > \n> > test_expect_code: command exited with 0, we wanted 129 nongit git instaweb -h\n> > not ok 169 - 'git instaweb -h' outside a repository\n> > <<<<<\n> \n> I thought the tests passed, but I may have neglected to run them on the\n> latest revision.  Go ahead and drop this for now and I'll send out a v2\n> either tonight or later this week.  My apologies.\n\nHmm. Is this just a matter of tweaking the tests, or is there something\nbigger going on?\n\nThese tests are _expecting_ the 129 exit code from most commands. And\nindeed, \"git log -h\" produces 129 for example. So it is not just shell\nscripts using \"rev-parse --parseopt\" that do this, and those scripts are\n(currently) consistent with the rest of Git.\n\nYou'd need something like this (untested) to hit the non-shell commands:\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 85e2f0ea7c..0ad43ca278 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1197,6 +1197,7 @@ int parse_options(int argc, const char **argv,\n \tparse_options_start_1(&ctx, argc, argv, prefix, options, flags);\n \tswitch (parse_options_step(&ctx, options, usagestr)) {\n \tcase PARSE_OPT_HELP:\n+\t\texit(0);\n \tcase PARSE_OPT_ERROR:\n \t\texit(129);\n \tcase PARSE_OPT_COMPLETE:\n@@ -1495,11 +1496,11 @@ void show_usage_with_options_if_asked(int ac, const char **av,\n \t\tif (!strcmp(av[1], \"-h\")) {\n \t\t\tusage_with_options_internal(NULL, usagestr, opts,\n \t\t\t\t\t\t    USAGE_NORMAL, USAGE_TO_STDOUT);\n-\t\t\texit(129);\n+\t\t\texit(0);\n \t\t} else if (!strcmp(av[1], \"--help-all\")) {\n \t\t\tusage_with_options_internal(NULL, usagestr, opts,\n \t\t\t\t\t\t    USAGE_FULL, USAGE_TO_STDOUT);\n-\t\t\texit(129);\n+\t\t\texit(0);\n \t\t}\n \t}\n }\n\nI agree with the general idea that \"-h\" usually should exit 0. But this\nis not just a bug fix for --parseopt, but a change in overall intent. It\nmight be worth digging in the commit history or list archive to see if\nthere's any discussion on why we are using 129 in the first place.\n\n-Peff\n"},{"id":"539224","messageId":"20260317150759.GA9975@coredump.intra.peff.net","threadId":"65249","inReplyTo":"20260317145543.GA1828@coredump.intra.peff.net","subject":"Re: [PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-17T15:07:59Z","receivedAt":"2026-03-17T15:08:00Z","isPatch":true,"body":"On Tue, Mar 17, 2026 at 10:55:43AM -0400, Jeff King wrote:\n\n> I agree with the general idea that \"-h\" usually should exit 0. But this\n> is not just a bug fix for --parseopt, but a change in overall intent. It\n> might be worth digging in the commit history or list archive to see if\n> there's any discussion on why we are using 129 in the first place.\n\nI dug around a little but couldn't find anything conclusive. The first\n\"129\" goes back to 2007:\n\n  https://lore.kernel.org/git/20071013221450.GC2875@steel.home/\n\nBut there (and most spots where it is added) it is being used to show\noptions when we have an unknown option. So I _think_ what mostly\nhappened is that we show options in two different scenarios: on an\nerror, and when somebody asks for \"-h\". And we never differentiated the\ntwo, and the latter just inherited the exit code for the former.\n\nFor example, when you look at ff43ec3e2d (parse-opt: create\nparse_options_step., 2008-06-23), it introduces:\n\n+       switch (parse_options_step(&ctx, options, usagestr)) {\n+       case PARSE_OPT_HELP:\n+               exit(129);\n\nand only later did we add PARSE_OPT_ERROR to that switch statement. And\nback then, PARSE_OPT_HELP came only from running usage_with_options().\n\nBut I couldn't find any discussion or intentional use of 129 for \"-h\"\noutput. I do suspect that there may still be some untangling to do. In\nthe earlier (again, untested) patch I showed, I put an exit(0) for that\nPARSE_OPT_HELP case. But it may be that there's more surgery needed to\ndifferentiate \"we showed help because of -h\" versus \"we showed help\nbecause you gave a bogus option\".\n\n-Peff\n"},{"id":"539242","messageId":"xmqqv7eul71y.fsf@gitster.g","threadId":"65249","inReplyTo":"20260317145543.GA1828@coredump.intra.peff.net","subject":"Re: [PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-17T17:06:17Z","receivedAt":"2026-03-17T17:06:20Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> I agree with the general idea that \"-h\" usually should exit 0.\n\nYes.\n\n> But this\n> is not just a bug fix for --parseopt, but a change in overall intent. It\n> might be worth digging in the commit history or list archive to see if\n> there's any discussion on why we are using 129 in the first place.\n\nAnd if it turns out that old discussion was convincing enough, I'd\nprefer to see us moving to --help/-h exiting with 0 eventually.  I\ndo not mind doing so at the Git 3.0 boundary, if an excuse to make\na big change is needed ;-)\n"},{"id":"539248","messageId":"20260317184441.GA574291@coredump.intra.peff.net","threadId":"65249","inReplyTo":"xmqqv7eul71y.fsf@gitster.g","subject":"Re: [PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-17T18:44:41Z","receivedAt":"2026-03-17T18:44:43Z","isPatch":true,"body":"On Tue, Mar 17, 2026 at 10:06:17AM -0700, Junio C Hamano wrote:\n\n> > But this\n> > is not just a bug fix for --parseopt, but a change in overall intent. It\n> > might be worth digging in the commit history or list archive to see if\n> > there's any discussion on why we are using 129 in the first place.\n> \n> And if it turns out that old discussion was convincing enough, I'd\n> prefer to see us moving to --help/-h exiting with 0 eventually.  I\n> do not mind doing so at the Git 3.0 boundary, if an excuse to make\n> a big change is needed ;-)\n\nI could not find anything convincing. :)\n\nWe should also apply our little grey cells and see if we can think of\nany reason somebody would be unhappy with a change of exit code now. The\nmost I could come up with is a script like:\n\n  #!/bin/sh\n  git rev-list \"$@\" >tmp &&\n  do_something <tmp\n\nwhere you might imagine it is run as \"foo.sh --objects HEAD\" or\nsomething. Right now, running:\n\n  foo.sh -h\n\nwill bail when rev-list returns 129, but in the proposed world it would\nkeep going and run do_something. And you can further imagine a world\nwhere the script then quietly produces the wrong answer, because\ndo_something thinks the rev-list output was empty (or actually it is\nworse; it gets the help output here).\n\nI think this is mostly a case of \"if it hurts, don't do it\". The \"-h\" is\nnot doing anything useful (the user does not even see the help text!),\nand if the script wants to support a usage message, it should parse the\n\"-h\" out separately.\n\nCould somebody maliciously convince you to pass \"-h\" to such a script?\nMaybe, but not only are we getting into a series of increasingly\nunlikely events, but I think that means you probably have worse\noption-injection risks.\n\nSo unless somebody can come up with a more compelling example, I don't\nreally see much backwards-compatibility risk. But maybe I just lack\nimagination. ;)\n\n-Peff\n"},{"id":"539271","messageId":"abnwxmoOw-ZLT858@fruit.crustytoothpaste.net","threadId":"65249","inReplyTo":"20260317184441.GA574291@coredump.intra.peff.net","subject":"Re: [PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-03-18T00:24:38Z","receivedAt":"2026-03-18T00:24:46Z","isPatch":true,"body":"On 2026-03-17 at 18:44:41, Jeff King wrote:\n> So unless somebody can come up with a more compelling example, I don't\n> really see much backwards-compatibility risk. But maybe I just lack\n> imagination. ;)\n\nThe only reason I can imagine intentionally using `-h` in a script is to\nfind out whether an option is supported.  For instance, I have this\nalias:\n\n    co = \"!f() { if git checkout -h | grep -qs recurse-submodules; \\\n        then git checkout --recurse-submodules \\\"$@\\\"; \\\n        else git checkout \\\"$@\\\" && git sui; \\\n        fi; };f\"\n\n(`sui` is `submodule update --init`.)\n\nIn most cases, that's going to be upstream of a pipe, so unless you're\nusing `-o pipefail` (which is only in POSIX in POSIX 1003.1-2024), it's\ngoing to succeed anyway.\n\nI suppose one can also use it to generate a manual page, which manual\npage generators do, but that seems silly when Git provides much better\nones unless you desperately need one in a different language for which\nGit has translations but no manual page.\n\nNone of these seem like they're likely to care about the exit status and\nI suspect that if they do, they are probably using `|| true` to ignore\nthe unexpected 129 exit code.\n\nSo I agree that there's unlikely to be any sort of backward\ncompatibility issues.  If the consensus is that this is shipped only in\n3.0, then we can do that, but I think many people are not going to care\nand those that do will welcome the change, so I'd just rather treat it\nas a bug that we fix.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"539273","messageId":"20260318012214.GC720335@coredump.intra.peff.net","threadId":"65249","inReplyTo":"abnwxmoOw-ZLT858@fruit.crustytoothpaste.net","subject":"Re: [PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-03-18T01:22:14Z","receivedAt":"2026-03-18T01:22:16Z","isPatch":true,"body":"On Wed, Mar 18, 2026 at 12:24:38AM +0000, brian m. carlson wrote:\n\n> None of these seem like they're likely to care about the exit status and\n> I suspect that if they do, they are probably using `|| true` to ignore\n> the unexpected 129 exit code.\n\nYeah, I'd venture to say that moving from 129 to 0 would be a strict\nimprovement for the cases you outlined.\n\n> So I agree that there's unlikely to be any sort of backward\n> compatibility issues.  If the consensus is that this is shipped only in\n> 3.0, then we can do that, but I think many people are not going to care\n> and those that do will welcome the change, so I'd just rather treat it\n> as a bug that we fix.\n\nNah, I think everything I have seen points to treating this as a simple\nimprovement / fix that we can do in the regular way.\n\n-Peff\n"},{"id":"539276","messageId":"xmqqikatj1nr.fsf@gitster.g","threadId":"65249","inReplyTo":"20260318012214.GC720335@coredump.intra.peff.net","subject":"Re: [PATCH] rev-parse: have --parseopt callers exit 0 on --help","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-18T02:45:44Z","receivedAt":"2026-03-18T02:45:47Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> On Wed, Mar 18, 2026 at 12:24:38AM +0000, brian m. carlson wrote:\n>\n>> None of these seem like they're likely to care about the exit status and\n>> I suspect that if they do, they are probably using `|| true` to ignore\n>> the unexpected 129 exit code.\n>\n> Yeah, I'd venture to say that moving from 129 to 0 would be a strict\n> improvement for the cases you outlined.\n>\n>> So I agree that there's unlikely to be any sort of backward\n>> compatibility issues.  If the consensus is that this is shipped only in\n>> 3.0, then we can do that, but I think many people are not going to care\n>> and those that do will welcome the change, so I'd just rather treat it\n>> as a bug that we fix.\n>\n> Nah, I think everything I have seen points to treating this as a simple\n> improvement / fix that we can do in the regular way.\n\nI 100% agree.  The message I earlier wrote (which contained the\nmention of Git 3.0) lacked \"even\" before an \"if\".\n\n"},{"id":"546939","messageId":"20260701212442.1430084-1-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260316220742.1286157-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 0/4] rev-parse: exit 0 on --help","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-01T21:24:38Z","receivedAt":"2026-07-01T21:24:53Z","isPatch":true,"body":"The standard philosophy for Unix software when a help option (such as\n--help) is specified is that the software should exit 0, printing the\nhelp output to standard output, since the standard output is for\nuser-requested output and the program performed the requested task\nsuccessfully.  If the user specifies an incorrect option, then the help\noutput should be printed to standard error (since the user has made a\nmistake) and it should exit unsuccessfully.\n\ngit rev-parse --parseopt properly directs the output in both of these\ncases, but it currently exits 129 when it receives a --help or -h option\non the command line, which causes its invoking script to do the same.\nThis is not in line with the usual behavior and it causes scripts using\nthis command to exit unsuccessfully on --help as well.\n\nThis series introduces some changes to distinguish the --help and -h\noptions from other cases in which we print help output and adjusts the\nexit code to 0 from those two options.  We continue to exit 129 when the\noptions are invalid, which is useful information to have for callers.\nWe also make the relevant changes such that `git rev-parse --parseopt`\ndoes the same thing as long as it is invoked in the way specified in the\nmanual page (which a quick GitHub search shows almost everyone does).\n\nOne of the patches is rather long because we have many cases in which\nwe've hard-coded exit code 129 into our tests.  However, the changes\nthere should not be complex, only somewhat tedious to review.\n\nbrian m. carlson (4):\n  t1517: skip svn tests if svn is not installed\n  parse-options: add a separate case for help output on error\n  rev-parse: have --parseopt callers exit 0 on --help\n  parse-options: exit 0 on -h\n\n builtin/blame.c                    |  2 ++\n builtin/shortlog.c                 |  2 ++\n builtin/update-index.c             |  2 ++\n contrib/subtree/t/t7900-subtree.sh |  2 +-\n parse-options.c                    | 20 ++++++++++----\n parse-options.h                    |  3 ++-\n t/for-each-ref-tests.sh            |  2 +-\n t/t0012-help.sh                    |  2 +-\n t/t0040-parse-options.sh           |  2 +-\n t/t0450-txt-doc-vs-help.sh         |  2 +-\n t/t0610-reftable-basics.sh         |  4 +--\n t/t1403-show-ref.sh                |  2 +-\n t/t1410-reflog.sh                  |  4 +--\n t/t1418-reflog-exists.sh           |  2 +-\n t/t1502-rev-parse-parseopt.sh      | 23 +++++++++-------\n t/t1502/optionspec-neg.help        |  1 +\n t/t1502/optionspec.help            |  1 +\n t/t1517-outside-repo.sh            | 43 +++++++++++++++++++++---------\n t/t1800-hook.sh                    |  4 +--\n t/t1900-repo-info.sh               |  2 +-\n t/t1901-repo-structure.sh          |  2 +-\n t/t2006-checkout-index-basic.sh    |  6 ++---\n t/t2107-update-index-basic.sh      |  2 +-\n t/t3004-ls-files-basic.sh          |  6 ++---\n t/t3200-branch.sh                  |  2 +-\n t/t3903-stash.sh                   |  4 +--\n t/t4200-rerere.sh                  |  2 +-\n t/t5200-update-server-info.sh      |  2 +-\n t/t5304-prune.sh                   |  2 +-\n t/t5400-send-pack.sh               |  4 +--\n t/t5512-ls-remote.sh               |  2 +-\n t/t6300-for-each-ref.sh            |  4 +--\n t/t6500-gc.sh                      |  2 +-\n t/t7030-verify-tag.sh              |  4 +--\n t/t7508-status.sh                  |  4 +--\n t/t7510-signed-commit.sh           |  4 +--\n t/t7600-merge.sh                   |  2 +-\n t/t7800-difftool.sh                |  3 +--\n t/t7900-maintenance.sh             |  2 +-\n usage.c                            |  2 +-\n 40 files changed, 113 insertions(+), 73 deletions(-)\n\n"},{"id":"546940","messageId":"20260701212442.1430084-4-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260701212442.1430084-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 3/4] rev-parse: have --parseopt callers exit 0 on --help","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-01T21:24:41Z","receivedAt":"2026-07-01T21:24:53Z","isPatch":true,"body":"The standard philosophy for Unix software when a help option (such as\n--help) is specified is that the software should exit 0, printing the\nhelp output to standard output, since the standard output is for\nuser-requested output and the program performed the requested task\nsuccessfully.  If the user specifies an incorrect option, then the help\noutput should be printed to standard error (since the user has made a\nmistake) and it should exit unsuccessfully.\n\ngit rev-parse --parseopt properly directs the output in both of these\ncases, but it currently exits 129 when it receives a --help or -h option\non the command line, which causes its invoking script to do the same.\nThis is not in line with the usual behavior and it causes scripts using\nthis command to exit unsuccessfully on --help as well.\n\nNote that Git subcommands implemented using scripts, such as git\nsubmodule, don't have this problem because Git itself intercepts the\n--help option and runs man (or a similar tool), which then exits 0.\nHowever, this still affects the myriad scripts that use this\nfunctionality because Git is widespread and the --parseopt functionality\nis a good way to get sensible option parsing across shells in a portable\nway.\n\nBecause git rev-parse --parseopt is intended to be eval'd by the shell,\nwhen help output is to be printed to standard output, Git actually\nprints a cat command with a heredoc since the standard output is being\nevaluated by the shell.  Thus, to do the right thing, simply add an\n\"exit 0\" right after the end of the heredoc, which will cause the\ninvoking program to exit successfully.\n\nThe usual invocation recommended by the manual page is this:\n\n    eval \"$(echo \"$OPTS_SPEC\" | git rev-parse --parseopt -- \"$@\" || echo exit $?)\"\n\nThus, the fact that git rev-parse --parseopt still exits 129 in this\ncase is irrelevant, since the \"echo exit $?\" will print \"exit 129\", but\nthat will be after the \"exit 0\" printed by Git—and thus ignored, since\nthe shell will have already exited successfully.\n\nUpdate the tests for this case.  Note that we no longer need to delete\nonly the first and last lines in some tests, so add a command to delete\nthe end of the heredoc as well.  We could do something clever with sed\nto delete all but the last two lines or switch to head and tail, but\nthose would be more complicated and less readable, so just stick with\nthe simple approach.\n\nIn t1517, add three shell scripts to the failure case because they no\nlonger return 129 as expected.  In a future commit, we'll change the\nexpected result to exit 0 and these will become successful again.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n parse-options.c               | 2 +-\n t/t1502-rev-parse-parseopt.sh | 9 +++++++--\n t/t1502/optionspec-neg.help   | 1 +\n t/t1502/optionspec.help       | 1 +\n t/t1517-outside-repo.sh       | 6 +++---\n 5 files changed, 13 insertions(+), 6 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex a623961800..67a2d372d0 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1479,7 +1479,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \tfputc('\\n', outfile);\n \n \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n-\t\tfputs(\"EOF\\n\", outfile);\n+\t\tfputs(\"EOF\\nexit 0\\n\", outfile);\n \n \treturn PARSE_OPT_HELP;\n }\ndiff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh\nindex 3962f1d288..455608c429 100755\n--- a/t/t1502-rev-parse-parseopt.sh\n+++ b/t/t1502-rev-parse-parseopt.sh\n@@ -12,7 +12,7 @@ check_invalid_long_option () {\n \t\t\tcat <<-\\EOF &&\n \t\t\terror: unknown option `'${opt#--}\\''\n \t\t\tEOF\n-\t\t\tsed -e 1d -e \\$d <\"$TEST_DIRECTORY/t1502/$spec.help\"\n+\t\t\tsed -e 1d -e /EOF/d -e \\$d <\"$TEST_DIRECTORY/t1502/$spec.help\"\n \t\t} >expect &&\n \t\ttest_expect_code 129 git rev-parse --parseopt -- $opt \\\n \t\t\t2>output <\"$TEST_DIRECTORY/t1502/$spec\" &&\n@@ -87,6 +87,7 @@ test_expect_success 'test --parseopt help output no switches' '\n |    some-command does foo and bar!\n |\n |EOF\n+|exit 0\n END_EXPECT\n \ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_no_switches &&\n \ttest_cmp expect output\n@@ -100,6 +101,7 @@ test_expect_success 'test --parseopt help output hidden switches' '\n |    some-command does foo and bar!\n |\n |EOF\n+|exit 0\n END_EXPECT\n \ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n@@ -115,6 +117,7 @@ test_expect_success 'test --parseopt help-all output hidden switches' '\n |    --[no-]hidden1        A hidden switch\n |\n |EOF\n+|exit 0\n END_EXPECT\n \ttest_expect_code 129 git rev-parse --parseopt -- --help-all > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n@@ -125,7 +128,7 @@ test_expect_success 'test --parseopt invalid switch help output' '\n \t\tcat <<-\\EOF &&\n \t\terror: unknown option `does-not-exist'\\''\n \t\tEOF\n-\t\tsed -e 1d -e \\$d <\"$TEST_DIRECTORY/t1502/optionspec.help\"\n+\t\tsed -e 1d -e /EOF/d -e \\$d <\"$TEST_DIRECTORY/t1502/optionspec.help\"\n \t} >expect &&\n \ttest_expect_code 129 git rev-parse --parseopt -- --does-not-exist 1>/dev/null 2>output < optionspec &&\n \ttest_cmp expect output\n@@ -252,6 +255,7 @@ test_expect_success 'test --parseopt help output: \"wrapped\" options normal \"or:\"\n \t|    -h, --help            show the help\n \t|\n \t|EOF\n+\t|exit 0\n \tEND_EXPECT\n \n \ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\n@@ -289,6 +293,7 @@ test_expect_success 'test --parseopt help output: multi-line blurb after empty l\n \t|    -h, --help            show the help\n \t|\n \t|EOF\n+\t|exit 0\n \tEND_EXPECT\n \n \ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\ndiff --git a/t/t1502/optionspec-neg.help b/t/t1502/optionspec-neg.help\nindex 7a29f8cb03..f85be7b8fd 100644\n--- a/t/t1502/optionspec-neg.help\n+++ b/t/t1502/optionspec-neg.help\n@@ -10,3 +10,4 @@ usage: some-command [options] <args>...\n     --no-negative         cannot be positivated\n \n EOF\n+exit 0\ndiff --git a/t/t1502/optionspec.help b/t/t1502/optionspec.help\nindex cbdd54d41b..ded35ebc82 100755\n--- a/t/t1502/optionspec.help\n+++ b/t/t1502/optionspec.help\n@@ -34,3 +34,4 @@ Extras\n     --[no-]extra1         line above used to cause a segfault but no longer does\n \n EOF\n+exit 0\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex 583784f21b..99bda36d17 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -133,10 +133,10 @@ do\n \tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n \tget-tar-commit-id | \\\n \tgui | gui--askpass | \\\n-\thttp-backend | http-fetch | http-push | init-db | \\\n+\thttp-backend | http-fetch | http-push | init-db | instaweb | \\\n \tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\n-\tmktag | p4 | p4.py | pickaxe | remote-ftp | remote-ftps | \\\n-\tremote-http | remote-https | replay | send-email | \\\n+\tmktag | p4 | p4.py | pickaxe | quiltimport | remote-ftp | remote-ftps | \\\n+\tremote-http | remote-https | replay | request-pull | send-email | \\\n \tsh-i18n--envsubst | shell | show | stage | submodule | svn | \\\n \tupload-archive--writer | upload-pack | web--browse | whatchanged)\n \t\texpect_outcome=expect_failure ;;\n"},{"id":"546941","messageId":"20260701212442.1430084-2-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260701212442.1430084-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 1/4] t1517: skip svn tests if svn is not installed","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-01T21:24:39Z","receivedAt":"2026-07-01T21:24:53Z","isPatch":true,"body":"The svn tests currently assume that git-svn's option parsing will always\nfail the tests because it exits 0 on --help, not 129.  However, in a\nfuture commit, we'll expect it to exit 0 and the tests will then need to\nbe updated to succeed in some cases and fail in others.\n\nWe therefore need to have t1517 determine whether the Subversion Perl\nmodules are present, since if they are not, git-svn will die on start\nand then it needs to continue to expect failure.  Add a stripped down\nversion of the tests in t/lib-git-svn.sh as a prerequisite we can use\nhere for our svn tests.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n t/t1517-outside-repo.sh | 10 ++++++++++\n 1 file changed, 10 insertions(+)\n\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex c557f2f55c..583784f21b 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -4,6 +4,14 @@ test_description='check random commands outside repo'\n \n . ./test-lib.sh\n \n+test_lazy_prereq SVN '\n+\ttest_have_prereq PERL && test -n \"$NO_SVN_TESTS\" && perl -w -e \"\n+\t\tuse SVN::Core;\n+\t\tuse SVN::Repos;\n+\t\t\\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n+\t\"\n+'\n+\n test_expect_success 'set up a non-repo directory and test file' '\n \tGIT_CEILING_DIRECTORIES=$(pwd) &&\n \texport GIT_CEILING_DIRECTORIES &&\n@@ -138,6 +146,8 @@ do\n \tcase \"$cmd\" in\n \tinstaweb)\n \t\tprereq=PERL ;;\n+\tsvn)\n+\t\tprereq=SVN ;;\n \t*)\n \t\tprereq= ;;\n \tesac\n"},{"id":"546942","messageId":"20260701212442.1430084-5-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260701212442.1430084-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 4/4] parse-options: exit 0 on -h","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-01T21:24:42Z","receivedAt":"2026-07-01T21:24:53Z","isPatch":true,"body":"The standard philosophy for Unix software when a help option (such as\n--help) is specified is that the software should exit 0, printing the\nhelp output to standard output, since the standard output is for\nuser-requested output and the program performed the requested task\nsuccessfully.  If the user specifies an incorrect option, then the help\noutput should be printed to standard error (since the user has made a\nmistake) and it should exit unsuccessfully.\n\nMost of our commands currently exit 129 on receiving the -h option to\nprint the short help, which does not line up with the standard\nphilosophy above.  Let's change that to exit 0 instead.\n\nThis requires changes to a variety of tests which previously wanted the\n129 exit code, so update them.  Note that because git diff does its own\noption parsing, it still exits with 129, so update some of the tests to\nexpect either exit status.\n\nSome commands also now pass with -h but not --help-all, so handle those\ncases differently for those commands.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n builtin/blame.c                    |  1 +\n builtin/shortlog.c                 |  1 +\n builtin/update-index.c             |  1 +\n contrib/subtree/t/t7900-subtree.sh |  2 +-\n parse-options.c                    | 13 +++++++---\n t/for-each-ref-tests.sh            |  2 +-\n t/t0012-help.sh                    |  2 +-\n t/t0040-parse-options.sh           |  2 +-\n t/t0450-txt-doc-vs-help.sh         |  2 +-\n t/t0610-reftable-basics.sh         |  4 +--\n t/t1403-show-ref.sh                |  2 +-\n t/t1410-reflog.sh                  |  4 +--\n t/t1418-reflog-exists.sh           |  2 +-\n t/t1502-rev-parse-parseopt.sh      | 14 +++++------\n t/t1517-outside-repo.sh            | 39 ++++++++++++++++++------------\n t/t1800-hook.sh                    |  4 +--\n t/t1900-repo-info.sh               |  2 +-\n t/t1901-repo-structure.sh          |  2 +-\n t/t2006-checkout-index-basic.sh    |  6 ++---\n t/t2107-update-index-basic.sh      |  2 +-\n t/t3004-ls-files-basic.sh          |  6 ++---\n t/t3200-branch.sh                  |  2 +-\n t/t3903-stash.sh                   |  4 +--\n t/t4200-rerere.sh                  |  2 +-\n t/t5200-update-server-info.sh      |  2 +-\n t/t5304-prune.sh                   |  2 +-\n t/t5400-send-pack.sh               |  4 +--\n t/t5512-ls-remote.sh               |  2 +-\n t/t6300-for-each-ref.sh            |  4 +--\n t/t6500-gc.sh                      |  2 +-\n t/t7030-verify-tag.sh              |  4 +--\n t/t7508-status.sh                  |  4 +--\n t/t7510-signed-commit.sh           |  4 +--\n t/t7600-merge.sh                   |  2 +-\n t/t7800-difftool.sh                |  3 +--\n t/t7900-maintenance.sh             |  2 +-\n usage.c                            |  2 +-\n 37 files changed, 86 insertions(+), 72 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 65d43c7d48..38749f79c2 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -1013,6 +1013,7 @@ int cmd_blame(int argc,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n+\t\t\texit(0);\n \t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\tcase PARSE_OPT_SUBCOMMAND:\ndiff --git a/builtin/shortlog.c b/builtin/shortlog.c\nindex cd262bd376..4c78d2e5ba 100644\n--- a/builtin/shortlog.c\n+++ b/builtin/shortlog.c\n@@ -433,6 +433,7 @@ int cmd_shortlog(int argc,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n+\t\t\texit(0);\n \t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\tcase PARSE_OPT_SUBCOMMAND:\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex ac4610ec94..6810327209 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1133,6 +1133,7 @@ int cmd_update_index(int argc,\n \t\t\tbreak;\n \t\tswitch (parseopt_state) {\n \t\tcase PARSE_OPT_HELP:\n+\t\t\texit(0);\n \t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\t\texit(129);\ndiff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh\nindex 4194687cfb..c10f283b38 100755\n--- a/contrib/subtree/t/t7900-subtree.sh\n+++ b/contrib/subtree/t/t7900-subtree.sh\n@@ -99,7 +99,7 @@ test_create_subtree_add () {\n }\n \n test_expect_success 'shows short help text for -h' '\n-\ttest_expect_code 129 git subtree -h >out 2>err &&\n+\tgit subtree -h >out 2>err &&\n \ttest_must_be_empty err &&\n \tgrep -e \"^ *or: git subtree pull\" out &&\n \tgrep -F -e \"--[no-]annotate\" out\ndiff --git a/parse-options.c b/parse-options.c\nindex 67a2d372d0..742444eead 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1135,8 +1135,9 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tgoto unknown;\n \t\tcase PARSE_OPT_HELP:\n-\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\tgoto show_usage;\n+\t\tcase PARSE_OPT_HELP_ERROR:\n+\t\t\tgoto show_usage_stderr;\n \t\tcase PARSE_OPT_NON_OPTION:\n \t\tcase PARSE_OPT_SUBCOMMAND:\n \t\tcase PARSE_OPT_COMPLETE:\n@@ -1170,6 +1171,9 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n  show_usage:\n \treturn usage_with_options_internal(ctx, usagestr, options,\n \t\t\t\t\t   USAGE_NORMAL, USAGE_TO_STDOUT);\n+ show_usage_stderr:\n+\treturn usage_with_options_internal(ctx, usagestr, options,\n+\t\t\t\t\t   USAGE_NORMAL, USAGE_TO_STDERR);\n }\n \n int parse_options_end(struct parse_opt_ctx_t *ctx)\n@@ -1201,6 +1205,7 @@ int parse_options(int argc, const char **argv,\n \tparse_options_start_1(&ctx, argc, argv, prefix, options, flags);\n \tswitch (parse_options_step(&ctx, options, usagestr)) {\n \tcase PARSE_OPT_HELP:\n+\t\texit(0);\n \tcase PARSE_OPT_HELP_ERROR:\n \tcase PARSE_OPT_ERROR:\n \t\texit(129);\n@@ -1481,7 +1486,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n \t\tfputs(\"EOF\\nexit 0\\n\", outfile);\n \n-\treturn PARSE_OPT_HELP;\n+\treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n }\n \n void NORETURN usage_with_options(const char * const *usagestr,\n@@ -1500,11 +1505,11 @@ void show_usage_with_options_if_asked(int ac, const char **av,\n \t\tif (!strcmp(av[1], \"-h\")) {\n \t\t\tusage_with_options_internal(NULL, usagestr, opts,\n \t\t\t\t\t\t    USAGE_NORMAL, USAGE_TO_STDOUT);\n-\t\t\texit(129);\n+\t\t\texit(0);\n \t\t} else if (!strcmp(av[1], \"--help-all\")) {\n \t\t\tusage_with_options_internal(NULL, usagestr, opts,\n \t\t\t\t\t\t    USAGE_FULL, USAGE_TO_STDOUT);\n-\t\t\texit(129);\n+\t\t\texit(0);\n \t\t}\n \t}\n }\ndiff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh\nindex bd2d45c971..b95e5b6ca0 100644\n--- a/t/for-each-ref-tests.sh\n+++ b/t/for-each-ref-tests.sh\n@@ -522,7 +522,7 @@ test_expect_success 'Verify descending sort' '\n '\n \n test_expect_success 'Give help even with invalid sort atoms' '\n-\ttest_expect_code 129 ${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&\n+\t${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&\n \tgrep \"^usage: ${git_for_each_ref}\" actual\n '\n \ndiff --git a/t/t0012-help.sh b/t/t0012-help.sh\nindex c33501bdcd..7815ff14f2 100755\n--- a/t/t0012-help.sh\n+++ b/t/t0012-help.sh\n@@ -260,7 +260,7 @@ do\n \t\t(\n \t\t\tGIT_CEILING_DIRECTORIES=$(pwd) &&\n \t\t\texport GIT_CEILING_DIRECTORIES &&\n-\t\t\ttest_expect_code 129 git -C sub $builtin -h >output 2>err\n+\t\t\tgit -C sub $builtin -h >output 2>err\n \t\t) &&\n \t\ttest_must_be_empty err &&\n \t\ttest_grep usage output\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ca55ea8228..30895ad6d2 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -68,7 +68,7 @@ Alias\n EOF\n \n test_expect_success 'test help' '\n-\ttest_must_fail test-tool parse-options -h >output 2>output.err &&\n+\ttest-tool parse-options -h >output 2>output.err &&\n \ttest_must_be_empty output.err &&\n \ttest_cmp expect output\n '\ndiff --git a/t/t0450-txt-doc-vs-help.sh b/t/t0450-txt-doc-vs-help.sh\nindex 822b0d55a5..d2844368e4 100755\n--- a/t/t0450-txt-doc-vs-help.sh\n+++ b/t/t0450-txt-doc-vs-help.sh\n@@ -29,7 +29,7 @@ help_to_synopsis () {\n \t\treturn 0\n \tfi &&\n \tmkdir -p \"$out_dir\" &&\n-\ttest_expect_code 129 git $builtin -h >\"$out.raw\" 2>&1 &&\n+\ttest_might_fail git $builtin -h >\"$out.raw\" 2>&1 &&\n \tsed -n \\\n \t\t-e '1,/^$/ {\n \t\t\t/^$/d;\ndiff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh\nindex e19e036898..4135db95ed 100755\n--- a/t/t0610-reftable-basics.sh\n+++ b/t/t0610-reftable-basics.sh\n@@ -15,9 +15,9 @@ export GIT_TEST_DEFAULT_REF_FORMAT\n INVALID_OID=$(test_oid 001)\n \n test_expect_success 'pack-refs does not crash with -h' '\n-\ttest_expect_code 129 git pack-refs -h >usage &&\n+\tgit pack-refs -h >usage &&\n \ttest_grep \"[Uu]sage: git pack-refs \" usage &&\n-\ttest_expect_code 129 nongit git pack-refs -h >usage &&\n+\tnongit git pack-refs -h >usage &&\n \ttest_grep \"[Uu]sage: git pack-refs \" usage\n '\n \ndiff --git a/t/t1403-show-ref.sh b/t/t1403-show-ref.sh\nindex 36c903ca19..db4300da44 100755\n--- a/t/t1403-show-ref.sh\n+++ b/t/t1403-show-ref.sh\n@@ -165,7 +165,7 @@ test_expect_success 'show-ref --branches, --tags, --head, pattern' '\n '\n \n test_expect_success 'show-ref --heads is deprecated and hidden' '\n-\ttest_expect_code 129 git show-ref -h >short-help &&\n+\tgit show-ref -h >short-help &&\n \ttest_grep ! -e --heads short-help &&\n \tgit show-ref --heads >actual 2>warning &&\n \ttest_grep ! deprecated warning &&\ndiff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh\nindex ce71f9a30a..6e921bc167 100755\n--- a/t/t1410-reflog.sh\n+++ b/t/t1410-reflog.sh\n@@ -107,12 +107,12 @@ test_expect_success setup '\n '\n \n test_expect_success 'correct usage on sub-command -h' '\n-\ttest_expect_code 129 git reflog expire -h >err &&\n+\tgit reflog expire -h >err &&\n \tgrep \"git reflog expire\" err\n '\n \n test_expect_success 'correct usage on \"git reflog show -h\"' '\n-\ttest_expect_code 129 git reflog show -h >err &&\n+\tgit reflog show -h >err &&\n \tgrep -F \"git reflog [show]\" err\n '\n \ndiff --git a/t/t1418-reflog-exists.sh b/t/t1418-reflog-exists.sh\nindex d51ecd5e92..10387792e3 100755\n--- a/t/t1418-reflog-exists.sh\n+++ b/t/t1418-reflog-exists.sh\n@@ -12,7 +12,7 @@ test_expect_success 'setup' '\n \n test_expect_success 'usage' '\n \ttest_expect_code 129 git reflog exists &&\n-\ttest_expect_code 129 git reflog exists -h\n+\tgit reflog exists -h\n '\n \n test_expect_success 'usage: unknown option' '\ndiff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh\nindex 455608c429..fa97591b9f 100755\n--- a/t/t1502-rev-parse-parseopt.sh\n+++ b/t/t1502-rev-parse-parseopt.sh\n@@ -75,7 +75,7 @@ EOF\n '\n \n test_expect_success 'test --parseopt help output' '\n-\ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec &&\n+\tgit rev-parse --parseopt -- -h > output < optionspec &&\n \ttest_cmp \"$TEST_DIRECTORY/t1502/optionspec.help\" output\n '\n \n@@ -89,7 +89,7 @@ test_expect_success 'test --parseopt help output no switches' '\n |EOF\n |exit 0\n END_EXPECT\n-\ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_no_switches &&\n+\tgit rev-parse --parseopt -- -h > output < optionspec_no_switches &&\n \ttest_cmp expect output\n '\n \n@@ -103,7 +103,7 @@ test_expect_success 'test --parseopt help output hidden switches' '\n |EOF\n |exit 0\n END_EXPECT\n-\ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_only_hidden_switches &&\n+\tgit rev-parse --parseopt -- -h > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n '\n \n@@ -119,7 +119,7 @@ test_expect_success 'test --parseopt help-all output hidden switches' '\n |EOF\n |exit 0\n END_EXPECT\n-\ttest_expect_code 129 git rev-parse --parseopt -- --help-all > output < optionspec_only_hidden_switches &&\n+\tgit rev-parse --parseopt -- --help-all > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n '\n \n@@ -258,7 +258,7 @@ test_expect_success 'test --parseopt help output: \"wrapped\" options normal \"or:\"\n \t|exit 0\n \tEND_EXPECT\n \n-\ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\n+\tgit rev-parse --parseopt -- -h <spec >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -296,12 +296,12 @@ test_expect_success 'test --parseopt help output: multi-line blurb after empty l\n \t|exit 0\n \tEND_EXPECT\n \n-\ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\n+\tgit rev-parse --parseopt -- -h <spec >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'test --parseopt help output for optionspec-neg' '\n-\ttest_expect_code 129 git rev-parse --parseopt -- \\\n+\tgit rev-parse --parseopt -- \\\n \t\t-h >output <\"$TEST_DIRECTORY/t1502/optionspec-neg\" &&\n \ttest_cmp \"$TEST_DIRECTORY/t1502/optionspec-neg.help\" output\n '\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex 99bda36d17..b45c52bd02 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -130,18 +130,25 @@ do\n \tarchimport | citool | credential-netrc | credential-libsecret | \\\n \tcredential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \\\n \tdaemon | \\\n-\tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n-\tget-tar-commit-id | \\\n+\tdifftool--helper | format-rev | fsck-objects |  get-tar-commit-id | \\\n \tgui | gui--askpass | \\\n-\thttp-backend | http-fetch | http-push | init-db | instaweb | \\\n-\tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\n-\tmktag | p4 | p4.py | pickaxe | quiltimport | remote-ftp | remote-ftps | \\\n-\tremote-http | remote-https | replay | request-pull | send-email | \\\n-\tsh-i18n--envsubst | shell | show | stage | submodule | svn | \\\n-\tupload-archive--writer | upload-pack | web--browse | whatchanged)\n-\t\texpect_outcome=expect_failure ;;\n+\thttp-backend | http-fetch | http-push | init-db | \\\n+\tmktag | p4 | p4.py | pickaxe | remote-ftp | remote-ftps | \\\n+\tremote-http | remote-https | replay | send-email | \\\n+\tsh-i18n--envsubst | shell | show | stage | \\\n+\tupload-archive--writer | upload-pack | whatchanged)\n+\t\th_expect_outcome=expect_failure\n+\t\tall_expect_outcome=expect_failure\n+\t\t;;\n+\tfilter-branch | merge-octopus | merge-one-file | merge-resolve | \\\n+\tmergetool | submodule | svn | web--browse)\n+\t\th_expect_outcome=expect_success\n+\t\tall_expect_outcome=expect_failure\n+\t\t;;\n \t*)\n-\t\texpect_outcome=expect_success ;;\n+\t\th_expect_outcome=expect_success\n+\t\tall_expect_outcome=expect_success\n+\t\t;;\n \tesac\n \tcase \"$cmd\" in\n \tinstaweb)\n@@ -151,20 +158,20 @@ do\n \t*)\n \t\tprereq= ;;\n \tesac\n-\ttest_$expect_outcome $prereq \"'git $cmd -h' outside a repository\" '\n-\t\ttest_expect_code 129 nongit git $cmd -h >usage &&\n+\ttest_$h_expect_outcome $prereq \"'git $cmd -h' outside a repository\" '\n+\t\tnongit git $cmd -h >usage &&\n \t\ttest_grep \"[Uu]sage: git $cmd \" usage\n \t'\n-\ttest_$expect_outcome $prereq \"'git $cmd --help-all' outside a repository\" '\n-\t\ttest_expect_code 129 nongit git $cmd --help-all >usage &&\n+\ttest_$all_expect_outcome $prereq \"'git $cmd --help-all' outside a repository\" '\n+\t\tnongit git $cmd --help-all >usage &&\n \t\ttest_grep \"[Uu]sage: git $cmd \" usage\n \t'\n done\n \n test_expect_success 'fmt-merge-msg does not crash with -h' '\n-\ttest_expect_code 129 git fmt-merge-msg -h >usage &&\n+\tgit fmt-merge-msg -h >usage &&\n \ttest_grep \"[Uu]sage: git fmt-merge-msg \" usage &&\n-\ttest_expect_code 129 nongit git fmt-merge-msg -h >usage &&\n+\tnongit git fmt-merge-msg -h >usage &&\n \ttest_grep \"[Uu]sage: git fmt-merge-msg \" usage\n '\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 0132e772e4..2ea9fa13c5 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -75,10 +75,10 @@ sentinel_detector () {\n test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook &&\n \ttest_expect_code 129 git hook run &&\n-\ttest_expect_code 129 git hook run -h &&\n+\tgit hook run -h &&\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n-\ttest_expect_code 129 git hook list -h &&\n+\tgit hook list -h &&\n \tgrep \"unknown option\" err\n '\n \ndiff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh\nindex 39bb77dda0..826686955d 100755\n--- a/t/t1900-repo-info.sh\n+++ b/t/t1900-repo-info.sh\n@@ -150,7 +150,7 @@ test_expect_success 'git repo info --keys uses lines as its default output forma\n '\n \n test_expect_success 'git repo info -h shows only repo info usage' '\n-\ttest_must_fail git repo info -h >actual &&\n+\tgit repo info -h >actual &&\n \ttest_grep \"git repo info\" actual &&\n \ttest_grep ! \"git repo structure\" actual\n '\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 10050abd70..02cc2b594a 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -225,7 +225,7 @@ test_expect_success 'progress meter option' '\n '\n \n test_expect_success 'git repo structure -h shows only repo structure usage' '\n-\ttest_must_fail git repo structure -h >actual &&\n+\tgit repo structure -h >actual &&\n \ttest_grep \"git repo structure\" actual &&\n \ttest_grep ! \"git repo info\" actual\n '\ndiff --git a/t/t2006-checkout-index-basic.sh b/t/t2006-checkout-index-basic.sh\nindex fedd2cc097..6538a24c95 100755\n--- a/t/t2006-checkout-index-basic.sh\n+++ b/t/t2006-checkout-index-basic.sh\n@@ -16,15 +16,15 @@ test_expect_success 'checkout-index -h in broken repository' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\t>.git/index &&\n-\t\ttest_expect_code 129 git checkout-index -h >usage 2>&1\n+\t\tgit checkout-index -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\n \n test_expect_success 'checkout-index does not crash with -h' '\n-\ttest_expect_code 129 git checkout-index -h >usage &&\n+\tgit checkout-index -h >usage &&\n \ttest_grep \"[Uu]sage: git checkout-index \" usage &&\n-\ttest_expect_code 129 nongit git checkout-index -h >usage &&\n+\tnongit git checkout-index -h >usage &&\n \ttest_grep \"[Uu]sage: git checkout-index \" usage\n '\n \ndiff --git a/t/t2107-update-index-basic.sh b/t/t2107-update-index-basic.sh\nindex 3bffe5da8a..004878322e 100755\n--- a/t/t2107-update-index-basic.sh\n+++ b/t/t2107-update-index-basic.sh\n@@ -23,7 +23,7 @@ test_expect_success 'update-index -h with corrupt index' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\t>.git/index &&\n-\t\ttest_expect_code 129 git update-index -h >usage 2>&1\n+\t\tgit update-index -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage: git update-index\" broken/usage\n '\ndiff --git a/t/t3004-ls-files-basic.sh b/t/t3004-ls-files-basic.sh\nindex 4034a5a59f..c57afcb841 100755\n--- a/t/t3004-ls-files-basic.sh\n+++ b/t/t3004-ls-files-basic.sh\n@@ -29,15 +29,15 @@ test_expect_success 'ls-files -h in corrupt repository' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\t>.git/index &&\n-\t\ttest_expect_code 129 git ls-files -h >usage 2>&1\n+\t\tgit ls-files -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage: git ls-files \" broken/usage\n '\n \n test_expect_success 'ls-files does not crash with -h' '\n-\ttest_expect_code 129 git ls-files -h >usage &&\n+\tgit ls-files -h >usage &&\n \ttest_grep \"[Uu]sage: git ls-files \" usage &&\n-\ttest_expect_code 129 nongit git ls-files -h >usage &&\n+\tnongit git ls-files -h >usage &&\n \ttest_grep \"[Uu]sage: git ls-files \" usage\n '\n \ndiff --git a/t/t3200-branch.sh b/t/t3200-branch.sh\nindex e7829c2c4b..dec0e77e3c 100755\n--- a/t/t3200-branch.sh\n+++ b/t/t3200-branch.sh\n@@ -33,7 +33,7 @@ test_expect_success REFFILES 'branch -h in broken repository' '\n \t\tcd broken &&\n \t\tgit init -b main &&\n \t\t>.git/refs/heads/main &&\n-\t\ttest_expect_code 129 git branch -h >usage 2>&1\n+\t\tgit branch -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex ecc35aae82..bc07e2a6ec 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -27,13 +27,13 @@ test_expect_success 'usage on cmd and subcommand invalid option' '\n '\n \n test_expect_success 'usage on main command -h emits a summary of subcommands' '\n-\ttest_expect_code 129 git stash -h >usage &&\n+\tgit stash -h >usage &&\n \tgrep -F \"usage: git stash list\" usage &&\n \tgrep -F \"or: git stash show\" usage\n '\n \n test_expect_success 'usage for subcommands should emit subcommand usage' '\n-\ttest_expect_code 129 git stash push -h >usage &&\n+\tgit stash push -h >usage &&\n \tgrep -F \"usage: git stash [push\" usage\n '\n \ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 1717f407c8..e1b474cc0f 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -438,7 +438,7 @@ test_expect_success 'rerere --no-no-rerere-autoupdate' '\n '\n \n test_expect_success 'rerere -h' '\n-\ttest_must_fail git rerere -h >help &&\n+\tgit rerere -h >help &&\n \ttest_grep [Uu]sage help\n '\n \ndiff --git a/t/t5200-update-server-info.sh b/t/t5200-update-server-info.sh\nindex a551e955b5..a0630cc1fc 100755\n--- a/t/t5200-update-server-info.sh\n+++ b/t/t5200-update-server-info.sh\n@@ -47,7 +47,7 @@ test_expect_success 'midx does not create duplicate pack entries' '\n '\n \n test_expect_success 'update-server-info does not crash with -h' '\n-\ttest_expect_code 129 git update-server-info -h >usage &&\n+\tgit update-server-info -h >usage &&\n \ttest_grep \"[Uu]sage: git update-server-info \" usage\n '\n \ndiff --git a/t/t5304-prune.sh b/t/t5304-prune.sh\nindex 2be7cd30de..e26e833d89 100755\n--- a/t/t5304-prune.sh\n+++ b/t/t5304-prune.sh\n@@ -365,7 +365,7 @@ test_expect_success 'gc.recentObjectsHook' '\n '\n \n test_expect_success 'prune does not crash with -h' '\n-\ttest_expect_code 129 git prune -h >usage &&\n+\tgit prune -h >usage &&\n \ttest_grep \"[Uu]sage: git prune \" usage\n '\n \ndiff --git a/t/t5400-send-pack.sh b/t/t5400-send-pack.sh\nindex b32a0a6aa7..6aa5838e2b 100755\n--- a/t/t5400-send-pack.sh\n+++ b/t/t5400-send-pack.sh\n@@ -56,9 +56,9 @@ test_expect_success setup '\n \tgit log'\n \n test_expect_success 'send-pack does not crash with -h' '\n-\ttest_expect_code 129 git send-pack -h >usage &&\n+\tgit send-pack -h >usage &&\n \ttest_grep \"[Uu]sage: git send-pack \" usage &&\n-\ttest_expect_code 129 nongit git send-pack -h >usage &&\n+\tnongit git send-pack -h >usage &&\n \ttest_grep \"[Uu]sage: git send-pack \" usage\n '\n \ndiff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh\nindex 5930f55186..8345bc0b14 100755\n--- a/t/t5512-ls-remote.sh\n+++ b/t/t5512-ls-remote.sh\n@@ -86,7 +86,7 @@ test_expect_success 'ls-remote -h is deprecated w/o warning' '\n '\n \n test_expect_success 'ls-remote --heads is deprecated and hidden w/o warning' '\n-\ttest_expect_code 129 git ls-remote -h >short-help &&\n+\tgit ls-remote -h >short-help &&\n \ttest_grep ! -e --head short-help &&\n \tgit ls-remote --heads self >actual 2>warning &&\n \ttest_cmp expected.branches actual &&\ndiff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh\nindex 1d9809114d..6d27b42ff1 100755\n--- a/t/t6300-for-each-ref.sh\n+++ b/t/t6300-for-each-ref.sh\n@@ -8,9 +8,9 @@ test_description='for-each-ref test'\n . ./test-lib.sh\n \n test_expect_success \"for-each-ref does not crash with -h\" '\n-\ttest_expect_code 129 git for-each-ref -h >usage &&\n+\tgit for-each-ref -h >usage &&\n \ttest_grep \"[Uu]sage: git for-each-ref \" usage &&\n-\ttest_expect_code 129 nongit git for-each-ref -h >usage &&\n+\tnongit git for-each-ref -h >usage &&\n \ttest_grep \"[Uu]sage: git for-each-ref \" usage\n '\n \ndiff --git a/t/t6500-gc.sh b/t/t6500-gc.sh\nindex ea9aaad470..b40d13d7ff 100755\n--- a/t/t6500-gc.sh\n+++ b/t/t6500-gc.sh\n@@ -35,7 +35,7 @@ test_expect_success 'gc -h with invalid configuration' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\techo \"[gc] pruneexpire = CORRUPT\" >>.git/config &&\n-\t\ttest_expect_code 129 git gc -h >usage 2>&1\n+\t\tgit gc -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\ndiff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh\nindex 2c147072c1..3bc5d1e9a2 100755\n--- a/t/t7030-verify-tag.sh\n+++ b/t/t7030-verify-tag.sh\n@@ -8,9 +8,9 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n test_expect_success GPG 'verify-tag does not crash with -h' '\n-\ttest_expect_code 129 git verify-tag -h >usage &&\n+\tgit verify-tag -h >usage &&\n \ttest_grep \"[Uu]sage: git verify-tag \" usage &&\n-\ttest_expect_code 129 nongit git verify-tag -h >usage &&\n+\tnongit git verify-tag -h >usage &&\n \ttest_grep \"[Uu]sage: git verify-tag \" usage\n '\n \ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex c2057bc94c..de7d7beec3 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -16,7 +16,7 @@ test_expect_success 'status -h in broken repository' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\techo \"[status] showuntrackedfiles = CORRUPT\" >>.git/config &&\n-\t\ttest_expect_code 129 git status -h >usage 2>&1\n+\t\tgit status -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\n@@ -28,7 +28,7 @@ test_expect_success 'commit -h in broken repository' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\techo \"[status] showuntrackedfiles = CORRUPT\" >>.git/config &&\n-\t\ttest_expect_code 129 git commit -h >usage 2>&1\n+\t\tgit commit -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex aa9108da54..e641f9e334 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -9,9 +9,9 @@ GNUPGHOME_NOT_USED=$GNUPGHOME\n . \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n test_expect_success GPG 'verify-commit does not crash with -h' '\n-\ttest_expect_code 129 git verify-commit -h >usage &&\n+\tgit verify-commit -h >usage &&\n \ttest_grep \"[Uu]sage: git verify-commit \" usage &&\n-\ttest_expect_code 129 nongit git verify-commit -h >usage &&\n+\tnongit git verify-commit -h >usage &&\n \ttest_grep \"[Uu]sage: git verify-commit \" usage\n '\n \ndiff --git a/t/t7600-merge.sh b/t/t7600-merge.sh\nindex f877d9a433..fd3d1d67f9 100755\n--- a/t/t7600-merge.sh\n+++ b/t/t7600-merge.sh\n@@ -173,7 +173,7 @@ test_expect_success 'merge -h with invalid index' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\t>.git/index &&\n-\t\ttest_expect_code 129 git merge -h >usage\n+\t\tgit merge -h >usage\n \t) &&\n \ttest_grep \"[Uu]sage: git merge\" broken/usage\n '\ndiff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh\nindex 8a91ff3603..961de3efab 100755\n--- a/t/t7800-difftool.sh\n+++ b/t/t7800-difftool.sh\n@@ -27,12 +27,11 @@ prompt_given ()\n }\n \n test_expect_success 'basic usage requires no repo' '\n-\ttest_expect_code 129 git difftool -h >output &&\n+\tgit difftool -h >output &&\n \ttest_grep ^usage: output &&\n \t# create a ceiling directory to prevent Git from finding a repo\n \tmkdir -p not/repo &&\n \ttest_when_finished rm -r not &&\n-\ttest_expect_code 129 \\\n \tenv GIT_CEILING_DIRECTORIES=\"$(pwd)/not\" \\\n \tgit -C not/repo difftool -h >output &&\n \ttest_grep ^usage: output\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d7f82e1bec..9886f641fc 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -35,7 +35,7 @@ test_systemd_analyze_verify () {\n }\n \n test_expect_success 'help text' '\n-\ttest_expect_code 129 git maintenance -h >actual &&\n+\tgit maintenance -h >actual &&\n \ttest_grep \"usage: git maintenance <subcommand>\" actual &&\n \ttest_expect_code 129 git maintenance barf 2>err &&\n \ttest_grep \"unknown subcommand: \\`barf'\\''\" err &&\ndiff --git a/usage.c b/usage.c\nindex 527edb1e79..3f0118ab2a 100644\n--- a/usage.c\n+++ b/usage.c\n@@ -188,7 +188,7 @@ static void show_usage_if_asked_helper(const char *err, ...)\n \tva_start(params, err);\n \tvfreportf(stdout, _(\"usage: \"), err, params);\n \tva_end(params);\n-\texit(129);\n+\texit(0);\n }\n \n void show_usage_if_asked(int ac, const char **av, const char *err)\n"},{"id":"546943","messageId":"20260701212442.1430084-3-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260701212442.1430084-1-sandals@crustytoothpaste.net","subject":"[PATCH v2 2/4] parse-options: add a separate case for help output on error","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-01T21:24:40Z","receivedAt":"2026-07-01T21:24:53Z","isPatch":true,"body":"When we parse a command line option such as -h or --help, we currently\nexit 129, since that is the exit code when help output is printed. In a\nfuture commit, we'll change this to exit 0 instead, since we're doing\nwhat the user wanted successfully.\n\nHowever, there are some cases where we print help output because the\nuser has provided ambiguous or invalid input, such as an ambiguous\noption, and we'll want to exit unsuccessfully there.  Make this easier\nby defining a new return code, PARSE_OPT_HELP_ERROR, that can be used in\nthis case, while reserving PARSE_OPT_HELP for those cases where the user\nhas requested help directly.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n builtin/blame.c        | 1 +\n builtin/shortlog.c     | 1 +\n builtin/update-index.c | 1 +\n parse-options.c        | 7 ++++++-\n parse-options.h        | 3 ++-\n 5 files changed, 11 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex ffbd3ce5c5..65d43c7d48 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -1013,6 +1013,7 @@ int cmd_blame(int argc,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n+\t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\texit(129);\ndiff --git a/builtin/shortlog.c b/builtin/shortlog.c\nindex 6b2a0b93b5..cd262bd376 100644\n--- a/builtin/shortlog.c\n+++ b/builtin/shortlog.c\n@@ -433,6 +433,7 @@ int cmd_shortlog(int argc,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n+\t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\texit(129);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 3d6646c318..ac4610ec94 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1133,6 +1133,7 @@ int cmd_update_index(int argc,\n \t\t\tbreak;\n \t\tswitch (parseopt_state) {\n \t\tcase PARSE_OPT_HELP:\n+\t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\t\texit(129);\n \t\tcase PARSE_OPT_COMPLETE:\ndiff --git a/parse-options.c b/parse-options.c\nindex f4647e0099..a623961800 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -583,7 +583,7 @@ static enum parse_opt_result parse_long_opt(\n \t\t\tambiguous.option->long_name,\n \t\t\t(abbrev.flags & OPT_UNSET) ?  \"no-\" : \"\",\n \t\t\tabbrev.option->long_name);\n-\t\treturn PARSE_OPT_HELP;\n+\t\treturn PARSE_OPT_HELP_ERROR;\n \t}\n \tif (abbrev.option) {\n \t\tif (*arg_end)\n@@ -1037,6 +1037,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\t\tusage_with_options(usagestr, options);\n \t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\tcase PARSE_OPT_HELP:\n+\t\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\tcase PARSE_OPT_ERROR:\n \t\t\tcase PARSE_OPT_DONE:\n \t\t\tcase PARSE_OPT_NON_OPTION:\n@@ -1072,6 +1073,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\tcase PARSE_OPT_NON_OPTION:\n \t\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\tcase PARSE_OPT_HELP:\n+\t\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\t\tBUG(\"parse_short_opt() cannot return these\");\n \t\t\tcase PARSE_OPT_DONE:\n@@ -1099,6 +1101,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\t\tcase PARSE_OPT_HELP:\n+\t\t\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\t\t\tBUG(\"parse_short_opt() cannot return these\");\n \t\t\t\tcase PARSE_OPT_DONE:\n \t\t\t\t\tbreak;\n@@ -1132,6 +1135,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tgoto unknown;\n \t\tcase PARSE_OPT_HELP:\n+\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\tgoto show_usage;\n \t\tcase PARSE_OPT_NON_OPTION:\n \t\tcase PARSE_OPT_SUBCOMMAND:\n@@ -1197,6 +1201,7 @@ int parse_options(int argc, const char **argv,\n \tparse_options_start_1(&ctx, argc, argv, prefix, options, flags);\n \tswitch (parse_options_step(&ctx, options, usagestr)) {\n \tcase PARSE_OPT_HELP:\n+\tcase PARSE_OPT_HELP_ERROR:\n \tcase PARSE_OPT_ERROR:\n \t\texit(129);\n \tcase PARSE_OPT_COMPLETE:\ndiff --git a/parse-options.h b/parse-options.h\nindex 0d1f738f8d..3ec8ba5cc8 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -57,7 +57,8 @@ enum parse_opt_option_flags {\n };\n \n enum parse_opt_result {\n-\tPARSE_OPT_COMPLETE = -3,\n+\tPARSE_OPT_COMPLETE = -4,\n+\tPARSE_OPT_HELP_ERROR = -3,\n \tPARSE_OPT_HELP = -2,\n \tPARSE_OPT_ERROR = -1,\t/* must be the same as error() */\n \tPARSE_OPT_DONE = 0,\t/* fixed so that \"return 0\" works */\n"},{"id":"546946","messageId":"xmqqcxx6mkjn.fsf@gitster.g","threadId":"65249","inReplyTo":"20260701212442.1430084-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH v2 0/4] rev-parse: exit 0 on --help","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-01T22:06:52Z","receivedAt":"2026-07-01T22:06:55Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> The standard philosophy for Unix software when a help option (such as\n> --help) is specified is that the software should exit 0, printing the\n> help output to standard output, since the standard output is for\n> user-requested output and the program performed the requested task\n> successfully.  If the user specifies an incorrect option, then the help\n> output should be printed to standard error (since the user has made a\n> mistake) and it should exit unsuccessfully.\n\nHmph.\n\n> git rev-parse --parseopt properly directs the output in both of these\n> cases, but it currently exits 129 when it receives a --help or -h option\n> on the command line, which causes its invoking script to do the same.\n> This is not in line with the usual behavior and it causes scripts using\n> this command to exit unsuccessfully on --help as well.\n>\n> This series introduces some changes to distinguish the --help and -h\n> options from other cases in which we print help output and adjusts the\n> exit code to 0 from those two options.  We continue to exit 129 when the\n> options are invalid, which is useful information to have for callers.\n> We also make the relevant changes such that `git rev-parse --parseopt`\n> does the same thing as long as it is invoked in the way specified in the\n> manual page (which a quick GitHub search shows almost everyone does).\n>\n> One of the patches is rather long because we have many cases in which\n> we've hard-coded exit code 129 into our tests.  However, the changes\n> there should not be complex, only somewhat tedious to review.\n\nIt is borderline for \"yes, we all know it is obvious that things\nshould have worked this way from day one, we regret that it is not\nthe case, but it has been working differently and users' scripts all\nhave been working with the current behaviour, and it is likely that\nthey will all break\".\n\nTwo big things that make it much less likely, saving grace, are that\nthis is only about \"--help\" (which is unlikely to be a part of\nend-user script), and this makes the invocation succeed (if we were\nchanging from exit 0 to exit 129, we would be breaking tons more).\n\n;-)\n"},{"id":"546947","messageId":"xmqq8q7umkcy.fsf@gitster.g","threadId":"65249","inReplyTo":"20260701212442.1430084-2-sandals@crustytoothpaste.net","subject":"Re: [PATCH v2 1/4] t1517: skip svn tests if svn is not installed","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-01T22:10:53Z","receivedAt":"2026-07-01T22:10:56Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> The svn tests currently assume that git-svn's option parsing will always\n> fail the tests because it exits 0 on --help, not 129.  However, in a\n> future commit, we'll expect it to exit 0 and the tests will then need to\n> be updated to succeed in some cases and fail in others.\n>\n> We therefore need to have t1517 determine whether the Subversion Perl\n> modules are present, since if they are not, git-svn will die on start\n> and then it needs to continue to expect failure.  Add a stripped down\n> version of the tests in t/lib-git-svn.sh as a prerequisite we can use\n> here for our svn tests.\n>\n> Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n> ---\n>  t/t1517-outside-repo.sh | 10 ++++++++++\n>  1 file changed, 10 insertions(+)\n>\n> diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\n> index c557f2f55c..583784f21b 100755\n> --- a/t/t1517-outside-repo.sh\n> +++ b/t/t1517-outside-repo.sh\n> @@ -4,6 +4,14 @@ test_description='check random commands outside repo'\n>  \n>  . ./test-lib.sh\n>  \n> +test_lazy_prereq SVN '\n> +\ttest_have_prereq PERL && test -n \"$NO_SVN_TESTS\" && perl -w -e \"\n> +\t\tuse SVN::Core;\n> +\t\tuse SVN::Repos;\n> +\t\t\\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n> +\t\"\n> +'\n\nThis corresponds to 42 in t/lib-git-svn.sh?  We can use any non-zero\nvalue here, but just being curious.\n\n>  test_expect_success 'set up a non-repo directory and test file' '\n>  \tGIT_CEILING_DIRECTORIES=$(pwd) &&\n>  \texport GIT_CEILING_DIRECTORIES &&\n> @@ -138,6 +146,8 @@ do\n>  \tcase \"$cmd\" in\n>  \tinstaweb)\n>  \t\tprereq=PERL ;;\n> +\tsvn)\n> +\t\tprereq=SVN ;;\n>  \t*)\n>  \t\tprereq= ;;\n>  \tesac\n"},{"id":"546948","messageId":"xmqq4iiimk3v.fsf@gitster.g","threadId":"65249","inReplyTo":"20260701212442.1430084-4-sandals@crustytoothpaste.net","subject":"Re: [PATCH v2 3/4] rev-parse: have --parseopt callers exit 0 on --help","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-01T22:16:20Z","receivedAt":"2026-07-01T22:16:23Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> The usual invocation recommended by the manual page is this:\n>\n>     eval \"$(echo \"$OPTS_SPEC\" | git rev-parse --parseopt -- \"$@\" || echo exit $?)\"\n>\n> Thus, the fact that git rev-parse --parseopt still exits 129 in this\n> case is irrelevant, since the \"echo exit $?\" will print \"exit 129\", but\n> that will be after the \"exit 0\" printed by Git—and thus ignored, since\n> the shell will have already exited successfully.\n\nYuck, but ...\n\n>  \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n> -\t\tfputs(\"EOF\\n\", outfile);\n> +\t\tfputs(\"EOF\\nexit 0\\n\", outfile);\n\n... it does its job ;-).\n"},{"id":"546949","messageId":"xmqqzf0al51j.fsf@gitster.g","threadId":"65249","inReplyTo":"20260701212442.1430084-2-sandals@crustytoothpaste.net","subject":"Re: [PATCH v2 1/4] t1517: skip svn tests if svn is not installed","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-01T22:27:04Z","receivedAt":"2026-07-01T22:27:07Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> +test_lazy_prereq SVN '\n> +\ttest_have_prereq PERL && test -n \"$NO_SVN_TESTS\" && perl -w -e \"\n> +\t\tuse SVN::Core;\n> +\t\tuse SVN::Repos;\n> +\t\t\\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n> +\t\"\n> +'\n\nIf \"have_prereq PERL\" is not satisfied, SVN is not satisfied.\n\nIf NO_SVN_TESTS is an empty string (or unset), \"test -n\" fails, and\nSVN is not satisfied.  Questionable---am I misreading this part of\nthe logic???\n\nThe perl script would not barf only if use SVN::* succeed and then\nSVN::Core::VERSION is strictly better than '1.1.0'.  If not, i.e.,\nlibsvn-perl is not available, or its version is older, then we fail\nwith exit(42), and SVN is not satisfied.\n\n\n\n>  test_expect_success 'set up a non-repo directory and test file' '\n>  \tGIT_CEILING_DIRECTORIES=$(pwd) &&\n>  \texport GIT_CEILING_DIRECTORIES &&\n> @@ -138,6 +146,8 @@ do\n>  \tcase \"$cmd\" in\n>  \tinstaweb)\n>  \t\tprereq=PERL ;;\n> +\tsvn)\n> +\t\tprereq=SVN ;;\n>  \t*)\n>  \t\tprereq= ;;\n>  \tesac\n"},{"id":"546957","messageId":"20260702053724.GA3437623@coredump.intra.peff.net","threadId":"65249","inReplyTo":"20260701212442.1430084-2-sandals@crustytoothpaste.net","subject":"Re: [PATCH v2 1/4] t1517: skip svn tests if svn is not installed","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-07-02T05:37:24Z","receivedAt":"2026-07-02T05:37:31Z","isPatch":true,"body":"On Wed, Jul 01, 2026 at 09:24:39PM +0000, brian m. carlson wrote:\n\n> +test_lazy_prereq SVN '\n> +\ttest_have_prereq PERL && test -n \"$NO_SVN_TESTS\" && perl -w -e \"\n> +\t\tuse SVN::Core;\n> +\t\tuse SVN::Repos;\n> +\t\t\\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n> +\t\"\n> +'\n\nThe single-quotes in your inline perl will be interpreted as ending (and\nrestarting) the lazy-prereq snippet. So you actually get a bare:\n\n  $SVN::Core::VERSION gt 1.1.0 or exit(42);\n\nfed to perl (no quotes around 1.1.0). We sometimes catch these cases\nautomatically it results in an extra argument to test_expect_success,\netc. But here you are unlucky enough that it does not (and anyway, we do\nnot seem to have the same safety check for test_lazy_prereq; we'd just\nignore the extra arguments).\n\nAnd of course being perl, it doesn't complain. I'm not sure how it is\ninterpreted, but I doubt the use of \"gt\" is right. My version of\nSVN::Core is 1.14.5, which is (correctly) more than \"1.1.0\", but is\n(incorrect) not more than \"1.2.0\".\n\nI think the \"gt\" bug is inherited from lib-git-svn.sh (unless I'm just\nholding it wrong), but the single-quote one is new (it happens at the\ntop-level in the original).\n\n-Peff\n"},{"id":"546969","messageId":"20260702083806.GA481298@coredump.intra.peff.net","threadId":"65249","inReplyTo":"20260701212442.1430084-3-sandals@crustytoothpaste.net","subject":"Re: [PATCH v2 2/4] parse-options: add a separate case for help output on error","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-07-02T08:38:06Z","receivedAt":"2026-07-02T08:38:08Z","isPatch":true,"body":"On Wed, Jul 01, 2026 at 09:24:40PM +0000, brian m. carlson wrote:\n\n> However, there are some cases where we print help output because the\n> user has provided ambiguous or invalid input, such as an ambiguous\n> option, and we'll want to exit unsuccessfully there.  Make this easier\n> by defining a new return code, PARSE_OPT_HELP_ERROR, that can be used in\n> this case, while reserving PARSE_OPT_HELP for those cases where the user\n> has requested help directly.\n\nMakes sense. We'd want to audit every spot that generates PARSE_OPT_HELP\nand see if it should be PARSE_OPT_HELP_ERROR. I only see one spot\ntouched here:\n\n> --- a/parse-options.c\n> +++ b/parse-options.c\n> @@ -583,7 +583,7 @@ static enum parse_opt_result parse_long_opt(\n>  \t\t\tambiguous.option->long_name,\n>  \t\t\t(abbrev.flags & OPT_UNSET) ?  \"no-\" : \"\",\n>  \t\t\tabbrev.option->long_name);\n> -\t\treturn PARSE_OPT_HELP;\n> +\t\treturn PARSE_OPT_HELP_ERROR;\n>  \t}\n\nThat one makes sense. The other site that generates it is within\nusage_with_options_internal(), which handles both asked-for \"-h\" and\nunexpected errors, but still always returns PARSE_OPT_HELP.\n\nAh...it looks like you _do_ switch it in patch 4 (when the distinction\nbetween the two starts to make a difference). I think it should be done\nin this patch, though, since the point is generating the correct\nHELP/HELP_ERROR here (even though it does not yet matter).\n\nI wonder if we'd also want:\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 742444eead..08c21d9fc0 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1373,7 +1373,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \tparse_options_check_harder(opts);\n \n \tif (!usagestr)\n-\t\treturn PARSE_OPT_HELP;\n+\t\treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n \n \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n \t\tfprintf(outfile, \"cat <<\\\\EOF\\n\");\n\nI can't figure out when we wouldn't have a usagestr, though. Perhaps not\never from parse-options itself, but only when called via\nusage_with_options() or something? That function does not look at our\nreturn value so it would not matter, but it feels like we should keep\nthings consistent.\n\n-Peff\n"},{"id":"546970","messageId":"20260702084519.GB481298@coredump.intra.peff.net","threadId":"65249","inReplyTo":"xmqqcxx6mkjn.fsf@gitster.g","subject":"Re: [PATCH v2 0/4] rev-parse: exit 0 on --help","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-07-02T08:45:19Z","receivedAt":"2026-07-02T08:45:20Z","isPatch":true,"body":"On Wed, Jul 01, 2026 at 03:06:52PM -0700, Junio C Hamano wrote:\n\n> > One of the patches is rather long because we have many cases in which\n> > we've hard-coded exit code 129 into our tests.  However, the changes\n> > there should not be complex, only somewhat tedious to review.\n> \n> It is borderline for \"yes, we all know it is obvious that things\n> should have worked this way from day one, we regret that it is not\n> the case, but it has been working differently and users' scripts all\n> have been working with the current behaviour, and it is likely that\n> they will all break\".\n> \n> Two big things that make it much less likely, saving grace, are that\n> this is only about \"--help\" (which is unlikely to be a part of\n> end-user script), and this makes the invocation succeed (if we were\n> changing from exit 0 to exit 129, we would be breaking tons more).\n\nMy big concern is a script accidentally continuing when fed \"--help\" and\ngenerating nonsense. But I think the eval magic explained in patch 3\nmakes that unlikely (any such caller was already kind-of broken).\n\nThe other issue I raised in the earlier round is that a script like:\n\n  cat >git-foo <<\\EOF\n  #!/bin/sh\n  git log --my-options \"$@\" >output || exit 1\n  do_something <output\n  EOF\n\nwhen invoked as \"git foo --help\" will now call do_something with\nnonsense input, rather than exiting from the \"error\" returned by\ngit-log. This only affects hacky little scripts like this that are not\notherwise parsing their own options, but sometimes those are the most\ncommon. ;)\n\nI'm not convinced there will be much fallout, but it is possible.\n\n-Peff\n"},{"id":"547000","messageId":"akZ6H84Tzzgu8L5W@fruit.crustytoothpaste.net","threadId":"65249","inReplyTo":"xmqqzf0al51j.fsf@gitster.g","subject":"Re: [PATCH v2 1/4] t1517: skip svn tests if svn is not installed2sy","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-02T14:47:59Z","receivedAt":"2026-07-02T14:48:02Z","isPatch":true,"body":"On 2026-07-01 at 22:27:04, Junio C Hamano wrote:\n> \"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n> \n> > +test_lazy_prereq SVN '\n> > +\ttest_have_prereq PERL && test -n \"$NO_SVN_TESTS\" && perl -w -e \"\n> > +\t\tuse SVN::Core;\n> > +\t\tuse SVN::Repos;\n> > +\t\t\\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n> > +\t\"\n> > +'\n> \n> If \"have_prereq PERL\" is not satisfied, SVN is not satisfied.\n\nCorrect.\n\n> If NO_SVN_TESTS is an empty string (or unset), \"test -n\" fails, and\n> SVN is not satisfied.  Questionable---am I misreading this part of\n> the logic???\n\nI think that's reversed, yes.\n\n> The perl script would not barf only if use SVN::* succeed and then\n> SVN::Core::VERSION is strictly better than '1.1.0'.  If not, i.e.,\n> libsvn-perl is not available, or its version is older, then we fail\n> with exit(42), and SVN is not satisfied.\n\nCorrect.  And yes, this came in from `t/lib-git-svn.sh`.  I'll probably\njust simplify this to omit the version check since it's very unlikely\nthat anybody is using SVN 1.0 any more and, as Peff pointed out, this\ndoesn't actually work using a string comparison.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"547117","messageId":"xmqq33xzize2.fsf@gitster.g","threadId":"65249","inReplyTo":"20260702053724.GA3437623@coredump.intra.peff.net","subject":"Re: [PATCH v2 1/4] t1517: skip svn tests if svn is not installed","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-03T20:36:37Z","receivedAt":"2026-07-03T20:36:39Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> fed to perl (no quotes around 1.1.0). We sometimes catch these cases\n> automatically it results in an extra argument to test_expect_success,\n> etc. But here you are unlucky enough that it does not (and anyway, we do\n> not seem to have the same safety check for test_lazy_prereq; we'd just\n> ignore the extra arguments).\n>\n> And of course being perl, it doesn't complain. I'm not sure how it is\n> interpreted,\n\nI happen to know ;-).\n\nWhen you have more than two sequences of digits separated by dot,\nlike IP address 192.168.1.1, you are telling Perl to interpret the\nsequence as a string, each byte of it is the number denoted by these\ndigits.  I believe this was invented primarily for IP addresses, but\nit does not have to be just four digits.  To wit:\n\n    $ perl -e 'print 65.66.67;'\n    ABC\n    $ perl -e 'print 65.66.67.68.69;'\n    ABCDE\n\nOf course, 65.66 is not AB, but a floating-point number that is\nbetween integers 65 and 66:\n\n    $ perl -e 'print 65.66;'\n    65.66\n\n> but I doubt the use of \"gt\" is right.\n\nTrue.\n"},{"id":"547118","messageId":"xmqqwlvbhkpz.fsf@gitster.g","threadId":"65249","inReplyTo":"20260702084519.GB481298@coredump.intra.peff.net","subject":"Re: [PATCH v2 0/4] rev-parse: exit 0 on --help","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-03T20:38:48Z","receivedAt":"2026-07-03T20:38:51Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> The other issue I raised in the earlier round is that a script like:\n>\n>   cat >git-foo <<\\EOF\n>   #!/bin/sh\n>   git log --my-options \"$@\" >output || exit 1\n>   do_something <output\n>   EOF\n>\n> when invoked as \"git foo --help\" will now call do_something with\n> nonsense input, rather than exiting from the \"error\" returned by\n> git-log. This only affects hacky little scripts like this that are not\n> otherwise parsing their own options, but sometimes those are the most\n> common. ;)\n\nYeah, I agree that the above is a much more likely breakage scenario\nthan I imagined.\n\n> I'm not convinced there will be much fallout, but it is possible.\n\nTrue.\n"},{"id":"547125","messageId":"20260704044743.GA2255427@coredump.intra.peff.net","threadId":"65249","inReplyTo":"xmqq33xzize2.fsf@gitster.g","subject":"Re: [PATCH v2 1/4] t1517: skip svn tests if svn is not installed","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-07-04T04:47:43Z","receivedAt":"2026-07-04T04:47:45Z","isPatch":true,"body":"On Fri, Jul 03, 2026 at 01:36:37PM -0700, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n> \n> > fed to perl (no quotes around 1.1.0). We sometimes catch these cases\n> > automatically it results in an extra argument to test_expect_success,\n> > etc. But here you are unlucky enough that it does not (and anyway, we do\n> > not seem to have the same safety check for test_lazy_prereq; we'd just\n> > ignore the extra arguments).\n> >\n> > And of course being perl, it doesn't complain. I'm not sure how it is\n> > interpreted,\n> \n> I happen to know ;-).\n> \n> When you have more than two sequences of digits separated by dot,\n> like IP address 192.168.1.1, you are telling Perl to interpret the\n> sequence as a string, each byte of it is the number denoted by these\n> digits.  I believe this was invented primarily for IP addresses, but\n> it does not have to be just four digits.  To wit:\n> \n>     $ perl -e 'print 65.66.67;'\n>     ABC\n>     $ perl -e 'print 65.66.67.68.69;'\n>     ABCDE\n> \n> Of course, 65.66 is not AB, but a floating-point number that is\n> between integers 65 and 66:\n> \n>     $ perl -e 'print 65.66;'\n>     65.66\n\nAh, thanks. It is both exciting and horrifying that in 2026 I can still\nlearn new perl esoterica. :)\n\n-Peff\n"},{"id":"547412","messageId":"20260708001557.3581080-2-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260708001557.3581080-1-sandals@crustytoothpaste.net","subject":"[PATCH v3 1/4] t1517: skip svn tests if svn is not installed","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-08T00:15:54Z","receivedAt":"2026-07-08T00:16:07Z","isPatch":true,"body":"The svn tests currently assume that git-svn's option parsing will always\nfail the tests because it exits 0 on --help, not 129.  However, in a\nfuture commit, we'll expect it to exit 0 and the tests will then need to\nbe updated to succeed in some cases and fail in others.\n\nWe therefore need to have t1517 determine whether the Subversion Perl\nmodules are present, since if they are not, git-svn will die on start\nand then it needs to continue to expect failure.  Add a stripped down\nversion of the tests in t/lib-git-svn.sh as a prerequisite we can use\nhere for our svn tests.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n t/t1517-outside-repo.sh | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex c557f2f55c..6421bdb3c3 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -4,6 +4,13 @@ test_description='check random commands outside repo'\n \n . ./test-lib.sh\n \n+test_lazy_prereq SVN '\n+\ttest_have_prereq PERL && test -z \"$NO_SVN_TESTS\" && perl -w -e \"\n+\t\tuse SVN::Core;\n+\t\tuse SVN::Repos;\n+\t\"\n+'\n+\n test_expect_success 'set up a non-repo directory and test file' '\n \tGIT_CEILING_DIRECTORIES=$(pwd) &&\n \texport GIT_CEILING_DIRECTORIES &&\n@@ -138,6 +145,8 @@ do\n \tcase \"$cmd\" in\n \tinstaweb)\n \t\tprereq=PERL ;;\n+\tsvn)\n+\t\tprereq=SVN ;;\n \t*)\n \t\tprereq= ;;\n \tesac\n"},{"id":"547413","messageId":"20260708001557.3581080-4-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260708001557.3581080-1-sandals@crustytoothpaste.net","subject":"[PATCH v3 3/4] rev-parse: have --parseopt callers exit 0 on --help","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-08T00:15:56Z","receivedAt":"2026-07-08T00:16:07Z","isPatch":true,"body":"The standard philosophy for Unix software when a help option (such as\n--help) is specified is that the software should exit 0, printing the\nhelp output to standard output, since the standard output is for\nuser-requested output and the program performed the requested task\nsuccessfully.  If the user specifies an incorrect option, then the help\noutput should be printed to standard error (since the user has made a\nmistake) and it should exit unsuccessfully.\n\ngit rev-parse --parseopt properly directs the output in both of these\ncases, but it currently exits 129 when it receives a --help or -h option\non the command line, which causes its invoking script to do the same.\nThis is not in line with the usual behavior and it causes scripts using\nthis command to exit unsuccessfully on --help as well.\n\nNote that Git subcommands implemented using scripts, such as git\nsubmodule, don't have this problem because Git itself intercepts the\n--help option and runs man (or a similar tool), which then exits 0.\nHowever, this still affects the myriad scripts that use this\nfunctionality because Git is widespread and the --parseopt functionality\nis a good way to get sensible option parsing across shells in a portable\nway.\n\nBecause git rev-parse --parseopt is intended to be eval'd by the shell,\nwhen help output is to be printed to standard output, Git actually\nprints a cat command with a heredoc since the standard output is being\nevaluated by the shell.  Thus, to do the right thing, simply add an\n\"exit 0\" right after the end of the heredoc, which will cause the\ninvoking program to exit successfully.\n\nThe usual invocation recommended by the manual page is this:\n\n    eval \"$(echo \"$OPTS_SPEC\" | git rev-parse --parseopt -- \"$@\" || echo exit $?)\"\n\nThus, the fact that git rev-parse --parseopt still exits 129 in this\ncase is irrelevant, since the \"echo exit $?\" will print \"exit 129\", but\nthat will be after the \"exit 0\" printed by Git—and thus ignored, since\nthe shell will have already exited successfully.\n\nUpdate the tests for this case.  Note that we no longer need to delete\nonly the first and last lines in some tests, so add a command to delete\nthe end of the heredoc as well.  We could do something clever with sed\nto delete all but the last two lines or switch to head and tail, but\nthose would be more complicated and less readable, so just stick with\nthe simple approach.\n\nIn t1517, add three shell scripts to the failure case because they no\nlonger return 129 as expected.  In a future commit, we'll change the\nexpected result to exit 0 and these will become successful again.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n parse-options.c               | 2 +-\n t/t1502-rev-parse-parseopt.sh | 9 +++++++--\n t/t1502/optionspec-neg.help   | 1 +\n t/t1502/optionspec.help       | 1 +\n t/t1517-outside-repo.sh       | 6 +++---\n 5 files changed, 13 insertions(+), 6 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex fd8ceed82b..cc3a8b0fe3 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1479,7 +1479,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \tfputc('\\n', outfile);\n \n \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n-\t\tfputs(\"EOF\\n\", outfile);\n+\t\tfputs(\"EOF\\nexit 0\\n\", outfile);\n \n \treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n }\ndiff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh\nindex 3962f1d288..455608c429 100755\n--- a/t/t1502-rev-parse-parseopt.sh\n+++ b/t/t1502-rev-parse-parseopt.sh\n@@ -12,7 +12,7 @@ check_invalid_long_option () {\n \t\t\tcat <<-\\EOF &&\n \t\t\terror: unknown option `'${opt#--}\\''\n \t\t\tEOF\n-\t\t\tsed -e 1d -e \\$d <\"$TEST_DIRECTORY/t1502/$spec.help\"\n+\t\t\tsed -e 1d -e /EOF/d -e \\$d <\"$TEST_DIRECTORY/t1502/$spec.help\"\n \t\t} >expect &&\n \t\ttest_expect_code 129 git rev-parse --parseopt -- $opt \\\n \t\t\t2>output <\"$TEST_DIRECTORY/t1502/$spec\" &&\n@@ -87,6 +87,7 @@ test_expect_success 'test --parseopt help output no switches' '\n |    some-command does foo and bar!\n |\n |EOF\n+|exit 0\n END_EXPECT\n \ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_no_switches &&\n \ttest_cmp expect output\n@@ -100,6 +101,7 @@ test_expect_success 'test --parseopt help output hidden switches' '\n |    some-command does foo and bar!\n |\n |EOF\n+|exit 0\n END_EXPECT\n \ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n@@ -115,6 +117,7 @@ test_expect_success 'test --parseopt help-all output hidden switches' '\n |    --[no-]hidden1        A hidden switch\n |\n |EOF\n+|exit 0\n END_EXPECT\n \ttest_expect_code 129 git rev-parse --parseopt -- --help-all > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n@@ -125,7 +128,7 @@ test_expect_success 'test --parseopt invalid switch help output' '\n \t\tcat <<-\\EOF &&\n \t\terror: unknown option `does-not-exist'\\''\n \t\tEOF\n-\t\tsed -e 1d -e \\$d <\"$TEST_DIRECTORY/t1502/optionspec.help\"\n+\t\tsed -e 1d -e /EOF/d -e \\$d <\"$TEST_DIRECTORY/t1502/optionspec.help\"\n \t} >expect &&\n \ttest_expect_code 129 git rev-parse --parseopt -- --does-not-exist 1>/dev/null 2>output < optionspec &&\n \ttest_cmp expect output\n@@ -252,6 +255,7 @@ test_expect_success 'test --parseopt help output: \"wrapped\" options normal \"or:\"\n \t|    -h, --help            show the help\n \t|\n \t|EOF\n+\t|exit 0\n \tEND_EXPECT\n \n \ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\n@@ -289,6 +293,7 @@ test_expect_success 'test --parseopt help output: multi-line blurb after empty l\n \t|    -h, --help            show the help\n \t|\n \t|EOF\n+\t|exit 0\n \tEND_EXPECT\n \n \ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\ndiff --git a/t/t1502/optionspec-neg.help b/t/t1502/optionspec-neg.help\nindex 7a29f8cb03..f85be7b8fd 100644\n--- a/t/t1502/optionspec-neg.help\n+++ b/t/t1502/optionspec-neg.help\n@@ -10,3 +10,4 @@ usage: some-command [options] <args>...\n     --no-negative         cannot be positivated\n \n EOF\n+exit 0\ndiff --git a/t/t1502/optionspec.help b/t/t1502/optionspec.help\nindex cbdd54d41b..ded35ebc82 100755\n--- a/t/t1502/optionspec.help\n+++ b/t/t1502/optionspec.help\n@@ -34,3 +34,4 @@ Extras\n     --[no-]extra1         line above used to cause a segfault but no longer does\n \n EOF\n+exit 0\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex 6421bdb3c3..03fa2f9cdf 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -132,10 +132,10 @@ do\n \tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n \tget-tar-commit-id | \\\n \tgui | gui--askpass | \\\n-\thttp-backend | http-fetch | http-push | init-db | \\\n+\thttp-backend | http-fetch | http-push | init-db | instaweb | \\\n \tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\n-\tmktag | p4 | p4.py | pickaxe | remote-ftp | remote-ftps | \\\n-\tremote-http | remote-https | replay | send-email | \\\n+\tmktag | p4 | p4.py | pickaxe | quiltimport | remote-ftp | remote-ftps | \\\n+\tremote-http | remote-https | replay | request-pull | send-email | \\\n \tsh-i18n--envsubst | shell | show | stage | submodule | svn | \\\n \tupload-archive--writer | upload-pack | web--browse | whatchanged)\n \t\texpect_outcome=expect_failure ;;\n"},{"id":"547414","messageId":"20260708001557.3581080-3-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260708001557.3581080-1-sandals@crustytoothpaste.net","subject":"[PATCH v3 2/4] parse-options: add a separate case for help output on error","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-08T00:15:55Z","receivedAt":"2026-07-08T00:16:07Z","isPatch":true,"body":"When we parse a command line option such as -h or --help, we currently\nexit 129, since that is the exit code when help output is printed. In a\nfuture commit, we'll change this to exit 0 instead, since we're doing\nwhat the user wanted successfully.\n\nHowever, there are some cases where we print help output because the\nuser has provided ambiguous or invalid input, such as an ambiguous\noption, and we'll want to exit unsuccessfully there.  Make this easier\nby defining a new return code, PARSE_OPT_HELP_ERROR, that can be used in\nthis case, while reserving PARSE_OPT_HELP for those cases where the user\nhas requested help directly.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n builtin/blame.c        |  1 +\n builtin/shortlog.c     |  1 +\n builtin/update-index.c |  1 +\n parse-options.c        | 11 ++++++++---\n parse-options.h        |  3 ++-\n 5 files changed, 13 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex ffbd3ce5c5..65d43c7d48 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -1013,6 +1013,7 @@ int cmd_blame(int argc,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n+\t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\texit(129);\ndiff --git a/builtin/shortlog.c b/builtin/shortlog.c\nindex 6b2a0b93b5..cd262bd376 100644\n--- a/builtin/shortlog.c\n+++ b/builtin/shortlog.c\n@@ -433,6 +433,7 @@ int cmd_shortlog(int argc,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n+\t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\texit(129);\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex 3d6646c318..ac4610ec94 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1133,6 +1133,7 @@ int cmd_update_index(int argc,\n \t\t\tbreak;\n \t\tswitch (parseopt_state) {\n \t\tcase PARSE_OPT_HELP:\n+\t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\t\texit(129);\n \t\tcase PARSE_OPT_COMPLETE:\ndiff --git a/parse-options.c b/parse-options.c\nindex f4647e0099..fd8ceed82b 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -583,7 +583,7 @@ static enum parse_opt_result parse_long_opt(\n \t\t\tambiguous.option->long_name,\n \t\t\t(abbrev.flags & OPT_UNSET) ?  \"no-\" : \"\",\n \t\t\tabbrev.option->long_name);\n-\t\treturn PARSE_OPT_HELP;\n+\t\treturn PARSE_OPT_HELP_ERROR;\n \t}\n \tif (abbrev.option) {\n \t\tif (*arg_end)\n@@ -1037,6 +1037,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\t\tusage_with_options(usagestr, options);\n \t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\tcase PARSE_OPT_HELP:\n+\t\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\tcase PARSE_OPT_ERROR:\n \t\t\tcase PARSE_OPT_DONE:\n \t\t\tcase PARSE_OPT_NON_OPTION:\n@@ -1072,6 +1073,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\tcase PARSE_OPT_NON_OPTION:\n \t\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\tcase PARSE_OPT_HELP:\n+\t\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\t\tBUG(\"parse_short_opt() cannot return these\");\n \t\t\tcase PARSE_OPT_DONE:\n@@ -1099,6 +1101,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\t\t\tcase PARSE_OPT_SUBCOMMAND:\n \t\t\t\tcase PARSE_OPT_COMPLETE:\n \t\t\t\tcase PARSE_OPT_HELP:\n+\t\t\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\t\t\tBUG(\"parse_short_opt() cannot return these\");\n \t\t\t\tcase PARSE_OPT_DONE:\n \t\t\t\t\tbreak;\n@@ -1132,6 +1135,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tgoto unknown;\n \t\tcase PARSE_OPT_HELP:\n+\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\tgoto show_usage;\n \t\tcase PARSE_OPT_NON_OPTION:\n \t\tcase PARSE_OPT_SUBCOMMAND:\n@@ -1197,6 +1201,7 @@ int parse_options(int argc, const char **argv,\n \tparse_options_start_1(&ctx, argc, argv, prefix, options, flags);\n \tswitch (parse_options_step(&ctx, options, usagestr)) {\n \tcase PARSE_OPT_HELP:\n+\tcase PARSE_OPT_HELP_ERROR:\n \tcase PARSE_OPT_ERROR:\n \t\texit(129);\n \tcase PARSE_OPT_COMPLETE:\n@@ -1363,7 +1368,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \tparse_options_check_harder(opts);\n \n \tif (!usagestr)\n-\t\treturn PARSE_OPT_HELP;\n+\t\treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n \n \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n \t\tfprintf(outfile, \"cat <<\\\\EOF\\n\");\n@@ -1476,7 +1481,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n \t\tfputs(\"EOF\\n\", outfile);\n \n-\treturn PARSE_OPT_HELP;\n+\treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n }\n \n void NORETURN usage_with_options(const char * const *usagestr,\ndiff --git a/parse-options.h b/parse-options.h\nindex 0d1f738f8d..3ec8ba5cc8 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -57,7 +57,8 @@ enum parse_opt_option_flags {\n };\n \n enum parse_opt_result {\n-\tPARSE_OPT_COMPLETE = -3,\n+\tPARSE_OPT_COMPLETE = -4,\n+\tPARSE_OPT_HELP_ERROR = -3,\n \tPARSE_OPT_HELP = -2,\n \tPARSE_OPT_ERROR = -1,\t/* must be the same as error() */\n \tPARSE_OPT_DONE = 0,\t/* fixed so that \"return 0\" works */\n"},{"id":"547415","messageId":"20260708001557.3581080-1-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260701212442.1430084-1-sandals@crustytoothpaste.net","subject":"[PATCH v3 0/4] parseopt: exit 0 on help","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-08T00:15:53Z","receivedAt":"2026-07-08T00:16:07Z","isPatch":true,"body":"The standard philosophy for Unix software when a help option (such as\n--help) is specified is that the software should exit 0, printing the\nhelp output to standard output, since the standard output is for\nuser-requested output and the program performed the requested task\nsuccessfully.  If the user specifies an incorrect option, then the help\noutput should be printed to standard error (since the user has made a\nmistake) and it should exit unsuccessfully.\n\ngit rev-parse --parseopt properly directs the output in both of these\ncases, but it currently exits 129 when it receives a --help or -h option\non the command line, which causes its invoking script to do the same.\nThis is not in line with the usual behavior and it causes scripts using\nthis command to exit unsuccessfully on --help as well.\n\nThis series introduces some changes to distinguish the --help and -h\noptions from other cases in which we print help output and adjusts the\nexit code to 0 from those two options.  We continue to exit 129 when the\noptions are invalid, which is useful information to have for callers.\nWe also make the relevant changes such that `git rev-parse --parseopt`\ndoes the same thing as long as it is invoked in the way specified in the\nmanual page (which a quick GitHub search shows almost everyone does).\n\nChanges since v2:\n\n* Fix inverted condition in t1517.\n* Stop checking for old versions of SVN Perl libraries since they are\n  so old nobody is using them.\n* Adjust the various cases where we choose between the error and\n  non-error help output.\n\nbrian m. carlson (4):\n  t1517: skip svn tests if svn is not installed\n  parse-options: add a separate case for help output on error\n  rev-parse: have --parseopt callers exit 0 on --help\n  parse-options: exit 0 on -h\n\n builtin/blame.c                    |  2 ++\n builtin/shortlog.c                 |  2 ++\n builtin/update-index.c             |  2 ++\n contrib/subtree/t/t7900-subtree.sh |  2 +-\n parse-options.c                    | 22 +++++++++++-----\n parse-options.h                    |  3 ++-\n t/for-each-ref-tests.sh            |  2 +-\n t/t0012-help.sh                    |  2 +-\n t/t0040-parse-options.sh           |  2 +-\n t/t0450-txt-doc-vs-help.sh         |  2 +-\n t/t0610-reftable-basics.sh         |  4 +--\n t/t1403-show-ref.sh                |  2 +-\n t/t1410-reflog.sh                  |  4 +--\n t/t1418-reflog-exists.sh           |  2 +-\n t/t1502-rev-parse-parseopt.sh      | 23 +++++++++-------\n t/t1502/optionspec-neg.help        |  1 +\n t/t1502/optionspec.help            |  1 +\n t/t1517-outside-repo.sh            | 42 +++++++++++++++++++++---------\n t/t1800-hook.sh                    |  4 +--\n t/t1900-repo-info.sh               |  2 +-\n t/t1901-repo-structure.sh          |  2 +-\n t/t2006-checkout-index-basic.sh    |  6 ++---\n t/t2107-update-index-basic.sh      |  2 +-\n t/t3004-ls-files-basic.sh          |  6 ++---\n t/t3200-branch.sh                  |  2 +-\n t/t3903-stash.sh                   |  4 +--\n t/t4200-rerere.sh                  |  2 +-\n t/t5200-update-server-info.sh      |  2 +-\n t/t5304-prune.sh                   |  2 +-\n t/t5400-send-pack.sh               |  4 +--\n t/t5512-ls-remote.sh               |  2 +-\n t/t6300-for-each-ref.sh            |  4 +--\n t/t6500-gc.sh                      |  2 +-\n t/t7030-verify-tag.sh              |  4 +--\n t/t7508-status.sh                  |  4 +--\n t/t7510-signed-commit.sh           |  4 +--\n t/t7600-merge.sh                   |  2 +-\n t/t7800-difftool.sh                |  3 +--\n t/t7900-maintenance.sh             |  2 +-\n usage.c                            |  2 +-\n 40 files changed, 113 insertions(+), 74 deletions(-)\n\nRange-diff against v2:\n1:  558a53cc20 ! 1:  c8c7eac5f7 t1517: skip svn tests if svn is not installed\n    @@ t/t1517-outside-repo.sh: test_description='check random commands outside repo'\n      . ./test-lib.sh\n      \n     +test_lazy_prereq SVN '\n    -+\ttest_have_prereq PERL && test -n \"$NO_SVN_TESTS\" && perl -w -e \"\n    ++\ttest_have_prereq PERL && test -z \"$NO_SVN_TESTS\" && perl -w -e \"\n     +\t\tuse SVN::Core;\n     +\t\tuse SVN::Repos;\n    -+\t\t\\$SVN::Core::VERSION gt '1.1.0' or exit(42);\n     +\t\"\n     +'\n     +\n2:  2b5ce2fb4c ! 2:  daa7aa2534 parse-options: add a separate case for help output on error\n    @@ parse-options.c: int parse_options(int argc, const char **argv,\n      \tcase PARSE_OPT_ERROR:\n      \t\texit(129);\n      \tcase PARSE_OPT_COMPLETE:\n    +@@ parse-options.c: static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n    + \tparse_options_check_harder(opts);\n    + \n    + \tif (!usagestr)\n    +-\t\treturn PARSE_OPT_HELP;\n    ++\t\treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n    + \n    + \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n    + \t\tfprintf(outfile, \"cat <<\\\\EOF\\n\");\n    +@@ parse-options.c: static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n    + \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n    + \t\tfputs(\"EOF\\n\", outfile);\n    + \n    +-\treturn PARSE_OPT_HELP;\n    ++\treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n    + }\n    + \n    + void NORETURN usage_with_options(const char * const *usagestr,\n     \n      ## parse-options.h ##\n     @@ parse-options.h: enum parse_opt_option_flags {\n3:  e5d0167544 ! 3:  af69daffc3 rev-parse: have --parseopt callers exit 0 on --help\n    @@ parse-options.c: static enum parse_opt_result usage_with_options_internal(struct\n     -\t\tfputs(\"EOF\\n\", outfile);\n     +\t\tfputs(\"EOF\\nexit 0\\n\", outfile);\n      \n    - \treturn PARSE_OPT_HELP;\n    + \treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n      }\n     \n      ## t/t1502-rev-parse-parseopt.sh ##\n4:  98481005ff ! 4:  f68c53015c parse-options: exit 0 on -h\n    @@ parse-options.c: int parse_options(int argc, const char **argv,\n      \tcase PARSE_OPT_HELP_ERROR:\n      \tcase PARSE_OPT_ERROR:\n      \t\texit(129);\n    -@@ parse-options.c: static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n    - \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n    - \t\tfputs(\"EOF\\nexit 0\\n\", outfile);\n    - \n    --\treturn PARSE_OPT_HELP;\n    -+\treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n    - }\n    - \n    - void NORETURN usage_with_options(const char * const *usagestr,\n     @@ parse-options.c: void show_usage_with_options_if_asked(int ac, const char **av,\n      \t\tif (!strcmp(av[1], \"-h\")) {\n      \t\t\tusage_with_options_internal(NULL, usagestr, opts,\n"},{"id":"547416","messageId":"20260708001557.3581080-5-sandals@crustytoothpaste.net","threadId":"65249","inReplyTo":"20260708001557.3581080-1-sandals@crustytoothpaste.net","subject":"[PATCH v3 4/4] parse-options: exit 0 on -h","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-07-08T00:15:57Z","receivedAt":"2026-07-08T00:16:07Z","isPatch":true,"body":"The standard philosophy for Unix software when a help option (such as\n--help) is specified is that the software should exit 0, printing the\nhelp output to standard output, since the standard output is for\nuser-requested output and the program performed the requested task\nsuccessfully.  If the user specifies an incorrect option, then the help\noutput should be printed to standard error (since the user has made a\nmistake) and it should exit unsuccessfully.\n\nMost of our commands currently exit 129 on receiving the -h option to\nprint the short help, which does not line up with the standard\nphilosophy above.  Let's change that to exit 0 instead.\n\nThis requires changes to a variety of tests which previously wanted the\n129 exit code, so update them.  Note that because git diff does its own\noption parsing, it still exits with 129, so update some of the tests to\nexpect either exit status.\n\nSome commands also now pass with -h but not --help-all, so handle those\ncases differently for those commands.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n builtin/blame.c                    |  1 +\n builtin/shortlog.c                 |  1 +\n builtin/update-index.c             |  1 +\n contrib/subtree/t/t7900-subtree.sh |  2 +-\n parse-options.c                    | 11 ++++++---\n t/for-each-ref-tests.sh            |  2 +-\n t/t0012-help.sh                    |  2 +-\n t/t0040-parse-options.sh           |  2 +-\n t/t0450-txt-doc-vs-help.sh         |  2 +-\n t/t0610-reftable-basics.sh         |  4 +--\n t/t1403-show-ref.sh                |  2 +-\n t/t1410-reflog.sh                  |  4 +--\n t/t1418-reflog-exists.sh           |  2 +-\n t/t1502-rev-parse-parseopt.sh      | 14 +++++------\n t/t1517-outside-repo.sh            | 39 ++++++++++++++++++------------\n t/t1800-hook.sh                    |  4 +--\n t/t1900-repo-info.sh               |  2 +-\n t/t1901-repo-structure.sh          |  2 +-\n t/t2006-checkout-index-basic.sh    |  6 ++---\n t/t2107-update-index-basic.sh      |  2 +-\n t/t3004-ls-files-basic.sh          |  6 ++---\n t/t3200-branch.sh                  |  2 +-\n t/t3903-stash.sh                   |  4 +--\n t/t4200-rerere.sh                  |  2 +-\n t/t5200-update-server-info.sh      |  2 +-\n t/t5304-prune.sh                   |  2 +-\n t/t5400-send-pack.sh               |  4 +--\n t/t5512-ls-remote.sh               |  2 +-\n t/t6300-for-each-ref.sh            |  4 +--\n t/t6500-gc.sh                      |  2 +-\n t/t7030-verify-tag.sh              |  4 +--\n t/t7508-status.sh                  |  4 +--\n t/t7510-signed-commit.sh           |  4 +--\n t/t7600-merge.sh                   |  2 +-\n t/t7800-difftool.sh                |  3 +--\n t/t7900-maintenance.sh             |  2 +-\n usage.c                            |  2 +-\n 37 files changed, 85 insertions(+), 71 deletions(-)\n\ndiff --git a/builtin/blame.c b/builtin/blame.c\nindex 65d43c7d48..38749f79c2 100644\n--- a/builtin/blame.c\n+++ b/builtin/blame.c\n@@ -1013,6 +1013,7 @@ int cmd_blame(int argc,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n+\t\t\texit(0);\n \t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\tcase PARSE_OPT_SUBCOMMAND:\ndiff --git a/builtin/shortlog.c b/builtin/shortlog.c\nindex cd262bd376..4c78d2e5ba 100644\n--- a/builtin/shortlog.c\n+++ b/builtin/shortlog.c\n@@ -433,6 +433,7 @@ int cmd_shortlog(int argc,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tbreak;\n \t\tcase PARSE_OPT_HELP:\n+\t\t\texit(0);\n \t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\tcase PARSE_OPT_SUBCOMMAND:\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex ac4610ec94..6810327209 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -1133,6 +1133,7 @@ int cmd_update_index(int argc,\n \t\t\tbreak;\n \t\tswitch (parseopt_state) {\n \t\tcase PARSE_OPT_HELP:\n+\t\t\texit(0);\n \t\tcase PARSE_OPT_HELP_ERROR:\n \t\tcase PARSE_OPT_ERROR:\n \t\t\texit(129);\ndiff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh\nindex 4194687cfb..c10f283b38 100755\n--- a/contrib/subtree/t/t7900-subtree.sh\n+++ b/contrib/subtree/t/t7900-subtree.sh\n@@ -99,7 +99,7 @@ test_create_subtree_add () {\n }\n \n test_expect_success 'shows short help text for -h' '\n-\ttest_expect_code 129 git subtree -h >out 2>err &&\n+\tgit subtree -h >out 2>err &&\n \ttest_must_be_empty err &&\n \tgrep -e \"^ *or: git subtree pull\" out &&\n \tgrep -F -e \"--[no-]annotate\" out\ndiff --git a/parse-options.c b/parse-options.c\nindex cc3a8b0fe3..08c21d9fc0 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -1135,8 +1135,9 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n \t\tcase PARSE_OPT_UNKNOWN:\n \t\t\tgoto unknown;\n \t\tcase PARSE_OPT_HELP:\n-\t\tcase PARSE_OPT_HELP_ERROR:\n \t\t\tgoto show_usage;\n+\t\tcase PARSE_OPT_HELP_ERROR:\n+\t\t\tgoto show_usage_stderr;\n \t\tcase PARSE_OPT_NON_OPTION:\n \t\tcase PARSE_OPT_SUBCOMMAND:\n \t\tcase PARSE_OPT_COMPLETE:\n@@ -1170,6 +1171,9 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx,\n  show_usage:\n \treturn usage_with_options_internal(ctx, usagestr, options,\n \t\t\t\t\t   USAGE_NORMAL, USAGE_TO_STDOUT);\n+ show_usage_stderr:\n+\treturn usage_with_options_internal(ctx, usagestr, options,\n+\t\t\t\t\t   USAGE_NORMAL, USAGE_TO_STDERR);\n }\n \n int parse_options_end(struct parse_opt_ctx_t *ctx)\n@@ -1201,6 +1205,7 @@ int parse_options(int argc, const char **argv,\n \tparse_options_start_1(&ctx, argc, argv, prefix, options, flags);\n \tswitch (parse_options_step(&ctx, options, usagestr)) {\n \tcase PARSE_OPT_HELP:\n+\t\texit(0);\n \tcase PARSE_OPT_HELP_ERROR:\n \tcase PARSE_OPT_ERROR:\n \t\texit(129);\n@@ -1500,11 +1505,11 @@ void show_usage_with_options_if_asked(int ac, const char **av,\n \t\tif (!strcmp(av[1], \"-h\")) {\n \t\t\tusage_with_options_internal(NULL, usagestr, opts,\n \t\t\t\t\t\t    USAGE_NORMAL, USAGE_TO_STDOUT);\n-\t\t\texit(129);\n+\t\t\texit(0);\n \t\t} else if (!strcmp(av[1], \"--help-all\")) {\n \t\t\tusage_with_options_internal(NULL, usagestr, opts,\n \t\t\t\t\t\t    USAGE_FULL, USAGE_TO_STDOUT);\n-\t\t\texit(129);\n+\t\t\texit(0);\n \t\t}\n \t}\n }\ndiff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh\nindex bd2d45c971..b95e5b6ca0 100644\n--- a/t/for-each-ref-tests.sh\n+++ b/t/for-each-ref-tests.sh\n@@ -522,7 +522,7 @@ test_expect_success 'Verify descending sort' '\n '\n \n test_expect_success 'Give help even with invalid sort atoms' '\n-\ttest_expect_code 129 ${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&\n+\t${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&\n \tgrep \"^usage: ${git_for_each_ref}\" actual\n '\n \ndiff --git a/t/t0012-help.sh b/t/t0012-help.sh\nindex c33501bdcd..7815ff14f2 100755\n--- a/t/t0012-help.sh\n+++ b/t/t0012-help.sh\n@@ -260,7 +260,7 @@ do\n \t\t(\n \t\t\tGIT_CEILING_DIRECTORIES=$(pwd) &&\n \t\t\texport GIT_CEILING_DIRECTORIES &&\n-\t\t\ttest_expect_code 129 git -C sub $builtin -h >output 2>err\n+\t\t\tgit -C sub $builtin -h >output 2>err\n \t\t) &&\n \t\ttest_must_be_empty err &&\n \t\ttest_grep usage output\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ca55ea8228..30895ad6d2 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -68,7 +68,7 @@ Alias\n EOF\n \n test_expect_success 'test help' '\n-\ttest_must_fail test-tool parse-options -h >output 2>output.err &&\n+\ttest-tool parse-options -h >output 2>output.err &&\n \ttest_must_be_empty output.err &&\n \ttest_cmp expect output\n '\ndiff --git a/t/t0450-txt-doc-vs-help.sh b/t/t0450-txt-doc-vs-help.sh\nindex 822b0d55a5..d2844368e4 100755\n--- a/t/t0450-txt-doc-vs-help.sh\n+++ b/t/t0450-txt-doc-vs-help.sh\n@@ -29,7 +29,7 @@ help_to_synopsis () {\n \t\treturn 0\n \tfi &&\n \tmkdir -p \"$out_dir\" &&\n-\ttest_expect_code 129 git $builtin -h >\"$out.raw\" 2>&1 &&\n+\ttest_might_fail git $builtin -h >\"$out.raw\" 2>&1 &&\n \tsed -n \\\n \t\t-e '1,/^$/ {\n \t\t\t/^$/d;\ndiff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh\nindex e19e036898..4135db95ed 100755\n--- a/t/t0610-reftable-basics.sh\n+++ b/t/t0610-reftable-basics.sh\n@@ -15,9 +15,9 @@ export GIT_TEST_DEFAULT_REF_FORMAT\n INVALID_OID=$(test_oid 001)\n \n test_expect_success 'pack-refs does not crash with -h' '\n-\ttest_expect_code 129 git pack-refs -h >usage &&\n+\tgit pack-refs -h >usage &&\n \ttest_grep \"[Uu]sage: git pack-refs \" usage &&\n-\ttest_expect_code 129 nongit git pack-refs -h >usage &&\n+\tnongit git pack-refs -h >usage &&\n \ttest_grep \"[Uu]sage: git pack-refs \" usage\n '\n \ndiff --git a/t/t1403-show-ref.sh b/t/t1403-show-ref.sh\nindex 36c903ca19..db4300da44 100755\n--- a/t/t1403-show-ref.sh\n+++ b/t/t1403-show-ref.sh\n@@ -165,7 +165,7 @@ test_expect_success 'show-ref --branches, --tags, --head, pattern' '\n '\n \n test_expect_success 'show-ref --heads is deprecated and hidden' '\n-\ttest_expect_code 129 git show-ref -h >short-help &&\n+\tgit show-ref -h >short-help &&\n \ttest_grep ! -e --heads short-help &&\n \tgit show-ref --heads >actual 2>warning &&\n \ttest_grep ! deprecated warning &&\ndiff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh\nindex ce71f9a30a..6e921bc167 100755\n--- a/t/t1410-reflog.sh\n+++ b/t/t1410-reflog.sh\n@@ -107,12 +107,12 @@ test_expect_success setup '\n '\n \n test_expect_success 'correct usage on sub-command -h' '\n-\ttest_expect_code 129 git reflog expire -h >err &&\n+\tgit reflog expire -h >err &&\n \tgrep \"git reflog expire\" err\n '\n \n test_expect_success 'correct usage on \"git reflog show -h\"' '\n-\ttest_expect_code 129 git reflog show -h >err &&\n+\tgit reflog show -h >err &&\n \tgrep -F \"git reflog [show]\" err\n '\n \ndiff --git a/t/t1418-reflog-exists.sh b/t/t1418-reflog-exists.sh\nindex d51ecd5e92..10387792e3 100755\n--- a/t/t1418-reflog-exists.sh\n+++ b/t/t1418-reflog-exists.sh\n@@ -12,7 +12,7 @@ test_expect_success 'setup' '\n \n test_expect_success 'usage' '\n \ttest_expect_code 129 git reflog exists &&\n-\ttest_expect_code 129 git reflog exists -h\n+\tgit reflog exists -h\n '\n \n test_expect_success 'usage: unknown option' '\ndiff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh\nindex 455608c429..fa97591b9f 100755\n--- a/t/t1502-rev-parse-parseopt.sh\n+++ b/t/t1502-rev-parse-parseopt.sh\n@@ -75,7 +75,7 @@ EOF\n '\n \n test_expect_success 'test --parseopt help output' '\n-\ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec &&\n+\tgit rev-parse --parseopt -- -h > output < optionspec &&\n \ttest_cmp \"$TEST_DIRECTORY/t1502/optionspec.help\" output\n '\n \n@@ -89,7 +89,7 @@ test_expect_success 'test --parseopt help output no switches' '\n |EOF\n |exit 0\n END_EXPECT\n-\ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_no_switches &&\n+\tgit rev-parse --parseopt -- -h > output < optionspec_no_switches &&\n \ttest_cmp expect output\n '\n \n@@ -103,7 +103,7 @@ test_expect_success 'test --parseopt help output hidden switches' '\n |EOF\n |exit 0\n END_EXPECT\n-\ttest_expect_code 129 git rev-parse --parseopt -- -h > output < optionspec_only_hidden_switches &&\n+\tgit rev-parse --parseopt -- -h > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n '\n \n@@ -119,7 +119,7 @@ test_expect_success 'test --parseopt help-all output hidden switches' '\n |EOF\n |exit 0\n END_EXPECT\n-\ttest_expect_code 129 git rev-parse --parseopt -- --help-all > output < optionspec_only_hidden_switches &&\n+\tgit rev-parse --parseopt -- --help-all > output < optionspec_only_hidden_switches &&\n \ttest_cmp expect output\n '\n \n@@ -258,7 +258,7 @@ test_expect_success 'test --parseopt help output: \"wrapped\" options normal \"or:\"\n \t|exit 0\n \tEND_EXPECT\n \n-\ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\n+\tgit rev-parse --parseopt -- -h <spec >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -296,12 +296,12 @@ test_expect_success 'test --parseopt help output: multi-line blurb after empty l\n \t|exit 0\n \tEND_EXPECT\n \n-\ttest_must_fail git rev-parse --parseopt -- -h <spec >actual &&\n+\tgit rev-parse --parseopt -- -h <spec >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'test --parseopt help output for optionspec-neg' '\n-\ttest_expect_code 129 git rev-parse --parseopt -- \\\n+\tgit rev-parse --parseopt -- \\\n \t\t-h >output <\"$TEST_DIRECTORY/t1502/optionspec-neg\" &&\n \ttest_cmp \"$TEST_DIRECTORY/t1502/optionspec-neg.help\" output\n '\ndiff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh\nindex 03fa2f9cdf..d1e915786e 100755\n--- a/t/t1517-outside-repo.sh\n+++ b/t/t1517-outside-repo.sh\n@@ -129,18 +129,25 @@ do\n \tarchimport | citool | credential-netrc | credential-libsecret | \\\n \tcredential-osxkeychain | cvsexportcommit | cvsimport | cvsserver | \\\n \tdaemon | \\\n-\tdifftool--helper | filter-branch | format-rev | fsck-objects | \\\n-\tget-tar-commit-id | \\\n+\tdifftool--helper | format-rev | fsck-objects |  get-tar-commit-id | \\\n \tgui | gui--askpass | \\\n-\thttp-backend | http-fetch | http-push | init-db | instaweb | \\\n-\tmerge-octopus | merge-one-file | merge-resolve | mergetool | \\\n-\tmktag | p4 | p4.py | pickaxe | quiltimport | remote-ftp | remote-ftps | \\\n-\tremote-http | remote-https | replay | request-pull | send-email | \\\n-\tsh-i18n--envsubst | shell | show | stage | submodule | svn | \\\n-\tupload-archive--writer | upload-pack | web--browse | whatchanged)\n-\t\texpect_outcome=expect_failure ;;\n+\thttp-backend | http-fetch | http-push | init-db | \\\n+\tmktag | p4 | p4.py | pickaxe | remote-ftp | remote-ftps | \\\n+\tremote-http | remote-https | replay | send-email | \\\n+\tsh-i18n--envsubst | shell | show | stage | \\\n+\tupload-archive--writer | upload-pack | whatchanged)\n+\t\th_expect_outcome=expect_failure\n+\t\tall_expect_outcome=expect_failure\n+\t\t;;\n+\tfilter-branch | merge-octopus | merge-one-file | merge-resolve | \\\n+\tmergetool | submodule | svn | web--browse)\n+\t\th_expect_outcome=expect_success\n+\t\tall_expect_outcome=expect_failure\n+\t\t;;\n \t*)\n-\t\texpect_outcome=expect_success ;;\n+\t\th_expect_outcome=expect_success\n+\t\tall_expect_outcome=expect_success\n+\t\t;;\n \tesac\n \tcase \"$cmd\" in\n \tinstaweb)\n@@ -150,20 +157,20 @@ do\n \t*)\n \t\tprereq= ;;\n \tesac\n-\ttest_$expect_outcome $prereq \"'git $cmd -h' outside a repository\" '\n-\t\ttest_expect_code 129 nongit git $cmd -h >usage &&\n+\ttest_$h_expect_outcome $prereq \"'git $cmd -h' outside a repository\" '\n+\t\tnongit git $cmd -h >usage &&\n \t\ttest_grep \"[Uu]sage: git $cmd \" usage\n \t'\n-\ttest_$expect_outcome $prereq \"'git $cmd --help-all' outside a repository\" '\n-\t\ttest_expect_code 129 nongit git $cmd --help-all >usage &&\n+\ttest_$all_expect_outcome $prereq \"'git $cmd --help-all' outside a repository\" '\n+\t\tnongit git $cmd --help-all >usage &&\n \t\ttest_grep \"[Uu]sage: git $cmd \" usage\n \t'\n done\n \n test_expect_success 'fmt-merge-msg does not crash with -h' '\n-\ttest_expect_code 129 git fmt-merge-msg -h >usage &&\n+\tgit fmt-merge-msg -h >usage &&\n \ttest_grep \"[Uu]sage: git fmt-merge-msg \" usage &&\n-\ttest_expect_code 129 nongit git fmt-merge-msg -h >usage &&\n+\tnongit git fmt-merge-msg -h >usage &&\n \ttest_grep \"[Uu]sage: git fmt-merge-msg \" usage\n '\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 0132e772e4..2ea9fa13c5 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -75,10 +75,10 @@ sentinel_detector () {\n test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook &&\n \ttest_expect_code 129 git hook run &&\n-\ttest_expect_code 129 git hook run -h &&\n+\tgit hook run -h &&\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n-\ttest_expect_code 129 git hook list -h &&\n+\tgit hook list -h &&\n \tgrep \"unknown option\" err\n '\n \ndiff --git a/t/t1900-repo-info.sh b/t/t1900-repo-info.sh\nindex 39bb77dda0..826686955d 100755\n--- a/t/t1900-repo-info.sh\n+++ b/t/t1900-repo-info.sh\n@@ -150,7 +150,7 @@ test_expect_success 'git repo info --keys uses lines as its default output forma\n '\n \n test_expect_success 'git repo info -h shows only repo info usage' '\n-\ttest_must_fail git repo info -h >actual &&\n+\tgit repo info -h >actual &&\n \ttest_grep \"git repo info\" actual &&\n \ttest_grep ! \"git repo structure\" actual\n '\ndiff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh\nindex 10050abd70..02cc2b594a 100755\n--- a/t/t1901-repo-structure.sh\n+++ b/t/t1901-repo-structure.sh\n@@ -225,7 +225,7 @@ test_expect_success 'progress meter option' '\n '\n \n test_expect_success 'git repo structure -h shows only repo structure usage' '\n-\ttest_must_fail git repo structure -h >actual &&\n+\tgit repo structure -h >actual &&\n \ttest_grep \"git repo structure\" actual &&\n \ttest_grep ! \"git repo info\" actual\n '\ndiff --git a/t/t2006-checkout-index-basic.sh b/t/t2006-checkout-index-basic.sh\nindex fedd2cc097..6538a24c95 100755\n--- a/t/t2006-checkout-index-basic.sh\n+++ b/t/t2006-checkout-index-basic.sh\n@@ -16,15 +16,15 @@ test_expect_success 'checkout-index -h in broken repository' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\t>.git/index &&\n-\t\ttest_expect_code 129 git checkout-index -h >usage 2>&1\n+\t\tgit checkout-index -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\n \n test_expect_success 'checkout-index does not crash with -h' '\n-\ttest_expect_code 129 git checkout-index -h >usage &&\n+\tgit checkout-index -h >usage &&\n \ttest_grep \"[Uu]sage: git checkout-index \" usage &&\n-\ttest_expect_code 129 nongit git checkout-index -h >usage &&\n+\tnongit git checkout-index -h >usage &&\n \ttest_grep \"[Uu]sage: git checkout-index \" usage\n '\n \ndiff --git a/t/t2107-update-index-basic.sh b/t/t2107-update-index-basic.sh\nindex 3bffe5da8a..004878322e 100755\n--- a/t/t2107-update-index-basic.sh\n+++ b/t/t2107-update-index-basic.sh\n@@ -23,7 +23,7 @@ test_expect_success 'update-index -h with corrupt index' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\t>.git/index &&\n-\t\ttest_expect_code 129 git update-index -h >usage 2>&1\n+\t\tgit update-index -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage: git update-index\" broken/usage\n '\ndiff --git a/t/t3004-ls-files-basic.sh b/t/t3004-ls-files-basic.sh\nindex 4034a5a59f..c57afcb841 100755\n--- a/t/t3004-ls-files-basic.sh\n+++ b/t/t3004-ls-files-basic.sh\n@@ -29,15 +29,15 @@ test_expect_success 'ls-files -h in corrupt repository' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\t>.git/index &&\n-\t\ttest_expect_code 129 git ls-files -h >usage 2>&1\n+\t\tgit ls-files -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage: git ls-files \" broken/usage\n '\n \n test_expect_success 'ls-files does not crash with -h' '\n-\ttest_expect_code 129 git ls-files -h >usage &&\n+\tgit ls-files -h >usage &&\n \ttest_grep \"[Uu]sage: git ls-files \" usage &&\n-\ttest_expect_code 129 nongit git ls-files -h >usage &&\n+\tnongit git ls-files -h >usage &&\n \ttest_grep \"[Uu]sage: git ls-files \" usage\n '\n \ndiff --git a/t/t3200-branch.sh b/t/t3200-branch.sh\nindex e7829c2c4b..dec0e77e3c 100755\n--- a/t/t3200-branch.sh\n+++ b/t/t3200-branch.sh\n@@ -33,7 +33,7 @@ test_expect_success REFFILES 'branch -h in broken repository' '\n \t\tcd broken &&\n \t\tgit init -b main &&\n \t\t>.git/refs/heads/main &&\n-\t\ttest_expect_code 129 git branch -h >usage 2>&1\n+\t\tgit branch -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\ndiff --git a/t/t3903-stash.sh b/t/t3903-stash.sh\nindex ecc35aae82..bc07e2a6ec 100755\n--- a/t/t3903-stash.sh\n+++ b/t/t3903-stash.sh\n@@ -27,13 +27,13 @@ test_expect_success 'usage on cmd and subcommand invalid option' '\n '\n \n test_expect_success 'usage on main command -h emits a summary of subcommands' '\n-\ttest_expect_code 129 git stash -h >usage &&\n+\tgit stash -h >usage &&\n \tgrep -F \"usage: git stash list\" usage &&\n \tgrep -F \"or: git stash show\" usage\n '\n \n test_expect_success 'usage for subcommands should emit subcommand usage' '\n-\ttest_expect_code 129 git stash push -h >usage &&\n+\tgit stash push -h >usage &&\n \tgrep -F \"usage: git stash [push\" usage\n '\n \ndiff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh\nindex 1717f407c8..e1b474cc0f 100755\n--- a/t/t4200-rerere.sh\n+++ b/t/t4200-rerere.sh\n@@ -438,7 +438,7 @@ test_expect_success 'rerere --no-no-rerere-autoupdate' '\n '\n \n test_expect_success 'rerere -h' '\n-\ttest_must_fail git rerere -h >help &&\n+\tgit rerere -h >help &&\n \ttest_grep [Uu]sage help\n '\n \ndiff --git a/t/t5200-update-server-info.sh b/t/t5200-update-server-info.sh\nindex a551e955b5..a0630cc1fc 100755\n--- a/t/t5200-update-server-info.sh\n+++ b/t/t5200-update-server-info.sh\n@@ -47,7 +47,7 @@ test_expect_success 'midx does not create duplicate pack entries' '\n '\n \n test_expect_success 'update-server-info does not crash with -h' '\n-\ttest_expect_code 129 git update-server-info -h >usage &&\n+\tgit update-server-info -h >usage &&\n \ttest_grep \"[Uu]sage: git update-server-info \" usage\n '\n \ndiff --git a/t/t5304-prune.sh b/t/t5304-prune.sh\nindex 2be7cd30de..e26e833d89 100755\n--- a/t/t5304-prune.sh\n+++ b/t/t5304-prune.sh\n@@ -365,7 +365,7 @@ test_expect_success 'gc.recentObjectsHook' '\n '\n \n test_expect_success 'prune does not crash with -h' '\n-\ttest_expect_code 129 git prune -h >usage &&\n+\tgit prune -h >usage &&\n \ttest_grep \"[Uu]sage: git prune \" usage\n '\n \ndiff --git a/t/t5400-send-pack.sh b/t/t5400-send-pack.sh\nindex b32a0a6aa7..6aa5838e2b 100755\n--- a/t/t5400-send-pack.sh\n+++ b/t/t5400-send-pack.sh\n@@ -56,9 +56,9 @@ test_expect_success setup '\n \tgit log'\n \n test_expect_success 'send-pack does not crash with -h' '\n-\ttest_expect_code 129 git send-pack -h >usage &&\n+\tgit send-pack -h >usage &&\n \ttest_grep \"[Uu]sage: git send-pack \" usage &&\n-\ttest_expect_code 129 nongit git send-pack -h >usage &&\n+\tnongit git send-pack -h >usage &&\n \ttest_grep \"[Uu]sage: git send-pack \" usage\n '\n \ndiff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh\nindex 5930f55186..8345bc0b14 100755\n--- a/t/t5512-ls-remote.sh\n+++ b/t/t5512-ls-remote.sh\n@@ -86,7 +86,7 @@ test_expect_success 'ls-remote -h is deprecated w/o warning' '\n '\n \n test_expect_success 'ls-remote --heads is deprecated and hidden w/o warning' '\n-\ttest_expect_code 129 git ls-remote -h >short-help &&\n+\tgit ls-remote -h >short-help &&\n \ttest_grep ! -e --head short-help &&\n \tgit ls-remote --heads self >actual 2>warning &&\n \ttest_cmp expected.branches actual &&\ndiff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh\nindex 1d9809114d..6d27b42ff1 100755\n--- a/t/t6300-for-each-ref.sh\n+++ b/t/t6300-for-each-ref.sh\n@@ -8,9 +8,9 @@ test_description='for-each-ref test'\n . ./test-lib.sh\n \n test_expect_success \"for-each-ref does not crash with -h\" '\n-\ttest_expect_code 129 git for-each-ref -h >usage &&\n+\tgit for-each-ref -h >usage &&\n \ttest_grep \"[Uu]sage: git for-each-ref \" usage &&\n-\ttest_expect_code 129 nongit git for-each-ref -h >usage &&\n+\tnongit git for-each-ref -h >usage &&\n \ttest_grep \"[Uu]sage: git for-each-ref \" usage\n '\n \ndiff --git a/t/t6500-gc.sh b/t/t6500-gc.sh\nindex ea9aaad470..b40d13d7ff 100755\n--- a/t/t6500-gc.sh\n+++ b/t/t6500-gc.sh\n@@ -35,7 +35,7 @@ test_expect_success 'gc -h with invalid configuration' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\techo \"[gc] pruneexpire = CORRUPT\" >>.git/config &&\n-\t\ttest_expect_code 129 git gc -h >usage 2>&1\n+\t\tgit gc -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\ndiff --git a/t/t7030-verify-tag.sh b/t/t7030-verify-tag.sh\nindex 2c147072c1..3bc5d1e9a2 100755\n--- a/t/t7030-verify-tag.sh\n+++ b/t/t7030-verify-tag.sh\n@@ -8,9 +8,9 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n test_expect_success GPG 'verify-tag does not crash with -h' '\n-\ttest_expect_code 129 git verify-tag -h >usage &&\n+\tgit verify-tag -h >usage &&\n \ttest_grep \"[Uu]sage: git verify-tag \" usage &&\n-\ttest_expect_code 129 nongit git verify-tag -h >usage &&\n+\tnongit git verify-tag -h >usage &&\n \ttest_grep \"[Uu]sage: git verify-tag \" usage\n '\n \ndiff --git a/t/t7508-status.sh b/t/t7508-status.sh\nindex c2057bc94c..de7d7beec3 100755\n--- a/t/t7508-status.sh\n+++ b/t/t7508-status.sh\n@@ -16,7 +16,7 @@ test_expect_success 'status -h in broken repository' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\techo \"[status] showuntrackedfiles = CORRUPT\" >>.git/config &&\n-\t\ttest_expect_code 129 git status -h >usage 2>&1\n+\t\tgit status -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\n@@ -28,7 +28,7 @@ test_expect_success 'commit -h in broken repository' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\techo \"[status] showuntrackedfiles = CORRUPT\" >>.git/config &&\n-\t\ttest_expect_code 129 git commit -h >usage 2>&1\n+\t\tgit commit -h >usage 2>&1\n \t) &&\n \ttest_grep \"[Uu]sage\" broken/usage\n '\ndiff --git a/t/t7510-signed-commit.sh b/t/t7510-signed-commit.sh\nindex aa9108da54..e641f9e334 100755\n--- a/t/t7510-signed-commit.sh\n+++ b/t/t7510-signed-commit.sh\n@@ -9,9 +9,9 @@ GNUPGHOME_NOT_USED=$GNUPGHOME\n . \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n test_expect_success GPG 'verify-commit does not crash with -h' '\n-\ttest_expect_code 129 git verify-commit -h >usage &&\n+\tgit verify-commit -h >usage &&\n \ttest_grep \"[Uu]sage: git verify-commit \" usage &&\n-\ttest_expect_code 129 nongit git verify-commit -h >usage &&\n+\tnongit git verify-commit -h >usage &&\n \ttest_grep \"[Uu]sage: git verify-commit \" usage\n '\n \ndiff --git a/t/t7600-merge.sh b/t/t7600-merge.sh\nindex f877d9a433..fd3d1d67f9 100755\n--- a/t/t7600-merge.sh\n+++ b/t/t7600-merge.sh\n@@ -173,7 +173,7 @@ test_expect_success 'merge -h with invalid index' '\n \t\tcd broken &&\n \t\tgit init &&\n \t\t>.git/index &&\n-\t\ttest_expect_code 129 git merge -h >usage\n+\t\tgit merge -h >usage\n \t) &&\n \ttest_grep \"[Uu]sage: git merge\" broken/usage\n '\ndiff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh\nindex 8a91ff3603..961de3efab 100755\n--- a/t/t7800-difftool.sh\n+++ b/t/t7800-difftool.sh\n@@ -27,12 +27,11 @@ prompt_given ()\n }\n \n test_expect_success 'basic usage requires no repo' '\n-\ttest_expect_code 129 git difftool -h >output &&\n+\tgit difftool -h >output &&\n \ttest_grep ^usage: output &&\n \t# create a ceiling directory to prevent Git from finding a repo\n \tmkdir -p not/repo &&\n \ttest_when_finished rm -r not &&\n-\ttest_expect_code 129 \\\n \tenv GIT_CEILING_DIRECTORIES=\"$(pwd)/not\" \\\n \tgit -C not/repo difftool -h >output &&\n \ttest_grep ^usage: output\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d7f82e1bec..9886f641fc 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -35,7 +35,7 @@ test_systemd_analyze_verify () {\n }\n \n test_expect_success 'help text' '\n-\ttest_expect_code 129 git maintenance -h >actual &&\n+\tgit maintenance -h >actual &&\n \ttest_grep \"usage: git maintenance <subcommand>\" actual &&\n \ttest_expect_code 129 git maintenance barf 2>err &&\n \ttest_grep \"unknown subcommand: \\`barf'\\''\" err &&\ndiff --git a/usage.c b/usage.c\nindex 527edb1e79..3f0118ab2a 100644\n--- a/usage.c\n+++ b/usage.c\n@@ -188,7 +188,7 @@ static void show_usage_if_asked_helper(const char *err, ...)\n \tva_start(params, err);\n \tvfreportf(stdout, _(\"usage: \"), err, params);\n \tva_end(params);\n-\texit(129);\n+\texit(0);\n }\n \n void show_usage_if_asked(int ac, const char **av, const char *err)\n"},{"id":"547420","messageId":"xmqqmrw2xoet.fsf@gitster.g","threadId":"65249","inReplyTo":"20260708001557.3581080-3-sandals@crustytoothpaste.net","subject":"Re: [PATCH v3 2/4] parse-options: add a separate case for help output on error","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-08T01:26:02Z","receivedAt":"2026-07-08T01:26:05Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> @@ -1363,7 +1368,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n>  \tparse_options_check_harder(opts);\n>  \n>  \tif (!usagestr)\n> -\t\treturn PARSE_OPT_HELP;\n> +\t\treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n\nThis part, IIUC, got updated from the previous round.  Looks\nsensible.\n\n> @@ -1476,7 +1481,7 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t\n>  \tif (!err && ctx && ctx->flags & PARSE_OPT_SHELL_EVAL)\n>  \t\tfputs(\"EOF\\n\", outfile);\n>  \n> -\treturn PARSE_OPT_HELP;\n> +\treturn err ? PARSE_OPT_HELP_ERROR : PARSE_OPT_HELP;\n>  }\n\nAnd the above matches this part, of course.\n\n"},{"id":"547421","messageId":"xmqqldbmxo9y.fsf@gitster.g","threadId":"65249","inReplyTo":"20260708001557.3581080-2-sandals@crustytoothpaste.net","subject":"Re: [PATCH v3 1/4] t1517: skip svn tests if svn is not installed","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-08T01:28:57Z","receivedAt":"2026-07-08T01:28:59Z","isPatch":true,"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> The svn tests currently assume that git-svn's option parsing will always\n> fail the tests because it exits 0 on --help, not 129.  However, in a\n> future commit, we'll expect it to exit 0 and the tests will then need to\n> be updated to succeed in some cases and fail in others.\n>\n> We therefore need to have t1517 determine whether the Subversion Perl\n> modules are present, since if they are not, git-svn will die on start\n> and then it needs to continue to expect failure.  Add a stripped down\n> version of the tests in t/lib-git-svn.sh as a prerequisite we can use\n> here for our svn tests.\n\nPerfect.  This is stripped down because what this models after is\nbroken in the statement that this version does not copy, IIRC, which\nmay deserve to be stated here to help future readers.\n"},{"id":"547444","messageId":"20260708035930.GB41684@coredump.intra.peff.net","threadId":"65249","inReplyTo":"20260708001557.3581080-1-sandals@crustytoothpaste.net","subject":"Re: [PATCH v3 0/4] parseopt: exit 0 on help","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-07-08T03:59:30Z","receivedAt":"2026-07-08T03:59:32Z","isPatch":true,"body":"On Wed, Jul 08, 2026 at 12:15:53AM +0000, brian m. carlson wrote:\n\n> Changes since v2:\n> \n> * Fix inverted condition in t1517.\n> * Stop checking for old versions of SVN Perl libraries since they are\n>   so old nobody is using them.\n> * Adjust the various cases where we choose between the error and\n>   non-error help output.\n\nThanks, I have no complaints on this version.\n\nIn the earlier thread I sketched out a hypothetical caller that might be\naffected by the change, but beyond raw speculation, I don't think we\nhave any way of knowing how common such a thing is. So I'm inclined to\nproceed and see if anybody screams during this development cycle.\n\n-Peff\n"},{"id":"547533","messageId":"xmqqzf01rznj.fsf@gitster.g","threadId":"65249","inReplyTo":"20260708035930.GB41684@coredump.intra.peff.net","subject":"Re: [PATCH v3 0/4] parseopt: exit 0 on help","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-08T20:32:00Z","receivedAt":"2026-07-08T20:32:06Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> On Wed, Jul 08, 2026 at 12:15:53AM +0000, brian m. carlson wrote:\n>\n>> Changes since v2:\n>> \n>> * Fix inverted condition in t1517.\n>> * Stop checking for old versions of SVN Perl libraries since they are\n>>   so old nobody is using them.\n>> * Adjust the various cases where we choose between the error and\n>>   non-error help output.\n>\n> Thanks, I have no complaints on this version.\n>\n> In the earlier thread I sketched out a hypothetical caller that might be\n> affected by the change, but beyond raw speculation, I don't think we\n> have any way of knowing how common such a thing is. So I'm inclined to\n> proceed and see if anybody screams during this development cycle.\n\n;-)  100% agreed on the issue of possible regressions.\n"}]}