All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH] builtin/whoami: add new 'whoami' command
@ 2026-08-25 20:46 Andrew Pleeter via GitGitGadget
  2026-08-25 21:24 ` brian m. carlson
                   ` (7 more replies)
  0 siblings, 8 replies; 20+ messages in thread
From: Andrew Pleeter via GitGitGadget @ 2026-08-25 20:46 UTC (permalink / raw)
  To: git; +Cc: Andrew Pleeter, anpl1623

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

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

^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH] builtin/whoami: add new 'whoami' command
  2026-08-25 20:46 [PATCH] builtin/whoami: add new 'whoami' command Andrew Pleeter via GitGitGadget
@ 2026-08-25 21:24 ` brian m. carlson
  2026-08-25 21:41 ` Junio C Hamano
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 20+ messages in thread
From: brian m. carlson @ 2026-08-25 21:24 UTC (permalink / raw)
  To: Andrew Pleeter via GitGitGadget; +Cc: git, Andrew Pleeter

[-- Attachment #1: Type: text/plain, Size: 3965 bytes --]

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

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

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

> +	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

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 325 bytes --]

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH] builtin/whoami: add new 'whoami' command
  2026-08-25 20:46 [PATCH] builtin/whoami: add new 'whoami' command Andrew Pleeter via GitGitGadget
  2026-08-25 21:24 ` brian m. carlson
@ 2026-08-25 21:41 ` Junio C Hamano
  2026-08-31 23:59 ` [PATCH v2] builtin/ident: add new 'ident' command Andrew Pleeter via GitGitGadget
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 20+ messages in thread
From: Junio C Hamano @ 2026-08-25 21:41 UTC (permalink / raw)
  To: Andrew Pleeter via GitGitGadget; +Cc: git, Andrew Pleeter

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

> +`-a`::
> +`--author`::
> +	Show author identity.
> +
> +`-c`::
> +`--committer`::
> +	Show committer identity.

This pair hints the users they are equals.

But the code tells us otherwise:

> +	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:

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

> +`-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>


^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v2] builtin/ident: add new 'ident' command
  2026-08-25 20:46 [PATCH] builtin/whoami: add new 'whoami' command Andrew Pleeter via GitGitGadget
  2026-08-25 21:24 ` brian m. carlson
  2026-08-25 21:41 ` Junio C Hamano
@ 2026-08-31 23:59 ` Andrew Pleeter via GitGitGadget
  2026-09-01  4:39   ` Jeff King
  2026-09-03  2:49 ` [PATCH v3] var: support broken-down idents, default key, multiple args, and -z Andrew Pleeter via GitGitGadget
                   ` (4 subsequent siblings)
  7 siblings, 1 reply; 20+ messages in thread
From: Andrew Pleeter via GitGitGadget @ 2026-08-31 23:59 UTC (permalink / raw)
  To: git; +Cc: brian m. carlson, Andrew Pleeter, Andrew Pleeter

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

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

^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH v2] builtin/ident: add new 'ident' command
  2026-08-31 23:59 ` [PATCH v2] builtin/ident: add new 'ident' command Andrew Pleeter via GitGitGadget
@ 2026-09-01  4:39   ` Jeff King
  2026-09-01  5:00     ` Junio C Hamano
  0 siblings, 1 reply; 20+ messages in thread
From: Jeff King @ 2026-09-01  4:39 UTC (permalink / raw)
  To: Andrew Pleeter via GitGitGadget; +Cc: git, brian m. carlson, Andrew Pleeter

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?

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

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v2] builtin/ident: add new 'ident' command
  2026-09-01  4:39   ` Jeff King
@ 2026-09-01  5:00     ` Junio C Hamano
  0 siblings, 0 replies; 20+ messages in thread
From: Junio C Hamano @ 2026-09-01  5:00 UTC (permalink / raw)
  To: Jeff King
  Cc: Andrew Pleeter via GitGitGadget, git, brian m. carlson,
	Andrew Pleeter

Jeff King <peff@peff.net> writes:

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

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

^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v3] var: support broken-down idents, default key, multiple args, and -z
  2026-08-25 20:46 [PATCH] builtin/whoami: add new 'whoami' command Andrew Pleeter via GitGitGadget
                   ` (2 preceding siblings ...)
  2026-08-31 23:59 ` [PATCH v2] builtin/ident: add new 'ident' command Andrew Pleeter via GitGitGadget
@ 2026-09-03  2:49 ` Andrew Pleeter via GitGitGadget
  2026-09-03 17:40   ` Junio C Hamano
  2026-09-04  9:11   ` Phillip Wood
  2026-09-08  4:09 ` [PATCH v4] var: support broken-down idents, signing " Andrew Pleeter via GitGitGadget
                   ` (3 subsequent siblings)
  7 siblings, 2 replies; 20+ messages in thread
From: Andrew Pleeter via GitGitGadget @ 2026-09-03  2:49 UTC (permalink / raw)
  To: git
  Cc: brian m. carlson, Jeff King, Junio C Hamano, Andrew Pleeter,
	Andrew Pleeter

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(-)

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

^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z
  2026-09-03  2:49 ` [PATCH v3] var: support broken-down idents, default key, multiple args, and -z Andrew Pleeter via GitGitGadget
@ 2026-09-03 17:40   ` Junio C Hamano
  2026-09-03 18:22     ` Ben Knoble
  2026-09-04  9:11   ` Phillip Wood
  1 sibling, 1 reply; 20+ messages in thread
From: Junio C Hamano @ 2026-09-03 17:40 UTC (permalink / raw)
  To: Andrew Pleeter via GitGitGadget
  Cc: git, brian m. carlson, Jeff King, Andrew Pleeter

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

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

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

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

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

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

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

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

    $ git var GIT_AUTHOR_DATE

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

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

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

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

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

Here is my attempt:

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

Similarly for GIT_COMMITTER_*.


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

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

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

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

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

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

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

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

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

An overly long line.

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

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

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

I dunno.

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

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

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

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

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

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

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

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

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

Beware overly long lines.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Just a trivia, but

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

would be equivalent.

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z
  2026-09-03 17:40   ` Junio C Hamano
@ 2026-09-03 18:22     ` Ben Knoble
  0 siblings, 0 replies; 20+ messages in thread
From: Ben Knoble @ 2026-09-03 18:22 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Andrew Pleeter via GitGitGadget, git, brian m. carlson, Jeff King,
	Andrew Pleeter


> 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?

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

^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z
  2026-09-03  2:49 ` [PATCH v3] var: support broken-down idents, default key, multiple args, and -z Andrew Pleeter via GitGitGadget
  2026-09-03 17:40   ` Junio C Hamano
@ 2026-09-04  9:11   ` Phillip Wood
  2026-09-04 15:57     ` Junio C Hamano
  1 sibling, 1 reply; 20+ messages in thread
From: Phillip Wood @ 2026-09-04  9:11 UTC (permalink / raw)
  To: Andrew Pleeter via GitGitGadget, git
  Cc: brian m. carlson, Jeff King, Junio C Hamano, Andrew Pleeter

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

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

> +	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
> +		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


^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z
  2026-09-04  9:11   ` Phillip Wood
@ 2026-09-04 15:57     ` Junio C Hamano
  2026-09-08  9:07       ` Phillip Wood
  0 siblings, 1 reply; 20+ messages in thread
From: Junio C Hamano @ 2026-09-04 15:57 UTC (permalink / raw)
  To: Phillip Wood
  Cc: Andrew Pleeter via GitGitGadget, git, brian m. carlson, Jeff King,
	Andrew Pleeter

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

>> [...]
>> -	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.

^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v4] var: support broken-down idents, signing key, multiple args, and -z
  2026-08-25 20:46 [PATCH] builtin/whoami: add new 'whoami' command Andrew Pleeter via GitGitGadget
                   ` (3 preceding siblings ...)
  2026-09-03  2:49 ` [PATCH v3] var: support broken-down idents, default key, multiple args, and -z Andrew Pleeter via GitGitGadget
@ 2026-09-08  4:09 ` Andrew Pleeter via GitGitGadget
  2026-09-08 13:54   ` Phillip Wood
  2026-09-08 20:43 ` [PATCH v5] " Andrew Pleeter via GitGitGadget
                   ` (2 subsequent siblings)
  7 siblings, 1 reply; 20+ messages in thread
From: Andrew Pleeter via GitGitGadget @ 2026-09-08  4:09 UTC (permalink / raw)
  To: git
  Cc: brian m. carlson, Jeff King, Junio C Hamano, Ben Knoble,
	Phillip Wood, Andrew Pleeter, Andrew Pleeter

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(-)

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

^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z
  2026-09-04 15:57     ` Junio C Hamano
