Git development
 help / color / mirror / Atom feed
From: Junio C Hamano <gitster@pobox.com>
To: "Andrew Pleeter via GitGitGadget" <gitgitgadget@gmail.com>
Cc: git@vger.kernel.org,
	 "brian m. carlson" <sandals@crustytoothpaste.net>,
	Jeff King <peff@peff.net>,
	 Andrew Pleeter <andrewpleeter@gmail.com>
Subject: Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z
Date: Thu, 03 Sep 2026 10:40:48 -0700	[thread overview]
Message-ID: <xmqqbjaecjxb.fsf@gitster.g> (raw)
In-Reply-To: <pull.2388.v3.git.git.1788403792962.gitgitgadget@gmail.com> (Andrew Pleeter via GitGitGadget's message of "Thu, 03 Sep 2026 02:49:52 +0000")

"Andrew Pleeter via GitGitGadget" <gitgitgadget@gmail.com> writes:

> - Allow passing multiple variable arguments (e.g., 'git var
>   GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
>   sequentially.

Yes.  This would be really useful if anybody wants to make it a
component in serious scripting.

>  Documentation/git-var.adoc |  43 ++++++++-
>  builtin/var.c              | 189 ++++++++++++++++++++++++++++++++-----
>  t/t0007-git-var.sh         |  77 +++++++++++++++
>  3 files changed, 284 insertions(+), 25 deletions(-)
>
> diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
> index 697c10aded..30bf2c12a8 100644
> --- a/Documentation/git-var.adoc
> +++ b/Documentation/git-var.adoc
> @@ -9,7 +9,7 @@ git-var - Show a Git logical variable
>  SYNOPSIS
>  --------
>  [synopsis]
> -git var (-l | <variable>)
> +git var (-l [-z] | [-z] <variable>...)

It might make sense to split the two vastly different modes of
operation into separate lines in the synopsis, i.e.,

    git var [-z] -l
    git var [-z] <variable>...

>  VARIABLES
>  ---------
>  `GIT_AUTHOR_IDENT`::
>      The author of a piece of code.

This shows that " of a piece of code" was inherited from the
original, and while it is not your fault, the phrasing is awkward
and misleading.  When a user runs:

    $ git var GIT_AUTHOR_DATE

the command does not look at any particular piece of code or report
when it was written.  We are better off without " of a piece of
code" in this entry (unless we can replace it with something better)
and in all the other new entries.

> +`GIT_AUTHOR_NAME`::
> +    The name of the author of a piece of code.
> +
> +`GIT_AUTHOR_EMAIL`::
> +    The email of the author of a piece of code.
> +
> +`GIT_AUTHOR_DATE`::
> +    The date and timezone of the author of a piece of code.

So let's discuss what we can replace "of a piece of code" with.

The original motivation for GIT_AUTHOR_IDENT (and similarly
GIT_COMMITTER_IDENT) was to give scripts a way to construct a
string that they can pass to 'git hash-object -t commit' to create a
commit object.  GIT_AUTHOR_IDENT is what would appear on the
"author" line (and GIT_COMMITTER_IDENT on the "committer" line) in
the resulting commit object if you were to run 'git commit' right
now.  IDENT has a clear meaning (given above); the other three are
individual fields broken out of it.

Explaining these four (IDENT, NAME, EMAIL, DATE) along those lines
would make it easier for readers to understand.

Here is my attempt:

    GIT_AUTHOR_IDENT::
    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.

Similarly for GIT_COMMITTER_*.


> +`GIT_DEFAULT_KEY`::
> +    The default commit signing key ID or fingerprint, if configured or enabled.

In the same spirit as the explanation of 'GIT_AUTHOR_IDENT' above,
we could describe this as "the key that would be used to sign the
resulting commit if you were to run 'git commit' right now".  I
wonder if that is easier to reason about.

Yes, I am suggesting moving away from using "DEFAULT" in the name.

> +static char *ident_part(const char *ident, char part)

Use an enum instead of 'char part', unless you derive that single
character directly from end-user input.

> +{
> +	struct ident_split split;
> +
> +	if (!ident)
> +		return NULL;
> +	if (split_ident_line(&split, ident, strlen(ident)))
> +		return NULL;
> +
> +	switch (part) {
> +	case 'n':
> +		if (!split.name_begin || !split.name_end)
> +			return NULL;
> +		return xmemdupz(split.name_begin, split.name_end - split.name_begin);
> +	case 'e':
> +		if (!split.mail_begin || !split.mail_end)
> +			return NULL;
> +		return xmemdupz(split.mail_begin, split.mail_end - split.mail_begin);
> +	case 'd':
> +		if (!split.date_begin)
> +			return NULL;
> +		if (split.tz_end)
> +			return xmemdupz(split.date_begin, split.tz_end - split.date_begin);
> +		if (split.date_end)
> +			return xmemdupz(split.date_begin, split.date_end - split.date_begin);
> +		return NULL;

The line is getting overly long.  Aim to wrap at around ~70 columns.

> +	default:
> +		return NULL;
> +	}
> +}

> +static char *default_key(int ident_flag UNUSED)
> +{
> +	int gpgsign = 0;
> +	char *signing_key = NULL;
> +
> +	if (repo_config_get_string(the_repository, "user.signingkey", &signing_key) == 0 && signing_key && *signing_key)
> +		return signing_key;

An overly long line.

Do not make numeric comparison with 0 as "== 0" or "!= 0".

	if (!repo_config_get_string(the_repository,
				    "user.signingkey", &signing_key) &&
	    signing_key && *signing_key)
		return signing_key;

The reason why you pretend that user.signingkey is not even defined
when it is defined to be an empty string is because otherwise there
won't be a way for a user to override a key defined in per-user
configuration file in per-repository configuration file.  It may
deserve an in-code comment to explain that, or is it too obvious?

I dunno.

By the way, by using repo_config_get_string(), you are willing to
give an error message from config_error_nonbool() when the user has
user.signingkey mistakenly defined as a valueless true, i.e.,

	[user]
		name = A U Thor
		email = au@th.or
		signingkey

I think it is OK to give an error message here, to give the user a
chance to notice and fix the mistake in their configuration file,
and keep going as if the entry did not even exist.  I just wanted to
make sure we are all aware that it is what our new code is doing.

> +	free(signing_key);
> +
> +	if (repo_config_get_bool(the_repository, "commit.gpgsign", &gpgsign) == 0 && gpgsign)
> +		return get_signing_key_id();

I am not sure if this is a good idea.  Whether the "git commit"
command is told to trigger GPG signing via the configuration
variable, or via the "--gpg-sign" command line option, wouldn't the
signing key returned by get_signing_key_id() be used either way?

In other words, I can explain why the previous entry checks the
"user.signingkey" configuration variable, but I cannot give an
explanation why we check the "commit.gpgsign" configuration variable
here.

> @@ -125,10 +203,34 @@ static struct git_var git_vars[] = {
>  		.name = "GIT_COMMITTER_IDENT",
>  		.read = committer,
>  	},
> +	{
> +		.name = "GIT_COMMITTER_NAME",
> +		.read = committer_name,
> +	},
> ...

These are pretty straight-forward additions.  It shows that the
original code structure was designed for extensibility.

> @@ -172,10 +278,11 @@ static struct git_var git_vars[] = {
>  	},
>  };
>  
> -static void list_vars(void)
> +static void list_vars(int null_term)
>  {
>  	struct git_var *ptr;
>  	char *val;
> +	char eol = null_term ? '\0' : '\n';
>  
>  	for (ptr = git_vars; ptr->read; ptr++)
>  		if ((val = ptr->read(0))) {
> @@ -184,10 +291,10 @@ static void list_vars(void)
>  
>  				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);
> +					printf("%s=%s%c", ptr->name, list.items[i].string, eol);

Beware overly long lines.

> @@ -196,6 +303,8 @@ static void list_vars(void)
>  static const struct git_var *get_git_var(const char *var)
>  {
>  	struct git_var *ptr;
> +	if (!strcmp(var, "GIT_SIGNING_KEY"))
> +		var = "GIT_DEFAULT_KEY";

We should be able to do much better than this.  Aim to stick to the
table-driven approach as much as possible.  For example, we could
add a "const char *" member to struct git_var that says it is an
alias to another key, add an entry

	{
		.name = "GIT_SIGNING_KEY",
		.alias = "GIT_DEFAULT_KEY",
		.read = default_key,
	},

there, and teach the enumerating iterator (aka list_vars) to skip an
entry that is an alias (because it would give the user redundant
information) while allowing the locating iterator (aka get_git_var)
to find it.

>  	for (ptr = git_vars; ptr->read; ptr++) {
>  		if (strcmp(var, ptr->name) == 0) {
>  			return ptr;
> @@ -219,30 +331,65 @@ int cmd_var(int argc,
>  	    const char *prefix UNUSED,
>  	    struct repository *repo UNUSED)
>  {
> -	const struct git_var *git_var;
> -	char *val;
> +	struct strvec vars = STRVEC_INIT;
> +	int list = 0;
> +	int null_term = 0;
> +	int i;
>  
>  	show_usage_if_asked(argc, argv, var_usage);
> -	if (argc != 2)
> -		usage(var_usage);
>  
> -	if (strcmp(argv[1], "-l") == 0) {
> -		repo_config(the_repository, show_config, NULL);
> -		list_vars();
> +	for (i = 1; i < argc; i++) {
> +		const char *arg = argv[i];
> +
> +		if (!strcmp(arg, "-l")) {
> +			list = 1;
> +		} else if (!strcmp(arg, "-z")) {
> +			null_term = 1;
> +		} else if (!strcmp(arg, "--")) {
> +			for (i = i + 1; i < argc; i++)
> +				strvec_push(&vars, argv[i]);
> +			break;

Everything else in this loop seems sensible, but this one is iffy.
What makes us need this special casing of "--" and everything that
follows it?

> +		} else if (arg[0] == '-') {
> +			usage(var_usage);
> +		} else {
> +			strvec_push(&vars, arg);
> +		}
> +	}

Have you considered using parse_options() API instead of this
handwritten loop?  You'd only need to recognise two options '-l' and
'-z' into two booleans "int list" and "int null_termination", and
then

	if (list) {
		... barf if something remains in argc/argv[] ...
		
	} else {
		... everyhing in argc/argv[] are vars ...
	}

I do not see why we would even need a vars that is a copy of
argc/argv[] after options are parsed out.

> +	if (!vars.nr)
>  		usage(var_usage);

I do not know if this is friendly for a tool meant primarily for
scripts.  "git var foo bar" would give information about foo and
bar, "git var foo" would give informmation about foo, and it is
natural "git var" alone would give nothing.

This matters because it is plausible to have a list of vars in a
variable and then run "git var -z $variables | xargs -0 ..." to
process the vars and their values.  If you barf for an empty list,
then they need to see if they have nothing in $variable, which
becomes more than "test -z "$variable" because people often take
advantage of the fact that IFS is ignored to write their accumulator
like so

	variable=" "
	for n in ...
	do
		variable="$variable$n "
	done

	case "$variable" in
	*" "word" "*)
		: has the word
		;;
	esac

which means that $variable may not be an empty string.  It is easier
for scripts if "git var <variable>..." did not barf when the number
of <variable>s happens to be zero.

> +test_expect_success 'get multiple variables with -z' '
> +	test_tick &&
> +	printf "%s\0%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&

Just a trivia, but

	printf "%s\0" "$GIT_AUTHOR_NAME "$GIT_AUTHOR_EMAIL" >expect

would be equivalent.

  reply	other threads:[~2026-09-03 17:40 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-25 20:46 [PATCH] builtin/whoami: add new 'whoami' command Andrew Pleeter via GitGitGadget
2026-08-25 21:24 ` brian m. carlson
2026-08-25 21:41 ` Junio C Hamano
2026-08-31 23:59 ` [PATCH v2] builtin/ident: add new 'ident' command Andrew Pleeter via GitGitGadget
2026-09-01  4:39   ` Jeff King
2026-09-01  5:00     ` Junio C Hamano
2026-09-03  2:49 ` [PATCH v3] var: support broken-down idents, default key, multiple args, and -z Andrew Pleeter via GitGitGadget
2026-09-03 17:40   ` Junio C Hamano [this message]
2026-09-03 18:22     ` Ben Knoble
2026-09-04  9:11   ` Phillip Wood
2026-09-04 15:57     ` Junio C Hamano
2026-09-08  9:07       ` Phillip Wood
2026-09-08  4:09 ` [PATCH v4] var: support broken-down idents, signing " Andrew Pleeter via GitGitGadget
2026-09-08 13:54   ` Phillip Wood
2026-09-08 20:43 ` [PATCH v5] " Andrew Pleeter via GitGitGadget
2026-09-08 21:53   ` Junio C Hamano
2026-09-09  1:24 ` [PATCH v6] " Andrew Pleeter via GitGitGadget
2026-09-09 15:36   ` Phillip Wood
2026-09-09 16:42   ` Junio C Hamano
2026-09-10  3:09 ` [PATCH v7] " Andrew Pleeter via GitGitGadget

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=xmqqbjaecjxb.fsf@gitster.g \
    --to=gitster@pobox.com \
    --cc=andrewpleeter@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=gitgitgadget@gmail.com \
    --cc=peff@peff.net \
    --cc=sandals@crustytoothpaste.net \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox