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
main
Junio C Hamano 2025-10-14 12:56:09 -07:00
commit 44dee53a30
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,22 +208,32 @@ 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)
if (!err)
*(char **)opt->value = fix_filename(p->prefix, value);
return err; return err;
} }
if (!value)
return 0;

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: case OPTION_CALLBACK:
{ {
const char *p_arg = NULL; const char *p_arg = NULL;

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 {