Volume XXII, number 279Tuesday, October 6, 2026Latest message 34 minutes ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patchbuiltin/whoami: add new 'whoami' command

35 messages between Aug 25, 2026 and Oct 2, 2026, from Andrew Pleeter via GitGitGadget, brian m. carlson, Junio C Hamano, Jeff King, Ben Knoble, Phillip Wood, Andrew Pleeter.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Andrew Pleeter via GitGitGadgetAug 25, 2026, 20:46 UTC on lore
From: anpl1623 <andrewpleeter@gmail.com>

Add a builtin 'whoami' command to inspect and display the resolved author and committer identity along with the commit signing configuration (GPG/SSH key ID and commit.gpgsign status) used when creating Git commits.

Support optional flags (--author, --committer, --name, --email, --signing-key, and --verbose) for targeted querying and scripting.

Include documentation in Documentation/git-whoami.adoc and regression tests in t/t0015-whoami.sh.

Signed-off-by: anpl1623 <andrewpleeter@gmail.com>
---
    builtin/whoami: add new 'whoami' command
    
    
    Title:
    ======
    
    builtin/whoami: add new 'whoami' command
    
    ------------------------------------------------------------------------
    
    
    Description:
    ============
    
    SUMMARY
    
    Adds a new builtin git whoami command that inspects and displays the
    user's active author and committer identity along with their commit
    signing configuration (GPG/SSH key ID and commit.gpgsign status).
    
    MOTIVATION
    
    Users often work across multiple environments, profiles, or repositories
    with different global/local configs and signing keys. Currently,
    verifying what identity and signing key will be attached to a new commit
    requires checking several individual git config and git var settings.
    git whoami provides a simple, direct porcelain command to verify this in
    one step.
    
    CHANGES
    
     * Core implementation: Added builtin/whoami.c using Git's ident.h and
       gpg-interface.h APIs.
     * CLI flags supported:
       * git whoami (default overview)
       * -a, --author (author identity: Name <email>)
       * -c, --committer (committer identity: Name <email>)
       * -n, --name (name only)
       * -e, --email (email only)
       * -s, --signing-key (signing key ID)
       * -v, --verbose (detailed identity and signing breakdown)
     * Documentation: Added manpage in Documentation/git-whoami.adoc.
     * Build integration: Added to Makefile, meson.build, command-list.txt,
       and builtin.h.
     * Test suite: Added comprehensive automated tests in t/t0015-whoami.sh.
    
    EXAMPLE OUTPUT
    
    $ git whoami
    Author:    Jane Doe <jane@example.com>
    Committer: Jane Doe <jane@example.com>
    Signing:   /Users/jane/.ssh/id_ed25519_git (format: ssh, commit.gpgsign: true)
    
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v1
Pull-Request: https://github.com/git/git/pull/2388
 Documentation/git-whoami.adoc |  48 ++++++++
 Documentation/meson.build     |   1 +
 Makefile                      |   1 +
 builtin.h                     |   1 +
 builtin/whoami.c              | 201 ++++++++++++++++++++++++++++++++++
 command-list.txt              |   1 +
 git.c                         |   1 +
 meson.build                   |   1 +
 t/meson.build                 |   1 +
 t/t0015-whoami.sh             | 181 ++++++++++++++++++++++++++++++
 10 files changed, 437 insertions(+)
 create mode 100644 Documentation/git-whoami.adoc
 create mode 100644 builtin/whoami.c
 create mode 100755 t/t0015-whoami.sh
Show changes to 10 files +437 −0

Documentation/git-whoami.adoc, Documentation/meson.build, Makefile, builtin.h, builtin/whoami.c, command-list.txt, git.c, meson.build, t/meson.build, t/t0015-whoami.sh

