Merge branch 'jc/optional-path' into jch

Configuration variables that take a pathname as a value
(e.g. blame.ignorerevsfile) can be marked as optional by prefixing
":(optoinal)" before its value.

Comments?

* jc/optional-path:
  parseopt: values of pathname type can be prefixed with :(optional)
  config: values of pathname type can be prefixed with :(optional)
  t7500: make each piece more independent
Junio C Hamano 2025-10-06 10:25:24 -07:00
commit 74bc6fc513
7 changed files with 94 additions and 23 deletions

View File

@ -357,7 +357,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
~~~~~~~~~

View File

@ -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
------------------------------------


View File

@ -1278,11 +1278,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;
}


View File

@ -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:
{

View File

@ -37,12 +37,31 @@ 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 &&
(
GIT_EDITOR="echo hello >\"\$1\"" &&
export GIT_EDITOR &&
test_must_fail git commit
test_must_fail git commit --allow-empty
)
'

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
)
'

@ -50,33 +69,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"
'

View File

@ -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;

View File

@ -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 {