From 666b29b58f7c95007fe0384737c1ff506b138136 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 28 Sep 2025 17:29:14 -0400 Subject: [PATCH 1/3] t7500: make each piece more independent These tests prepare the working tree & index state to have something to be committed, and try a sequence of "test_must_fail git commit". If an earlier one did not fail by a bug, a later one will fail for a wrong reason (namely, "nothing to commit"). Give them "--allow-empty" to make sure that they would work even when there is nothing to commit by accident. Signed-off-by: Junio C Hamano Signed-off-by: Taylor Blau Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- t/t7500-commit-template-squash-signoff.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/t/t7500-commit-template-squash-signoff.sh b/t/t7500-commit-template-squash-signoff.sh index 4dca8d97a7..05cda50186 100755 --- a/t/t7500-commit-template-squash-signoff.sh +++ b/t/t7500-commit-template-squash-signoff.sh @@ -42,7 +42,7 @@ test_expect_success 'nonexistent template file in config should return error' ' ( GIT_EDITOR="echo hello >\"\$1\"" && export GIT_EDITOR && - test_must_fail git commit + test_must_fail git commit --allow-empty ) ' @@ -50,33 +50,33 @@ test_expect_success 'nonexistent template file in config should return error' ' TEMPLATE="$PWD"/template test_expect_success 'unedited template should not commit' ' - echo "template line" > "$TEMPLATE" && - test_must_fail git commit --template "$TEMPLATE" + echo "template line" >"$TEMPLATE" && + test_must_fail git commit --allow-empty --template "$TEMPLATE" ' test_expect_success 'unedited template with comments should not commit' ' - echo "# comment in template" >> "$TEMPLATE" && - test_must_fail git commit --template "$TEMPLATE" + echo "# comment in template" >>"$TEMPLATE" && + test_must_fail git commit --allow-empty --template "$TEMPLATE" ' test_expect_success 'a Signed-off-by line by itself should not commit' ' ( test_set_editor "$TEST_DIRECTORY"/t7500/add-signed-off && - test_must_fail git commit --template "$TEMPLATE" + test_must_fail git commit --allow-empty --template "$TEMPLATE" ) ' test_expect_success 'adding comments to a template should not commit' ' ( test_set_editor "$TEST_DIRECTORY"/t7500/add-comments && - test_must_fail git commit --template "$TEMPLATE" + test_must_fail git commit --allow-empty --template "$TEMPLATE" ) ' test_expect_success 'adding real content to a template should commit' ' ( test_set_editor "$TEST_DIRECTORY"/t7500/add-content && - git commit --template "$TEMPLATE" + git commit --allow-empty --template "$TEMPLATE" ) && commit_msg_is "template linecommit message" ' From f744457cca4ecc00d829cffaae2abd1b9d27a3b8 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 28 Sep 2025 17:29:15 -0400 Subject: [PATCH 2/3] config: values of pathname type can be prefixed with :(optional) Sometimes people want to specify additional configuration data as "best effort" basis. Maybe commit.template configuration file points at somewhere in ~/template/ but on a particular system, the file may not exist and the user may be OK without using the template in such a case. When the value given to a configuration variable whose type is pathname wants to signal such an optional file, it can be marked by prepending ":(optional)" in front of it. Such a setting that is marked optional would avoid getting the command barf for a missing file, as an optional configuration setting that names a missing file is not even seen. cf. Signed-off-by: Junio C Hamano Signed-off-by: Taylor Blau Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- Documentation/config.adoc | 4 +++- config.c | 16 ++++++++++++++-- t/t7500-commit-template-squash-signoff.sh | 9 +++++++++ wrapper.c | 13 +++++++++++++ wrapper.h | 4 +++- 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Documentation/config.adoc b/Documentation/config.adoc index cc769251be..7301ced836 100644 --- a/Documentation/config.adoc +++ b/Documentation/config.adoc @@ -358,7 +358,9 @@ compiled without runtime prefix support, the compiled-in prefix will be substituted instead. In the unlikely event that a literal path needs to be specified that should _not_ be expanded, it needs to be prefixed by `./`, like so: `./%(prefix)/bin`. - ++ +If prefixed with `:(optional)`, the configuration variable is treated +as if it does not exist, if the named path does not exist. Variables ~~~~~~~~~ diff --git a/config.c b/config.c index 97ffef4270..73fc74c8fa 100644 --- a/config.c +++ b/config.c @@ -1279,11 +1279,23 @@ int git_config_string(char **dest, const char *var, const char *value) int git_config_pathname(char **dest, const char *var, const char *value) { + int is_optional; + char *path; + if (!value) return config_error_nonbool(var); - *dest = interpolate_path(value, 0); - if (!*dest) + + is_optional = skip_prefix(value, ":(optional)", &value); + path = interpolate_path(value, 0); + if (!path) die(_("failed to expand user dir in: '%s'"), value); + + if (is_optional && is_missing_file(path)) { + free(path); + return 0; + } + + *dest = path; return 0; } diff --git a/t/t7500-commit-template-squash-signoff.sh b/t/t7500-commit-template-squash-signoff.sh index 05cda50186..366f7f23b3 100755 --- a/t/t7500-commit-template-squash-signoff.sh +++ b/t/t7500-commit-template-squash-signoff.sh @@ -46,6 +46,15 @@ test_expect_success 'nonexistent template file in config should return error' ' ) ' +test_expect_success 'nonexistent optional template file in config' ' + test_config commit.template ":(optional)$PWD"/notexist && + ( + GIT_EDITOR="echo hello >\"\$1\"" && + export GIT_EDITOR && + git commit --allow-empty + ) +' + # From now on we'll use a template file that exists. TEMPLATE="$PWD"/template diff --git a/wrapper.c b/wrapper.c index 2f00d2ac87..3d507d4204 100644 --- a/wrapper.c +++ b/wrapper.c @@ -721,6 +721,19 @@ int xgethostname(char *buf, size_t len) return ret; } +int is_missing_file(const char *filename) +{ + struct stat st; + + if (stat(filename, &st) < 0) { + if (errno == ENOENT) + return 1; + die_errno(_("could not stat %s"), filename); + } + + return 0; +} + int is_empty_or_missing_file(const char *filename) { struct stat st; diff --git a/wrapper.h b/wrapper.h index 7df824e34a..44a8597ac3 100644 --- a/wrapper.h +++ b/wrapper.h @@ -66,7 +66,9 @@ void write_file_buf(const char *path, const char *buf, size_t len); __attribute__((format (printf, 2, 3))) void write_file(const char *path, const char *fmt, ...); -/* Return 1 if the file is empty or does not exists, 0 otherwise. */ +/* Return 1 if the file does not exist, 0 otherwise. */ +int is_missing_file(const char *filename); +/* Return 1 if the file is empty or does not exist, 0 otherwise. */ int is_empty_or_missing_file(const char *filename); enum fsync_action { From ce61a885c9543c5796d63dc45dcaa9d1e2eef052 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 28 Sep 2025 17:29:16 -0400 Subject: [PATCH 3/3] 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 Signed-off-by: Taylor Blau Signed-off-by: D. Ben Knoble Signed-off-by: Junio C Hamano --- Documentation/gitcli.adoc | 14 ++++++++++ parse-options.c | 31 +++++++++++++++-------- t/t7500-commit-template-squash-signoff.sh | 10 ++++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/Documentation/gitcli.adoc b/Documentation/gitcli.adoc index 1ea681b59d..ef2a0a399d 100644 --- a/Documentation/gitcli.adoc +++ b/Documentation/gitcli.adoc @@ -216,6 +216,20 @@ $ git describe --abbrev=10 HEAD # correct $ git describe --abbrev 10 HEAD # NOT WHAT YOU MEANT ---------------------------- + +Magic filename options +~~~~~~~~~~~~~~~~~~~~~~ +Options that take a filename allow a prefix `:(optional)`. For example: + +---------------------------- +git commit -F :(optional)COMMIT_EDITMSG +# if COMMIT_EDITMSG does not exist, equivalent to +git commit +---------------------------- + +Like with configuration values, if the named file is missing Git behaves as if +the option was not given at all. See "Values" in linkgit:git-config[1]. + NOTES ON FREQUENTLY CONFUSED OPTIONS ------------------------------------ diff --git a/parse-options.c b/parse-options.c index 5224203ffe..4faf66023a 100644 --- a/parse-options.c +++ b/parse-options.c @@ -133,7 +133,6 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, { const char *arg; const int unset = flags & OPT_UNSET; - int err; if (unset && p->opt) 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, case OPTION_FILENAME: { const char *value; - - FREE_AND_NULL(*(char **)opt->value); - - err = 0; + int is_optional; if (unset) value = NULL; else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) - value = (const char *) opt->defval; - else - err = get_arg(p, opt, flags, &value); + value = (char *)opt->defval; + else { + int err = get_arg(p, opt, flags, &value); + if (err) + return err; + } + if (!value) + return 0; - if (!err) - *(char **)opt->value = fix_filename(p->prefix, value); - return err; + is_optional = skip_prefix(value, ":(optional)", &value); + if (!value) + is_optional = 0; + value = fix_filename(p->prefix, value); + if (is_optional && is_empty_or_missing_file(value)) { + free((char *)value); + } else { + FREE_AND_NULL(*(char **)opt->value); + *(const char **)opt->value = value; + } + return 0; } case OPTION_CALLBACK: { diff --git a/t/t7500-commit-template-squash-signoff.sh b/t/t7500-commit-template-squash-signoff.sh index 366f7f23b3..c065f12baf 100755 --- a/t/t7500-commit-template-squash-signoff.sh +++ b/t/t7500-commit-template-squash-signoff.sh @@ -37,6 +37,16 @@ test_expect_success 'nonexistent template file should return error' ' ) ' +test_expect_success 'nonexistent optional template file on command line' ' + echo changes >> foo && + git add foo && + ( + GIT_EDITOR="echo hello >\"\$1\"" && + export GIT_EDITOR && + git commit --template ":(optional)$PWD/notexist" + ) +' + test_expect_success 'nonexistent template file in config should return error' ' test_config commit.template "$PWD"/notexist && (