diff --git a/Documentation/git-whoami.adoc b/Documentation/git-whoami.adoc
new file mode 100644
index 0000000000..80670f10e9
--- /dev/null
+++ b/Documentation/git-whoami.adoc
@@ -0,0 +1,48 @@
+git-whoami(1)
+=============
+
+NAME
+----
+git-whoami - Show current user identity and commit signing information
+
+
+SYNOPSIS
+--------
+[synopsis]
+git whoami [options]
+
+DESCRIPTION
+-----------
+Displays the author and committer identity (name and email address) and
+commit signing configuration that will be used when signing and recording
+Git commits.
+
+OPTIONS
+-------
+`-a`::
+`--author`::
+	Show author identity.
+
+`-c`::
+`--committer`::
+	Show committer identity.
+
+`-n`::
+`--name`::
+	Show name only.
+
+`-e`::
+`--email`::
+	Show email only.
+
+`-s`::
+`--signing-key`::
+	Show commit signing key only.
+
+`-v`::
+`--verbose`::
+	Show detailed identity and signing status.
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index f4854f802d..3476e41f29 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -163,6 +163,7 @@ manpages = {
   'git-verify-tag.adoc' : 1,
   'git-version.adoc' : 1,
   'git-web--browse.adoc' : 1,
+  'git-whoami.adoc' : 1,
   'git-worktree.adoc' : 1,
   'git-write-tree.adoc' : 1,
   'git.adoc' : 1,
diff --git a/Makefile b/Makefile
index d4b775953d..3af620d73d 100644
--- a/Makefile
+++ b/Makefile
@@ -1517,6 +1517,7 @@ BUILTIN_OBJS += builtin/var.o
 BUILTIN_OBJS += builtin/verify-commit.o
 BUILTIN_OBJS += builtin/verify-pack.o
 BUILTIN_OBJS += builtin/verify-tag.o
+BUILTIN_OBJS += builtin/whoami.o
 BUILTIN_OBJS += builtin/worktree.o
 BUILTIN_OBJS += builtin/write-tree.o
 
diff --git a/builtin.h b/builtin.h
index 4e47a4ebd3..9e38298274 100644
--- a/builtin.h
+++ b/builtin.h
@@ -284,5 +284,6 @@ int cmd_verify_pack(int argc, const char **argv, const char *prefix, struct repo
 int cmd_show_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_pack_refs(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_replace(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_whoami(int argc, const char **argv, const char *prefix, struct repository *repo);
 
 #endif
diff --git a/builtin/whoami.c b/builtin/whoami.c
new file mode 100644
index 0000000000..3830f76c93
--- /dev/null
+++ b/builtin/whoami.c
@@ -0,0 +1,201 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "builtin.h"
+#include "config.h"
+#include "gettext.h"
+#include "gpg-interface.h"
+#include "ident.h"
+#include "parse-options.h"
+#include "strbuf.h"
+
+static const char * const whoami_usage[] = {
+	N_("git whoami [options]"),
+	NULL
+};
+
+int cmd_whoami(int argc,
+	       const char **argv,
+	       const char *prefix,
+	       struct repository *repo UNUSED)
+{
+	int show_author = 0;
+	int show_committer = 0;
+	int show_name = 0;
+	int show_email = 0;
+	int show_signing_key = 0;
+	int verbose = 0;
+	int ret = 0;
+
+	struct option whoami_options[] = {
+		OPT_BOOL('a', "author", &show_author, N_("show author identity")),
+		OPT_BOOL('c', "committer", &show_committer, N_("show committer identity")),
+		OPT_BOOL('n', "name", &show_name, N_("show name only")),
+		OPT_BOOL('e', "email", &show_email, N_("show email only")),
+		OPT_BOOL('s', "signing-key", &show_signing_key, N_("show commit signing key")),
+		OPT__VERBOSE(&verbose, N_("show detailed identity and signing status")),
+		OPT_END()
+	};
+
+	struct strbuf author_info = STRBUF_INIT;
+	struct strbuf committer_info = STRBUF_INIT;
+	struct ident_split author_split, committer_split;
+	struct strbuf author_name = STRBUF_INIT;
+	struct strbuf author_email = STRBUF_INIT;
+	struct strbuf committer_name = STRBUF_INIT;
+	struct strbuf committer_email = STRBUF_INIT;
+
+	char *signing_key = NULL;
+	char *gpg_format = NULL;
+	char *ssh_default_key_cmd = NULL;
+	char *resolved_key = NULL;
+	int gpgsign = 0;
+	int is_ssh = 0;
+
+	argc = parse_options(argc, argv, prefix, whoami_options,
+			     whoami_usage, 0);
+
+	if (argc > 0)
+		usage_with_options(whoami_usage, whoami_options);
+
+	die_for_incompatible_opt2(show_author, "--author", show_committer, "--committer");
+	die_for_incompatible_opt2(show_name, "--name", show_email, "--email");
+	die_for_incompatible_opt2(show_signing_key, "--signing-key", show_name, "--name");
+	die_for_incompatible_opt2(show_signing_key, "--signing-key", show_email, "--email");
+	die_for_incompatible_opt2(show_signing_key, "--signing-key", show_author, "--author");
+	die_for_incompatible_opt2(show_signing_key, "--signing-key", show_committer, "--committer");
+	die_for_incompatible_opt2(show_signing_key, "--signing-key", verbose, "--verbose");
+	die_for_incompatible_opt2(verbose, "--verbose", show_name, "--name");
+	die_for_incompatible_opt2(verbose, "--verbose", show_email, "--email");
+	die_for_incompatible_opt2(verbose, "--verbose", show_author, "--author");
+	die_for_incompatible_opt2(verbose, "--verbose", show_committer, "--committer");
+
+	repo_config(the_repository, git_default_config, NULL);
+
+	strbuf_addstr(&author_info, git_author_info(IDENT_NO_DATE));
+	strbuf_addstr(&committer_info, git_committer_info(IDENT_NO_DATE));
+
+	if (split_ident_line(&author_split, author_info.buf, author_info.len) == 0) {
+		if (author_split.name_begin && author_split.name_end)
+			strbuf_add(&author_name, author_split.name_begin,
+				   author_split.name_end - author_split.name_begin);
+		if (author_split.mail_begin && author_split.mail_end)
+			strbuf_add(&author_email, author_split.mail_begin,
+				   author_split.mail_end - author_split.mail_begin);
+	}
+
+	if (split_ident_line(&committer_split, committer_info.buf, committer_info.len) == 0) {
+		if (committer_split.name_begin && committer_split.name_end)
+			strbuf_add(&committer_name, committer_split.name_begin,
+				   committer_split.name_end - committer_split.name_begin);
+		if (committer_split.mail_begin && committer_split.mail_end)
+			strbuf_add(&committer_email, committer_split.mail_begin,
+				   committer_split.mail_end - committer_split.mail_begin);
+	}
+
+	repo_config_get_bool(the_repository, "commit.gpgsign", &gpgsign);
+	repo_config_get_string(the_repository, "user.signingkey", &signing_key);
+	repo_config_get_string(the_repository, "gpg.format", &gpg_format);
+	repo_config_get_string(the_repository, "gpg.ssh.defaultkeycommand", &ssh_default_key_cmd);
+
+	is_ssh = gpg_format && !strcmp(gpg_format, "ssh");
+
+	if (signing_key && *signing_key) {
+		resolved_key = xstrdup(signing_key);
+	} else if (is_ssh) {
+		if (ssh_default_key_cmd && *ssh_default_key_cmd)
+			resolved_key = get_signing_key_id();
+	} else if (gpgsign) {
+		resolved_key = get_signing_key_id();
+	}
+
+	if (show_signing_key) {
+		if (resolved_key && *resolved_key) {
+			puts(resolved_key);
+			ret = 0;
+		} else {
+			ret = 1;
+		}
+		goto cleanup;
+	}
+
+	if (show_name) {
+		if (show_author)
+			puts(author_name.buf);
+		else
+			puts(committer_name.buf);
+		goto cleanup;
+	}
+
+	if (show_email) {
+		if (show_author)
+			puts(author_email.buf);
+		else
+			puts(committer_email.buf);
+		goto cleanup;
+	}
+
+	if (show_author) {
+		puts(author_info.buf);
+		goto cleanup;
+	}
+
+	if (show_committer) {
+		puts(committer_info.buf);
+		goto cleanup;
+	}
+
+	if (verbose) {
+		printf(_("Author Name:      %s\n"), author_name.buf);
+		printf(_("Author Email:     %s\n"), author_email.buf);
+		printf(_("Committer Name:   %s\n"), committer_name.buf);
+		printf(_("Committer Email:  %s\n"), committer_email.buf);
+		if (signing_key && *signing_key)
+			printf(_("Signing Key:      %s\n"), signing_key);
+		else if (resolved_key && *resolved_key)
+			printf(_("Signing Key:      %s (default fallback)\n"), resolved_key);
+		else
+			printf(_("Signing Key:      %s\n"), _("none"));
+		printf(_("Signing Format:   %s\n"),
+		       gpg_format ? gpg_format : "openpgp");
+		printf(_("GPG Signing:      %s\n"),
+		       gpgsign ? _("enabled") : _("disabled"));
+	} else {
+		printf(_("Author:    %s\n"), author_info.buf);
+		printf(_("Committer: %s\n"), committer_info.buf);
+		if (gpgsign) {
+			if (signing_key && *signing_key) {
+				printf(_("Signing:   %s (format: %s, commit.gpgsign: true)\n"),
+				       signing_key,
+				       gpg_format ? gpg_format : "openpgp");
+			} else if (resolved_key && *resolved_key) {
+				printf(_("Signing:   default key (%s) (format: %s, commit.gpgsign: true)\n"),
+				       resolved_key,
+				       gpg_format ? gpg_format : "openpgp");
+			} else {
+				printf(_("Signing:   enabled (no signing key configured)\n"));
+			}
+		} else {
+			if (signing_key && *signing_key) {
+				printf(_("Signing:   disabled (key: %s, format: %s, commit.gpgsign: false)\n"),
+				       signing_key,
+				       gpg_format ? gpg_format : "openpgp");
+			} else {
+				printf(_("Signing:   disabled (commit.gpgsign: false)\n"));
+			}
+		}
+	}
+
+cleanup:
+	free(signing_key);
+	free(gpg_format);
+	free(ssh_default_key_cmd);
+	free(resolved_key);
+	strbuf_release(&author_info);
+	strbuf_release(&committer_info);
+	strbuf_release(&author_name);
+	strbuf_release(&author_email);
+	strbuf_release(&committer_name);
+	strbuf_release(&committer_email);
+
+	return ret;
+}
diff --git a/command-list.txt b/command-list.txt
index 21b802c420..0276bc2a53 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -210,6 +210,7 @@ git-verify-pack                         plumbinginterrogators
 git-verify-tag                          ancillaryinterrogators
 git-version                             ancillaryinterrogators
 git-whatchanged                         ancillaryinterrogators          complete
+git-whoami                              ancillaryinterrogators
 git-worktree                            mainporcelain
 git-write-tree                          plumbingmanipulators
 gitattributes                           userinterfaces
diff --git a/git.c b/git.c
index e5f1811b6b..83629178c9 100644
--- a/git.c
+++ b/git.c
@@ -680,6 +680,7 @@ static struct cmd_struct commands[] = {
 #ifndef WITH_BREAKING_CHANGES
 	{ "whatchanged", cmd_whatchanged, RUN_SETUP | DEPRECATED },
 #endif
+	{ "whoami", cmd_whoami, RUN_SETUP_GENTLY },
 	{ "worktree", cmd_worktree, RUN_SETUP },
 	{ "write-tree", cmd_write_tree, RUN_SETUP },
 };
diff --git a/meson.build b/meson.build
index d86f2acd2b..0daf395752 100644
--- a/meson.build
+++ b/meson.build
@@ -726,6 +726,7 @@ builtin_sources = [
   'builtin/verify-commit.c',
   'builtin/verify-pack.c',
   'builtin/verify-tag.c',
+  'builtin/whoami.c',
   'builtin/worktree.c',
   'builtin/write-tree.c',
 ]
diff --git a/t/meson.build b/t/meson.build
index 181d61a8a0..8c111148da 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -87,6 +87,7 @@ integration_tests = [
   't0012-help.sh',
   't0013-sha1dc.sh',
   't0014-alias.sh',
+  't0015-whoami.sh',
   't0017-env-helper.sh',
   't0018-advice.sh',
   't0019-json-writer.sh',
diff --git a/t/t0015-whoami.sh b/t/t0015-whoami.sh
new file mode 100755
index 0000000000..2a2d9b6dd3
--- /dev/null
+++ b/t/t0015-whoami.sh
@@ -0,0 +1,181 @@
+#!/bin/sh
+
+test_description='basic sanity checks for git whoami'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-gpg.sh"
+
+test_expect_success 'default output format without signing' '
+	cat >expect <<-EOF &&
+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
+	Signing:   disabled (commit.gpgsign: false)
+	EOF
+	git whoami >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git whoami --author' '
+	echo "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" >expect &&
+	git whoami --author >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git whoami --committer' '
+	echo "$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" >expect &&
+	git whoami --committer >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git whoami --author --name and --author --email' '
+	echo "$GIT_AUTHOR_NAME" >expect_name &&
+	git whoami --author --name >actual_name &&
+	test_cmp expect_name actual_name &&
+	echo "$GIT_AUTHOR_EMAIL" >expect_email &&
+	git whoami --author --email >actual_email &&
+	test_cmp expect_email actual_email
+'
+
+test_expect_success 'git whoami --committer --name and --committer --email' '
+	echo "$GIT_COMMITTER_NAME" >expect_name &&
+	git whoami --committer --name >actual_name &&
+	test_cmp expect_name actual_name &&
+	echo "$GIT_COMMITTER_EMAIL" >expect_email &&
+	git whoami --committer --email >actual_email &&
+	test_cmp expect_email actual_email
+'
+
+test_expect_success 'git whoami --signing-key when signing is disabled and unset' '
+	test_config commit.gpgsign false &&
+	test_unconfig user.signingkey &&
+	test_must_fail git whoami --signing-key
+'
+
+test_expect_success 'git whoami with explicitly configured signing key' '
+	test_config user.signingkey "TEST_KEY_123" &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format ssh &&
+	cat >expect <<-EOF &&
+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
+	Signing:   TEST_KEY_123 (format: ssh, commit.gpgsign: true)
+	EOF
+	git whoami >actual &&
+	test_cmp expect actual &&
+	echo "TEST_KEY_123" >expect_key &&
+	git whoami --signing-key >actual_key &&
+	test_cmp expect_key actual_key
+'
+
+test_expect_success 'git whoami with signing disabled but key configured' '
+	test_config user.signingkey "TEST_KEY_123" &&
+	test_config commit.gpgsign false &&
+	test_config gpg.format openpgp &&
+	cat >expect <<-EOF &&
+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
+	Signing:   disabled (key: TEST_KEY_123, format: openpgp, commit.gpgsign: false)
+	EOF
+	git whoami >actual &&
+	test_cmp expect actual &&
+	echo "TEST_KEY_123" >expect_key &&
+	git whoami --signing-key >actual_key &&
+	test_cmp expect_key actual_key
+'
+
+test_expect_success 'git whoami with openpgp signing enabled without explicit key' '
+	test_unconfig user.signingkey &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format openpgp &&
+	cat >expect <<-EOF &&
+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
+	Signing:   default key ($GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>) (format: openpgp, commit.gpgsign: true)
+	EOF
+	git whoami >actual &&
+	test_cmp expect actual &&
+	echo "$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" >expect_key &&
+	git whoami --signing-key >actual_key &&
+	test_cmp expect_key actual_key
+'
+
+test_expect_success 'git whoami with ssh signing enabled without explicit key' '
+	test_unconfig user.signingkey &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format ssh &&
+	test_unconfig gpg.ssh.defaultKeyCommand &&
+	cat >expect <<-EOF &&
+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
+	Signing:   enabled (no signing key configured)
+	EOF
+	git whoami >actual &&
+	test_cmp expect actual &&
+	test_must_fail git whoami --signing-key
+'
+
+test_expect_success GPGSSH 'git whoami with ssh defaultKeyCommand' '
+	test_unconfig user.signingkey &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format ssh &&
+	test_config gpg.ssh.defaultKeyCommand "cat \"${GPGSSH_KEY_PRIMARY}.pub\"" &&
+	git whoami --signing-key >actual_key &&
+	test_grep "^SHA256:" actual_key
+'
+
+test_expect_success 'git whoami -v / --verbose' '
+	test_config user.signingkey "MY_SIGNING_KEY" &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format openpgp &&
+	cat >expect <<-EOF &&
+	Author Name:      $GIT_AUTHOR_NAME
+	Author Email:     $GIT_AUTHOR_EMAIL
+	Committer Name:   $GIT_COMMITTER_NAME
+	Committer Email:  $GIT_COMMITTER_EMAIL
+	Signing Key:      MY_SIGNING_KEY
+	Signing Format:   openpgp
+	GPG Signing:      enabled
+	EOF
+	git whoami -v >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git whoami with environment variable overrides' '
+	test_unconfig user.signingkey &&
+	test_config commit.gpgsign false &&
+	cat >expect <<-EOF &&
+	Author:    Custom Author <custom.author@example.com>
+	Committer: Custom Committer <custom.committer@example.com>
+	Signing:   disabled (commit.gpgsign: false)
+	EOF
+	GIT_AUTHOR_NAME="Custom Author" \
+	GIT_AUTHOR_EMAIL="custom.author@example.com" \
+	GIT_COMMITTER_NAME="Custom Committer" \
+	GIT_COMMITTER_EMAIL="custom.committer@example.com" \
+	git whoami >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'incompatible option combinations fail' '
+	test_must_fail git whoami --author --committer 2>err &&
+	test_grep "cannot be used together" err &&
+	test_must_fail git whoami --name --email 2>err &&
+	test_grep "cannot be used together" err &&
+	test_must_fail git whoami --signing-key --name 2>err &&
+	test_grep "cannot be used together" err &&
+	test_must_fail git whoami --signing-key --email 2>err &&
+	test_grep "cannot be used together" err &&
+	test_must_fail git whoami --signing-key --author 2>err &&
+	test_grep "cannot be used together" err &&
+	test_must_fail git whoami --signing-key --committer 2>err &&
+	test_grep "cannot be used together" err &&
+	test_must_fail git whoami --verbose --name 2>err &&
+	test_grep "cannot be used together" err
+'
+
+test_expect_success 'git whoami outside of repository' '
+	nongit git whoami --author >actual &&
+	test_grep "<" actual
+'
+
+test_done

base-commit: 593c42fe075be0c8cd5239b3a2f21c610cbc9798
-- 
gitgitgadget
brian m. carlsonAug 25, 2026, 21:24 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH] builtin/whoami: add new 'whoami' command

On 2026-08-25 at 20:46:42, Andrew Pleeter via GitGitGadget wrote:
> From: anpl1623 <andrewpleeter@gmail.com>

You will probably want this to match your real name since you're using it in the email. We prefer that people use their real names, but don't require it, but I assume that it's not a problem since you just CC'd yourself using it (and it's also in your email address).

Show 7 quoted lines
> Add a builtin 'whoami' command to inspect and display the resolved
> author and committer identity along with the commit signing
> configuration (GPG/SSH key ID and commit.gpgsign status) used when
> creating Git commits.
> 
> Support optional flags (--author, --committer, --name, --email,
> --signing-key, and --verbose) for targeted querying and scripting.

I suspect users will want a way to get _all_ of the output in a machine-readable way, so you'd probably want to provide some method of doing that. Note that because your existing endpoints provide translated strings, they are not suitable for this. That doesn't mean that they should not be translated (because they should) but we'd probably want a format like the following:

user.author.name=A U Thor user.author.email=author@example.com

Other formats are possible, though.

Possibly a `-z` option for NUL-terminated instead of LF-terminated output might be warranted as well unless we're certain that our output will never contain a newline (hint: config options can).

> Include documentation in Documentation/git-whoami.adoc and regression
> tests in t/t0015-whoami.sh.
> 
> Signed-off-by: anpl1623 <andrewpleeter@gmail.com>
Again, you'll want to sign this off with your real name.
Show 8 quoted lines
>     MOTIVATION
>     
>     Users often work across multiple environments, profiles, or repositories
>     with different global/local configs and signing keys. Currently,
>     verifying what identity and signing key will be attached to a new commit
>     requires checking several individual git config and git var settings.
>     git whoami provides a simple, direct porcelain command to verify this in
>     one step.

I think this should go in the commit message. I thought to myself, "Well, there are already ways to get this information, so why add a new one?" Telling us why your patch is compelling and solves an important purpose is appropriate for the commit message.

I might also like to see an explanation as to why this wouldn't work better in `git var` or elsewhere instead, since much of the information is already there. Since that's an alternative you've rejected, tell us why and sell us on your vision.

> +	repo_config(the_repository, git_default_config, NULL);

Let's not add more uses of `the_repository`. Use the `repo` argument to the main function above, taking care to handle the NULL case.

Show 15 quoted lines
> +	repo_config_get_bool(the_repository, "commit.gpgsign", &gpgsign);
> +	repo_config_get_string(the_repository, "user.signingkey", &signing_key);
> +	repo_config_get_string(the_repository, "gpg.format", &gpg_format);
> +	repo_config_get_string(the_repository, "gpg.ssh.defaultkeycommand", &ssh_default_key_cmd);
> +
> +	is_ssh = gpg_format && !strcmp(gpg_format, "ssh");
> +
> +	if (signing_key && *signing_key) {
> +		resolved_key = xstrdup(signing_key);
> +	} else if (is_ssh) {
> +		if (ssh_default_key_cmd && *ssh_default_key_cmd)
> +			resolved_key = get_signing_key_id();
> +	} else if (gpgsign) {
> +		resolved_key = get_signing_key_id();
> +	}
Should this also do something useful for X.509 keys?

Overall, I don't have a strong need for this and I'm fine using the existing functionality. However, I see how it could be useful and if it were merged and available in the versions of Git I use, then I might make use of it.

Perhaps others think this is compelling, though, so I'm interested to hear other opinions about the utility of the command.

-- 
brian m. carlson (they/them)
Toronto, Ontario, CA
Junio C HamanoAug 25, 2026, 21:41 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH] builtin/whoami: add new 'whoami' command

"Andrew Pleeter via GitGitGadget" <gitgitgadget@gmail.com> writes:
>     builtin/whoami: add new 'whoami' command

In general, I would really really want to see us refrain from adding any more random subcommands.

Show 7 quoted lines
> +`-a`::
> +`--author`::
> +	Show author identity.
> +
> +`-c`::
> +`--committer`::
> +	Show committer identity.
This pair hints the users they are equals.
But the code tells us otherwise:
Show 8 quoted lines
> +	if (show_name) {
> +		if (show_author)
> +			puts(author_name.buf);
> +		else
> +			puts(committer_name.buf);
> +		goto cleanup;
> +	}
> +

So when "-n" is in effect, "-c" is completely ignored. Lack of "-a" means "-c" instead. The same story holds for "-e".

Yet later in the code that is executed when neither "-n" or "-e" is in effect:

Show 10 quoted lines
> +	if (show_author) {
> +		puts(author_info.buf);
> +		goto cleanup;
> +	}
> +
> +	if (show_committer) {
> +		puts(committer_info.buf);
> +		goto cleanup;
> +	}
> +

Here, lack of "-a" is not sufficient to view committer information and you'd explicitly need to pass "-c" if you want to view committer information.

So confusing.
Show 7 quoted lines
> +`-n`::
> +`--name`::
> +	Show name only.
> +
> +`-e`::
> +`--email`::
> +	Show email only.

Why not make -a/-c/-n/-e more additive instead? Something along the lines of ...

    $ git ident -a -e -n
    Andrew Pleeter <andrewpleeter@gmail.com>
    $ git ident -a -n -v
    Author: Andrew Pleeter
    $ git ident -a -c -e
    <andrewpleeter@gmail.com>
    <andrewpleeter@gmail.com>
    $ git ident -a -c -e -v
    Author: <andrewpleeter@gmail.com>
    Committer: <andrewpleeter@gmail.com>
Andrew Pleeter via GitGitGadgetAug 31, 2026, 23:59 UTC in reply to Andrew Pleeter via GitGitGadget on lore

[PATCH v2] builtin/ident: add new 'ident' command

From: Andrew Pleeter <andrewpleeter@gmail.com>

Add a builtin 'ident' command to inspect and display the resolved author and committer identity along with optional commit signing configuration used when creating Git commits.

While existing plumbing commands like 'git var' and 'git config' expose individual pieces of identity and configuration, discovering what identity and signing key will actually be attached to a new commit requires multiple independent queries and manual correlation. 'git config' only reads raw values without performing environment overrides or GECOS detection, while 'git var' returns full ident strings with timestamps without exposing commit signing status.

'git ident' provides a unified command with additive, composable options:
  - Identity scope selectors (-a / --author, -c / --committer) choose
    which identities to format (defaulting to both when neither is specified).
  - Component selectors (-n / --name, -e / --email) choose which parts
    to format (defaulting to full 'Name <email>' when neither or both are
    specified).
  - -v / --verbose prepends 'Author: ' or 'Committer: ' role labels.
  - -s / --signing-key resolves and outputs the commit signing key.
  - --porcelain produces machine-readable key-value pairs.
  - -z / --null terminates output records with NUL bytes.

Include documentation in Documentation/git-ident.adoc and regression tests in t/t0015-ident.sh.

Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
    builtin/whoami: add new 'whoami' command
    
    
    Title:
    ======
    
    builtin/whoami: add new 'whoami' command
    
    ------------------------------------------------------------------------
    
    
    Description:
    ============
    
    SUMMARY
    
    Adds a new builtin git whoami command that inspects and displays the
    user's active author and committer identity along with their commit
    signing configuration (GPG/SSH key ID and commit.gpgsign status).
    
    MOTIVATION
    
    Users often work across multiple environments, profiles, or repositories
    with different global/local configs and signing keys. Currently,
    verifying what identity and signing key will be attached to a new commit
    requires checking several individual git config and git var settings.
    git whoami provides a simple, direct porcelain command to verify this in
    one step.
    
    CHANGES
    
     * Core implementation: Added builtin/whoami.c using Git's ident.h and
       gpg-interface.h APIs.
     * CLI flags supported:
       * git whoami (default overview)
       * -a, --author (author identity: Name <email>)
       * -c, --committer (committer identity: Name <email>)
       * -n, --name (name only)
       * -e, --email (email only)
       * -s, --signing-key (signing key ID)
       * -v, --verbose (detailed identity and signing breakdown)
     * Documentation: Added manpage in Documentation/git-whoami.adoc.
     * Build integration: Added to Makefile, meson.build, command-list.txt,
       and builtin.h.
     * Test suite: Added comprehensive automated tests in t/t0015-whoami.sh.
    
    EXAMPLE OUTPUT
    
    $ git whoami
    Author:    Jane Doe <jane@example.com>
    Committer: Jane Doe <jane@example.com>
    Signing:   /Users/jane/.ssh/id_ed25519_git (format: ssh, commit.gpgsign: true)
    
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v2
Pull-Request: https://github.com/git/git/pull/2388
Range-diff vs v1:
 1:  263aebca08 ! 1:  f322e7fedd builtin/whoami: add new 'whoami' command
     @@
       ## Metadata ##
     -Author: anpl1623 <andrewpleeter@gmail.com>
     +Author: Andrew Pleeter <andrewpleeter@gmail.com>
      
       ## Commit message ##
     -    builtin/whoami: add new 'whoami' command
     +    builtin/ident: add new 'ident' command
      
     -    Add a builtin 'whoami' command to inspect and display the resolved
     -    author and committer identity along with the commit signing
     -    configuration (GPG/SSH key ID and commit.gpgsign status) used when
     -    creating Git commits.
     +    Add a builtin 'ident' command to inspect and display the resolved author
     +    and committer identity along with optional commit signing configuration
     +    used when creating Git commits.
      
     -    Support optional flags (--author, --committer, --name, --email,
     -    --signing-key, and --verbose) for targeted querying and scripting.
     +    While existing plumbing commands like 'git var' and 'git config' expose
     +    individual pieces of identity and configuration, discovering what identity
     +    and signing key will actually be attached to a new commit requires multiple
     +    independent queries and manual correlation. 'git config' only reads raw
     +    values without performing environment overrides or GECOS detection, while
     +    'git var' returns full ident strings with timestamps without exposing
     +    commit signing status.
      
     -    Include documentation in Documentation/git-whoami.adoc and regression
     -    tests in t/t0015-whoami.sh.
     +    'git ident' provides a unified command with additive, composable options:
     +      - Identity scope selectors (-a / --author, -c / --committer) choose
     +        which identities to format (defaulting to both when neither is specified).
     +      - Component selectors (-n / --name, -e / --email) choose which parts
     +        to format (defaulting to full 'Name <email>' when neither or both are
     +        specified).
     +      - -v / --verbose prepends 'Author: ' or 'Committer: ' role labels.
     +      - -s / --signing-key resolves and outputs the commit signing key.
     +      - --porcelain produces machine-readable key-value pairs.
     +      - -z / --null terminates output records with NUL bytes.
      
     -    Signed-off-by: anpl1623 <andrewpleeter@gmail.com>
     +    Include documentation in Documentation/git-ident.adoc and regression
     +    tests in t/t0015-ident.sh.
      
     - ## Documentation/git-whoami.adoc (new) ##
     +    Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
     +
     + ## Documentation/git-ident.adoc (new) ##
      @@
     -+git-whoami(1)
     -+=============
     ++git-ident(1)
     ++============
      +
      +NAME
      +----
     -+git-whoami - Show current user identity and commit signing information
     ++git-ident - Show current user identity and commit signing information
      +
      +
      +SYNOPSIS
      +--------
      +[synopsis]
     -+git whoami [options]
     ++git ident [<options>]
      +
      +DESCRIPTION
      +-----------
      +Displays the author and committer identity (name and email address) and
     -+commit signing configuration that will be used when signing and recording
     ++optionally commit signing configuration that will be used when recording
      +Git commits.
      +
     ++The identity selectors (`-a` / `--author`, `-c` / `--committer`) and component
     ++selectors (`-n` / `--name`, `-e` / `--email`) are additive and composable:
     ++
     ++* If neither `--author` nor `--committer` is specified, both identities are shown.
     ++* If neither `--name` nor `--email` is specified (or if both are specified),
     ++  the full identity format (`Name <email>`) is shown.
     ++* If `-v` / `--verbose` is specified, entries are labeled with their role
     ++  (`Author: ` / `Committer: `).
     ++
      +OPTIONS
      +-------
      +`-a`::
     @@ Documentation/git-whoami.adoc (new)
      +
      +`-e`::
      +`--email`::
     -+	Show email only.
     ++	Show email address only (formatted as `<email>`).
      +
      +`-s`::
      +`--signing-key`::
     -+	Show commit signing key only.
     ++	Show the signing key that Git would resolve for signing commits.
     ++	Resolution proceeds in the following order:
     ++	* The explicitly configured `user.signingKey`, if set.
     ++	* If `gpg.format` is set to `ssh` and `gpg.ssh.defaultKeyCommand`
     ++	  is configured, executes the command to obtain the public key.
     ++	* If `gpg.format` is `openpgp` or `x509` and `commit.gpgsign` is
     ++	  enabled, falls back to the committer identity.
     ++	* If signing is disabled (`commit.gpgsign=false`) and no explicit key
     ++	  is configured, exits with status 1.
     ++
     ++`--porcelain`::
     ++	Give the output in an easy-to-parse format for scripts.
     ++	Outputs key-value pairs (`user.author.name`, `user.author.email`,
     ++	`user.committer.name`, `user.committer.email`, `user.signingkey`,
     ++	`gpg.format`, `commit.gpgsign`).
     ++
     ++`-z`::
     ++`--null`::
     ++	Terminate output lines or entries with NUL instead of a newline.
      +
      +`-v`::
      +`--verbose`::
     -+	Show detailed identity and signing status.
     ++	Prepend role labels (`Author: `, `Committer: `) to identity entries.
     ++
     ++EXAMPLES
     ++--------
     ++
     ++* Show full author and committer identities:
     +++
     ++------------
     ++$ git ident
     ++Author Name <author@example.com>
     ++Committer Name <committer@example.com>
     ++------------
     ++
     ++* Show author identity only:
     +++
     ++------------
     ++$ git ident -a -e -n
     ++Author Name <author@example.com>
     ++------------
     ++
     ++* Show labeled author name:
     +++
     ++------------
     ++$ git ident -a -n -v
     ++Author: Author Name
     ++------------
     ++
     ++* Show author and committer email addresses:
     +++
     ++------------
     ++$ git ident -a -c -e
     ++<author@example.com>
     ++<committer@example.com>
     ++------------
     ++
     ++* Show labeled author and committer email addresses:
     +++
     ++------------
     ++$ git ident -a -c -e -v
     ++Author: <author@example.com>
     ++Committer: <committer@example.com>
     ++------------
      +
      +GIT
      +---
     @@ Documentation/git-whoami.adoc (new)
      
       ## Documentation/meson.build ##
      @@ Documentation/meson.build: manpages = {
     -   'git-verify-tag.adoc' : 1,
     -   'git-version.adoc' : 1,
     -   'git-web--browse.adoc' : 1,
     -+  'git-whoami.adoc' : 1,
     -   'git-worktree.adoc' : 1,
     -   'git-write-tree.adoc' : 1,
     -   'git.adoc' : 1,
     +   'git-help.adoc' : 1,
     +   'git-history.adoc' : 1,
     +   'git-hook.adoc' : 1,
     ++  'git-ident.adoc' : 1,
     +   'git-http-backend.adoc' : 1,
     +   'git-http-fetch.adoc' : 1,
     +   'git-http-push.adoc' : 1,
      
       ## Makefile ##
     -@@ Makefile: BUILTIN_OBJS += builtin/var.o
     - BUILTIN_OBJS += builtin/verify-commit.o
     - BUILTIN_OBJS += builtin/verify-pack.o
     - BUILTIN_OBJS += builtin/verify-tag.o
     -+BUILTIN_OBJS += builtin/whoami.o
     - BUILTIN_OBJS += builtin/worktree.o
     - BUILTIN_OBJS += builtin/write-tree.o
     - 
     +@@ Makefile: BUILTIN_OBJS += builtin/hash-object.o
     + BUILTIN_OBJS += builtin/help.o
     + BUILTIN_OBJS += builtin/history.o
     + BUILTIN_OBJS += builtin/hook.o
     ++BUILTIN_OBJS += builtin/ident.o
     + BUILTIN_OBJS += builtin/index-pack.o
     + BUILTIN_OBJS += builtin/init-db.o
     + BUILTIN_OBJS += builtin/interpret-trailers.o
      
       ## builtin.h ##
     +@@ builtin.h: int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repo
     + int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
     + int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
     + int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
     ++int cmd_ident(int argc, const char **argv, const char *prefix, struct repository *repo);
     + int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
     + int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
     + int cmd_interpret_trailers(int argc, const char **argv, const char *prefix, struct repository *repo);
      @@ builtin.h: int cmd_verify_pack(int argc, const char **argv, const char *prefix, struct repo
       int cmd_show_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
       int cmd_pack_refs(int argc, const char **argv, const char *prefix, struct repository *repo);
       int cmd_replace(int argc, const char **argv, const char *prefix, struct repository *repo);
     -+int cmd_whoami(int argc, const char **argv, const char *prefix, struct repository *repo);
     - 
     +-
       #endif
      
     - ## builtin/whoami.c (new) ##
     + ## builtin/ident.c (new) ##
      @@
      +#define USE_THE_REPOSITORY_VARIABLE
      +
     @@ builtin/whoami.c (new)
      +#include "parse-options.h"
      +#include "strbuf.h"
      +
     -+static const char * const whoami_usage[] = {
     -+	N_("git whoami [options]"),
     ++static const char * const ident_usage[] = {
     ++	N_("git ident [<options>]"),
      +	NULL
      +};
      +
     -+int cmd_whoami(int argc,
     -+	       const char **argv,
     -+	       const char *prefix,
     -+	       struct repository *repo UNUSED)
     ++enum ident_who {
     ++	IDENT_AUTHOR = 1 << 0,
     ++	IDENT_COMMITTER = 1 << 1,
     ++};
     ++
     ++enum ident_part {
     ++	IDENT_NAME = 1 << 0,
     ++	IDENT_EMAIL = 1 << 1,
     ++};
     ++
     ++struct ident_config {
     ++	int gpgsign;
     ++	char *signing_key;
     ++	char *gpg_format;
     ++	char *ssh_default_key_cmd;
     ++};
     ++
     ++static int ident_config_cb(const char *var, const char *value,
     ++			   const struct config_context *ctx, void *data)
     ++{
     ++	struct ident_config *cfg = data;
     ++
     ++	if (!strcmp(var, "commit.gpgsign")) {
     ++		cfg->gpgsign = git_config_bool(var, value);
     ++		return 0;
     ++	}
     ++	if (!strcmp(var, "user.signingkey"))
     ++		return git_config_string(&cfg->signing_key, var, value);
     ++	if (!strcmp(var, "gpg.format"))
     ++		return git_config_string(&cfg->gpg_format, var, value);
     ++	if (!strcmp(var, "gpg.ssh.defaultkeycommand"))
     ++		return git_config_string(&cfg->ssh_default_key_cmd, var, value);
     ++
     ++	return git_default_config(var, value, ctx, data);
     ++}
     ++
     ++static void print_ident_entry(const char *label,
     ++			      const struct strbuf *name,
     ++			      const struct strbuf *email,
     ++			      int parts,
     ++			      int verbose,
     ++			      char eol)
     ++{
     ++	struct strbuf out = STRBUF_INIT;
     ++
     ++	if (verbose && label)
     ++		strbuf_addf(&out, "%s: ", label);
     ++
     ++	if ((parts & IDENT_NAME) && (parts & IDENT_EMAIL))
     ++		strbuf_addf(&out, "%s <%s>", name->buf, email->buf);
     ++	else if (parts & IDENT_NAME)
     ++		strbuf_addbuf(&out, name);
     ++	else if (parts & IDENT_EMAIL)
     ++		strbuf_addf(&out, "<%s>", email->buf);
     ++
     ++	printf("%s%c", out.buf, eol);
     ++	strbuf_release(&out);
     ++}
     ++
     ++int cmd_ident(int argc,
     ++	      const char **argv,
     ++	      const char *prefix,
     ++	      struct repository *repo)
      +{
      +	int show_author = 0;
      +	int show_committer = 0;
      +	int show_name = 0;
      +	int show_email = 0;
      +	int show_signing_key = 0;
     ++	int porcelain = 0;
     ++	int nul_term = 0;
      +	int verbose = 0;
      +	int ret = 0;
     ++	char eol;
     ++	int selected_who = 0;
     ++	int selected_parts = 0;
      +
     -+	struct option whoami_options[] = {
     ++	struct option ident_options[] = {
      +		OPT_BOOL('a', "author", &show_author, N_("show author identity")),
      +		OPT_BOOL('c', "committer", &show_committer, N_("show committer identity")),
      +		OPT_BOOL('n', "name", &show_name, N_("show name only")),
      +		OPT_BOOL('e', "email", &show_email, N_("show email only")),
      +		OPT_BOOL('s', "signing-key", &show_signing_key, N_("show commit signing key")),
     -+		OPT__VERBOSE(&verbose, N_("show detailed identity and signing status")),
     ++		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
     ++		OPT_BOOL('z', "null", &nul_term, N_("terminate entries with NUL")),
     ++		OPT__VERBOSE(&verbose, N_("show detailed identity")),
      +		OPT_END()
      +	};
      +
     ++	struct ident_config cfg = { 0 };
      +	struct strbuf author_info = STRBUF_INIT;
      +	struct strbuf committer_info = STRBUF_INIT;
      +	struct ident_split author_split, committer_split;
     @@ builtin/whoami.c (new)
      +	struct strbuf author_email = STRBUF_INIT;
      +	struct strbuf committer_name = STRBUF_INIT;
      +	struct strbuf committer_email = STRBUF_INIT;
     -+
     -+	char *signing_key = NULL;
     -+	char *gpg_format = NULL;
     -+	char *ssh_default_key_cmd = NULL;
      +	char *resolved_key = NULL;
     -+	int gpgsign = 0;
      +	int is_ssh = 0;
      +
     -+	argc = parse_options(argc, argv, prefix, whoami_options,
     -+			     whoami_usage, 0);
     ++	argc = parse_options(argc, argv, prefix, ident_options,
     ++			     ident_usage, 0);
      +
      +	if (argc > 0)
     -+		usage_with_options(whoami_usage, whoami_options);
     -+
     -+	die_for_incompatible_opt2(show_author, "--author", show_committer, "--committer");
     -+	die_for_incompatible_opt2(show_name, "--name", show_email, "--email");
     -+	die_for_incompatible_opt2(show_signing_key, "--signing-key", show_name, "--name");
     -+	die_for_incompatible_opt2(show_signing_key, "--signing-key", show_email, "--email");
     -+	die_for_incompatible_opt2(show_signing_key, "--signing-key", show_author, "--author");
     -+	die_for_incompatible_opt2(show_signing_key, "--signing-key", show_committer, "--committer");
     -+	die_for_incompatible_opt2(show_signing_key, "--signing-key", verbose, "--verbose");
     -+	die_for_incompatible_opt2(verbose, "--verbose", show_name, "--name");
     -+	die_for_incompatible_opt2(verbose, "--verbose", show_email, "--email");
     -+	die_for_incompatible_opt2(verbose, "--verbose", show_author, "--author");
     -+	die_for_incompatible_opt2(verbose, "--verbose", show_committer, "--committer");
     -+
     -+	repo_config(the_repository, git_default_config, NULL);
     ++		usage_with_options(ident_usage, ident_options);
     ++
     ++	die_for_incompatible_opt2(porcelain, "--porcelain", verbose, "--verbose");
     ++	die_for_incompatible_opt2(porcelain, "--porcelain", show_name, "--name");
     ++	die_for_incompatible_opt2(porcelain, "--porcelain", show_email, "--email");
     ++
     ++	eol = nul_term ? '\0' : '\n';
     ++
     ++	repo_config(repo, ident_config_cb, &cfg);
      +
      +	strbuf_addstr(&author_info, git_author_info(IDENT_NO_DATE));
      +	strbuf_addstr(&committer_info, git_committer_info(IDENT_NO_DATE));
     @@ builtin/whoami.c (new)
      +				   committer_split.mail_end - committer_split.mail_begin);
      +	}
      +
     -+	repo_config_get_bool(the_repository, "commit.gpgsign", &gpgsign);
     -+	repo_config_get_string(the_repository, "user.signingkey", &signing_key);
     -+	repo_config_get_string(the_repository, "gpg.format", &gpg_format);
     -+	repo_config_get_string(the_repository, "gpg.ssh.defaultkeycommand", &ssh_default_key_cmd);
     ++	is_ssh = cfg.gpg_format && !strcmp(cfg.gpg_format, "ssh");
      +
     -+	is_ssh = gpg_format && !strcmp(gpg_format, "ssh");
     -+
     -+	if (signing_key && *signing_key) {
     -+		resolved_key = xstrdup(signing_key);
     ++	if (cfg.signing_key && *cfg.signing_key) {
     ++		resolved_key = xstrdup(cfg.signing_key);
      +	} else if (is_ssh) {
     -+		if (ssh_default_key_cmd && *ssh_default_key_cmd)
     ++		if (cfg.ssh_default_key_cmd && *cfg.ssh_default_key_cmd)
      +			resolved_key = get_signing_key_id();
     -+	} else if (gpgsign) {
     ++	} else if (cfg.gpgsign) {
      +		resolved_key = get_signing_key_id();
      +	}
      +
     -+	if (show_signing_key) {
     ++	if (show_signing_key && !show_author && !show_committer && !show_name && !show_email && !porcelain) {
      +		if (resolved_key && *resolved_key) {
     -+			puts(resolved_key);
     ++			if (verbose)
     ++				printf(_("Signing Key: %s\n"), resolved_key);
     ++			else
     ++				printf("%s%c", resolved_key, eol);
      +			ret = 0;
      +		} else {
      +			ret = 1;
     @@ builtin/whoami.c (new)
      +		goto cleanup;
      +	}
      +
     -+	if (show_name) {
     -+		if (show_author)
     -+			puts(author_name.buf);
     -+		else
     -+			puts(committer_name.buf);
     -+		goto cleanup;
     -+	}
     -+
     -+	if (show_email) {
     -+		if (show_author)
     -+			puts(author_email.buf);
     -+		else
     -+			puts(committer_email.buf);
     ++	if (show_author)
     ++		selected_who |= IDENT_AUTHOR;
     ++	if (show_committer)
     ++		selected_who |= IDENT_COMMITTER;
     ++	if (!selected_who)
     ++		selected_who = IDENT_AUTHOR | IDENT_COMMITTER;
     ++
     ++	if (show_name)
     ++		selected_parts |= IDENT_NAME;
     ++	if (show_email)
     ++		selected_parts |= IDENT_EMAIL;
     ++	if (!selected_parts)
     ++		selected_parts = IDENT_NAME | IDENT_EMAIL;
     ++
     ++	if (porcelain) {
     ++		if (selected_who & IDENT_AUTHOR) {
     ++			printf("user.author.name=%s%c", author_name.buf, eol);
     ++			printf("user.author.email=%s%c", author_email.buf, eol);
     ++		}
     ++		if (selected_who & IDENT_COMMITTER) {
     ++			printf("user.committer.name=%s%c", committer_name.buf, eol);
     ++			printf("user.committer.email=%s%c", committer_email.buf, eol);
     ++		}
     ++		if ((selected_who & (IDENT_AUTHOR | IDENT_COMMITTER)) == (IDENT_AUTHOR | IDENT_COMMITTER)) {
     ++			printf("user.signingkey=%s%c",
     ++			       (cfg.signing_key && *cfg.signing_key) ? cfg.signing_key :
     ++			       (resolved_key && *resolved_key) ? resolved_key : "none",
     ++			       eol);
     ++			printf("gpg.format=%s%c",
     ++			       cfg.gpg_format ? cfg.gpg_format : "openpgp", eol);
     ++			printf("commit.gpgsign=%s%c",
     ++			       cfg.gpgsign ? "true" : "false", eol);
     ++		}
      +		goto cleanup;
      +	}
      +
     -+	if (show_author) {
     -+		puts(author_info.buf);
     -+		goto cleanup;
     -+	}
     ++	if (selected_who & IDENT_AUTHOR)
     ++		print_ident_entry("Author", &author_name, &author_email,
     ++				  selected_parts, verbose, eol);
      +
     -+	if (show_committer) {
     -+		puts(committer_info.buf);
     -+		goto cleanup;
     -+	}
     ++	if (selected_who & IDENT_COMMITTER)
     ++		print_ident_entry("Committer", &committer_name, &committer_email,
     ++				  selected_parts, verbose, eol);
      +
     -+	if (verbose) {
     -+		printf(_("Author Name:      %s\n"), author_name.buf);
     -+		printf(_("Author Email:     %s\n"), author_email.buf);
     -+		printf(_("Committer Name:   %s\n"), committer_name.buf);
     -+		printf(_("Committer Email:  %s\n"), committer_email.buf);
     -+		if (signing_key && *signing_key)
     -+			printf(_("Signing Key:      %s\n"), signing_key);
     -+		else if (resolved_key && *resolved_key)
     -+			printf(_("Signing Key:      %s (default fallback)\n"), resolved_key);
     -+		else
     -+			printf(_("Signing Key:      %s\n"), _("none"));
     -+		printf(_("Signing Format:   %s\n"),
     -+		       gpg_format ? gpg_format : "openpgp");
     -+		printf(_("GPG Signing:      %s\n"),
     -+		       gpgsign ? _("enabled") : _("disabled"));
     -+	} else {
     -+		printf(_("Author:    %s\n"), author_info.buf);
     -+		printf(_("Committer: %s\n"), committer_info.buf);
     -+		if (gpgsign) {
     -+			if (signing_key && *signing_key) {
     -+				printf(_("Signing:   %s (format: %s, commit.gpgsign: true)\n"),
     -+				       signing_key,
     -+				       gpg_format ? gpg_format : "openpgp");
     -+			} else if (resolved_key && *resolved_key) {
     -+				printf(_("Signing:   default key (%s) (format: %s, commit.gpgsign: true)\n"),
     -+				       resolved_key,
     -+				       gpg_format ? gpg_format : "openpgp");
     -+			} else {
     -+				printf(_("Signing:   enabled (no signing key configured)\n"));
     -+			}
     ++	if (show_signing_key) {
     ++		if (resolved_key && *resolved_key) {
     ++			if (verbose)
     ++				printf(_("Signing Key: %s\n"), resolved_key);
     ++			else
     ++				printf("%s%c", resolved_key, eol);
      +		} else {
     -+			if (signing_key && *signing_key) {
     -+				printf(_("Signing:   disabled (key: %s, format: %s, commit.gpgsign: false)\n"),
     -+				       signing_key,
     -+				       gpg_format ? gpg_format : "openpgp");
     -+			} else {
     -+				printf(_("Signing:   disabled (commit.gpgsign: false)\n"));
     -+			}
     ++			ret = 1;
      +		}
      +	}
      +
      +cleanup:
     -+	free(signing_key);
     -+	free(gpg_format);
     -+	free(ssh_default_key_cmd);
     ++	free(cfg.signing_key);
     ++	free(cfg.gpg_format);
     ++	free(cfg.ssh_default_key_cmd);
      +	free(resolved_key);
      +	strbuf_release(&author_info);
      +	strbuf_release(&committer_info);
     @@ builtin/whoami.c (new)
      +}
      
       ## command-list.txt ##
     -@@ command-list.txt: git-verify-pack                         plumbinginterrogators
     - git-verify-tag                          ancillaryinterrogators
     - git-version                             ancillaryinterrogators
     - git-whatchanged                         ancillaryinterrogators          complete
     -+git-whoami                              ancillaryinterrogators
     - git-worktree                            mainporcelain
     - git-write-tree                          plumbingmanipulators
     - gitattributes                           userinterfaces
     +@@ command-list.txt: git-hash-object                         plumbingmanipulators
     + git-help                                ancillaryinterrogators          complete
     + git-history                             mainporcelain           history
     + git-hook                                purehelpers
     ++git-ident                               ancillaryinterrogators
     + git-http-backend                        synchingrepositories
     + git-http-fetch                          synchelpers
     + git-http-push                           synchelpers
      
       ## git.c ##
      @@ git.c: static struct cmd_struct commands[] = {
     - #ifndef WITH_BREAKING_CHANGES
     - 	{ "whatchanged", cmd_whatchanged, RUN_SETUP | DEPRECATED },
     - #endif
     -+	{ "whoami", cmd_whoami, RUN_SETUP_GENTLY },
     - 	{ "worktree", cmd_worktree, RUN_SETUP },
     - 	{ "write-tree", cmd_write_tree, RUN_SETUP },
     - };
     + 	{ "help", cmd_help },
     + 	{ "history", cmd_history, RUN_SETUP },
     + 	{ "hook", cmd_hook, RUN_SETUP_GENTLY },
     ++	{ "ident", cmd_ident, RUN_SETUP_GENTLY },
     + 	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
     + 	{ "init", cmd_init_db },
     + 	{ "init-db", cmd_init_db },
      
       ## meson.build ##
      @@ meson.build: builtin_sources = [
     -   'builtin/verify-commit.c',
     -   'builtin/verify-pack.c',
     -   'builtin/verify-tag.c',
     -+  'builtin/whoami.c',
     -   'builtin/worktree.c',
     -   'builtin/write-tree.c',
     - ]
     +   'builtin/help.c',
     +   'builtin/history.c',
     +   'builtin/hook.c',
     ++  'builtin/ident.c',
     +   'builtin/index-pack.c',
     +   'builtin/init-db.c',
     +   'builtin/interpret-trailers.c',
      
       ## t/meson.build ##
      @@ t/meson.build: integration_tests = [
         't0012-help.sh',
         't0013-sha1dc.sh',
         't0014-alias.sh',
     -+  't0015-whoami.sh',
     ++  't0015-ident.sh',
         't0017-env-helper.sh',
         't0018-advice.sh',
         't0019-json-writer.sh',
      
     - ## t/t0015-whoami.sh (new) ##
     + ## t/t0015-ident.sh (new) ##
      @@
      +#!/bin/sh
      +
     -+test_description='basic sanity checks for git whoami'
     ++test_description='basic sanity checks for git ident'
      +
      +. ./test-lib.sh
      +. "$TEST_DIRECTORY/lib-gpg.sh"
      +
     -+test_expect_success 'default output format without signing' '
     ++test_expect_success 'default output format (author + committer)' '
     ++	cat >expect <<-EOF &&
     ++	$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
     ++	$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
     ++	EOF
     ++	git ident >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'git ident -v / --verbose default' '
      +	cat >expect <<-EOF &&
     -+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
     ++	Author: $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
      +	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
     -+	Signing:   disabled (commit.gpgsign: false)
      +	EOF
     -+	git whoami >actual &&
     ++	git ident -v >actual &&
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'git whoami --author' '
     ++test_expect_success 'git ident --author / -a' '
      +	echo "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" >expect &&
     -+	git whoami --author >actual &&
     -+	test_cmp expect actual
     ++	git ident --author >actual &&
     ++	test_cmp expect actual &&
     ++	git ident -a >actual_short &&
     ++	test_cmp expect actual_short
      +'
      +
     -+test_expect_success 'git whoami --committer' '
     ++test_expect_success 'git ident --committer / -c' '
      +	echo "$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" >expect &&
     -+	git whoami --committer >actual &&
     ++	git ident --committer >actual &&
     ++	test_cmp expect actual &&
     ++	git ident -c >actual_short &&
     ++	test_cmp expect actual_short
     ++'
     ++
     ++test_expect_success 'git ident -a -c' '
     ++	cat >expect <<-EOF &&
     ++	$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
     ++	$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
     ++	EOF
     ++	git ident -a -c >actual &&
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'git whoami --author --name and --author --email' '
     ++test_expect_success 'git ident -a -n and -a -e' '
      +	echo "$GIT_AUTHOR_NAME" >expect_name &&
     -+	git whoami --author --name >actual_name &&
     ++	git ident -a -n >actual_name &&
      +	test_cmp expect_name actual_name &&
     -+	echo "$GIT_AUTHOR_EMAIL" >expect_email &&
     -+	git whoami --author --email >actual_email &&
     ++	echo "<$GIT_AUTHOR_EMAIL>" >expect_email &&
     ++	git ident -a -e >actual_email &&
      +	test_cmp expect_email actual_email
      +'
      +
     -+test_expect_success 'git whoami --committer --name and --committer --email' '
     ++test_expect_success 'git ident -c -n and -c -e' '
      +	echo "$GIT_COMMITTER_NAME" >expect_name &&
     -+	git whoami --committer --name >actual_name &&
     ++	git ident -c -n >actual_name &&
      +	test_cmp expect_name actual_name &&
     -+	echo "$GIT_COMMITTER_EMAIL" >expect_email &&
     -+	git whoami --committer --email >actual_email &&
     ++	echo "<$GIT_COMMITTER_EMAIL>" >expect_email &&
     ++	git ident -c -e >actual_email &&
      +	test_cmp expect_email actual_email
      +'
      +
     -+test_expect_success 'git whoami --signing-key when signing is disabled and unset' '
     -+	test_config commit.gpgsign false &&
     -+	test_unconfig user.signingkey &&
     -+	test_must_fail git whoami --signing-key
     ++test_expect_success 'git ident -a -e -n (additive full ident)' '
     ++	echo "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" >expect &&
     ++	git ident -a -e -n >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'git ident -a -n -v (labeled name)' '
     ++	echo "Author: $GIT_AUTHOR_NAME" >expect &&
     ++	git ident -a -n -v >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'git ident -a -e -v (labeled email)' '
     ++	echo "Author: <$GIT_AUTHOR_EMAIL>" >expect &&
     ++	git ident -a -e -v >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'git ident -a -c -e (unlabeled emails)' '
     ++	cat >expect <<-EOF &&
     ++	<$GIT_AUTHOR_EMAIL>
     ++	<$GIT_COMMITTER_EMAIL>
     ++	EOF
     ++	git ident -a -c -e >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'git ident -a -c -e -v (labeled emails)' '
     ++	cat >expect <<-EOF &&
     ++	Author: <$GIT_AUTHOR_EMAIL>
     ++	Committer: <$GIT_COMMITTER_EMAIL>
     ++	EOF
     ++	git ident -a -c -e -v >actual &&
     ++	test_cmp expect actual
      +'
      +
     -+test_expect_success 'git whoami with explicitly configured signing key' '
     ++test_expect_success 'git ident -a -c -n -v (labeled names)' '
     ++	cat >expect <<-EOF &&
     ++	Author: $GIT_AUTHOR_NAME
     ++	Committer: $GIT_COMMITTER_NAME
     ++	EOF
     ++	git ident -a -c -n -v >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'git ident with -z on single fields' '
     ++	printf "$GIT_AUTHOR_NAME\0" >expect_name &&
     ++	git ident -a -n -z >actual_name &&
     ++	test_cmp expect_name actual_name &&
     ++	printf "<$GIT_AUTHOR_EMAIL>\0" >expect_email &&
     ++	git ident -a -e -z >actual_email &&
     ++	test_cmp expect_email actual_email
     ++'
     ++
     ++test_expect_success 'git ident with -z on multiple entries' '
     ++	printf "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\0$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\0" >expect &&
     ++	git ident -z >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'git ident --porcelain format' '
      +	test_config user.signingkey "TEST_KEY_123" &&
      +	test_config commit.gpgsign true &&
      +	test_config gpg.format ssh &&
      +	cat >expect <<-EOF &&
     -+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
     -+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
     -+	Signing:   TEST_KEY_123 (format: ssh, commit.gpgsign: true)
     ++	user.author.name=$GIT_AUTHOR_NAME
     ++	user.author.email=$GIT_AUTHOR_EMAIL
     ++	user.committer.name=$GIT_COMMITTER_NAME
     ++	user.committer.email=$GIT_COMMITTER_EMAIL
     ++	user.signingkey=TEST_KEY_123
     ++	gpg.format=ssh
     ++	commit.gpgsign=true
      +	EOF
     -+	git whoami >actual &&
     -+	test_cmp expect actual &&
     -+	echo "TEST_KEY_123" >expect_key &&
     -+	git whoami --signing-key >actual_key &&
     -+	test_cmp expect_key actual_key
     ++	git ident --porcelain >actual &&
     ++	test_cmp expect actual
      +'
      +
     -+test_expect_success 'git whoami with signing disabled but key configured' '
     -+	test_config user.signingkey "TEST_KEY_123" &&
     -+	test_config commit.gpgsign false &&
     -+	test_config gpg.format openpgp &&
     ++test_expect_success 'git ident -a --porcelain format' '
      +	cat >expect <<-EOF &&
     -+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
     -+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
     -+	Signing:   disabled (key: TEST_KEY_123, format: openpgp, commit.gpgsign: false)
     ++	user.author.name=$GIT_AUTHOR_NAME
     ++	user.author.email=$GIT_AUTHOR_EMAIL
      +	EOF
     -+	git whoami >actual &&
     -+	test_cmp expect actual &&
     ++	git ident -a --porcelain >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'git ident --porcelain -z format' '
     ++	test_config user.signingkey "TEST_KEY_123" &&
     ++	test_config commit.gpgsign true &&
     ++	test_config gpg.format ssh &&
     ++	printf "user.author.name=$GIT_AUTHOR_NAME\0user.author.email=$GIT_AUTHOR_EMAIL\0user.committer.name=$GIT_COMMITTER_NAME\0user.committer.email=$GIT_COMMITTER_EMAIL\0user.signingkey=TEST_KEY_123\0gpg.format=ssh\0commit.gpgsign=true\0" >expect &&
     ++	git ident --porcelain -z >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'git ident --signing-key when signing is disabled and unset' '
     ++	test_config commit.gpgsign false &&
     ++	test_unconfig user.signingkey &&
     ++	test_must_fail git ident --signing-key
     ++'
     ++
     ++test_expect_success 'git ident with explicitly configured signing key' '
     ++	test_config user.signingkey "TEST_KEY_123" &&
     ++	test_config commit.gpgsign true &&
     ++	test_config gpg.format ssh &&
      +	echo "TEST_KEY_123" >expect_key &&
     -+	git whoami --signing-key >actual_key &&
     ++	git ident --signing-key >actual_key &&
      +	test_cmp expect_key actual_key
      +'
      +
     -+test_expect_success 'git whoami with openpgp signing enabled without explicit key' '
     ++test_expect_success 'git ident with openpgp signing enabled without explicit key' '
      +	test_unconfig user.signingkey &&
      +	test_config commit.gpgsign true &&
      +	test_config gpg.format openpgp &&
     -+	cat >expect <<-EOF &&
     -+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
     -+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
     -+	Signing:   default key ($GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>) (format: openpgp, commit.gpgsign: true)
     -+	EOF
     -+	git whoami >actual &&
     -+	test_cmp expect actual &&
      +	echo "$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" >expect_key &&
     -+	git whoami --signing-key >actual_key &&
     ++	git ident --signing-key >actual_key &&
      +	test_cmp expect_key actual_key
      +'
      +
     -+test_expect_success 'git whoami with ssh signing enabled without explicit key' '
     ++test_expect_success 'git ident with ssh signing enabled without explicit key' '
      +	test_unconfig user.signingkey &&
      +	test_config commit.gpgsign true &&
      +	test_config gpg.format ssh &&
      +	test_unconfig gpg.ssh.defaultKeyCommand &&
     -+	cat >expect <<-EOF &&
     -+	Author:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
     -+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
     -+	Signing:   enabled (no signing key configured)
     -+	EOF
     -+	git whoami >actual &&
     -+	test_cmp expect actual &&
     -+	test_must_fail git whoami --signing-key
     ++	test_must_fail git ident --signing-key
      +'
      +
     -+test_expect_success GPGSSH 'git whoami with ssh defaultKeyCommand' '
     ++test_expect_success GPGSSH 'git ident with ssh defaultKeyCommand' '
      +	test_unconfig user.signingkey &&
      +	test_config commit.gpgsign true &&
      +	test_config gpg.format ssh &&
      +	test_config gpg.ssh.defaultKeyCommand "cat \"${GPGSSH_KEY_PRIMARY}.pub\"" &&
     -+	git whoami --signing-key >actual_key &&
     ++	git ident --signing-key >actual_key &&
      +	test_grep "^SHA256:" actual_key
      +'
      +
     -+test_expect_success 'git whoami -v / --verbose' '
     -+	test_config user.signingkey "MY_SIGNING_KEY" &&
     -+	test_config commit.gpgsign true &&
     -+	test_config gpg.format openpgp &&
     -+	cat >expect <<-EOF &&
     -+	Author Name:      $GIT_AUTHOR_NAME
     -+	Author Email:     $GIT_AUTHOR_EMAIL
     -+	Committer Name:   $GIT_COMMITTER_NAME
     -+	Committer Email:  $GIT_COMMITTER_EMAIL
     -+	Signing Key:      MY_SIGNING_KEY
     -+	Signing Format:   openpgp
     -+	GPG Signing:      enabled
     -+	EOF
     -+	git whoami -v >actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'git whoami with environment variable overrides' '
     ++test_expect_success 'git ident with environment variable overrides' '
      +	test_unconfig user.signingkey &&
      +	test_config commit.gpgsign false &&
      +	cat >expect <<-EOF &&
     -+	Author:    Custom Author <custom.author@example.com>
     -+	Committer: Custom Committer <custom.committer@example.com>
     -+	Signing:   disabled (commit.gpgsign: false)
     ++	Custom Author <custom.author@example.com>
     ++	Custom Committer <custom.committer@example.com>
      +	EOF
      +	GIT_AUTHOR_NAME="Custom Author" \
      +	GIT_AUTHOR_EMAIL="custom.author@example.com" \
      +	GIT_COMMITTER_NAME="Custom Committer" \
      +	GIT_COMMITTER_EMAIL="custom.committer@example.com" \
     -+	git whoami >actual &&
     ++	git ident >actual &&
      +	test_cmp expect actual
      +'
      +
      +test_expect_success 'incompatible option combinations fail' '
     -+	test_must_fail git whoami --author --committer 2>err &&
     -+	test_grep "cannot be used together" err &&
     -+	test_must_fail git whoami --name --email 2>err &&
     -+	test_grep "cannot be used together" err &&
     -+	test_must_fail git whoami --signing-key --name 2>err &&
     -+	test_grep "cannot be used together" err &&
     -+	test_must_fail git whoami --signing-key --email 2>err &&
     -+	test_grep "cannot be used together" err &&
     -+	test_must_fail git whoami --signing-key --author 2>err &&
     ++	test_must_fail git ident --porcelain --verbose 2>err &&
      +	test_grep "cannot be used together" err &&
     -+	test_must_fail git whoami --signing-key --committer 2>err &&
     ++	test_must_fail git ident --porcelain --name 2>err &&
      +	test_grep "cannot be used together" err &&
     -+	test_must_fail git whoami --verbose --name 2>err &&
     ++	test_must_fail git ident --porcelain --email 2>err &&
      +	test_grep "cannot be used together" err
      +'
      +
     -+test_expect_success 'git whoami outside of repository' '
     -+	nongit git whoami --author >actual &&
     ++test_expect_success 'git ident outside of repository' '
     ++	nongit git ident --author >actual &&
      +	test_grep "<" actual
      +'
      +
 Documentation/git-ident.adoc | 116 +++++++++++++++++
 Documentation/meson.build    |   1 +
 Makefile                     |   1 +
 builtin.h                    |   2 +-
 builtin/ident.c              | 243 +++++++++++++++++++++++++++++++++++
 command-list.txt             |   1 +
 git.c                        |   1 +
 meson.build                  |   1 +
 t/meson.build                |   1 +
 t/t0015-ident.sh             | 234 +++++++++++++++++++++++++++++++++
 10 files changed, 600 insertions(+), 1 deletion(-)
 create mode 100644 Documentation/git-ident.adoc
 create mode 100644 builtin/ident.c
 create mode 100755 t/t0015-ident.sh
Show changes to 10 files +600 −1

Documentation/git-ident.adoc, Documentation/meson.build, Makefile, builtin.h, builtin/ident.c, command-list.txt, git.c, meson.build, t/meson.build, t/t0015-ident.sh

diff --git a/Documentation/git-ident.adoc b/Documentation/git-ident.adoc
new file mode 100644
index 0000000000..292a361c9b
--- /dev/null
+++ b/Documentation/git-ident.adoc
@@ -0,0 +1,116 @@
+git-ident(1)
+============
+
+NAME
+----
+git-ident - Show current user identity and commit signing information
+
+
+SYNOPSIS
+--------
+[synopsis]
+git ident [<options>]
+
+DESCRIPTION
+-----------
+Displays the author and committer identity (name and email address) and
+optionally commit signing configuration that will be used when recording
+Git commits.
+
+The identity selectors (`-a` / `--author`, `-c` / `--committer`) and component
+selectors (`-n` / `--name`, `-e` / `--email`) are additive and composable:
+
+* If neither `--author` nor `--committer` is specified, both identities are shown.
+* If neither `--name` nor `--email` is specified (or if both are specified),
+  the full identity format (`Name <email>`) is shown.
+* If `-v` / `--verbose` is specified, entries are labeled with their role
+  (`Author: ` / `Committer: `).
+
+OPTIONS
+-------
+`-a`::
+`--author`::
+	Show author identity.
+
+`-c`::
+`--committer`::
+	Show committer identity.
+
+`-n`::
+`--name`::
+	Show name only.
+
+`-e`::
+`--email`::
+	Show email address only (formatted as `<email>`).
+
+`-s`::
+`--signing-key`::
+	Show the signing key that Git would resolve for signing commits.
+	Resolution proceeds in the following order:
+	* The explicitly configured `user.signingKey`, if set.
+	* If `gpg.format` is set to `ssh` and `gpg.ssh.defaultKeyCommand`
+	  is configured, executes the command to obtain the public key.
+	* If `gpg.format` is `openpgp` or `x509` and `commit.gpgsign` is
+	  enabled, falls back to the committer identity.
+	* If signing is disabled (`commit.gpgsign=false`) and no explicit key
+	  is configured, exits with status 1.
+
+`--porcelain`::
+	Give the output in an easy-to-parse format for scripts.
+	Outputs key-value pairs (`user.author.name`, `user.author.email`,
+	`user.committer.name`, `user.committer.email`, `user.signingkey`,
+	`gpg.format`, `commit.gpgsign`).
+
+`-z`::
+`--null`::
+	Terminate output lines or entries with NUL instead of a newline.
+
+`-v`::
+`--verbose`::
+	Prepend role labels (`Author: `, `Committer: `) to identity entries.
+
+EXAMPLES
+--------
+
+* Show full author and committer identities:
++
+------------
+$ git ident
+Author Name <author@example.com>
+Committer Name <committer@example.com>
+------------
+
+* Show author identity only:
++
+------------
+$ git ident -a -e -n
+Author Name <author@example.com>
+------------
+
+* Show labeled author name:
++
+------------
+$ git ident -a -n -v
+Author: Author Name
+------------
+
+* Show author and committer email addresses:
++
+------------
+$ git ident -a -c -e
+<author@example.com>
+<committer@example.com>
+------------
+
+* Show labeled author and committer email addresses:
++
+------------
+$ git ident -a -c -e -v
+Author: <author@example.com>
+Committer: <committer@example.com>
+------------
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index f4854f802d..864e5f620c 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -67,6 +67,7 @@ manpages = {
   'git-help.adoc' : 1,
   'git-history.adoc' : 1,
   'git-hook.adoc' : 1,
+  'git-ident.adoc' : 1,
   'git-http-backend.adoc' : 1,
   'git-http-fetch.adoc' : 1,
   'git-http-push.adoc' : 1,
diff --git a/Makefile b/Makefile
index d4b775953d..359d8a8917 100644
--- a/Makefile
+++ b/Makefile
@@ -1442,6 +1442,7 @@ BUILTIN_OBJS += builtin/hash-object.o
 BUILTIN_OBJS += builtin/help.o
 BUILTIN_OBJS += builtin/history.o
 BUILTIN_OBJS += builtin/hook.o
+BUILTIN_OBJS += builtin/ident.o
 BUILTIN_OBJS += builtin/index-pack.o
 BUILTIN_OBJS += builtin/init-db.o
 BUILTIN_OBJS += builtin/interpret-trailers.o
diff --git a/builtin.h b/builtin.h
index 4e47a4ebd3..b17abed3d4 100644
--- a/builtin.h
+++ b/builtin.h
@@ -199,6 +199,7 @@ int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repo
 int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
+int cmd_ident(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_interpret_trailers(int argc, const char **argv, const char *prefix, struct repository *repo);
@@ -284,5 +285,4 @@ int cmd_verify_pack(int argc, const char **argv, const char *prefix, struct repo
 int cmd_show_ref(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_pack_refs(int argc, const char **argv, const char *prefix, struct repository *repo);
 int cmd_replace(int argc, const char **argv, const char *prefix, struct repository *repo);
-
 #endif
diff --git a/builtin/ident.c b/builtin/ident.c
new file mode 100644
index 0000000000..3571d8ad66
--- /dev/null
+++ b/builtin/ident.c
@@ -0,0 +1,243 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "builtin.h"
+#include "config.h"
+#include "gettext.h"
+#include "gpg-interface.h"
+#include "ident.h"
+#include "parse-options.h"
+#include "strbuf.h"
+
+static const char * const ident_usage[] = {
+	N_("git ident [<options>]"),
+	NULL
+};
+
+enum ident_who {
+	IDENT_AUTHOR = 1 << 0,
+	IDENT_COMMITTER = 1 << 1,
+};
+
+enum ident_part {
+	IDENT_NAME = 1 << 0,
+	IDENT_EMAIL = 1 << 1,
+};
+
+struct ident_config {
+	int gpgsign;
+	char *signing_key;
+	char *gpg_format;
+	char *ssh_default_key_cmd;
+};
+
+static int ident_config_cb(const char *var, const char *value,
+			   const struct config_context *ctx, void *data)
+{
+	struct ident_config *cfg = data;
+
+	if (!strcmp(var, "commit.gpgsign")) {
+		cfg->gpgsign = git_config_bool(var, value);
+		return 0;
+	}
+	if (!strcmp(var, "user.signingkey"))
+		return git_config_string(&cfg->signing_key, var, value);
+	if (!strcmp(var, "gpg.format"))
+		return git_config_string(&cfg->gpg_format, var, value);
+	if (!strcmp(var, "gpg.ssh.defaultkeycommand"))
+		return git_config_string(&cfg->ssh_default_key_cmd, var, value);
+
+	return git_default_config(var, value, ctx, data);
+}
+
+static void print_ident_entry(const char *label,
+			      const struct strbuf *name,
+			      const struct strbuf *email,
+			      int parts,
+			      int verbose,
+			      char eol)
+{
+	struct strbuf out = STRBUF_INIT;
+
+	if (verbose && label)
+		strbuf_addf(&out, "%s: ", label);
+
+	if ((parts & IDENT_NAME) && (parts & IDENT_EMAIL))
+		strbuf_addf(&out, "%s <%s>", name->buf, email->buf);
+	else if (parts & IDENT_NAME)
+		strbuf_addbuf(&out, name);
+	else if (parts & IDENT_EMAIL)
+		strbuf_addf(&out, "<%s>", email->buf);
+
+	printf("%s%c", out.buf, eol);
+	strbuf_release(&out);
+}
+
+int cmd_ident(int argc,
+	      const char **argv,
+	      const char *prefix,
+	      struct repository *repo)
+{
+	int show_author = 0;
+	int show_committer = 0;
+	int show_name = 0;
+	int show_email = 0;
+	int show_signing_key = 0;
+	int porcelain = 0;
+	int nul_term = 0;
+	int verbose = 0;
+	int ret = 0;
+	char eol;
+	int selected_who = 0;
+	int selected_parts = 0;
+
+	struct option ident_options[] = {
+		OPT_BOOL('a', "author", &show_author, N_("show author identity")),
+		OPT_BOOL('c', "committer", &show_committer, N_("show committer identity")),
+		OPT_BOOL('n', "name", &show_name, N_("show name only")),
+		OPT_BOOL('e', "email", &show_email, N_("show email only")),
+		OPT_BOOL('s', "signing-key", &show_signing_key, N_("show commit signing key")),
+		OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
+		OPT_BOOL('z', "null", &nul_term, N_("terminate entries with NUL")),
+		OPT__VERBOSE(&verbose, N_("show detailed identity")),
+		OPT_END()
+	};
+
+	struct ident_config cfg = { 0 };
+	struct strbuf author_info = STRBUF_INIT;
+	struct strbuf committer_info = STRBUF_INIT;
+	struct ident_split author_split, committer_split;
+	struct strbuf author_name = STRBUF_INIT;
+	struct strbuf author_email = STRBUF_INIT;
+	struct strbuf committer_name = STRBUF_INIT;
+	struct strbuf committer_email = STRBUF_INIT;
+	char *resolved_key = NULL;
+	int is_ssh = 0;
+
+	argc = parse_options(argc, argv, prefix, ident_options,
+			     ident_usage, 0);
+
+	if (argc > 0)
+		usage_with_options(ident_usage, ident_options);
+
+	die_for_incompatible_opt2(porcelain, "--porcelain", verbose, "--verbose");
+	die_for_incompatible_opt2(porcelain, "--porcelain", show_name, "--name");
+	die_for_incompatible_opt2(porcelain, "--porcelain", show_email, "--email");
+
+	eol = nul_term ? '\0' : '\n';
+
+	repo_config(repo, ident_config_cb, &cfg);
+
+	strbuf_addstr(&author_info, git_author_info(IDENT_NO_DATE));
+	strbuf_addstr(&committer_info, git_committer_info(IDENT_NO_DATE));
+
+	if (split_ident_line(&author_split, author_info.buf, author_info.len) == 0) {
+		if (author_split.name_begin && author_split.name_end)
+			strbuf_add(&author_name, author_split.name_begin,
+				   author_split.name_end - author_split.name_begin);
+		if (author_split.mail_begin && author_split.mail_end)
+			strbuf_add(&author_email, author_split.mail_begin,
+				   author_split.mail_end - author_split.mail_begin);
+	}
+
+	if (split_ident_line(&committer_split, committer_info.buf, committer_info.len) == 0) {
+		if (committer_split.name_begin && committer_split.name_end)
+			strbuf_add(&committer_name, committer_split.name_begin,
+				   committer_split.name_end - committer_split.name_begin);
+		if (committer_split.mail_begin && committer_split.mail_end)
+			strbuf_add(&committer_email, committer_split.mail_begin,
+				   committer_split.mail_end - committer_split.mail_begin);
+	}
+
+	is_ssh = cfg.gpg_format && !strcmp(cfg.gpg_format, "ssh");
+
+	if (cfg.signing_key && *cfg.signing_key) {
+		resolved_key = xstrdup(cfg.signing_key);
+	} else if (is_ssh) {
+		if (cfg.ssh_default_key_cmd && *cfg.ssh_default_key_cmd)
+			resolved_key = get_signing_key_id();
+	} else if (cfg.gpgsign) {
+		resolved_key = get_signing_key_id();
+	}
+
+	if (show_signing_key && !show_author && !show_committer && !show_name && !show_email && !porcelain) {
+		if (resolved_key && *resolved_key) {
+			if (verbose)
+				printf(_("Signing Key: %s\n"), resolved_key);
+			else
+				printf("%s%c", resolved_key, eol);
+			ret = 0;
+		} else {
+			ret = 1;
+		}
+		goto cleanup;
+	}
+
+	if (show_author)
+		selected_who |= IDENT_AUTHOR;
+	if (show_committer)
+		selected_who |= IDENT_COMMITTER;
+	if (!selected_who)
+		selected_who = IDENT_AUTHOR | IDENT_COMMITTER;
+
+	if (show_name)
+		selected_parts |= IDENT_NAME;
+	if (show_email)
+		selected_parts |= IDENT_EMAIL;
+	if (!selected_parts)
+		selected_parts = IDENT_NAME | IDENT_EMAIL;
+
+	if (porcelain) {
+		if (selected_who & IDENT_AUTHOR) {
+			printf("user.author.name=%s%c", author_name.buf, eol);
+			printf("user.author.email=%s%c", author_email.buf, eol);
+		}
+		if (selected_who & IDENT_COMMITTER) {
+			printf("user.committer.name=%s%c", committer_name.buf, eol);
+			printf("user.committer.email=%s%c", committer_email.buf, eol);
+		}
+		if ((selected_who & (IDENT_AUTHOR | IDENT_COMMITTER)) == (IDENT_AUTHOR | IDENT_COMMITTER)) {
+			printf("user.signingkey=%s%c",
+			       (cfg.signing_key && *cfg.signing_key) ? cfg.signing_key :
+			       (resolved_key && *resolved_key) ? resolved_key : "none",
+			       eol);
+			printf("gpg.format=%s%c",
+			       cfg.gpg_format ? cfg.gpg_format : "openpgp", eol);
+			printf("commit.gpgsign=%s%c",
+			       cfg.gpgsign ? "true" : "false", eol);
+		}
+		goto cleanup;
+	}
+
+	if (selected_who & IDENT_AUTHOR)
+		print_ident_entry("Author", &author_name, &author_email,
+				  selected_parts, verbose, eol);
+
+	if (selected_who & IDENT_COMMITTER)
+		print_ident_entry("Committer", &committer_name, &committer_email,
+				  selected_parts, verbose, eol);
+
+	if (show_signing_key) {
+		if (resolved_key && *resolved_key) {
+			if (verbose)
+				printf(_("Signing Key: %s\n"), resolved_key);
+			else
+				printf("%s%c", resolved_key, eol);
+		} else {
+			ret = 1;
+		}
+	}
+
+cleanup:
+	free(cfg.signing_key);
+	free(cfg.gpg_format);
+	free(cfg.ssh_default_key_cmd);
+	free(resolved_key);
+	strbuf_release(&author_info);
+	strbuf_release(&committer_info);
+	strbuf_release(&author_name);
+	strbuf_release(&author_email);
+	strbuf_release(&committer_name);
+	strbuf_release(&committer_email);
+
+	return ret;
+}
diff --git a/command-list.txt b/command-list.txt
index 21b802c420..39a22c271f 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -118,6 +118,7 @@ git-hash-object                         plumbingmanipulators
 git-help                                ancillaryinterrogators          complete
 git-history                             mainporcelain           history
 git-hook                                purehelpers
+git-ident                               ancillaryinterrogators
 git-http-backend                        synchingrepositories
 git-http-fetch                          synchelpers
 git-http-push                           synchelpers
diff --git a/git.c b/git.c
index 96df15b5cd..926187351a 100644
--- a/git.c
+++ b/git.c
@@ -593,6 +593,7 @@ static struct cmd_struct commands[] = {
 	{ "help", cmd_help },
 	{ "history", cmd_history, RUN_SETUP },
 	{ "hook", cmd_hook, RUN_SETUP_GENTLY },
+	{ "ident", cmd_ident, RUN_SETUP_GENTLY },
 	{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
 	{ "init", cmd_init_db },
 	{ "init-db", cmd_init_db },
diff --git a/meson.build b/meson.build
index d86f2acd2b..37821ed890 100644
--- a/meson.build
+++ b/meson.build
@@ -654,6 +654,7 @@ builtin_sources = [
   'builtin/help.c',
   'builtin/history.c',
   'builtin/hook.c',
+  'builtin/ident.c',
   'builtin/index-pack.c',
   'builtin/init-db.c',
   'builtin/interpret-trailers.c',
diff --git a/t/meson.build b/t/meson.build
index 181d61a8a0..3738d86140 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -87,6 +87,7 @@ integration_tests = [
   't0012-help.sh',
   't0013-sha1dc.sh',
   't0014-alias.sh',
+  't0015-ident.sh',
   't0017-env-helper.sh',
   't0018-advice.sh',
   't0019-json-writer.sh',
diff --git a/t/t0015-ident.sh b/t/t0015-ident.sh
new file mode 100755
index 0000000000..8b6358e771
--- /dev/null
+++ b/t/t0015-ident.sh
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+test_description='basic sanity checks for git ident'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-gpg.sh"
+
+test_expect_success 'default output format (author + committer)' '
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
+	$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
+	EOF
+	git ident >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident -v / --verbose default' '
+	cat >expect <<-EOF &&
+	Author: $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
+	Committer: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
+	EOF
+	git ident -v >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident --author / -a' '
+	echo "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" >expect &&
+	git ident --author >actual &&
+	test_cmp expect actual &&
+	git ident -a >actual_short &&
+	test_cmp expect actual_short
+'
+
+test_expect_success 'git ident --committer / -c' '
+	echo "$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" >expect &&
+	git ident --committer >actual &&
+	test_cmp expect actual &&
+	git ident -c >actual_short &&
+	test_cmp expect actual_short
+'
+
+test_expect_success 'git ident -a -c' '
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>
+	$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>
+	EOF
+	git ident -a -c >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident -a -n and -a -e' '
+	echo "$GIT_AUTHOR_NAME" >expect_name &&
+	git ident -a -n >actual_name &&
+	test_cmp expect_name actual_name &&
+	echo "<$GIT_AUTHOR_EMAIL>" >expect_email &&
+	git ident -a -e >actual_email &&
+	test_cmp expect_email actual_email
+'
+
+test_expect_success 'git ident -c -n and -c -e' '
+	echo "$GIT_COMMITTER_NAME" >expect_name &&
+	git ident -c -n >actual_name &&
+	test_cmp expect_name actual_name &&
+	echo "<$GIT_COMMITTER_EMAIL>" >expect_email &&
+	git ident -c -e >actual_email &&
+	test_cmp expect_email actual_email
+'
+
+test_expect_success 'git ident -a -e -n (additive full ident)' '
+	echo "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>" >expect &&
+	git ident -a -e -n >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident -a -n -v (labeled name)' '
+	echo "Author: $GIT_AUTHOR_NAME" >expect &&
+	git ident -a -n -v >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident -a -e -v (labeled email)' '
+	echo "Author: <$GIT_AUTHOR_EMAIL>" >expect &&
+	git ident -a -e -v >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident -a -c -e (unlabeled emails)' '
+	cat >expect <<-EOF &&
+	<$GIT_AUTHOR_EMAIL>
+	<$GIT_COMMITTER_EMAIL>
+	EOF
+	git ident -a -c -e >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident -a -c -e -v (labeled emails)' '
+	cat >expect <<-EOF &&
+	Author: <$GIT_AUTHOR_EMAIL>
+	Committer: <$GIT_COMMITTER_EMAIL>
+	EOF
+	git ident -a -c -e -v >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident -a -c -n -v (labeled names)' '
+	cat >expect <<-EOF &&
+	Author: $GIT_AUTHOR_NAME
+	Committer: $GIT_COMMITTER_NAME
+	EOF
+	git ident -a -c -n -v >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident with -z on single fields' '
+	printf "$GIT_AUTHOR_NAME\0" >expect_name &&
+	git ident -a -n -z >actual_name &&
+	test_cmp expect_name actual_name &&
+	printf "<$GIT_AUTHOR_EMAIL>\0" >expect_email &&
+	git ident -a -e -z >actual_email &&
+	test_cmp expect_email actual_email
+'
+
+test_expect_success 'git ident with -z on multiple entries' '
+	printf "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\0$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\0" >expect &&
+	git ident -z >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident --porcelain format' '
+	test_config user.signingkey "TEST_KEY_123" &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format ssh &&
+	cat >expect <<-EOF &&
+	user.author.name=$GIT_AUTHOR_NAME
+	user.author.email=$GIT_AUTHOR_EMAIL
+	user.committer.name=$GIT_COMMITTER_NAME
+	user.committer.email=$GIT_COMMITTER_EMAIL
+	user.signingkey=TEST_KEY_123
+	gpg.format=ssh
+	commit.gpgsign=true
+	EOF
+	git ident --porcelain >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident -a --porcelain format' '
+	cat >expect <<-EOF &&
+	user.author.name=$GIT_AUTHOR_NAME
+	user.author.email=$GIT_AUTHOR_EMAIL
+	EOF
+	git ident -a --porcelain >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident --porcelain -z format' '
+	test_config user.signingkey "TEST_KEY_123" &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format ssh &&
+	printf "user.author.name=$GIT_AUTHOR_NAME\0user.author.email=$GIT_AUTHOR_EMAIL\0user.committer.name=$GIT_COMMITTER_NAME\0user.committer.email=$GIT_COMMITTER_EMAIL\0user.signingkey=TEST_KEY_123\0gpg.format=ssh\0commit.gpgsign=true\0" >expect &&
+	git ident --porcelain -z >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git ident --signing-key when signing is disabled and unset' '
+	test_config commit.gpgsign false &&
+	test_unconfig user.signingkey &&
+	test_must_fail git ident --signing-key
+'
+
+test_expect_success 'git ident with explicitly configured signing key' '
+	test_config user.signingkey "TEST_KEY_123" &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format ssh &&
+	echo "TEST_KEY_123" >expect_key &&
+	git ident --signing-key >actual_key &&
+	test_cmp expect_key actual_key
+'
+
+test_expect_success 'git ident with openpgp signing enabled without explicit key' '
+	test_unconfig user.signingkey &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format openpgp &&
+	echo "$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" >expect_key &&
+	git ident --signing-key >actual_key &&
+	test_cmp expect_key actual_key
+'
+
+test_expect_success 'git ident with ssh signing enabled without explicit key' '
+	test_unconfig user.signingkey &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format ssh &&
+	test_unconfig gpg.ssh.defaultKeyCommand &&
+	test_must_fail git ident --signing-key
+'
+
+test_expect_success GPGSSH 'git ident with ssh defaultKeyCommand' '
+	test_unconfig user.signingkey &&
+	test_config commit.gpgsign true &&
+	test_config gpg.format ssh &&
+	test_config gpg.ssh.defaultKeyCommand "cat \"${GPGSSH_KEY_PRIMARY}.pub\"" &&
+	git ident --signing-key >actual_key &&
+	test_grep "^SHA256:" actual_key
+'
+
+test_expect_success 'git ident with environment variable overrides' '
+	test_unconfig user.signingkey &&
+	test_config commit.gpgsign false &&
+	cat >expect <<-EOF &&
+	Custom Author <custom.author@example.com>
+	Custom Committer <custom.committer@example.com>
+	EOF
+	GIT_AUTHOR_NAME="Custom Author" \
+	GIT_AUTHOR_EMAIL="custom.author@example.com" \
+	GIT_COMMITTER_NAME="Custom Committer" \
+	GIT_COMMITTER_EMAIL="custom.committer@example.com" \
+	git ident >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'incompatible option combinations fail' '
+	test_must_fail git ident --porcelain --verbose 2>err &&
+	test_grep "cannot be used together" err &&
+	test_must_fail git ident --porcelain --name 2>err &&
+	test_grep "cannot be used together" err &&
+	test_must_fail git ident --porcelain --email 2>err &&
+	test_grep "cannot be used together" err
+'
+
+test_expect_success 'git ident outside of repository' '
+	nongit git ident --author >actual &&
+	test_grep "<" actual
+'
+
+test_done

base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
-- 
gitgitgadget
Jeff KingSep 1, 2026, 04:39 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH v2] builtin/ident: add new 'ident' command

On Mon, Aug 31, 2026 at 11:59:06PM +0000, Andrew Pleeter via GitGitGadget wrote:
Show 7 quoted lines
> While existing plumbing commands like 'git var' and 'git config' expose
> individual pieces of identity and configuration, discovering what identity
> and signing key will actually be attached to a new commit requires multiple
> independent queries and manual correlation. 'git config' only reads raw
> values without performing environment overrides or GECOS detection, while
> 'git var' returns full ident strings with timestamps without exposing
> commit signing status.

This is just my gut reaction, but: would it be simpler to teach git var to provide those broken-down pieces than to introduce a whole new command?

This works now:
  git var GIT_COMMITTER_IDENT
but why not:
  git var GIT_COMMITTER_NAME
  git var GIT_COMMITTER_EMAIL
  git var GIT_COMMITTER_DATE

Those are well-known names already; they're what we use for reading in the broken-down values from the environment. And likewise for GIT_AUTHOR_*.

Let's see what else is in your feature list:
> 'git ident' provides a unified command with additive, composable options:
>   - Identity scope selectors (-a / --author, -c / --committer) choose
>     which identities to format (defaulting to both when neither is specified).
I think that works by switching between the two var families above.
>   - Component selectors (-n / --name, -e / --email) choose which parts
>     to format (defaulting to full 'Name <email>' when neither or both are
>     specified).

We don't allow mix-and-match here (nor even multiple values!), so you'd have to do:

  ident="$(git var GIT_AUTHOR_NAME) <$(git var GIT_AUTHOR_EMAIL)"

I think it would be reasonable for git-var to accept multiple values and output them one per line (or with NULs via "-z"). That doesn't really make things easier in shell, but it might help scripts in other languages.

We _could_ go as far as providing a format language like we do in for-each-ref, etc, where we offer to shell-quote. And then you can do:

  eval "$(git var --shell-quote --format='
		name=%(GIT_AUTHOR_NAME)
		email=%(GIT_AUTHOR_EMAIL)
	')"

but IMHO that is probably going too far. It sometimes lets you simplify shell use of the tool at the expense of a weird and complicated interface (I kind of which we didn't have it in for-each-ref).

>   - -v / --verbose prepends 'Author: ' or 'Committer: ' role labels.
Seems like something that git-var might benefit from, too.
>   - -s / --signing-key resolves and outputs the commit signing key.
Likewise, this feels like it should be a git-var entry.
>   - --porcelain produces machine-readable key-value pairs.
>   - -z / --null terminates output records with NUL bytes.
Likewise.
My main feeling on suggesting this is that:
  1. We already have a lot of commands, and this one feels very
     specialized.
  2. Most of these suggestions could make git-var better for reading
     idents _and_ for reading its other variables.
-Peff
Junio C HamanoSep 1, 2026, 05:00 UTC in reply to Jeff King on lore

Re: [PATCH v2] builtin/ident: add new 'ident' command

Jeff King <peff@peff.net> writes:
Show 13 quoted lines
> On Mon, Aug 31, 2026 at 11:59:06PM +0000, Andrew Pleeter via GitGitGadget wrote:
>
>> While existing plumbing commands like 'git var' and 'git config' expose
>> individual pieces of identity and configuration, discovering what identity
>> and signing key will actually be attached to a new commit requires multiple
>> independent queries and manual correlation. 'git config' only reads raw
>> values without performing environment overrides or GECOS detection, while
>> 'git var' returns full ident strings with timestamps without exposing
>> commit signing status.
>
> This is just my gut reaction, but: would it be simpler to teach git var
> to provide those broken-down pieces than to introduce a whole new
> command?

We long timers among our reviewer base already expressed the same sentiment.

Show 7 quoted lines
> My main feeling on suggesting this is that:
>
>   1. We already have a lot of commands, and this one feels very
>      specialized.
>
>   2. Most of these suggestions could make git-var better for reading
>      idents _and_ for reading its other variables.
And you said it the best among our responses, I think.
Thanks.
Andrew Pleeter via GitGitGadgetSep 3, 2026, 02:49 UTC in reply to Andrew Pleeter via GitGitGadget on lore

[PATCH v3] var: support broken-down idents, default key, multiple args, and -z

From: Andrew Pleeter <andrewpleeter@gmail.com>

While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT, extracting individual components (name, email, or date) currently requires callers to manually parse the composite string. Furthermore, there is no way to query the resolved commit signing key through 'git var', and the command only accepts a single variable at a time.

Teach 'git var' to expose individual identity components and commit signing configuration, and allow querying multiple variables with optional NUL-termination:

- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
- Add GIT_DEFAULT_KEY (with GIT_SIGNING_KEY alias) to resolve the
  configured or default commit signing key ID / fingerprint.
- Allow passing multiple variable arguments (e.g., 'git var
  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
  sequentially.
- Support '-z' to terminate variable outputs and 'git var -l -z'
  entries with NUL bytes.
- Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
    var: support broken-down idents, default key, multiple args, and -z
    
    Teach git var to expose individual identity components and commit
    signing configuration, and allow querying multiple variables with
    optional NUL-termination.
    
    Following discussion on v1/v2 with Jeff King and Junio C Hamano, rather
    than introducing a new standalone subcommand (whoami or ident), this
    version enhances git var:
    
    
    Changes since v2:
    =================
    
     * Drop git ident / git whoami subcommand entirely.
     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
     * Add GIT_DEFAULT_KEY (with GIT_SIGNING_KEY alias) to resolve commit
       signing keys.
     * Teach git var to accept multiple variable arguments (git var <var1>
       <var2> ...).
     * Add -z option to terminate outputs with NUL bytes (including git var
       -l -z).
     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v3
Pull-Request: https://github.com/git/git/pull/2388
Range-diff vs v2:
 1:  f322e7fedd < -:  ---------- builtin/ident: add new 'ident' command
 -:  ---------- > 1:  b86340dd0e var: support broken-down idents, default key, multiple args, and -z
 Documentation/git-var.adoc |  43 ++++++++-
 builtin/var.c              | 189 ++++++++++++++++++++++++++++++++-----
 t/t0007-git-var.sh         |  77 +++++++++++++++
 3 files changed, 284 insertions(+), 25 deletions(-)
Show changes to 3 files +284 −25

Documentation/git-var.adoc, builtin/var.c, t/t0007-git-var.sh

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>...)
 
 DESCRIPTION
 -----------
@@ -24,20 +24,55 @@ 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.
+
 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
+Eric W. Biederman
+ebiederm@lnxi.com
+------------
 
 VARIABLES
 ---------
 `GIT_AUTHOR_IDENT`::
     The author of a piece of code.
 
+`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.
+
 `GIT_COMMITTER_IDENT`::
     The person who put a piece of code into Git.
 
+`GIT_COMMITTER_NAME`::
+    The name of the person who put a piece of code into Git.
+
+`GIT_COMMITTER_EMAIL`::
+    The email of the person who put a piece of code into Git.
+
+`GIT_COMMITTER_DATE`::
+    The date and timezone of the person who put a piece of code into Git.
+
+`GIT_DEFAULT_KEY`::
+    The default commit signing key ID or fingerprint, if configured or enabled.
+
 `GIT_EDITOR`::
     Text editor for use by Git commands.  The value is meant to be
     interpreted by the shell when it is used.  Examples: `~/bin/vi`,
diff --git a/builtin/var.c b/builtin/var.c
index cc3a43cde2..94207cbc7c 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -12,25 +12,103 @@
 #include "config.h"
 #include "editor.h"
 #include "environment.h"
+#include "gpg-interface.h"
 #include "ident.h"
 #include "pager.h"
 #include "refs.h"
 #include "path.h"
 #include "strbuf.h"
+#include "strvec.h"
 #include "run-command.h"
 
-static const char var_usage[] = "git var (-l | <variable>)";
+static const char var_usage[] = "git var (-l [-z] | [-z] <variable>...)";
 
 static char *committer(int ident_flag)
 {
 	return xstrdup_or_null(git_committer_info(ident_flag));
 }
 
+static char *ident_part(const char *ident, char part)
+{
+	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;
+	default:
+		return NULL;
+	}
+}
+
+static char *committer_name(int ident_flag)
+{
+	return ident_part(git_committer_info(ident_flag), 'n');
+}
+
+static char *committer_email(int ident_flag)
+{
+	return ident_part(git_committer_info(ident_flag), 'e');
+}
+
+static char *committer_date(int ident_flag)
+{
+	return ident_part(git_committer_info(ident_flag), 'd');
+}
+
 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), 'n');
+}
+
+static char *author_email(int ident_flag)
+{
+	return ident_part(git_author_info(ident_flag), 'e');
+}
+
+static char *author_date(int ident_flag)
+{
+	return ident_part(git_author_info(ident_flag), 'd');
+}
+
+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;
+	free(signing_key);
+
+	if (repo_config_get_bool(the_repository, "commit.gpgsign", &gpgsign) == 0 && gpgsign)
+		return get_signing_key_id();
+
+	return NULL;
+}
+
 static char *editor(int ident_flag UNUSED)
 {
 	return xstrdup_or_null(git_editor());
@@ -125,10 +203,34 @@ 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,
@@ -145,6 +247,10 @@ static struct git_var git_vars[] = {
 		.name = "GIT_DEFAULT_BRANCH",
 		.read = default_branch,
 	},
+	{
+		.name = "GIT_DEFAULT_KEY",
+		.read = default_key,
+	},
 	{
 		.name = "GIT_SHELL_PATH",
 		.read = shell_path,
@@ -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);
 				string_list_clear(&list, 0);
 			} else {
-				printf("%s=%s\n", ptr->name, val);
+				printf("%s=%s%c", ptr->name, val, eol);
 			}
 			free(val);
 		}
@@ -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";
 	for (ptr = git_vars; ptr->read; ptr++) {
 		if (strcmp(var, ptr->name) == 0) {
 			return ptr;
@@ -207,10 +316,13 @@ 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 null_term = cb ? *(int *)cb : 0;
+	char eol = null_term ? '\0' : '\n';
+
 	if (value)
-		printf("%s=%s\n", var, value);
+		printf("%s=%s%c", var, value, eol);
 	else
-		printf("%s\n", var);
+		printf("%s%c", var, eol);
 	return git_default_config(var, value, ctx, cb);
 }
 
@@ -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;
+		} else if (arg[0] == '-') {
+			usage(var_usage);
+		} else {
+			strvec_push(&vars, arg);
+		}
+	}
+
+	if (list) {
+		if (vars.nr > 0) {
+			strvec_clear(&vars);
+			usage(var_usage);
+		}
+		repo_config(the_repository, show_config, &null_term);
+		list_vars(null_term);
 		return 0;
 	}
-	repo_config(the_repository, git_default_config, NULL);
 
-	git_var = get_git_var(argv[1]);
-	if (!git_var)
+	if (!vars.nr)
 		usage(var_usage);
 
-	val = git_var->read(IDENT_STRICT);
-	if (!val)
-		return 1;
+	repo_config(the_repository, git_default_config, NULL);
+
+	for (size_t j = 0; j < vars.nr; j++) {
+		const struct git_var *git_var = get_git_var(vars.v[j]);
+		char *val;
+
+		if (!git_var) {
+			strvec_clear(&vars);
+			usage(var_usage);
+		}
+
+		val = git_var->read(IDENT_STRICT);
+		if (!val) {
+			strvec_clear(&vars);
+			return 1;
+		}
 
-	printf("%s\n", val);
-	free(val);
+		printf("%s%c", val, null_term ? '\0' : '\n');
+		free(val);
+	}
 
+	strvec_clear(&vars);
 	return 0;
 }
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 2b60317758..c437c968bb 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -276,4 +276,81 @@ test_expect_success '`git var -l` works even without HOME' '
 	)
 '
 
+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 'get multiple variables' '
+	test_tick &&
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME
+	$GIT_AUTHOR_EMAIL
+	$GIT_COMMITTER_NAME
+	$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 'get multiple variables with -z' '
+	test_tick &&
+	printf "%s\0%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
+	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git var -l -z' '
+	git var -l -z >actual &&
+	tr "\0" "\n" <actual | grep "^GIT_AUTHOR_NAME=" >filtered &&
+	echo "GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME" >expect &&
+	test_cmp expect filtered
+'
+
+test_expect_success 'get GIT_DEFAULT_KEY with user.signingkey configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	echo "TEST_KEY_ID" >expect &&
+	git var GIT_DEFAULT_KEY >actual &&
+	test_cmp expect actual &&
+	git var GIT_SIGNING_KEY >actual.alias &&
+	test_cmp expect actual.alias
+'
+
+test_expect_success 'get GIT_DEFAULT_KEY fails when unset and signing disabled' '
+	test_config user.signingkey "" &&
+	test_config commit.gpgsign false &&
+	test_must_fail git var GIT_DEFAULT_KEY
+'
+
+test_expect_success 'git var -l lists new variables' '
+	git var -l >actual &&
+	grep "^GIT_AUTHOR_NAME=" actual &&
+	grep "^GIT_AUTHOR_EMAIL=" actual &&
+	grep "^GIT_AUTHOR_DATE=" actual &&
+	grep "^GIT_COMMITTER_NAME=" actual &&
+	grep "^GIT_COMMITTER_EMAIL=" actual &&
+	grep "^GIT_COMMITTER_DATE=" actual
+'
+
 test_done

base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
-- 
gitgitgadget
Junio C HamanoSep 3, 2026, 17:40 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z

"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.

Show 15 quoted lines
>  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.

Show 8 quoted lines
> +`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.

Show 25 quoted lines
> +{
> +	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;
> +	}
> +}
Show 7 quoted lines
> +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.

Show 9 quoted lines
> @@ -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.

Show 19 quoted lines
> @@ -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.
Show 6 quoted 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.

Show 32 quoted lines
>  	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?

Show 6 quoted lines
> +		} 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.
Ben KnobleSep 3, 2026, 18:22 UTC in reply to Junio C Hamano on lore

Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z

Show 11 quoted lines
> Le 3 sept. 2026 à 14:02, Junio C Hamano <gitster@pobox.com> a écrit :
> 
> "Andrew Pleeter via GitGitGadget" <gitgitgadget@gmail.com> writes:
> 
>> +    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.
I believe « git var » prints usage and exits 129 today, at least on my 2.55.0.340.g8e2bf96aa5.
Lifting the limitation may be sensible, though I suspect the empty case is likely to indicate a logic error earlier in the script; either way, probably out of scope here for now?
Show 23 quoted lines
> 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.
Phillip WoodSep 4, 2026, 09:11 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z

Hi Andrew
On 03/09/2026 03:49, Andrew Pleeter via GitGitGadget wrote:
> From: Andrew Pleeter <andrewpleeter@gmail.com>
> 
> - Support '-z' to terminate variable outputs and 'git var -l -z'
>    entries with NUL bytes.
Hurray! I agree with Junios comments and have left a few of my own below.
Show 5 quoted lines
>   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";

Do we really need an alias? GIT_DEFAULT_KEY is pretty meaningless to me, whereas GIT_SIGNING_KEY is clearly a key for signing. What's the usecase for this by the way. If a script is using git to sign then it does not need to query the default key because git will use it automatically. If a script wants to use the key to sign something else doesn't it need to also know which signing scheme git is using (ssh, gpg, etc) , or is that obvious from the key?

Show 13 quoted lines
>   	for (ptr = git_vars; ptr->read; ptr++) {
>   		if (strcmp(var, ptr->name) == 0) {
>   			return ptr;
> @@ -207,10 +316,13 @@ 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 null_term = cb ? *(int *)cb : 0;
> +	char eol = null_term ? '\0' : '\n';
> +
>   	if (value)
> -		printf("%s=%s\n", var, value);
> +		printf("%s=%s%c", var, value, eol);

A key can contain "=" so this format is ambiguous. When the user passes "-z" we should use the same format as "git config list -z" which avoids that ambiguity

	printf("%s%c%s%c", var, eol == '\n' ? '=' : '\n', value, eol);
>   	else
> -		printf("%s\n", var);
> +		printf("%s%c", var, eol);

It would be worth checking what "git config list -z" does when there is no value and matching that. Does it print "key\n\0", or "key\0"?

Show 15 quoted lines
> +	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;
> +		} else if (arg[0] == '-') {
> +			usage(var_usage);
> +		} else {
> +			strvec_push(&vars, arg);

I think we should break out of the loop when arg is "--", or does not begin with '-', and treat the rest as variable names to print. There is not need to support "git var GIT_AUTHOR_NAME -z GIT_AUTHOR_EMAIL" in a plumbing command.

> [...]
> -	printf("%s\n", val);
> -	free(val);
> +		printf("%s%c", val, null_term ? '\0' : '\n');

Multi-valued variables are a bit of a problem here, they're built on the assumption that the individual values do not contain a newline, but as they are paths I'm not sure that is necessarily true. With -z it would be better to print '\0' after each value as we do in list_vars(). Ideally we wouldn't use a single string to pass multiple values around, but a simple fix would be to use '\0' to separate the individual values instead of '\n' so that we can split them unambiguously when we print them.

Thanks for working on this, being able to specify multiple variables that are printed unambiguously is a really useful improvement.

Phillip
Show 94 quoted lines
> +		free(val);
> +	}
>   
> +	strvec_clear(&vars);
>   	return 0;
>   }
> diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
> index 2b60317758..c437c968bb 100755
> --- a/t/t0007-git-var.sh
> +++ b/t/t0007-git-var.sh
> @@ -276,4 +276,81 @@ test_expect_success '`git var -l` works even without HOME' '
>   	)
>   '
>   
> +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 'get multiple variables' '
> +	test_tick &&
> +	cat >expect <<-EOF &&
> +	$GIT_AUTHOR_NAME
> +	$GIT_AUTHOR_EMAIL
> +	$GIT_COMMITTER_NAME
> +	$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 'get multiple variables with -z' '
> +	test_tick &&
> +	printf "%s\0%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
> +	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'git var -l -z' '
> +	git var -l -z >actual &&
> +	tr "\0" "\n" <actual | grep "^GIT_AUTHOR_NAME=" >filtered &&
> +	echo "GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME" >expect &&
> +	test_cmp expect filtered
> +'
> +
> +test_expect_success 'get GIT_DEFAULT_KEY with user.signingkey configured' '
> +	test_config user.signingkey "TEST_KEY_ID" &&
> +	echo "TEST_KEY_ID" >expect &&
> +	git var GIT_DEFAULT_KEY >actual &&
> +	test_cmp expect actual &&
> +	git var GIT_SIGNING_KEY >actual.alias &&
> +	test_cmp expect actual.alias
> +'
> +
> +test_expect_success 'get GIT_DEFAULT_KEY fails when unset and signing disabled' '
> +	test_config user.signingkey "" &&
> +	test_config commit.gpgsign false &&
> +	test_must_fail git var GIT_DEFAULT_KEY
> +'
> +
> +test_expect_success 'git var -l lists new variables' '
> +	git var -l >actual &&
> +	grep "^GIT_AUTHOR_NAME=" actual &&
> +	grep "^GIT_AUTHOR_EMAIL=" actual &&
> +	grep "^GIT_AUTHOR_DATE=" actual &&
> +	grep "^GIT_COMMITTER_NAME=" actual &&
> +	grep "^GIT_COMMITTER_EMAIL=" actual &&
> +	grep "^GIT_COMMITTER_DATE=" actual
> +'
> +
>   test_done
> 
> base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
Junio C HamanoSep 4, 2026, 15:57 UTC in reply to Phillip Wood on lore

Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z

Phillip Wood <phillip.wood123@gmail.com> writes:
Show 16 quoted lines
>>   	if (value)
>> -		printf("%s=%s\n", var, value);
>> +		printf("%s=%s%c", var, value, eol);
>
> A key can contain "=" so this format is ambiguous. When the user passes 
> "-z" we should use the same format as "git config list -z" which avoids 
> that ambiguity
>
> 	printf("%s%c%s%c", var, eol == '\n' ? '=' : '\n', value, eol);
>
>>   	else
>> -		printf("%s\n", var);
>> +		printf("%s%c", var, eol);
>
> It would be worth checking what "git config list -z" does when there is 
> no value and matching that. Does it print "key\n\0", or "key\0"?

By "key" do you mean "var"? The namespace of "var" for "git var" proper (like GIT_COMMITTER_IDENT) are very much under our control, but it also gives all the configuration variables, whose names can indeed have '=' in a three-level varlable name. This is an excellent suggestion.

> I think we should break out of the loop when arg is "--", or does not 
> begin with '-', and treat the rest as variable names to print. There is 
> not need to support "git var GIT_AUTHOR_NAME -z GIT_AUTHOR_EMAIL" in a 
> plumbing command.

Not limited to plumbing, but anywhere in Git. Let's stick to and force users adopt the simple rule that "git help cli" gives them. Options first and then args, among which revs coe first and then paths after disambiguating "--". I know as historical wart some commands may take dashed options after args, but I am fine if we tightened the rule at Git 3.0 boundary to more strictly enforced the option/argument ordering rule.

Show 12 quoted lines
>> [...]
>> -	printf("%s\n", val);
>> -	free(val);
>> +		printf("%s%c", val, null_term ? '\0' : '\n');
>
> Multi-valued variables are a bit of a problem here, they're built on the 
> assumption that the individual values do not contain a newline, but as 
> they are paths I'm not sure that is necessarily true. With -z it would 
> be better to print '\0' after each value as we do in list_vars(). 
> Ideally we wouldn't use a single string to pass multiple values around, 
> but a simple fix would be to use '\0' to separate the individual values 
> instead of '\n' so that we can split them unambiguously when we print them.

Hmph, what does "git config -l" do for multi-valued keys? We should mimick it, I would think.

Another thing that might be worth doing is to see if we can separate out "git config -l" handling out of "git var" with a breaking change at big version boundary.

> Thanks for working on this, being able to specify multiple variables 
> that are printed unambiguously is a really useful improvement.
Indeed.  Thanks, both.
Andrew Pleeter via GitGitGadgetSep 8, 2026, 04:09 UTC in reply to Andrew Pleeter via GitGitGadget on lore

[PATCH v4] var: support broken-down idents, signing key, multiple args, and -z

From: Andrew Pleeter <andrewpleeter@gmail.com>

While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT, extracting individual components (name, email, or date) currently requires callers to manually parse the composite string. Furthermore, there is no way to query the resolved commit signing key through 'git var', and the command only accepts a single variable at a time.

Teach 'git var' to expose individual identity components and commit signing configuration, and allow querying multiple variables with optional NUL-termination:

- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
- Add GIT_SIGNING_KEY to resolve the key that would be used to sign
  the resulting commit if you were to run 'git commit' right now.
- Allow passing multiple variable arguments (e.g., 'git var
  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
  sequentially.
- Support '-z' to terminate variable outputs with NUL bytes.
- Format 'git var -l -z' using the same convention as 'git config
  list -z' (newline separating key and value, NUL separating entries).
- Delimit values of multi-valued variables with NUL when '-z' is given.
- Use parse_options() to strictly require options before arguments.
- Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
    var: support broken-down idents, signing key, multiple args, and -z
    
    Teach git var to expose individual identity components and commit
    signing configuration, and allow querying multiple variables with
    optional NUL-termination.
    
    
    Changes since v3:
    =================
    
     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip
       Wood and Junio C Hamano; dropped the alias mechanism and
       commit.gpgsign check.
     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in
       builtin/var.c, strictly enforcing that options precede variable
       arguments.
     * Adopted git config list -z format (key\nvalue\0) for git var -l -z to
       prevent ambiguity with = in config keys.
     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with
       NUL bytes under -z.
     * Replaced char part in ident_part() with enum ident_part.
     * Split synopsis in Documentation/git-var.adoc into separate lines for
       -l and <variable>..., and removed awkward legacy phrasing ("of a
       piece of code").
     * Added tests in t/t0007-git-var.sh covering the new -z format,
       multi-valued -z, and argument ordering.
    
    
    Changes since v2:
    =================
    
     * Drop git ident / git whoami subcommand entirely.
     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
     * Add GIT_SIGNING_KEY to resolve commit signing keys.
     * Teach git var to accept multiple variable arguments (git var <var1>
       <var2> ...).
     * Add -z option to terminate outputs with NUL bytes (including git var
       -l -z).
     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v4
Pull-Request: https://github.com/git/git/pull/2388
Range-diff vs v3:
 1:  b86340dd0e ! 1:  6fbf973e84 var: support broken-down idents, default key, multiple args, and -z
     @@ Metadata
      Author: Andrew Pleeter <andrewpleeter@gmail.com>
      
       ## Commit message ##
     -    var: support broken-down idents, default key, multiple args, and -z
     +    var: support broken-down idents, signing key, multiple args, and -z
      
          While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,
          extracting individual components (name, email, or date) currently
     @@ Commit message
      
          - Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
          - Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
     -    - Add GIT_DEFAULT_KEY (with GIT_SIGNING_KEY alias) to resolve the
     -      configured or default commit signing key ID / fingerprint.
     +    - Add GIT_SIGNING_KEY to resolve the key that would be used to sign
     +      the resulting commit if you were to run 'git commit' right now.
          - Allow passing multiple variable arguments (e.g., 'git var
            GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
            sequentially.
     -    - Support '-z' to terminate variable outputs and 'git var -l -z'
     -      entries with NUL bytes.
     +    - Support '-z' to terminate variable outputs with NUL bytes.
     +    - Format 'git var -l -z' using the same convention as 'git config
     +      list -z' (newline separating key and value, NUL separating entries).
     +    - Delimit values of multi-valued variables with NUL when '-z' is given.
     +    - Use parse_options() to strictly require options before arguments.
          - Update Documentation/git-var.adoc and t/t0007-git-var.sh.
      
          Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
     @@ Documentation/git-var.adoc: git-var - Show a Git logical variable
       --------
       [synopsis]
      -git var (-l | <variable>)
     -+git var (-l [-z] | [-z] <variable>...)
     ++git var [-z] -l
     ++git var [-z] <variable>...
       
       DESCRIPTION
       -----------
     @@ Documentation/git-var.adoc: OPTIONS
       VARIABLES
       ---------
       `GIT_AUTHOR_IDENT`::
     -     The author of a piece of code.
     - 
     +-    The author of a piece of code.
     ++    The author.
     ++
      +`GIT_AUTHOR_NAME`::
     -+    The name of the author of a piece of code.
     ++    The name of the author.
      +
      +`GIT_AUTHOR_EMAIL`::
     -+    The email of the author of a piece of code.
     ++    The email of the author.
      +
      +`GIT_AUTHOR_DATE`::
     -+    The date and timezone of the author of a piece of code.
     -+
     - `GIT_COMMITTER_IDENT`::
     -     The person who put a piece of code into Git.
     ++    The date and timezone of the author.
       
     + `GIT_COMMITTER_IDENT`::
     +-    The person who put a piece of code into Git.
     ++    The committer.
     ++
      +`GIT_COMMITTER_NAME`::
     -+    The name of the person who put a piece of code into Git.
     ++    The name of the committer.
      +
      +`GIT_COMMITTER_EMAIL`::
     -+    The email of the person who put a piece of code into Git.
     ++    The email of the committer.
      +
      +`GIT_COMMITTER_DATE`::
     -+    The date and timezone of the person who put a piece of code into Git.
     -+
     -+`GIT_DEFAULT_KEY`::
     -+    The default commit signing key ID or fingerprint, if configured or enabled.
     ++    The date and timezone of the committer.
      +
     ++`GIT_SIGNING_KEY`::
     ++    The key that would be used to sign the resulting commit if you were
     ++    to run `git commit` right now.
     + 
       `GIT_EDITOR`::
           Text editor for use by Git commands.  The value is meant to be
     -     interpreted by the shell when it is used.  Examples: `~/bin/vi`,
     +@@ Documentation/git-var.adoc: endif::git-default-pager[]
     +     The path to the global (per-user) configuration files, if any.
     + 
     + 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.  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.
      
       ## builtin/var.c ##
      @@
     @@ builtin/var.c
      +#include "gpg-interface.h"
       #include "ident.h"
       #include "pager.h"
     - #include "refs.h"
     +-#include "refs.h"
     ++#include "parse-options.h"
       #include "path.h"
     - #include "strbuf.h"
     -+#include "strvec.h"
     +-#include "strbuf.h"
     ++#include "refs.h"
       #include "run-command.h"
     ++#include "strbuf.h"
     ++#include "string-list.h"
     ++
     ++static const char * const var_usage[] = {
     ++	N_("git var [-z] -l"),
     ++	N_("git var [-z] <variable>..."),
     ++	NULL
     ++};
       
      -static const char var_usage[] = "git var (-l | <variable>)";
     -+static const char var_usage[] = "git var (-l [-z] | [-z] <variable>...)";
     ++enum ident_part {
     ++	IDENT_NAME,
     ++	IDENT_MAIL,
     ++	IDENT_DATE,
     ++};
       
       static char *committer(int ident_flag)
       {
       	return xstrdup_or_null(git_committer_info(ident_flag));
       }
       
     -+static char *ident_part(const char *ident, char part)
     ++static char *ident_part(const char *ident, enum ident_part part)
      +{
      +	struct ident_split split;
      +
     @@ builtin/var.c
      +		return NULL;
      +
      +	switch (part) {
     -+	case 'n':
     ++	case IDENT_NAME:
      +		if (!split.name_begin || !split.name_end)
      +			return NULL;
     -+		return xmemdupz(split.name_begin, split.name_end - split.name_begin);
     -+	case 'e':
     ++		return xmemdupz(split.name_begin,
     ++				split.name_end - split.name_begin);
     ++	case IDENT_MAIL:
      +		if (!split.mail_begin || !split.mail_end)
      +			return NULL;
     -+		return xmemdupz(split.mail_begin, split.mail_end - split.mail_begin);
     -+	case 'd':
     ++		return xmemdupz(split.mail_begin,
     ++				split.mail_end - split.mail_begin);
     ++	case IDENT_DATE:
      +		if (!split.date_begin)
      +			return NULL;
      +		if (split.tz_end)
     -+			return xmemdupz(split.date_begin, split.tz_end - split.date_begin);
     ++			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 xmemdupz(split.date_begin,
     ++					split.date_end -
     ++					split.date_begin);
      +		return NULL;
      +	default:
      +		return NULL;
     @@ builtin/var.c
      +
      +static char *committer_name(int ident_flag)
      +{
     -+	return ident_part(git_committer_info(ident_flag), 'n');
     ++	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), 'e');
     ++	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), 'd');
     ++	return ident_part(git_committer_info(ident_flag), IDENT_DATE);
      +}
      +
       static char *author(int ident_flag)
     @@ builtin/var.c
       
      +static char *author_name(int ident_flag)
      +{
     -+	return ident_part(git_author_info(ident_flag), 'n');
     ++	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), 'e');
     ++	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), 'd');
     ++	return ident_part(git_author_info(ident_flag), IDENT_DATE);
      +}
      +
     -+static char *default_key(int ident_flag UNUSED)
     ++static char *git_signing_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)
     ++	/*
     ++	 * An empty string in user.signingkey allows overriding and
     ++	 * clearing a key defined in an outer (e.g. global) config.
     ++	 */
     ++	if (!repo_config_get_string(the_repository,
     ++				    "user.signingkey", &signing_key)) {
     ++		if (!signing_key || !*signing_key) {
     ++			free(signing_key);
     ++			return NULL;
     ++		}
      +		return signing_key;
     -+	free(signing_key);
     -+
     -+	if (repo_config_get_bool(the_repository, "commit.gpgsign", &gpgsign) == 0 && gpgsign)
     -+		return get_signing_key_id();
     ++	}
      +
     -+	return NULL;
     ++	signing_key = get_signing_key_id();
     ++	if (signing_key && !*signing_key) {
     ++		free(signing_key);
     ++		return NULL;
     ++	}
     ++	return signing_key;
      +}
      +
       static char *editor(int ident_flag UNUSED)
       {
       	return xstrdup_or_null(git_editor());
     +@@ builtin/var.c: static char *git_config_val_global(int ident_flag UNUSED)
     + 	free(xdg);
     + 	free(user);
     + 	strbuf_trim_trailing_newline(&buf);
     +-	if (buf.len == 0) {
     ++	if (!buf.len) {
     + 		strbuf_release(&buf);
     + 		return NULL;
     + 	}
      @@ builtin/var.c: static struct git_var git_vars[] = {
       		.name = "GIT_COMMITTER_IDENT",
       		.read = committer,
     @@ builtin/var.c: static struct git_var git_vars[] = {
       		.read = default_branch,
       	},
      +	{
     -+		.name = "GIT_DEFAULT_KEY",
     -+		.read = default_key,
     ++		.name = "GIT_SIGNING_KEY",
     ++		.read = git_signing_key,
      +	},
       	{
       		.name = "GIT_SHELL_PATH",
     @@ builtin/var.c: static struct git_var git_vars[] = {
      +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))) {
     -@@ builtin/var.c: static void list_vars(void)
     - 
     - 				string_list_split(&list, val, "\n", -1);
     - 				for (size_t i = 0; i < list.nr; i++)
     +-	char *val;
     +-
     +-	for (ptr = git_vars; ptr->read; ptr++)
     +-		if ((val = ptr->read(0))) {
     +-			if (ptr->multivalued && *val) {
     +-				struct string_list list = STRING_LIST_INIT_DUP;
     +-
     +-				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);
     - 				string_list_clear(&list, 0);
     - 			} else {
     +-				string_list_clear(&list, 0);
     +-			} else {
      -				printf("%s=%s\n", ptr->name, val);
     -+				printf("%s=%s%c", ptr->name, val, eol);
     - 			}
     - 			free(val);
     +-			}
     +-			free(val);
     ++	char delim = null_term ? '\n' : '=';
     ++	char eol = null_term ? '\0' : '\n';
     ++
     ++	for (ptr = git_vars; ptr->read; ptr++) {
     ++		char *val = ptr->read(0);
     ++
     ++		if (!val)
     ++			continue;
     ++
     ++		if (ptr->multivalued && *val) {
     ++			struct string_list list = STRING_LIST_INIT_DUP;
     ++
     ++			string_list_split(&list, val, "\n", -1);
     ++			for (size_t i = 0; i < list.nr; i++)
     ++				printf("%s%c%s%c", ptr->name, delim,
     ++				       list.items[i].string, eol);
     ++			string_list_clear(&list, 0);
     ++		} else {
     ++			printf("%s%c%s%c", ptr->name, delim, val, eol);
       		}
     -@@ builtin/var.c: static void list_vars(void)
     ++		free(val);
     ++	}
     + }
     + 
       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";
     ++
       	for (ptr = git_vars; ptr->read; ptr++) {
     - 		if (strcmp(var, ptr->name) == 0) {
     +-		if (strcmp(var, ptr->name) == 0) {
     ++		if (!strcmp(var, ptr->name))
       			return ptr;
     +-		}
     + 	}
     + 	return NULL;
     + }
      @@ builtin/var.c: 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 null_term = cb ? *(int *)cb : 0;
     -+	char eol = null_term ? '\0' : '\n';
      +
       	if (value)
      -		printf("%s=%s\n", var, value);
     -+		printf("%s=%s%c", var, value, eol);
     ++		printf("%s%c%s%c", var, null_term ? '\n' : '=',
     ++		       value, null_term ? '\0' : '\n');
       	else
      -		printf("%s\n", var);
     -+		printf("%s%c", var, eol);
     ++		printf("%s%c", var, null_term ? '\0' : '\n');
       	return git_default_config(var, value, ctx, cb);
       }
       
     -@@ builtin/var.c: int cmd_var(int argc,
     - 	    const char *prefix UNUSED,
     + 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;
     -+	struct strvec vars = STRVEC_INIT;
      +	int list = 0;
      +	int null_term = 0;
      +	int i;
     ++	struct option options[] = {
     ++		OPT_BOOL('l', NULL, &list,
     ++			 N_("list all variables")),
     ++		OPT_BOOL('z', NULL, &null_term,
     ++			 N_("terminate entries with NUL")),
     ++		OPT_END(),
     ++	};
       
     - 	show_usage_if_asked(argc, argv, var_usage);
     +-	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();
     -+	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;
     -+		} else if (arg[0] == '-') {
     -+			usage(var_usage);
     -+		} else {
     -+			strvec_push(&vars, arg);
     -+		}
     -+	}
     -+
      +	if (list) {
     -+		if (vars.nr > 0) {
     -+			strvec_clear(&vars);
     -+			usage(var_usage);
     -+		}
     ++		if (argc)
     ++			usage_with_options(var_usage, options);
      +		repo_config(the_repository, show_config, &null_term);
      +		list_vars(null_term);
       		return 0;
       	}
     --	repo_config(the_repository, git_default_config, NULL);
     ++
     ++	if (!argc)
     ++		usage_with_options(var_usage, options);
     ++
     ++	for (i = 0; i < argc; i++) {
     ++		if (!get_git_var(argv[i]))
     ++			usage_with_options(var_usage, options);
     ++	}
     ++
     + 	repo_config(the_repository, git_default_config, NULL);
       
      -	git_var = get_git_var(argv[1]);
      -	if (!git_var)
     -+	if (!vars.nr)
     - 		usage(var_usage);
     - 
     --	val = git_var->read(IDENT_STRICT);
     --	if (!val)
     --		return 1;
     -+	repo_config(the_repository, git_default_config, NULL);
     -+
     -+	for (size_t j = 0; j < vars.nr; j++) {
     -+		const struct git_var *git_var = get_git_var(vars.v[j]);
     +-		usage(var_usage);
     ++	for (i = 0; i < argc; i++) {
     ++		const struct git_var *git_var = get_git_var(argv[i]);
      +		char *val;
      +
     -+		if (!git_var) {
     -+			strvec_clear(&vars);
     -+			usage(var_usage);
     -+		}
     -+
      +		val = git_var->read(IDENT_STRICT);
     -+		if (!val) {
     -+			strvec_clear(&vars);
     ++		if (!val)
      +			return 1;
     -+		}
     + 
     +-	val = git_var->read(IDENT_STRICT);
     +-	if (!val)
     +-		return 1;
     ++		if (git_var->multivalued && null_term && *val) {
     ++			struct string_list values = STRING_LIST_INIT_DUP;
       
      -	printf("%s\n", val);
      -	free(val);
     -+		printf("%s%c", val, null_term ? '\0' : '\n');
     ++			string_list_split(&values, val, "\n", -1);
     ++			for (size_t j = 0; j < values.nr; j++) {
     ++				const char *s = values.items[j].string;
     ++
     ++				printf("%s%c", s, '\0');
     ++			}
     ++			string_list_clear(&values, 0);
     ++		} else {
     ++			printf("%s%c", val, null_term ? '\0' : '\n');
     ++		}
      +		free(val);
      +	}
       
     -+	strvec_clear(&vars);
       	return 0;
       }
      
     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '
      +
      +test_expect_success 'get multiple variables with -z' '
      +	test_tick &&
     -+	printf "%s\0%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
     ++	printf "%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
      +	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >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 &&
     ++	printf "%s\0" "$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expected &&
     ++	test_cmp expected actual
     ++'
     ++
      +test_expect_success 'git var -l -z' '
      +	git var -l -z >actual &&
     -+	tr "\0" "\n" <actual | grep "^GIT_AUTHOR_NAME=" >filtered &&
     -+	echo "GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME" >expect &&
     -+	test_cmp expect filtered
     ++	tr "\0" "\n" <actual >actual.lines &&
     ++	echo "$GIT_AUTHOR_NAME" >expect &&
     ++	sed -n "/^GIT_AUTHOR_NAME$/{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 'get GIT_DEFAULT_KEY with user.signingkey configured' '
     ++test_expect_success 'get GIT_SIGNING_KEY with user.signingkey configured' '
      +	test_config user.signingkey "TEST_KEY_ID" &&
      +	echo "TEST_KEY_ID" >expect &&
     -+	git var GIT_DEFAULT_KEY >actual &&
     -+	test_cmp expect actual &&
     -+	git var GIT_SIGNING_KEY >actual.alias &&
     -+	test_cmp expect actual.alias
     ++	git var GIT_SIGNING_KEY >actual &&
     ++	test_cmp expect actual
      +'
      +
     -+test_expect_success 'get GIT_DEFAULT_KEY fails when unset and signing disabled' '
     ++test_expect_success 'get GIT_SIGNING_KEY fails when unset' '
      +	test_config user.signingkey "" &&
     -+	test_config commit.gpgsign false &&
     -+	test_must_fail git var GIT_DEFAULT_KEY
     ++	test_must_fail git var GIT_SIGNING_KEY
      +'
      +
      +test_expect_success 'git var -l lists new variables' '
      +	git var -l >actual &&
     -+	grep "^GIT_AUTHOR_NAME=" actual &&
     -+	grep "^GIT_AUTHOR_EMAIL=" actual &&
     -+	grep "^GIT_AUTHOR_DATE=" actual &&
     -+	grep "^GIT_COMMITTER_NAME=" actual &&
     -+	grep "^GIT_COMMITTER_EMAIL=" actual &&
     -+	grep "^GIT_COMMITTER_DATE=" 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_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '
     ++	test_config user.signingkey "TEST_KEY_ID" &&
     ++	git var -l >actual &&
     ++	test_grep "^GIT_SIGNING_KEY=TEST_KEY_ID" actual
     ++'
     ++
     ++test_expect_success 'options must precede variable arguments' '
     ++	test_must_fail git var GIT_AUTHOR_NAME -z
      +'
      +
       test_done
 Documentation/git-var.adoc |  55 ++++++--
 builtin/var.c              | 255 +++++++++++++++++++++++++++++++------
 t/t0007-git-var.sh         |  95 ++++++++++++++
 3 files changed, 355 insertions(+), 50 deletions(-)
Show changes to 3 files +355 −50

Documentation/git-var.adoc, builtin/var.c, t/t0007-git-var.sh

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index 697c10aded..999525ce71 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -9,7 +9,8 @@ git-var - Show a Git logical variable
 SYNOPSIS
 --------
 [synopsis]
-git var (-l | <variable>)
+git var [-z] -l
+git var [-z] <variable>...
 
 DESCRIPTION
 -----------
@@ -24,19 +25,55 @@ 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.
+
 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
+Eric W. Biederman
+ebiederm@lnxi.com
+------------
 
 VARIABLES
 ---------
 `GIT_AUTHOR_IDENT`::
-    The author of a piece of code.
+    The author.
+
+`GIT_AUTHOR_NAME`::
+    The name of the author.
+
+`GIT_AUTHOR_EMAIL`::
+    The email of the author.
+
+`GIT_AUTHOR_DATE`::
+    The date and timezone of the author.
 
 `GIT_COMMITTER_IDENT`::
-    The person who put a piece of code into Git.
+    The committer.
+
+`GIT_COMMITTER_NAME`::
+    The name of the committer.
+
+`GIT_COMMITTER_EMAIL`::
+    The email of the committer.
+
+`GIT_COMMITTER_DATE`::
+    The date and timezone of the committer.
+
+`GIT_SIGNING_KEY`::
+    The key that would be used to sign the resulting commit if you were
+    to run `git commit` right now.
 
 `GIT_EDITOR`::
     Text editor for use by Git commands.  The value is meant to be
@@ -85,9 +122,9 @@ endif::git-default-pager[]
     The path to the global (per-user) configuration files, if any.
 
 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.  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.
diff --git a/builtin/var.c b/builtin/var.c
index cc3a43cde2..6fc037543a 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -12,25 +12,130 @@
 #include "config.h"
 #include "editor.h"
 #include "environment.h"
+#include "gpg-interface.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 * const var_usage[] = {
+	N_("git var [-z] -l"),
+	N_("git var [-z] <variable>..."),
+	NULL
+};
 
-static const char var_usage[] = "git var (-l | <variable>)";
+enum ident_part {
+	IDENT_NAME,
+	IDENT_MAIL,
+	IDENT_DATE,
+};
 
 static char *committer(int ident_flag)
 {
 	return xstrdup_or_null(git_committer_info(ident_flag));
 }
 
+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)
+			return NULL;
+		return xmemdupz(split.name_begin,
+				split.name_end - split.name_begin);
+	case IDENT_MAIL:
+		if (!split.mail_begin || !split.mail_end)
+			return NULL;
+		return xmemdupz(split.mail_begin,
+				split.mail_end - split.mail_begin);
+	case IDENT_DATE:
+		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;
+	default:
+		return NULL;
+	}
+}
+
+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 *git_signing_key(int ident_flag UNUSED)
+{
+	char *signing_key = NULL;
+
+	/*
+	 * An empty string in user.signingkey allows overriding and
+	 * clearing a key defined in an outer (e.g. global) config.
+	 */
+	if (!repo_config_get_string(the_repository,
+				    "user.signingkey", &signing_key)) {
+		if (!signing_key || !*signing_key) {
+			free(signing_key);
+			return NULL;
+		}
+		return signing_key;
+	}
+
+	signing_key = get_signing_key_id();
+	if (signing_key && !*signing_key) {
+		free(signing_key);
+		return NULL;
+	}
+	return signing_key;
+}
+
 static char *editor(int ident_flag UNUSED)
 {
 	return xstrdup_or_null(git_editor());
@@ -108,7 +213,7 @@ static char *git_config_val_global(int ident_flag UNUSED)
 	free(xdg);
 	free(user);
 	strbuf_trim_trailing_newline(&buf);
-	if (buf.len == 0) {
+	if (!buf.len) {
 		strbuf_release(&buf);
 		return NULL;
 	}
@@ -125,10 +230,34 @@ 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,
@@ -145,6 +274,10 @@ static struct git_var git_vars[] = {
 		.name = "GIT_DEFAULT_BRANCH",
 		.read = default_branch,
 	},
+	{
+		.name = "GIT_SIGNING_KEY",
+		.read = git_signing_key,
+	},
 	{
 		.name = "GIT_SHELL_PATH",
 		.read = shell_path,
@@ -172,34 +305,40 @@ static struct git_var git_vars[] = {
 	},
 };
 
-static void list_vars(void)
+static void list_vars(int null_term)
 {
 	struct git_var *ptr;
-	char *val;
-
-	for (ptr = git_vars; ptr->read; ptr++)
-		if ((val = ptr->read(0))) {
-			if (ptr->multivalued && *val) {
-				struct string_list list = STRING_LIST_INIT_DUP;
-
-				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);
-			}
-			free(val);
+	char delim = null_term ? '\n' : '=';
+	char eol = null_term ? '\0' : '\n';
+
+	for (ptr = git_vars; ptr->read; ptr++) {
+		char *val = ptr->read(0);
+
+		if (!val)
+			continue;
+
+		if (ptr->multivalued && *val) {
+			struct string_list list = STRING_LIST_INIT_DUP;
+
+			string_list_split(&list, val, "\n", -1);
+			for (size_t i = 0; i < list.nr; i++)
+				printf("%s%c%s%c", ptr->name, delim,
+				       list.items[i].string, eol);
+			string_list_clear(&list, 0);
+		} else {
+			printf("%s%c%s%c", ptr->name, delim, val, eol);
 		}
+		free(val);
+	}
 }
 
 static const struct git_var *get_git_var(const char *var)
 {
 	struct git_var *ptr;
+
 	for (ptr = git_vars; ptr->read; ptr++) {
-		if (strcmp(var, ptr->name) == 0) {
+		if (!strcmp(var, ptr->name))
 			return ptr;
-		}
 	}
 	return NULL;
 }
@@ -207,42 +346,76 @@ 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 null_term = cb ? *(int *)cb : 0;
+
 	if (value)
-		printf("%s=%s\n", var, value);
+		printf("%s%c%s%c", var, null_term ? '\n' : '=',
+		       value, null_term ? '\0' : '\n');
 	else
-		printf("%s\n", var);
+		printf("%s%c", var, null_term ? '\0' : '\n');
 	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 null_term = 0;
+	int i;
+	struct option options[] = {
+		OPT_BOOL('l', NULL, &list,
+			 N_("list all variables")),
+		OPT_BOOL('z', NULL, &null_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, &null_term);
+		list_vars(null_term);
 		return 0;
 	}
+
+	if (!argc)
+		usage_with_options(var_usage, options);
+
+	for (i = 0; i < argc; i++) {
+		if (!get_git_var(argv[i]))
+			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);
+	for (i = 0; i < argc; i++) {
+		const struct git_var *git_var = get_git_var(argv[i]);
+		char *val;
+
+		val = git_var->read(IDENT_STRICT);
+		if (!val)
+			return 1;
 
-	val = git_var->read(IDENT_STRICT);
-	if (!val)
-		return 1;
+		if (git_var->multivalued && null_term && *val) {
+			struct string_list values = STRING_LIST_INIT_DUP;
 
-	printf("%s\n", val);
-	free(val);
+			string_list_split(&values, val, "\n", -1);
+			for (size_t j = 0; j < values.nr; j++) {
+				const char *s = values.items[j].string;
+
+				printf("%s%c", s, '\0');
+			}
+			string_list_clear(&values, 0);
+		} else {
+			printf("%s%c", val, null_term ? '\0' : '\n');
+		}
+		free(val);
+	}
 
 	return 0;
 }
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 2b60317758..27cc595291 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -276,4 +276,99 @@ test_expect_success '`git var -l` works even without HOME' '
 	)
 '
 
+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 'get multiple variables' '
+	test_tick &&
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME
+	$GIT_AUTHOR_EMAIL
+	$GIT_COMMITTER_NAME
+	$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 'get multiple variables with -z' '
+	test_tick &&
+	printf "%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
+	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >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 &&
+	printf "%s\0" "$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expected &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git var -l -z' '
+	git var -l -z >actual &&
+	tr "\0" "\n" <actual >actual.lines &&
+	echo "$GIT_AUTHOR_NAME" >expect &&
+	sed -n "/^GIT_AUTHOR_NAME$/{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 'get GIT_SIGNING_KEY with user.signingkey configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	echo "TEST_KEY_ID" >expect &&
+	git var GIT_SIGNING_KEY >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '
+	test_config user.signingkey "" &&
+	test_must_fail git var GIT_SIGNING_KEY
+'
+
+test_expect_success 'git var -l lists new variables' '
+	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_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	git var -l >actual &&
+	test_grep "^GIT_SIGNING_KEY=TEST_KEY_ID" actual
+'
+
+test_expect_success 'options must precede variable arguments' '
+	test_must_fail git var GIT_AUTHOR_NAME -z
+'
+
 test_done

base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
-- 
gitgitgadget
Phillip WoodSep 8, 2026, 09:07 UTC in reply to Junio C Hamano on lore

Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z

Hi Junio
On 04/09/2026 16:57, Junio C Hamano wrote:
Show 20 quoted lines
> Phillip Wood <phillip.wood123@gmail.com> writes:
> 
>>>    	if (value)
>>> -		printf("%s=%s\n", var, value);
>>> +		printf("%s=%s%c", var, value, eol);
>>
>> A key can contain "=" so this format is ambiguous. When the user passes
>> "-z" we should use the same format as "git config list -z" which avoids
>> that ambiguity
>>
>> 	printf("%s%c%s%c", var, eol == '\n' ? '=' : '\n', value, eol);
>>
>>>    	else
>>> -		printf("%s\n", var);
>>> +		printf("%s%c", var, eol);
>>
>> It would be worth checking what "git config list -z" does when there is
>> no value and matching that. Does it print "key\n\0", or "key\0"?
> 
> By "key" do you mean "var"? 
I meant the config key which is in variable var
Show 5 quoted lines
> The namespace of "var" for "git var"
> proper (like GIT_COMMITTER_IDENT) are very much under our control,
> but it also gives all the configuration variables, whose names can
> indeed have '=' in a three-level varlable name.  This is an
> excellent suggestion.
Show 15 quoted lines
>>> [...]
>>> -	printf("%s\n", val);
>>> -	free(val);
>>> +		printf("%s%c", val, null_term ? '\0' : '\n');
>>
>> Multi-valued variables are a bit of a problem here, they're built on the
>> assumption that the individual values do not contain a newline, but as
>> they are paths I'm not sure that is necessarily true. With -z it would
>> be better to print '\0' after each value as we do in list_vars().
>> Ideally we wouldn't use a single string to pass multiple values around,
>> but a simple fix would be to use '\0' to separate the individual values
>> instead of '\n' so that we can split them unambiguously when we print them.
> 
> Hmph, what does "git config -l" do for multi-valued keys?  We should
> mimick it, I would think.

With -z it nul terminates each value. I wonder if we should be printing the variable names here when the user passes more than one var name. That would make it easier to parse multivalued vars which can have a variable number of values, or we could print an extra delimiter after the last value of multivalued vars like "git merge-tree" does to separate the different sections of its output.

> Another thing that might be worth doing is to see if we can separate
> out "git config -l" handling out of "git var" with a breaking change
> at big version boundary.

Yes, it would be nice to be able to print just the GIT_* vars without having to print the config as well.

Show 5 quoted lines
>> Thanks for working on this, being able to specify multiple variables
>> that are printed unambiguously is a really useful improvement.
> 
> Indeed.  Thanks, both.
> 
Phillip WoodSep 8, 2026, 13:54 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH v4] var: support broken-down idents, signing key, multiple args, and -z

On 08/09/2026 05:09, Andrew Pleeter via GitGitGadget wrote:
Show 55 quoted lines
> From: Andrew Pleeter <andrewpleeter@gmail.com>
> 
> While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,
> extracting individual components (name, email, or date) currently
> requires callers to manually parse the composite string. Furthermore,
> there is no way to query the resolved commit signing key through
> 'git var', and the command only accepts a single variable at a time.
> 
> Teach 'git var' to expose individual identity components and commit
> signing configuration, and allow querying multiple variables with
> optional NUL-termination:
> 
> - Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
> - Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
> - Add GIT_SIGNING_KEY to resolve the key that would be used to sign
>    the resulting commit if you were to run 'git commit' right now.
> - Allow passing multiple variable arguments (e.g., 'git var
>    GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
>    sequentially.
> - Support '-z' to terminate variable outputs with NUL bytes.
> - Format 'git var -l -z' using the same convention as 'git config
>    list -z' (newline separating key and value, NUL separating entries).
> - Delimit values of multi-valued variables with NUL when '-z' is given.
> - Use parse_options() to strictly require options before arguments.
> - Update Documentation/git-var.adoc and t/t0007-git-var.sh.
> 
> Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
> ---
>      var: support broken-down idents, signing key, multiple args, and -z
>      
>      Teach git var to expose individual identity components and commit
>      signing configuration, and allow querying multiple variables with
>      optional NUL-termination.
>      
>      
>      Changes since v3:
>      =================
>      
>       * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip
>         Wood and Junio C Hamano; dropped the alias mechanism and
>         commit.gpgsign check.
>       * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in
>         builtin/var.c, strictly enforcing that options precede variable
>         arguments.
>       * Adopted git config list -z format (key\nvalue\0) for git var -l -z to
>         prevent ambiguity with = in config keys.
>       * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with
>         NUL bytes under -z.
>       * Replaced char part in ident_part() with enum ident_part.
>       * Split synopsis in Documentation/git-var.adoc into separate lines for
>         -l and <variable>..., and removed awkward legacy phrasing ("of a
>         piece of code").
>       * Added tests in t/t0007-git-var.sh covering the new -z format,
>         multi-valued -z, and argument ordering.
> 
That all sounds good, lets look at the code ...
> diff --git a/builtin/var.c b/builtin/var.c
> index cc3a43cde2..6fc037543a 100644
> --- a/builtin/var.c
> +++ b/builtin/var.c
 > [...]> +static char *git_signing_key(int ident_flag UNUSED)
Show 21 quoted lines
> +{
> +	char *signing_key = NULL;
> +
> +	/*
> +	 * An empty string in user.signingkey allows overriding and
> +	 * clearing a key defined in an outer (e.g. global) config.
> +	 */
> +	if (!repo_config_get_string(the_repository,
> +				    "user.signingkey", &signing_key)) {
> +		if (!signing_key || !*signing_key) {
> +			free(signing_key);
> +			return NULL;
> +		}
> +		return signing_key;
> +	}
> +
> +	signing_key = get_signing_key_id();
> +	if (signing_key && !*signing_key) {
> +		free(signing_key);
> +		return NULL;
> +	}
 > +	return signing_key;
 > +}

Looking at sign_buffer() in gpg-interface.c it looks like git calls get_signing_key() to obtain the default key - why are we doing something different here? I'm also still curious how this is expected to be used.

Show 20 quoted lines
> -static void list_vars(void)
> +static void list_vars(int null_term)
>   {
>   	struct git_var *ptr;
> -	char *val;
> -
> -	for (ptr = git_vars; ptr->read; ptr++)
> -		if ((val = ptr->read(0))) {
> -			if (ptr->multivalued && *val) {
> -				struct string_list list = STRING_LIST_INIT_DUP;
> -
> -				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);
> -			}
> -			free(val);
> +	char delim = null_term ? '\n' : '=';

We are in control of the variable names and we know they do not currently contain '=' so we don't currently need a different format here with '-z'. However it is possible that might change in the future (for example using "GIT_PAGER:<my-command>" to return the pager for "<my-command>" that could be an alias containing '=') so using the same format as config keys is probably a good idea. We should document the format above.

Show 12 quoted lines
> +	char eol = null_term ? '\0' : '\n';
> +
> +	for (ptr = git_vars; ptr->read; ptr++) {
> +		char *val = ptr->read(0);
> +
> +		if (!val)
> +			continue;
> +
> +		if (ptr->multivalued && *val) {
> +			struct string_list list = STRING_LIST_INIT_DUP;
> +
> +			string_list_split(&list, val, "\n", -1);

As I said before, I think we should switch to using '\0' instead of '\n' when we build the multivalued string so that we can safely handle values that contain '\n'.

Show 10 quoted lines
> +			for (size_t i = 0; i < list.nr; i++)
> +				printf("%s%c%s%c", ptr->name, delim,
> +				       list.items[i].string, eol);
> +			string_list_clear(&list, 0);
> +		} else {
> +			printf("%s%c%s%c", ptr->name, delim, val, eol);
>   		}
> +		free(val);
> +	}
>   }   
Show 5 quoted lines
> @@ -207,42 +346,76 @@ 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 null_term = cb ? *(int *)cb : 0;

This seems unnecessarily complicated, can't we just make sure we always pass a non-null pointer cb? Also '\0' is known as NUL, not NULL.

	int *nul_term = cb;
	char term = *nul_term ? '\0' : '\n';
	char delim = *nul_term ? '\n' : '=';
and then use term and delim below.
Show 27 quoted lines
> +
>   	if (value)
> -		printf("%s=%s\n", var, value);
> +		printf("%s%c%s%c", var, null_term ? '\n' : '=',
> +		       value, null_term ? '\0' : '\n');
>   	else
> -		printf("%s\n", var);
> +		printf("%s%c", var, null_term ? '\0' : '\n');
>   	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 null_term = 0;
> +	int i;
> +	struct option options[] = {
> +		OPT_BOOL('l', NULL, &list,
> +			 N_("list all variables")),
> +		OPT_BOOL('z', NULL, &null_term,
> +			 N_("terminate entries with NUL")),

The help is correct, we should use nul_term as the variable name. Using parse_options() is a nice improvement.

Show 26 quoted lines
> +		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, &null_term);
> +		list_vars(null_term);
>   		return 0;
>   	}
> +
> +	if (!argc)
> +		usage_with_options(var_usage, options);
> +
> +	for (i = 0; i < argc; i++) {
> +		if (!get_git_var(argv[i]))
> +			usage_with_options(var_usage, options);

Do we really need to walk all the var names here - can't we just error out if we see an invalid one later?

Show 14 quoted lines
> +	}
> +
>   	repo_config(the_repository, git_default_config, NULL);
>   
> -	git_var = get_git_var(argv[1]);
> -	if (!git_var)
> -		usage(var_usage);
> +	for (i = 0; i < argc; i++) {
> +		const struct git_var *git_var = get_git_var(argv[i]);
> +		char *val;
> +
> +		val = git_var->read(IDENT_STRICT);
> +		if (!val)
> +			return 1;

If the user asked for multiple vars to be printed, erroring out because one is not set is not very friendly, It would be better to print a blank record and continue.

Show 5 quoted lines
>   
> -	val = git_var->read(IDENT_STRICT);
> -	if (!val)
> -		return 1;
> +		if (git_var->multivalued && null_term && *val) {
Why "*val" ?
Show 9 quoted lines
> +			struct string_list values = STRING_LIST_INIT_DUP;
>   
> -	printf("%s\n", val);
> -	free(val);
> +			string_list_split(&values, val, "\n", -1);
> +			for (size_t j = 0; j < values.nr; j++) {
> +				const char *s = values.items[j].string;
> +
> +				printf("%s%c", s, '\0');

If we're printing multiple var then the caller has no way to tell if a var has multiple values which makes it tricky or impossible to match up the values we print to the vars that were requested. We could change the output format when multiple vars are requested to print the var name as will like we do with '-l', or we could print an extra terminator after a multi-valued var and properly document which vars are multi-valued. The latter means the caller can match up the values without worrying about parsing the var names.

Thanks
Phillip
Show 116 quoted lines
> +			}
> +			string_list_clear(&values, 0);
> +		} else {
> +			printf("%s%c", val, null_term ? '\0' : '\n');
> +		}
> +		free(val);
> +	}
>   
>   	return 0;
>   }
> diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
> index 2b60317758..27cc595291 100755
> --- a/t/t0007-git-var.sh
> +++ b/t/t0007-git-var.sh
> @@ -276,4 +276,99 @@ test_expect_success '`git var -l` works even without HOME' '
>   	)
>   '
>   
> +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 'get multiple variables' '
> +	test_tick &&
> +	cat >expect <<-EOF &&
> +	$GIT_AUTHOR_NAME
> +	$GIT_AUTHOR_EMAIL
> +	$GIT_COMMITTER_NAME
> +	$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 'get multiple variables with -z' '
> +	test_tick &&
> +	printf "%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
> +	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >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 &&
> +	printf "%s\0" "$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expected &&
> +	test_cmp expected actual
> +'
> +
> +test_expect_success 'git var -l -z' '
> +	git var -l -z >actual &&
> +	tr "\0" "\n" <actual >actual.lines &&
> +	echo "$GIT_AUTHOR_NAME" >expect &&
> +	sed -n "/^GIT_AUTHOR_NAME$/{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 'get GIT_SIGNING_KEY with user.signingkey configured' '
> +	test_config user.signingkey "TEST_KEY_ID" &&
> +	echo "TEST_KEY_ID" >expect &&
> +	git var GIT_SIGNING_KEY >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'get GIT_SIGNING_KEY fails when unset' '
> +	test_config user.signingkey "" &&
> +	test_must_fail git var GIT_SIGNING_KEY
> +'
> +
> +test_expect_success 'git var -l lists new variables' '
> +	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_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '
> +	test_config user.signingkey "TEST_KEY_ID" &&
> +	git var -l >actual &&
> +	test_grep "^GIT_SIGNING_KEY=TEST_KEY_ID" actual
> +'
> +
> +test_expect_success 'options must precede variable arguments' '
> +	test_must_fail git var GIT_AUTHOR_NAME -z
> +'
> +
>   test_done
> 
> base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
Andrew Pleeter via GitGitGadgetSep 8, 2026, 20:43 UTC in reply to Andrew Pleeter via GitGitGadget on lore

[PATCH v5] var: support broken-down idents, signing key, multiple args, and -z

From: Andrew Pleeter <andrewpleeter@gmail.com>

While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT, extracting individual components (name, email, or date) currently requires callers to manually parse the composite string. Furthermore, there is no way to query the resolved commit signing key through 'git var', and the command only accepts a single variable at a time.

Teach 'git var' to expose individual identity components and commit signing configuration, and allow querying multiple variables with optional NUL-termination:

- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
- Add GIT_SIGNING_KEY to resolve the key that would be used to sign
  the resulting commit if you were to run 'git commit' right now.
- Allow passing multiple variable arguments (e.g., 'git var
  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
  sequentially.
- Support '-z' to terminate variable outputs with NUL bytes.
- Format 'git var -l -z' using the same convention as 'git config
  list -z' (newline separating key and value, NUL separating entries).
- Delimit values of multi-valued variables with NUL when '-z' is given,
  and output an extra delimiter after multi-valued variables when
  querying multiple variables to disambiguate the stream.
- When querying multiple variables, print an empty record for any
  variable that has no value and continue processing remaining variables.
- Use parse_options() to strictly require options before arguments.
- Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
    var: support broken-down idents, signing key, multiple args, and -z
    
    Teach git var to expose individual identity components and commit
    signing configuration, and allow querying multiple variables with
    optional NUL-termination.
    
    
    Changes since v4:
    =================
    
     * Simplified git_signing_key() to directly call get_signing_key() as
       used throughout Git (in tag, send-pack, and sign_buffer()).
     * Renamed null_term to nul_term across builtin/var.c, and simplified
       show_config() callback handling.
     * Replaced the redundant pre-validation loop in cmd_var() by validating
       arguments directly in the main execution loop.
     * When querying multiple variables, print an empty record (blank line
       or \0 with -z) for any variable that has no value, and continue
       printing remaining variables instead of terminating prematurely.
     * Switched multi-valued variable storage (git_config_val_global()) to
       internal \0 delimiters, iterating directly through string sequences
       without allocating a temporary string_list.
     * For multi-variable queries, output an extra delimiter (\n or \0)
       after multi-valued variables to clearly mark the end of their list.
     * Explicitly documented the git var -l -z format and multi-variable
       handling in Documentation/git-var.adoc.
     * Added comprehensive tests for unset variables and multi-valued stream
       delimiters in t/t0007-git-var.sh.
    
    
    Changes since v3:
    =================
    
     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip
       Wood and Junio C Hamano; dropped the alias mechanism and
       commit.gpgsign check.
     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in
       builtin/var.c, strictly enforcing that options precede variable
       arguments.
     * Adopted git config list -z format (key\nvalue\0) for git var -l -z to
       prevent ambiguity with = in config keys.
     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with
       NUL bytes under -z.
     * Replaced char part in ident_part() with enum ident_part.
     * Split synopsis in Documentation/git-var.adoc into separate lines for
       -l and <variable>..., and removed awkward legacy phrasing ("of a
       piece of code").
     * Added tests in t/t0007-git-var.sh covering the new -z format,
       multi-valued -z, and argument ordering.
    
    
    Changes since v2:
    =================
    
     * Drop git ident / git whoami subcommand entirely.
     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
     * Add GIT_SIGNING_KEY to resolve commit signing keys.
     * Teach git var to accept multiple variable arguments (git var <var1>
       <var2> ...).
     * Add -z option to terminate outputs with NUL bytes (including git var
       -l -z).
     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v5
Pull-Request: https://github.com/git/git/pull/2388
Range-diff vs v4:
 1:  6fbf973e84 ! 1:  04833673ea var: support broken-down idents, signing key, multiple args, and -z
     @@ Commit message
          - Support '-z' to terminate variable outputs with NUL bytes.
          - Format 'git var -l -z' using the same convention as 'git config
            list -z' (newline separating key and value, NUL separating entries).
     -    - Delimit values of multi-valued variables with NUL when '-z' is given.
     +    - Delimit values of multi-valued variables with NUL when '-z' is given,
     +      and output an extra delimiter after multi-valued variables when
     +      querying multiple variables to disambiguate the stream.
     +    - When querying multiple variables, print an empty record for any
     +      variable that has no value and continue processing remaining variables.
          - Use parse_options() to strictly require options before arguments.
          - Update Documentation/git-var.adoc and t/t0007-git-var.sh.
      
     @@ Documentation/git-var.adoc: git-var - Show a Git logical variable
       
       DESCRIPTION
       -----------
     +-Prints a Git logical variable. Exits with code 1 if the variable has
     +-no value.
     ++Prints Git logical variables. Exits with code 1 if any requested
     ++variable has no value. When multiple variables are requested, an empty
     ++record (a blank line, or an empty NUL-terminated record when `-z` is given)
     ++is printed for any variable that has no value, and the command continues
     ++processing the remaining variables.
     + 
     + OPTIONS
     + -------
      @@ Documentation/git-var.adoc: 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.
     ++	Terminate entries with NUL instead of newline. When used with
     ++	`-l`, the variable name and its value are separated by a newline,
     ++	and each entry is terminated with a NUL byte.
      +
       EXAMPLES
       --------
     @@ Documentation/git-var.adoc: endif::git-default-pager[]
      -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.  Callers should
     -+be prepared for any such path value to contain multiple items.
     ++and are listed in order from highest to lowest priority. When querying
     ++multiple variables, an extra newline (or an extra NUL byte if `-z` is
     ++given) is printed after the values of a multi-valued variable to mark the
     ++end of its list. 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.
     @@ builtin/var.c
       #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
      +};
     - 
     --static const char var_usage[] = "git var (-l | <variable>)";
     ++
      +enum ident_part {
      +	IDENT_NAME,
      +	IDENT_MAIL,
     @@ builtin/var.c
      +
      +static char *git_signing_key(int ident_flag UNUSED)
      +{
     -+	char *signing_key = NULL;
     -+
     -+	/*
     -+	 * An empty string in user.signingkey allows overriding and
     -+	 * clearing a key defined in an outer (e.g. global) config.
     -+	 */
     -+	if (!repo_config_get_string(the_repository,
     -+				    "user.signingkey", &signing_key)) {
     -+		if (!signing_key || !*signing_key) {
     -+			free(signing_key);
     -+			return NULL;
     -+		}
     -+		return signing_key;
     -+	}
     ++	char *signing_key = get_signing_key();
      +
     -+	signing_key = get_signing_key_id();
      +	if (signing_key && !*signing_key) {
      +		free(signing_key);
      +		return NULL;
     @@ builtin/var.c
       {
       	return xstrdup_or_null(git_editor());
      @@ builtin/var.c: static char *git_config_val_global(int ident_flag UNUSED)
     + 	git_global_config_paths(&user, &xdg);
     + 	if (xdg && *xdg) {
     + 		normalize_path_copy(xdg, xdg);
     +-		strbuf_addf(&buf, "%s\n", xdg);
     ++		strbuf_addstr(&buf, xdg);
     ++		strbuf_addch(&buf, '\0');
     + 	}
     + 	if (user && *user) {
     + 		normalize_path_copy(user, user);
     +-		strbuf_addf(&buf, "%s\n", user);
     ++		strbuf_addstr(&buf, user);
     ++		strbuf_addch(&buf, '\0');
     + 	}
       	free(xdg);
       	free(user);
     - 	strbuf_trim_trailing_newline(&buf);
     +-	strbuf_trim_trailing_newline(&buf);
      -	if (buf.len == 0) {
      +	if (!buf.len) {
       		strbuf_release(&buf);
       		return NULL;
       	}
     ++	strbuf_addch(&buf, '\0');
     + 	return strbuf_detach(&buf, &unused);
     + }
     + 
      @@ builtin/var.c: static struct git_var git_vars[] = {
       		.name = "GIT_COMMITTER_IDENT",
       		.read = committer,
     @@ builtin/var.c: static struct git_var git_vars[] = {
       };
       
      -static void list_vars(void)
     -+static void list_vars(int null_term)
     ++static void list_vars(int nul_term)
       {
       	struct git_var *ptr;
      -	char *val;
     @@ builtin/var.c: static struct git_var git_vars[] = {
      -				printf("%s=%s\n", ptr->name, val);
      -			}
      -			free(val);
     -+	char delim = null_term ? '\n' : '=';
     -+	char eol = null_term ? '\0' : '\n';
     ++	char delim = nul_term ? '\n' : '=';
     ++	char term = nul_term ? '\0' : '\n';
      +
      +	for (ptr = git_vars; ptr->read; ptr++) {
      +		char *val = ptr->read(0);
     @@ builtin/var.c: static struct git_var git_vars[] = {
      +		if (!val)
      +			continue;
      +
     -+		if (ptr->multivalued && *val) {
     -+			struct string_list list = STRING_LIST_INIT_DUP;
     -+
     -+			string_list_split(&list, val, "\n", -1);
     -+			for (size_t i = 0; i < list.nr; i++)
     -+				printf("%s%c%s%c", ptr->name, delim,
     -+				       list.items[i].string, eol);
     -+			string_list_clear(&list, 0);
     ++		if (ptr->multivalued) {
     ++			for (const char *s = val; *s; s += strlen(s) + 1)
     ++				printf("%s%c%s%c", ptr->name, delim, s, term);
      +		} else {
     -+			printf("%s%c%s%c", ptr->name, delim, val, eol);
     ++			printf("%s%c%s%c", ptr->name, delim, val, term);
       		}
      +		free(val);
      +	}
     @@ builtin/var.c: 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 null_term = cb ? *(int *)cb : 0;
     ++	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, null_term ? '\n' : '=',
     -+		       value, null_term ? '\0' : '\n');
     ++		printf("%s%c%s%c", var, delim, value, term);
       	else
      -		printf("%s\n", var);
     -+		printf("%s%c", var, null_term ? '\0' : '\n');
     ++		printf("%s%c", var, term);
       	return git_default_config(var, value, ctx, cb);
       }
       
     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)
      -	const struct git_var *git_var;
      -	char *val;
      +	int list = 0;
     -+	int null_term = 0;
     ++	int nul_term = 0;
     ++	int ret = 0;
      +	int i;
     ++	char term;
      +	struct option options[] = {
      +		OPT_BOOL('l', NULL, &list,
      +			 N_("list all variables")),
     -+		OPT_BOOL('z', NULL, &null_term,
     ++		OPT_BOOL('z', NULL, &nul_term,
      +			 N_("terminate entries with NUL")),
      +		OPT_END(),
      +	};
     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)
      +	if (list) {
      +		if (argc)
      +			usage_with_options(var_usage, options);
     -+		repo_config(the_repository, show_config, &null_term);
     -+		list_vars(null_term);
     ++		repo_config(the_repository, show_config, &nul_term);
     ++		list_vars(nul_term);
       		return 0;
       	}
      +
      +	if (!argc)
      +		usage_with_options(var_usage, options);
     -+
     -+	for (i = 0; i < argc; i++) {
     -+		if (!get_git_var(argv[i]))
     -+			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);
     ++	term = nul_term ? '\0' : '\n';
     ++
      +	for (i = 0; i < argc; i++) {
      +		const struct git_var *git_var = get_git_var(argv[i]);
      +		char *val;
     -+
     -+		val = git_var->read(IDENT_STRICT);
     -+		if (!val)
     -+			return 1;
       
      -	val = git_var->read(IDENT_STRICT);
      -	if (!val)
      -		return 1;
     -+		if (git_var->multivalued && null_term && *val) {
     -+			struct string_list values = STRING_LIST_INIT_DUP;
     ++		if (!git_var)
     ++			usage_with_options(var_usage, options);
       
      -	printf("%s\n", val);
      -	free(val);
     -+			string_list_split(&values, val, "\n", -1);
     -+			for (size_t j = 0; j < values.nr; j++) {
     -+				const char *s = values.items[j].string;
     ++		val = git_var->read(IDENT_STRICT);
     ++		if (!val) {
     ++			if (argc == 1)
     ++				return 1;
     ++			ret = 1;
     ++			printf("%c", term);
     ++			continue;
     ++		}
      +
     -+				printf("%s%c", s, '\0');
     -+			}
     -+			string_list_clear(&values, 0);
     ++		if (git_var->multivalued) {
     ++			for (const char *s = val; *s; s += strlen(s) + 1)
     ++				printf("%s%c", s, term);
     ++			if (argc > 1)
     ++				printf("%c", term);
      +		} else {
     -+			printf("%s%c", val, null_term ? '\0' : '\n');
     ++			printf("%s%c", val, term);
      +		}
      +		free(val);
      +	}
       
     - 	return 0;
     +-	return 0;
     ++	return ret;
       }
      
       ## t/t0007-git-var.sh ##
     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '
      +test_expect_success 'options must precede variable arguments' '
      +	test_must_fail git var GIT_AUTHOR_NAME -z
      +'
     ++
     ++test_expect_success 'get multiple variables with unset variable outputs blank record' '
     ++	test_config user.signingkey "" &&
     ++	cat >expect <<-EOF &&
     ++	$GIT_AUTHOR_NAME
     ++
     ++	$GIT_COMMITTER_NAME
     ++	EOF
     ++	test_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'get multiple variables with -z and unset variable' '
     ++	test_config user.signingkey "" &&
     ++	printf "%s\0\0%s\0" "$GIT_AUTHOR_NAME" "$GIT_COMMITTER_NAME" >expect &&
     ++	test_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
     ++	test_cmp expect actual
     ++'
     ++
     ++test_expect_success 'get multiple variables including multi-valued variable with -z' '
     ++	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
     ++	printf "%s\0%s\0%s\0\0%s\0" "$GIT_AUTHOR_NAME" \
     ++		"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" \
     ++		"$GIT_AUTHOR_EMAIL" >expect &&
     ++	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
     ++		git var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&
     ++	test_cmp expect actual
     ++'
      +
       test_done
 Documentation/git-var.adoc |  67 ++++++++--
 builtin/var.c              | 249 ++++++++++++++++++++++++++++++-------
 t/t0007-git-var.sh         | 123 ++++++++++++++++++
 3 files changed, 383 insertions(+), 56 deletions(-)
Show changes to 3 files +383 −56

Documentation/git-var.adoc, builtin/var.c, t/t0007-git-var.sh

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index 697c10aded..40f9f320ef 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -9,12 +9,16 @@ 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. Exits with code 1 if any requested
+variable has no value. When multiple variables are requested, an empty
+record (a blank line, or an empty NUL-terminated record when `-z` is given)
+is printed for any variable that has no value, and the command continues
+processing the remaining variables.
 
 OPTIONS
 -------
@@ -24,19 +28,57 @@ 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`, 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
+Eric W. Biederman
+ebiederm@lnxi.com
+------------
 
 VARIABLES
 ---------
 `GIT_AUTHOR_IDENT`::
-    The author of a piece of code.
+    The author.
+
+`GIT_AUTHOR_NAME`::
+    The name of the author.
+
+`GIT_AUTHOR_EMAIL`::
+    The email of the author.
+
+`GIT_AUTHOR_DATE`::
+    The date and timezone of the author.
 
 `GIT_COMMITTER_IDENT`::
-    The person who put a piece of code into Git.
+    The committer.
+
+`GIT_COMMITTER_NAME`::
+    The name of the committer.
+
+`GIT_COMMITTER_EMAIL`::
+    The email of the committer.
+
+`GIT_COMMITTER_DATE`::
+    The date and timezone of the committer.
+
+`GIT_SIGNING_KEY`::
+    The key that would be used to sign the resulting commit if you were
+    to run `git commit` right now.
 
 `GIT_EDITOR`::
     Text editor for use by Git commands.  The value is meant to be
@@ -85,9 +127,12 @@ endif::git-default-pager[]
     The path to the global (per-user) configuration files, if any.
 
 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, an extra newline (or an extra NUL byte if `-z` is
+given) is printed after the values of a multi-valued variable to mark the
+end of its list. 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.
diff --git a/builtin/var.c b/builtin/var.c
index cc3a43cde2..a6d14d7b35 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -12,25 +12,116 @@
 #include "config.h"
 #include "editor.h"
 #include "environment.h"
+#include "gpg-interface.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 *committer(int ident_flag)
 {
 	return xstrdup_or_null(git_committer_info(ident_flag));
 }
 
+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)
+			return NULL;
+		return xmemdupz(split.name_begin,
+				split.name_end - split.name_begin);
+	case IDENT_MAIL:
+		if (!split.mail_begin || !split.mail_end)
+			return NULL;
+		return xmemdupz(split.mail_begin,
+				split.mail_end - split.mail_begin);
+	case IDENT_DATE:
+		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;
+	default:
+		return NULL;
+	}
+}
+
+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 *git_signing_key(int ident_flag UNUSED)
+{
+	char *signing_key = get_signing_key();
+
+	if (signing_key && !*signing_key) {
+		free(signing_key);
+		return NULL;
+	}
+	return signing_key;
+}
+
 static char *editor(int ident_flag UNUSED)
 {
 	return xstrdup_or_null(git_editor());
@@ -99,19 +190,21 @@ static char *git_config_val_global(int ident_flag UNUSED)
 	git_global_config_paths(&user, &xdg);
 	if (xdg && *xdg) {
 		normalize_path_copy(xdg, xdg);
-		strbuf_addf(&buf, "%s\n", xdg);
+		strbuf_addstr(&buf, xdg);
+		strbuf_addch(&buf, '\0');
 	}
 	if (user && *user) {
 		normalize_path_copy(user, user);
-		strbuf_addf(&buf, "%s\n", user);
+		strbuf_addstr(&buf, user);
+		strbuf_addch(&buf, '\0');
 	}
 	free(xdg);
 	free(user);
-	strbuf_trim_trailing_newline(&buf);
-	if (buf.len == 0) {
+	if (!buf.len) {
 		strbuf_release(&buf);
 		return NULL;
 	}
+	strbuf_addch(&buf, '\0');
 	return strbuf_detach(&buf, &unused);
 }
 
@@ -125,10 +218,34 @@ 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,
@@ -145,6 +262,10 @@ static struct git_var git_vars[] = {
 		.name = "GIT_DEFAULT_BRANCH",
 		.read = default_branch,
 	},
+	{
+		.name = "GIT_SIGNING_KEY",
+		.read = git_signing_key,
+	},
 	{
 		.name = "GIT_SHELL_PATH",
 		.read = shell_path,
@@ -172,34 +293,35 @@ static struct git_var git_vars[] = {
 	},
 };
 
-static void list_vars(void)
+static void list_vars(int nul_term)
 {
 	struct git_var *ptr;
-	char *val;
-
-	for (ptr = git_vars; ptr->read; ptr++)
-		if ((val = ptr->read(0))) {
-			if (ptr->multivalued && *val) {
-				struct string_list list = STRING_LIST_INIT_DUP;
-
-				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);
-			}
-			free(val);
+	char delim = nul_term ? '\n' : '=';
+	char term = nul_term ? '\0' : '\n';
+
+	for (ptr = git_vars; ptr->read; ptr++) {
+		char *val = ptr->read(0);
+
+		if (!val)
+			continue;
+
+		if (ptr->multivalued) {
+			for (const char *s = val; *s; s += strlen(s) + 1)
+				printf("%s%c%s%c", ptr->name, delim, s, term);
+		} else {
+			printf("%s%c%s%c", ptr->name, delim, val, term);
 		}
+		free(val);
+	}
 }
 
 static const struct git_var *get_git_var(const char *var)
 {
 	struct git_var *ptr;
+
 	for (ptr = git_vars; ptr->read; ptr++) {
-		if (strcmp(var, ptr->name) == 0) {
+		if (!strcmp(var, ptr->name))
 			return ptr;
-		}
 	}
 	return NULL;
 }
@@ -207,42 +329,79 @@ 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;
+	int ret = 0;
+	int i;
+	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);
+	term = nul_term ? '\0' : '\n';
+
+	for (i = 0; i < argc; i++) {
+		const struct git_var *git_var = get_git_var(argv[i]);
+		char *val;
 
-	val = git_var->read(IDENT_STRICT);
-	if (!val)
-		return 1;
+		if (!git_var)
+			usage_with_options(var_usage, options);
 
-	printf("%s\n", val);
-	free(val);
+		val = git_var->read(IDENT_STRICT);
+		if (!val) {
+			if (argc == 1)
+				return 1;
+			ret = 1;
+			printf("%c", term);
+			continue;
+		}
+
+		if (git_var->multivalued) {
+			for (const char *s = val; *s; s += strlen(s) + 1)
+				printf("%s%c", s, term);
+			if (argc > 1)
+				printf("%c", term);
+		} else {
+			printf("%s%c", val, term);
+		}
+		free(val);
+	}
 
-	return 0;
+	return ret;
 }
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 2b60317758..92b68b9ab4 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -276,4 +276,127 @@ test_expect_success '`git var -l` works even without HOME' '
 	)
 '
 
+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 'get multiple variables' '
+	test_tick &&
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME
+	$GIT_AUTHOR_EMAIL
+	$GIT_COMMITTER_NAME
+	$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 'get multiple variables with -z' '
+	test_tick &&
+	printf "%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
+	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >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 &&
+	printf "%s\0" "$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expected &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git var -l -z' '
+	git var -l -z >actual &&
+	tr "\0" "\n" <actual >actual.lines &&
+	echo "$GIT_AUTHOR_NAME" >expect &&
+	sed -n "/^GIT_AUTHOR_NAME$/{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 'get GIT_SIGNING_KEY with user.signingkey configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	echo "TEST_KEY_ID" >expect &&
+	git var GIT_SIGNING_KEY >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '
+	test_config user.signingkey "" &&
+	test_must_fail git var GIT_SIGNING_KEY
+'
+
+test_expect_success 'git var -l lists new variables' '
+	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_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	git var -l >actual &&
+	test_grep "^GIT_SIGNING_KEY=TEST_KEY_ID" actual
+'
+
+test_expect_success 'options must precede variable arguments' '
+	test_must_fail git var GIT_AUTHOR_NAME -z
+'
+
+test_expect_success 'get multiple variables with unset variable outputs blank record' '
+	test_config user.signingkey "" &&
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME
+
+	$GIT_COMMITTER_NAME
+	EOF
+	test_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables with -z and unset variable' '
+	test_config user.signingkey "" &&
+	printf "%s\0\0%s\0" "$GIT_AUTHOR_NAME" "$GIT_COMMITTER_NAME" >expect &&
+	test_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables including multi-valued variable with -z' '
+	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
+	printf "%s\0%s\0%s\0\0%s\0" "$GIT_AUTHOR_NAME" \
+		"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" \
+		"$GIT_AUTHOR_EMAIL" >expect &&
+	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
+		git var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&
+	test_cmp expect actual
+'
+
 test_done

base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
-- 
gitgitgadget
Junio C HamanoSep 8, 2026, 21:53 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH v5] var: support broken-down idents, signing key, multiple args, and -z

"Andrew Pleeter via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 13 quoted lines
>  VARIABLES
>  ---------
>  `GIT_AUTHOR_IDENT`::
> +    The author.
> +
> +`GIT_AUTHOR_NAME`::
> +    The name of the author.
> +
> +`GIT_AUTHOR_EMAIL`::
> +    The email of the author.
> +
> +`GIT_AUTHOR_DATE`::
> +    The date and timezone of the author.

The above (and the COMMITTER counterparts) gives almost no useful information. On the other hand, the description used here ...

> +`GIT_SIGNING_KEY`::
> +    The key that would be used to sign the resulting commit if you were
> +    to run `git commit` right now.
... explains what significance this key has much better.
Show 13 quoted lines
> @@ -85,9 +127,12 @@ endif::git-default-pager[]
>      The path to the global (per-user) configuration files, if any.
>  
>  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, an extra newline (or an extra NUL byte if `-z` is
> +given) is printed after the values of a multi-valued variable to mark the
> +end of its list. Callers should be prepared for any such path value to
> +contain multiple items.

Hmph. The added bulk of this description is only because we can now optionally use NUL delimiting as opposed to LF? Are we changing the output in a backward incompatible way for callers that do not pass the -z option?

For example, "git var -l" output ends like so for me in today's Git:
    $ git var -l | tail -n 3
    GIT_CONFIG_SYSTEM=/home/gitster/g/seen/etc/gitconfig
    GIT_CONFIG_GLOBAL=/home/gitster/.config/git/config
    GIT_CONFIG_GLOBAL=/home/gitster/.gitconfig
    $ git var GIT_CONFIG_GLOBAL
    /home/gitster/.config/git/config
    /home/gitster/.gitconfig

You mention "an extra newline". Will it appear in the above output with this version? If so, wouldn't that be a breaking change?

Show 36 quoted lines
> +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)
> +			return NULL;
> +		return xmemdupz(split.name_begin,
> +				split.name_end - split.name_begin);
> +	case IDENT_MAIL:
> +		if (!split.mail_begin || !split.mail_end)
> +			return NULL;
> +		return xmemdupz(split.mail_begin,
> +				split.mail_end - split.mail_begin);
> +	case IDENT_DATE:
> +		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;
> +	default:
> +		return NULL;
> +	}
> +}

In many cases in the above you punt and return NULL, but aren't there some cases where it clearly is a data error that deserves die() or a programming error that deserves BUG()?

For example, my quick read of split_ident_line() tells me that it is impossible for split.name_begin or split.name_end to be NULL if the function signals success by returning 0. If I am not misreading the code, returning NULL when IDENT_NAME is requested in the above code is sweeping a programming error under the rug.

If the caller passed anything other than IDENT_{NAME,MAIL,DATE}, it similarly is a programming error. The default: arm should not hide it underr the rug by returning NULL, but complain loudly with a BUG(), no?

Show 14 quoted lines
> @@ -99,19 +190,21 @@ static char *git_config_val_global(int ident_flag UNUSED)
>  	git_global_config_paths(&user, &xdg);
>  	if (xdg && *xdg) {
>  		normalize_path_copy(xdg, xdg);
> -		strbuf_addf(&buf, "%s\n", xdg);
> +		strbuf_addstr(&buf, xdg);
> +		strbuf_addch(&buf, '\0');
>  	}
>  	if (user && *user) {
>  		normalize_path_copy(user, user);
> -		strbuf_addf(&buf, "%s\n", user);
> +		strbuf_addstr(&buf, user);
> +		strbuf_addch(&buf, '\0');
>  	}

Mental note: we used to use LF at the end, but in this version we add NUL here.

Show 9 quoted lines
>  	free(xdg);
>  	free(user);
> -	strbuf_trim_trailing_newline(&buf);
> -	if (buf.len == 0) {
> +	if (!buf.len) {
>  		strbuf_release(&buf);
>  		return NULL;
>  	}
> +	strbuf_addch(&buf, '\0');
And then we add an extra NUL after that.
>  	return strbuf_detach(&buf, &unused);
>  }
Show 19 quoted lines
> @@ -172,34 +293,35 @@ static struct git_var git_vars[] = {
>  	},
>  };
>  
> -static void list_vars(void)
> +static void list_vars(int nul_term)
>  {
>  	struct git_var *ptr;
> -	char *val;
> -
> -	for (ptr = git_vars; ptr->read; ptr++)
> -		if ((val = ptr->read(0))) {
> -			if (ptr->multivalued && *val) {
> -				struct string_list list = STRING_LIST_INIT_DUP;
> -
> -				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);

We used to split at LF (because we used to concatenate with LF in the git_config_val_globa() that grabs potentially multiple values) and then showed them one by one.

Show 16 quoted lines
> -			} else {
> -				printf("%s=%s\n", ptr->name, val);
> -			}
> -			free(val);
> +	char delim = nul_term ? '\n' : '=';
> +	char term = nul_term ? '\0' : '\n';
> +
> +	for (ptr = git_vars; ptr->read; ptr++) {
> +		char *val = ptr->read(0);
> +
> +		if (!val)
> +			continue;
> +
> +		if (ptr->multivalued) {
> +			for (const char *s = val; *s; s += strlen(s) + 1)
> +				printf("%s%c%s%c", ptr->name, delim, s, term);

Now we use each string pieces (s), skip the string we just showed by advancing the pointer by strlen(s) + 1. If multi-valued variable has ever an empty string as one of the possible values, this scheme would break down, but right now GIT_CONFIG_GLOBAL is the only thing that is .multivalued, and neither the HOME or XDG path is likely to be ever empty, so this may be OK, perhaps? If xdg is defined to be a non-empty string (i.e., "if (xdg && *xdg)" is taken) but if calling normalize_path_copy(xdg, xdg) makes it an empty string, then git_config_val_global() will give "\0/home/gitster/.gitconfig\0\0" for me (the first NUL is after the empty xdg value, the second NUL is terminating HOME value, and the third NUL concludes the whole thing), and then this loop will exit without showing anything (not just skipping an empty XDG, but hiding perfectly healthy HOME value). Is that a concern?

I wonder if we should correct how .multivalued field is handled before we add more of them. For example, .multivalued = 1 item may use something different from .read that uses a string-list to carry the information

	{
		.name = "GIT_CONFIG_GLOBAL",
		.multiread = git_config_val_global,
	},
	static int git_config_val_global(struct string_list *list)
	{
		git_global_config_paths(...);
		if (xdg available)
			string_list_append(list, xdg);
		if (user availble)
			string_list_append(list, user);
		return 0;
	}
	
and then the above part of the code would look more like
	for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
		if (ptr->read) {
			... single read as before ...
		} else (ptr->multiread) {
			struct string_list list = STRING_LIST_INIT;
			ptr->multiread(&list);
			for (size_t i = 0; i < list.nr; i++)
				... use list.items[i].string ...
		}
	}
Show 12 quoted lines
>  static const struct git_var *get_git_var(const char *var)
>  {
>  	struct git_var *ptr;
> +
>  	for (ptr = git_vars; ptr->read; ptr++) {
> -		if (strcmp(var, ptr->name) == 0) {
> +		if (!strcmp(var, ptr->name))
>  			return ptr;
> -		}
>  	}
>  	return NULL;
>  }

An unrelated change like this is distracting and makes it less likely for your patch to succeed. Leave such a clean-up out of a patch that is about a new feature, or fixing a bug.

Andrew Pleeter via GitGitGadgetSep 9, 2026, 01:24 UTC in reply to Andrew Pleeter via GitGitGadget on lore

[PATCH v6] var: support broken-down idents, signing key, multiple args, and -z

From: Andrew Pleeter <andrewpleeter@gmail.com>

While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT, extracting individual components (name, email, or date) currently requires callers to manually parse the composite string. Furthermore, there is no way to query the resolved commit signing key through 'git var', and the command only accepts a single variable at a time.

Teach 'git var' to expose individual identity components and commit signing configuration, and allow querying multiple variables with optional NUL-termination:

- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
- Add GIT_SIGNING_KEY to resolve the key that would be used to sign
  the resulting commit if you were to run 'git commit' right now.
- Allow passing multiple variable arguments (e.g., 'git var
  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
  sequentially.
- Support '-z' to terminate variable outputs with NUL bytes.
- Format 'git var -l -z' using the same convention as 'git config
  list -z' (newline separating key and value, NUL separating entries).
- Delimit values of multi-valued variables with NUL when '-z' is given,
  and output an extra delimiter after multi-valued variables when
  querying multiple variables to disambiguate the stream.
- When querying multiple variables, print an empty record for any
  variable that has no value and continue processing remaining variables.
- Use parse_options() to strictly require options before arguments.
- Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
    var: support broken-down idents, signing key, multiple args, and -z
    
    Teach git var to expose individual identity components and commit
    signing configuration, and allow querying multiple variables with
    optional NUL-termination.
    
    
    Changes since v5:
    =================
    
     * Reverted cosmetic refactoring in get_git_var() to keep the diff
       minimal and focused on adding ptr->multiread.
     * Added BUG("unknown ident_part %d", part) to default case in
       ident_part() and removed redundant NULL checks on split.name_begin
       and split.mail_begin.
     * Adopted first-class int (*multiread)(struct string_list *) callback
       in struct git_var for multi-valued variables like GIT_CONFIG_GLOBAL,
       cleanly populating a struct string_list instead of relying on
       embedded NUL buffers.
     * Improved GIT_AUTHOR_* and GIT_COMMITTER_* documentation in
       Documentation/git-var.adoc to describe the values that would be used
       if you were to run git commit right now.
     * Explicitly documented that single-variable queries and git var -l do
       not print an extra delimiter after multi-valued variables.
    
    
    Changes since v4:
    =================
    
     * Simplified git_signing_key() to directly call get_signing_key() as
       used throughout Git (in tag, send-pack, and sign_buffer()).
     * Renamed null_term to nul_term across builtin/var.c, and simplified
       show_config() callback handling.
     * Replaced the redundant pre-validation loop in cmd_var() by validating
       arguments directly in the main execution loop.
     * When querying multiple variables, print an empty record (blank line
       or \0 with -z) for any variable that has no value, and continue
       printing remaining variables instead of terminating prematurely.
     * Switched multi-valued variable storage (git_config_val_global()) to
       internal \0 delimiters, iterating directly through string sequences
       without allocating a temporary string_list.
     * For multi-variable queries, output an extra delimiter (\n or \0)
       after multi-valued variables to clearly mark the end of their list.
     * Explicitly documented the git var -l -z format and multi-variable
       handling in Documentation/git-var.adoc.
     * Added comprehensive tests for unset variables and multi-valued stream
       delimiters in t/t0007-git-var.sh.
    
    
    Changes since v3:
    =================
    
     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip
       Wood and Junio C Hamano; dropped the alias mechanism and
       commit.gpgsign check.
     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in
       builtin/var.c, strictly enforcing that options precede variable
       arguments.
     * Adopted git config list -z format (key\nvalue\0) for git var -l -z to
       prevent ambiguity with = in config keys.
     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with
       NUL bytes under -z.
     * Replaced char part in ident_part() with enum ident_part.
     * Split synopsis in Documentation/git-var.adoc into separate lines for
       -l and <variable>..., and removed awkward legacy phrasing ("of a
       piece of code").
     * Added tests in t/t0007-git-var.sh covering the new -z format,
       multi-valued -z, and argument ordering.
    
    
    Changes since v2:
    =================
    
     * Drop git ident / git whoami subcommand entirely.
     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
     * Add GIT_SIGNING_KEY to resolve commit signing keys.
     * Teach git var to accept multiple variable arguments (git var <var1>
       <var2> ...).
     * Add -z option to terminate outputs with NUL bytes (including git var
       -l -z).
     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v6
Pull-Request: https://github.com/git/git/pull/2388
Range-diff vs v5:
 1:  04833673ea ! 1:  3b51f61a11 var: support broken-down idents, signing key, multiple args, and -z
     @@ Documentation/git-var.adoc: OPTIONS
       ---------
       `GIT_AUTHOR_IDENT`::
      -    The author of a piece of code.
     -+    The author.
     ++    The author name, email, and date that would be used if you were to
     ++    run `git commit` right now.
      +
      +`GIT_AUTHOR_NAME`::
     -+    The name of the author.
     ++    The author name that would be used if you were to run `git commit`
     ++    right now.
      +
      +`GIT_AUTHOR_EMAIL`::
     -+    The email of the author.
     ++    The author email that would be used if you were to run `git commit`
     ++    right now.
      +
      +`GIT_AUTHOR_DATE`::
     -+    The date and timezone of the author.
     ++    The author date and timezone that would be used if you were to run
     ++    `git commit` right now.
       
       `GIT_COMMITTER_IDENT`::
      -    The person who put a piece of code into Git.
     -+    The committer.
     ++    The committer name, email, and date that would be used if you were
     ++    to run `git commit` right now.
      +
      +`GIT_COMMITTER_NAME`::
     -+    The name of the committer.
     ++    The name of the committer that would be used if you were to run
     ++    `git commit` right now.
      +
      +`GIT_COMMITTER_EMAIL`::
     -+    The email of the committer.
     ++    The email of the committer that would be used if you were to run
     ++    `git commit` right now.
      +
      +`GIT_COMMITTER_DATE`::
     -+    The date and timezone of the committer.
     ++    The committer date and timezone that would be used if you were to run
     ++    `git commit` right now.
      +
      +`GIT_SIGNING_KEY`::
      +    The key that would be used to sign the resulting commit if you were
     @@ Documentation/git-var.adoc: endif::git-default-pager[]
      +and are listed in order from highest to lowest priority. When querying
      +multiple variables, an extra newline (or an extra NUL byte if `-z` is
      +given) is printed after the values of a multi-valued variable to mark the
     -+end of its list. Callers should be prepared for any such path value to
     ++end of its list. (Single-variable queries and `git var -l` do not print
     ++an extra delimiter). 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
     @@ builtin/var.c
       #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
      +};
     -+
     + 
     +-static const char var_usage[] = "git var (-l | <variable>)";
      +enum ident_part {
      +	IDENT_NAME,
      +	IDENT_MAIL,
     @@ builtin/var.c
      +
      +	switch (part) {
      +	case IDENT_NAME:
     -+		if (!split.name_begin || !split.name_end)
     -+			return NULL;
      +		return xmemdupz(split.name_begin,
      +				split.name_end - split.name_begin);
      +	case IDENT_MAIL:
     -+		if (!split.mail_begin || !split.mail_end)
     -+			return NULL;
      +		return xmemdupz(split.mail_begin,
      +				split.mail_end - split.mail_begin);
      +	case IDENT_DATE:
     @@ builtin/var.c
      +					split.date_begin);
      +		return NULL;
      +	default:
     -+		return NULL;
     ++		BUG("unknown ident_part %d", part);
      +	}
      +}
      +
     @@ builtin/var.c
       static char *editor(int ident_flag UNUSED)
       {
       	return xstrdup_or_null(git_editor());
     -@@ builtin/var.c: static char *git_config_val_global(int ident_flag UNUSED)
     +@@ builtin/var.c: static char *git_config_val_system(int ident_flag UNUSED)
     + 	return NULL;
     + }
     + 
     +-static char *git_config_val_global(int ident_flag UNUSED)
     ++static int 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);
     -+		strbuf_addstr(&buf, xdg);
     -+		strbuf_addch(&buf, '\0');
     ++		string_list_append(list, xdg);
       	}
       	if (user && *user) {
       		normalize_path_copy(user, user);
      -		strbuf_addf(&buf, "%s\n", user);
     -+		strbuf_addstr(&buf, user);
     -+		strbuf_addch(&buf, '\0');
     ++		string_list_append(list, user);
       	}
       	free(xdg);
       	free(user);
      -	strbuf_trim_trailing_newline(&buf);
      -	if (buf.len == 0) {
     -+	if (!buf.len) {
     - 		strbuf_release(&buf);
     - 		return NULL;
     - 	}
     -+	strbuf_addch(&buf, '\0');
     - 	return strbuf_detach(&buf, &unused);
     +-		strbuf_release(&buf);
     +-		return NULL;
     +-	}
     +-	return strbuf_detach(&buf, &unused);
     ++	return !list->nr;
       }
       
     -@@ builtin/var.c: static struct git_var git_vars[] = {
     + struct git_var {
     + 	const char *name;
     + 	char *(*read)(int);
     +-	int multivalued;
     ++	int (*multiread)(struct string_list *);
     + };
     + static struct git_var git_vars[] = {
     + 	{
       		.name = "GIT_COMMITTER_IDENT",
       		.read = committer,
       	},
     @@ builtin/var.c: static struct git_var git_vars[] = {
       	{
       		.name = "GIT_SHELL_PATH",
       		.read = shell_path,
     +@@ builtin/var.c: static struct git_var git_vars[] = {
     + 	},
     + 	{
     + 		.name = "GIT_CONFIG_GLOBAL",
     +-		.read = git_config_val_global,
     +-		.multivalued = 1,
     ++		.multiread = git_config_val_global,
     + 	},
     + 	{
     + 		.name = "",
      @@ builtin/var.c: static struct git_var git_vars[] = {
       	},
       };
     @@ builtin/var.c: static struct git_var git_vars[] = {
       {
       	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);
     -+	char delim = nul_term ? '\n' : '=';
     -+	char term = nul_term ? '\0' : '\n';
     -+
     -+	for (ptr = git_vars; ptr->read; ptr++) {
     -+		char *val = ptr->read(0);
     -+
     -+		if (!val)
     -+			continue;
     -+
     -+		if (ptr->multivalued) {
     -+			for (const char *s = val; *s; s += strlen(s) + 1)
     -+				printf("%s%c%s%c", ptr->name, delim, s, term);
     -+		} else {
     -+			printf("%s%c%s%c", ptr->name, delim, val, term);
     ++		} else if (ptr->multiread) {
     ++			struct string_list list = STRING_LIST_INIT_DUP;
     ++			size_t i;
     ++
     ++			if (!ptr->multiread(&list)) {
     ++				for (i = 0; i < list.nr; i++)
     ++					printf("%s%c%s%c", ptr->name, delim,
     ++					       list.items[i].string, term);
     ++			}
     ++			string_list_clear(&list, 0);
       		}
     -+		free(val);
      +	}
       }
       
       static const struct git_var *get_git_var(const char *var)
       {
       	struct git_var *ptr;
     -+
     - 	for (ptr = git_vars; ptr->read; ptr++) {
     --		if (strcmp(var, ptr->name) == 0) {
     -+		if (!strcmp(var, ptr->name))
     +-	for (ptr = git_vars; ptr->read; ptr++) {
     ++	for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
     + 		if (strcmp(var, ptr->name) == 0) {
       			return ptr;
     --		}
     - 	}
     - 	return NULL;
     - }
     + 		}
      @@ builtin/var.c: 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)
     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)
      -	if (!git_var)
      -		usage(var_usage);
      +	term = nul_term ? '\0' : '\n';
     -+
     -+	for (i = 0; i < argc; i++) {
     -+		const struct git_var *git_var = get_git_var(argv[i]);
     -+		char *val;
       
      -	val = git_var->read(IDENT_STRICT);
      -	if (!val)
      -		return 1;
     -+		if (!git_var)
     -+			usage_with_options(var_usage, options);
     ++	for (i = 0; i < argc; i++) {
     ++		const struct git_var *git_var = get_git_var(argv[i]);
       
      -	printf("%s\n", val);
      -	free(val);
     -+		val = git_var->read(IDENT_STRICT);
     -+		if (!val) {
     -+			if (argc == 1)
     -+				return 1;
     -+			ret = 1;
     -+			printf("%c", term);
     -+			continue;
     -+		}
     ++		if (!git_var)
     ++			usage_with_options(var_usage, options);
     ++
     ++		if (git_var->read) {
     ++			char *val = git_var->read(IDENT_STRICT);
      +
     -+		if (git_var->multivalued) {
     -+			for (const char *s = val; *s; s += strlen(s) + 1)
     -+				printf("%s%c", s, term);
     -+			if (argc > 1)
     ++			if (!val) {
     ++				if (argc == 1)
     ++					return 1;
     ++				ret = 1;
      +				printf("%c", term);
     -+		} else {
     ++				continue;
     ++			}
      +			printf("%s%c", val, term);
     ++			free(val);
     ++		} else if (git_var->multiread) {
     ++			struct string_list list = STRING_LIST_INIT_DUP;
     ++			size_t j;
     ++
     ++			if (git_var->multiread(&list) || !list.nr) {
     ++				if (argc == 1) {
     ++					string_list_clear(&list, 0);
     ++					return 1;
     ++				}
     ++				ret = 1;
     ++				printf("%c", term);
     ++			} else {
     ++				for (j = 0; j < list.nr; j++)
     ++					printf("%s%c", list.items[j].string, term);
     ++				if (argc > 1)
     ++					printf("%c", term);
     ++			}
     ++			string_list_clear(&list, 0);
      +		}
     -+		free(val);
      +	}
       
      -	return 0;
 Documentation/git-var.adoc |  76 +++++++++--
 builtin/var.c              | 265 ++++++++++++++++++++++++++++++-------
 t/t0007-git-var.sh         | 123 +++++++++++++++++
 3 files changed, 402 insertions(+), 62 deletions(-)
Show changes to 3 files +402 −62

Documentation/git-var.adoc, builtin/var.c, t/t0007-git-var.sh

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index 697c10aded..2c1eaf3cf7 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -9,12 +9,16 @@ 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. Exits with code 1 if any requested
+variable has no value. When multiple variables are requested, an empty
+record (a blank line, or an empty NUL-terminated record when `-z` is given)
+is printed for any variable that has no value, and the command continues
+processing the remaining variables.
 
 OPTIONS
 -------
@@ -24,19 +28,65 @@ 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`, 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
+Eric W. Biederman
+ebiederm@lnxi.com
+------------
 
 VARIABLES
 ---------
 `GIT_AUTHOR_IDENT`::
-    The author of a piece of code.
+    The author name, email, and date that would be used if you were to
+    run `git commit` right now.
+
+`GIT_AUTHOR_NAME`::
+    The author name that would be used if you were to run `git commit`
+    right now.
+
+`GIT_AUTHOR_EMAIL`::
+    The author email that would be used if you were to run `git commit`
+    right now.
+
+`GIT_AUTHOR_DATE`::
+    The author date and timezone that would be used if you were to run
+    `git commit` right now.
 
 `GIT_COMMITTER_IDENT`::
-    The person who put a piece of code into Git.
+    The committer name, email, and date that would be used if you were
+    to run `git commit` right now.
+
+`GIT_COMMITTER_NAME`::
+    The name of the committer that would be used if you were to run
+    `git commit` right now.
+
+`GIT_COMMITTER_EMAIL`::
+    The email of the committer that would be used if you were to run
+    `git commit` right now.
+
+`GIT_COMMITTER_DATE`::
+    The committer date and timezone that would be used if you were to run
+    `git commit` right now.
+
+`GIT_SIGNING_KEY`::
+    The key that would be used to sign the resulting commit if you were
+    to run `git commit` right now.
 
 `GIT_EDITOR`::
     Text editor for use by Git commands.  The value is meant to be
@@ -85,9 +135,13 @@ endif::git-default-pager[]
     The path to the global (per-user) configuration files, if any.
 
 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, an extra newline (or an extra NUL byte if `-z` is
+given) is printed after the values of a multi-valued variable to mark the
+end of its list. (Single-variable queries and `git var -l` do not print
+an extra delimiter). 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.
diff --git a/builtin/var.c b/builtin/var.c
index cc3a43cde2..decada1602 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -12,25 +12,112 @@
 #include "config.h"
 #include "editor.h"
 #include "environment.h"
+#include "gpg-interface.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 * const var_usage[] = {
+	N_("git var [-z] -l"),
+	N_("git var [-z] <variable>..."),
+	NULL
+};
 
-static const char var_usage[] = "git var (-l | <variable>)";
+enum ident_part {
+	IDENT_NAME,
+	IDENT_MAIL,
+	IDENT_DATE,
+};
 
 static char *committer(int ident_flag)
 {
 	return xstrdup_or_null(git_committer_info(ident_flag));
 }
 
+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:
+		return xmemdupz(split.name_begin,
+				split.name_end - split.name_begin);
+	case IDENT_MAIL:
+		return xmemdupz(split.mail_begin,
+				split.mail_end - split.mail_begin);
+	case IDENT_DATE:
+		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;
+	default:
+		BUG("unknown ident_part %d", part);
+	}
+}
+
+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 *git_signing_key(int ident_flag UNUSED)
+{
+	char *signing_key = get_signing_key();
+
+	if (signing_key && !*signing_key) {
+		free(signing_key);
+		return NULL;
+	}
+	return signing_key;
+}
+
 static char *editor(int ident_flag UNUSED)
 {
 	return xstrdup_or_null(git_editor());
@@ -90,45 +177,62 @@ static char *git_config_val_system(int ident_flag UNUSED)
 	return NULL;
 }
 
-static char *git_config_val_global(int ident_flag UNUSED)
+static int 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);
+	return !list->nr;
 }
 
 struct git_var {
 	const char *name;
 	char *(*read)(int);
-	int multivalued;
+	int (*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,
@@ -145,6 +249,10 @@ static struct git_var git_vars[] = {
 		.name = "GIT_DEFAULT_BRANCH",
 		.read = default_branch,
 	},
+	{
+		.name = "GIT_SIGNING_KEY",
+		.read = git_signing_key,
+	},
 	{
 		.name = "GIT_SHELL_PATH",
 		.read = shell_path,
@@ -163,8 +271,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 +279,38 @@ 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 if (ptr->multiread) {
+			struct string_list list = STRING_LIST_INIT_DUP;
+			size_t i;
+
+			if (!ptr->multiread(&list)) {
+				for (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 +321,91 @@ 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;
+	int ret = 0;
+	int i;
+	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);
+	term = nul_term ? '\0' : '\n';
 
-	val = git_var->read(IDENT_STRICT);
-	if (!val)
-		return 1;
+	for (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;
+				ret = 1;
+				printf("%c", term);
+				continue;
+			}
+			printf("%s%c", val, term);
+			free(val);
+		} else if (git_var->multiread) {
+			struct string_list list = STRING_LIST_INIT_DUP;
+			size_t j;
+
+			if (git_var->multiread(&list) || !list.nr) {
+				if (argc == 1) {
+					string_list_clear(&list, 0);
+					return 1;
+				}
+				ret = 1;
+				printf("%c", term);
+			} else {
+				for (j = 0; j < list.nr; j++)
+					printf("%s%c", list.items[j].string, term);
+				if (argc > 1)
+					printf("%c", term);
+			}
+			string_list_clear(&list, 0);
+		}
+	}
 
-	return 0;
+	return ret;
 }
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 2b60317758..92b68b9ab4 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -276,4 +276,127 @@ test_expect_success '`git var -l` works even without HOME' '
 	)
 '
 
+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 'get multiple variables' '
+	test_tick &&
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME
+	$GIT_AUTHOR_EMAIL
+	$GIT_COMMITTER_NAME
+	$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 'get multiple variables with -z' '
+	test_tick &&
+	printf "%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
+	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >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 &&
+	printf "%s\0" "$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expected &&
+	test_cmp expected actual
+'
+
+test_expect_success 'git var -l -z' '
+	git var -l -z >actual &&
+	tr "\0" "\n" <actual >actual.lines &&
+	echo "$GIT_AUTHOR_NAME" >expect &&
+	sed -n "/^GIT_AUTHOR_NAME$/{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 'get GIT_SIGNING_KEY with user.signingkey configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	echo "TEST_KEY_ID" >expect &&
+	git var GIT_SIGNING_KEY >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '
+	test_config user.signingkey "" &&
+	test_must_fail git var GIT_SIGNING_KEY
+'
+
+test_expect_success 'git var -l lists new variables' '
+	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_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	git var -l >actual &&
+	test_grep "^GIT_SIGNING_KEY=TEST_KEY_ID" actual
+'
+
+test_expect_success 'options must precede variable arguments' '
+	test_must_fail git var GIT_AUTHOR_NAME -z
+'
+
+test_expect_success 'get multiple variables with unset variable outputs blank record' '
+	test_config user.signingkey "" &&
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME
+
+	$GIT_COMMITTER_NAME
+	EOF
+	test_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables with -z and unset variable' '
+	test_config user.signingkey "" &&
+	printf "%s\0\0%s\0" "$GIT_AUTHOR_NAME" "$GIT_COMMITTER_NAME" >expect &&
+	test_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables including multi-valued variable with -z' '
+	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
+	printf "%s\0%s\0%s\0\0%s\0" "$GIT_AUTHOR_NAME" \
+		"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" \
+		"$GIT_AUTHOR_EMAIL" >expect &&
+	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
+		git var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&
+	test_cmp expect actual
+'
+
 test_done

base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
-- 
gitgitgadget
Phillip WoodSep 9, 2026, 15:36 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH v6] var: support broken-down idents, signing key, multiple args, and -z

Hi Andrew
On 09/09/2026 02:24, Andrew Pleeter via GitGitGadget wrote:
Show 16 quoted lines
> From: Andrew Pleeter <andrewpleeter@gmail.com>
> 
> While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,
> extracting individual components (name, email, or date) currently
> requires callers to manually parse the composite string. Furthermore,
> there is no way to query the resolved commit signing key through
> 'git var', and the command only accepts a single variable at a time.
> 
> Teach 'git var' to expose individual identity components and commit
> signing configuration, and allow querying multiple variables with
> optional NUL-termination:
> 
> - Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
> - Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
> - Add GIT_SIGNING_KEY to resolve the key that would be used to sign
>    the resulting commit if you were to run 'git commit' right now.

I'm still curious what the use case for GIT_SIGNING_KEY is. Is the key alone enough for the caller to determine if they should be using gpg or ssh? I've asked this twice already - when a reviewer asks a question it is helpful to rely rather than just sending a new version of the patch.

Show 16 quoted lines
> - Allow passing multiple variable arguments (e.g., 'git var
>    GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
>    sequentially.
> - Support '-z' to terminate variable outputs with NUL bytes.
> - Format 'git var -l -z' using the same convention as 'git config
>    list -z' (newline separating key and value, NUL separating entries).
> - Delimit values of multi-valued variables with NUL when '-z' is given,
>    and output an extra delimiter after multi-valued variables when
>    querying multiple variables to disambiguate the stream.
> - When querying multiple variables, print an empty record for any
>    variable that has no value and continue processing remaining variables.
> - Use parse_options() to strictly require options before arguments.
> - Update Documentation/git-var.adoc and t/t0007-git-var.sh.
> 
> Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
> ---
Show 18 quoted lines
> diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
> index 697c10aded..2c1eaf3cf7 100644
> --- a/Documentation/git-var.adoc
> +++ b/Documentation/git-var.adoc
> @@ -9,12 +9,16 @@ 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. Exits with code 1 if any requested
> +variable has no value.

I'm not sure that is very useful when the user asks for more than one variable - they can see the value was empty by looking at the output and means callers cannot check for fatal errors such as an invalid variable name by simply looking for a non-zero exit code.

Show 15 quoted lines
> When multiple variables are requested, an empty
> +record (a blank line, or an empty NUL-terminated record when `-z` is given)
> +is printed for any variable that has no value, and the command continues
> +processing the remaining variables.
>   
>   OPTIONS
>   -------
> @@ -24,19 +28,65 @@ 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`, the variable name and its value are separated by a newline,
> +	and each entry is terminated with a NUL byte.
Good
Show 12 quoted lines
> @@ -85,9 +135,13 @@ endif::git-default-pager[]
>       The path to the global (per-user) configuration files, if any.
>   
>   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, an extra newline (or an extra NUL byte if `-z` is
> +given) is printed after the values of a multi-valued variable to mark the
> +end of its list.

We should mark each mult-valued varibale in its description so that users know when to expect a list.

Show 6 quoted lines
> (Single-variable queries and `git var -l` do not print
> +an extra delimiter). 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.
Show 28 quoted lines
> -static char *git_config_val_global(int ident_flag UNUSED)
> +static int 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);
> +	return !list->nr;
>   }

This is a nice improvement that could perhaps be split out into a separate preparatory change together with the change from a flag to a different read function for multi-valued variables below.

Show 7 quoted lines
>   
>   struct git_var {
>   	const char *name;
>   	char *(*read)(int);
> -	int multivalued;
> +	int (*multiread)(struct string_list *);
>   };
Show 28 quoted lines
> -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 if (ptr->multiread) {

We should just assume that ptr->multread is set when ptr->read is not, or possibly add an else clause that calls BUG().

Show 9 quoted lines
> +			struct string_list list = STRING_LIST_INIT_DUP;
> +			size_t i;
> +
> +			if (!ptr->multiread(&list)) {
> +				for (i = 0; i < list.nr; i++)
> +					printf("%s%c%s%c", ptr->name, delim,
> +					       list.items[i].string, term);
> +			}
> +			string_list_clear(&list, 0);
Show 5 quoted lines
>   int cmd_var(int argc,
>   	    const char **argv,
> -	    const char *prefix UNUSED,
> +	    const char *prefix,
>   	    struct repository *repo UNUSED)
 > [...]
Show 15 quoted lines
> +	for (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;
> +				ret = 1;

What's the benefit of this? The caller can see there was an empty value so why do we want a non-zero exit code as well. For example, if the caller is asking for GIT_CONFIG_SYSTEM and GIT_CONFIG_GLOBAL but the user ran the script with GIT_CONFIG_NOSYSTEM then that shouldn't be an error - the caller should just not use the system config.

Show 10 quoted lines
> +				printf("%c", term);
> +				continue;
> +			}
> +			printf("%s%c", val, term);
> +			free(val);
> +		} else if (git_var->multiread) {
> +			struct string_list list = STRING_LIST_INIT_DUP;
> +			size_t j;
> +
> +			if (git_var->multiread(&list) || !list.nr) {
Why are we checking the return value of the function and the list length 
- surely the list length tells us everything we need to know.
Show 13 quoted lines
> +				if (argc == 1) {
> +					string_list_clear(&list, 0);
> +					return 1;
> +				}
> +				ret = 1;
> +				printf("%c", term);
> +			} else {
> +				for (j = 0; j < list.nr; j++)
> +					printf("%s%c", list.items[j].string, term);
> +				if (argc > 1)
> +					printf("%c", term);
> +			}
> +			string_list_clear(&list, 0);
I think the above can be simplified to
	} else {
		struct string_list list = STRING_LIST_INIT_NODUP;
		
		git_var->multiread(&list);
		if (argc == 1 && !list.nr) {
			return 1;
		}
		for (j = 0; j < list.nr; j++)
			printf("%s%c", list.items[j].string, term);
		if (argc > 1)
			putc(term);
		string_list_clear(&list, 0);
	}

I've not had time to look too closely at the tests, but I did notice they use test_cmp() on files containing '\0' which isn't a good idea because diff will see them as binary files. We have helpers like nul_to_q to translate nul to a printable character. I'm going to be off the list from tomorrow until the middle of next week so it will be a few days before I look at the next (and hopefully final) version.

Thanks
Phillip
Show 140 quoted lines
> +		}
> +	}
>   
> -	return 0;
> +	return ret;
>   }
> diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
> index 2b60317758..92b68b9ab4 100755
> --- a/t/t0007-git-var.sh
> +++ b/t/t0007-git-var.sh
> @@ -276,4 +276,127 @@ test_expect_success '`git var -l` works even without HOME' '
>   	)
>   '
>   
> +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 'get multiple variables' '
> +	test_tick &&
> +	cat >expect <<-EOF &&
> +	$GIT_AUTHOR_NAME
> +	$GIT_AUTHOR_EMAIL
> +	$GIT_COMMITTER_NAME
> +	$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 'get multiple variables with -z' '
> +	test_tick &&
> +	printf "%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
> +	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >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 &&
> +	printf "%s\0" "$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expected &&
> +	test_cmp expected actual
> +'
> +
> +test_expect_success 'git var -l -z' '
> +	git var -l -z >actual &&
> +	tr "\0" "\n" <actual >actual.lines &&
> +	echo "$GIT_AUTHOR_NAME" >expect &&
> +	sed -n "/^GIT_AUTHOR_NAME$/{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 'get GIT_SIGNING_KEY with user.signingkey configured' '
> +	test_config user.signingkey "TEST_KEY_ID" &&
> +	echo "TEST_KEY_ID" >expect &&
> +	git var GIT_SIGNING_KEY >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'get GIT_SIGNING_KEY fails when unset' '
> +	test_config user.signingkey "" &&
> +	test_must_fail git var GIT_SIGNING_KEY
> +'
> +
> +test_expect_success 'git var -l lists new variables' '
> +	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_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '
> +	test_config user.signingkey "TEST_KEY_ID" &&
> +	git var -l >actual &&
> +	test_grep "^GIT_SIGNING_KEY=TEST_KEY_ID" actual
> +'
> +
> +test_expect_success 'options must precede variable arguments' '
> +	test_must_fail git var GIT_AUTHOR_NAME -z
> +'
> +
> +test_expect_success 'get multiple variables with unset variable outputs blank record' '
> +	test_config user.signingkey "" &&
> +	cat >expect <<-EOF &&
> +	$GIT_AUTHOR_NAME
> +
> +	$GIT_COMMITTER_NAME
> +	EOF
> +	test_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'get multiple variables with -z and unset variable' '
> +	test_config user.signingkey "" &&
> +	printf "%s\0\0%s\0" "$GIT_AUTHOR_NAME" "$GIT_COMMITTER_NAME" >expect &&
> +	test_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success 'get multiple variables including multi-valued variable with -z' '
> +	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
> +	printf "%s\0%s\0%s\0\0%s\0" "$GIT_AUTHOR_NAME" \
> +		"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" \
> +		"$GIT_AUTHOR_EMAIL" >expect &&
> +	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
> +		git var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&
> +	test_cmp expect actual
> +'
> +
>   test_done
> 
> base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
Junio C HamanoSep 9, 2026, 16:42 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH v6] var: support broken-down idents, signing key, multiple args, and -z

"Andrew Pleeter via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 18 quoted lines
>  VARIABLES
>  ---------
>  `GIT_AUTHOR_IDENT`::
> -    The author of a piece of code.
> +    The author name, email, and date that would be used if you were to
> +    run `git commit` right now.
> +
> +`GIT_AUTHOR_NAME`::
> +    The author name that would be used if you were to run `git commit`
> +    right now.
> +
> +`GIT_AUTHOR_EMAIL`::
> +    The author email that would be used if you were to run `git commit`
> +    right now.
> +
> +`GIT_AUTHOR_DATE`::
> +    The author date and timezone that would be used if you were to run
> +    `git commit` right now.

This is better than the previous iteration, but wastes reader's time with full of repetitions. Have you looked at the one suggested in https://lore.kernel.org/git/xmqqbjaecjxb.fsf@gitster.g/ for example to present more information in much more concise way?

Show 11 quoted lines
>  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, an extra newline (or an extra NUL byte if `-z` is
> +given) is printed after the values of a multi-valued variable to mark the
> +end of its list. (Single-variable queries and `git var -l` do not print
> +an extra delimiter). Callers should be prepared for any such path value to
> +contain multiple items.

This makes it clear that the query forms we had before this change will produce the same output, and that queries asking for more than one value use a new format, which is good.

However, I am not sure why we want an extra delimiter only after a multi-valued variable. Does it mean that the reading script needs to be aware of which variables are multi-valued and which are not? It is not clear whether this extra delimiter is present only when a potentially multi-valued variable actually has multiple values, or if we will have the extra delimiter even when such a variable happens to have only a single (or perhaps zero) value.

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

... some paths may not be printed even when explicitly requested in the new "give me values of these variables" form, it appears to me that the extra delimiter, even with the reader's knowledge of which variables are multi-valued, does not help identify which value corresponds to which requested variable. I can accept, to a limited degree, the argument that a list of 'val' is less work to parse than a list of 'var=val' simply because you do not have to strip 'var=' from the front. However, it looks to me that the proposed format makes the wrong trade-off by making it harder to match a variable to its value(s).

Show 18 quoted lines
> diff --git a/builtin/var.c b/builtin/var.c
> index cc3a43cde2..decada1602 100644
> +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:
> +		return xmemdupz(split.name_begin,
> +				split.name_end - split.name_begin);
> +	case IDENT_MAIL:
> +		return xmemdupz(split.mail_begin,
> +				split.mail_end - split.mail_begin);

This is better in that it no longer returns NULL upon an impossible condition like the previous iteration did. Even better, we could protect ourselves against breakage caused by careless updates to split_ident_line() and git_*_info() functions we rely on by keep the check but mark BUG(), e.g.,

                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);
Show 12 quoted lines
> +	case IDENT_DATE:
> +		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;

I gave ".name_begin/.name_end cannot be NULL with the way you call the other routines" in my previous response as a mere example, while hoping that you'd do similar due dilligence to other values. With the way committer_date() and author_date() are called (below), can fmt_ident() ever return an ident without datestamp and timezone, requiring us to fall back on NULL returns like this? You are not passing IDENT_NO_DATE flag anywhere, are you?

Show 36 quoted lines
>  struct git_var {
>  	const char *name;
>  	char *(*read)(int);
> -	int multivalued;
> +	int (*multiread)(struct string_list *);
>  };
> ...
>  int cmd_var(int argc,
>  	    const char **argv,
> -	    const char *prefix UNUSED,
> +	    const char *prefix,
>  	    struct repository *repo UNUSED)
>  {
> +	int list = 0;
> +	int nul_term = 0;
> +	int ret = 0;
> +	int i;
> +	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(),
> +	};
>  
> +	argc = parse_options(argc, argv, prefix, options,
> +			     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);
>  
> +	if (list) {
> +		if (argc)
> +			usage_with_options(var_usage, options);
> +		repo_config(the_repository, show_config, &nul_term);
> +		list_vars(nul_term);
>  		return 0;
>  	}
OK.  Using "-l" and having named variables are incompatible.
> +
> +	if (!argc)
> +		usage_with_options(var_usage, options);

And not having named variables without "-l" invites the usage message. We used to call usage() that exits with 129 and usage_with_options() does so, too.

Show 20 quoted lines
>  	repo_config(the_repository, git_default_config, NULL);
>  
> +	term = nul_term ? '\0' : '\n';
>  
> +	for (i = 0; i < argc; i++) {
> +		const struct git_var *git_var = get_git_var(argv[i]);
>  
> +		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;
> +				ret = 1;
> +				printf("%c", term);
> +				continue;
> +			}

So the idea is when there is a single var on the command line, missing value gives no output and exit(1), but in the new "more than one variable" mode, we note the fact that we had one failing variable, emit a line terminator (NUL or LF) to help readers that expect one "line" per request.

Show 13 quoted lines
> +			printf("%s%c", val, term);
> +			free(val);
> +		} else if (git_var->multiread) {
> +			struct string_list list = STRING_LIST_INIT_DUP;
> +			size_t j;
> +
> +			if (git_var->multiread(&list) || !list.nr) {
> +				if (argc == 1) {
> +					string_list_clear(&list, 0);
> +					return 1;
> +				}
> +				ret = 1;
> +				printf("%c", term);

The same for variables that may have multiple values when they lack any value.

Show 5 quoted lines
> +			} else {
> +				for (j = 0; j < list.nr; j++)
> +					printf("%s%c", list.items[j].string, term);
> +				if (argc > 1)
> +					printf("%c", term);

So this answers the question I had on ambiguous documentation. A variable that can have multiple values (including 0 values) will have N "lines" of N values, plus an empty "line".

Show 7 quoted lines
> +			}
> +			string_list_clear(&list, 0);
> +		}
> +	}
>  
> +	return ret;
>  }

And we return "ret" that memorizes if we ever had a failure in the middle. When there is no failure, we return 0 that is the value "ret" was initialized with.

If I were designing this, I'd rather (1) get rid of the "empty line" convention for multi-valued variables, and (2) model multi-variable mode more after "-l" mode. IOW, instead of thinking of the case where the user gave us two variables like two "git var VARIBLE$N" calls given back to back, thinking it more like "git var -l | grep -e VARIABLE1= -e VARIABLE2=".

Thanks.
Andrew Pleeter via GitGitGadgetSep 10, 2026, 03:09 UTC in reply to Andrew Pleeter via GitGitGadget on lore

[PATCH v7] var: support broken-down idents, signing key, multiple args, and -z

From: Andrew Pleeter <andrewpleeter@gmail.com>

While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT, extracting individual components (name, email, or date) currently requires callers to manually parse the composite string. Furthermore, there is no way to query the resolved commit signing key through 'git var', and the command only accepts a single variable at a time.

Teach 'git var' to expose individual identity components and commit signing configuration, and allow querying multiple variables with optional NUL-termination:

- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
- Add GIT_SIGNING_KEY to resolve the key that would be used to sign
  the resulting commit if you were to run 'git commit' right now.
- Allow passing multiple variable arguments (e.g., 'git var
  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
  sequentially.
- Support '-z' to terminate variable outputs with NUL bytes.
- Format 'git var -l -z' using the same convention as 'git config
  list -z' (newline separating key and value, NUL separating entries).
- Delimit values of multi-valued variables with NUL when '-z' is given,
  and output an extra delimiter after multi-valued variables when
  querying multiple variables to disambiguate the stream.
- When querying multiple variables, print an empty record for any
  variable that has no value and continue processing remaining variables.
- Use parse_options() to strictly require options before arguments.
- Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
    var: support broken-down idents, signing key, multiple args, and -z
    
    Teach git var to expose individual identity components and commit
    signing configuration, and allow querying multiple variables with
    optional NUL-termination.
    
    
    Changes since v6:
    =================
    
     * Grouped GIT_AUTHOR_* and GIT_COMMITTER_* entries together into
       concise definitions in Documentation/git-var.adoc to avoid repetitive
       descriptions, per feedback from Junio C. Hamano.
     * Added explicit BUG() checks in ident_part() for NULL name/email
       pointers and date/timezone to protect against unforeseen changes in
       split_ident_line().
     * Changed struct git_var member multiread to return void (void
       (*multiread)(struct string_list *)) since list->nr communicates
       length.
     * Simplified list_vars() to use an unconditional else block for
       ptr->multiread instead of redundant else if.
     * In cmd_var(), do not set a non-zero exit status for missing variables
       in multi-variable queries; callers can see the empty record (e.g.
       GIT_CONFIG_SYSTEM under GIT_CONFIG_NOSYSTEM), matching Phillip Wood's
       recommendation.
     * Used putc(term, stdout) in cmd_var() when emitting delimiters.
     * In t/t0007-git-var.sh, converted -z tests to pipe output through
       nul_to_q so test_cmp produces clean diffs rather than binary
       comparison errors.
    
    
    Changes since v5:
    =================
    
     * Reverted cosmetic refactoring in get_git_var() to keep the diff
       minimal and focused on adding ptr->multiread.
     * Added BUG("unknown ident_part %d", part) to default case in
       ident_part() and removed redundant NULL checks on split.name_begin
       and split.mail_begin.
     * Adopted first-class int (*multiread)(struct string_list *) callback
       in struct git_var for multi-valued variables like GIT_CONFIG_GLOBAL,
       cleanly populating a struct string_list instead of relying on
       embedded NUL buffers.
     * Improved GIT_AUTHOR_* and GIT_COMMITTER_* documentation in
       Documentation/git-var.adoc to describe the values that would be used
       if you were to run git commit right now.
     * Explicitly documented that single-variable queries and git var -l do
       not print an extra delimiter after multi-valued variables.
    
    
    Changes since v4:
    =================
    
     * Simplified git_signing_key() to directly call get_signing_key() as
       used throughout Git (in tag, send-pack, and sign_buffer()).
     * Renamed null_term to nul_term across builtin/var.c, and simplified
       show_config() callback handling.
     * Replaced the redundant pre-validation loop in cmd_var() by validating
       arguments directly in the main execution loop.
     * When querying multiple variables, print an empty record (blank line
       or \0 with -z) for any variable that has no value, and continue
       printing remaining variables instead of terminating prematurely.
     * Switched multi-valued variable storage (git_config_val_global()) to
       internal \0 delimiters, iterating directly through string sequences
       without allocating a temporary string_list.
     * For multi-variable queries, output an extra delimiter (\n or \0)
       after multi-valued variables to clearly mark the end of their list.
     * Explicitly documented the git var -l -z format and multi-variable
       handling in Documentation/git-var.adoc.
     * Added comprehensive tests for unset variables and multi-valued stream
       delimiters in t/t0007-git-var.sh.
    
    
    Changes since v3:
    =================
    
     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip
       Wood and Junio C Hamano; dropped the alias mechanism and
       commit.gpgsign check.
     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in
       builtin/var.c, strictly enforcing that options precede variable
       arguments.
     * Adopted git config list -z format (key\nvalue\0) for git var -l -z to
       prevent ambiguity with = in config keys.
     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with
       NUL bytes under -z.
     * Replaced char part in ident_part() with enum ident_part.
     * Split synopsis in Documentation/git-var.adoc into separate lines for
       -l and <variable>..., and removed awkward legacy phrasing ("of a
       piece of code").
     * Added tests in t/t0007-git-var.sh covering the new -z format,
       multi-valued -z, and argument ordering.
    
    
    Changes since v2:
    =================
    
     * Drop git ident / git whoami subcommand entirely.
     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
     * Add GIT_SIGNING_KEY to resolve commit signing keys.
     * Teach git var to accept multiple variable arguments (git var <var1>
       <var2> ...).
     * Add -z option to terminate outputs with NUL bytes (including git var
       -l -z).
     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v7
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v7
Pull-Request: https://github.com/git/git/pull/2388
Range-diff vs v6:
 1:  3b51f61a11 ! 1:  87f5f459ed var: support broken-down idents, signing key, multiple args, and -z
     @@ Documentation/git-var.adoc: OPTIONS
       ---------
       `GIT_AUTHOR_IDENT`::
      -    The author of a piece of code.
     -+    The author name, email, and date that would be used if you were to
     -+    run `git commit` right now.
     -+
      +`GIT_AUTHOR_NAME`::
     -+    The author name that would be used if you were to run `git commit`
     -+    right now.
     -+
      +`GIT_AUTHOR_EMAIL`::
     -+    The author email that would be used if you were to run `git commit`
     -+    right now.
     -+
      +`GIT_AUTHOR_DATE`::
     -+    The author date and timezone that would be used if you were to run
     -+    `git commit` right now.
     ++    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.
     -+    The committer name, email, and date that would be used if you were
     -+    to run `git commit` right now.
     -+
      +`GIT_COMMITTER_NAME`::
     -+    The name of the committer that would be used if you were to run
     -+    `git commit` right now.
     -+
      +`GIT_COMMITTER_EMAIL`::
     -+    The email of the committer that would be used if you were to run
     -+    `git commit` right now.
     -+
      +`GIT_COMMITTER_DATE`::
     -+    The committer date and timezone that would be used if you were to run
     -+    `git commit` right now.
     ++    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_SIGNING_KEY`::
      +    The key that would be used to sign the resulting commit if you were
     @@ builtin/var.c
       #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
      +};
     - 
     --static const char var_usage[] = "git var (-l | <variable>)";
     ++
      +enum ident_part {
      +	IDENT_NAME,
      +	IDENT_MAIL,
     @@ builtin/var.c
      +
      +	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)
     -+			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;
     ++		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);
      +	}
     @@ builtin/var.c: static char *git_config_val_system(int ident_flag UNUSED)
       }
       
      -static char *git_config_val_global(int ident_flag UNUSED)
     -+static int git_config_val_global(struct string_list *list)
     ++static void git_config_val_global(struct string_list *list)
       {
      -	struct strbuf buf = STRBUF_INIT;
       	char *user, *xdg;
     @@ builtin/var.c: static char *git_config_val_system(int ident_flag UNUSED)
      -		return NULL;
      -	}
      -	return strbuf_detach(&buf, &unused);
     -+	return !list->nr;
       }
       
       struct git_var {
       	const char *name;
       	char *(*read)(int);
      -	int multivalued;
     -+	int (*multiread)(struct string_list *);
     ++	void (*multiread)(struct string_list *);
       };
       static struct git_var git_vars[] = {
       	{
     @@ builtin/var.c: static struct git_var git_vars[] = {
      +				free(val);
       			}
      -			free(val);
     -+		} else if (ptr->multiread) {
     ++		} else {
      +			struct string_list list = STRING_LIST_INIT_DUP;
      +			size_t i;
      +
     -+			if (!ptr->multiread(&list)) {
     -+				for (i = 0; i < list.nr; i++)
     -+					printf("%s%c%s%c", ptr->name, delim,
     -+					       list.items[i].string, term);
     -+			}
     ++			ptr->multiread(&list);
     ++			for (i = 0; i < list.nr; i++)
     ++				printf("%s%c%s%c", ptr->name, delim,
     ++				       list.items[i].string, term);
      +			string_list_clear(&list, 0);
       		}
      +	}
     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)
      -	char *val;
      +	int list = 0;
      +	int nul_term = 0;
     -+	int ret = 0;
      +	int i;
      +	char term;
      +	struct option options[] = {
     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)
      -	if (!git_var)
      -		usage(var_usage);
      +	term = nul_term ? '\0' : '\n';
     ++
     ++	for (i = 0; i < argc; i++) {
     ++		const struct git_var *git_var = get_git_var(argv[i]);
       
      -	val = git_var->read(IDENT_STRICT);
      -	if (!val)
      -		return 1;
     -+	for (i = 0; i < argc; i++) {
     -+		const struct git_var *git_var = get_git_var(argv[i]);
     ++		if (!git_var)
     ++			usage_with_options(var_usage, options);
       
      -	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;
     -+				ret = 1;
     -+				printf("%c", term);
     ++				putc(term, stdout);
      +				continue;
      +			}
      +			printf("%s%c", val, term);
      +			free(val);
     -+		} else if (git_var->multiread) {
     ++		} else {
      +			struct string_list list = STRING_LIST_INIT_DUP;
      +			size_t j;
      +
     -+			if (git_var->multiread(&list) || !list.nr) {
     -+				if (argc == 1) {
     -+					string_list_clear(&list, 0);
     -+					return 1;
     -+				}
     -+				ret = 1;
     -+				printf("%c", term);
     -+			} else {
     -+				for (j = 0; j < list.nr; j++)
     -+					printf("%s%c", list.items[j].string, term);
     -+				if (argc > 1)
     -+					printf("%c", term);
     ++			git_var->multiread(&list);
     ++			if (argc == 1 && !list.nr) {
     ++				string_list_clear(&list, 0);
     ++				return 1;
      +			}
     ++			for (j = 0; j < list.nr; j++)
     ++				printf("%s%c", list.items[j].string, term);
     ++			if (argc > 1)
     ++				putc(term, stdout);
      +			string_list_clear(&list, 0);
      +		}
      +	}
       
     --	return 0;
     -+	return ret;
     + 	return 0;
       }
      
       ## t/t0007-git-var.sh ##
     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '
      +
      +test_expect_success 'get multiple variables with -z' '
      +	test_tick &&
     -+	printf "%s\0" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
     -+	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&
     ++	printf "%sQ%sQ" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
     ++	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >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 &&
     -+	printf "%s\0" "$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expected &&
     -+	test_cmp expected actual
     ++	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' '
     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '
      +
      +	$GIT_COMMITTER_NAME
      +	EOF
     -+	test_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
     ++	git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
      +	test_cmp expect actual
      +'
      +
      +test_expect_success 'get multiple variables with -z and unset variable' '
      +	test_config user.signingkey "" &&
     -+	printf "%s\0\0%s\0" "$GIT_AUTHOR_NAME" "$GIT_COMMITTER_NAME" >expect &&
     -+	test_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
     ++	printf "%sQ%sQ" "$GIT_AUTHOR_NAME" "Q$GIT_COMMITTER_NAME" >expect &&
     ++	git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&
     ++	nul_to_q <actual.raw >actual &&
      +	test_cmp expect actual
      +'
      +
      +test_expect_success 'get multiple variables including multi-valued variable with -z' '
      +	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
     -+	printf "%s\0%s\0%s\0\0%s\0" "$GIT_AUTHOR_NAME" \
     ++	printf "%sQ%sQ%sQQ%sQ" "$GIT_AUTHOR_NAME" \
      +		"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" \
      +		"$GIT_AUTHOR_EMAIL" >expect &&
      +	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
     -+		git var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&
     ++		git var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual.raw &&
     ++	nul_to_q <actual.raw >actual &&
      +	test_cmp expect actual
      +'
      +
 Documentation/git-var.adoc |  66 ++++++++--
 builtin/var.c              | 252 +++++++++++++++++++++++++++++--------
 t/t0007-git-var.sh         | 127 +++++++++++++++++++
 3 files changed, 384 insertions(+), 61 deletions(-)
Show changes to 3 files +384 −61

Documentation/git-var.adoc, builtin/var.c, t/t0007-git-var.sh

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index 697c10aded..ccbb9f8e8d 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -9,12 +9,16 @@ 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. Exits with code 1 if any requested
+variable has no value. When multiple variables are requested, an empty
+record (a blank line, or an empty NUL-terminated record when `-z` is given)
+is printed for any variable that has no value, and the command continues
+processing the remaining variables.
 
 OPTIONS
 -------
@@ -24,19 +28,55 @@ 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`, 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
+Eric W. Biederman
+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_SIGNING_KEY`::
+    The key that would be used to sign the resulting commit if you were
+    to run `git commit` right now.
 
 `GIT_EDITOR`::
     Text editor for use by Git commands.  The value is meant to be
@@ -85,9 +125,13 @@ endif::git-default-pager[]
     The path to the global (per-user) configuration files, if any.
 
 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, an extra newline (or an extra NUL byte if `-z` is
+given) is printed after the values of a multi-valued variable to mark the
+end of its list. (Single-variable queries and `git var -l` do not print
+an extra delimiter). 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.
diff --git a/builtin/var.c b/builtin/var.c
index cc3a43cde2..e8472ac722 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -12,25 +12,109 @@
 #include "config.h"
 #include "editor.h"
 #include "environment.h"
+#include "gpg-interface.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 *committer(int ident_flag)
 {
 	return xstrdup_or_null(git_committer_info(ident_flag));
 }
 
+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_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 *git_signing_key(int ident_flag UNUSED)
+{
+	char *signing_key = get_signing_key();
+
+	if (signing_key && !*signing_key) {
+		free(signing_key);
+		return NULL;
+	}
+	return signing_key;
+}
+
 static char *editor(int ident_flag UNUSED)
 {
 	return xstrdup_or_null(git_editor());
@@ -90,45 +174,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,
@@ -145,6 +245,10 @@ static struct git_var git_vars[] = {
 		.name = "GIT_DEFAULT_BRANCH",
 		.read = default_branch,
 	},
+	{
+		.name = "GIT_SIGNING_KEY",
+		.read = git_signing_key,
+	},
 	{
 		.name = "GIT_SHELL_PATH",
 		.read = shell_path,
@@ -163,8 +267,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 +275,37 @@ 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;
+			size_t i;
+
+			ptr->multiread(&list);
+			for (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 +316,85 @@ 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;
+	int i;
+	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);
+	term = nul_term ? '\0' : '\n';
+
+	for (i = 0; i < argc; i++) {
+		const struct git_var *git_var = get_git_var(argv[i]);
 
-	val = git_var->read(IDENT_STRICT);
-	if (!val)
-		return 1;
+		if (!git_var)
+			usage_with_options(var_usage, options);
 
-	printf("%s\n", val);
-	free(val);
+		if (git_var->read) {
+			char *val = git_var->read(IDENT_STRICT);
+
+			if (!val) {
+				if (argc == 1)
+					return 1;
+				putc(term, stdout);
+				continue;
+			}
+			printf("%s%c", val, term);
+			free(val);
+		} else {
+			struct string_list list = STRING_LIST_INIT_DUP;
+			size_t j;
+
+			git_var->multiread(&list);
+			if (argc == 1 && !list.nr) {
+				string_list_clear(&list, 0);
+				return 1;
+			}
+			for (j = 0; j < list.nr; j++)
+				printf("%s%c", list.items[j].string, term);
+			if (argc > 1)
+				putc(term, stdout);
+			string_list_clear(&list, 0);
+		}
+	}
 
 	return 0;
 }
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 2b60317758..75f688a1b1 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -276,4 +276,131 @@ test_expect_success '`git var -l` works even without HOME' '
 	)
 '
 
+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 'get multiple variables' '
+	test_tick &&
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME
+	$GIT_AUTHOR_EMAIL
+	$GIT_COMMITTER_NAME
+	$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 'get multiple variables with -z' '
+	test_tick &&
+	printf "%sQ%sQ" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
+	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >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 &&
+	echo "$GIT_AUTHOR_NAME" >expect &&
+	sed -n "/^GIT_AUTHOR_NAME$/{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 'get GIT_SIGNING_KEY with user.signingkey configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	echo "TEST_KEY_ID" >expect &&
+	git var GIT_SIGNING_KEY >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '
+	test_config user.signingkey "" &&
+	test_must_fail git var GIT_SIGNING_KEY
+'
+
+test_expect_success 'git var -l lists new variables' '
+	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_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	git var -l >actual &&
+	test_grep "^GIT_SIGNING_KEY=TEST_KEY_ID" actual
+'
+
+test_expect_success 'options must precede variable arguments' '
+	test_must_fail git var GIT_AUTHOR_NAME -z
+'
+
+test_expect_success 'get multiple variables with unset variable outputs blank record' '
+	test_config user.signingkey "" &&
+	cat >expect <<-EOF &&
+	$GIT_AUTHOR_NAME
+
+	$GIT_COMMITTER_NAME
+	EOF
+	git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables with -z and unset variable' '
+	test_config user.signingkey "" &&
+	printf "%sQ%sQ" "$GIT_AUTHOR_NAME" "Q$GIT_COMMITTER_NAME" >expect &&
+	git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&
+	nul_to_q <actual.raw >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables including multi-valued variable with -z' '
+	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
+	printf "%sQ%sQ%sQQ%sQ" "$GIT_AUTHOR_NAME" \
+		"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" \
+		"$GIT_AUTHOR_EMAIL" >expect &&
+	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
+		git var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual.raw &&
+	nul_to_q <actual.raw >actual &&
+	test_cmp expect actual
+'
+
 test_done

base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
-- 
gitgitgadget
Junio C HamanoSep 14, 2026, 19:34 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH v7] var: support broken-down idents, signing key, multiple args, and -z