@ 2026-09-08  9:07       ` Phillip Wood
  0 siblings, 0 replies; 20+ messages in thread
From: Phillip Wood @ 2026-09-08  9:07 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Andrew Pleeter via GitGitGadget, git, brian m. carlson, Jeff King,
	Andrew Pleeter

Hi Junio

On 04/09/2026 16:57, Junio C Hamano wrote:
> 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

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

>>> [...]
>>> -	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.

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


^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v4] var: support broken-down idents, signing key, multiple args, and -z
  2026-09-08  4:09 ` [PATCH v4] var: support broken-down idents, signing " Andrew Pleeter via GitGitGadget
@ 2026-09-08 13:54   ` Phillip Wood
  0 siblings, 0 replies; 20+ messages in thread
From: Phillip Wood @ 2026-09-08 13:54 UTC (permalink / raw)
  To: Andrew Pleeter via GitGitGadget, git
  Cc: brian m. carlson, Jeff King, Junio C Hamano, Ben Knoble,
	Andrew Pleeter



On 08/09/2026 05:09, Andrew Pleeter via GitGitGadget wrote:
> 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)
> +{
> +	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.

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

> +	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'.

> +			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);
> +	}
>   }   

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

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

> +		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?

> +	}
> +
>   	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.

>   
> -	val = git_var->read(IDENT_STRICT);
> -	if (!val)
> -		return 1;
> +		if (git_var->multivalued && null_term && *val) {

Why "*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');

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

> +			}
> +			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


^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v5] var: support broken-down idents, signing key, multiple args, and -z
  2026-08-25 20:46 [PATCH] builtin/whoami: add new 'whoami' command Andrew Pleeter via GitGitGadget
                   ` (4 preceding siblings ...)
  2026-09-08  4:09 ` [PATCH v4] var: support broken-down idents, signing " Andrew Pleeter via GitGitGadget
@ 2026-09-08 20:43 ` Andrew Pleeter via GitGitGadget
  2026-09-08 21:53   ` Junio C Hamano
  2026-09-09  1:24 ` [PATCH v6] " Andrew Pleeter via GitGitGadget
  2026-09-10  3:09 ` [PATCH v7] " Andrew Pleeter via GitGitGadget
  7 siblings, 1 reply; 20+ messages in thread
From: Andrew Pleeter via GitGitGadget @ 2026-09-08 20:43 UTC (permalink / raw)
  To: git
  Cc: brian m. carlson, Jeff King, Junio C Hamano, Ben Knoble,
	Phillip Wood, Andrew Pleeter, Andrew Pleeter

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(-)

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

^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH v5] var: support broken-down idents, signing key, multiple args, and -z
  2026-09-08 20:43 ` [PATCH v5] " Andrew Pleeter via GitGitGadget
