parseopt: values of pathname type can be prefixed with :(optional)

In the previous step, we introduced an optional filename that can be given to a configuration variable, and nullify the fact that such a configuration setting even existed if the named path is missing or empty. Let's do the same for command line options that name a pathname. Signed-off-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Taylor Blau <me@ttaylorr.com> Signed-off-by: D. Ben Knoble <ben.knoble+github@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Junio C Hamano committed Sep 28, 2025 at 17:29 UTC ccfcaf399ffcc91553395a8de8e833e7685e7cc2
3 files changed +44 -11
Documentation/gitcli.adoc
+14
@@ -216,6 +216,20 @@ $ git describe --abbrev=10 HEAD # correct
216 $ git describe --abbrev 10 HEAD # NOT WHAT YOU MEANT
217 ----------------------------
218
219 +
220 +Magic filename options
221 +~~~~~~~~~~~~~~~~~~~~~~
222 +Options that take a filename allow a prefix `:(optional)`. For example:
223 +
224 +----------------------------
225 +git commit -F :(optional)COMMIT_EDITMSG
226 +# if COMMIT_EDITMSG does not exist, equivalent to
227 +git commit
228 +----------------------------
229 +
230 +Like with configuration values, if the named file is missing Git behaves as if
231 +the option was not given at all. See "Values" in linkgit:git-config[1].
232 +
233 NOTES ON FREQUENTLY CONFUSED OPTIONS
234 ------------------------------------
235
parse-options.c
+20 -11
@@ -133,7 +133,6 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,
133 {
134 const char *arg;
135 const int unset = flags & OPT_UNSET;
136 - int err;
136
137 if (unset && p->opt)
138 return error(_("%s takes no value"), optname(opt, flags));
@@ -209,21 +208,31 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,
208 case OPTION_FILENAME:
209 {
210 const char *value;
212 -
213 - FREE_AND_NULL(*(char **)opt->value);
214 -
215 - err = 0;
211 + int is_optional;
212
213 if (unset)
214 value = NULL;
215 else if (opt->flags & PARSE_OPT_OPTARG && !p->opt)
220 - value = (const char *) opt->defval;
221 - else
222 - err = get_arg(p, opt, flags, &value);
216 + value = (char *)opt->defval;
217 + else {
218 + int err = get_arg(p, opt, flags, &value);
219 + if (err)
220 + return err;
221 + }
222 + if (!value)
223 + return 0;
224
224 - if (!err)
225 - *(char **)opt->value = fix_filename(p->prefix, value);
226 - return err;
225 + is_optional = skip_prefix(value, ":(optional)", &value);
226 + if (!value)
227 + is_optional = 0;
228 + value = fix_filename(p->prefix, value);
229 + if (is_optional && is_empty_or_missing_file(value)) {
230 + free((char *)value);
231 + } else {
232 + FREE_AND_NULL(*(char **)opt->value);
233 + *(const char **)opt->value = value;
234 + }
235 + return 0;
236 }
237 case OPTION_CALLBACK:
238 {
t/t7500-commit-template-squash-signoff.sh
+10
@@ -37,6 +37,16 @@ test_expect_success 'nonexistent template file should return error' '
37 )
38 '
39
40 +test_expect_success 'nonexistent optional template file on command line' '
41 + echo changes >> foo &&
42 + git add foo &&
43 + (
44 + GIT_EDITOR="echo hello >\"\$1\"" &&
45 + export GIT_EDITOR &&
46 + git commit --template ":(optional)$PWD/notexist"
47 + )
48 +'
49 +
50 test_expect_success 'nonexistent template file in config should return error' '
51 test_config commit.template "$PWD"/notexist &&
52 (