"Andrew Pleeter via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 7 quoted lines
>  DESCRIPTION
>  -----------
> +Prints Git logical variables. Exits with code 1 if any requested
> +variable has no value. When multiple variables are requested, an empty
> +record (a blank line, or an empty NUL-terminated record when `-z` is given)
> +is printed for any variable that has no value, and the command continues
> +processing the remaining variables.

Very clearly described. Although it makes it sound as if the command always notices a variable without any value and reports failure with its exit value, no matter in what mode, but I do not think that matches what the code does (below).

Show 40 quoted lines
>  int cmd_var(int argc,
> ...
> +	term = nul_term ? '\0' : '\n';
> +
> +	for (i = 0; i < argc; i++) {
> +		const struct git_var *git_var = get_git_var(argv[i]);
>  
> +		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;
> +				putc(term, stdout);
> +				continue;
> +			}
> +			printf("%s%c", val, term);
> +			free(val);
> +		} else {
> +			struct string_list list = STRING_LIST_INIT_DUP;
> +			size_t j;
> +
> +			git_var->multiread(&list);
> +			if (argc == 1 && !list.nr) {
> +				string_list_clear(&list, 0);
> +				return 1;
> +			}
> +			for (j = 0; j < list.nr; j++)
> +				printf("%s%c", list.items[j].string, term);
> +			if (argc > 1)
> +				putc(term, stdout);
> +			string_list_clear(&list, 0);
> +		}
> +	}
>  
>  	return 0;
>  }

