Merge branch 'ap/var-broken-down-idents' into next

The 'git var' command has been updated to output individual components
of the author and committer identities, take multiple keys on the
command-line, and support NUL-delimited output with the '-z' option.

* ap/var-broken-down-idents:
  var: add broken-out identity variables
  var: accept more than one variable
  var: add "-z" output mode
  var: represent multi-valued variables with a string_list
next
Junio C Hamano 2026-10-07 07:29:20 -07:00
commit 6ae874acdf
3 changed files with 403 additions and 60 deletions

View File

@ -9,12 +9,23 @@ git-var - Show a Git logical variable
SYNOPSIS
--------
[synopsis]
git var (-l | <variable>)
git var [-z] -l
git var [-z] <variable>...

DESCRIPTION
-----------
Prints a Git logical variable. Exits with code 1 if the variable has
no value.
Prints Git logical variables. When a single variable is requested, its
bare value is printed, and the command exits with code 1 if it has no
value. When multiple variables are requested, they are printed as
`VARIABLE=value` pairs, separated by newlines; a variable that has no
value is omitted from the output, which is not by itself an error.

If `-z` is given, the output format changes depending on the mode:

* With a single variable, the bare value is terminated by a NUL byte.
* With multiple variables or with `-l`, the variable name and value are
separated by a newline, and each entry is terminated by a NUL byte
(`VARIABLE\nvalue\0`), in the same way as `git config list -z`.

OPTIONS
-------
@ -24,19 +35,53 @@ OPTIONS
as well. (However, the configuration variables listing functionality
is deprecated in favor of `git config list`.)

`-z`::
Terminate entries with NUL instead of newline. When used with
`-l` or when multiple variables are requested, the variable name
and its value are separated by a newline, and each entry is
terminated with a NUL byte.

EXAMPLES
--------
$ git var GIT_AUTHOR_IDENT
Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600
* Get the author identity:
+
------------
$ git var GIT_AUTHOR_IDENT
Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600
------------

* Get the author name and email:
+
------------
$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
GIT_AUTHOR_NAME=Eric W. Biederman
GIT_AUTHOR_EMAIL=ebiederm@lnxi.com
------------


VARIABLES
---------
`GIT_AUTHOR_IDENT`::
The author of a piece of code.
`GIT_AUTHOR_NAME`::
`GIT_AUTHOR_EMAIL`::
`GIT_AUTHOR_DATE`::
The authorship information that would be recorded in the
resulting commit object if you ran `git commit` right now.
`GIT_AUTHOR_IDENT` consists of the author's name, e-mail
address, and timestamp+timezone. These three pieces of
information are available separately as `GIT_AUTHOR_NAME`,
`GIT_AUTHOR_EMAIL`, and `GIT_AUTHOR_DATE`.

`GIT_COMMITTER_IDENT`::
The person who put a piece of code into Git.
`GIT_COMMITTER_NAME`::
`GIT_COMMITTER_EMAIL`::
`GIT_COMMITTER_DATE`::
The committer information that would be recorded in the
resulting commit object if you ran `git commit` right now.
`GIT_COMMITTER_IDENT` consists of the committer's name, e-mail
address, and timestamp+timezone. These three pieces of
information are available separately as `GIT_COMMITTER_NAME`,
`GIT_COMMITTER_EMAIL`, and `GIT_COMMITTER_DATE`.

`GIT_EDITOR`::
Text editor for use by Git commands. The value is meant to be
@ -83,11 +128,14 @@ endif::git-default-pager[]

`GIT_CONFIG_GLOBAL`::
The path to the global (per-user) configuration files, if any.
This variable can have multiple values.

Most path values contain only one value. However, some can contain multiple
values, which are separated by newlines, and are listed in order from highest to
lowest priority. Callers should be prepared for any such path value to contain
multiple items.
values, which are separated by newlines (or NUL bytes if `-z` is given),
and are listed in order from highest to lowest priority. When querying
multiple variables (or using `-l`), each value is output as a separate
`VARIABLE=value` entry (or `VARIABLE\nvalue\0` with `-z`). Callers should
be prepared for any such path value to contain multiple items.

Note that paths are printed even if they do not exist, but not if they are
disabled by other environment variables.

View File

@ -14,23 +14,95 @@
#include "environment.h"
#include "ident.h"
#include "pager.h"
#include "refs.h"
#include "parse-options.h"
#include "path.h"
#include "strbuf.h"
#include "refs.h"
#include "run-command.h"
#include "strbuf.h"
#include "string-list.h"

static const char var_usage[] = "git var (-l | <variable>)";
static const char * const var_usage[] = {
N_("git var [-z] -l"),
N_("git var [-z] <variable>..."),
NULL
};