@ 2026-09-08 21:53   ` Junio C Hamano
  0 siblings, 0 replies; 20+ messages in thread
From: Junio C Hamano @ 2026-09-08 21:53 UTC (permalink / raw)
  To: Andrew Pleeter via GitGitGadget
  Cc: git, brian m. carlson, Jeff King, Ben Knoble, Phillip Wood,
	Andrew Pleeter

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

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

> @@ -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?

> +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?

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

>  	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);
>  }

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

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

	}

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

^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v6] var: support broken-down idents, signing key, multiple args, and -z
  2026-08-25 20:46 [PATCH] builtin/whoami: add new 'whoami' command Andrew Pleeter via GitGitGadget
                   ` (5 preceding siblings ...)
  2026-09-08 20:43 ` [PATCH v5] " Andrew Pleeter via GitGitGadget
@ 2026-09-09  1:24 ` Andrew Pleeter via GitGitGadget
  2026-09-09 15:36   ` Phillip Wood
  2026-09-09 16:42   ` Junio C Hamano
  2026-09-10  3:09 ` [PATCH v7] " Andrew Pleeter via GitGitGadget
  7 siblings, 2 replies; 20+ messages in thread
From: Andrew Pleeter via GitGitGadget @ 2026-09-09  1:24 UTC (permalink / raw)
  To: git
  Cc: brian m. carlson, Jeff King, Junio C Hamano, Ben Knoble,
	Phillip Wood, Andrew Pleeter, Andrew Pleeter

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(-)

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

^ permalink raw reply related	[flat|nested] 20+ messages in thread

* Re: [PATCH v6] var: support broken-down idents, signing key, multiple args, and -z
  2026-09-09  1:24 ` [PATCH v6] " Andrew Pleeter via GitGitGadget
@ 2026-09-09 15:36   ` Phillip Wood
  2026-09-09 16:42   ` Junio C Hamano
  1 sibling, 0 replies; 20+ messages in thread
From: Phillip Wood @ 2026-09-09 15:36 UTC (permalink / raw)
  To: Andrew Pleeter via GitGitGadget, git
  Cc: brian m. carlson, Jeff King, Junio C Hamano, Ben Knoble,
	Andrew Pleeter

Hi Andrew

On 09/09/2026 02:24, Andrew Pleeter via GitGitGadget wrote:
> 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.
> - 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>
> ---

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

> 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

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

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

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

>   
>   struct git_var {
>   	const char *name;
>   	char *(*read)(int);
> -	int multivalued;
> +	int (*multiread)(struct string_list *);
>   };

> -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().

> +			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);

>   int cmd_var(int argc,
>   	    const char **argv,
> -	    const char *prefix UNUSED,
> +	    const char *prefix,
>   	    struct repository *repo UNUSED)
 > [...]
> +	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.

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

> +				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

> +		}
> +	}
>   
> -	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


^ permalink raw reply	[flat|nested] 20+ messages in thread

* Re: [PATCH v6] var: support broken-down idents, signing key, multiple args, and -z
  2026-09-09  1:24 ` [PATCH v6] " Andrew Pleeter via GitGitGadget
  2026-09-09 15:36   ` Phillip Wood
@ 2026-09-09 16:42   ` Junio C Hamano
  1 sibling, 0 replies; 20+ messages in thread
From: Junio C Hamano @ 2026-09-09 16:42 UTC (permalink / raw)
  To: Andrew Pleeter via GitGitGadget
  Cc: git, brian m. carlson, Jeff King, Ben Knoble, Phillip Wood,
	Andrew Pleeter

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

>  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?

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

> 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);

> +	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?

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

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

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

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


> +			}
> +			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.


^ permalink raw reply	[flat|nested] 20+ messages in thread

* [PATCH v7] var: support broken-down idents, signing key, multiple args, and -z
  2026-08-25 20:46 [PATCH] builtin/whoami: add new 'whoami' command Andrew Pleeter via GitGitGadget
                   ` (6 preceding siblings ...)
  2026-09-09  1:24 ` [PATCH v6] " Andrew Pleeter via GitGitGadget
@ 2026-09-10  3:09 ` Andrew Pleeter via GitGitGadget
  7 siblings, 0 replies; 20+ messages in thread
From: Andrew Pleeter via GitGitGadget @ 2026-09-10  3:09 UTC (permalink / raw)
  To: git
  Cc: brian m. carlson, Jeff King, Junio C Hamano, Ben Knoble,
	Phillip Wood, Andrew Pleeter, Andrew Pleeter

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(-)

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

^ permalink raw reply related	[flat|nested] 20+ messages in thread

end of thread, other threads:[~2026-09-10  3:10 UTC | newest]

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

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.