When we ask for a single variable, 'argc' is 1 (and we never update 'argc' in the loop, which is good), and we return 1 upon seeing a missing value. We also do the same when we receive a 0-element list back for a multi-valued variable. Otherwise, nobody in the loop remembers that we had any such failure; the loop continues, and we return 0 unconditionally. A "missing value" anomaly noticed during the loop gets forgotten.

Either the documentation or the code needs to be updated, I think.

I am still not convinced this output format is easy for scripts to handle when multi-valued variables are involved. It is also a bit unclear what exactly "variable has no value" means. A variable whose value is an empty string is not such a variable, right? If a multi-valued variable has an empty string and the string "hello" as its value, would the output from the command confuse the reading script into thinking that the first blank line signals that the variable has no value, for example? Having to know which variables are multi-valued and which are not before parsing the output format does not help, either.

We could, of course, disambiguate by prefixing these lines with variable names followed by '=' (or NUL), which would likely eliminate the ambiguity. But I understand that you are trying to allow the parsers to proceed without having to strip prefixes from each input, which is why the format tries to rely solely on the correspondence between command-line arguments and output lines. I, however, doubt you succeeded in doing so without making the output ambiguous.

Thanks.
Andrew Pleeter via GitGitGadgetSep 14, 2026, 22:50 UTC in reply to Andrew Pleeter via GitGitGadget on lore