enum ident_part {
IDENT_NAME,
IDENT_MAIL,
IDENT_DATE,
};

static char *ident_part(const char *ident, enum ident_part part)
{
struct ident_split split;

if (!ident)
return NULL;
if (split_ident_line(&split, ident, strlen(ident)))
return NULL;

switch (part) {
case IDENT_NAME:
if (!split.name_begin || !split.name_end)
BUG("split_ident_line() gave NULL names???");
return xmemdupz(split.name_begin,
split.name_end - split.name_begin);
case IDENT_MAIL:
if (!split.mail_begin || !split.mail_end)
BUG("split_ident_line() gave NULL mail???");
return xmemdupz(split.mail_begin,
split.mail_end - split.mail_begin);
case IDENT_DATE:
if (!split.date_begin || !split.tz_end)
BUG("split_ident_line() gave NULL date/tz???");
return xmemdupz(split.date_begin,
split.tz_end - split.date_begin);
default:
BUG("unknown ident_part %d", part);
}
}

static char *committer(int ident_flag)
{
return xstrdup_or_null(git_committer_info(ident_flag));
}

static char *committer_name(int ident_flag)
{
return ident_part(git_committer_info(ident_flag), IDENT_NAME);
}

static char *committer_email(int ident_flag)
{
return ident_part(git_committer_info(ident_flag), IDENT_MAIL);
}

static char *committer_date(int ident_flag)
{
return ident_part(git_committer_info(ident_flag), IDENT_DATE);
}

static char *author(int ident_flag)
{
return xstrdup_or_null(git_author_info(ident_flag));
}

static char *author_name(int ident_flag)
{
return ident_part(git_author_info(ident_flag), IDENT_NAME);
}

static char *author_email(int ident_flag)
{
return ident_part(git_author_info(ident_flag), IDENT_MAIL);
}

static char *author_date(int ident_flag)
{
return ident_part(git_author_info(ident_flag), IDENT_DATE);
}

static char *editor(int ident_flag UNUSED)
{
return xstrdup_or_null(git_editor());
@ -90,45 +162,61 @@ static char *git_config_val_system(int ident_flag UNUSED)
return NULL;
}

static char *git_config_val_global(int ident_flag UNUSED)
static void git_config_val_global(struct string_list *list)
{
struct strbuf buf = STRBUF_INIT;
char *user, *xdg;
size_t unused;

git_global_config_paths(&user, &xdg);
if (xdg && *xdg) {
normalize_path_copy(xdg, xdg);
strbuf_addf(&buf, "%s\n", xdg);
string_list_append(list, xdg);
}
if (user && *user) {
normalize_path_copy(user, user);
strbuf_addf(&buf, "%s\n", user);
string_list_append(list, user);
}
free(xdg);
free(user);
strbuf_trim_trailing_newline(&buf);
if (buf.len == 0) {
strbuf_release(&buf);
return NULL;
}
return strbuf_detach(&buf, &unused);
}

