Merge branch 'jc/optional-path'

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

* jc/optional-path:
  parseopt: values of pathname type can be prefixed with :(optional)
  config: values of pathname type can be prefixed with :(optional)
  t7500: fix GIT_EDITOR shell snippet
  t7500: make each piece more independent
This commit is contained in:
Junio C Hamano
2025-10-14 12:56:09 -07:00
7 changed files with 95 additions and 25 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 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 be specified that should _not_ be expanded, it needs to be prefixed by
`./`, like so: `./%(prefix)/bin`. `./`, 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 Variables
~~~~~~~~~ ~~~~~~~~~

View File

@@ -216,6 +216,20 @@ $ git describe --abbrev=10 HEAD # correct
$ git describe --abbrev 10 HEAD # NOT WHAT YOU MEANT $ 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 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 git_config_pathname(char **dest, const char *var, const char *value)
{ {
int is_optional;
char *path;
if (!value) if (!value)
return config_error_nonbool(var); 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); die(_("failed to expand user dir in: '%s'"), value);
if (is_optional && is_missing_file(path)) {
free(path);
return 0;
}
*dest = path;
return 0; 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 char *arg;
const int unset = flags & OPT_UNSET; const int unset = flags & OPT_UNSET;
int err;
if (unset && p->opt) if (unset && p->opt)
return error(_("%s takes no value"), optname(opt, flags)); 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: case OPTION_FILENAME:
{ {
const char *value; const char *value;
int is_optional;
FREE_AND_NULL(*(char **)opt->value);
err = 0;
if (unset) if (unset)
value = NULL; value = NULL;
else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) else if (opt->flags & PARSE_OPT_OPTARG && !p->opt)
value = (const char *) opt->defval; value = (char *)opt->defval;
else else {
err = get_arg(p, opt, flags, &value); int err = get_arg(p, opt, flags, &value);
if (err)
return err;
}
if (!value)
return 0;
if (!err) is_optional = skip_prefix(value, ":(optional)", &value);
*(char **)opt->value = fix_filename(p->prefix, value); if (!value)
return err; 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: case OPTION_CALLBACK:
{ {

View File

@@ -31,52 +31,70 @@ test_expect_success 'nonexistent template file should return error' '
echo changes >> foo && echo changes >> foo &&
git add foo && git add foo &&
( (
GIT_EDITOR="echo hello >\"\$1\"" && GIT_EDITOR="echo hello >" &&
export GIT_EDITOR && export GIT_EDITOR &&
test_must_fail git commit --template "$PWD"/notexist test_must_fail git commit --template "$PWD"/notexist
) )
' '
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_expect_success 'nonexistent template file in config should return error' '
test_config commit.template "$PWD"/notexist && test_config commit.template "$PWD"/notexist &&
( (
GIT_EDITOR="echo hello >\"\$1\"" && GIT_EDITOR="echo hello >" &&
export GIT_EDITOR && 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 >" git commit --allow-empty &&
git cat-file commit HEAD | sed -e "1,/^$/d" >actual &&
echo hello >expect &&
test_cmp expect actual
'
# From now on we'll use a template file that exists. # From now on we'll use a template file that exists.
TEMPLATE="$PWD"/template TEMPLATE="$PWD"/template
test_expect_success 'unedited template should not commit' ' test_expect_success 'unedited template should not commit' '
echo "template line" > "$TEMPLATE" && echo "template line" >"$TEMPLATE" &&
test_must_fail git commit --template "$TEMPLATE" test_must_fail git commit --allow-empty --template "$TEMPLATE"
' '
test_expect_success 'unedited template with comments should not commit' ' test_expect_success 'unedited template with comments should not commit' '
echo "# comment in template" >> "$TEMPLATE" && echo "# comment in template" >>"$TEMPLATE" &&
test_must_fail git commit --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_expect_success 'a Signed-off-by line by itself should not commit' '
( (
test_set_editor "$TEST_DIRECTORY"/t7500/add-signed-off && 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_expect_success 'adding comments to a template should not commit' '
( (
test_set_editor "$TEST_DIRECTORY"/t7500/add-comments && 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_expect_success 'adding real content to a template should commit' '
( (
test_set_editor "$TEST_DIRECTORY"/t7500/add-content && test_set_editor "$TEST_DIRECTORY"/t7500/add-content &&
git commit --template "$TEMPLATE" git commit --allow-empty --template "$TEMPLATE"
) && ) &&
commit_msg_is "template linecommit message" commit_msg_is "template linecommit message"
' '

View File

@@ -721,6 +721,19 @@ int xgethostname(char *buf, size_t len)
return ret; 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) int is_empty_or_missing_file(const char *filename)
{ {
struct stat st; 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))) __attribute__((format (printf, 2, 3)))
void write_file(const char *path, const char *fmt, ...); 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); int is_empty_or_missing_file(const char *filename);
enum fsync_action { enum fsync_action {