[PATCH v8] var: support broken-down idents, signing key, multiple args, and -z

From: Andrew Pleeter <andrewpleeter@gmail.com>

While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT, extracting individual components (name, email, or date) currently requires callers to manually parse the composite string. Furthermore, there is no way to query the resolved commit signing key through 'git var', and the command only accepts a single variable at a time.

Teach 'git var' to expose individual identity components and commit signing configuration, and allow querying multiple variables with optional NUL-termination:

- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
- Add GIT_SIGNING_KEY to resolve the key that would be used to sign
  the resulting commit if you were to run 'git commit' right now.
- Allow passing multiple variable arguments (e.g., 'git var
  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL').
- When a single variable is requested, print its bare value for backward
  compatibility.
- When multiple variables are requested, model the output after
  'git var -l' by printing 'VARIABLE=value' pairs (or 'VARIABLE\nvalue\0'
  when '-z' is given).
- Format multi-valued variables in multi-variable mode as repeated
  'VARIABLE=value' entries (or 'VARIABLE\nvalue\0' with '-z'),
  eliminating stream ambiguity without extra trailing delimiters.
- When querying multiple variables, omit any variable that has no value,
  continue processing remaining variables, and exit with code 1.
- Support '-z' to terminate variable outputs with NUL bytes.
- Format 'git var -l -z' using the same convention as 'git config
  list -z' (newline separating key and value, NUL separating entries).
- Use parse_options() to strictly require options before arguments.
- Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
    var: support broken-down idents, signing key, multiple args, and -z
    
    Teach git var to expose individual identity components and commit
    signing configuration, and allow querying multiple variables with
    optional NUL-termination.
    
    
    Changes since v7:
    =================
    
     * Modelled multi-variable querying after git var -l, printing
       VARIABLE=value pairs (or VARIABLE\nvalue\0 when -z is given) instead
       of bare values, per feedback from Junio C. Hamano.
     * Formatted multi-valued variables in multi-variable mode as repeated
       VARIABLE=value entries (or VARIABLE\nvalue\0 with -z), eliminating
       stream ambiguity and the need for extra trailing delimiters.
     * When querying multiple variables, unset variables are omitted from
       output and the command terminates with exit code 1.
     * Updated Documentation/git-var.adoc and t/t0007-git-var.sh
       accordingly.
    
    
    Changes since v6:
    =================
    
     * Grouped GIT_AUTHOR_* and GIT_COMMITTER_* entries together into
       concise definitions in Documentation/git-var.adoc to avoid repetitive
       descriptions, per feedback from Junio C. Hamano.
     * Added explicit BUG() checks in ident_part() for NULL name/email
       pointers and date/timezone to protect against unforeseen changes in
       split_ident_line().
     * Changed struct git_var member multiread to return void (void
       (*multiread)(struct string_list *)) since list->nr communicates
       length.
     * Simplified list_vars() to use an unconditional else block for
       ptr->multiread instead of redundant else if.
     * In cmd_var(), do not set a non-zero exit status for missing variables
       in multi-variable queries; callers can see the empty record (e.g.
       GIT_CONFIG_SYSTEM under GIT_CONFIG_NOSYSTEM), matching Phillip Wood's
       recommendation.
     * Used putc(term, stdout) in cmd_var() when emitting delimiters.
     * In t/t0007-git-var.sh, converted -z tests to pipe output through
       nul_to_q so test_cmp produces clean diffs rather than binary
       comparison errors.
    
    
    Changes since v5:
    =================
    
     * Reverted cosmetic refactoring in get_git_var() to keep the diff
       minimal and focused on adding ptr->multiread.
     * Added BUG("unknown ident_part %d", part) to default case in
       ident_part() and removed redundant NULL checks on split.name_begin
       and split.mail_begin.
     * Adopted first-class int (*multiread)(struct string_list *) callback
       in struct git_var for multi-valued variables like GIT_CONFIG_GLOBAL,
       cleanly populating a struct string_list instead of relying on
       embedded NUL buffers.
     * Improved GIT_AUTHOR_* and GIT_COMMITTER_* documentation in
       Documentation/git-var.adoc to describe the values that would be used
       if you were to run git commit right now.
     * Explicitly documented that single-variable queries and git var -l do
       not print an extra delimiter after multi-valued variables.
    
    
    Changes since v4:
    =================
    
     * Simplified git_signing_key() to directly call get_signing_key() as
       used throughout Git (in tag, send-pack, and sign_buffer()).
     * Renamed null_term to nul_term across builtin/var.c, and simplified
       show_config() callback handling.
     * Replaced the redundant pre-validation loop in cmd_var() by validating
       arguments directly in the main execution loop.
     * When querying multiple variables, print an empty record (blank line
       or \0 with -z) for any variable that has no value, and continue
       printing remaining variables instead of terminating prematurely.
     * Switched multi-valued variable storage (git_config_val_global()) to
       internal \0 delimiters, iterating directly through string sequences
       without allocating a temporary string_list.
     * For multi-variable queries, output an extra delimiter (\n or \0)
       after multi-valued variables to clearly mark the end of their list.
     * Explicitly documented the git var -l -z format and multi-variable
       handling in Documentation/git-var.adoc.
     * Added comprehensive tests for unset variables and multi-valued stream
       delimiters in t/t0007-git-var.sh.
    
    
    Changes since v3:
    =================
    
     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip
       Wood and Junio C Hamano; dropped the alias mechanism and
       commit.gpgsign check.
     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in
       builtin/var.c, strictly enforcing that options precede variable
       arguments.
     * Adopted git config list -z format (key\nvalue\0) for git var -l -z to
       prevent ambiguity with = in config keys.
     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with
       NUL bytes under -z.
     * Replaced char part in ident_part() with enum ident_part.
     * Split synopsis in Documentation/git-var.adoc into separate lines for
       -l and <variable>..., and removed awkward legacy phrasing ("of a
       piece of code").
     * Added tests in t/t0007-git-var.sh covering the new -z format,
       multi-valued -z, and argument ordering.
    
    
    Changes since v2:
    =================
    
     * Drop git ident / git whoami subcommand entirely.
     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
     * Add GIT_SIGNING_KEY to resolve commit signing keys.
     * Teach git var to accept multiple variable arguments (git var <var1>
       <var2> ...).
     * Add -z option to terminate outputs with NUL bytes (including git var
       -l -z).
     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v8
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v8
Pull-Request: https://github.com/git/git/pull/2388
Range-diff vs v7:
 1:  87f5f459ed ! 1:  b9c14a51f5 var: support broken-down idents, signing key, multiple args, and -z
     @@ Commit message
          - Add GIT_SIGNING_KEY to resolve the key that would be used to sign
            the resulting commit if you were to run 'git commit' right now.
          - Allow passing multiple variable arguments (e.g., 'git var
     -      GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable
     -      sequentially.
     +      GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL').
     +    - When a single variable is requested, print its bare value for backward
     +      compatibility.
     +    - When multiple variables are requested, model the output after
     +      'git var -l' by printing 'VARIABLE=value' pairs (or 'VARIABLE\nvalue\0'
     +      when '-z' is given).
     +    - Format multi-valued variables in multi-variable mode as repeated
     +      'VARIABLE=value' entries (or 'VARIABLE\nvalue\0' with '-z'),
     +      eliminating stream ambiguity without extra trailing delimiters.
     +    - When querying multiple variables, omit any variable that has no value,
     +      continue processing remaining variables, and exit with code 1.
          - Support '-z' to terminate variable outputs with NUL bytes.
          - Format 'git var -l -z' using the same convention as 'git config
            list -z' (newline separating key and value, NUL separating entries).
     -    - Delimit values of multi-valued variables with NUL when '-z' is given,
     -      and output an extra delimiter after multi-valued variables when
     -      querying multiple variables to disambiguate the stream.
     -    - When querying multiple variables, print an empty record for any
     -      variable that has no value and continue processing remaining variables.
          - Use parse_options() to strictly require options before arguments.
          - Update Documentation/git-var.adoc and t/t0007-git-var.sh.
      
     @@ Documentation/git-var.adoc: git-var - Show a Git logical variable
       -----------
      -Prints a Git logical variable. Exits with code 1 if the variable has
      -no value.
     -+Prints Git logical variables. Exits with code 1 if any requested
     -+variable has no value. When multiple variables are requested, an empty
     -+record (a blank line, or an empty NUL-terminated record when `-z` is given)
     -+is printed for any variable that has no value, and the command continues
     -+processing the remaining variables.
     ++Prints Git logical variables. When a single variable is requested, its
     ++bare value is printed. When multiple variables are requested, they are
     ++printed as `VARIABLE=value` pairs, separated by newlines.
     ++
     ++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`).
     ++
     ++If any requested variable has no value, nothing is printed for that
     ++variable, processing continues for the remaining variables, and the
     ++command exits with code 1.
       
       OPTIONS
       -------
     @@ Documentation/git-var.adoc: OPTIONS
       
      +`-z`::
      +	Terminate entries with NUL instead of newline. When used with
     -+	`-l`, the variable name and its value are separated by a newline,
     -+	and each entry is terminated with a NUL byte.
     ++	`-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
       --------
     @@ Documentation/git-var.adoc: OPTIONS
      ++
      +------------
      +$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
     -+Eric W. Biederman
     -+ebiederm@lnxi.com
     ++GIT_AUTHOR_NAME=Eric W. Biederman
     ++GIT_AUTHOR_EMAIL=ebiederm@lnxi.com
      +------------
       
       VARIABLES
     @@ Documentation/git-var.adoc: endif::git-default-pager[]
      -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, an extra newline (or an extra NUL byte if `-z` is
     -+given) is printed after the values of a multi-valued variable to mark the
     -+end of its list. (Single-variable queries and `git var -l` do not print
     -+an extra delimiter). Callers should be prepared for any such path value to
     -+contain multiple items.
     ++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.
     @@ builtin/var.c: static struct git_var git_vars[] = {
       {
       	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);
     ++	char delim = nul_term ? '\n' : '=';
     ++	char term = nul_term ? '\0' : '\n';
     ++
     ++	for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
     ++		if (ptr->read) {
     ++			char *val = ptr->read(0);
     ++
      +			if (val) {
      +				printf("%s%c%s%c", ptr->name, delim, val, term);
      +				free(val);
     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)
      -	char *val;
      +	int list = 0;
      +	int nul_term = 0;
     ++	int ret = 0;
      +	int i;
     ++	char delim;
      +	char term;
      +	struct option options[] = {
      +		OPT_BOOL('l', NULL, &list,
     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)
      -	git_var = get_git_var(argv[1]);
      -	if (!git_var)
      -		usage(var_usage);
     ++	delim = nul_term ? '\n' : '=';
      +	term = nul_term ? '\0' : '\n';
     -+
     -+	for (i = 0; i < argc; i++) {
     -+		const struct git_var *git_var = get_git_var(argv[i]);
       
      -	val = git_var->read(IDENT_STRICT);
      -	if (!val)
      -		return 1;
     -+		if (!git_var)
     -+			usage_with_options(var_usage, options);
     ++	for (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;
     -+				putc(term, stdout);
     ++				ret = 1;
      +				continue;
      +			}
     -+			printf("%s%c", val, term);
     ++			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;
      +			size_t j;
      +
      +			git_var->multiread(&list);
     -+			if (argc == 1 && !list.nr) {
     -+				string_list_clear(&list, 0);
     -+				return 1;
     ++			if (!list.nr)
     ++				ret = 1;
     ++			for (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);
      +			}
     -+			for (j = 0; j < list.nr; j++)
     -+				printf("%s%c", list.items[j].string, term);
     -+			if (argc > 1)
     -+				putc(term, stdout);
      +			string_list_clear(&list, 0);
      +		}
      +	}
       
     - 	return 0;
     +-	return 0;
     ++	return ret;
       }
      
       ## t/t0007-git-var.sh ##
     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '
      +test_expect_success 'get multiple variables' '
      +	test_tick &&
      +	cat >expect <<-EOF &&
     -+	$GIT_AUTHOR_NAME
     -+	$GIT_AUTHOR_EMAIL
     -+	$GIT_COMMITTER_NAME
     -+	$GIT_COMMITTER_EMAIL
     ++	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
     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '
      +
      +test_expect_success 'get multiple variables with -z' '
      +	test_tick &&
     -+	printf "%sQ%sQ" "$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
     ++	printf "GIT_AUTHOR_NAME\n%sQGIT_AUTHOR_EMAIL\n%sQ" \
     ++		"$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
      +	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual.raw &&
      +	nul_to_q <actual.raw >actual &&
      +	test_cmp expect actual
     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '
      +	test_must_fail git var GIT_AUTHOR_NAME -z
      +'
      +
     -+test_expect_success 'get multiple variables with unset variable outputs blank record' '
     ++test_expect_success 'get multiple variables with unset variable exits with 1 and omits unset' '
      +	test_config user.signingkey "" &&
      +	cat >expect <<-EOF &&
     -+	$GIT_AUTHOR_NAME
     -+
     -+	$GIT_COMMITTER_NAME
     ++	GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME
     ++	GIT_COMMITTER_NAME=$GIT_COMMITTER_NAME
      +	EOF
     -+	git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
     ++	test_expect_code 1 git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
      +	test_cmp expect actual
      +'
      +
      +test_expect_success 'get multiple variables with -z and unset variable' '
      +	test_config user.signingkey "" &&
     -+	printf "%sQ%sQ" "$GIT_AUTHOR_NAME" "Q$GIT_COMMITTER_NAME" >expect &&
     -+	git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&
     ++	printf "GIT_AUTHOR_NAME\n%sQGIT_COMMITTER_NAME\n%sQ" \
     ++		"$GIT_AUTHOR_NAME" "$GIT_COMMITTER_NAME" >expect &&
     ++	test_expect_code 1 git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&
      +	nul_to_q <actual.raw >actual &&
      +	test_cmp expect actual
      +'
      +
     ++test_expect_success 'get multiple variables including multi-valued variable' '
     ++	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
     ++	cat >expect <<-EOF &&
     ++	GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME
     ++	GIT_CONFIG_GLOBAL=$TRASHDIR/foo/git/config
     ++	GIT_CONFIG_GLOBAL=$TRASHDIR/.gitconfig
     ++	GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL
     ++	EOF
     ++	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
     ++		git var GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&
     ++	test_cmp expect actual
     ++'
     ++
      +test_expect_success 'get multiple variables including multi-valued variable with -z' '
      +	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
     -+	printf "%sQ%sQ%sQQ%sQ" "$GIT_AUTHOR_NAME" \
     ++	printf "GIT_AUTHOR_NAME\n%sQGIT_CONFIG_GLOBAL\n%sQGIT_CONFIG_GLOBAL\n%sQGIT_AUTHOR_EMAIL\n%sQ" \
     ++		"$GIT_AUTHOR_NAME" \
      +		"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" \
      +		"$GIT_AUTHOR_EMAIL" >expect &&
      +	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '
      +	nul_to_q <actual.raw >actual &&
      +	test_cmp expect actual
      +'
     ++
     ++test_expect_success 'get multiple variables with unset multi-valued variable' '
     ++	cat >expect <<-EOF &&
     ++	GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME
     ++	GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL
     ++	EOF
     ++	test_env GIT_CONFIG_GLOBAL= test_expect_code 1 git var GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&
     ++	test_cmp expect actual
     ++'
      +
       test_done
 Documentation/git-var.adoc |  74 +++++++++--
 builtin/var.c              | 263 +++++++++++++++++++++++++++++--------
 t/t0007-git-var.sh         | 151 +++++++++++++++++++++
 3 files changed, 424 insertions(+), 64 deletions(-)
Show changes to 3 files +424 −64

Documentation/git-var.adoc, builtin/var.c, t/t0007-git-var.sh

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index 697c10aded..162d6d1722 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -9,12 +9,25 @@ 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. When multiple variables are requested, they are
+printed as `VARIABLE=value` pairs, separated by newlines.
+
+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`).
+
+If any requested variable has no value, nothing is printed for that
+variable, processing continues for the remaining variables, and the
+command exits with code 1.
 
 OPTIONS
 -------
@@ -24,19 +37,56 @@ 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_SIGNING_KEY`::
+    The key that would be used to sign the resulting commit if you were
+    to run `git commit` right now.
 
 `GIT_EDITOR`::
     Text editor for use by Git commands.  The value is meant to be
@@ -85,9 +135,11 @@ endif::git-default-pager[]
     The path to the global (per-user) configuration files, if any.
 
 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.
diff --git a/builtin/var.c b/builtin/var.c
index cc3a43cde2..3b651a3eb6 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -12,25 +12,109 @@
 #include "config.h"
 #include "editor.h"
 #include "environment.h"
+#include "gpg-interface.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 *committer(int ident_flag)
 {
 	return xstrdup_or_null(git_committer_info(ident_flag));
 }
 
+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_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 *git_signing_key(int ident_flag UNUSED)
+{
+	char *signing_key = get_signing_key();
+
+	if (signing_key && !*signing_key) {
+		free(signing_key);
+		return NULL;
+	}
+	return signing_key;
+}
+
 static char *editor(int ident_flag UNUSED)
 {
 	return xstrdup_or_null(git_editor());
@@ -90,45 +174,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,
@@ -145,6 +245,10 @@ static struct git_var git_vars[] = {
 		.name = "GIT_DEFAULT_BRANCH",
 		.read = default_branch,
 	},
+	{
+		.name = "GIT_SIGNING_KEY",
+		.read = git_signing_key,
+	},
 	{
 		.name = "GIT_SHELL_PATH",
 		.read = shell_path,
@@ -163,8 +267,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 +275,37 @@ static struct git_var git_vars[] = {
 	},
 };
 
-static void list_vars(void)
+static void list_vars(int nul_term)
 {
 	struct git_var *ptr;
-	char *val;
-
-	for (ptr = git_vars; ptr->read; ptr++)
-		if ((val = ptr->read(0))) {
-			if (ptr->multivalued && *val) {
-				struct string_list list = STRING_LIST_INIT_DUP;
-
-				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);
+	char delim = nul_term ? '\n' : '=';
+	char term = nul_term ? '\0' : '\n';
+
+	for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
+		if (ptr->read) {
+			char *val = ptr->read(0);
+
+			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;
+			size_t i;
+
+			ptr->multiread(&list);
+			for (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 +316,90 @@ 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;
+	int ret = 0;
+	int i;
+	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 (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) {
+				ret = 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;
+			size_t j;
+
+			git_var->multiread(&list);
+			if (!list.nr)
+				ret = 1;
+			for (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;
+	return ret;
 }
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 2b60317758..da6cbaade9 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -276,4 +276,155 @@ test_expect_success '`git var -l` works even without HOME' '
 	)
 '
 
+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 'get multiple variables' '
+	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 'get multiple variables with -z' '
+	test_tick &&
+	printf "GIT_AUTHOR_NAME\n%sQGIT_AUTHOR_EMAIL\n%sQ" \
+		"$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" >expect &&
+	git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >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 &&
+	echo "$GIT_AUTHOR_NAME" >expect &&
+	sed -n "/^GIT_AUTHOR_NAME$/{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 'get GIT_SIGNING_KEY with user.signingkey configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	echo "TEST_KEY_ID" >expect &&
+	git var GIT_SIGNING_KEY >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '
+	test_config user.signingkey "" &&
+	test_must_fail git var GIT_SIGNING_KEY
+'
+
+test_expect_success 'git var -l lists new variables' '
+	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_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '
+	test_config user.signingkey "TEST_KEY_ID" &&
+	git var -l >actual &&
+	test_grep "^GIT_SIGNING_KEY=TEST_KEY_ID" actual
+'
+
+test_expect_success 'options must precede variable arguments' '
+	test_must_fail git var GIT_AUTHOR_NAME -z
+'
+
+test_expect_success 'get multiple variables with unset variable exits with 1 and omits unset' '
+	test_config user.signingkey "" &&
+	cat >expect <<-EOF &&
+	GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME
+	GIT_COMMITTER_NAME=$GIT_COMMITTER_NAME
+	EOF
+	test_expect_code 1 git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables with -z and unset variable' '
+	test_config user.signingkey "" &&
+	printf "GIT_AUTHOR_NAME\n%sQGIT_COMMITTER_NAME\n%sQ" \
+		"$GIT_AUTHOR_NAME" "$GIT_COMMITTER_NAME" >expect &&
+	test_expect_code 1 git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&
+	nul_to_q <actual.raw >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables including multi-valued variable' '
+	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
+	cat >expect <<-EOF &&
+	GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME
+	GIT_CONFIG_GLOBAL=$TRASHDIR/foo/git/config
+	GIT_CONFIG_GLOBAL=$TRASHDIR/.gitconfig
+	GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL
+	EOF
+	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
+		git var GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables including multi-valued variable with -z' '
+	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
+	printf "GIT_AUTHOR_NAME\n%sQGIT_CONFIG_GLOBAL\n%sQGIT_CONFIG_GLOBAL\n%sQGIT_AUTHOR_EMAIL\n%sQ" \
+		"$GIT_AUTHOR_NAME" \
+		"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" \
+		"$GIT_AUTHOR_EMAIL" >expect &&
+	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
+		git var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual.raw &&
+	nul_to_q <actual.raw >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables with unset multi-valued variable' '
+	cat >expect <<-EOF &&
+	GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME
+	GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL
+	EOF
+	test_env GIT_CONFIG_GLOBAL= test_expect_code 1 git var GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&
+	test_cmp expect actual
+'
+
 test_done

base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e
-- 
gitgitgadget
Junio C HamanoSep 15, 2026, 17:21 UTC in reply to Andrew Pleeter via GitGitGadget on lore

Re: [PATCH v8] var: support broken-down idents, signing key, multiple args, and -z

"Andrew Pleeter via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 11 quoted lines
> From: Andrew Pleeter <andrewpleeter@gmail.com>
>
> While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,
> extracting individual components (name, email, or date) currently
> requires callers to manually parse the composite string. Furthermore,
> there is no way to query the resolved commit signing key through
> 'git var', and the command only accepts a single variable at a time.
>
> Teach 'git var' to expose individual identity components and commit
> signing configuration, and allow querying multiple variables with
> optional NUL-termination:

The huge laundary list (below) strongly tells us that this single patch is doing too many things at once and it is better done as a multi-patch series. Also, some descriptions seem to hint how the implementation evolved during the development of this patch, which nobody is interested when they read "git log" output (which is the ultimate target audience we write our commit log messages for).

I would say this should be split into at least 3 patches.
 (1) Add "-z" output mode.
     To allow reading scripts to unambiguously parse output from
     "git var (-l | <var>)" command, implement a NUL terminated
     output mode, similar to how "git config -l -z" shows list of
     configuration variables and their values.  When showing the
     value of a single variable this only makes difference for
     variable with multiple values, but in the next step in this
     series, we will introduce a mode where multiple variables are
     queried.
 (2) Add (2 <= argc) mode that displays like "var -l" mode on top.
     To allow reading values for multiple variables with a single
     command invocation, teach "var" to take more than one variable,
     and show output the same way as "git var -l [-z]", giving list
     of "var=value" but only for variables requested by the user.
 (3) Add new variables.
     Scripts reading from "git var GIT_AUTHOR_IDENT" needs to parse
     the output if they want to extract only the author name.  
     To allow scripts to easily access broken-out fields of
     GIT_{AUTHOR,COMMITTER}_IDENT, add a GIT_AUTHOR_NAME variable
     and its friends, as well as GIT_SIGNING_KEY.

After you receive a review, you should respond and try to engage in a dialog with reviewers, before sending a new iteration of a patch.

When your new iteration is different from what reviewer suggested, without such an exchange beforehand, reviewers cannot tell if that is merely due to miscommunication, or you had a good reason to do things differently. Don't make reviewers feel as if they are talking to silent machine that takes an earlier iteration of the patch with their input and spits out a new iteration.

Thanks.
Show 21 quoted lines
> - Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
> - Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
> - Add GIT_SIGNING_KEY to resolve the key that would be used to sign
>   the resulting commit if you were to run 'git commit' right now.
> - Allow passing multiple variable arguments (e.g., 'git var
>   GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL').
> - When a single variable is requested, print its bare value for backward
>   compatibility.
> - When multiple variables are requested, model the output after
>   'git var -l' by printing 'VARIABLE=value' pairs (or 'VARIABLE\nvalue\0'
>   when '-z' is given).
> - Format multi-valued variables in multi-variable mode as repeated
>   'VARIABLE=value' entries (or 'VARIABLE\nvalue\0' with '-z'),
>   eliminating stream ambiguity without extra trailing delimiters.
> - When querying multiple variables, omit any variable that has no value,
>   continue processing remaining variables, and exit with code 1.
> - Support '-z' to terminate variable outputs with NUL bytes.
> - Format 'git var -l -z' using the same convention as 'git config
>   list -z' (newline separating key and value, NUL separating entries).
> - Use parse_options() to strictly require options before arguments.
> - Update Documentation/git-var.adoc and t/t0007-git-var.sh.
Andrew PleeterSep 15, 2026, 22:02 UTC in reply to Junio C Hamano on lore

Re: [PATCH v8] var: support broken-down idents, signing key, multiple args, and -z

Hi Junio,

Thank you for the guidance, and I sincerely apologize for the lack of communication. I had posted replies on the GitHub pull request and did not realize until now that GitGitGadget does not mirror PR comments back to the mailing list. I certainly did not intend to be silent or ignore your reviews.

I completely agree with your feedback. Packing all of these features into a single commit makes the patch difficult to review and overcomplicates the history.

I will restructure the series for v9 into three separate patches as you suggested:

1. Add the -z output mode to git var (-l and single-variable).
2. Teach git var to accept multiple variables (argc >= 2) with var=value
   output.
3. Expose the broken-out identity components and GIT_SIGNING_KEY.
I will send v9 shortly. Thank you again for your patience and direction.

Best regards, Andrew

Phillip WoodSep 22, 2026, 14:57 UTC in reply to Andrew Pleeter on lore

Re: [PATCH v8] var: support broken-down idents, signing key, multiple args, and -z

On 15/09/2026 23:02, Andrew Pleeter wrote:
Show 6 quoted lines
> Hi Junio,
> 
> Thank you for the guidance, and I sincerely apologize for the lack of
> communication. I had posted replies on the GitHub pull request and did
> not realize until now that GitGitGadget does not mirror PR comments back
> to the mailing list.

Ah, that explains the silence! I had a quick look at the PR comments but did not notice any response to my question about the use of GIT_SIGNING_KEY.

Show 14 quoted lines
> I certainly did not intend to be silent or ignore
> your reviews.
> 
> I completely agree with your feedback. Packing all of these features into
> a single commit makes the patch difficult to review and overcomplicates
> the history.
> 
> I will restructure the series for v9 into three separate patches as you
> suggested:
> 
> 1. Add the -z output mode to git var (-l and single-variable).
> 2. Teach git var to accept multiple variables (argc >= 2) with var=value
>     output.
> 3. Expose the broken-out identity components and GIT_SIGNING_KEY.
That sounds like a good direction
Thanks
Phillip
Show 5 quoted lines
> 
> I will send v9 shortly. Thank you again for your patience and direction.
> 
> Best regards,
> Andrew
Andrew PleeterSep 26, 2026, 16:19 UTC in reply to Phillip Wood on lore

Re: [PATCH v8] var: support broken-down idents, signing key, multiple args, and -z

Hi Phillip,

Sorry for the silence. I was replying on the GitGitGadget PR and did not realise those comments never reach the list. That is also why v6 switched to get_signing_key() without a word from me about your question.

You were right to keep asking about GIT_SIGNING_KEY. I am dropping it from the series. get_signing_key() returns a string whose meaning depends on gpg.format, and the variable gives the caller no way to tell which kind it got:

  gpg.format=openpgp, user.signingkey unset
    $ git var GIT_SIGNING_KEY
    C O Mitter <committer@example.com>
  gpg.format=openpgp, user.signingkey=DEADBEEF
    $ git var GIT_SIGNING_KEY
    DEADBEEF
  gpg.format=ssh, user.signingkey=/path/to/key.pub
    $ git var GIT_SIGNING_KEY
    /path/to/key.pub

So no, the key alone does not tell a caller whether to use gpg or ssh; they would have to read gpg.format as well. And in the default case the value is a committer ident rather than a key, because that is what get_signing_key() hands to gpg to look up. Better to drop it than ship a GIT_SIGNING_KEY that sometimes isn't one. I will revisit it when I can name a caller it serves.

On the exit code, your GIT_CONFIG_NOSYSTEM example convinced me. In v9, asking for several variables omits the ones with no value and exits 0. Non-zero is reserved for real errors such as an unknown variable name, so callers can detect those from the exit code alone. A single variable still exits 1 when it has no value.

The string_list conversion is split out as you suggested:
  1/4  var: represent multi-valued variables with a string_list
  2/4  var: add "-z" output mode
  3/4  var: accept more than one variable
  4/4  var: add broken-out identity variables

parse_options() sits in 2/4, since "-z" is what requires it. GIT_CONFIG_GLOBAL is now documented as multi-valued, and the tests use nul_to_q instead of test_cmp over files containing NUL.

One place I did not follow you: instead of a trailing delimiter for a multi-valued variable when argc > 1, each value gets its own entry:

  $ git var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL
  GIT_AUTHOR_IDENT=A U Thor <author@example.com> 1112912053 -0700
  GIT_CONFIG_GLOBAL=/home/me/.config/git/config
  GIT_CONFIG_GLOBAL=/home/me/.gitconfig

Same shape as "git var -l", so one parser handles both. Happy to switch if you prefer the trailing delimiter. I should have raised it last time instead of just changing it.

Thanks, Andrew

Andrew PleeterSep 26, 2026, 16:20 UTC in reply to Junio C Hamano on lore

[PATCH v9 0/4] var: -z output, multiple variables, and broken-out idents

This is a reroll of the single patch in v8, split up as Junio asked for, with the changes Phillip asked for on the list.

v8 was one commit doing four things at once. It is now four patches:
  1/4 converts the internal representation of a multi-valued variable
      from a newline-joined string to a string_list, with no change in
      output. Phillip suggested doing this as its own preparatory step.
  2/4 adds "-z", which is what forces the switch to parse_options(), so
      that conversion lives here rather than in 1/4.
  3/4 teaches "git var" to take more than one variable.
  4/4 adds GIT_AUTHOR_NAME and friends.
Changes since v8:
 * GIT_SIGNING_KEY is dropped. Phillip asked three times how it was
   meant to be used, and once I looked properly the answer was that the
   value cannot be interpreted without also reading gpg.format, and that
   in the default configuration it is a committer ident rather than a
   key at all. I would rather leave it out than define a variable I
   cannot describe. Details are in my reply to him on this thread.
 * Asking for several variables no longer exits non-zero just because
   one of them has no value; such a variable is left out of the output
   and the rest are still shown. A non-zero status is now reserved for
   real errors, such as naming a variable that does not exist, so
   callers can detect those from the exit code. A single variable still
   exits 1 when it has no value, as before. This is Phillip's
   GIT_CONFIG_NOSYSTEM point.
 * GIT_CONFIG_GLOBAL is documented as a variable that can have more than
   one value.
 * The commit messages are prose rather than a list of bullet points,
   and no longer narrate how the patch was developed.

The tests use nul_to_q rather than running test_cmp over files containing NUL bytes, which Phillip pointed out in v6. Each patch builds and passes t0007 on its own.

One thing I did not do, and would like an opinion on: for a multi-valued variable in multi-variable mode, rather than emitting a trailing delimiter, each value is shown as its own "VARIABLE=value" entry, which matches "git var -l". Phillip suggested the trailing delimiter and I am happy to switch.

Andrew Pleeter (4):
  var: represent multi-valued variables with a string_list
  var: add "-z" output mode
  var: accept more than one variable
  var: add broken-out identity variables
 Documentation/git-var.adoc |  68 +++++++++--
 builtin/var.c              | 242 +++++++++++++++++++++++++++++--------
 t/t0007-git-var.sh         | 153 +++++++++++++++++++++++
 3 files changed, 403 insertions(+), 60 deletions(-)
-- 
2.54.0 (Apple Git-157)
Andrew PleeterSep 26, 2026, 16:20 UTC in reply to Junio C Hamano on lore

[PATCH v9 1/4] var: represent multi-valued variables with a string_list

A variable whose value is a list, of which GIT_CONFIG_GLOBAL is currently the only one, is read into a single string whose elements are joined with newlines, and "git var -l" splits that string apart again before showing each element. Round-tripping through a delimiter that can legitimately appear in a value is fragile, and it forces every future caller to know which variables need splitting.

Give "struct git_var" a separate "multiread" function that appends the elements to a string_list, and let the single "read" function stay for the variables that have exactly one value. A variable uses one or the other, never both, so which one is set also says whether the variable is multi-valued.

The output is unchanged. While at it, say in the documentation that GIT_CONFIG_GLOBAL can have multiple values, so that callers know when to expect a list.

Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
 Documentation/git-var.adoc |  1 +
 builtin/var.c              | 79 +++++++++++++++++++++-----------------
 2 files changed, 44 insertions(+), 36 deletions(-)
Show changes to 2 files +44 −36

Documentation/git-var.adoc, builtin/var.c

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index 697c10aded..de3007732d 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -83,6 +83,7 @@ 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
diff --git a/builtin/var.c b/builtin/var.c
index cc3a43cde2..9f7c8a6113 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -14,10 +14,11 @@
 #include "environment.h"
 #include "ident.h"
 #include "pager.h"
-#include "refs.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>)";
 
@@ -90,35 +91,27 @@ 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[] = {
 	{
@@ -163,8 +156,7 @@ static struct git_var git_vars[] = {
 	},
 	{
 		.name = "GIT_CONFIG_GLOBAL",
-		.read = git_config_val_global,
-		.multivalued = 1,
+		.multiread = git_config_val_global,
 	},
 	{
 		.name = "",
@@ -175,28 +167,30 @@ static struct git_var git_vars[] = {
 static void list_vars(void)
 {
 	struct git_var *ptr;
-	char *val;
-
-	for (ptr = git_vars; ptr->read; ptr++)
-		if ((val = ptr->read(0))) {
-			if (ptr->multivalued && *val) {
-				struct string_list list = STRING_LIST_INIT_DUP;
-
-				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 {
+
+	for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
+		if (ptr->read) {
+			char *val = ptr->read(0);
+
+			if (val) {
 				printf("%s=%s\n", ptr->name, val);
+				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=%s\n", ptr->name, list.items[i].string);
+			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;
 		}
@@ -220,7 +214,6 @@ int cmd_var(int argc,
 	    struct repository *repo UNUSED)
 {
 	const struct git_var *git_var;
-	char *val;
 
 	show_usage_if_asked(argc, argv, var_usage);
 	if (argc != 2)
@@ -237,12 +230,26 @@ int cmd_var(int argc,
 	if (!git_var)
 		usage(var_usage);
 
-	val = git_var->read(IDENT_STRICT);
-	if (!val)
-		return 1;
+	if (git_var->read) {
+		char *val = git_var->read(IDENT_STRICT);
+
+		if (!val)
+			return 1;
 
-	printf("%s\n", val);
-	free(val);
+		printf("%s\n", val);
+		free(val);
+	} else {
+		struct string_list list = STRING_LIST_INIT_DUP;
+
+		git_var->multiread(&list);
+		if (!list.nr) {
+			string_list_clear(&list, 0);
+			return 1;
+		}
+		for (size_t i = 0; i < list.nr; i++)
+			printf("%s\n", list.items[i].string);
+		string_list_clear(&list, 0);
+	}
 
 	return 0;
 }
-- 
2.54.0 (Apple Git-157)
Andrew PleeterSep 26, 2026, 16:20 UTC in reply to Junio C Hamano on lore

[PATCH v9 2/4] var: add "-z" output mode

Scripts that read from "git var" have no unambiguous way to parse its output. A value that itself contains a newline, and a variable that has more than one value, both run together with the newline that terminates each entry.

Teach "git var" a "-z" option that terminates each entry with a NUL byte instead. With "-l", the variable name and its value are separated by a newline and each entry is terminated by NUL, which is the same convention "git config list -z" already uses. When a single variable is asked for, its bare value is terminated by NUL.

Parsing of the command line is switched over to parse_options() in order to accept the new option; as a side effect, options now have to come before the variable name.

Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
 Documentation/git-var.adoc | 19 ++++++++---
 builtin/var.c              | 67 +++++++++++++++++++++++++++-----------
 t/t0007-git-var.sh         | 30 +++++++++++++++++
 3 files changed, 93 insertions(+), 23 deletions(-)
Show changes to 3 files +93 −23

Documentation/git-var.adoc, builtin/var.c, t/t0007-git-var.sh

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index de3007732d..9052fbc549 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -9,13 +9,19 @@ 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.
 
+If `-z` is given, the value is terminated by a NUL byte instead of a
+newline. With `-l`, the variable name and its 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
 -------
 `-l`::
@@ -24,6 +30,11 @@ 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`, 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
@@ -86,9 +97,9 @@ endif::git-default-pager[]
     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.  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.
diff --git a/builtin/var.c b/builtin/var.c
index 9f7c8a6113..c9691070b8 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -14,13 +14,18 @@
 #include "environment.h"
 #include "ident.h"
 #include "pager.h"
+#include "parse-options.h"
 #include "path.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
+};
 
 static char *committer(int ident_flag)
 {
@@ -164,16 +169,18 @@ static struct git_var git_vars[] = {
 	},
 };
 
-static void list_vars(void)
+static void list_vars(int nul_term)
 {
 	struct git_var *ptr;
+	char delim = nul_term ? '\n' : '=';
+	char term = nul_term ? '\0' : '\n';
 
 	for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
 		if (ptr->read) {
 			char *val = ptr->read(0);
 
 			if (val) {
-				printf("%s=%s\n", ptr->name, val);
+				printf("%s%c%s%c", ptr->name, delim, val, term);
 				free(val);
 			}
 		} else {
@@ -181,7 +188,8 @@ static void list_vars(void)
 
 			ptr->multiread(&list);
 			for (size_t i = 0; i < list.nr; i++)
-				printf("%s=%s\n", ptr->name, list.items[i].string);
+				printf("%s%c%s%c", ptr->name, delim,
+				       list.items[i].string, term);
 			string_list_clear(&list, 0);
 		}
 	}
@@ -201,34 +209,55 @@ 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;
-
-	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();
+	int list = 0;
+	int nul_term = 0;
+	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(),
+	};
+
+	argc = parse_options(argc, argv, prefix, options,
+			     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+
+	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 != 1)
+		usage_with_options(var_usage, options);
+
 	repo_config(the_repository, git_default_config, NULL);
 
-	git_var = get_git_var(argv[1]);
+	term = nul_term ? '\0' : '\n';
+
+	git_var = get_git_var(argv[0]);
 	if (!git_var)
-		usage(var_usage);
+		usage_with_options(var_usage, options);
 
 	if (git_var->read) {
 		char *val = git_var->read(IDENT_STRICT);
@@ -236,7 +265,7 @@ int cmd_var(int argc,
 		if (!val)
 			return 1;
 
-		printf("%s\n", val);
+		printf("%s%c", val, term);
 		free(val);
 	} else {
 		struct string_list list = STRING_LIST_INIT_DUP;
@@ -247,7 +276,7 @@ int cmd_var(int argc,
 			return 1;
 		}
 		for (size_t i = 0; i < list.nr; i++)
-			printf("%s\n", list.items[i].string);
+			printf("%s%c", list.items[i].string, term);
 		string_list_clear(&list, 0);
 	}
 
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 2b60317758..661d0539c0 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -276,4 +276,34 @@ 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_done
-- 
2.54.0 (Apple Git-157)
Andrew PleeterSep 26, 2026, 16:20 UTC in reply to Junio C Hamano on lore

[PATCH v9 3/4] var: accept more than one variable

"git var" takes exactly one variable and shows its value, so a script that needs several of them has to run the command once per variable, paying process startup and configuration parsing each time.

Teach "git var" to take any number of variables and show them the way "git var -l" does, i.e. as "VARIABLE=value" (or "VARIABLE\nvalue\0" under "-z"), but limited to the variables the user asked for. A single variable still shows its bare value, and still exits with status 1 when it has no value, so existing users are unaffected.

When more than one variable is asked for, a variable that has no value is simply left out of the output and the rest are still shown. That a variable is unset is not an error in itself: a caller asking for both GIT_CONFIG_SYSTEM and GIT_CONFIG_GLOBAL should not be told that something went wrong merely because the user set GIT_CONFIG_NOSYSTEM. Keeping a non-zero status for genuine errors, such as naming a variable that does not exist, also lets callers detect those by looking at the exit code alone.

Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
 Documentation/git-var.adoc | 32 +++++++++++-------
 builtin/var.c              | 56 +++++++++++++++++++------------
 t/t0007-git-var.sh         | 67 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 123 insertions(+), 32 deletions(-)
Show changes to 3 files +123 −32

Documentation/git-var.adoc, builtin/var.c, t/t0007-git-var.sh

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index 9052fbc549..d0772d9743 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -10,17 +10,22 @@ SYNOPSIS
 --------
 [synopsis]
 git var [-z] -l
-git var [-z] <variable>
+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 value is terminated by a NUL byte instead of a
-newline. With `-l`, the variable name and its 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`.
+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
 -------
@@ -32,8 +37,9 @@ OPTIONS
 
 `-z`::
 	Terminate entries with NUL instead of newline. When used with
-	`-l`, the variable name and its value are separated by a
-	newline, and each entry is terminated with a NUL byte.
+	`-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
 --------
@@ -97,9 +103,11 @@ endif::git-default-pager[]
     This variable can have multiple values.
 
 Most path values contain only one value. However, some can contain multiple
-values, which are separated by newlines (or NUL bytes if `-z` is given), 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.
diff --git a/builtin/var.c b/builtin/var.c
index c9691070b8..5fbebc62e2 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -23,7 +23,7 @@
 
 static const char * const var_usage[] = {
 	N_("git var [-z] -l"),
-	N_("git var [-z] <variable>"),
+	N_("git var [-z] <variable>..."),
 	NULL
 };
 
@@ -225,9 +225,9 @@ int cmd_var(int argc,
 	    const char *prefix,
 	    struct repository *repo UNUSED)
 {
-	const struct git_var *git_var;
 	int list = 0;
 	int nul_term = 0;
+	char delim;
 	char term;
 	struct option options[] = {
 		OPT_BOOL('l', NULL, &list,
@@ -248,36 +248,52 @@ int cmd_var(int argc,
 		return 0;
 	}
 
-	if (argc != 1)
+	if (!argc)
 		usage_with_options(var_usage, options);
 
 	repo_config(the_repository, git_default_config, NULL);
 
+	delim = nul_term ? '\n' : '=';
 	term = nul_term ? '\0' : '\n';
 
-	git_var = get_git_var(argv[0]);
-	if (!git_var)
-		usage_with_options(var_usage, options);
+	for (int i = 0; i < argc; i++) {
+		const struct git_var *git_var = get_git_var(argv[i]);
 
-	if (git_var->read) {
-		char *val = git_var->read(IDENT_STRICT);
+		if (!git_var)
+			usage_with_options(var_usage, options);
 
-		if (!val)
-			return 1;
+		if (git_var->read) {
+			char *val = git_var->read(IDENT_STRICT);
 
-		printf("%s%c", val, term);
-		free(val);
-	} else {
-		struct string_list list = STRING_LIST_INIT_DUP;
+			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 (!list.nr) {
+			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 1;
 		}
-		for (size_t i = 0; i < list.nr; i++)
-			printf("%s%c", list.items[i].string, term);
-		string_list_clear(&list, 0);
 	}
 
 	return 0;
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 661d0539c0..593610300a 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -306,4 +306,71 @@ 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_done
-- 
2.54.0 (Apple Git-157)
Andrew PleeterSep 26, 2026, 16:20 UTC in reply to Junio C Hamano on lore

[PATCH v9 4/4] var: add broken-out identity variables

A script that wants only the author's name has to ask for GIT_AUTHOR_IDENT and take the result apart itself, which means reimplementing the rules split_ident_line() already knows, and getting them subtly wrong for names that contain an e-mail address or other awkward characters.

Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL and GIT_AUTHOR_DATE, together with their GIT_COMMITTER_* counterparts, which give the individual pieces of the corresponding *_IDENT variable. They are derived from the same ident string, so they are strict in the same way: asking for one of them fails if the identity cannot be determined.

Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>
---
 Documentation/git-var.adoc | 36 +++++++++++++--
 builtin/var.c              | 90 ++++++++++++++++++++++++++++++++++++++
 t/t0007-git-var.sh         | 56 ++++++++++++++++++++++++
 3 files changed, 178 insertions(+), 4 deletions(-)
Show changes to 3 files +178 −4

Documentation/git-var.adoc, builtin/var.c, t/t0007-git-var.sh

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index d0772d9743..6982228f97 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -43,17 +43,45 @@ OPTIONS
 
 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
diff --git a/builtin/var.c b/builtin/var.c
index 5fbebc62e2..dd4325e5b5 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -27,16 +27,82 @@ static const char * const var_usage[] = {
 	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());
@@ -123,10 +189,34 @@ 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,
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 593610300a..e43becff1c 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -373,4 +373,60 @@ test_expect_success 'unknown variable is a usage error' '
 	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
-- 
2.54.0 (Apple Git-157)
Phillip WoodOct 2, 2026, 14:11 UTC in reply to Andrew Pleeter on lore

Re: [PATCH v9 4/4] var: add broken-out identity variables

Hi Andrew
The implementation looks good, just one small comment on the tests.
On 26/09/2026 17:20, Andrew Pleeter wrote:
Show 12 quoted lines
> +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
> +'

I think it would have been sufficient just to list all the identity components at once, rather than having separate tests for each one, but it is not worth re-rolling just for that.

Thanks
Phillip
Show 44 quoted lines
> +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
Phillip WoodOct 2, 2026, 14:11 UTC in reply to Andrew Pleeter on lore

Re: [PATCH v9 0/4] var: -z output, multiple variables, and broken-out idents

Hi Andrew
On 26/09/2026 17:20, Andrew Pleeter wrote:
Show 15 quoted lines
> This is a reroll of the single patch in v8, split up as Junio asked
> for, with the changes Phillip asked for on the list.
> 
> v8 was one commit doing four things at once. It is now four patches:
> 
>    1/4 converts the internal representation of a multi-valued variable
>        from a newline-joined string to a string_list, with no change in
>        output. Phillip suggested doing this as its own preparatory step.
> 
>    2/4 adds "-z", which is what forces the switch to parse_options(), so
>        that conversion lives here rather than in 1/4.
> 
>    3/4 teaches "git var" to take more than one variable.
> 
>    4/4 adds GIT_AUTHOR_NAME and friends.

These patches look good to me. I've left a comment on the tests in patches 3 and 4, but I'm not sure they are enough to warrant a re-roll.

Thanks for working on this, supporting multiple values and NUL termination are really useful improvements.

Phillip
Show 44 quoted lines
> Changes since v8:
> 
>   * GIT_SIGNING_KEY is dropped. Phillip asked three times how it was
>     meant to be used, and once I looked properly the answer was that the
>     value cannot be interpreted without also reading gpg.format, and that
>     in the default configuration it is a committer ident rather than a
>     key at all. I would rather leave it out than define a variable I
>     cannot describe. Details are in my reply to him on this thread.
> 
>   * Asking for several variables no longer exits non-zero just because
>     one of them has no value; such a variable is left out of the output
>     and the rest are still shown. A non-zero status is now reserved for
>     real errors, such as naming a variable that does not exist, so
>     callers can detect those from the exit code. A single variable still
>     exits 1 when it has no value, as before. This is Phillip's
>     GIT_CONFIG_NOSYSTEM point.
> 
>   * GIT_CONFIG_GLOBAL is documented as a variable that can have more than
>     one value.
> 
>   * The commit messages are prose rather than a list of bullet points,
>     and no longer narrate how the patch was developed.
> 
> The tests use nul_to_q rather than running test_cmp over files
> containing NUL bytes, which Phillip pointed out in v6. Each patch builds
> and passes t0007 on its own.
> 
> One thing I did not do, and would like an opinion on: for a multi-valued
> variable in multi-variable mode, rather than emitting a trailing
> delimiter, each value is shown as its own "VARIABLE=value" entry, which
> matches "git var -l". Phillip suggested the trailing delimiter and I am
> happy to switch.
> 
> Andrew Pleeter (4):
>    var: represent multi-valued variables with a string_list
>    var: add "-z" output mode
>    var: accept more than one variable
>    var: add broken-out identity variables
> 
>   Documentation/git-var.adoc |  68 +++++++++--
>   builtin/var.c              | 242 +++++++++++++++++++++++++++++--------
>   t/t0007-git-var.sh         | 153 +++++++++++++++++++++++
>   3 files changed, 403 insertions(+), 60 deletions(-)
> 
Phillip WoodOct 2, 2026, 14:11 UTC in reply to Andrew Pleeter on lore

Re: [PATCH v9 3/4] var: accept more than one variable

Hi Andrew

This all looks fine, though we typically avoid using test_env() because it introduces a hidden subshell. I've left a couple of suggestions below, but unless there is another reason to re-roll I wouldn't worry too much.

On 26/09/2026 17:20, Andrew Pleeter wrote:
Show 8 quoted lines
> +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 &&

There is no need to use test_env here, "GIT_CONFIG_GLOBAL= git var ..." is all that's needed.

Show 5 quoted lines
> +	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 &&

Here test_env is also not needed, "env GIT_CONFIG_GLOBAL= test_expect_code 1 git var ..." would be our typical style.

Thanks
Phillip
Show 9 quoted lines
> +	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_done
Andrew PleeterOct 2, 2026, 17:41 UTC in reply to Phillip Wood on lore

Re: [PATCH v9 0/4] var: -z output, multiple variables, and broken-out idents

Hi Phillip,

Thanks for the review, and for the test_env pointer. Both nits are fair, and both of those tests are newer than the rest of the series:

  - 3/4: "GIT_CONFIG_GLOBAL= git var ..." and "env GIT_CONFIG_GLOBAL=
    test_expect_code 1 git var ..." are clearly better than dragging a
    subshell in. I will use that style from here on.
  - 4/4: the per-component tests collapse into "get several identity
    components at once", which already covers the same ground.

I will fold both in if anything else prompts a v10. If the series goes in as it stands, I can send them afterwards as a cleanup instead, whichever Junio prefers.

Thanks also for the GIT_SIGNING_KEY questions. Dropping it was the right call, and I would not have got there if you had not asked three times.

Thanks, Andrew

Back to recent threads