struct git_var {
const char *name;
char *(*read)(int);
int multivalued;
void (*multiread)(struct string_list *);
};
static struct git_var git_vars[] = {
{
.name = "GIT_COMMITTER_IDENT",
.read = committer,
},
{
.name = "GIT_COMMITTER_NAME",
.read = committer_name,
},
{
.name = "GIT_COMMITTER_EMAIL",
.read = committer_email,
},
{
.name = "GIT_COMMITTER_DATE",
.read = committer_date,
},
{
.name = "GIT_AUTHOR_IDENT",
.read = author,
},
{
.name = "GIT_AUTHOR_NAME",
.read = author_name,
},
{
.name = "GIT_AUTHOR_EMAIL",
.read = author_email,
},
{
.name = "GIT_AUTHOR_DATE",
.read = author_date,
},
{
.name = "GIT_EDITOR",
.read = editor,
@ -163,8 +251,7 @@ static struct git_var git_vars[] = {
},
{
.name = "GIT_CONFIG_GLOBAL",
.read = git_config_val_global,
.multivalued = 1,
.multiread = git_config_val_global,
},
{
.name = "",
@ -172,31 +259,36 @@ static struct git_var git_vars[] = {
},
};

static void list_vars(void)
static void list_vars(int nul_term)
{
struct git_var *ptr;
char *val;
char delim = nul_term ? '\n' : '=';
char term = nul_term ? '\0' : '\n';

for (ptr = git_vars; ptr->read; ptr++)
if ((val = ptr->read(0))) {
if (ptr->multivalued && *val) {
struct string_list list = STRING_LIST_INIT_DUP;
for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
if (ptr->read) {
char *val = ptr->read(0);

string_list_split(&list, val, "\n", -1);
for (size_t i = 0; i < list.nr; i++)
printf("%s=%s\n", ptr->name, list.items[i].string);
string_list_clear(&list, 0);
} else {
printf("%s=%s\n", ptr->name, val);
if (val) {
printf("%s%c%s%c", ptr->name, delim, val, term);
free(val);
}
free(val);
} else {
struct string_list list = STRING_LIST_INIT_DUP;

ptr->multiread(&list);
for (size_t i = 0; i < list.nr; i++)
printf("%s%c%s%c", ptr->name, delim,
list.items[i].string, term);
string_list_clear(&list, 0);
}
}
}

static const struct git_var *get_git_var(const char *var)
{
struct git_var *ptr;
for (ptr = git_vars; ptr->read; ptr++) {
for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
if (strcmp(var, ptr->name) == 0) {
return ptr;
}
@ -207,42 +299,92 @@ static const struct git_var *get_git_var(const char *var)
static int show_config(const char *var, const char *value,
const struct config_context *ctx, void *cb)
{
int *nul_term = cb;
char delim = *nul_term ? '\n' : '=';
char term = *nul_term ? '\0' : '\n';

if (value)
printf("%s=%s\n", var, value);
printf("%s%c%s%c", var, delim, value, term);
else
printf("%s\n", var);
printf("%s%c", var, term);
return git_default_config(var, value, ctx, cb);
}

int cmd_var(int argc,
const char **argv,
const char *prefix UNUSED,
const char *prefix,
struct repository *repo UNUSED)
{
const struct git_var *git_var;
char *val;
int list = 0;
int nul_term = 0;
char delim;
char term;
struct option options[] = {
OPT_BOOL('l', NULL, &list,
N_("list all variables")),
OPT_BOOL('z', NULL, &nul_term,
N_("terminate entries with NUL")),
OPT_END(),
};

show_usage_if_asked(argc, argv, var_usage);
if (argc != 2)
usage(var_usage);
argc = parse_options(argc, argv, prefix, options,
var_usage, PARSE_OPT_STOP_AT_NON_OPTION);

if (strcmp(argv[1], "-l") == 0) {
repo_config(the_repository, show_config, NULL);
list_vars();
if (list) {
if (argc)
usage_with_options(var_usage, options);
repo_config(the_repository, show_config, &nul_term);
list_vars(nul_term);
return 0;
}

if (!argc)
usage_with_options(var_usage, options);

repo_config(the_repository, git_default_config, NULL);

git_var = get_git_var(argv[1]);
if (!git_var)
usage(var_usage);
delim = nul_term ? '\n' : '=';
term = nul_term ? '\0' : '\n';

val = git_var->read(IDENT_STRICT);
if (!val)
return 1;
for (int i = 0; i < argc; i++) {
const struct git_var *git_var = get_git_var(argv[i]);

printf("%s\n", val);
free(val);
if (!git_var)
usage_with_options(var_usage, options);

if (git_var->read) {
char *val = git_var->read(IDENT_STRICT);

if (!val) {
if (argc == 1)
return 1;
continue;
}
if (argc == 1)
printf("%s%c", val, term);
else
printf("%s%c%s%c", git_var->name, delim,
val, term);
free(val);
} else {
struct string_list list = STRING_LIST_INIT_DUP;

git_var->multiread(&list);
if (argc == 1 && !list.nr) {
string_list_clear(&list, 0);
return 1;
}
for (size_t j = 0; j < list.nr; j++) {
if (argc == 1)
printf("%s%c", list.items[j].string,
term);
else
printf("%s%c%s%c", git_var->name, delim,
list.items[j].string, term);
}
string_list_clear(&list, 0);
}
}

return 0;
}

View File

@ -276,4 +276,157 @@ test_expect_success '`git var -l` works even without HOME' '
)
'

test_expect_success 'get a variable with -z' '
printf "%sQ" "$(git var GIT_AUTHOR_IDENT)" >expect &&
git var -z GIT_AUTHOR_IDENT >actual.raw &&
nul_to_q <actual.raw >actual &&
test_cmp expect actual
'

test_expect_success 'get multi-valued variable with -z' '
TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" git var -z GIT_CONFIG_GLOBAL >actual.raw &&
printf "%sQ%sQ" "$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expect &&
nul_to_q <actual.raw >actual &&
test_cmp expect actual
'

test_expect_success 'git var -l -z' '
git var -l -z >actual &&
tr "\0" "\n" <actual >actual.lines &&
git var GIT_AUTHOR_IDENT >expect &&
sed -n "/^GIT_AUTHOR_IDENT$/{n;p;}" actual.lines >actual.author &&
test_cmp expect actual.author &&
echo false >expect &&
sed -n "/^core\.bare$/{n;p;}" actual.lines >actual.bare &&
test_cmp expect actual.bare
'

test_expect_success 'options must precede variable arguments' '
test_must_fail git var GIT_AUTHOR_IDENT -z
'

test_expect_success 'get multiple variables' '
test_tick &&
cat >expect <<-EOF &&
GIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE
GIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
EOF
git var GIT_AUTHOR_IDENT GIT_COMMITTER_IDENT >actual &&
test_cmp expect actual
'

test_expect_success 'get multiple variables with -z' '
test_tick &&
printf "GIT_AUTHOR_IDENT\n%sQGIT_COMMITTER_IDENT\n%sQ" \
"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE" \
"$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE" >expect &&
git var -z GIT_AUTHOR_IDENT GIT_COMMITTER_IDENT >actual.raw &&
nul_to_q <actual.raw >actual &&
test_cmp expect actual
'

test_expect_success 'get multiple variables including multi-valued variable' '
test_tick &&
TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
cat >expect <<-EOF &&
GIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE
GIT_CONFIG_GLOBAL=$TRASHDIR/foo/git/config
GIT_CONFIG_GLOBAL=$TRASHDIR/.gitconfig
GIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
EOF
HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
git var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL GIT_COMMITTER_IDENT >actual &&
test_cmp expect actual
'

test_expect_success 'get multiple variables including multi-valued variable with -z' '
test_tick &&
TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
printf "GIT_AUTHOR_IDENT\n%sQGIT_CONFIG_GLOBAL\n%sQGIT_CONFIG_GLOBAL\n%sQ" \
"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE" \
"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expect &&
HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
git var -z GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL >actual.raw &&
nul_to_q <actual.raw >actual &&
test_cmp expect actual
'

test_expect_success 'variable without a value is omitted but is not an error' '
test_tick &&
cat >expect <<-EOF &&
GIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE
GIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
EOF
test_env GIT_CONFIG_GLOBAL= \
git var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL GIT_COMMITTER_IDENT >actual &&
test_cmp expect actual
'

test_expect_success 'a single variable without a value still exits with 1' '
test_env GIT_CONFIG_GLOBAL= test_expect_code 1 git var GIT_CONFIG_GLOBAL >out &&
test_must_be_empty out
'

test_expect_success 'unknown variable is a usage error' '
test_must_fail git var GIT_AUTHOR_IDENT NO_SUCH_VARIABLE 2>err &&
test_grep usage err
'

test_expect_success 'get author identity components' '
test_tick &&
echo "$GIT_AUTHOR_NAME" >expect.name &&
echo "$GIT_AUTHOR_EMAIL" >expect.email &&
echo "$GIT_AUTHOR_DATE" >expect.date &&
git var GIT_AUTHOR_NAME >actual.name &&
git var GIT_AUTHOR_EMAIL >actual.email &&
git var GIT_AUTHOR_DATE >actual.date &&
test_cmp expect.name actual.name &&
test_cmp expect.email actual.email &&
test_cmp expect.date actual.date
'

test_expect_success 'get committer identity components' '
test_tick &&
echo "$GIT_COMMITTER_NAME" >expect.name &&
echo "$GIT_COMMITTER_EMAIL" >expect.email &&
echo "$GIT_COMMITTER_DATE" >expect.date &&
git var GIT_COMMITTER_NAME >actual.name &&
git var GIT_COMMITTER_EMAIL >actual.email &&
git var GIT_COMMITTER_DATE >actual.date &&
test_cmp expect.name actual.name &&
test_cmp expect.email actual.email &&
test_cmp expect.date actual.date
'

test_expect_success !FAIL_PREREQS,!AUTOIDENT 'identity components are strict' '
(
sane_unset GIT_COMMITTER_NAME &&
sane_unset GIT_COMMITTER_EMAIL &&
test_must_fail git var GIT_COMMITTER_NAME
)
'

test_expect_success 'get several identity components at once' '
test_tick &&
cat >expect <<-EOF &&
GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME
GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL
GIT_COMMITTER_NAME=$GIT_COMMITTER_NAME
GIT_COMMITTER_EMAIL=$GIT_COMMITTER_EMAIL
EOF
git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&
test_cmp expect actual
'

test_expect_success 'git var -l lists the identity components' '
git var -l >actual &&
test_grep "^GIT_AUTHOR_NAME=" actual &&
test_grep "^GIT_AUTHOR_EMAIL=" actual &&
test_grep "^GIT_AUTHOR_DATE=" actual &&
test_grep "^GIT_COMMITTER_NAME=" actual &&
test_grep "^GIT_COMMITTER_EMAIL=" actual &&
test_grep "^GIT_COMMITTER_DATE=" actual
'

test_done