{"thread":{"id":"66219","subject":"[PATCH] builtin/whoami: add new 'whoami' command","startedAt":"2026-08-25T20:46:45Z","lastAt":"2026-10-02T17:41:46Z","messageCount":35,"participants":["Andrew Pleeter via GitGitGadget","brian m. carlson","Junio C Hamano","Jeff King","Ben Knoble","Phillip Wood","Andrew Pleeter"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"551238","messageId":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","threadId":"66219","inReplyTo":null,"subject":"[PATCH] builtin/whoami: add new 'whoami' command","fromName":"Andrew Pleeter via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-25T20:46:42Z","receivedAt":"2026-08-25T20:46:45Z","isPatch":true,"body":"From: anpl1623 <andrewpleeter@gmail.com>\n\nAdd a builtin 'whoami' command to inspect and display the resolved\nauthor and committer identity along with the commit signing\nconfiguration (GPG/SSH key ID and commit.gpgsign status) used when\ncreating Git commits.\n\nSupport optional flags (--author, --committer, --name, --email,\n--signing-key, and --verbose) for targeted querying and scripting.\n\nInclude documentation in Documentation/git-whoami.adoc and regression\ntests in t/t0015-whoami.sh.\n\nSigned-off-by: anpl1623 <andrewpleeter@gmail.com>\n---\n    builtin/whoami: add new 'whoami' command\n    \n    \n    Title:\n    ======\n    \n    builtin/whoami: add new 'whoami' command\n    \n    ------------------------------------------------------------------------\n    \n    \n    Description:\n    ============\n    \n    SUMMARY\n    \n    Adds a new builtin git whoami command that inspects and displays the\n    user's active author and committer identity along with their commit\n    signing configuration (GPG/SSH key ID and commit.gpgsign status).\n    \n    MOTIVATION\n    \n    Users often work across multiple environments, profiles, or repositories\n    with different global/local configs and signing keys. Currently,\n    verifying what identity and signing key will be attached to a new commit\n    requires checking several individual git config and git var settings.\n    git whoami provides a simple, direct porcelain command to verify this in\n    one step.\n    \n    CHANGES\n    \n     * Core implementation: Added builtin/whoami.c using Git's ident.h and\n       gpg-interface.h APIs.\n     * CLI flags supported:\n       * git whoami (default overview)\n       * -a, --author (author identity: Name <email>)\n       * -c, --committer (committer identity: Name <email>)\n       * -n, --name (name only)\n       * -e, --email (email only)\n       * -s, --signing-key (signing key ID)\n       * -v, --verbose (detailed identity and signing breakdown)\n     * Documentation: Added manpage in Documentation/git-whoami.adoc.\n     * Build integration: Added to Makefile, meson.build, command-list.txt,\n       and builtin.h.\n     * Test suite: Added comprehensive automated tests in t/t0015-whoami.sh.\n    \n    EXAMPLE OUTPUT\n    \n    $ git whoami\n    Author:    Jane Doe <jane@example.com>\n    Committer: Jane Doe <jane@example.com>\n    Signing:   /Users/jane/.ssh/id_ed25519_git (format: ssh, commit.gpgsign: true)\n    \n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v1\nPull-Request: https://github.com/git/git/pull/2388\n\n Documentation/git-whoami.adoc |  48 ++++++++\n Documentation/meson.build     |   1 +\n Makefile                      |   1 +\n builtin.h                     |   1 +\n builtin/whoami.c              | 201 ++++++++++++++++++++++++++++++++++\n command-list.txt              |   1 +\n git.c                         |   1 +\n meson.build                   |   1 +\n t/meson.build                 |   1 +\n t/t0015-whoami.sh             | 181 ++++++++++++++++++++++++++++++\n 10 files changed, 437 insertions(+)\n create mode 100644 Documentation/git-whoami.adoc\n create mode 100644 builtin/whoami.c\n create mode 100755 t/t0015-whoami.sh\n\ndiff --git a/Documentation/git-whoami.adoc b/Documentation/git-whoami.adoc\nnew file mode 100644\nindex 0000000000..80670f10e9\n--- /dev/null\n+++ b/Documentation/git-whoami.adoc\n@@ -0,0 +1,48 @@\n+git-whoami(1)\n+=============\n+\n+NAME\n+----\n+git-whoami - Show current user identity and commit signing information\n+\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git whoami [options]\n+\n+DESCRIPTION\n+-----------\n+Displays the author and committer identity (name and email address) and\n+commit signing configuration that will be used when signing and recording\n+Git commits.\n+\n+OPTIONS\n+-------\n+`-a`::\n+`--author`::\n+\tShow author identity.\n+\n+`-c`::\n+`--committer`::\n+\tShow committer identity.\n+\n+`-n`::\n+`--name`::\n+\tShow name only.\n+\n+`-e`::\n+`--email`::\n+\tShow email only.\n+\n+`-s`::\n+`--signing-key`::\n+\tShow commit signing key only.\n+\n+`-v`::\n+`--verbose`::\n+\tShow detailed identity and signing status.\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex f4854f802d..3476e41f29 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -163,6 +163,7 @@ manpages = {\n   'git-verify-tag.adoc' : 1,\n   'git-version.adoc' : 1,\n   'git-web--browse.adoc' : 1,\n+  'git-whoami.adoc' : 1,\n   'git-worktree.adoc' : 1,\n   'git-write-tree.adoc' : 1,\n   'git.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex d4b775953d..3af620d73d 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1517,6 +1517,7 @@ BUILTIN_OBJS += builtin/var.o\n BUILTIN_OBJS += builtin/verify-commit.o\n BUILTIN_OBJS += builtin/verify-pack.o\n BUILTIN_OBJS += builtin/verify-tag.o\n+BUILTIN_OBJS += builtin/whoami.o\n BUILTIN_OBJS += builtin/worktree.o\n BUILTIN_OBJS += builtin/write-tree.o\n \ndiff --git a/builtin.h b/builtin.h\nindex 4e47a4ebd3..9e38298274 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -284,5 +284,6 @@ int cmd_verify_pack(int argc, const char **argv, const char *prefix, struct repo\n int cmd_show_ref(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_pack_refs(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_replace(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_whoami(int argc, const char **argv, const char *prefix, struct repository *repo);\n \n #endif\ndiff --git a/builtin/whoami.c b/builtin/whoami.c\nnew file mode 100644\nindex 0000000000..3830f76c93\n--- /dev/null\n+++ b/builtin/whoami.c\n@@ -0,0 +1,201 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"builtin.h\"\n+#include \"config.h\"\n+#include \"gettext.h\"\n+#include \"gpg-interface.h\"\n+#include \"ident.h\"\n+#include \"parse-options.h\"\n+#include \"strbuf.h\"\n+\n+static const char * const whoami_usage[] = {\n+\tN_(\"git whoami [options]\"),\n+\tNULL\n+};\n+\n+int cmd_whoami(int argc,\n+\t       const char **argv,\n+\t       const char *prefix,\n+\t       struct repository *repo UNUSED)\n+{\n+\tint show_author = 0;\n+\tint show_committer = 0;\n+\tint show_name = 0;\n+\tint show_email = 0;\n+\tint show_signing_key = 0;\n+\tint verbose = 0;\n+\tint ret = 0;\n+\n+\tstruct option whoami_options[] = {\n+\t\tOPT_BOOL('a', \"author\", &show_author, N_(\"show author identity\")),\n+\t\tOPT_BOOL('c', \"committer\", &show_committer, N_(\"show committer identity\")),\n+\t\tOPT_BOOL('n', \"name\", &show_name, N_(\"show name only\")),\n+\t\tOPT_BOOL('e', \"email\", &show_email, N_(\"show email only\")),\n+\t\tOPT_BOOL('s', \"signing-key\", &show_signing_key, N_(\"show commit signing key\")),\n+\t\tOPT__VERBOSE(&verbose, N_(\"show detailed identity and signing status\")),\n+\t\tOPT_END()\n+\t};\n+\n+\tstruct strbuf author_info = STRBUF_INIT;\n+\tstruct strbuf committer_info = STRBUF_INIT;\n+\tstruct ident_split author_split, committer_split;\n+\tstruct strbuf author_name = STRBUF_INIT;\n+\tstruct strbuf author_email = STRBUF_INIT;\n+\tstruct strbuf committer_name = STRBUF_INIT;\n+\tstruct strbuf committer_email = STRBUF_INIT;\n+\n+\tchar *signing_key = NULL;\n+\tchar *gpg_format = NULL;\n+\tchar *ssh_default_key_cmd = NULL;\n+\tchar *resolved_key = NULL;\n+\tint gpgsign = 0;\n+\tint is_ssh = 0;\n+\n+\targc = parse_options(argc, argv, prefix, whoami_options,\n+\t\t\t     whoami_usage, 0);\n+\n+\tif (argc > 0)\n+\t\tusage_with_options(whoami_usage, whoami_options);\n+\n+\tdie_for_incompatible_opt2(show_author, \"--author\", show_committer, \"--committer\");\n+\tdie_for_incompatible_opt2(show_name, \"--name\", show_email, \"--email\");\n+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", show_name, \"--name\");\n+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", show_email, \"--email\");\n+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", show_author, \"--author\");\n+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", show_committer, \"--committer\");\n+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", verbose, \"--verbose\");\n+\tdie_for_incompatible_opt2(verbose, \"--verbose\", show_name, \"--name\");\n+\tdie_for_incompatible_opt2(verbose, \"--verbose\", show_email, \"--email\");\n+\tdie_for_incompatible_opt2(verbose, \"--verbose\", show_author, \"--author\");\n+\tdie_for_incompatible_opt2(verbose, \"--verbose\", show_committer, \"--committer\");\n+\n+\trepo_config(the_repository, git_default_config, NULL);\n+\n+\tstrbuf_addstr(&author_info, git_author_info(IDENT_NO_DATE));\n+\tstrbuf_addstr(&committer_info, git_committer_info(IDENT_NO_DATE));\n+\n+\tif (split_ident_line(&author_split, author_info.buf, author_info.len) == 0) {\n+\t\tif (author_split.name_begin && author_split.name_end)\n+\t\t\tstrbuf_add(&author_name, author_split.name_begin,\n+\t\t\t\t   author_split.name_end - author_split.name_begin);\n+\t\tif (author_split.mail_begin && author_split.mail_end)\n+\t\t\tstrbuf_add(&author_email, author_split.mail_begin,\n+\t\t\t\t   author_split.mail_end - author_split.mail_begin);\n+\t}\n+\n+\tif (split_ident_line(&committer_split, committer_info.buf, committer_info.len) == 0) {\n+\t\tif (committer_split.name_begin && committer_split.name_end)\n+\t\t\tstrbuf_add(&committer_name, committer_split.name_begin,\n+\t\t\t\t   committer_split.name_end - committer_split.name_begin);\n+\t\tif (committer_split.mail_begin && committer_split.mail_end)\n+\t\t\tstrbuf_add(&committer_email, committer_split.mail_begin,\n+\t\t\t\t   committer_split.mail_end - committer_split.mail_begin);\n+\t}\n+\n+\trepo_config_get_bool(the_repository, \"commit.gpgsign\", &gpgsign);\n+\trepo_config_get_string(the_repository, \"user.signingkey\", &signing_key);\n+\trepo_config_get_string(the_repository, \"gpg.format\", &gpg_format);\n+\trepo_config_get_string(the_repository, \"gpg.ssh.defaultkeycommand\", &ssh_default_key_cmd);\n+\n+\tis_ssh = gpg_format && !strcmp(gpg_format, \"ssh\");\n+\n+\tif (signing_key && *signing_key) {\n+\t\tresolved_key = xstrdup(signing_key);\n+\t} else if (is_ssh) {\n+\t\tif (ssh_default_key_cmd && *ssh_default_key_cmd)\n+\t\t\tresolved_key = get_signing_key_id();\n+\t} else if (gpgsign) {\n+\t\tresolved_key = get_signing_key_id();\n+\t}\n+\n+\tif (show_signing_key) {\n+\t\tif (resolved_key && *resolved_key) {\n+\t\t\tputs(resolved_key);\n+\t\t\tret = 0;\n+\t\t} else {\n+\t\t\tret = 1;\n+\t\t}\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (show_name) {\n+\t\tif (show_author)\n+\t\t\tputs(author_name.buf);\n+\t\telse\n+\t\t\tputs(committer_name.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (show_email) {\n+\t\tif (show_author)\n+\t\t\tputs(author_email.buf);\n+\t\telse\n+\t\t\tputs(committer_email.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (show_author) {\n+\t\tputs(author_info.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (show_committer) {\n+\t\tputs(committer_info.buf);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (verbose) {\n+\t\tprintf(_(\"Author Name:      %s\\n\"), author_name.buf);\n+\t\tprintf(_(\"Author Email:     %s\\n\"), author_email.buf);\n+\t\tprintf(_(\"Committer Name:   %s\\n\"), committer_name.buf);\n+\t\tprintf(_(\"Committer Email:  %s\\n\"), committer_email.buf);\n+\t\tif (signing_key && *signing_key)\n+\t\t\tprintf(_(\"Signing Key:      %s\\n\"), signing_key);\n+\t\telse if (resolved_key && *resolved_key)\n+\t\t\tprintf(_(\"Signing Key:      %s (default fallback)\\n\"), resolved_key);\n+\t\telse\n+\t\t\tprintf(_(\"Signing Key:      %s\\n\"), _(\"none\"));\n+\t\tprintf(_(\"Signing Format:   %s\\n\"),\n+\t\t       gpg_format ? gpg_format : \"openpgp\");\n+\t\tprintf(_(\"GPG Signing:      %s\\n\"),\n+\t\t       gpgsign ? _(\"enabled\") : _(\"disabled\"));\n+\t} else {\n+\t\tprintf(_(\"Author:    %s\\n\"), author_info.buf);\n+\t\tprintf(_(\"Committer: %s\\n\"), committer_info.buf);\n+\t\tif (gpgsign) {\n+\t\t\tif (signing_key && *signing_key) {\n+\t\t\t\tprintf(_(\"Signing:   %s (format: %s, commit.gpgsign: true)\\n\"),\n+\t\t\t\t       signing_key,\n+\t\t\t\t       gpg_format ? gpg_format : \"openpgp\");\n+\t\t\t} else if (resolved_key && *resolved_key) {\n+\t\t\t\tprintf(_(\"Signing:   default key (%s) (format: %s, commit.gpgsign: true)\\n\"),\n+\t\t\t\t       resolved_key,\n+\t\t\t\t       gpg_format ? gpg_format : \"openpgp\");\n+\t\t\t} else {\n+\t\t\t\tprintf(_(\"Signing:   enabled (no signing key configured)\\n\"));\n+\t\t\t}\n+\t\t} else {\n+\t\t\tif (signing_key && *signing_key) {\n+\t\t\t\tprintf(_(\"Signing:   disabled (key: %s, format: %s, commit.gpgsign: false)\\n\"),\n+\t\t\t\t       signing_key,\n+\t\t\t\t       gpg_format ? gpg_format : \"openpgp\");\n+\t\t\t} else {\n+\t\t\t\tprintf(_(\"Signing:   disabled (commit.gpgsign: false)\\n\"));\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+cleanup:\n+\tfree(signing_key);\n+\tfree(gpg_format);\n+\tfree(ssh_default_key_cmd);\n+\tfree(resolved_key);\n+\tstrbuf_release(&author_info);\n+\tstrbuf_release(&committer_info);\n+\tstrbuf_release(&author_name);\n+\tstrbuf_release(&author_email);\n+\tstrbuf_release(&committer_name);\n+\tstrbuf_release(&committer_email);\n+\n+\treturn ret;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex 21b802c420..0276bc2a53 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -210,6 +210,7 @@ git-verify-pack                         plumbinginterrogators\n git-verify-tag                          ancillaryinterrogators\n git-version                             ancillaryinterrogators\n git-whatchanged                         ancillaryinterrogators          complete\n+git-whoami                              ancillaryinterrogators\n git-worktree                            mainporcelain\n git-write-tree                          plumbingmanipulators\n gitattributes                           userinterfaces\ndiff --git a/git.c b/git.c\nindex e5f1811b6b..83629178c9 100644\n--- a/git.c\n+++ b/git.c\n@@ -680,6 +680,7 @@ static struct cmd_struct commands[] = {\n #ifndef WITH_BREAKING_CHANGES\n \t{ \"whatchanged\", cmd_whatchanged, RUN_SETUP | DEPRECATED },\n #endif\n+\t{ \"whoami\", cmd_whoami, RUN_SETUP_GENTLY },\n \t{ \"worktree\", cmd_worktree, RUN_SETUP },\n \t{ \"write-tree\", cmd_write_tree, RUN_SETUP },\n };\ndiff --git a/meson.build b/meson.build\nindex d86f2acd2b..0daf395752 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -726,6 +726,7 @@ builtin_sources = [\n   'builtin/verify-commit.c',\n   'builtin/verify-pack.c',\n   'builtin/verify-tag.c',\n+  'builtin/whoami.c',\n   'builtin/worktree.c',\n   'builtin/write-tree.c',\n ]\ndiff --git a/t/meson.build b/t/meson.build\nindex 181d61a8a0..8c111148da 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -87,6 +87,7 @@ integration_tests = [\n   't0012-help.sh',\n   't0013-sha1dc.sh',\n   't0014-alias.sh',\n+  't0015-whoami.sh',\n   't0017-env-helper.sh',\n   't0018-advice.sh',\n   't0019-json-writer.sh',\ndiff --git a/t/t0015-whoami.sh b/t/t0015-whoami.sh\nnew file mode 100755\nindex 0000000000..2a2d9b6dd3\n--- /dev/null\n+++ b/t/t0015-whoami.sh\n@@ -0,0 +1,181 @@\n+#!/bin/sh\n+\n+test_description='basic sanity checks for git whoami'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success 'default output format without signing' '\n+\tcat >expect <<-EOF &&\n+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n+\tSigning:   disabled (commit.gpgsign: false)\n+\tEOF\n+\tgit whoami >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git whoami --author' '\n+\techo \"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\" >expect &&\n+\tgit whoami --author >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git whoami --committer' '\n+\techo \"$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\" >expect &&\n+\tgit whoami --committer >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git whoami --author --name and --author --email' '\n+\techo \"$GIT_AUTHOR_NAME\" >expect_name &&\n+\tgit whoami --author --name >actual_name &&\n+\ttest_cmp expect_name actual_name &&\n+\techo \"$GIT_AUTHOR_EMAIL\" >expect_email &&\n+\tgit whoami --author --email >actual_email &&\n+\ttest_cmp expect_email actual_email\n+'\n+\n+test_expect_success 'git whoami --committer --name and --committer --email' '\n+\techo \"$GIT_COMMITTER_NAME\" >expect_name &&\n+\tgit whoami --committer --name >actual_name &&\n+\ttest_cmp expect_name actual_name &&\n+\techo \"$GIT_COMMITTER_EMAIL\" >expect_email &&\n+\tgit whoami --committer --email >actual_email &&\n+\ttest_cmp expect_email actual_email\n+'\n+\n+test_expect_success 'git whoami --signing-key when signing is disabled and unset' '\n+\ttest_config commit.gpgsign false &&\n+\ttest_unconfig user.signingkey &&\n+\ttest_must_fail git whoami --signing-key\n+'\n+\n+test_expect_success 'git whoami with explicitly configured signing key' '\n+\ttest_config user.signingkey \"TEST_KEY_123\" &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format ssh &&\n+\tcat >expect <<-EOF &&\n+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n+\tSigning:   TEST_KEY_123 (format: ssh, commit.gpgsign: true)\n+\tEOF\n+\tgit whoami >actual &&\n+\ttest_cmp expect actual &&\n+\techo \"TEST_KEY_123\" >expect_key &&\n+\tgit whoami --signing-key >actual_key &&\n+\ttest_cmp expect_key actual_key\n+'\n+\n+test_expect_success 'git whoami with signing disabled but key configured' '\n+\ttest_config user.signingkey \"TEST_KEY_123\" &&\n+\ttest_config commit.gpgsign false &&\n+\ttest_config gpg.format openpgp &&\n+\tcat >expect <<-EOF &&\n+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n+\tSigning:   disabled (key: TEST_KEY_123, format: openpgp, commit.gpgsign: false)\n+\tEOF\n+\tgit whoami >actual &&\n+\ttest_cmp expect actual &&\n+\techo \"TEST_KEY_123\" >expect_key &&\n+\tgit whoami --signing-key >actual_key &&\n+\ttest_cmp expect_key actual_key\n+'\n+\n+test_expect_success 'git whoami with openpgp signing enabled without explicit key' '\n+\ttest_unconfig user.signingkey &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format openpgp &&\n+\tcat >expect <<-EOF &&\n+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n+\tSigning:   default key ($GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>) (format: openpgp, commit.gpgsign: true)\n+\tEOF\n+\tgit whoami >actual &&\n+\ttest_cmp expect actual &&\n+\techo \"$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\" >expect_key &&\n+\tgit whoami --signing-key >actual_key &&\n+\ttest_cmp expect_key actual_key\n+'\n+\n+test_expect_success 'git whoami with ssh signing enabled without explicit key' '\n+\ttest_unconfig user.signingkey &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format ssh &&\n+\ttest_unconfig gpg.ssh.defaultKeyCommand &&\n+\tcat >expect <<-EOF &&\n+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n+\tSigning:   enabled (no signing key configured)\n+\tEOF\n+\tgit whoami >actual &&\n+\ttest_cmp expect actual &&\n+\ttest_must_fail git whoami --signing-key\n+'\n+\n+test_expect_success GPGSSH 'git whoami with ssh defaultKeyCommand' '\n+\ttest_unconfig user.signingkey &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config gpg.ssh.defaultKeyCommand \"cat \\\"${GPGSSH_KEY_PRIMARY}.pub\\\"\" &&\n+\tgit whoami --signing-key >actual_key &&\n+\ttest_grep \"^SHA256:\" actual_key\n+'\n+\n+test_expect_success 'git whoami -v / --verbose' '\n+\ttest_config user.signingkey \"MY_SIGNING_KEY\" &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format openpgp &&\n+\tcat >expect <<-EOF &&\n+\tAuthor Name:      $GIT_AUTHOR_NAME\n+\tAuthor Email:     $GIT_AUTHOR_EMAIL\n+\tCommitter Name:   $GIT_COMMITTER_NAME\n+\tCommitter Email:  $GIT_COMMITTER_EMAIL\n+\tSigning Key:      MY_SIGNING_KEY\n+\tSigning Format:   openpgp\n+\tGPG Signing:      enabled\n+\tEOF\n+\tgit whoami -v >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git whoami with environment variable overrides' '\n+\ttest_unconfig user.signingkey &&\n+\ttest_config commit.gpgsign false &&\n+\tcat >expect <<-EOF &&\n+\tAuthor:    Custom Author <custom.author@example.com>\n+\tCommitter: Custom Committer <custom.committer@example.com>\n+\tSigning:   disabled (commit.gpgsign: false)\n+\tEOF\n+\tGIT_AUTHOR_NAME=\"Custom Author\" \\\n+\tGIT_AUTHOR_EMAIL=\"custom.author@example.com\" \\\n+\tGIT_COMMITTER_NAME=\"Custom Committer\" \\\n+\tGIT_COMMITTER_EMAIL=\"custom.committer@example.com\" \\\n+\tgit whoami >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'incompatible option combinations fail' '\n+\ttest_must_fail git whoami --author --committer 2>err &&\n+\ttest_grep \"cannot be used together\" err &&\n+\ttest_must_fail git whoami --name --email 2>err &&\n+\ttest_grep \"cannot be used together\" err &&\n+\ttest_must_fail git whoami --signing-key --name 2>err &&\n+\ttest_grep \"cannot be used together\" err &&\n+\ttest_must_fail git whoami --signing-key --email 2>err &&\n+\ttest_grep \"cannot be used together\" err &&\n+\ttest_must_fail git whoami --signing-key --author 2>err &&\n+\ttest_grep \"cannot be used together\" err &&\n+\ttest_must_fail git whoami --signing-key --committer 2>err &&\n+\ttest_grep \"cannot be used together\" err &&\n+\ttest_must_fail git whoami --verbose --name 2>err &&\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n+test_expect_success 'git whoami outside of repository' '\n+\tnongit git whoami --author >actual &&\n+\ttest_grep \"<\" actual\n+'\n+\n+test_done\n\nbase-commit: 593c42fe075be0c8cd5239b3a2f21c610cbc9798\n-- \ngitgitgadget\n"},{"id":"551244","messageId":"ao4IDZMbgvTTYksS@fruit.crustytoothpaste.net","threadId":"66219","inReplyTo":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","subject":"Re: [PATCH] builtin/whoami: add new 'whoami' command","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-08-25T21:24:30Z","receivedAt":"2026-08-25T21:24:38Z","isPatch":true,"body":"On 2026-08-25 at 20:46:42, Andrew Pleeter via GitGitGadget wrote:\n> From: anpl1623 <andrewpleeter@gmail.com>\n\nYou will probably want this to match your real name since you're using\nit in the email.  We prefer that people use their real names, but don't\nrequire it, but I assume that it's not a problem since you just CC'd\nyourself using it (and it's also in your email address).\n\n> Add a builtin 'whoami' command to inspect and display the resolved\n> author and committer identity along with the commit signing\n> configuration (GPG/SSH key ID and commit.gpgsign status) used when\n> creating Git commits.\n> \n> Support optional flags (--author, --committer, --name, --email,\n> --signing-key, and --verbose) for targeted querying and scripting.\n\nI suspect users will want a way to get _all_ of the output in a\nmachine-readable way, so you'd probably want to provide some method of\ndoing that.  Note that because your existing endpoints provide\ntranslated strings, they are not suitable for this.  That doesn't mean\nthat they should not be translated (because they should) but we'd\nprobably want a format like the following:\n\nuser.author.name=A U Thor\nuser.author.email=author@example.com\n\nOther formats are possible, though.\n\nPossibly a `-z` option for NUL-terminated instead of LF-terminated\noutput might be warranted as well unless we're certain that our output\nwill never contain a newline (hint: config options can).\n\n> Include documentation in Documentation/git-whoami.adoc and regression\n> tests in t/t0015-whoami.sh.\n> \n> Signed-off-by: anpl1623 <andrewpleeter@gmail.com>\n\nAgain, you'll want to sign this off with your real name.\n\n>     MOTIVATION\n>     \n>     Users often work across multiple environments, profiles, or repositories\n>     with different global/local configs and signing keys. Currently,\n>     verifying what identity and signing key will be attached to a new commit\n>     requires checking several individual git config and git var settings.\n>     git whoami provides a simple, direct porcelain command to verify this in\n>     one step.\n\nI think this should go in the commit message.  I thought to myself,\n\"Well, there are already ways to get this information, so why add a new\none?\"  Telling us why your patch is compelling and solves an important\npurpose is appropriate for the commit message.\n\nI might also like to see an explanation as to why this wouldn't work\nbetter in `git var` or elsewhere instead, since much of the information\nis already there.  Since that's an alternative you've rejected, tell us\nwhy and sell us on your vision.\n\n> +\trepo_config(the_repository, git_default_config, NULL);\n\nLet's not add more uses of `the_repository`.  Use the `repo` argument to\nthe main function above, taking care to handle the NULL case.\n\n> +\trepo_config_get_bool(the_repository, \"commit.gpgsign\", &gpgsign);\n> +\trepo_config_get_string(the_repository, \"user.signingkey\", &signing_key);\n> +\trepo_config_get_string(the_repository, \"gpg.format\", &gpg_format);\n> +\trepo_config_get_string(the_repository, \"gpg.ssh.defaultkeycommand\", &ssh_default_key_cmd);\n> +\n> +\tis_ssh = gpg_format && !strcmp(gpg_format, \"ssh\");\n> +\n> +\tif (signing_key && *signing_key) {\n> +\t\tresolved_key = xstrdup(signing_key);\n> +\t} else if (is_ssh) {\n> +\t\tif (ssh_default_key_cmd && *ssh_default_key_cmd)\n> +\t\t\tresolved_key = get_signing_key_id();\n> +\t} else if (gpgsign) {\n> +\t\tresolved_key = get_signing_key_id();\n> +\t}\n\nShould this also do something useful for X.509 keys?\n\nOverall, I don't have a strong need for this and I'm fine using the\nexisting functionality.  However, I see how it could be useful and if it\nwere merged and available in the versions of Git I use, then I might\nmake use of it.\n\nPerhaps others think this is compelling, though, so I'm interested to\nhear other opinions about the utility of the command.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"551250","messageId":"xmqq7bldlvxk.fsf@gitster.g","threadId":"66219","inReplyTo":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","subject":"Re: [PATCH] builtin/whoami: add new 'whoami' command","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-25T21:41:43Z","receivedAt":"2026-08-25T21:41:45Z","isPatch":true,"body":"\"Andrew Pleeter via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>     builtin/whoami: add new 'whoami' command\n\nIn general, I would really really want to see us refrain from adding\nany more random subcommands.\n\n> +`-a`::\n> +`--author`::\n> +\tShow author identity.\n> +\n> +`-c`::\n> +`--committer`::\n> +\tShow committer identity.\n\nThis pair hints the users they are equals.\n\nBut the code tells us otherwise:\n\n> +\tif (show_name) {\n> +\t\tif (show_author)\n> +\t\t\tputs(author_name.buf);\n> +\t\telse\n> +\t\t\tputs(committer_name.buf);\n> +\t\tgoto cleanup;\n> +\t}\n> +\n\nSo when \"-n\" is in effect, \"-c\" is completely ignored.  Lack of \"-a\"\nmeans \"-c\" instead.  The same story holds for \"-e\".\n\nYet later in the code that is executed when neither \"-n\" or \"-e\" is\nin effect:\n\n> +\tif (show_author) {\n> +\t\tputs(author_info.buf);\n> +\t\tgoto cleanup;\n> +\t}\n> +\n> +\tif (show_committer) {\n> +\t\tputs(committer_info.buf);\n> +\t\tgoto cleanup;\n> +\t}\n> +\n\nHere, lack of \"-a\" is not sufficient to view committer information\nand you'd explicitly need to pass \"-c\" if you want to view committer\ninformation.\n\nSo confusing.\n\n> +`-n`::\n> +`--name`::\n> +\tShow name only.\n> +\n> +`-e`::\n> +`--email`::\n> +\tShow email only.\n\nWhy not make -a/-c/-n/-e more additive instead?  Something along the\nlines of ...\n\n    $ git ident -a -e -n\n    Andrew Pleeter <andrewpleeter@gmail.com>\n    $ git ident -a -n -v\n    Author: Andrew Pleeter\n    $ git ident -a -c -e\n    <andrewpleeter@gmail.com>\n    <andrewpleeter@gmail.com>\n    $ git ident -a -c -e -v\n    Author: <andrewpleeter@gmail.com>\n    Committer: <andrewpleeter@gmail.com>\n\n"},{"id":"551610","messageId":"pull.2388.v2.git.git.1788220746663.gitgitgadget@gmail.com","threadId":"66219","inReplyTo":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","subject":"[PATCH v2] builtin/ident: add new 'ident' command","fromName":"Andrew Pleeter via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-08-31T23:59:06Z","receivedAt":"2026-08-31T23:59:10Z","isPatch":true,"body":"From: Andrew Pleeter <andrewpleeter@gmail.com>\n\nAdd a builtin 'ident' command to inspect and display the resolved author\nand committer identity along with optional commit signing configuration\nused when creating Git commits.\n\nWhile existing plumbing commands like 'git var' and 'git config' expose\nindividual pieces of identity and configuration, discovering what identity\nand signing key will actually be attached to a new commit requires multiple\nindependent queries and manual correlation. 'git config' only reads raw\nvalues without performing environment overrides or GECOS detection, while\n'git var' returns full ident strings with timestamps without exposing\ncommit signing status.\n\n'git ident' provides a unified command with additive, composable options:\n  - Identity scope selectors (-a / --author, -c / --committer) choose\n    which identities to format (defaulting to both when neither is specified).\n  - Component selectors (-n / --name, -e / --email) choose which parts\n    to format (defaulting to full 'Name <email>' when neither or both are\n    specified).\n  - -v / --verbose prepends 'Author: ' or 'Committer: ' role labels.\n  - -s / --signing-key resolves and outputs the commit signing key.\n  - --porcelain produces machine-readable key-value pairs.\n  - -z / --null terminates output records with NUL bytes.\n\nInclude documentation in Documentation/git-ident.adoc and regression\ntests in t/t0015-ident.sh.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n    builtin/whoami: add new 'whoami' command\n    \n    \n    Title:\n    ======\n    \n    builtin/whoami: add new 'whoami' command\n    \n    ------------------------------------------------------------------------\n    \n    \n    Description:\n    ============\n    \n    SUMMARY\n    \n    Adds a new builtin git whoami command that inspects and displays the\n    user's active author and committer identity along with their commit\n    signing configuration (GPG/SSH key ID and commit.gpgsign status).\n    \n    MOTIVATION\n    \n    Users often work across multiple environments, profiles, or repositories\n    with different global/local configs and signing keys. Currently,\n    verifying what identity and signing key will be attached to a new commit\n    requires checking several individual git config and git var settings.\n    git whoami provides a simple, direct porcelain command to verify this in\n    one step.\n    \n    CHANGES\n    \n     * Core implementation: Added builtin/whoami.c using Git's ident.h and\n       gpg-interface.h APIs.\n     * CLI flags supported:\n       * git whoami (default overview)\n       * -a, --author (author identity: Name <email>)\n       * -c, --committer (committer identity: Name <email>)\n       * -n, --name (name only)\n       * -e, --email (email only)\n       * -s, --signing-key (signing key ID)\n       * -v, --verbose (detailed identity and signing breakdown)\n     * Documentation: Added manpage in Documentation/git-whoami.adoc.\n     * Build integration: Added to Makefile, meson.build, command-list.txt,\n       and builtin.h.\n     * Test suite: Added comprehensive automated tests in t/t0015-whoami.sh.\n    \n    EXAMPLE OUTPUT\n    \n    $ git whoami\n    Author:    Jane Doe <jane@example.com>\n    Committer: Jane Doe <jane@example.com>\n    Signing:   /Users/jane/.ssh/id_ed25519_git (format: ssh, commit.gpgsign: true)\n    \n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v2\nPull-Request: https://github.com/git/git/pull/2388\n\nRange-diff vs v1:\n\n 1:  263aebca08 ! 1:  f322e7fedd builtin/whoami: add new 'whoami' command\n     @@\n       ## Metadata ##\n     -Author: anpl1623 <andrewpleeter@gmail.com>\n     +Author: Andrew Pleeter <andrewpleeter@gmail.com>\n      \n       ## Commit message ##\n     -    builtin/whoami: add new 'whoami' command\n     +    builtin/ident: add new 'ident' command\n      \n     -    Add a builtin 'whoami' command to inspect and display the resolved\n     -    author and committer identity along with the commit signing\n     -    configuration (GPG/SSH key ID and commit.gpgsign status) used when\n     -    creating Git commits.\n     +    Add a builtin 'ident' command to inspect and display the resolved author\n     +    and committer identity along with optional commit signing configuration\n     +    used when creating Git commits.\n      \n     -    Support optional flags (--author, --committer, --name, --email,\n     -    --signing-key, and --verbose) for targeted querying and scripting.\n     +    While existing plumbing commands like 'git var' and 'git config' expose\n     +    individual pieces of identity and configuration, discovering what identity\n     +    and signing key will actually be attached to a new commit requires multiple\n     +    independent queries and manual correlation. 'git config' only reads raw\n     +    values without performing environment overrides or GECOS detection, while\n     +    'git var' returns full ident strings with timestamps without exposing\n     +    commit signing status.\n      \n     -    Include documentation in Documentation/git-whoami.adoc and regression\n     -    tests in t/t0015-whoami.sh.\n     +    'git ident' provides a unified command with additive, composable options:\n     +      - Identity scope selectors (-a / --author, -c / --committer) choose\n     +        which identities to format (defaulting to both when neither is specified).\n     +      - Component selectors (-n / --name, -e / --email) choose which parts\n     +        to format (defaulting to full 'Name <email>' when neither or both are\n     +        specified).\n     +      - -v / --verbose prepends 'Author: ' or 'Committer: ' role labels.\n     +      - -s / --signing-key resolves and outputs the commit signing key.\n     +      - --porcelain produces machine-readable key-value pairs.\n     +      - -z / --null terminates output records with NUL bytes.\n      \n     -    Signed-off-by: anpl1623 <andrewpleeter@gmail.com>\n     +    Include documentation in Documentation/git-ident.adoc and regression\n     +    tests in t/t0015-ident.sh.\n      \n     - ## Documentation/git-whoami.adoc (new) ##\n     +    Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n     +\n     + ## Documentation/git-ident.adoc (new) ##\n      @@\n     -+git-whoami(1)\n     -+=============\n     ++git-ident(1)\n     ++============\n      +\n      +NAME\n      +----\n     -+git-whoami - Show current user identity and commit signing information\n     ++git-ident - Show current user identity and commit signing information\n      +\n      +\n      +SYNOPSIS\n      +--------\n      +[synopsis]\n     -+git whoami [options]\n     ++git ident [<options>]\n      +\n      +DESCRIPTION\n      +-----------\n      +Displays the author and committer identity (name and email address) and\n     -+commit signing configuration that will be used when signing and recording\n     ++optionally commit signing configuration that will be used when recording\n      +Git commits.\n      +\n     ++The identity selectors (`-a` / `--author`, `-c` / `--committer`) and component\n     ++selectors (`-n` / `--name`, `-e` / `--email`) are additive and composable:\n     ++\n     ++* If neither `--author` nor `--committer` is specified, both identities are shown.\n     ++* If neither `--name` nor `--email` is specified (or if both are specified),\n     ++  the full identity format (`Name <email>`) is shown.\n     ++* If `-v` / `--verbose` is specified, entries are labeled with their role\n     ++  (`Author: ` / `Committer: `).\n     ++\n      +OPTIONS\n      +-------\n      +`-a`::\n     @@ Documentation/git-whoami.adoc (new)\n      +\n      +`-e`::\n      +`--email`::\n     -+\tShow email only.\n     ++\tShow email address only (formatted as `<email>`).\n      +\n      +`-s`::\n      +`--signing-key`::\n     -+\tShow commit signing key only.\n     ++\tShow the signing key that Git would resolve for signing commits.\n     ++\tResolution proceeds in the following order:\n     ++\t* The explicitly configured `user.signingKey`, if set.\n     ++\t* If `gpg.format` is set to `ssh` and `gpg.ssh.defaultKeyCommand`\n     ++\t  is configured, executes the command to obtain the public key.\n     ++\t* If `gpg.format` is `openpgp` or `x509` and `commit.gpgsign` is\n     ++\t  enabled, falls back to the committer identity.\n     ++\t* If signing is disabled (`commit.gpgsign=false`) and no explicit key\n     ++\t  is configured, exits with status 1.\n     ++\n     ++`--porcelain`::\n     ++\tGive the output in an easy-to-parse format for scripts.\n     ++\tOutputs key-value pairs (`user.author.name`, `user.author.email`,\n     ++\t`user.committer.name`, `user.committer.email`, `user.signingkey`,\n     ++\t`gpg.format`, `commit.gpgsign`).\n     ++\n     ++`-z`::\n     ++`--null`::\n     ++\tTerminate output lines or entries with NUL instead of a newline.\n      +\n      +`-v`::\n      +`--verbose`::\n     -+\tShow detailed identity and signing status.\n     ++\tPrepend role labels (`Author: `, `Committer: `) to identity entries.\n     ++\n     ++EXAMPLES\n     ++--------\n     ++\n     ++* Show full author and committer identities:\n     +++\n     ++------------\n     ++$ git ident\n     ++Author Name <author@example.com>\n     ++Committer Name <committer@example.com>\n     ++------------\n     ++\n     ++* Show author identity only:\n     +++\n     ++------------\n     ++$ git ident -a -e -n\n     ++Author Name <author@example.com>\n     ++------------\n     ++\n     ++* Show labeled author name:\n     +++\n     ++------------\n     ++$ git ident -a -n -v\n     ++Author: Author Name\n     ++------------\n     ++\n     ++* Show author and committer email addresses:\n     +++\n     ++------------\n     ++$ git ident -a -c -e\n     ++<author@example.com>\n     ++<committer@example.com>\n     ++------------\n     ++\n     ++* Show labeled author and committer email addresses:\n     +++\n     ++------------\n     ++$ git ident -a -c -e -v\n     ++Author: <author@example.com>\n     ++Committer: <committer@example.com>\n     ++------------\n      +\n      +GIT\n      +---\n     @@ Documentation/git-whoami.adoc (new)\n      \n       ## Documentation/meson.build ##\n      @@ Documentation/meson.build: manpages = {\n     -   'git-verify-tag.adoc' : 1,\n     -   'git-version.adoc' : 1,\n     -   'git-web--browse.adoc' : 1,\n     -+  'git-whoami.adoc' : 1,\n     -   'git-worktree.adoc' : 1,\n     -   'git-write-tree.adoc' : 1,\n     -   'git.adoc' : 1,\n     +   'git-help.adoc' : 1,\n     +   'git-history.adoc' : 1,\n     +   'git-hook.adoc' : 1,\n     ++  'git-ident.adoc' : 1,\n     +   'git-http-backend.adoc' : 1,\n     +   'git-http-fetch.adoc' : 1,\n     +   'git-http-push.adoc' : 1,\n      \n       ## Makefile ##\n     -@@ Makefile: BUILTIN_OBJS += builtin/var.o\n     - BUILTIN_OBJS += builtin/verify-commit.o\n     - BUILTIN_OBJS += builtin/verify-pack.o\n     - BUILTIN_OBJS += builtin/verify-tag.o\n     -+BUILTIN_OBJS += builtin/whoami.o\n     - BUILTIN_OBJS += builtin/worktree.o\n     - BUILTIN_OBJS += builtin/write-tree.o\n     - \n     +@@ Makefile: BUILTIN_OBJS += builtin/hash-object.o\n     + BUILTIN_OBJS += builtin/help.o\n     + BUILTIN_OBJS += builtin/history.o\n     + BUILTIN_OBJS += builtin/hook.o\n     ++BUILTIN_OBJS += builtin/ident.o\n     + BUILTIN_OBJS += builtin/index-pack.o\n     + BUILTIN_OBJS += builtin/init-db.o\n     + BUILTIN_OBJS += builtin/interpret-trailers.o\n      \n       ## builtin.h ##\n     +@@ builtin.h: int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repo\n     + int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n     ++int cmd_ident(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\n     + int cmd_interpret_trailers(int argc, const char **argv, const char *prefix, struct repository *repo);\n      @@ builtin.h: int cmd_verify_pack(int argc, const char **argv, const char *prefix, struct repo\n       int cmd_show_ref(int argc, const char **argv, const char *prefix, struct repository *repo);\n       int cmd_pack_refs(int argc, const char **argv, const char *prefix, struct repository *repo);\n       int cmd_replace(int argc, const char **argv, const char *prefix, struct repository *repo);\n     -+int cmd_whoami(int argc, const char **argv, const char *prefix, struct repository *repo);\n     - \n     +-\n       #endif\n      \n     - ## builtin/whoami.c (new) ##\n     + ## builtin/ident.c (new) ##\n      @@\n      +#define USE_THE_REPOSITORY_VARIABLE\n      +\n     @@ builtin/whoami.c (new)\n      +#include \"parse-options.h\"\n      +#include \"strbuf.h\"\n      +\n     -+static const char * const whoami_usage[] = {\n     -+\tN_(\"git whoami [options]\"),\n     ++static const char * const ident_usage[] = {\n     ++\tN_(\"git ident [<options>]\"),\n      +\tNULL\n      +};\n      +\n     -+int cmd_whoami(int argc,\n     -+\t       const char **argv,\n     -+\t       const char *prefix,\n     -+\t       struct repository *repo UNUSED)\n     ++enum ident_who {\n     ++\tIDENT_AUTHOR = 1 << 0,\n     ++\tIDENT_COMMITTER = 1 << 1,\n     ++};\n     ++\n     ++enum ident_part {\n     ++\tIDENT_NAME = 1 << 0,\n     ++\tIDENT_EMAIL = 1 << 1,\n     ++};\n     ++\n     ++struct ident_config {\n     ++\tint gpgsign;\n     ++\tchar *signing_key;\n     ++\tchar *gpg_format;\n     ++\tchar *ssh_default_key_cmd;\n     ++};\n     ++\n     ++static int ident_config_cb(const char *var, const char *value,\n     ++\t\t\t   const struct config_context *ctx, void *data)\n     ++{\n     ++\tstruct ident_config *cfg = data;\n     ++\n     ++\tif (!strcmp(var, \"commit.gpgsign\")) {\n     ++\t\tcfg->gpgsign = git_config_bool(var, value);\n     ++\t\treturn 0;\n     ++\t}\n     ++\tif (!strcmp(var, \"user.signingkey\"))\n     ++\t\treturn git_config_string(&cfg->signing_key, var, value);\n     ++\tif (!strcmp(var, \"gpg.format\"))\n     ++\t\treturn git_config_string(&cfg->gpg_format, var, value);\n     ++\tif (!strcmp(var, \"gpg.ssh.defaultkeycommand\"))\n     ++\t\treturn git_config_string(&cfg->ssh_default_key_cmd, var, value);\n     ++\n     ++\treturn git_default_config(var, value, ctx, data);\n     ++}\n     ++\n     ++static void print_ident_entry(const char *label,\n     ++\t\t\t      const struct strbuf *name,\n     ++\t\t\t      const struct strbuf *email,\n     ++\t\t\t      int parts,\n     ++\t\t\t      int verbose,\n     ++\t\t\t      char eol)\n     ++{\n     ++\tstruct strbuf out = STRBUF_INIT;\n     ++\n     ++\tif (verbose && label)\n     ++\t\tstrbuf_addf(&out, \"%s: \", label);\n     ++\n     ++\tif ((parts & IDENT_NAME) && (parts & IDENT_EMAIL))\n     ++\t\tstrbuf_addf(&out, \"%s <%s>\", name->buf, email->buf);\n     ++\telse if (parts & IDENT_NAME)\n     ++\t\tstrbuf_addbuf(&out, name);\n     ++\telse if (parts & IDENT_EMAIL)\n     ++\t\tstrbuf_addf(&out, \"<%s>\", email->buf);\n     ++\n     ++\tprintf(\"%s%c\", out.buf, eol);\n     ++\tstrbuf_release(&out);\n     ++}\n     ++\n     ++int cmd_ident(int argc,\n     ++\t      const char **argv,\n     ++\t      const char *prefix,\n     ++\t      struct repository *repo)\n      +{\n      +\tint show_author = 0;\n      +\tint show_committer = 0;\n      +\tint show_name = 0;\n      +\tint show_email = 0;\n      +\tint show_signing_key = 0;\n     ++\tint porcelain = 0;\n     ++\tint nul_term = 0;\n      +\tint verbose = 0;\n      +\tint ret = 0;\n     ++\tchar eol;\n     ++\tint selected_who = 0;\n     ++\tint selected_parts = 0;\n      +\n     -+\tstruct option whoami_options[] = {\n     ++\tstruct option ident_options[] = {\n      +\t\tOPT_BOOL('a', \"author\", &show_author, N_(\"show author identity\")),\n      +\t\tOPT_BOOL('c', \"committer\", &show_committer, N_(\"show committer identity\")),\n      +\t\tOPT_BOOL('n', \"name\", &show_name, N_(\"show name only\")),\n      +\t\tOPT_BOOL('e', \"email\", &show_email, N_(\"show email only\")),\n      +\t\tOPT_BOOL('s', \"signing-key\", &show_signing_key, N_(\"show commit signing key\")),\n     -+\t\tOPT__VERBOSE(&verbose, N_(\"show detailed identity and signing status\")),\n     ++\t\tOPT_BOOL(0, \"porcelain\", &porcelain, N_(\"machine-readable output\")),\n     ++\t\tOPT_BOOL('z', \"null\", &nul_term, N_(\"terminate entries with NUL\")),\n     ++\t\tOPT__VERBOSE(&verbose, N_(\"show detailed identity\")),\n      +\t\tOPT_END()\n      +\t};\n      +\n     ++\tstruct ident_config cfg = { 0 };\n      +\tstruct strbuf author_info = STRBUF_INIT;\n      +\tstruct strbuf committer_info = STRBUF_INIT;\n      +\tstruct ident_split author_split, committer_split;\n     @@ builtin/whoami.c (new)\n      +\tstruct strbuf author_email = STRBUF_INIT;\n      +\tstruct strbuf committer_name = STRBUF_INIT;\n      +\tstruct strbuf committer_email = STRBUF_INIT;\n     -+\n     -+\tchar *signing_key = NULL;\n     -+\tchar *gpg_format = NULL;\n     -+\tchar *ssh_default_key_cmd = NULL;\n      +\tchar *resolved_key = NULL;\n     -+\tint gpgsign = 0;\n      +\tint is_ssh = 0;\n      +\n     -+\targc = parse_options(argc, argv, prefix, whoami_options,\n     -+\t\t\t     whoami_usage, 0);\n     ++\targc = parse_options(argc, argv, prefix, ident_options,\n     ++\t\t\t     ident_usage, 0);\n      +\n      +\tif (argc > 0)\n     -+\t\tusage_with_options(whoami_usage, whoami_options);\n     -+\n     -+\tdie_for_incompatible_opt2(show_author, \"--author\", show_committer, \"--committer\");\n     -+\tdie_for_incompatible_opt2(show_name, \"--name\", show_email, \"--email\");\n     -+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", show_name, \"--name\");\n     -+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", show_email, \"--email\");\n     -+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", show_author, \"--author\");\n     -+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", show_committer, \"--committer\");\n     -+\tdie_for_incompatible_opt2(show_signing_key, \"--signing-key\", verbose, \"--verbose\");\n     -+\tdie_for_incompatible_opt2(verbose, \"--verbose\", show_name, \"--name\");\n     -+\tdie_for_incompatible_opt2(verbose, \"--verbose\", show_email, \"--email\");\n     -+\tdie_for_incompatible_opt2(verbose, \"--verbose\", show_author, \"--author\");\n     -+\tdie_for_incompatible_opt2(verbose, \"--verbose\", show_committer, \"--committer\");\n     -+\n     -+\trepo_config(the_repository, git_default_config, NULL);\n     ++\t\tusage_with_options(ident_usage, ident_options);\n     ++\n     ++\tdie_for_incompatible_opt2(porcelain, \"--porcelain\", verbose, \"--verbose\");\n     ++\tdie_for_incompatible_opt2(porcelain, \"--porcelain\", show_name, \"--name\");\n     ++\tdie_for_incompatible_opt2(porcelain, \"--porcelain\", show_email, \"--email\");\n     ++\n     ++\teol = nul_term ? '\\0' : '\\n';\n     ++\n     ++\trepo_config(repo, ident_config_cb, &cfg);\n      +\n      +\tstrbuf_addstr(&author_info, git_author_info(IDENT_NO_DATE));\n      +\tstrbuf_addstr(&committer_info, git_committer_info(IDENT_NO_DATE));\n     @@ builtin/whoami.c (new)\n      +\t\t\t\t   committer_split.mail_end - committer_split.mail_begin);\n      +\t}\n      +\n     -+\trepo_config_get_bool(the_repository, \"commit.gpgsign\", &gpgsign);\n     -+\trepo_config_get_string(the_repository, \"user.signingkey\", &signing_key);\n     -+\trepo_config_get_string(the_repository, \"gpg.format\", &gpg_format);\n     -+\trepo_config_get_string(the_repository, \"gpg.ssh.defaultkeycommand\", &ssh_default_key_cmd);\n     ++\tis_ssh = cfg.gpg_format && !strcmp(cfg.gpg_format, \"ssh\");\n      +\n     -+\tis_ssh = gpg_format && !strcmp(gpg_format, \"ssh\");\n     -+\n     -+\tif (signing_key && *signing_key) {\n     -+\t\tresolved_key = xstrdup(signing_key);\n     ++\tif (cfg.signing_key && *cfg.signing_key) {\n     ++\t\tresolved_key = xstrdup(cfg.signing_key);\n      +\t} else if (is_ssh) {\n     -+\t\tif (ssh_default_key_cmd && *ssh_default_key_cmd)\n     ++\t\tif (cfg.ssh_default_key_cmd && *cfg.ssh_default_key_cmd)\n      +\t\t\tresolved_key = get_signing_key_id();\n     -+\t} else if (gpgsign) {\n     ++\t} else if (cfg.gpgsign) {\n      +\t\tresolved_key = get_signing_key_id();\n      +\t}\n      +\n     -+\tif (show_signing_key) {\n     ++\tif (show_signing_key && !show_author && !show_committer && !show_name && !show_email && !porcelain) {\n      +\t\tif (resolved_key && *resolved_key) {\n     -+\t\t\tputs(resolved_key);\n     ++\t\t\tif (verbose)\n     ++\t\t\t\tprintf(_(\"Signing Key: %s\\n\"), resolved_key);\n     ++\t\t\telse\n     ++\t\t\t\tprintf(\"%s%c\", resolved_key, eol);\n      +\t\t\tret = 0;\n      +\t\t} else {\n      +\t\t\tret = 1;\n     @@ builtin/whoami.c (new)\n      +\t\tgoto cleanup;\n      +\t}\n      +\n     -+\tif (show_name) {\n     -+\t\tif (show_author)\n     -+\t\t\tputs(author_name.buf);\n     -+\t\telse\n     -+\t\t\tputs(committer_name.buf);\n     -+\t\tgoto cleanup;\n     -+\t}\n     -+\n     -+\tif (show_email) {\n     -+\t\tif (show_author)\n     -+\t\t\tputs(author_email.buf);\n     -+\t\telse\n     -+\t\t\tputs(committer_email.buf);\n     ++\tif (show_author)\n     ++\t\tselected_who |= IDENT_AUTHOR;\n     ++\tif (show_committer)\n     ++\t\tselected_who |= IDENT_COMMITTER;\n     ++\tif (!selected_who)\n     ++\t\tselected_who = IDENT_AUTHOR | IDENT_COMMITTER;\n     ++\n     ++\tif (show_name)\n     ++\t\tselected_parts |= IDENT_NAME;\n     ++\tif (show_email)\n     ++\t\tselected_parts |= IDENT_EMAIL;\n     ++\tif (!selected_parts)\n     ++\t\tselected_parts = IDENT_NAME | IDENT_EMAIL;\n     ++\n     ++\tif (porcelain) {\n     ++\t\tif (selected_who & IDENT_AUTHOR) {\n     ++\t\t\tprintf(\"user.author.name=%s%c\", author_name.buf, eol);\n     ++\t\t\tprintf(\"user.author.email=%s%c\", author_email.buf, eol);\n     ++\t\t}\n     ++\t\tif (selected_who & IDENT_COMMITTER) {\n     ++\t\t\tprintf(\"user.committer.name=%s%c\", committer_name.buf, eol);\n     ++\t\t\tprintf(\"user.committer.email=%s%c\", committer_email.buf, eol);\n     ++\t\t}\n     ++\t\tif ((selected_who & (IDENT_AUTHOR | IDENT_COMMITTER)) == (IDENT_AUTHOR | IDENT_COMMITTER)) {\n     ++\t\t\tprintf(\"user.signingkey=%s%c\",\n     ++\t\t\t       (cfg.signing_key && *cfg.signing_key) ? cfg.signing_key :\n     ++\t\t\t       (resolved_key && *resolved_key) ? resolved_key : \"none\",\n     ++\t\t\t       eol);\n     ++\t\t\tprintf(\"gpg.format=%s%c\",\n     ++\t\t\t       cfg.gpg_format ? cfg.gpg_format : \"openpgp\", eol);\n     ++\t\t\tprintf(\"commit.gpgsign=%s%c\",\n     ++\t\t\t       cfg.gpgsign ? \"true\" : \"false\", eol);\n     ++\t\t}\n      +\t\tgoto cleanup;\n      +\t}\n      +\n     -+\tif (show_author) {\n     -+\t\tputs(author_info.buf);\n     -+\t\tgoto cleanup;\n     -+\t}\n     ++\tif (selected_who & IDENT_AUTHOR)\n     ++\t\tprint_ident_entry(\"Author\", &author_name, &author_email,\n     ++\t\t\t\t  selected_parts, verbose, eol);\n      +\n     -+\tif (show_committer) {\n     -+\t\tputs(committer_info.buf);\n     -+\t\tgoto cleanup;\n     -+\t}\n     ++\tif (selected_who & IDENT_COMMITTER)\n     ++\t\tprint_ident_entry(\"Committer\", &committer_name, &committer_email,\n     ++\t\t\t\t  selected_parts, verbose, eol);\n      +\n     -+\tif (verbose) {\n     -+\t\tprintf(_(\"Author Name:      %s\\n\"), author_name.buf);\n     -+\t\tprintf(_(\"Author Email:     %s\\n\"), author_email.buf);\n     -+\t\tprintf(_(\"Committer Name:   %s\\n\"), committer_name.buf);\n     -+\t\tprintf(_(\"Committer Email:  %s\\n\"), committer_email.buf);\n     -+\t\tif (signing_key && *signing_key)\n     -+\t\t\tprintf(_(\"Signing Key:      %s\\n\"), signing_key);\n     -+\t\telse if (resolved_key && *resolved_key)\n     -+\t\t\tprintf(_(\"Signing Key:      %s (default fallback)\\n\"), resolved_key);\n     -+\t\telse\n     -+\t\t\tprintf(_(\"Signing Key:      %s\\n\"), _(\"none\"));\n     -+\t\tprintf(_(\"Signing Format:   %s\\n\"),\n     -+\t\t       gpg_format ? gpg_format : \"openpgp\");\n     -+\t\tprintf(_(\"GPG Signing:      %s\\n\"),\n     -+\t\t       gpgsign ? _(\"enabled\") : _(\"disabled\"));\n     -+\t} else {\n     -+\t\tprintf(_(\"Author:    %s\\n\"), author_info.buf);\n     -+\t\tprintf(_(\"Committer: %s\\n\"), committer_info.buf);\n     -+\t\tif (gpgsign) {\n     -+\t\t\tif (signing_key && *signing_key) {\n     -+\t\t\t\tprintf(_(\"Signing:   %s (format: %s, commit.gpgsign: true)\\n\"),\n     -+\t\t\t\t       signing_key,\n     -+\t\t\t\t       gpg_format ? gpg_format : \"openpgp\");\n     -+\t\t\t} else if (resolved_key && *resolved_key) {\n     -+\t\t\t\tprintf(_(\"Signing:   default key (%s) (format: %s, commit.gpgsign: true)\\n\"),\n     -+\t\t\t\t       resolved_key,\n     -+\t\t\t\t       gpg_format ? gpg_format : \"openpgp\");\n     -+\t\t\t} else {\n     -+\t\t\t\tprintf(_(\"Signing:   enabled (no signing key configured)\\n\"));\n     -+\t\t\t}\n     ++\tif (show_signing_key) {\n     ++\t\tif (resolved_key && *resolved_key) {\n     ++\t\t\tif (verbose)\n     ++\t\t\t\tprintf(_(\"Signing Key: %s\\n\"), resolved_key);\n     ++\t\t\telse\n     ++\t\t\t\tprintf(\"%s%c\", resolved_key, eol);\n      +\t\t} else {\n     -+\t\t\tif (signing_key && *signing_key) {\n     -+\t\t\t\tprintf(_(\"Signing:   disabled (key: %s, format: %s, commit.gpgsign: false)\\n\"),\n     -+\t\t\t\t       signing_key,\n     -+\t\t\t\t       gpg_format ? gpg_format : \"openpgp\");\n     -+\t\t\t} else {\n     -+\t\t\t\tprintf(_(\"Signing:   disabled (commit.gpgsign: false)\\n\"));\n     -+\t\t\t}\n     ++\t\t\tret = 1;\n      +\t\t}\n      +\t}\n      +\n      +cleanup:\n     -+\tfree(signing_key);\n     -+\tfree(gpg_format);\n     -+\tfree(ssh_default_key_cmd);\n     ++\tfree(cfg.signing_key);\n     ++\tfree(cfg.gpg_format);\n     ++\tfree(cfg.ssh_default_key_cmd);\n      +\tfree(resolved_key);\n      +\tstrbuf_release(&author_info);\n      +\tstrbuf_release(&committer_info);\n     @@ builtin/whoami.c (new)\n      +}\n      \n       ## command-list.txt ##\n     -@@ command-list.txt: git-verify-pack                         plumbinginterrogators\n     - git-verify-tag                          ancillaryinterrogators\n     - git-version                             ancillaryinterrogators\n     - git-whatchanged                         ancillaryinterrogators          complete\n     -+git-whoami                              ancillaryinterrogators\n     - git-worktree                            mainporcelain\n     - git-write-tree                          plumbingmanipulators\n     - gitattributes                           userinterfaces\n     +@@ command-list.txt: git-hash-object                         plumbingmanipulators\n     + git-help                                ancillaryinterrogators          complete\n     + git-history                             mainporcelain           history\n     + git-hook                                purehelpers\n     ++git-ident                               ancillaryinterrogators\n     + git-http-backend                        synchingrepositories\n     + git-http-fetch                          synchelpers\n     + git-http-push                           synchelpers\n      \n       ## git.c ##\n      @@ git.c: static struct cmd_struct commands[] = {\n     - #ifndef WITH_BREAKING_CHANGES\n     - \t{ \"whatchanged\", cmd_whatchanged, RUN_SETUP | DEPRECATED },\n     - #endif\n     -+\t{ \"whoami\", cmd_whoami, RUN_SETUP_GENTLY },\n     - \t{ \"worktree\", cmd_worktree, RUN_SETUP },\n     - \t{ \"write-tree\", cmd_write_tree, RUN_SETUP },\n     - };\n     + \t{ \"help\", cmd_help },\n     + \t{ \"history\", cmd_history, RUN_SETUP },\n     + \t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n     ++\t{ \"ident\", cmd_ident, RUN_SETUP_GENTLY },\n     + \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n     + \t{ \"init\", cmd_init_db },\n     + \t{ \"init-db\", cmd_init_db },\n      \n       ## meson.build ##\n      @@ meson.build: builtin_sources = [\n     -   'builtin/verify-commit.c',\n     -   'builtin/verify-pack.c',\n     -   'builtin/verify-tag.c',\n     -+  'builtin/whoami.c',\n     -   'builtin/worktree.c',\n     -   'builtin/write-tree.c',\n     - ]\n     +   'builtin/help.c',\n     +   'builtin/history.c',\n     +   'builtin/hook.c',\n     ++  'builtin/ident.c',\n     +   'builtin/index-pack.c',\n     +   'builtin/init-db.c',\n     +   'builtin/interpret-trailers.c',\n      \n       ## t/meson.build ##\n      @@ t/meson.build: integration_tests = [\n         't0012-help.sh',\n         't0013-sha1dc.sh',\n         't0014-alias.sh',\n     -+  't0015-whoami.sh',\n     ++  't0015-ident.sh',\n         't0017-env-helper.sh',\n         't0018-advice.sh',\n         't0019-json-writer.sh',\n      \n     - ## t/t0015-whoami.sh (new) ##\n     + ## t/t0015-ident.sh (new) ##\n      @@\n      +#!/bin/sh\n      +\n     -+test_description='basic sanity checks for git whoami'\n     ++test_description='basic sanity checks for git ident'\n      +\n      +. ./test-lib.sh\n      +. \"$TEST_DIRECTORY/lib-gpg.sh\"\n      +\n     -+test_expect_success 'default output format without signing' '\n     ++test_expect_success 'default output format (author + committer)' '\n     ++\tcat >expect <<-EOF &&\n     ++\t$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n     ++\t$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n     ++\tEOF\n     ++\tgit ident >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'git ident -v / --verbose default' '\n      +\tcat >expect <<-EOF &&\n     -+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n     ++\tAuthor: $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n      +\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n     -+\tSigning:   disabled (commit.gpgsign: false)\n      +\tEOF\n     -+\tgit whoami >actual &&\n     ++\tgit ident -v >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'git whoami --author' '\n     ++test_expect_success 'git ident --author / -a' '\n      +\techo \"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\" >expect &&\n     -+\tgit whoami --author >actual &&\n     -+\ttest_cmp expect actual\n     ++\tgit ident --author >actual &&\n     ++\ttest_cmp expect actual &&\n     ++\tgit ident -a >actual_short &&\n     ++\ttest_cmp expect actual_short\n      +'\n      +\n     -+test_expect_success 'git whoami --committer' '\n     ++test_expect_success 'git ident --committer / -c' '\n      +\techo \"$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\" >expect &&\n     -+\tgit whoami --committer >actual &&\n     ++\tgit ident --committer >actual &&\n     ++\ttest_cmp expect actual &&\n     ++\tgit ident -c >actual_short &&\n     ++\ttest_cmp expect actual_short\n     ++'\n     ++\n     ++test_expect_success 'git ident -a -c' '\n     ++\tcat >expect <<-EOF &&\n     ++\t$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n     ++\t$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n     ++\tEOF\n     ++\tgit ident -a -c >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'git whoami --author --name and --author --email' '\n     ++test_expect_success 'git ident -a -n and -a -e' '\n      +\techo \"$GIT_AUTHOR_NAME\" >expect_name &&\n     -+\tgit whoami --author --name >actual_name &&\n     ++\tgit ident -a -n >actual_name &&\n      +\ttest_cmp expect_name actual_name &&\n     -+\techo \"$GIT_AUTHOR_EMAIL\" >expect_email &&\n     -+\tgit whoami --author --email >actual_email &&\n     ++\techo \"<$GIT_AUTHOR_EMAIL>\" >expect_email &&\n     ++\tgit ident -a -e >actual_email &&\n      +\ttest_cmp expect_email actual_email\n      +'\n      +\n     -+test_expect_success 'git whoami --committer --name and --committer --email' '\n     ++test_expect_success 'git ident -c -n and -c -e' '\n      +\techo \"$GIT_COMMITTER_NAME\" >expect_name &&\n     -+\tgit whoami --committer --name >actual_name &&\n     ++\tgit ident -c -n >actual_name &&\n      +\ttest_cmp expect_name actual_name &&\n     -+\techo \"$GIT_COMMITTER_EMAIL\" >expect_email &&\n     -+\tgit whoami --committer --email >actual_email &&\n     ++\techo \"<$GIT_COMMITTER_EMAIL>\" >expect_email &&\n     ++\tgit ident -c -e >actual_email &&\n      +\ttest_cmp expect_email actual_email\n      +'\n      +\n     -+test_expect_success 'git whoami --signing-key when signing is disabled and unset' '\n     -+\ttest_config commit.gpgsign false &&\n     -+\ttest_unconfig user.signingkey &&\n     -+\ttest_must_fail git whoami --signing-key\n     ++test_expect_success 'git ident -a -e -n (additive full ident)' '\n     ++\techo \"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\" >expect &&\n     ++\tgit ident -a -e -n >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'git ident -a -n -v (labeled name)' '\n     ++\techo \"Author: $GIT_AUTHOR_NAME\" >expect &&\n     ++\tgit ident -a -n -v >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'git ident -a -e -v (labeled email)' '\n     ++\techo \"Author: <$GIT_AUTHOR_EMAIL>\" >expect &&\n     ++\tgit ident -a -e -v >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'git ident -a -c -e (unlabeled emails)' '\n     ++\tcat >expect <<-EOF &&\n     ++\t<$GIT_AUTHOR_EMAIL>\n     ++\t<$GIT_COMMITTER_EMAIL>\n     ++\tEOF\n     ++\tgit ident -a -c -e >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'git ident -a -c -e -v (labeled emails)' '\n     ++\tcat >expect <<-EOF &&\n     ++\tAuthor: <$GIT_AUTHOR_EMAIL>\n     ++\tCommitter: <$GIT_COMMITTER_EMAIL>\n     ++\tEOF\n     ++\tgit ident -a -c -e -v >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'git whoami with explicitly configured signing key' '\n     ++test_expect_success 'git ident -a -c -n -v (labeled names)' '\n     ++\tcat >expect <<-EOF &&\n     ++\tAuthor: $GIT_AUTHOR_NAME\n     ++\tCommitter: $GIT_COMMITTER_NAME\n     ++\tEOF\n     ++\tgit ident -a -c -n -v >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'git ident with -z on single fields' '\n     ++\tprintf \"$GIT_AUTHOR_NAME\\0\" >expect_name &&\n     ++\tgit ident -a -n -z >actual_name &&\n     ++\ttest_cmp expect_name actual_name &&\n     ++\tprintf \"<$GIT_AUTHOR_EMAIL>\\0\" >expect_email &&\n     ++\tgit ident -a -e -z >actual_email &&\n     ++\ttest_cmp expect_email actual_email\n     ++'\n     ++\n     ++test_expect_success 'git ident with -z on multiple entries' '\n     ++\tprintf \"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\\0$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\\0\" >expect &&\n     ++\tgit ident -z >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'git ident --porcelain format' '\n      +\ttest_config user.signingkey \"TEST_KEY_123\" &&\n      +\ttest_config commit.gpgsign true &&\n      +\ttest_config gpg.format ssh &&\n      +\tcat >expect <<-EOF &&\n     -+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n     -+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n     -+\tSigning:   TEST_KEY_123 (format: ssh, commit.gpgsign: true)\n     ++\tuser.author.name=$GIT_AUTHOR_NAME\n     ++\tuser.author.email=$GIT_AUTHOR_EMAIL\n     ++\tuser.committer.name=$GIT_COMMITTER_NAME\n     ++\tuser.committer.email=$GIT_COMMITTER_EMAIL\n     ++\tuser.signingkey=TEST_KEY_123\n     ++\tgpg.format=ssh\n     ++\tcommit.gpgsign=true\n      +\tEOF\n     -+\tgit whoami >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\techo \"TEST_KEY_123\" >expect_key &&\n     -+\tgit whoami --signing-key >actual_key &&\n     -+\ttest_cmp expect_key actual_key\n     ++\tgit ident --porcelain >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'git whoami with signing disabled but key configured' '\n     -+\ttest_config user.signingkey \"TEST_KEY_123\" &&\n     -+\ttest_config commit.gpgsign false &&\n     -+\ttest_config gpg.format openpgp &&\n     ++test_expect_success 'git ident -a --porcelain format' '\n      +\tcat >expect <<-EOF &&\n     -+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n     -+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n     -+\tSigning:   disabled (key: TEST_KEY_123, format: openpgp, commit.gpgsign: false)\n     ++\tuser.author.name=$GIT_AUTHOR_NAME\n     ++\tuser.author.email=$GIT_AUTHOR_EMAIL\n      +\tEOF\n     -+\tgit whoami >actual &&\n     -+\ttest_cmp expect actual &&\n     ++\tgit ident -a --porcelain >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'git ident --porcelain -z format' '\n     ++\ttest_config user.signingkey \"TEST_KEY_123\" &&\n     ++\ttest_config commit.gpgsign true &&\n     ++\ttest_config gpg.format ssh &&\n     ++\tprintf \"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 &&\n     ++\tgit ident --porcelain -z >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'git ident --signing-key when signing is disabled and unset' '\n     ++\ttest_config commit.gpgsign false &&\n     ++\ttest_unconfig user.signingkey &&\n     ++\ttest_must_fail git ident --signing-key\n     ++'\n     ++\n     ++test_expect_success 'git ident with explicitly configured signing key' '\n     ++\ttest_config user.signingkey \"TEST_KEY_123\" &&\n     ++\ttest_config commit.gpgsign true &&\n     ++\ttest_config gpg.format ssh &&\n      +\techo \"TEST_KEY_123\" >expect_key &&\n     -+\tgit whoami --signing-key >actual_key &&\n     ++\tgit ident --signing-key >actual_key &&\n      +\ttest_cmp expect_key actual_key\n      +'\n      +\n     -+test_expect_success 'git whoami with openpgp signing enabled without explicit key' '\n     ++test_expect_success 'git ident with openpgp signing enabled without explicit key' '\n      +\ttest_unconfig user.signingkey &&\n      +\ttest_config commit.gpgsign true &&\n      +\ttest_config gpg.format openpgp &&\n     -+\tcat >expect <<-EOF &&\n     -+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n     -+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n     -+\tSigning:   default key ($GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>) (format: openpgp, commit.gpgsign: true)\n     -+\tEOF\n     -+\tgit whoami >actual &&\n     -+\ttest_cmp expect actual &&\n      +\techo \"$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\" >expect_key &&\n     -+\tgit whoami --signing-key >actual_key &&\n     ++\tgit ident --signing-key >actual_key &&\n      +\ttest_cmp expect_key actual_key\n      +'\n      +\n     -+test_expect_success 'git whoami with ssh signing enabled without explicit key' '\n     ++test_expect_success 'git ident with ssh signing enabled without explicit key' '\n      +\ttest_unconfig user.signingkey &&\n      +\ttest_config commit.gpgsign true &&\n      +\ttest_config gpg.format ssh &&\n      +\ttest_unconfig gpg.ssh.defaultKeyCommand &&\n     -+\tcat >expect <<-EOF &&\n     -+\tAuthor:    $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n     -+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n     -+\tSigning:   enabled (no signing key configured)\n     -+\tEOF\n     -+\tgit whoami >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\ttest_must_fail git whoami --signing-key\n     ++\ttest_must_fail git ident --signing-key\n      +'\n      +\n     -+test_expect_success GPGSSH 'git whoami with ssh defaultKeyCommand' '\n     ++test_expect_success GPGSSH 'git ident with ssh defaultKeyCommand' '\n      +\ttest_unconfig user.signingkey &&\n      +\ttest_config commit.gpgsign true &&\n      +\ttest_config gpg.format ssh &&\n      +\ttest_config gpg.ssh.defaultKeyCommand \"cat \\\"${GPGSSH_KEY_PRIMARY}.pub\\\"\" &&\n     -+\tgit whoami --signing-key >actual_key &&\n     ++\tgit ident --signing-key >actual_key &&\n      +\ttest_grep \"^SHA256:\" actual_key\n      +'\n      +\n     -+test_expect_success 'git whoami -v / --verbose' '\n     -+\ttest_config user.signingkey \"MY_SIGNING_KEY\" &&\n     -+\ttest_config commit.gpgsign true &&\n     -+\ttest_config gpg.format openpgp &&\n     -+\tcat >expect <<-EOF &&\n     -+\tAuthor Name:      $GIT_AUTHOR_NAME\n     -+\tAuthor Email:     $GIT_AUTHOR_EMAIL\n     -+\tCommitter Name:   $GIT_COMMITTER_NAME\n     -+\tCommitter Email:  $GIT_COMMITTER_EMAIL\n     -+\tSigning Key:      MY_SIGNING_KEY\n     -+\tSigning Format:   openpgp\n     -+\tGPG Signing:      enabled\n     -+\tEOF\n     -+\tgit whoami -v >actual &&\n     -+\ttest_cmp expect actual\n     -+'\n     -+\n     -+test_expect_success 'git whoami with environment variable overrides' '\n     ++test_expect_success 'git ident with environment variable overrides' '\n      +\ttest_unconfig user.signingkey &&\n      +\ttest_config commit.gpgsign false &&\n      +\tcat >expect <<-EOF &&\n     -+\tAuthor:    Custom Author <custom.author@example.com>\n     -+\tCommitter: Custom Committer <custom.committer@example.com>\n     -+\tSigning:   disabled (commit.gpgsign: false)\n     ++\tCustom Author <custom.author@example.com>\n     ++\tCustom Committer <custom.committer@example.com>\n      +\tEOF\n      +\tGIT_AUTHOR_NAME=\"Custom Author\" \\\n      +\tGIT_AUTHOR_EMAIL=\"custom.author@example.com\" \\\n      +\tGIT_COMMITTER_NAME=\"Custom Committer\" \\\n      +\tGIT_COMMITTER_EMAIL=\"custom.committer@example.com\" \\\n     -+\tgit whoami >actual &&\n     ++\tgit ident >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'incompatible option combinations fail' '\n     -+\ttest_must_fail git whoami --author --committer 2>err &&\n     -+\ttest_grep \"cannot be used together\" err &&\n     -+\ttest_must_fail git whoami --name --email 2>err &&\n     -+\ttest_grep \"cannot be used together\" err &&\n     -+\ttest_must_fail git whoami --signing-key --name 2>err &&\n     -+\ttest_grep \"cannot be used together\" err &&\n     -+\ttest_must_fail git whoami --signing-key --email 2>err &&\n     -+\ttest_grep \"cannot be used together\" err &&\n     -+\ttest_must_fail git whoami --signing-key --author 2>err &&\n     ++\ttest_must_fail git ident --porcelain --verbose 2>err &&\n      +\ttest_grep \"cannot be used together\" err &&\n     -+\ttest_must_fail git whoami --signing-key --committer 2>err &&\n     ++\ttest_must_fail git ident --porcelain --name 2>err &&\n      +\ttest_grep \"cannot be used together\" err &&\n     -+\ttest_must_fail git whoami --verbose --name 2>err &&\n     ++\ttest_must_fail git ident --porcelain --email 2>err &&\n      +\ttest_grep \"cannot be used together\" err\n      +'\n      +\n     -+test_expect_success 'git whoami outside of repository' '\n     -+\tnongit git whoami --author >actual &&\n     ++test_expect_success 'git ident outside of repository' '\n     ++\tnongit git ident --author >actual &&\n      +\ttest_grep \"<\" actual\n      +'\n      +\n\n\n Documentation/git-ident.adoc | 116 +++++++++++++++++\n Documentation/meson.build    |   1 +\n Makefile                     |   1 +\n builtin.h                    |   2 +-\n builtin/ident.c              | 243 +++++++++++++++++++++++++++++++++++\n command-list.txt             |   1 +\n git.c                        |   1 +\n meson.build                  |   1 +\n t/meson.build                |   1 +\n t/t0015-ident.sh             | 234 +++++++++++++++++++++++++++++++++\n 10 files changed, 600 insertions(+), 1 deletion(-)\n create mode 100644 Documentation/git-ident.adoc\n create mode 100644 builtin/ident.c\n create mode 100755 t/t0015-ident.sh\n\ndiff --git a/Documentation/git-ident.adoc b/Documentation/git-ident.adoc\nnew file mode 100644\nindex 0000000000..292a361c9b\n--- /dev/null\n+++ b/Documentation/git-ident.adoc\n@@ -0,0 +1,116 @@\n+git-ident(1)\n+============\n+\n+NAME\n+----\n+git-ident - Show current user identity and commit signing information\n+\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git ident [<options>]\n+\n+DESCRIPTION\n+-----------\n+Displays the author and committer identity (name and email address) and\n+optionally commit signing configuration that will be used when recording\n+Git commits.\n+\n+The identity selectors (`-a` / `--author`, `-c` / `--committer`) and component\n+selectors (`-n` / `--name`, `-e` / `--email`) are additive and composable:\n+\n+* If neither `--author` nor `--committer` is specified, both identities are shown.\n+* If neither `--name` nor `--email` is specified (or if both are specified),\n+  the full identity format (`Name <email>`) is shown.\n+* If `-v` / `--verbose` is specified, entries are labeled with their role\n+  (`Author: ` / `Committer: `).\n+\n+OPTIONS\n+-------\n+`-a`::\n+`--author`::\n+\tShow author identity.\n+\n+`-c`::\n+`--committer`::\n+\tShow committer identity.\n+\n+`-n`::\n+`--name`::\n+\tShow name only.\n+\n+`-e`::\n+`--email`::\n+\tShow email address only (formatted as `<email>`).\n+\n+`-s`::\n+`--signing-key`::\n+\tShow the signing key that Git would resolve for signing commits.\n+\tResolution proceeds in the following order:\n+\t* The explicitly configured `user.signingKey`, if set.\n+\t* If `gpg.format` is set to `ssh` and `gpg.ssh.defaultKeyCommand`\n+\t  is configured, executes the command to obtain the public key.\n+\t* If `gpg.format` is `openpgp` or `x509` and `commit.gpgsign` is\n+\t  enabled, falls back to the committer identity.\n+\t* If signing is disabled (`commit.gpgsign=false`) and no explicit key\n+\t  is configured, exits with status 1.\n+\n+`--porcelain`::\n+\tGive the output in an easy-to-parse format for scripts.\n+\tOutputs key-value pairs (`user.author.name`, `user.author.email`,\n+\t`user.committer.name`, `user.committer.email`, `user.signingkey`,\n+\t`gpg.format`, `commit.gpgsign`).\n+\n+`-z`::\n+`--null`::\n+\tTerminate output lines or entries with NUL instead of a newline.\n+\n+`-v`::\n+`--verbose`::\n+\tPrepend role labels (`Author: `, `Committer: `) to identity entries.\n+\n+EXAMPLES\n+--------\n+\n+* Show full author and committer identities:\n++\n+------------\n+$ git ident\n+Author Name <author@example.com>\n+Committer Name <committer@example.com>\n+------------\n+\n+* Show author identity only:\n++\n+------------\n+$ git ident -a -e -n\n+Author Name <author@example.com>\n+------------\n+\n+* Show labeled author name:\n++\n+------------\n+$ git ident -a -n -v\n+Author: Author Name\n+------------\n+\n+* Show author and committer email addresses:\n++\n+------------\n+$ git ident -a -c -e\n+<author@example.com>\n+<committer@example.com>\n+------------\n+\n+* Show labeled author and committer email addresses:\n++\n+------------\n+$ git ident -a -c -e -v\n+Author: <author@example.com>\n+Committer: <committer@example.com>\n+------------\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex f4854f802d..864e5f620c 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -67,6 +67,7 @@ manpages = {\n   'git-help.adoc' : 1,\n   'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n+  'git-ident.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\n   'git-http-push.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex d4b775953d..359d8a8917 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1442,6 +1442,7 @@ BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n+BUILTIN_OBJS += builtin/ident.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\n BUILTIN_OBJS += builtin/interpret-trailers.o\ndiff --git a/builtin.h b/builtin.h\nindex 4e47a4ebd3..b17abed3d4 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -199,6 +199,7 @@ int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repo\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_ident(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_interpret_trailers(int argc, const char **argv, const char *prefix, struct repository *repo);\n@@ -284,5 +285,4 @@ int cmd_verify_pack(int argc, const char **argv, const char *prefix, struct repo\n int cmd_show_ref(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_pack_refs(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_replace(int argc, const char **argv, const char *prefix, struct repository *repo);\n-\n #endif\ndiff --git a/builtin/ident.c b/builtin/ident.c\nnew file mode 100644\nindex 0000000000..3571d8ad66\n--- /dev/null\n+++ b/builtin/ident.c\n@@ -0,0 +1,243 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"builtin.h\"\n+#include \"config.h\"\n+#include \"gettext.h\"\n+#include \"gpg-interface.h\"\n+#include \"ident.h\"\n+#include \"parse-options.h\"\n+#include \"strbuf.h\"\n+\n+static const char * const ident_usage[] = {\n+\tN_(\"git ident [<options>]\"),\n+\tNULL\n+};\n+\n+enum ident_who {\n+\tIDENT_AUTHOR = 1 << 0,\n+\tIDENT_COMMITTER = 1 << 1,\n+};\n+\n+enum ident_part {\n+\tIDENT_NAME = 1 << 0,\n+\tIDENT_EMAIL = 1 << 1,\n+};\n+\n+struct ident_config {\n+\tint gpgsign;\n+\tchar *signing_key;\n+\tchar *gpg_format;\n+\tchar *ssh_default_key_cmd;\n+};\n+\n+static int ident_config_cb(const char *var, const char *value,\n+\t\t\t   const struct config_context *ctx, void *data)\n+{\n+\tstruct ident_config *cfg = data;\n+\n+\tif (!strcmp(var, \"commit.gpgsign\")) {\n+\t\tcfg->gpgsign = git_config_bool(var, value);\n+\t\treturn 0;\n+\t}\n+\tif (!strcmp(var, \"user.signingkey\"))\n+\t\treturn git_config_string(&cfg->signing_key, var, value);\n+\tif (!strcmp(var, \"gpg.format\"))\n+\t\treturn git_config_string(&cfg->gpg_format, var, value);\n+\tif (!strcmp(var, \"gpg.ssh.defaultkeycommand\"))\n+\t\treturn git_config_string(&cfg->ssh_default_key_cmd, var, value);\n+\n+\treturn git_default_config(var, value, ctx, data);\n+}\n+\n+static void print_ident_entry(const char *label,\n+\t\t\t      const struct strbuf *name,\n+\t\t\t      const struct strbuf *email,\n+\t\t\t      int parts,\n+\t\t\t      int verbose,\n+\t\t\t      char eol)\n+{\n+\tstruct strbuf out = STRBUF_INIT;\n+\n+\tif (verbose && label)\n+\t\tstrbuf_addf(&out, \"%s: \", label);\n+\n+\tif ((parts & IDENT_NAME) && (parts & IDENT_EMAIL))\n+\t\tstrbuf_addf(&out, \"%s <%s>\", name->buf, email->buf);\n+\telse if (parts & IDENT_NAME)\n+\t\tstrbuf_addbuf(&out, name);\n+\telse if (parts & IDENT_EMAIL)\n+\t\tstrbuf_addf(&out, \"<%s>\", email->buf);\n+\n+\tprintf(\"%s%c\", out.buf, eol);\n+\tstrbuf_release(&out);\n+}\n+\n+int cmd_ident(int argc,\n+\t      const char **argv,\n+\t      const char *prefix,\n+\t      struct repository *repo)\n+{\n+\tint show_author = 0;\n+\tint show_committer = 0;\n+\tint show_name = 0;\n+\tint show_email = 0;\n+\tint show_signing_key = 0;\n+\tint porcelain = 0;\n+\tint nul_term = 0;\n+\tint verbose = 0;\n+\tint ret = 0;\n+\tchar eol;\n+\tint selected_who = 0;\n+\tint selected_parts = 0;\n+\n+\tstruct option ident_options[] = {\n+\t\tOPT_BOOL('a', \"author\", &show_author, N_(\"show author identity\")),\n+\t\tOPT_BOOL('c', \"committer\", &show_committer, N_(\"show committer identity\")),\n+\t\tOPT_BOOL('n', \"name\", &show_name, N_(\"show name only\")),\n+\t\tOPT_BOOL('e', \"email\", &show_email, N_(\"show email only\")),\n+\t\tOPT_BOOL('s', \"signing-key\", &show_signing_key, N_(\"show commit signing key\")),\n+\t\tOPT_BOOL(0, \"porcelain\", &porcelain, N_(\"machine-readable output\")),\n+\t\tOPT_BOOL('z', \"null\", &nul_term, N_(\"terminate entries with NUL\")),\n+\t\tOPT__VERBOSE(&verbose, N_(\"show detailed identity\")),\n+\t\tOPT_END()\n+\t};\n+\n+\tstruct ident_config cfg = { 0 };\n+\tstruct strbuf author_info = STRBUF_INIT;\n+\tstruct strbuf committer_info = STRBUF_INIT;\n+\tstruct ident_split author_split, committer_split;\n+\tstruct strbuf author_name = STRBUF_INIT;\n+\tstruct strbuf author_email = STRBUF_INIT;\n+\tstruct strbuf committer_name = STRBUF_INIT;\n+\tstruct strbuf committer_email = STRBUF_INIT;\n+\tchar *resolved_key = NULL;\n+\tint is_ssh = 0;\n+\n+\targc = parse_options(argc, argv, prefix, ident_options,\n+\t\t\t     ident_usage, 0);\n+\n+\tif (argc > 0)\n+\t\tusage_with_options(ident_usage, ident_options);\n+\n+\tdie_for_incompatible_opt2(porcelain, \"--porcelain\", verbose, \"--verbose\");\n+\tdie_for_incompatible_opt2(porcelain, \"--porcelain\", show_name, \"--name\");\n+\tdie_for_incompatible_opt2(porcelain, \"--porcelain\", show_email, \"--email\");\n+\n+\teol = nul_term ? '\\0' : '\\n';\n+\n+\trepo_config(repo, ident_config_cb, &cfg);\n+\n+\tstrbuf_addstr(&author_info, git_author_info(IDENT_NO_DATE));\n+\tstrbuf_addstr(&committer_info, git_committer_info(IDENT_NO_DATE));\n+\n+\tif (split_ident_line(&author_split, author_info.buf, author_info.len) == 0) {\n+\t\tif (author_split.name_begin && author_split.name_end)\n+\t\t\tstrbuf_add(&author_name, author_split.name_begin,\n+\t\t\t\t   author_split.name_end - author_split.name_begin);\n+\t\tif (author_split.mail_begin && author_split.mail_end)\n+\t\t\tstrbuf_add(&author_email, author_split.mail_begin,\n+\t\t\t\t   author_split.mail_end - author_split.mail_begin);\n+\t}\n+\n+\tif (split_ident_line(&committer_split, committer_info.buf, committer_info.len) == 0) {\n+\t\tif (committer_split.name_begin && committer_split.name_end)\n+\t\t\tstrbuf_add(&committer_name, committer_split.name_begin,\n+\t\t\t\t   committer_split.name_end - committer_split.name_begin);\n+\t\tif (committer_split.mail_begin && committer_split.mail_end)\n+\t\t\tstrbuf_add(&committer_email, committer_split.mail_begin,\n+\t\t\t\t   committer_split.mail_end - committer_split.mail_begin);\n+\t}\n+\n+\tis_ssh = cfg.gpg_format && !strcmp(cfg.gpg_format, \"ssh\");\n+\n+\tif (cfg.signing_key && *cfg.signing_key) {\n+\t\tresolved_key = xstrdup(cfg.signing_key);\n+\t} else if (is_ssh) {\n+\t\tif (cfg.ssh_default_key_cmd && *cfg.ssh_default_key_cmd)\n+\t\t\tresolved_key = get_signing_key_id();\n+\t} else if (cfg.gpgsign) {\n+\t\tresolved_key = get_signing_key_id();\n+\t}\n+\n+\tif (show_signing_key && !show_author && !show_committer && !show_name && !show_email && !porcelain) {\n+\t\tif (resolved_key && *resolved_key) {\n+\t\t\tif (verbose)\n+\t\t\t\tprintf(_(\"Signing Key: %s\\n\"), resolved_key);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s%c\", resolved_key, eol);\n+\t\t\tret = 0;\n+\t\t} else {\n+\t\t\tret = 1;\n+\t\t}\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (show_author)\n+\t\tselected_who |= IDENT_AUTHOR;\n+\tif (show_committer)\n+\t\tselected_who |= IDENT_COMMITTER;\n+\tif (!selected_who)\n+\t\tselected_who = IDENT_AUTHOR | IDENT_COMMITTER;\n+\n+\tif (show_name)\n+\t\tselected_parts |= IDENT_NAME;\n+\tif (show_email)\n+\t\tselected_parts |= IDENT_EMAIL;\n+\tif (!selected_parts)\n+\t\tselected_parts = IDENT_NAME | IDENT_EMAIL;\n+\n+\tif (porcelain) {\n+\t\tif (selected_who & IDENT_AUTHOR) {\n+\t\t\tprintf(\"user.author.name=%s%c\", author_name.buf, eol);\n+\t\t\tprintf(\"user.author.email=%s%c\", author_email.buf, eol);\n+\t\t}\n+\t\tif (selected_who & IDENT_COMMITTER) {\n+\t\t\tprintf(\"user.committer.name=%s%c\", committer_name.buf, eol);\n+\t\t\tprintf(\"user.committer.email=%s%c\", committer_email.buf, eol);\n+\t\t}\n+\t\tif ((selected_who & (IDENT_AUTHOR | IDENT_COMMITTER)) == (IDENT_AUTHOR | IDENT_COMMITTER)) {\n+\t\t\tprintf(\"user.signingkey=%s%c\",\n+\t\t\t       (cfg.signing_key && *cfg.signing_key) ? cfg.signing_key :\n+\t\t\t       (resolved_key && *resolved_key) ? resolved_key : \"none\",\n+\t\t\t       eol);\n+\t\t\tprintf(\"gpg.format=%s%c\",\n+\t\t\t       cfg.gpg_format ? cfg.gpg_format : \"openpgp\", eol);\n+\t\t\tprintf(\"commit.gpgsign=%s%c\",\n+\t\t\t       cfg.gpgsign ? \"true\" : \"false\", eol);\n+\t\t}\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (selected_who & IDENT_AUTHOR)\n+\t\tprint_ident_entry(\"Author\", &author_name, &author_email,\n+\t\t\t\t  selected_parts, verbose, eol);\n+\n+\tif (selected_who & IDENT_COMMITTER)\n+\t\tprint_ident_entry(\"Committer\", &committer_name, &committer_email,\n+\t\t\t\t  selected_parts, verbose, eol);\n+\n+\tif (show_signing_key) {\n+\t\tif (resolved_key && *resolved_key) {\n+\t\t\tif (verbose)\n+\t\t\t\tprintf(_(\"Signing Key: %s\\n\"), resolved_key);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s%c\", resolved_key, eol);\n+\t\t} else {\n+\t\t\tret = 1;\n+\t\t}\n+\t}\n+\n+cleanup:\n+\tfree(cfg.signing_key);\n+\tfree(cfg.gpg_format);\n+\tfree(cfg.ssh_default_key_cmd);\n+\tfree(resolved_key);\n+\tstrbuf_release(&author_info);\n+\tstrbuf_release(&committer_info);\n+\tstrbuf_release(&author_name);\n+\tstrbuf_release(&author_email);\n+\tstrbuf_release(&committer_name);\n+\tstrbuf_release(&committer_email);\n+\n+\treturn ret;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex 21b802c420..39a22c271f 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -118,6 +118,7 @@ git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n git-history                             mainporcelain           history\n git-hook                                purehelpers\n+git-ident                               ancillaryinterrogators\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\n git-http-push                           synchelpers\ndiff --git a/git.c b/git.c\nindex 96df15b5cd..926187351a 100644\n--- a/git.c\n+++ b/git.c\n@@ -593,6 +593,7 @@ static struct cmd_struct commands[] = {\n \t{ \"help\", cmd_help },\n \t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP_GENTLY },\n+\t{ \"ident\", cmd_ident, RUN_SETUP_GENTLY },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\n \t{ \"init-db\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex d86f2acd2b..37821ed890 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -654,6 +654,7 @@ builtin_sources = [\n   'builtin/help.c',\n   'builtin/history.c',\n   'builtin/hook.c',\n+  'builtin/ident.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\n   'builtin/interpret-trailers.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex 181d61a8a0..3738d86140 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -87,6 +87,7 @@ integration_tests = [\n   't0012-help.sh',\n   't0013-sha1dc.sh',\n   't0014-alias.sh',\n+  't0015-ident.sh',\n   't0017-env-helper.sh',\n   't0018-advice.sh',\n   't0019-json-writer.sh',\ndiff --git a/t/t0015-ident.sh b/t/t0015-ident.sh\nnew file mode 100755\nindex 0000000000..8b6358e771\n--- /dev/null\n+++ b/t/t0015-ident.sh\n@@ -0,0 +1,234 @@\n+#!/bin/sh\n+\n+test_description='basic sanity checks for git ident'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+test_expect_success 'default output format (author + committer)' '\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n+\t$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n+\tEOF\n+\tgit ident >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident -v / --verbose default' '\n+\tcat >expect <<-EOF &&\n+\tAuthor: $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n+\tCommitter: $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n+\tEOF\n+\tgit ident -v >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident --author / -a' '\n+\techo \"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\" >expect &&\n+\tgit ident --author >actual &&\n+\ttest_cmp expect actual &&\n+\tgit ident -a >actual_short &&\n+\ttest_cmp expect actual_short\n+'\n+\n+test_expect_success 'git ident --committer / -c' '\n+\techo \"$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\" >expect &&\n+\tgit ident --committer >actual &&\n+\ttest_cmp expect actual &&\n+\tgit ident -c >actual_short &&\n+\ttest_cmp expect actual_short\n+'\n+\n+test_expect_success 'git ident -a -c' '\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\n+\t$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\n+\tEOF\n+\tgit ident -a -c >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident -a -n and -a -e' '\n+\techo \"$GIT_AUTHOR_NAME\" >expect_name &&\n+\tgit ident -a -n >actual_name &&\n+\ttest_cmp expect_name actual_name &&\n+\techo \"<$GIT_AUTHOR_EMAIL>\" >expect_email &&\n+\tgit ident -a -e >actual_email &&\n+\ttest_cmp expect_email actual_email\n+'\n+\n+test_expect_success 'git ident -c -n and -c -e' '\n+\techo \"$GIT_COMMITTER_NAME\" >expect_name &&\n+\tgit ident -c -n >actual_name &&\n+\ttest_cmp expect_name actual_name &&\n+\techo \"<$GIT_COMMITTER_EMAIL>\" >expect_email &&\n+\tgit ident -c -e >actual_email &&\n+\ttest_cmp expect_email actual_email\n+'\n+\n+test_expect_success 'git ident -a -e -n (additive full ident)' '\n+\techo \"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\" >expect &&\n+\tgit ident -a -e -n >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident -a -n -v (labeled name)' '\n+\techo \"Author: $GIT_AUTHOR_NAME\" >expect &&\n+\tgit ident -a -n -v >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident -a -e -v (labeled email)' '\n+\techo \"Author: <$GIT_AUTHOR_EMAIL>\" >expect &&\n+\tgit ident -a -e -v >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident -a -c -e (unlabeled emails)' '\n+\tcat >expect <<-EOF &&\n+\t<$GIT_AUTHOR_EMAIL>\n+\t<$GIT_COMMITTER_EMAIL>\n+\tEOF\n+\tgit ident -a -c -e >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident -a -c -e -v (labeled emails)' '\n+\tcat >expect <<-EOF &&\n+\tAuthor: <$GIT_AUTHOR_EMAIL>\n+\tCommitter: <$GIT_COMMITTER_EMAIL>\n+\tEOF\n+\tgit ident -a -c -e -v >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident -a -c -n -v (labeled names)' '\n+\tcat >expect <<-EOF &&\n+\tAuthor: $GIT_AUTHOR_NAME\n+\tCommitter: $GIT_COMMITTER_NAME\n+\tEOF\n+\tgit ident -a -c -n -v >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident with -z on single fields' '\n+\tprintf \"$GIT_AUTHOR_NAME\\0\" >expect_name &&\n+\tgit ident -a -n -z >actual_name &&\n+\ttest_cmp expect_name actual_name &&\n+\tprintf \"<$GIT_AUTHOR_EMAIL>\\0\" >expect_email &&\n+\tgit ident -a -e -z >actual_email &&\n+\ttest_cmp expect_email actual_email\n+'\n+\n+test_expect_success 'git ident with -z on multiple entries' '\n+\tprintf \"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>\\0$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\\0\" >expect &&\n+\tgit ident -z >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident --porcelain format' '\n+\ttest_config user.signingkey \"TEST_KEY_123\" &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format ssh &&\n+\tcat >expect <<-EOF &&\n+\tuser.author.name=$GIT_AUTHOR_NAME\n+\tuser.author.email=$GIT_AUTHOR_EMAIL\n+\tuser.committer.name=$GIT_COMMITTER_NAME\n+\tuser.committer.email=$GIT_COMMITTER_EMAIL\n+\tuser.signingkey=TEST_KEY_123\n+\tgpg.format=ssh\n+\tcommit.gpgsign=true\n+\tEOF\n+\tgit ident --porcelain >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident -a --porcelain format' '\n+\tcat >expect <<-EOF &&\n+\tuser.author.name=$GIT_AUTHOR_NAME\n+\tuser.author.email=$GIT_AUTHOR_EMAIL\n+\tEOF\n+\tgit ident -a --porcelain >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident --porcelain -z format' '\n+\ttest_config user.signingkey \"TEST_KEY_123\" &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format ssh &&\n+\tprintf \"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 &&\n+\tgit ident --porcelain -z >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git ident --signing-key when signing is disabled and unset' '\n+\ttest_config commit.gpgsign false &&\n+\ttest_unconfig user.signingkey &&\n+\ttest_must_fail git ident --signing-key\n+'\n+\n+test_expect_success 'git ident with explicitly configured signing key' '\n+\ttest_config user.signingkey \"TEST_KEY_123\" &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format ssh &&\n+\techo \"TEST_KEY_123\" >expect_key &&\n+\tgit ident --signing-key >actual_key &&\n+\ttest_cmp expect_key actual_key\n+'\n+\n+test_expect_success 'git ident with openpgp signing enabled without explicit key' '\n+\ttest_unconfig user.signingkey &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format openpgp &&\n+\techo \"$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>\" >expect_key &&\n+\tgit ident --signing-key >actual_key &&\n+\ttest_cmp expect_key actual_key\n+'\n+\n+test_expect_success 'git ident with ssh signing enabled without explicit key' '\n+\ttest_unconfig user.signingkey &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format ssh &&\n+\ttest_unconfig gpg.ssh.defaultKeyCommand &&\n+\ttest_must_fail git ident --signing-key\n+'\n+\n+test_expect_success GPGSSH 'git ident with ssh defaultKeyCommand' '\n+\ttest_unconfig user.signingkey &&\n+\ttest_config commit.gpgsign true &&\n+\ttest_config gpg.format ssh &&\n+\ttest_config gpg.ssh.defaultKeyCommand \"cat \\\"${GPGSSH_KEY_PRIMARY}.pub\\\"\" &&\n+\tgit ident --signing-key >actual_key &&\n+\ttest_grep \"^SHA256:\" actual_key\n+'\n+\n+test_expect_success 'git ident with environment variable overrides' '\n+\ttest_unconfig user.signingkey &&\n+\ttest_config commit.gpgsign false &&\n+\tcat >expect <<-EOF &&\n+\tCustom Author <custom.author@example.com>\n+\tCustom Committer <custom.committer@example.com>\n+\tEOF\n+\tGIT_AUTHOR_NAME=\"Custom Author\" \\\n+\tGIT_AUTHOR_EMAIL=\"custom.author@example.com\" \\\n+\tGIT_COMMITTER_NAME=\"Custom Committer\" \\\n+\tGIT_COMMITTER_EMAIL=\"custom.committer@example.com\" \\\n+\tgit ident >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'incompatible option combinations fail' '\n+\ttest_must_fail git ident --porcelain --verbose 2>err &&\n+\ttest_grep \"cannot be used together\" err &&\n+\ttest_must_fail git ident --porcelain --name 2>err &&\n+\ttest_grep \"cannot be used together\" err &&\n+\ttest_must_fail git ident --porcelain --email 2>err &&\n+\ttest_grep \"cannot be used together\" err\n+'\n+\n+test_expect_success 'git ident outside of repository' '\n+\tnongit git ident --author >actual &&\n+\ttest_grep \"<\" actual\n+'\n+\n+test_done\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n-- \ngitgitgadget\n"},{"id":"551618","messageId":"20260901043944.GA1074757@coredump.intra.peff.net","threadId":"66219","inReplyTo":"pull.2388.v2.git.git.1788220746663.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] builtin/ident: add new 'ident' command","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-09-01T04:39:44Z","receivedAt":"2026-09-01T04:39:46Z","isPatch":true,"body":"On Mon, Aug 31, 2026 at 11:59:06PM +0000, Andrew Pleeter via GitGitGadget wrote:\n\n> While existing plumbing commands like 'git var' and 'git config' expose\n> individual pieces of identity and configuration, discovering what identity\n> and signing key will actually be attached to a new commit requires multiple\n> independent queries and manual correlation. 'git config' only reads raw\n> values without performing environment overrides or GECOS detection, while\n> 'git var' returns full ident strings with timestamps without exposing\n> commit signing status.\n\nThis is just my gut reaction, but: would it be simpler to teach git var\nto provide those broken-down pieces than to introduce a whole new\ncommand?\n\nThis works now:\n\n  git var GIT_COMMITTER_IDENT\n\nbut why not:\n\n  git var GIT_COMMITTER_NAME\n  git var GIT_COMMITTER_EMAIL\n  git var GIT_COMMITTER_DATE\n\nThose are well-known names already; they're what we use for reading in\nthe broken-down values from the environment. And likewise for\nGIT_AUTHOR_*.\n\nLet's see what else is in your feature list:\n\n> 'git ident' provides a unified command with additive, composable options:\n>   - Identity scope selectors (-a / --author, -c / --committer) choose\n>     which identities to format (defaulting to both when neither is specified).\n\nI think that works by switching between the two var families above.\n\n>   - Component selectors (-n / --name, -e / --email) choose which parts\n>     to format (defaulting to full 'Name <email>' when neither or both are\n>     specified).\n\nWe don't allow mix-and-match here (nor even multiple values!), so you'd\nhave to do:\n\n  ident=\"$(git var GIT_AUTHOR_NAME) <$(git var GIT_AUTHOR_EMAIL)\"\n\nI think it would be reasonable for git-var to accept multiple values and\noutput them one per line (or with NULs via \"-z\"). That doesn't really\nmake things easier in shell, but it might help scripts in other\nlanguages.\n\nWe _could_ go as far as providing a format language like we do in\nfor-each-ref, etc, where we offer to shell-quote. And then you can do:\n\n  eval \"$(git var --shell-quote --format='\n\t\tname=%(GIT_AUTHOR_NAME)\n\t\temail=%(GIT_AUTHOR_EMAIL)\n\t')\"\n\nbut IMHO that is probably going too far. It sometimes lets you simplify\nshell use of the tool at the expense of a weird and complicated\ninterface (I kind of which we didn't have it in for-each-ref).\n\n>   - -v / --verbose prepends 'Author: ' or 'Committer: ' role labels.\n\nSeems like something that git-var might benefit from, too.\n\n>   - -s / --signing-key resolves and outputs the commit signing key.\n\nLikewise, this feels like it should be a git-var entry.\n\n>   - --porcelain produces machine-readable key-value pairs.\n>   - -z / --null terminates output records with NUL bytes.\n\nLikewise.\n\n\nMy main feeling on suggesting this is that:\n\n  1. We already have a lot of commands, and this one feels very\n     specialized.\n\n  2. Most of these suggestions could make git-var better for reading\n     idents _and_ for reading its other variables.\n\n-Peff\n"},{"id":"551620","messageId":"xmqqzey1tvjr.fsf@gitster.g","threadId":"66219","inReplyTo":"20260901043944.GA1074757@coredump.intra.peff.net","subject":"Re: [PATCH v2] builtin/ident: add new 'ident' command","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-01T05:00:56Z","receivedAt":"2026-09-01T05:00:58Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> On Mon, Aug 31, 2026 at 11:59:06PM +0000, Andrew Pleeter via GitGitGadget wrote:\n>\n>> While existing plumbing commands like 'git var' and 'git config' expose\n>> individual pieces of identity and configuration, discovering what identity\n>> and signing key will actually be attached to a new commit requires multiple\n>> independent queries and manual correlation. 'git config' only reads raw\n>> values without performing environment overrides or GECOS detection, while\n>> 'git var' returns full ident strings with timestamps without exposing\n>> commit signing status.\n>\n> This is just my gut reaction, but: would it be simpler to teach git var\n> to provide those broken-down pieces than to introduce a whole new\n> command?\n\nWe long timers among our reviewer base already expressed the same\nsentiment.\n\n> My main feeling on suggesting this is that:\n>\n>   1. We already have a lot of commands, and this one feels very\n>      specialized.\n>\n>   2. Most of these suggestions could make git-var better for reading\n>      idents _and_ for reading its other variables.\n\nAnd you said it the best among our responses, I think.\n\nThanks.\n"},{"id":"551818","messageId":"pull.2388.v3.git.git.1788403792962.gitgitgadget@gmail.com","threadId":"66219","inReplyTo":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","subject":"[PATCH v3] var: support broken-down idents, default key, multiple args, and -z","fromName":"Andrew Pleeter via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-03T02:49:52Z","receivedAt":"2026-09-03T02:49:55Z","isPatch":true,"body":"From: Andrew Pleeter <andrewpleeter@gmail.com>\n\nWhile 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\nextracting individual components (name, email, or date) currently\nrequires callers to manually parse the composite string. Furthermore,\nthere is no way to query the resolved commit signing key through\n'git var', and the command only accepts a single variable at a time.\n\nTeach 'git var' to expose individual identity components and commit\nsigning configuration, and allow querying multiple variables with\noptional NUL-termination:\n\n- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n- Add GIT_DEFAULT_KEY (with GIT_SIGNING_KEY alias) to resolve the\n  configured or default commit signing key ID / fingerprint.\n- Allow passing multiple variable arguments (e.g., 'git var\n  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n  sequentially.\n- Support '-z' to terminate variable outputs and 'git var -l -z'\n  entries with NUL bytes.\n- Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n    var: support broken-down idents, default key, multiple args, and -z\n    \n    Teach git var to expose individual identity components and commit\n    signing configuration, and allow querying multiple variables with\n    optional NUL-termination.\n    \n    Following discussion on v1/v2 with Jeff King and Junio C Hamano, rather\n    than introducing a new standalone subcommand (whoami or ident), this\n    version enhances git var:\n    \n    \n    Changes since v2:\n    =================\n    \n     * Drop git ident / git whoami subcommand entirely.\n     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n     * Add GIT_DEFAULT_KEY (with GIT_SIGNING_KEY alias) to resolve commit\n       signing keys.\n     * Teach git var to accept multiple variable arguments (git var <var1>\n       <var2> ...).\n     * Add -z option to terminate outputs with NUL bytes (including git var\n       -l -z).\n     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v3\nPull-Request: https://github.com/git/git/pull/2388\n\nRange-diff vs v2:\n\n 1:  f322e7fedd < -:  ---------- builtin/ident: add new 'ident' command\n -:  ---------- > 1:  b86340dd0e var: support broken-down idents, default key, multiple args, and -z\n\n\n Documentation/git-var.adoc |  43 ++++++++-\n builtin/var.c              | 189 ++++++++++++++++++++++++++++++++-----\n t/t0007-git-var.sh         |  77 +++++++++++++++\n 3 files changed, 284 insertions(+), 25 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex 697c10aded..30bf2c12a8 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -9,7 +9,7 @@ git-var - Show a Git logical variable\n SYNOPSIS\n --------\n [synopsis]\n-git var (-l | <variable>)\n+git var (-l [-z] | [-z] <variable>...)\n \n DESCRIPTION\n -----------\n@@ -24,20 +24,55 @@ OPTIONS\n \tas well. (However, the configuration variables listing functionality\n \tis deprecated in favor of `git config list`.)\n \n+`-z`::\n+\tTerminate entries with NUL instead of newline.\n+\n EXAMPLES\n --------\n-\t$ git var GIT_AUTHOR_IDENT\n-\tEric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n-\n+* Get the author identity:\n++\n+------------\n+$ git var GIT_AUTHOR_IDENT\n+Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n+------------\n+\n+* Get the author name and email:\n++\n+------------\n+$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL\n+Eric W. Biederman\n+ebiederm@lnxi.com\n+------------\n \n VARIABLES\n ---------\n `GIT_AUTHOR_IDENT`::\n     The author of a piece of code.\n \n+`GIT_AUTHOR_NAME`::\n+    The name of the author of a piece of code.\n+\n+`GIT_AUTHOR_EMAIL`::\n+    The email of the author of a piece of code.\n+\n+`GIT_AUTHOR_DATE`::\n+    The date and timezone of the author of a piece of code.\n+\n `GIT_COMMITTER_IDENT`::\n     The person who put a piece of code into Git.\n \n+`GIT_COMMITTER_NAME`::\n+    The name of the person who put a piece of code into Git.\n+\n+`GIT_COMMITTER_EMAIL`::\n+    The email of the person who put a piece of code into Git.\n+\n+`GIT_COMMITTER_DATE`::\n+    The date and timezone of the person who put a piece of code into Git.\n+\n+`GIT_DEFAULT_KEY`::\n+    The default commit signing key ID or fingerprint, if configured or enabled.\n+\n `GIT_EDITOR`::\n     Text editor for use by Git commands.  The value is meant to be\n     interpreted by the shell when it is used.  Examples: `~/bin/vi`,\ndiff --git a/builtin/var.c b/builtin/var.c\nindex cc3a43cde2..94207cbc7c 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -12,25 +12,103 @@\n #include \"config.h\"\n #include \"editor.h\"\n #include \"environment.h\"\n+#include \"gpg-interface.h\"\n #include \"ident.h\"\n #include \"pager.h\"\n #include \"refs.h\"\n #include \"path.h\"\n #include \"strbuf.h\"\n+#include \"strvec.h\"\n #include \"run-command.h\"\n \n-static const char var_usage[] = \"git var (-l | <variable>)\";\n+static const char var_usage[] = \"git var (-l [-z] | [-z] <variable>...)\";\n \n static char *committer(int ident_flag)\n {\n \treturn xstrdup_or_null(git_committer_info(ident_flag));\n }\n \n+static char *ident_part(const char *ident, char part)\n+{\n+\tstruct ident_split split;\n+\n+\tif (!ident)\n+\t\treturn NULL;\n+\tif (split_ident_line(&split, ident, strlen(ident)))\n+\t\treturn NULL;\n+\n+\tswitch (part) {\n+\tcase 'n':\n+\t\tif (!split.name_begin || !split.name_end)\n+\t\t\treturn NULL;\n+\t\treturn xmemdupz(split.name_begin, split.name_end - split.name_begin);\n+\tcase 'e':\n+\t\tif (!split.mail_begin || !split.mail_end)\n+\t\t\treturn NULL;\n+\t\treturn xmemdupz(split.mail_begin, split.mail_end - split.mail_begin);\n+\tcase 'd':\n+\t\tif (!split.date_begin)\n+\t\t\treturn NULL;\n+\t\tif (split.tz_end)\n+\t\t\treturn xmemdupz(split.date_begin, split.tz_end - split.date_begin);\n+\t\tif (split.date_end)\n+\t\t\treturn xmemdupz(split.date_begin, split.date_end - split.date_begin);\n+\t\treturn NULL;\n+\tdefault:\n+\t\treturn NULL;\n+\t}\n+}\n+\n+static char *committer_name(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), 'n');\n+}\n+\n+static char *committer_email(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), 'e');\n+}\n+\n+static char *committer_date(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), 'd');\n+}\n+\n static char *author(int ident_flag)\n {\n \treturn xstrdup_or_null(git_author_info(ident_flag));\n }\n \n+static char *author_name(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), 'n');\n+}\n+\n+static char *author_email(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), 'e');\n+}\n+\n+static char *author_date(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), 'd');\n+}\n+\n+static char *default_key(int ident_flag UNUSED)\n+{\n+\tint gpgsign = 0;\n+\tchar *signing_key = NULL;\n+\n+\tif (repo_config_get_string(the_repository, \"user.signingkey\", &signing_key) == 0 && signing_key && *signing_key)\n+\t\treturn signing_key;\n+\tfree(signing_key);\n+\n+\tif (repo_config_get_bool(the_repository, \"commit.gpgsign\", &gpgsign) == 0 && gpgsign)\n+\t\treturn get_signing_key_id();\n+\n+\treturn NULL;\n+}\n+\n static char *editor(int ident_flag UNUSED)\n {\n \treturn xstrdup_or_null(git_editor());\n@@ -125,10 +203,34 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_COMMITTER_IDENT\",\n \t\t.read = committer,\n \t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_NAME\",\n+\t\t.read = committer_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_EMAIL\",\n+\t\t.read = committer_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_DATE\",\n+\t\t.read = committer_date,\n+\t},\n \t{\n \t\t.name = \"GIT_AUTHOR_IDENT\",\n \t\t.read = author,\n \t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_NAME\",\n+\t\t.read = author_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_EMAIL\",\n+\t\t.read = author_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_DATE\",\n+\t\t.read = author_date,\n+\t},\n \t{\n \t\t.name = \"GIT_EDITOR\",\n \t\t.read = editor,\n@@ -145,6 +247,10 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_DEFAULT_BRANCH\",\n \t\t.read = default_branch,\n \t},\n+\t{\n+\t\t.name = \"GIT_DEFAULT_KEY\",\n+\t\t.read = default_key,\n+\t},\n \t{\n \t\t.name = \"GIT_SHELL_PATH\",\n \t\t.read = shell_path,\n@@ -172,10 +278,11 @@ static struct git_var git_vars[] = {\n \t},\n };\n \n-static void list_vars(void)\n+static void list_vars(int null_term)\n {\n \tstruct git_var *ptr;\n \tchar *val;\n+\tchar eol = null_term ? '\\0' : '\\n';\n \n \tfor (ptr = git_vars; ptr->read; ptr++)\n \t\tif ((val = ptr->read(0))) {\n@@ -184,10 +291,10 @@ static void list_vars(void)\n \n \t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n \t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n+\t\t\t\t\tprintf(\"%s=%s%c\", ptr->name, list.items[i].string, eol);\n \t\t\t\tstring_list_clear(&list, 0);\n \t\t\t} else {\n-\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n+\t\t\t\tprintf(\"%s=%s%c\", ptr->name, val, eol);\n \t\t\t}\n \t\t\tfree(val);\n \t\t}\n@@ -196,6 +303,8 @@ static void list_vars(void)\n static const struct git_var *get_git_var(const char *var)\n {\n \tstruct git_var *ptr;\n+\tif (!strcmp(var, \"GIT_SIGNING_KEY\"))\n+\t\tvar = \"GIT_DEFAULT_KEY\";\n \tfor (ptr = git_vars; ptr->read; ptr++) {\n \t\tif (strcmp(var, ptr->name) == 0) {\n \t\t\treturn ptr;\n@@ -207,10 +316,13 @@ static const struct git_var *get_git_var(const char *var)\n static int show_config(const char *var, const char *value,\n \t\t       const struct config_context *ctx, void *cb)\n {\n+\tint null_term = cb ? *(int *)cb : 0;\n+\tchar eol = null_term ? '\\0' : '\\n';\n+\n \tif (value)\n-\t\tprintf(\"%s=%s\\n\", var, value);\n+\t\tprintf(\"%s=%s%c\", var, value, eol);\n \telse\n-\t\tprintf(\"%s\\n\", var);\n+\t\tprintf(\"%s%c\", var, eol);\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n@@ -219,30 +331,65 @@ int cmd_var(int argc,\n \t    const char *prefix UNUSED,\n \t    struct repository *repo UNUSED)\n {\n-\tconst struct git_var *git_var;\n-\tchar *val;\n+\tstruct strvec vars = STRVEC_INIT;\n+\tint list = 0;\n+\tint null_term = 0;\n+\tint i;\n \n \tshow_usage_if_asked(argc, argv, var_usage);\n-\tif (argc != 2)\n-\t\tusage(var_usage);\n \n-\tif (strcmp(argv[1], \"-l\") == 0) {\n-\t\trepo_config(the_repository, show_config, NULL);\n-\t\tlist_vars();\n+\tfor (i = 1; i < argc; i++) {\n+\t\tconst char *arg = argv[i];\n+\n+\t\tif (!strcmp(arg, \"-l\")) {\n+\t\t\tlist = 1;\n+\t\t} else if (!strcmp(arg, \"-z\")) {\n+\t\t\tnull_term = 1;\n+\t\t} else if (!strcmp(arg, \"--\")) {\n+\t\t\tfor (i = i + 1; i < argc; i++)\n+\t\t\t\tstrvec_push(&vars, argv[i]);\n+\t\t\tbreak;\n+\t\t} else if (arg[0] == '-') {\n+\t\t\tusage(var_usage);\n+\t\t} else {\n+\t\t\tstrvec_push(&vars, arg);\n+\t\t}\n+\t}\n+\n+\tif (list) {\n+\t\tif (vars.nr > 0) {\n+\t\t\tstrvec_clear(&vars);\n+\t\t\tusage(var_usage);\n+\t\t}\n+\t\trepo_config(the_repository, show_config, &null_term);\n+\t\tlist_vars(null_term);\n \t\treturn 0;\n \t}\n-\trepo_config(the_repository, git_default_config, NULL);\n \n-\tgit_var = get_git_var(argv[1]);\n-\tif (!git_var)\n+\tif (!vars.nr)\n \t\tusage(var_usage);\n \n-\tval = git_var->read(IDENT_STRICT);\n-\tif (!val)\n-\t\treturn 1;\n+\trepo_config(the_repository, git_default_config, NULL);\n+\n+\tfor (size_t j = 0; j < vars.nr; j++) {\n+\t\tconst struct git_var *git_var = get_git_var(vars.v[j]);\n+\t\tchar *val;\n+\n+\t\tif (!git_var) {\n+\t\t\tstrvec_clear(&vars);\n+\t\t\tusage(var_usage);\n+\t\t}\n+\n+\t\tval = git_var->read(IDENT_STRICT);\n+\t\tif (!val) {\n+\t\t\tstrvec_clear(&vars);\n+\t\t\treturn 1;\n+\t\t}\n \n-\tprintf(\"%s\\n\", val);\n-\tfree(val);\n+\t\tprintf(\"%s%c\", val, null_term ? '\\0' : '\\n');\n+\t\tfree(val);\n+\t}\n \n+\tstrvec_clear(&vars);\n \treturn 0;\n }\ndiff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\nindex 2b60317758..c437c968bb 100755\n--- a/t/t0007-git-var.sh\n+++ b/t/t0007-git-var.sh\n@@ -276,4 +276,81 @@ test_expect_success '`git var -l` works even without HOME' '\n \t)\n '\n \n+test_expect_success 'get author identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n+\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n+\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n+\tgit var GIT_AUTHOR_NAME >actual.name &&\n+\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n+\tgit var GIT_AUTHOR_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get committer identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n+\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n+\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n+\tgit var GIT_COMMITTER_NAME >actual.name &&\n+\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n+\tgit var GIT_COMMITTER_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get multiple variables' '\n+\ttest_tick &&\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME\n+\t$GIT_AUTHOR_EMAIL\n+\t$GIT_COMMITTER_NAME\n+\t$GIT_COMMITTER_EMAIL\n+\tEOF\n+\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z' '\n+\ttest_tick &&\n+\tprintf \"%s\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git var -l -z' '\n+\tgit var -l -z >actual &&\n+\ttr \"\\0\" \"\\n\" <actual | grep \"^GIT_AUTHOR_NAME=\" >filtered &&\n+\techo \"GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\" >expect &&\n+\ttest_cmp expect filtered\n+'\n+\n+test_expect_success 'get GIT_DEFAULT_KEY with user.signingkey configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\techo \"TEST_KEY_ID\" >expect &&\n+\tgit var GIT_DEFAULT_KEY >actual &&\n+\ttest_cmp expect actual &&\n+\tgit var GIT_SIGNING_KEY >actual.alias &&\n+\ttest_cmp expect actual.alias\n+'\n+\n+test_expect_success 'get GIT_DEFAULT_KEY fails when unset and signing disabled' '\n+\ttest_config user.signingkey \"\" &&\n+\ttest_config commit.gpgsign false &&\n+\ttest_must_fail git var GIT_DEFAULT_KEY\n+'\n+\n+test_expect_success 'git var -l lists new variables' '\n+\tgit var -l >actual &&\n+\tgrep \"^GIT_AUTHOR_NAME=\" actual &&\n+\tgrep \"^GIT_AUTHOR_EMAIL=\" actual &&\n+\tgrep \"^GIT_AUTHOR_DATE=\" actual &&\n+\tgrep \"^GIT_COMMITTER_NAME=\" actual &&\n+\tgrep \"^GIT_COMMITTER_EMAIL=\" actual &&\n+\tgrep \"^GIT_COMMITTER_DATE=\" actual\n+'\n+\n test_done\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n-- \ngitgitgadget\n"},{"id":"551888","messageId":"xmqqbjaecjxb.fsf@gitster.g","threadId":"66219","inReplyTo":"pull.2388.v3.git.git.1788403792962.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-03T17:40:48Z","receivedAt":"2026-09-03T17:40:53Z","isPatch":true,"body":"\"Andrew Pleeter via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> - Allow passing multiple variable arguments (e.g., 'git var\n>   GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n>   sequentially.\n\nYes.  This would be really useful if anybody wants to make it a\ncomponent in serious scripting.\n\n>  Documentation/git-var.adoc |  43 ++++++++-\n>  builtin/var.c              | 189 ++++++++++++++++++++++++++++++++-----\n>  t/t0007-git-var.sh         |  77 +++++++++++++++\n>  3 files changed, 284 insertions(+), 25 deletions(-)\n>\n> diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\n> index 697c10aded..30bf2c12a8 100644\n> --- a/Documentation/git-var.adoc\n> +++ b/Documentation/git-var.adoc\n> @@ -9,7 +9,7 @@ git-var - Show a Git logical variable\n>  SYNOPSIS\n>  --------\n>  [synopsis]\n> -git var (-l | <variable>)\n> +git var (-l [-z] | [-z] <variable>...)\n\nIt might make sense to split the two vastly different modes of\noperation into separate lines in the synopsis, i.e.,\n\n    git var [-z] -l\n    git var [-z] <variable>...\n\n>  VARIABLES\n>  ---------\n>  `GIT_AUTHOR_IDENT`::\n>      The author of a piece of code.\n\nThis shows that \" of a piece of code\" was inherited from the\noriginal, and while it is not your fault, the phrasing is awkward\nand misleading.  When a user runs:\n\n    $ git var GIT_AUTHOR_DATE\n\nthe command does not look at any particular piece of code or report\nwhen it was written.  We are better off without \" of a piece of\ncode\" in this entry (unless we can replace it with something better)\nand in all the other new entries.\n\n> +`GIT_AUTHOR_NAME`::\n> +    The name of the author of a piece of code.\n> +\n> +`GIT_AUTHOR_EMAIL`::\n> +    The email of the author of a piece of code.\n> +\n> +`GIT_AUTHOR_DATE`::\n> +    The date and timezone of the author of a piece of code.\n\nSo let's discuss what we can replace \"of a piece of code\" with.\n\nThe original motivation for GIT_AUTHOR_IDENT (and similarly\nGIT_COMMITTER_IDENT) was to give scripts a way to construct a\nstring that they can pass to 'git hash-object -t commit' to create a\ncommit object.  GIT_AUTHOR_IDENT is what would appear on the\n\"author\" line (and GIT_COMMITTER_IDENT on the \"committer\" line) in\nthe resulting commit object if you were to run 'git commit' right\nnow.  IDENT has a clear meaning (given above); the other three are\nindividual fields broken out of it.\n\nExplaining these four (IDENT, NAME, EMAIL, DATE) along those lines\nwould make it easier for readers to understand.\n\nHere is my attempt:\n\n    GIT_AUTHOR_IDENT::\n    GIT_AUTHOR_NAME::\n    GIT_AUTHOR_EMAIL::\n    GIT_AUTHOR_DATE::\n        The authorship information that would be recorded in the\n        resulting commit object if you ran 'git commit' right now.\n        GIT_AUTHOR_IDENT consists of the author's name, e-mail\n        address, and timestamp+timezone.  These three pieces of\n        information are available separately as GIT_AUTHOR_NAME,\n        GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n\nSimilarly for GIT_COMMITTER_*.\n\n\n> +`GIT_DEFAULT_KEY`::\n> +    The default commit signing key ID or fingerprint, if configured or enabled.\n\nIn the same spirit as the explanation of 'GIT_AUTHOR_IDENT' above,\nwe could describe this as \"the key that would be used to sign the\nresulting commit if you were to run 'git commit' right now\".  I\nwonder if that is easier to reason about.\n\nYes, I am suggesting moving away from using \"DEFAULT\" in the name.\n\n> +static char *ident_part(const char *ident, char part)\n\nUse an enum instead of 'char part', unless you derive that single\ncharacter directly from end-user input.\n\n> +{\n> +\tstruct ident_split split;\n> +\n> +\tif (!ident)\n> +\t\treturn NULL;\n> +\tif (split_ident_line(&split, ident, strlen(ident)))\n> +\t\treturn NULL;\n> +\n> +\tswitch (part) {\n> +\tcase 'n':\n> +\t\tif (!split.name_begin || !split.name_end)\n> +\t\t\treturn NULL;\n> +\t\treturn xmemdupz(split.name_begin, split.name_end - split.name_begin);\n> +\tcase 'e':\n> +\t\tif (!split.mail_begin || !split.mail_end)\n> +\t\t\treturn NULL;\n> +\t\treturn xmemdupz(split.mail_begin, split.mail_end - split.mail_begin);\n> +\tcase 'd':\n> +\t\tif (!split.date_begin)\n> +\t\t\treturn NULL;\n> +\t\tif (split.tz_end)\n> +\t\t\treturn xmemdupz(split.date_begin, split.tz_end - split.date_begin);\n> +\t\tif (split.date_end)\n> +\t\t\treturn xmemdupz(split.date_begin, split.date_end - split.date_begin);\n> +\t\treturn NULL;\n\nThe line is getting overly long.  Aim to wrap at around ~70 columns.\n\n> +\tdefault:\n> +\t\treturn NULL;\n> +\t}\n> +}\n\n> +static char *default_key(int ident_flag UNUSED)\n> +{\n> +\tint gpgsign = 0;\n> +\tchar *signing_key = NULL;\n> +\n> +\tif (repo_config_get_string(the_repository, \"user.signingkey\", &signing_key) == 0 && signing_key && *signing_key)\n> +\t\treturn signing_key;\n\nAn overly long line.\n\nDo not make numeric comparison with 0 as \"== 0\" or \"!= 0\".\n\n\tif (!repo_config_get_string(the_repository,\n\t\t\t\t    \"user.signingkey\", &signing_key) &&\n\t    signing_key && *signing_key)\n\t\treturn signing_key;\n\nThe reason why you pretend that user.signingkey is not even defined\nwhen it is defined to be an empty string is because otherwise there\nwon't be a way for a user to override a key defined in per-user\nconfiguration file in per-repository configuration file.  It may\ndeserve an in-code comment to explain that, or is it too obvious?\n\nI dunno.\n\nBy the way, by using repo_config_get_string(), you are willing to\ngive an error message from config_error_nonbool() when the user has\nuser.signingkey mistakenly defined as a valueless true, i.e.,\n\n\t[user]\n\t\tname = A U Thor\n\t\temail = au@th.or\n\t\tsigningkey\n\nI think it is OK to give an error message here, to give the user a\nchance to notice and fix the mistake in their configuration file,\nand keep going as if the entry did not even exist.  I just wanted to\nmake sure we are all aware that it is what our new code is doing.\n\n> +\tfree(signing_key);\n> +\n> +\tif (repo_config_get_bool(the_repository, \"commit.gpgsign\", &gpgsign) == 0 && gpgsign)\n> +\t\treturn get_signing_key_id();\n\nI am not sure if this is a good idea.  Whether the \"git commit\"\ncommand is told to trigger GPG signing via the configuration\nvariable, or via the \"--gpg-sign\" command line option, wouldn't the\nsigning key returned by get_signing_key_id() be used either way?\n\nIn other words, I can explain why the previous entry checks the\n\"user.signingkey\" configuration variable, but I cannot give an\nexplanation why we check the \"commit.gpgsign\" configuration variable\nhere.\n\n> @@ -125,10 +203,34 @@ static struct git_var git_vars[] = {\n>  \t\t.name = \"GIT_COMMITTER_IDENT\",\n>  \t\t.read = committer,\n>  \t},\n> +\t{\n> +\t\t.name = \"GIT_COMMITTER_NAME\",\n> +\t\t.read = committer_name,\n> +\t},\n> ...\n\nThese are pretty straight-forward additions.  It shows that the\noriginal code structure was designed for extensibility.\n\n> @@ -172,10 +278,11 @@ static struct git_var git_vars[] = {\n>  \t},\n>  };\n>  \n> -static void list_vars(void)\n> +static void list_vars(int null_term)\n>  {\n>  \tstruct git_var *ptr;\n>  \tchar *val;\n> +\tchar eol = null_term ? '\\0' : '\\n';\n>  \n>  \tfor (ptr = git_vars; ptr->read; ptr++)\n>  \t\tif ((val = ptr->read(0))) {\n> @@ -184,10 +291,10 @@ static void list_vars(void)\n>  \n>  \t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n>  \t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n> -\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n> +\t\t\t\t\tprintf(\"%s=%s%c\", ptr->name, list.items[i].string, eol);\n\nBeware overly long lines.\n\n> @@ -196,6 +303,8 @@ static void list_vars(void)\n>  static const struct git_var *get_git_var(const char *var)\n>  {\n>  \tstruct git_var *ptr;\n> +\tif (!strcmp(var, \"GIT_SIGNING_KEY\"))\n> +\t\tvar = \"GIT_DEFAULT_KEY\";\n\nWe should be able to do much better than this.  Aim to stick to the\ntable-driven approach as much as possible.  For example, we could\nadd a \"const char *\" member to struct git_var that says it is an\nalias to another key, add an entry\n\n\t{\n\t\t.name = \"GIT_SIGNING_KEY\",\n\t\t.alias = \"GIT_DEFAULT_KEY\",\n\t\t.read = default_key,\n\t},\n\nthere, and teach the enumerating iterator (aka list_vars) to skip an\nentry that is an alias (because it would give the user redundant\ninformation) while allowing the locating iterator (aka get_git_var)\nto find it.\n\n>  \tfor (ptr = git_vars; ptr->read; ptr++) {\n>  \t\tif (strcmp(var, ptr->name) == 0) {\n>  \t\t\treturn ptr;\n> @@ -219,30 +331,65 @@ int cmd_var(int argc,\n>  \t    const char *prefix UNUSED,\n>  \t    struct repository *repo UNUSED)\n>  {\n> -\tconst struct git_var *git_var;\n> -\tchar *val;\n> +\tstruct strvec vars = STRVEC_INIT;\n> +\tint list = 0;\n> +\tint null_term = 0;\n> +\tint i;\n>  \n>  \tshow_usage_if_asked(argc, argv, var_usage);\n> -\tif (argc != 2)\n> -\t\tusage(var_usage);\n>  \n> -\tif (strcmp(argv[1], \"-l\") == 0) {\n> -\t\trepo_config(the_repository, show_config, NULL);\n> -\t\tlist_vars();\n> +\tfor (i = 1; i < argc; i++) {\n> +\t\tconst char *arg = argv[i];\n> +\n> +\t\tif (!strcmp(arg, \"-l\")) {\n> +\t\t\tlist = 1;\n> +\t\t} else if (!strcmp(arg, \"-z\")) {\n> +\t\t\tnull_term = 1;\n> +\t\t} else if (!strcmp(arg, \"--\")) {\n> +\t\t\tfor (i = i + 1; i < argc; i++)\n> +\t\t\t\tstrvec_push(&vars, argv[i]);\n> +\t\t\tbreak;\n\nEverything else in this loop seems sensible, but this one is iffy.\nWhat makes us need this special casing of \"--\" and everything that\nfollows it?\n\n> +\t\t} else if (arg[0] == '-') {\n> +\t\t\tusage(var_usage);\n> +\t\t} else {\n> +\t\t\tstrvec_push(&vars, arg);\n> +\t\t}\n> +\t}\n\nHave you considered using parse_options() API instead of this\nhandwritten loop?  You'd only need to recognise two options '-l' and\n'-z' into two booleans \"int list\" and \"int null_termination\", and\nthen\n\n\tif (list) {\n\t\t... barf if something remains in argc/argv[] ...\n\t\t\n\t} else {\n\t\t... everyhing in argc/argv[] are vars ...\n\t}\n\nI do not see why we would even need a vars that is a copy of\nargc/argv[] after options are parsed out.\n\n> +\tif (!vars.nr)\n>  \t\tusage(var_usage);\n\nI do not know if this is friendly for a tool meant primarily for\nscripts.  \"git var foo bar\" would give information about foo and\nbar, \"git var foo\" would give informmation about foo, and it is\nnatural \"git var\" alone would give nothing.\n\nThis matters because it is plausible to have a list of vars in a\nvariable and then run \"git var -z $variables | xargs -0 ...\" to\nprocess the vars and their values.  If you barf for an empty list,\nthen they need to see if they have nothing in $variable, which\nbecomes more than \"test -z \"$variable\" because people often take\nadvantage of the fact that IFS is ignored to write their accumulator\nlike so\n\n\tvariable=\" \"\n\tfor n in ...\n\tdo\n\t\tvariable=\"$variable$n \"\n\tdone\n\n\tcase \"$variable\" in\n\t*\" \"word\" \"*)\n\t\t: has the word\n\t\t;;\n\tesac\n\nwhich means that $variable may not be an empty string.  It is easier\nfor scripts if \"git var <variable>...\" did not barf when the number\nof <variable>s happens to be zero.\n\n> +test_expect_success 'get multiple variables with -z' '\n> +\ttest_tick &&\n> +\tprintf \"%s\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n\nJust a trivia, but\n\n\tprintf \"%s\\0\" \"$GIT_AUTHOR_NAME \"$GIT_AUTHOR_EMAIL\" >expect\n\nwould be equivalent.\n"},{"id":"551893","messageId":"D27B3BE0-56E7-4388-9324-C16E2AAE2498@gmail.com","threadId":"66219","inReplyTo":"xmqqbjaecjxb.fsf@gitster.g","subject":"Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-09-03T18:22:46Z","receivedAt":"2026-09-03T18:22:59Z","isPatch":true,"body":"\n> Le 3 sept. 2026 à 14:02, Junio C Hamano <gitster@pobox.com> a écrit :\n> \n> ﻿\"Andrew Pleeter via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> +    if (!vars.nr)\n>>        usage(var_usage);\n> \n> I do not know if this is friendly for a tool meant primarily for\n> scripts.  \"git var foo bar\" would give information about foo and\n> bar, \"git var foo\" would give informmation about foo, and it is\n> natural \"git var\" alone would give nothing.\n\nI believe « git var » prints usage and exits 129 today, at least on my 2.55.0.340.g8e2bf96aa5.\n\nLifting 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?\n\n> This matters because it is plausible to have a list of vars in a\n> variable and then run \"git var -z $variables | xargs -0 ...\" to\n> process the vars and their values.  If you barf for an empty list,\n> then they need to see if they have nothing in $variable, which\n> becomes more than \"test -z \"$variable\" because people often take\n> advantage of the fact that IFS is ignored to write their accumulator\n> like so\n> \n>    variable=\" \"\n>    for n in ...\n>    do\n>        variable=\"$variable$n \"\n>    done\n> \n>    case \"$variable\" in\n>    *\" \"word\" \"*)\n>        : has the word\n>        ;;\n>    esac\n> \n> which means that $variable may not be an empty string.  It is easier\n> for scripts if \"git var <variable>...\" did not barf when the number\n> of <variable>s happens to be zero.\n"},{"id":"551937","messageId":"5634fdc4-d0f8-493d-b401-3c9753524034@gmail.com","threadId":"66219","inReplyTo":"pull.2388.v3.git.git.1788403792962.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-04T09:11:30Z","receivedAt":"2026-09-04T09:11:34Z","isPatch":true,"body":"Hi Andrew\n\nOn 03/09/2026 03:49, Andrew Pleeter via GitGitGadget wrote:\n> From: Andrew Pleeter <andrewpleeter@gmail.com>\n> \n> - Support '-z' to terminate variable outputs and 'git var -l -z'\n>    entries with NUL bytes.\n\nHurray! I agree with Junios comments and have left a few of my own below.\n>   static const struct git_var *get_git_var(const char *var)\n>   {\n>   \tstruct git_var *ptr;\n> +\tif (!strcmp(var, \"GIT_SIGNING_KEY\"))\n> +\t\tvar = \"GIT_DEFAULT_KEY\";\n\nDo we really need an alias? GIT_DEFAULT_KEY is pretty meaningless to me, \nwhereas GIT_SIGNING_KEY is clearly a key for signing. What's the usecase \nfor this by the way. If a script is using git to sign then it does not \nneed to query the default key because git will use it automatically. If \na script wants to use the key to sign something else doesn't it need to \nalso know which signing scheme git is using (ssh, gpg, etc) , or is that \nobvious from the key?\n\n>   \tfor (ptr = git_vars; ptr->read; ptr++) {\n>   \t\tif (strcmp(var, ptr->name) == 0) {\n>   \t\t\treturn ptr;\n> @@ -207,10 +316,13 @@ static const struct git_var *get_git_var(const char *var)\n>   static int show_config(const char *var, const char *value,\n>   \t\t       const struct config_context *ctx, void *cb)\n>   {\n> +\tint null_term = cb ? *(int *)cb : 0;\n> +\tchar eol = null_term ? '\\0' : '\\n';\n> +\n>   \tif (value)\n> -\t\tprintf(\"%s=%s\\n\", var, value);\n> +\t\tprintf(\"%s=%s%c\", var, value, eol);\n\nA key can contain \"=\" so this format is ambiguous. When the user passes \n\"-z\" we should use the same format as \"git config list -z\" which avoids \nthat ambiguity\n\n\tprintf(\"%s%c%s%c\", var, eol == '\\n' ? '=' : '\\n', value, eol);\n\n>   \telse\n> -\t\tprintf(\"%s\\n\", var);\n> +\t\tprintf(\"%s%c\", var, eol);\n\nIt would be worth checking what \"git config list -z\" does when there is \nno value and matching that. Does it print \"key\\n\\0\", or \"key\\0\"?\n\n> +\tfor (i = 1; i < argc; i++) {\n> +\t\tconst char *arg = argv[i];\n> +\n> +\t\tif (!strcmp(arg, \"-l\")) {\n> +\t\t\tlist = 1;\n> +\t\t} else if (!strcmp(arg, \"-z\")) {\n> +\t\t\tnull_term = 1;\n> +\t\t} else if (!strcmp(arg, \"--\")) {\n> +\t\t\tfor (i = i + 1; i < argc; i++)\n> +\t\t\t\tstrvec_push(&vars, argv[i]);\n> +\t\t\tbreak;\n> +\t\t} else if (arg[0] == '-') {\n> +\t\t\tusage(var_usage);\n> +\t\t} else {\n> +\t\t\tstrvec_push(&vars, arg);\n\nI think we should break out of the loop when arg is \"--\", or does not \nbegin with '-', and treat the rest as variable names to print. There is \nnot need to support \"git var GIT_AUTHOR_NAME -z GIT_AUTHOR_EMAIL\" in a \nplumbing command.\n\n> [...]\n> -\tprintf(\"%s\\n\", val);\n> -\tfree(val);\n> +\t\tprintf(\"%s%c\", val, null_term ? '\\0' : '\\n');\n\nMulti-valued variables are a bit of a problem here, they're built on the \nassumption that the individual values do not contain a newline, but as \nthey are paths I'm not sure that is necessarily true. With -z it would \nbe better to print '\\0' after each value as we do in list_vars(). \nIdeally we wouldn't use a single string to pass multiple values around, \nbut a simple fix would be to use '\\0' to separate the individual values \ninstead of '\\n' so that we can split them unambiguously when we print them.\n\nThanks for working on this, being able to specify multiple variables \nthat are printed unambiguously is a really useful improvement.\n\nPhillip\n> +\t\tfree(val);\n> +\t}\n>   \n> +\tstrvec_clear(&vars);\n>   \treturn 0;\n>   }\n> diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\n> index 2b60317758..c437c968bb 100755\n> --- a/t/t0007-git-var.sh\n> +++ b/t/t0007-git-var.sh\n> @@ -276,4 +276,81 @@ test_expect_success '`git var -l` works even without HOME' '\n>   \t)\n>   '\n>   \n> +test_expect_success 'get author identity components' '\n> +\ttest_tick &&\n> +\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n> +\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n> +\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n> +\tgit var GIT_AUTHOR_NAME >actual.name &&\n> +\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n> +\tgit var GIT_AUTHOR_DATE >actual.date &&\n> +\ttest_cmp expect.name actual.name &&\n> +\ttest_cmp expect.email actual.email &&\n> +\ttest_cmp expect.date actual.date\n> +'\n> +\n> +test_expect_success 'get committer identity components' '\n> +\ttest_tick &&\n> +\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n> +\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n> +\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n> +\tgit var GIT_COMMITTER_NAME >actual.name &&\n> +\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n> +\tgit var GIT_COMMITTER_DATE >actual.date &&\n> +\ttest_cmp expect.name actual.name &&\n> +\ttest_cmp expect.email actual.email &&\n> +\ttest_cmp expect.date actual.date\n> +'\n> +\n> +test_expect_success 'get multiple variables' '\n> +\ttest_tick &&\n> +\tcat >expect <<-EOF &&\n> +\t$GIT_AUTHOR_NAME\n> +\t$GIT_AUTHOR_EMAIL\n> +\t$GIT_COMMITTER_NAME\n> +\t$GIT_COMMITTER_EMAIL\n> +\tEOF\n> +\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'get multiple variables with -z' '\n> +\ttest_tick &&\n> +\tprintf \"%s\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n> +\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'git var -l -z' '\n> +\tgit var -l -z >actual &&\n> +\ttr \"\\0\" \"\\n\" <actual | grep \"^GIT_AUTHOR_NAME=\" >filtered &&\n> +\techo \"GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\" >expect &&\n> +\ttest_cmp expect filtered\n> +'\n> +\n> +test_expect_success 'get GIT_DEFAULT_KEY with user.signingkey configured' '\n> +\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n> +\techo \"TEST_KEY_ID\" >expect &&\n> +\tgit var GIT_DEFAULT_KEY >actual &&\n> +\ttest_cmp expect actual &&\n> +\tgit var GIT_SIGNING_KEY >actual.alias &&\n> +\ttest_cmp expect actual.alias\n> +'\n> +\n> +test_expect_success 'get GIT_DEFAULT_KEY fails when unset and signing disabled' '\n> +\ttest_config user.signingkey \"\" &&\n> +\ttest_config commit.gpgsign false &&\n> +\ttest_must_fail git var GIT_DEFAULT_KEY\n> +'\n> +\n> +test_expect_success 'git var -l lists new variables' '\n> +\tgit var -l >actual &&\n> +\tgrep \"^GIT_AUTHOR_NAME=\" actual &&\n> +\tgrep \"^GIT_AUTHOR_EMAIL=\" actual &&\n> +\tgrep \"^GIT_AUTHOR_DATE=\" actual &&\n> +\tgrep \"^GIT_COMMITTER_NAME=\" actual &&\n> +\tgrep \"^GIT_COMMITTER_EMAIL=\" actual &&\n> +\tgrep \"^GIT_COMMITTER_DATE=\" actual\n> +'\n> +\n>   test_done\n> \n> base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n\n"},{"id":"551981","messageId":"xmqqqzj957rm.fsf@gitster.g","threadId":"66219","inReplyTo":"5634fdc4-d0f8-493d-b401-3c9753524034@gmail.com","subject":"Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-04T15:57:33Z","receivedAt":"2026-09-04T15:57:35Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>>   \tif (value)\n>> -\t\tprintf(\"%s=%s\\n\", var, value);\n>> +\t\tprintf(\"%s=%s%c\", var, value, eol);\n>\n> A key can contain \"=\" so this format is ambiguous. When the user passes \n> \"-z\" we should use the same format as \"git config list -z\" which avoids \n> that ambiguity\n>\n> \tprintf(\"%s%c%s%c\", var, eol == '\\n' ? '=' : '\\n', value, eol);\n>\n>>   \telse\n>> -\t\tprintf(\"%s\\n\", var);\n>> +\t\tprintf(\"%s%c\", var, eol);\n>\n> It would be worth checking what \"git config list -z\" does when there is \n> no value and matching that. Does it print \"key\\n\\0\", or \"key\\0\"?\n\nBy \"key\" do you mean \"var\"?  The namespace of \"var\" for \"git var\"\nproper (like GIT_COMMITTER_IDENT) are very much under our control,\nbut it also gives all the configuration variables, whose names can\nindeed have '=' in a three-level varlable name.  This is an\nexcellent suggestion.\n\n> I think we should break out of the loop when arg is \"--\", or does not \n> begin with '-', and treat the rest as variable names to print. There is \n> not need to support \"git var GIT_AUTHOR_NAME -z GIT_AUTHOR_EMAIL\" in a \n> plumbing command.\n\nNot limited to plumbing, but anywhere in Git.  Let's stick to and\nforce users adopt the simple rule that \"git help cli\" gives them.\nOptions first and then args, among which revs coe first and then\npaths after disambiguating \"--\".  I know as historical wart some\ncommands may take dashed options after args, but I am fine if we\ntightened the rule at Git 3.0 boundary to more strictly enforced\nthe option/argument ordering rule.\n\n>> [...]\n>> -\tprintf(\"%s\\n\", val);\n>> -\tfree(val);\n>> +\t\tprintf(\"%s%c\", val, null_term ? '\\0' : '\\n');\n>\n> Multi-valued variables are a bit of a problem here, they're built on the \n> assumption that the individual values do not contain a newline, but as \n> they are paths I'm not sure that is necessarily true. With -z it would \n> be better to print '\\0' after each value as we do in list_vars(). \n> Ideally we wouldn't use a single string to pass multiple values around, \n> but a simple fix would be to use '\\0' to separate the individual values \n> instead of '\\n' so that we can split them unambiguously when we print them.\n\nHmph, what does \"git config -l\" do for multi-valued keys?  We should\nmimick it, I would think.\n\nAnother thing that might be worth doing is to see if we can separate\nout \"git config -l\" handling out of \"git var\" with a breaking change\nat big version boundary.\n\n> Thanks for working on this, being able to specify multiple variables \n> that are printed unambiguously is a really useful improvement.\n\nIndeed.  Thanks, both.\n"},{"id":"552181","messageId":"pull.2388.v4.git.git.1788840593177.gitgitgadget@gmail.com","threadId":"66219","inReplyTo":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","subject":"[PATCH v4] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Andrew Pleeter via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-08T04:09:53Z","receivedAt":"2026-09-08T04:09:56Z","isPatch":true,"body":"From: Andrew Pleeter <andrewpleeter@gmail.com>\n\nWhile 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\nextracting individual components (name, email, or date) currently\nrequires callers to manually parse the composite string. Furthermore,\nthere is no way to query the resolved commit signing key through\n'git var', and the command only accepts a single variable at a time.\n\nTeach 'git var' to expose individual identity components and commit\nsigning configuration, and allow querying multiple variables with\noptional NUL-termination:\n\n- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n- Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n  the resulting commit if you were to run 'git commit' right now.\n- Allow passing multiple variable arguments (e.g., 'git var\n  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n  sequentially.\n- Support '-z' to terminate variable outputs with NUL bytes.\n- Format 'git var -l -z' using the same convention as 'git config\n  list -z' (newline separating key and value, NUL separating entries).\n- Delimit values of multi-valued variables with NUL when '-z' is given.\n- Use parse_options() to strictly require options before arguments.\n- Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n    var: support broken-down idents, signing key, multiple args, and -z\n    \n    Teach git var to expose individual identity components and commit\n    signing configuration, and allow querying multiple variables with\n    optional NUL-termination.\n    \n    \n    Changes since v3:\n    =================\n    \n     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip\n       Wood and Junio C Hamano; dropped the alias mechanism and\n       commit.gpgsign check.\n     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in\n       builtin/var.c, strictly enforcing that options precede variable\n       arguments.\n     * Adopted git config list -z format (key\\nvalue\\0) for git var -l -z to\n       prevent ambiguity with = in config keys.\n     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with\n       NUL bytes under -z.\n     * Replaced char part in ident_part() with enum ident_part.\n     * Split synopsis in Documentation/git-var.adoc into separate lines for\n       -l and <variable>..., and removed awkward legacy phrasing (\"of a\n       piece of code\").\n     * Added tests in t/t0007-git-var.sh covering the new -z format,\n       multi-valued -z, and argument ordering.\n    \n    \n    Changes since v2:\n    =================\n    \n     * Drop git ident / git whoami subcommand entirely.\n     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n     * Add GIT_SIGNING_KEY to resolve commit signing keys.\n     * Teach git var to accept multiple variable arguments (git var <var1>\n       <var2> ...).\n     * Add -z option to terminate outputs with NUL bytes (including git var\n       -l -z).\n     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v4\nPull-Request: https://github.com/git/git/pull/2388\n\nRange-diff vs v3:\n\n 1:  b86340dd0e ! 1:  6fbf973e84 var: support broken-down idents, default key, multiple args, and -z\n     @@ Metadata\n      Author: Andrew Pleeter <andrewpleeter@gmail.com>\n      \n       ## Commit message ##\n     -    var: support broken-down idents, default key, multiple args, and -z\n     +    var: support broken-down idents, signing key, multiple args, and -z\n      \n          While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\n          extracting individual components (name, email, or date) currently\n     @@ Commit message\n      \n          - Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n          - Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n     -    - Add GIT_DEFAULT_KEY (with GIT_SIGNING_KEY alias) to resolve the\n     -      configured or default commit signing key ID / fingerprint.\n     +    - Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n     +      the resulting commit if you were to run 'git commit' right now.\n          - Allow passing multiple variable arguments (e.g., 'git var\n            GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n            sequentially.\n     -    - Support '-z' to terminate variable outputs and 'git var -l -z'\n     -      entries with NUL bytes.\n     +    - Support '-z' to terminate variable outputs with NUL bytes.\n     +    - Format 'git var -l -z' using the same convention as 'git config\n     +      list -z' (newline separating key and value, NUL separating entries).\n     +    - Delimit values of multi-valued variables with NUL when '-z' is given.\n     +    - Use parse_options() to strictly require options before arguments.\n          - Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n      \n          Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n     @@ Documentation/git-var.adoc: git-var - Show a Git logical variable\n       --------\n       [synopsis]\n      -git var (-l | <variable>)\n     -+git var (-l [-z] | [-z] <variable>...)\n     ++git var [-z] -l\n     ++git var [-z] <variable>...\n       \n       DESCRIPTION\n       -----------\n     @@ Documentation/git-var.adoc: OPTIONS\n       VARIABLES\n       ---------\n       `GIT_AUTHOR_IDENT`::\n     -     The author of a piece of code.\n     - \n     +-    The author of a piece of code.\n     ++    The author.\n     ++\n      +`GIT_AUTHOR_NAME`::\n     -+    The name of the author of a piece of code.\n     ++    The name of the author.\n      +\n      +`GIT_AUTHOR_EMAIL`::\n     -+    The email of the author of a piece of code.\n     ++    The email of the author.\n      +\n      +`GIT_AUTHOR_DATE`::\n     -+    The date and timezone of the author of a piece of code.\n     -+\n     - `GIT_COMMITTER_IDENT`::\n     -     The person who put a piece of code into Git.\n     ++    The date and timezone of the author.\n       \n     + `GIT_COMMITTER_IDENT`::\n     +-    The person who put a piece of code into Git.\n     ++    The committer.\n     ++\n      +`GIT_COMMITTER_NAME`::\n     -+    The name of the person who put a piece of code into Git.\n     ++    The name of the committer.\n      +\n      +`GIT_COMMITTER_EMAIL`::\n     -+    The email of the person who put a piece of code into Git.\n     ++    The email of the committer.\n      +\n      +`GIT_COMMITTER_DATE`::\n     -+    The date and timezone of the person who put a piece of code into Git.\n     -+\n     -+`GIT_DEFAULT_KEY`::\n     -+    The default commit signing key ID or fingerprint, if configured or enabled.\n     ++    The date and timezone of the committer.\n      +\n     ++`GIT_SIGNING_KEY`::\n     ++    The key that would be used to sign the resulting commit if you were\n     ++    to run `git commit` right now.\n     + \n       `GIT_EDITOR`::\n           Text editor for use by Git commands.  The value is meant to be\n     -     interpreted by the shell when it is used.  Examples: `~/bin/vi`,\n     +@@ Documentation/git-var.adoc: endif::git-default-pager[]\n     +     The path to the global (per-user) configuration files, if any.\n     + \n     + Most path values contain only one value. However, some can contain multiple\n     +-values, which are separated by newlines, and are listed in order from highest to\n     +-lowest priority.  Callers should be prepared for any such path value to contain\n     +-multiple items.\n     ++values, which are separated by newlines (or NUL bytes if `-z` is given),\n     ++and are listed in order from highest to lowest priority.  Callers should\n     ++be prepared for any such path value to contain multiple items.\n     + \n     + Note that paths are printed even if they do not exist, but not if they are\n     + disabled by other environment variables.\n      \n       ## builtin/var.c ##\n      @@\n     @@ builtin/var.c\n      +#include \"gpg-interface.h\"\n       #include \"ident.h\"\n       #include \"pager.h\"\n     - #include \"refs.h\"\n     +-#include \"refs.h\"\n     ++#include \"parse-options.h\"\n       #include \"path.h\"\n     - #include \"strbuf.h\"\n     -+#include \"strvec.h\"\n     +-#include \"strbuf.h\"\n     ++#include \"refs.h\"\n       #include \"run-command.h\"\n     ++#include \"strbuf.h\"\n     ++#include \"string-list.h\"\n     ++\n     ++static const char * const var_usage[] = {\n     ++\tN_(\"git var [-z] -l\"),\n     ++\tN_(\"git var [-z] <variable>...\"),\n     ++\tNULL\n     ++};\n       \n      -static const char var_usage[] = \"git var (-l | <variable>)\";\n     -+static const char var_usage[] = \"git var (-l [-z] | [-z] <variable>...)\";\n     ++enum ident_part {\n     ++\tIDENT_NAME,\n     ++\tIDENT_MAIL,\n     ++\tIDENT_DATE,\n     ++};\n       \n       static char *committer(int ident_flag)\n       {\n       \treturn xstrdup_or_null(git_committer_info(ident_flag));\n       }\n       \n     -+static char *ident_part(const char *ident, char part)\n     ++static char *ident_part(const char *ident, enum ident_part part)\n      +{\n      +\tstruct ident_split split;\n      +\n     @@ builtin/var.c\n      +\t\treturn NULL;\n      +\n      +\tswitch (part) {\n     -+\tcase 'n':\n     ++\tcase IDENT_NAME:\n      +\t\tif (!split.name_begin || !split.name_end)\n      +\t\t\treturn NULL;\n     -+\t\treturn xmemdupz(split.name_begin, split.name_end - split.name_begin);\n     -+\tcase 'e':\n     ++\t\treturn xmemdupz(split.name_begin,\n     ++\t\t\t\tsplit.name_end - split.name_begin);\n     ++\tcase IDENT_MAIL:\n      +\t\tif (!split.mail_begin || !split.mail_end)\n      +\t\t\treturn NULL;\n     -+\t\treturn xmemdupz(split.mail_begin, split.mail_end - split.mail_begin);\n     -+\tcase 'd':\n     ++\t\treturn xmemdupz(split.mail_begin,\n     ++\t\t\t\tsplit.mail_end - split.mail_begin);\n     ++\tcase IDENT_DATE:\n      +\t\tif (!split.date_begin)\n      +\t\t\treturn NULL;\n      +\t\tif (split.tz_end)\n     -+\t\t\treturn xmemdupz(split.date_begin, split.tz_end - split.date_begin);\n     ++\t\t\treturn xmemdupz(split.date_begin,\n     ++\t\t\t\t\tsplit.tz_end -\n     ++\t\t\t\t\tsplit.date_begin);\n      +\t\tif (split.date_end)\n     -+\t\t\treturn xmemdupz(split.date_begin, split.date_end - split.date_begin);\n     ++\t\t\treturn xmemdupz(split.date_begin,\n     ++\t\t\t\t\tsplit.date_end -\n     ++\t\t\t\t\tsplit.date_begin);\n      +\t\treturn NULL;\n      +\tdefault:\n      +\t\treturn NULL;\n     @@ builtin/var.c\n      +\n      +static char *committer_name(int ident_flag)\n      +{\n     -+\treturn ident_part(git_committer_info(ident_flag), 'n');\n     ++\treturn ident_part(git_committer_info(ident_flag), IDENT_NAME);\n      +}\n      +\n      +static char *committer_email(int ident_flag)\n      +{\n     -+\treturn ident_part(git_committer_info(ident_flag), 'e');\n     ++\treturn ident_part(git_committer_info(ident_flag), IDENT_MAIL);\n      +}\n      +\n      +static char *committer_date(int ident_flag)\n      +{\n     -+\treturn ident_part(git_committer_info(ident_flag), 'd');\n     ++\treturn ident_part(git_committer_info(ident_flag), IDENT_DATE);\n      +}\n      +\n       static char *author(int ident_flag)\n     @@ builtin/var.c\n       \n      +static char *author_name(int ident_flag)\n      +{\n     -+\treturn ident_part(git_author_info(ident_flag), 'n');\n     ++\treturn ident_part(git_author_info(ident_flag), IDENT_NAME);\n      +}\n      +\n      +static char *author_email(int ident_flag)\n      +{\n     -+\treturn ident_part(git_author_info(ident_flag), 'e');\n     ++\treturn ident_part(git_author_info(ident_flag), IDENT_MAIL);\n      +}\n      +\n      +static char *author_date(int ident_flag)\n      +{\n     -+\treturn ident_part(git_author_info(ident_flag), 'd');\n     ++\treturn ident_part(git_author_info(ident_flag), IDENT_DATE);\n      +}\n      +\n     -+static char *default_key(int ident_flag UNUSED)\n     ++static char *git_signing_key(int ident_flag UNUSED)\n      +{\n     -+\tint gpgsign = 0;\n      +\tchar *signing_key = NULL;\n      +\n     -+\tif (repo_config_get_string(the_repository, \"user.signingkey\", &signing_key) == 0 && signing_key && *signing_key)\n     ++\t/*\n     ++\t * An empty string in user.signingkey allows overriding and\n     ++\t * clearing a key defined in an outer (e.g. global) config.\n     ++\t */\n     ++\tif (!repo_config_get_string(the_repository,\n     ++\t\t\t\t    \"user.signingkey\", &signing_key)) {\n     ++\t\tif (!signing_key || !*signing_key) {\n     ++\t\t\tfree(signing_key);\n     ++\t\t\treturn NULL;\n     ++\t\t}\n      +\t\treturn signing_key;\n     -+\tfree(signing_key);\n     -+\n     -+\tif (repo_config_get_bool(the_repository, \"commit.gpgsign\", &gpgsign) == 0 && gpgsign)\n     -+\t\treturn get_signing_key_id();\n     ++\t}\n      +\n     -+\treturn NULL;\n     ++\tsigning_key = get_signing_key_id();\n     ++\tif (signing_key && !*signing_key) {\n     ++\t\tfree(signing_key);\n     ++\t\treturn NULL;\n     ++\t}\n     ++\treturn signing_key;\n      +}\n      +\n       static char *editor(int ident_flag UNUSED)\n       {\n       \treturn xstrdup_or_null(git_editor());\n     +@@ builtin/var.c: static char *git_config_val_global(int ident_flag UNUSED)\n     + \tfree(xdg);\n     + \tfree(user);\n     + \tstrbuf_trim_trailing_newline(&buf);\n     +-\tif (buf.len == 0) {\n     ++\tif (!buf.len) {\n     + \t\tstrbuf_release(&buf);\n     + \t\treturn NULL;\n     + \t}\n      @@ builtin/var.c: static struct git_var git_vars[] = {\n       \t\t.name = \"GIT_COMMITTER_IDENT\",\n       \t\t.read = committer,\n     @@ builtin/var.c: static struct git_var git_vars[] = {\n       \t\t.read = default_branch,\n       \t},\n      +\t{\n     -+\t\t.name = \"GIT_DEFAULT_KEY\",\n     -+\t\t.read = default_key,\n     ++\t\t.name = \"GIT_SIGNING_KEY\",\n     ++\t\t.read = git_signing_key,\n      +\t},\n       \t{\n       \t\t.name = \"GIT_SHELL_PATH\",\n     @@ builtin/var.c: static struct git_var git_vars[] = {\n      +static void list_vars(int null_term)\n       {\n       \tstruct git_var *ptr;\n     - \tchar *val;\n     -+\tchar eol = null_term ? '\\0' : '\\n';\n     - \n     - \tfor (ptr = git_vars; ptr->read; ptr++)\n     - \t\tif ((val = ptr->read(0))) {\n     -@@ builtin/var.c: static void list_vars(void)\n     - \n     - \t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n     - \t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n     +-\tchar *val;\n     +-\n     +-\tfor (ptr = git_vars; ptr->read; ptr++)\n     +-\t\tif ((val = ptr->read(0))) {\n     +-\t\t\tif (ptr->multivalued && *val) {\n     +-\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n     +-\n     +-\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n     +-\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n      -\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n     -+\t\t\t\t\tprintf(\"%s=%s%c\", ptr->name, list.items[i].string, eol);\n     - \t\t\t\tstring_list_clear(&list, 0);\n     - \t\t\t} else {\n     +-\t\t\t\tstring_list_clear(&list, 0);\n     +-\t\t\t} else {\n      -\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n     -+\t\t\t\tprintf(\"%s=%s%c\", ptr->name, val, eol);\n     - \t\t\t}\n     - \t\t\tfree(val);\n     +-\t\t\t}\n     +-\t\t\tfree(val);\n     ++\tchar delim = null_term ? '\\n' : '=';\n     ++\tchar eol = null_term ? '\\0' : '\\n';\n     ++\n     ++\tfor (ptr = git_vars; ptr->read; ptr++) {\n     ++\t\tchar *val = ptr->read(0);\n     ++\n     ++\t\tif (!val)\n     ++\t\t\tcontinue;\n     ++\n     ++\t\tif (ptr->multivalued && *val) {\n     ++\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n     ++\n     ++\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n     ++\t\t\tfor (size_t i = 0; i < list.nr; i++)\n     ++\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n     ++\t\t\t\t       list.items[i].string, eol);\n     ++\t\t\tstring_list_clear(&list, 0);\n     ++\t\t} else {\n     ++\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, eol);\n       \t\t}\n     -@@ builtin/var.c: static void list_vars(void)\n     ++\t\tfree(val);\n     ++\t}\n     + }\n     + \n       static const struct git_var *get_git_var(const char *var)\n       {\n       \tstruct git_var *ptr;\n     -+\tif (!strcmp(var, \"GIT_SIGNING_KEY\"))\n     -+\t\tvar = \"GIT_DEFAULT_KEY\";\n     ++\n       \tfor (ptr = git_vars; ptr->read; ptr++) {\n     - \t\tif (strcmp(var, ptr->name) == 0) {\n     +-\t\tif (strcmp(var, ptr->name) == 0) {\n     ++\t\tif (!strcmp(var, ptr->name))\n       \t\t\treturn ptr;\n     +-\t\t}\n     + \t}\n     + \treturn NULL;\n     + }\n      @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n       static int show_config(const char *var, const char *value,\n       \t\t       const struct config_context *ctx, void *cb)\n       {\n      +\tint null_term = cb ? *(int *)cb : 0;\n     -+\tchar eol = null_term ? '\\0' : '\\n';\n      +\n       \tif (value)\n      -\t\tprintf(\"%s=%s\\n\", var, value);\n     -+\t\tprintf(\"%s=%s%c\", var, value, eol);\n     ++\t\tprintf(\"%s%c%s%c\", var, null_term ? '\\n' : '=',\n     ++\t\t       value, null_term ? '\\0' : '\\n');\n       \telse\n      -\t\tprintf(\"%s\\n\", var);\n     -+\t\tprintf(\"%s%c\", var, eol);\n     ++\t\tprintf(\"%s%c\", var, null_term ? '\\0' : '\\n');\n       \treturn git_default_config(var, value, ctx, cb);\n       }\n       \n     -@@ builtin/var.c: int cmd_var(int argc,\n     - \t    const char *prefix UNUSED,\n     + int cmd_var(int argc,\n     + \t    const char **argv,\n     +-\t    const char *prefix UNUSED,\n     ++\t    const char *prefix,\n       \t    struct repository *repo UNUSED)\n       {\n      -\tconst struct git_var *git_var;\n      -\tchar *val;\n     -+\tstruct strvec vars = STRVEC_INIT;\n      +\tint list = 0;\n      +\tint null_term = 0;\n      +\tint i;\n     ++\tstruct option options[] = {\n     ++\t\tOPT_BOOL('l', NULL, &list,\n     ++\t\t\t N_(\"list all variables\")),\n     ++\t\tOPT_BOOL('z', NULL, &null_term,\n     ++\t\t\t N_(\"terminate entries with NUL\")),\n     ++\t\tOPT_END(),\n     ++\t};\n       \n     - \tshow_usage_if_asked(argc, argv, var_usage);\n     +-\tshow_usage_if_asked(argc, argv, var_usage);\n      -\tif (argc != 2)\n      -\t\tusage(var_usage);\n     ++\targc = parse_options(argc, argv, prefix, options,\n     ++\t\t\t     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n       \n      -\tif (strcmp(argv[1], \"-l\") == 0) {\n      -\t\trepo_config(the_repository, show_config, NULL);\n      -\t\tlist_vars();\n     -+\tfor (i = 1; i < argc; i++) {\n     -+\t\tconst char *arg = argv[i];\n     -+\n     -+\t\tif (!strcmp(arg, \"-l\")) {\n     -+\t\t\tlist = 1;\n     -+\t\t} else if (!strcmp(arg, \"-z\")) {\n     -+\t\t\tnull_term = 1;\n     -+\t\t} else if (!strcmp(arg, \"--\")) {\n     -+\t\t\tfor (i = i + 1; i < argc; i++)\n     -+\t\t\t\tstrvec_push(&vars, argv[i]);\n     -+\t\t\tbreak;\n     -+\t\t} else if (arg[0] == '-') {\n     -+\t\t\tusage(var_usage);\n     -+\t\t} else {\n     -+\t\t\tstrvec_push(&vars, arg);\n     -+\t\t}\n     -+\t}\n     -+\n      +\tif (list) {\n     -+\t\tif (vars.nr > 0) {\n     -+\t\t\tstrvec_clear(&vars);\n     -+\t\t\tusage(var_usage);\n     -+\t\t}\n     ++\t\tif (argc)\n     ++\t\t\tusage_with_options(var_usage, options);\n      +\t\trepo_config(the_repository, show_config, &null_term);\n      +\t\tlist_vars(null_term);\n       \t\treturn 0;\n       \t}\n     --\trepo_config(the_repository, git_default_config, NULL);\n     ++\n     ++\tif (!argc)\n     ++\t\tusage_with_options(var_usage, options);\n     ++\n     ++\tfor (i = 0; i < argc; i++) {\n     ++\t\tif (!get_git_var(argv[i]))\n     ++\t\t\tusage_with_options(var_usage, options);\n     ++\t}\n     ++\n     + \trepo_config(the_repository, git_default_config, NULL);\n       \n      -\tgit_var = get_git_var(argv[1]);\n      -\tif (!git_var)\n     -+\tif (!vars.nr)\n     - \t\tusage(var_usage);\n     - \n     --\tval = git_var->read(IDENT_STRICT);\n     --\tif (!val)\n     --\t\treturn 1;\n     -+\trepo_config(the_repository, git_default_config, NULL);\n     -+\n     -+\tfor (size_t j = 0; j < vars.nr; j++) {\n     -+\t\tconst struct git_var *git_var = get_git_var(vars.v[j]);\n     +-\t\tusage(var_usage);\n     ++\tfor (i = 0; i < argc; i++) {\n     ++\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n      +\t\tchar *val;\n      +\n     -+\t\tif (!git_var) {\n     -+\t\t\tstrvec_clear(&vars);\n     -+\t\t\tusage(var_usage);\n     -+\t\t}\n     -+\n      +\t\tval = git_var->read(IDENT_STRICT);\n     -+\t\tif (!val) {\n     -+\t\t\tstrvec_clear(&vars);\n     ++\t\tif (!val)\n      +\t\t\treturn 1;\n     -+\t\t}\n     + \n     +-\tval = git_var->read(IDENT_STRICT);\n     +-\tif (!val)\n     +-\t\treturn 1;\n     ++\t\tif (git_var->multivalued && null_term && *val) {\n     ++\t\t\tstruct string_list values = STRING_LIST_INIT_DUP;\n       \n      -\tprintf(\"%s\\n\", val);\n      -\tfree(val);\n     -+\t\tprintf(\"%s%c\", val, null_term ? '\\0' : '\\n');\n     ++\t\t\tstring_list_split(&values, val, \"\\n\", -1);\n     ++\t\t\tfor (size_t j = 0; j < values.nr; j++) {\n     ++\t\t\t\tconst char *s = values.items[j].string;\n     ++\n     ++\t\t\t\tprintf(\"%s%c\", s, '\\0');\n     ++\t\t\t}\n     ++\t\t\tstring_list_clear(&values, 0);\n     ++\t\t} else {\n     ++\t\t\tprintf(\"%s%c\", val, null_term ? '\\0' : '\\n');\n     ++\t\t}\n      +\t\tfree(val);\n      +\t}\n       \n     -+\tstrvec_clear(&vars);\n       \treturn 0;\n       }\n      \n     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '\n      +\n      +test_expect_success 'get multiple variables with -z' '\n      +\ttest_tick &&\n     -+\tprintf \"%s\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n     ++\tprintf \"%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n      +\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n     ++test_expect_success 'get multi-valued variable with -z' '\n     ++\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n     ++\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual &&\n     ++\tprintf \"%s\\0\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expected &&\n     ++\ttest_cmp expected actual\n     ++'\n     ++\n      +test_expect_success 'git var -l -z' '\n      +\tgit var -l -z >actual &&\n     -+\ttr \"\\0\" \"\\n\" <actual | grep \"^GIT_AUTHOR_NAME=\" >filtered &&\n     -+\techo \"GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\" >expect &&\n     -+\ttest_cmp expect filtered\n     ++\ttr \"\\0\" \"\\n\" <actual >actual.lines &&\n     ++\techo \"$GIT_AUTHOR_NAME\" >expect &&\n     ++\tsed -n \"/^GIT_AUTHOR_NAME$/{n;p;}\" actual.lines >actual.author &&\n     ++\ttest_cmp expect actual.author &&\n     ++\techo false >expect &&\n     ++\tsed -n \"/^core\\.bare$/{n;p;}\" actual.lines >actual.bare &&\n     ++\ttest_cmp expect actual.bare\n      +'\n      +\n     -+test_expect_success 'get GIT_DEFAULT_KEY with user.signingkey configured' '\n     ++test_expect_success 'get GIT_SIGNING_KEY with user.signingkey configured' '\n      +\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n      +\techo \"TEST_KEY_ID\" >expect &&\n     -+\tgit var GIT_DEFAULT_KEY >actual &&\n     -+\ttest_cmp expect actual &&\n     -+\tgit var GIT_SIGNING_KEY >actual.alias &&\n     -+\ttest_cmp expect actual.alias\n     ++\tgit var GIT_SIGNING_KEY >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n     -+test_expect_success 'get GIT_DEFAULT_KEY fails when unset and signing disabled' '\n     ++test_expect_success 'get GIT_SIGNING_KEY fails when unset' '\n      +\ttest_config user.signingkey \"\" &&\n     -+\ttest_config commit.gpgsign false &&\n     -+\ttest_must_fail git var GIT_DEFAULT_KEY\n     ++\ttest_must_fail git var GIT_SIGNING_KEY\n      +'\n      +\n      +test_expect_success 'git var -l lists new variables' '\n      +\tgit var -l >actual &&\n     -+\tgrep \"^GIT_AUTHOR_NAME=\" actual &&\n     -+\tgrep \"^GIT_AUTHOR_EMAIL=\" actual &&\n     -+\tgrep \"^GIT_AUTHOR_DATE=\" actual &&\n     -+\tgrep \"^GIT_COMMITTER_NAME=\" actual &&\n     -+\tgrep \"^GIT_COMMITTER_EMAIL=\" actual &&\n     -+\tgrep \"^GIT_COMMITTER_DATE=\" actual\n     ++\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n     ++\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n     ++\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n     ++\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n     ++\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n     ++\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n     ++'\n     ++\n     ++test_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '\n     ++\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n     ++\tgit var -l >actual &&\n     ++\ttest_grep \"^GIT_SIGNING_KEY=TEST_KEY_ID\" actual\n     ++'\n     ++\n     ++test_expect_success 'options must precede variable arguments' '\n     ++\ttest_must_fail git var GIT_AUTHOR_NAME -z\n      +'\n      +\n       test_done\n\n\n Documentation/git-var.adoc |  55 ++++++--\n builtin/var.c              | 255 +++++++++++++++++++++++++++++++------\n t/t0007-git-var.sh         |  95 ++++++++++++++\n 3 files changed, 355 insertions(+), 50 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex 697c10aded..999525ce71 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -9,7 +9,8 @@ git-var - Show a Git logical variable\n SYNOPSIS\n --------\n [synopsis]\n-git var (-l | <variable>)\n+git var [-z] -l\n+git var [-z] <variable>...\n \n DESCRIPTION\n -----------\n@@ -24,19 +25,55 @@ OPTIONS\n \tas well. (However, the configuration variables listing functionality\n \tis deprecated in favor of `git config list`.)\n \n+`-z`::\n+\tTerminate entries with NUL instead of newline.\n+\n EXAMPLES\n --------\n-\t$ git var GIT_AUTHOR_IDENT\n-\tEric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n-\n+* Get the author identity:\n++\n+------------\n+$ git var GIT_AUTHOR_IDENT\n+Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n+------------\n+\n+* Get the author name and email:\n++\n+------------\n+$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL\n+Eric W. Biederman\n+ebiederm@lnxi.com\n+------------\n \n VARIABLES\n ---------\n `GIT_AUTHOR_IDENT`::\n-    The author of a piece of code.\n+    The author.\n+\n+`GIT_AUTHOR_NAME`::\n+    The name of the author.\n+\n+`GIT_AUTHOR_EMAIL`::\n+    The email of the author.\n+\n+`GIT_AUTHOR_DATE`::\n+    The date and timezone of the author.\n \n `GIT_COMMITTER_IDENT`::\n-    The person who put a piece of code into Git.\n+    The committer.\n+\n+`GIT_COMMITTER_NAME`::\n+    The name of the committer.\n+\n+`GIT_COMMITTER_EMAIL`::\n+    The email of the committer.\n+\n+`GIT_COMMITTER_DATE`::\n+    The date and timezone of the committer.\n+\n+`GIT_SIGNING_KEY`::\n+    The key that would be used to sign the resulting commit if you were\n+    to run `git commit` right now.\n \n `GIT_EDITOR`::\n     Text editor for use by Git commands.  The value is meant to be\n@@ -85,9 +122,9 @@ endif::git-default-pager[]\n     The path to the global (per-user) configuration files, if any.\n \n Most path values contain only one value. However, some can contain multiple\n-values, which are separated by newlines, and are listed in order from highest to\n-lowest priority.  Callers should be prepared for any such path value to contain\n-multiple items.\n+values, which are separated by newlines (or NUL bytes if `-z` is given),\n+and are listed in order from highest to lowest priority.  Callers should\n+be prepared for any such path value to contain multiple items.\n \n Note that paths are printed even if they do not exist, but not if they are\n disabled by other environment variables.\ndiff --git a/builtin/var.c b/builtin/var.c\nindex cc3a43cde2..6fc037543a 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -12,25 +12,130 @@\n #include \"config.h\"\n #include \"editor.h\"\n #include \"environment.h\"\n+#include \"gpg-interface.h\"\n #include \"ident.h\"\n #include \"pager.h\"\n-#include \"refs.h\"\n+#include \"parse-options.h\"\n #include \"path.h\"\n-#include \"strbuf.h\"\n+#include \"refs.h\"\n #include \"run-command.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n+\n+static const char * const var_usage[] = {\n+\tN_(\"git var [-z] -l\"),\n+\tN_(\"git var [-z] <variable>...\"),\n+\tNULL\n+};\n \n-static const char var_usage[] = \"git var (-l | <variable>)\";\n+enum ident_part {\n+\tIDENT_NAME,\n+\tIDENT_MAIL,\n+\tIDENT_DATE,\n+};\n \n static char *committer(int ident_flag)\n {\n \treturn xstrdup_or_null(git_committer_info(ident_flag));\n }\n \n+static char *ident_part(const char *ident, enum ident_part part)\n+{\n+\tstruct ident_split split;\n+\n+\tif (!ident)\n+\t\treturn NULL;\n+\tif (split_ident_line(&split, ident, strlen(ident)))\n+\t\treturn NULL;\n+\n+\tswitch (part) {\n+\tcase IDENT_NAME:\n+\t\tif (!split.name_begin || !split.name_end)\n+\t\t\treturn NULL;\n+\t\treturn xmemdupz(split.name_begin,\n+\t\t\t\tsplit.name_end - split.name_begin);\n+\tcase IDENT_MAIL:\n+\t\tif (!split.mail_begin || !split.mail_end)\n+\t\t\treturn NULL;\n+\t\treturn xmemdupz(split.mail_begin,\n+\t\t\t\tsplit.mail_end - split.mail_begin);\n+\tcase IDENT_DATE:\n+\t\tif (!split.date_begin)\n+\t\t\treturn NULL;\n+\t\tif (split.tz_end)\n+\t\t\treturn xmemdupz(split.date_begin,\n+\t\t\t\t\tsplit.tz_end -\n+\t\t\t\t\tsplit.date_begin);\n+\t\tif (split.date_end)\n+\t\t\treturn xmemdupz(split.date_begin,\n+\t\t\t\t\tsplit.date_end -\n+\t\t\t\t\tsplit.date_begin);\n+\t\treturn NULL;\n+\tdefault:\n+\t\treturn NULL;\n+\t}\n+}\n+\n+static char *committer_name(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *committer_email(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *committer_date(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_DATE);\n+}\n+\n static char *author(int ident_flag)\n {\n \treturn xstrdup_or_null(git_author_info(ident_flag));\n }\n \n+static char *author_name(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *author_email(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *author_date(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_DATE);\n+}\n+\n+static char *git_signing_key(int ident_flag UNUSED)\n+{\n+\tchar *signing_key = NULL;\n+\n+\t/*\n+\t * An empty string in user.signingkey allows overriding and\n+\t * clearing a key defined in an outer (e.g. global) config.\n+\t */\n+\tif (!repo_config_get_string(the_repository,\n+\t\t\t\t    \"user.signingkey\", &signing_key)) {\n+\t\tif (!signing_key || !*signing_key) {\n+\t\t\tfree(signing_key);\n+\t\t\treturn NULL;\n+\t\t}\n+\t\treturn signing_key;\n+\t}\n+\n+\tsigning_key = get_signing_key_id();\n+\tif (signing_key && !*signing_key) {\n+\t\tfree(signing_key);\n+\t\treturn NULL;\n+\t}\n+\treturn signing_key;\n+}\n+\n static char *editor(int ident_flag UNUSED)\n {\n \treturn xstrdup_or_null(git_editor());\n@@ -108,7 +213,7 @@ static char *git_config_val_global(int ident_flag UNUSED)\n \tfree(xdg);\n \tfree(user);\n \tstrbuf_trim_trailing_newline(&buf);\n-\tif (buf.len == 0) {\n+\tif (!buf.len) {\n \t\tstrbuf_release(&buf);\n \t\treturn NULL;\n \t}\n@@ -125,10 +230,34 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_COMMITTER_IDENT\",\n \t\t.read = committer,\n \t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_NAME\",\n+\t\t.read = committer_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_EMAIL\",\n+\t\t.read = committer_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_DATE\",\n+\t\t.read = committer_date,\n+\t},\n \t{\n \t\t.name = \"GIT_AUTHOR_IDENT\",\n \t\t.read = author,\n \t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_NAME\",\n+\t\t.read = author_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_EMAIL\",\n+\t\t.read = author_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_DATE\",\n+\t\t.read = author_date,\n+\t},\n \t{\n \t\t.name = \"GIT_EDITOR\",\n \t\t.read = editor,\n@@ -145,6 +274,10 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_DEFAULT_BRANCH\",\n \t\t.read = default_branch,\n \t},\n+\t{\n+\t\t.name = \"GIT_SIGNING_KEY\",\n+\t\t.read = git_signing_key,\n+\t},\n \t{\n \t\t.name = \"GIT_SHELL_PATH\",\n \t\t.read = shell_path,\n@@ -172,34 +305,40 @@ static struct git_var git_vars[] = {\n \t},\n };\n \n-static void list_vars(void)\n+static void list_vars(int null_term)\n {\n \tstruct git_var *ptr;\n-\tchar *val;\n-\n-\tfor (ptr = git_vars; ptr->read; ptr++)\n-\t\tif ((val = ptr->read(0))) {\n-\t\t\tif (ptr->multivalued && *val) {\n-\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n-\n-\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n-\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n-\t\t\t\tstring_list_clear(&list, 0);\n-\t\t\t} else {\n-\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n-\t\t\t}\n-\t\t\tfree(val);\n+\tchar delim = null_term ? '\\n' : '=';\n+\tchar eol = null_term ? '\\0' : '\\n';\n+\n+\tfor (ptr = git_vars; ptr->read; ptr++) {\n+\t\tchar *val = ptr->read(0);\n+\n+\t\tif (!val)\n+\t\t\tcontinue;\n+\n+\t\tif (ptr->multivalued && *val) {\n+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\n+\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n+\t\t\tfor (size_t i = 0; i < list.nr; i++)\n+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n+\t\t\t\t       list.items[i].string, eol);\n+\t\t\tstring_list_clear(&list, 0);\n+\t\t} else {\n+\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, eol);\n \t\t}\n+\t\tfree(val);\n+\t}\n }\n \n static const struct git_var *get_git_var(const char *var)\n {\n \tstruct git_var *ptr;\n+\n \tfor (ptr = git_vars; ptr->read; ptr++) {\n-\t\tif (strcmp(var, ptr->name) == 0) {\n+\t\tif (!strcmp(var, ptr->name))\n \t\t\treturn ptr;\n-\t\t}\n \t}\n \treturn NULL;\n }\n@@ -207,42 +346,76 @@ static const struct git_var *get_git_var(const char *var)\n static int show_config(const char *var, const char *value,\n \t\t       const struct config_context *ctx, void *cb)\n {\n+\tint null_term = cb ? *(int *)cb : 0;\n+\n \tif (value)\n-\t\tprintf(\"%s=%s\\n\", var, value);\n+\t\tprintf(\"%s%c%s%c\", var, null_term ? '\\n' : '=',\n+\t\t       value, null_term ? '\\0' : '\\n');\n \telse\n-\t\tprintf(\"%s\\n\", var);\n+\t\tprintf(\"%s%c\", var, null_term ? '\\0' : '\\n');\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n int cmd_var(int argc,\n \t    const char **argv,\n-\t    const char *prefix UNUSED,\n+\t    const char *prefix,\n \t    struct repository *repo UNUSED)\n {\n-\tconst struct git_var *git_var;\n-\tchar *val;\n+\tint list = 0;\n+\tint null_term = 0;\n+\tint i;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL('l', NULL, &list,\n+\t\t\t N_(\"list all variables\")),\n+\t\tOPT_BOOL('z', NULL, &null_term,\n+\t\t\t N_(\"terminate entries with NUL\")),\n+\t\tOPT_END(),\n+\t};\n \n-\tshow_usage_if_asked(argc, argv, var_usage);\n-\tif (argc != 2)\n-\t\tusage(var_usage);\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (strcmp(argv[1], \"-l\") == 0) {\n-\t\trepo_config(the_repository, show_config, NULL);\n-\t\tlist_vars();\n+\tif (list) {\n+\t\tif (argc)\n+\t\t\tusage_with_options(var_usage, options);\n+\t\trepo_config(the_repository, show_config, &null_term);\n+\t\tlist_vars(null_term);\n \t\treturn 0;\n \t}\n+\n+\tif (!argc)\n+\t\tusage_with_options(var_usage, options);\n+\n+\tfor (i = 0; i < argc; i++) {\n+\t\tif (!get_git_var(argv[i]))\n+\t\t\tusage_with_options(var_usage, options);\n+\t}\n+\n \trepo_config(the_repository, git_default_config, NULL);\n \n-\tgit_var = get_git_var(argv[1]);\n-\tif (!git_var)\n-\t\tusage(var_usage);\n+\tfor (i = 0; i < argc; i++) {\n+\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n+\t\tchar *val;\n+\n+\t\tval = git_var->read(IDENT_STRICT);\n+\t\tif (!val)\n+\t\t\treturn 1;\n \n-\tval = git_var->read(IDENT_STRICT);\n-\tif (!val)\n-\t\treturn 1;\n+\t\tif (git_var->multivalued && null_term && *val) {\n+\t\t\tstruct string_list values = STRING_LIST_INIT_DUP;\n \n-\tprintf(\"%s\\n\", val);\n-\tfree(val);\n+\t\t\tstring_list_split(&values, val, \"\\n\", -1);\n+\t\t\tfor (size_t j = 0; j < values.nr; j++) {\n+\t\t\t\tconst char *s = values.items[j].string;\n+\n+\t\t\t\tprintf(\"%s%c\", s, '\\0');\n+\t\t\t}\n+\t\t\tstring_list_clear(&values, 0);\n+\t\t} else {\n+\t\t\tprintf(\"%s%c\", val, null_term ? '\\0' : '\\n');\n+\t\t}\n+\t\tfree(val);\n+\t}\n \n \treturn 0;\n }\ndiff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\nindex 2b60317758..27cc595291 100755\n--- a/t/t0007-git-var.sh\n+++ b/t/t0007-git-var.sh\n@@ -276,4 +276,99 @@ test_expect_success '`git var -l` works even without HOME' '\n \t)\n '\n \n+test_expect_success 'get author identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n+\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n+\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n+\tgit var GIT_AUTHOR_NAME >actual.name &&\n+\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n+\tgit var GIT_AUTHOR_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get committer identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n+\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n+\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n+\tgit var GIT_COMMITTER_NAME >actual.name &&\n+\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n+\tgit var GIT_COMMITTER_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get multiple variables' '\n+\ttest_tick &&\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME\n+\t$GIT_AUTHOR_EMAIL\n+\t$GIT_COMMITTER_NAME\n+\t$GIT_COMMITTER_EMAIL\n+\tEOF\n+\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z' '\n+\ttest_tick &&\n+\tprintf \"%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual &&\n+\tprintf \"%s\\0\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expected &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git var -l -z' '\n+\tgit var -l -z >actual &&\n+\ttr \"\\0\" \"\\n\" <actual >actual.lines &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect &&\n+\tsed -n \"/^GIT_AUTHOR_NAME$/{n;p;}\" actual.lines >actual.author &&\n+\ttest_cmp expect actual.author &&\n+\techo false >expect &&\n+\tsed -n \"/^core\\.bare$/{n;p;}\" actual.lines >actual.bare &&\n+\ttest_cmp expect actual.bare\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY with user.signingkey configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\techo \"TEST_KEY_ID\" >expect &&\n+\tgit var GIT_SIGNING_KEY >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '\n+\ttest_config user.signingkey \"\" &&\n+\ttest_must_fail git var GIT_SIGNING_KEY\n+'\n+\n+test_expect_success 'git var -l lists new variables' '\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n+'\n+\n+test_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_SIGNING_KEY=TEST_KEY_ID\" actual\n+'\n+\n+test_expect_success 'options must precede variable arguments' '\n+\ttest_must_fail git var GIT_AUTHOR_NAME -z\n+'\n+\n test_done\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n-- \ngitgitgadget\n"},{"id":"552193","messageId":"f3db3c4c-f80d-4f27-bc5a-e339e62d5ea2@gmail.com","threadId":"66219","inReplyTo":"xmqqqzj957rm.fsf@gitster.g","subject":"Re: [PATCH v3] var: support broken-down idents, default key, multiple args, and -z","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-08T09:07:51Z","receivedAt":"2026-09-08T09:07:55Z","isPatch":true,"body":"Hi Junio\n\nOn 04/09/2026 16:57, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n> \n>>>    \tif (value)\n>>> -\t\tprintf(\"%s=%s\\n\", var, value);\n>>> +\t\tprintf(\"%s=%s%c\", var, value, eol);\n>>\n>> A key can contain \"=\" so this format is ambiguous. When the user passes\n>> \"-z\" we should use the same format as \"git config list -z\" which avoids\n>> that ambiguity\n>>\n>> \tprintf(\"%s%c%s%c\", var, eol == '\\n' ? '=' : '\\n', value, eol);\n>>\n>>>    \telse\n>>> -\t\tprintf(\"%s\\n\", var);\n>>> +\t\tprintf(\"%s%c\", var, eol);\n>>\n>> It would be worth checking what \"git config list -z\" does when there is\n>> no value and matching that. Does it print \"key\\n\\0\", or \"key\\0\"?\n> \n> By \"key\" do you mean \"var\"? \n\nI meant the config key which is in variable var\n\n> The namespace of \"var\" for \"git var\"\n> proper (like GIT_COMMITTER_IDENT) are very much under our control,\n> but it also gives all the configuration variables, whose names can\n> indeed have '=' in a three-level varlable name.  This is an\n> excellent suggestion.\n\n>>> [...]\n>>> -\tprintf(\"%s\\n\", val);\n>>> -\tfree(val);\n>>> +\t\tprintf(\"%s%c\", val, null_term ? '\\0' : '\\n');\n>>\n>> Multi-valued variables are a bit of a problem here, they're built on the\n>> assumption that the individual values do not contain a newline, but as\n>> they are paths I'm not sure that is necessarily true. With -z it would\n>> be better to print '\\0' after each value as we do in list_vars().\n>> Ideally we wouldn't use a single string to pass multiple values around,\n>> but a simple fix would be to use '\\0' to separate the individual values\n>> instead of '\\n' so that we can split them unambiguously when we print them.\n> \n> Hmph, what does \"git config -l\" do for multi-valued keys?  We should\n> mimick it, I would think.\n\nWith -z it nul terminates each value. I wonder if we should be printing \nthe variable names here when the user passes more than one var name. \nThat would make it easier to parse multivalued vars which can have a \nvariable number of values, or we could print an extra delimiter after \nthe last value of multivalued vars like \"git merge-tree\" does to \nseparate the different sections of its output.\n\n> Another thing that might be worth doing is to see if we can separate\n> out \"git config -l\" handling out of \"git var\" with a breaking change\n> at big version boundary.\n\nYes, it would be nice to be able to print just the GIT_* vars without \nhaving to print the config as well.\n\n>> Thanks for working on this, being able to specify multiple variables\n>> that are printed unambiguously is a really useful improvement.\n> \n> Indeed.  Thanks, both.\n> \n\n"},{"id":"552213","messageId":"1a38944e-9895-474a-a6ad-277638aa49d0@gmail.com","threadId":"66219","inReplyTo":"pull.2388.v4.git.git.1788840593177.gitgitgadget@gmail.com","subject":"Re: [PATCH v4] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-08T13:54:42Z","receivedAt":"2026-09-08T13:54:48Z","isPatch":true,"body":"\n\nOn 08/09/2026 05:09, Andrew Pleeter via GitGitGadget wrote:\n> From: Andrew Pleeter <andrewpleeter@gmail.com>\n> \n> While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\n> extracting individual components (name, email, or date) currently\n> requires callers to manually parse the composite string. Furthermore,\n> there is no way to query the resolved commit signing key through\n> 'git var', and the command only accepts a single variable at a time.\n> \n> Teach 'git var' to expose individual identity components and commit\n> signing configuration, and allow querying multiple variables with\n> optional NUL-termination:\n> \n> - Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n> - Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n> - Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n>    the resulting commit if you were to run 'git commit' right now.\n> - Allow passing multiple variable arguments (e.g., 'git var\n>    GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n>    sequentially.\n> - Support '-z' to terminate variable outputs with NUL bytes.\n> - Format 'git var -l -z' using the same convention as 'git config\n>    list -z' (newline separating key and value, NUL separating entries).\n> - Delimit values of multi-valued variables with NUL when '-z' is given.\n> - Use parse_options() to strictly require options before arguments.\n> - Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n> \n> Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n> ---\n>      var: support broken-down idents, signing key, multiple args, and -z\n>      \n>      Teach git var to expose individual identity components and commit\n>      signing configuration, and allow querying multiple variables with\n>      optional NUL-termination.\n>      \n>      \n>      Changes since v3:\n>      =================\n>      \n>       * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip\n>         Wood and Junio C Hamano; dropped the alias mechanism and\n>         commit.gpgsign check.\n>       * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in\n>         builtin/var.c, strictly enforcing that options precede variable\n>         arguments.\n>       * Adopted git config list -z format (key\\nvalue\\0) for git var -l -z to\n>         prevent ambiguity with = in config keys.\n>       * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with\n>         NUL bytes under -z.\n>       * Replaced char part in ident_part() with enum ident_part.\n>       * Split synopsis in Documentation/git-var.adoc into separate lines for\n>         -l and <variable>..., and removed awkward legacy phrasing (\"of a\n>         piece of code\").\n>       * Added tests in t/t0007-git-var.sh covering the new -z format,\n>         multi-valued -z, and argument ordering.\n> \n\nThat all sounds good, lets look at the code ...\n\n> diff --git a/builtin/var.c b/builtin/var.c\n> index cc3a43cde2..6fc037543a 100644\n> --- a/builtin/var.c\n> +++ b/builtin/var.c\n > [...]> +static char *git_signing_key(int ident_flag UNUSED)\n> +{\n> +\tchar *signing_key = NULL;\n> +\n> +\t/*\n> +\t * An empty string in user.signingkey allows overriding and\n> +\t * clearing a key defined in an outer (e.g. global) config.\n> +\t */\n> +\tif (!repo_config_get_string(the_repository,\n> +\t\t\t\t    \"user.signingkey\", &signing_key)) {\n> +\t\tif (!signing_key || !*signing_key) {\n> +\t\t\tfree(signing_key);\n> +\t\t\treturn NULL;\n> +\t\t}\n> +\t\treturn signing_key;\n> +\t}\n> +\n> +\tsigning_key = get_signing_key_id();\n> +\tif (signing_key && !*signing_key) {\n> +\t\tfree(signing_key);\n> +\t\treturn NULL;\n> +\t}\n > +\treturn signing_key;\n > +}\n\nLooking at sign_buffer() in gpg-interface.c it looks like git calls \nget_signing_key() to obtain the default key - why are we doing something \ndifferent here? I'm also still curious how this is expected to be used.\n\n> -static void list_vars(void)\n> +static void list_vars(int null_term)\n>   {\n>   \tstruct git_var *ptr;\n> -\tchar *val;\n> -\n> -\tfor (ptr = git_vars; ptr->read; ptr++)\n> -\t\tif ((val = ptr->read(0))) {\n> -\t\t\tif (ptr->multivalued && *val) {\n> -\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n> -\n> -\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n> -\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n> -\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n> -\t\t\t\tstring_list_clear(&list, 0);\n> -\t\t\t} else {\n> -\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n> -\t\t\t}\n> -\t\t\tfree(val);\n> +\tchar delim = null_term ? '\\n' : '=';\n\nWe are in control of the variable names and we know they do not \ncurrently contain '=' so we don't currently need a different format here \nwith '-z'. However it is possible that might change in the future (for \nexample using \"GIT_PAGER:<my-command>\" to return the pager for \n\"<my-command>\" that could be an alias containing '=') so using the same \nformat as config keys is probably a good idea. We should document the \nformat above.\n\n> +\tchar eol = null_term ? '\\0' : '\\n';\n> +\n> +\tfor (ptr = git_vars; ptr->read; ptr++) {\n> +\t\tchar *val = ptr->read(0);\n> +\n> +\t\tif (!val)\n> +\t\t\tcontinue;\n> +\n> +\t\tif (ptr->multivalued && *val) {\n> +\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n> +\n> +\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n\nAs I said before, I think we should switch to using '\\0' instead of '\\n' \nwhen we build the multivalued string so that we can safely handle values \nthat contain '\\n'.\n\n> +\t\t\tfor (size_t i = 0; i < list.nr; i++)\n> +\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n> +\t\t\t\t       list.items[i].string, eol);\n> +\t\t\tstring_list_clear(&list, 0);\n> +\t\t} else {\n> +\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, eol);\n>   \t\t}\n> +\t\tfree(val);\n> +\t}\n>   }   \n\n> @@ -207,42 +346,76 @@ static const struct git_var *get_git_var(const char *var)\n>   static int show_config(const char *var, const char *value,\n>   \t\t       const struct config_context *ctx, void *cb)\n>   {\n> +\tint null_term = cb ? *(int *)cb : 0;\n\nThis seems unnecessarily complicated, can't we just make sure we always \npass a non-null pointer cb? Also '\\0' is known as NUL, not NULL.\n\n\tint *nul_term = cb;\n\tchar term = *nul_term ? '\\0' : '\\n';\n\tchar delim = *nul_term ? '\\n' : '=';\n\nand then use term and delim below.\n\n> +\n>   \tif (value)\n> -\t\tprintf(\"%s=%s\\n\", var, value);\n> +\t\tprintf(\"%s%c%s%c\", var, null_term ? '\\n' : '=',\n> +\t\t       value, null_term ? '\\0' : '\\n');\n>   \telse\n> -\t\tprintf(\"%s\\n\", var);\n> +\t\tprintf(\"%s%c\", var, null_term ? '\\0' : '\\n');\n>   \treturn git_default_config(var, value, ctx, cb);\n>   }\n>   \n>   int cmd_var(int argc,\n>   \t    const char **argv,\n> -\t    const char *prefix UNUSED,\n> +\t    const char *prefix,\n>   \t    struct repository *repo UNUSED)\n>   {\n> -\tconst struct git_var *git_var;\n> -\tchar *val;\n> +\tint list = 0;\n> +\tint null_term = 0;\n> +\tint i;\n> +\tstruct option options[] = {\n> +\t\tOPT_BOOL('l', NULL, &list,\n> +\t\t\t N_(\"list all variables\")),\n> +\t\tOPT_BOOL('z', NULL, &null_term,\n> +\t\t\t N_(\"terminate entries with NUL\")),\n\nThe help is correct, we should use nul_term as the variable name. Using \nparse_options() is a nice improvement.\n\n> +\t\tOPT_END(),\n> +\t};\n>   \n> -\tshow_usage_if_asked(argc, argv, var_usage);\n> -\tif (argc != 2)\n> -\t\tusage(var_usage);\n> +\targc = parse_options(argc, argv, prefix, options,\n> +\t\t\t     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n>   \n> -\tif (strcmp(argv[1], \"-l\") == 0) {\n> -\t\trepo_config(the_repository, show_config, NULL);\n> -\t\tlist_vars();\n> +\tif (list) {\n> +\t\tif (argc)\n> +\t\t\tusage_with_options(var_usage, options);\n> +\t\trepo_config(the_repository, show_config, &null_term);\n> +\t\tlist_vars(null_term);\n>   \t\treturn 0;\n>   \t}\n> +\n> +\tif (!argc)\n> +\t\tusage_with_options(var_usage, options);\n> +\n> +\tfor (i = 0; i < argc; i++) {\n> +\t\tif (!get_git_var(argv[i]))\n> +\t\t\tusage_with_options(var_usage, options);\n\nDo we really need to walk all the var names here - can't we just error \nout if we see an invalid one later?\n\n> +\t}\n> +\n>   \trepo_config(the_repository, git_default_config, NULL);\n>   \n> -\tgit_var = get_git_var(argv[1]);\n> -\tif (!git_var)\n> -\t\tusage(var_usage);\n> +\tfor (i = 0; i < argc; i++) {\n> +\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n> +\t\tchar *val;\n> +\n> +\t\tval = git_var->read(IDENT_STRICT);\n> +\t\tif (!val)\n> +\t\t\treturn 1;\n\nIf the user asked for multiple vars to be printed, erroring out because \none is not set is not very friendly, It would be better to print a blank \nrecord and continue.\n\n>   \n> -\tval = git_var->read(IDENT_STRICT);\n> -\tif (!val)\n> -\t\treturn 1;\n> +\t\tif (git_var->multivalued && null_term && *val) {\n\nWhy \"*val\" ?\n\n> +\t\t\tstruct string_list values = STRING_LIST_INIT_DUP;\n>   \n> -\tprintf(\"%s\\n\", val);\n> -\tfree(val);\n> +\t\t\tstring_list_split(&values, val, \"\\n\", -1);\n> +\t\t\tfor (size_t j = 0; j < values.nr; j++) {\n> +\t\t\t\tconst char *s = values.items[j].string;\n> +\n> +\t\t\t\tprintf(\"%s%c\", s, '\\0');\n\nIf we're printing multiple var then the caller has no way to tell if a \nvar has multiple values which makes it tricky or impossible to match up \nthe values we print to the vars that were requested. We could change the \noutput format when multiple vars are requested to print the var name as \nwill like we do with '-l', or we could print an extra terminator after a \nmulti-valued var and properly document which vars are multi-valued. The \nlatter means the caller can match up the values without worrying about \nparsing the var names.\n\nThanks\n\nPhillip\n\n> +\t\t\t}\n> +\t\t\tstring_list_clear(&values, 0);\n> +\t\t} else {\n> +\t\t\tprintf(\"%s%c\", val, null_term ? '\\0' : '\\n');\n> +\t\t}\n> +\t\tfree(val);\n> +\t}\n>   \n>   \treturn 0;\n>   }\n> diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\n> index 2b60317758..27cc595291 100755\n> --- a/t/t0007-git-var.sh\n> +++ b/t/t0007-git-var.sh\n> @@ -276,4 +276,99 @@ test_expect_success '`git var -l` works even without HOME' '\n>   \t)\n>   '\n>   \n> +test_expect_success 'get author identity components' '\n> +\ttest_tick &&\n> +\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n> +\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n> +\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n> +\tgit var GIT_AUTHOR_NAME >actual.name &&\n> +\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n> +\tgit var GIT_AUTHOR_DATE >actual.date &&\n> +\ttest_cmp expect.name actual.name &&\n> +\ttest_cmp expect.email actual.email &&\n> +\ttest_cmp expect.date actual.date\n> +'\n> +\n> +test_expect_success 'get committer identity components' '\n> +\ttest_tick &&\n> +\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n> +\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n> +\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n> +\tgit var GIT_COMMITTER_NAME >actual.name &&\n> +\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n> +\tgit var GIT_COMMITTER_DATE >actual.date &&\n> +\ttest_cmp expect.name actual.name &&\n> +\ttest_cmp expect.email actual.email &&\n> +\ttest_cmp expect.date actual.date\n> +'\n> +\n> +test_expect_success 'get multiple variables' '\n> +\ttest_tick &&\n> +\tcat >expect <<-EOF &&\n> +\t$GIT_AUTHOR_NAME\n> +\t$GIT_AUTHOR_EMAIL\n> +\t$GIT_COMMITTER_NAME\n> +\t$GIT_COMMITTER_EMAIL\n> +\tEOF\n> +\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'get multiple variables with -z' '\n> +\ttest_tick &&\n> +\tprintf \"%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n> +\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'get multi-valued variable with -z' '\n> +\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n> +\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual &&\n> +\tprintf \"%s\\0\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expected &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'git var -l -z' '\n> +\tgit var -l -z >actual &&\n> +\ttr \"\\0\" \"\\n\" <actual >actual.lines &&\n> +\techo \"$GIT_AUTHOR_NAME\" >expect &&\n> +\tsed -n \"/^GIT_AUTHOR_NAME$/{n;p;}\" actual.lines >actual.author &&\n> +\ttest_cmp expect actual.author &&\n> +\techo false >expect &&\n> +\tsed -n \"/^core\\.bare$/{n;p;}\" actual.lines >actual.bare &&\n> +\ttest_cmp expect actual.bare\n> +'\n> +\n> +test_expect_success 'get GIT_SIGNING_KEY with user.signingkey configured' '\n> +\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n> +\techo \"TEST_KEY_ID\" >expect &&\n> +\tgit var GIT_SIGNING_KEY >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'get GIT_SIGNING_KEY fails when unset' '\n> +\ttest_config user.signingkey \"\" &&\n> +\ttest_must_fail git var GIT_SIGNING_KEY\n> +'\n> +\n> +test_expect_success 'git var -l lists new variables' '\n> +\tgit var -l >actual &&\n> +\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n> +\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n> +\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n> +\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n> +\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n> +\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n> +'\n> +\n> +test_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '\n> +\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n> +\tgit var -l >actual &&\n> +\ttest_grep \"^GIT_SIGNING_KEY=TEST_KEY_ID\" actual\n> +'\n> +\n> +test_expect_success 'options must precede variable arguments' '\n> +\ttest_must_fail git var GIT_AUTHOR_NAME -z\n> +'\n> +\n>   test_done\n> \n> base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n\n"},{"id":"552260","messageId":"pull.2388.v5.git.git.1788900182711.gitgitgadget@gmail.com","threadId":"66219","inReplyTo":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","subject":"[PATCH v5] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Andrew Pleeter via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-08T20:43:02Z","receivedAt":"2026-09-08T20:43:05Z","isPatch":true,"body":"From: Andrew Pleeter <andrewpleeter@gmail.com>\n\nWhile 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\nextracting individual components (name, email, or date) currently\nrequires callers to manually parse the composite string. Furthermore,\nthere is no way to query the resolved commit signing key through\n'git var', and the command only accepts a single variable at a time.\n\nTeach 'git var' to expose individual identity components and commit\nsigning configuration, and allow querying multiple variables with\noptional NUL-termination:\n\n- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n- Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n  the resulting commit if you were to run 'git commit' right now.\n- Allow passing multiple variable arguments (e.g., 'git var\n  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n  sequentially.\n- Support '-z' to terminate variable outputs with NUL bytes.\n- Format 'git var -l -z' using the same convention as 'git config\n  list -z' (newline separating key and value, NUL separating entries).\n- Delimit values of multi-valued variables with NUL when '-z' is given,\n  and output an extra delimiter after multi-valued variables when\n  querying multiple variables to disambiguate the stream.\n- When querying multiple variables, print an empty record for any\n  variable that has no value and continue processing remaining variables.\n- Use parse_options() to strictly require options before arguments.\n- Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n    var: support broken-down idents, signing key, multiple args, and -z\n    \n    Teach git var to expose individual identity components and commit\n    signing configuration, and allow querying multiple variables with\n    optional NUL-termination.\n    \n    \n    Changes since v4:\n    =================\n    \n     * Simplified git_signing_key() to directly call get_signing_key() as\n       used throughout Git (in tag, send-pack, and sign_buffer()).\n     * Renamed null_term to nul_term across builtin/var.c, and simplified\n       show_config() callback handling.\n     * Replaced the redundant pre-validation loop in cmd_var() by validating\n       arguments directly in the main execution loop.\n     * When querying multiple variables, print an empty record (blank line\n       or \\0 with -z) for any variable that has no value, and continue\n       printing remaining variables instead of terminating prematurely.\n     * Switched multi-valued variable storage (git_config_val_global()) to\n       internal \\0 delimiters, iterating directly through string sequences\n       without allocating a temporary string_list.\n     * For multi-variable queries, output an extra delimiter (\\n or \\0)\n       after multi-valued variables to clearly mark the end of their list.\n     * Explicitly documented the git var -l -z format and multi-variable\n       handling in Documentation/git-var.adoc.\n     * Added comprehensive tests for unset variables and multi-valued stream\n       delimiters in t/t0007-git-var.sh.\n    \n    \n    Changes since v3:\n    =================\n    \n     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip\n       Wood and Junio C Hamano; dropped the alias mechanism and\n       commit.gpgsign check.\n     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in\n       builtin/var.c, strictly enforcing that options precede variable\n       arguments.\n     * Adopted git config list -z format (key\\nvalue\\0) for git var -l -z to\n       prevent ambiguity with = in config keys.\n     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with\n       NUL bytes under -z.\n     * Replaced char part in ident_part() with enum ident_part.\n     * Split synopsis in Documentation/git-var.adoc into separate lines for\n       -l and <variable>..., and removed awkward legacy phrasing (\"of a\n       piece of code\").\n     * Added tests in t/t0007-git-var.sh covering the new -z format,\n       multi-valued -z, and argument ordering.\n    \n    \n    Changes since v2:\n    =================\n    \n     * Drop git ident / git whoami subcommand entirely.\n     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n     * Add GIT_SIGNING_KEY to resolve commit signing keys.\n     * Teach git var to accept multiple variable arguments (git var <var1>\n       <var2> ...).\n     * Add -z option to terminate outputs with NUL bytes (including git var\n       -l -z).\n     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v5\nPull-Request: https://github.com/git/git/pull/2388\n\nRange-diff vs v4:\n\n 1:  6fbf973e84 ! 1:  04833673ea var: support broken-down idents, signing key, multiple args, and -z\n     @@ Commit message\n          - Support '-z' to terminate variable outputs with NUL bytes.\n          - Format 'git var -l -z' using the same convention as 'git config\n            list -z' (newline separating key and value, NUL separating entries).\n     -    - Delimit values of multi-valued variables with NUL when '-z' is given.\n     +    - Delimit values of multi-valued variables with NUL when '-z' is given,\n     +      and output an extra delimiter after multi-valued variables when\n     +      querying multiple variables to disambiguate the stream.\n     +    - When querying multiple variables, print an empty record for any\n     +      variable that has no value and continue processing remaining variables.\n          - Use parse_options() to strictly require options before arguments.\n          - Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n      \n     @@ Documentation/git-var.adoc: git-var - Show a Git logical variable\n       \n       DESCRIPTION\n       -----------\n     +-Prints a Git logical variable. Exits with code 1 if the variable has\n     +-no value.\n     ++Prints Git logical variables. Exits with code 1 if any requested\n     ++variable has no value. When multiple variables are requested, an empty\n     ++record (a blank line, or an empty NUL-terminated record when `-z` is given)\n     ++is printed for any variable that has no value, and the command continues\n     ++processing the remaining variables.\n     + \n     + OPTIONS\n     + -------\n      @@ Documentation/git-var.adoc: OPTIONS\n       \tas well. (However, the configuration variables listing functionality\n       \tis deprecated in favor of `git config list`.)\n       \n      +`-z`::\n     -+\tTerminate entries with NUL instead of newline.\n     ++\tTerminate entries with NUL instead of newline. When used with\n     ++\t`-l`, the variable name and its value are separated by a newline,\n     ++\tand each entry is terminated with a NUL byte.\n      +\n       EXAMPLES\n       --------\n     @@ Documentation/git-var.adoc: endif::git-default-pager[]\n      -lowest priority.  Callers should be prepared for any such path value to contain\n      -multiple items.\n      +values, which are separated by newlines (or NUL bytes if `-z` is given),\n     -+and are listed in order from highest to lowest priority.  Callers should\n     -+be prepared for any such path value to contain multiple items.\n     ++and are listed in order from highest to lowest priority. When querying\n     ++multiple variables, an extra newline (or an extra NUL byte if `-z` is\n     ++given) is printed after the values of a multi-valued variable to mark the\n     ++end of its list. Callers should be prepared for any such path value to\n     ++contain multiple items.\n       \n       Note that paths are printed even if they do not exist, but not if they are\n       disabled by other environment variables.\n     @@ builtin/var.c\n       #include \"run-command.h\"\n      +#include \"strbuf.h\"\n      +#include \"string-list.h\"\n     -+\n     + \n     +-static const char var_usage[] = \"git var (-l | <variable>)\";\n      +static const char * const var_usage[] = {\n      +\tN_(\"git var [-z] -l\"),\n      +\tN_(\"git var [-z] <variable>...\"),\n      +\tNULL\n      +};\n     - \n     --static const char var_usage[] = \"git var (-l | <variable>)\";\n     ++\n      +enum ident_part {\n      +\tIDENT_NAME,\n      +\tIDENT_MAIL,\n     @@ builtin/var.c\n      +\n      +static char *git_signing_key(int ident_flag UNUSED)\n      +{\n     -+\tchar *signing_key = NULL;\n     -+\n     -+\t/*\n     -+\t * An empty string in user.signingkey allows overriding and\n     -+\t * clearing a key defined in an outer (e.g. global) config.\n     -+\t */\n     -+\tif (!repo_config_get_string(the_repository,\n     -+\t\t\t\t    \"user.signingkey\", &signing_key)) {\n     -+\t\tif (!signing_key || !*signing_key) {\n     -+\t\t\tfree(signing_key);\n     -+\t\t\treturn NULL;\n     -+\t\t}\n     -+\t\treturn signing_key;\n     -+\t}\n     ++\tchar *signing_key = get_signing_key();\n      +\n     -+\tsigning_key = get_signing_key_id();\n      +\tif (signing_key && !*signing_key) {\n      +\t\tfree(signing_key);\n      +\t\treturn NULL;\n     @@ builtin/var.c\n       {\n       \treturn xstrdup_or_null(git_editor());\n      @@ builtin/var.c: static char *git_config_val_global(int ident_flag UNUSED)\n     + \tgit_global_config_paths(&user, &xdg);\n     + \tif (xdg && *xdg) {\n     + \t\tnormalize_path_copy(xdg, xdg);\n     +-\t\tstrbuf_addf(&buf, \"%s\\n\", xdg);\n     ++\t\tstrbuf_addstr(&buf, xdg);\n     ++\t\tstrbuf_addch(&buf, '\\0');\n     + \t}\n     + \tif (user && *user) {\n     + \t\tnormalize_path_copy(user, user);\n     +-\t\tstrbuf_addf(&buf, \"%s\\n\", user);\n     ++\t\tstrbuf_addstr(&buf, user);\n     ++\t\tstrbuf_addch(&buf, '\\0');\n     + \t}\n       \tfree(xdg);\n       \tfree(user);\n     - \tstrbuf_trim_trailing_newline(&buf);\n     +-\tstrbuf_trim_trailing_newline(&buf);\n      -\tif (buf.len == 0) {\n      +\tif (!buf.len) {\n       \t\tstrbuf_release(&buf);\n       \t\treturn NULL;\n       \t}\n     ++\tstrbuf_addch(&buf, '\\0');\n     + \treturn strbuf_detach(&buf, &unused);\n     + }\n     + \n      @@ builtin/var.c: static struct git_var git_vars[] = {\n       \t\t.name = \"GIT_COMMITTER_IDENT\",\n       \t\t.read = committer,\n     @@ builtin/var.c: static struct git_var git_vars[] = {\n       };\n       \n      -static void list_vars(void)\n     -+static void list_vars(int null_term)\n     ++static void list_vars(int nul_term)\n       {\n       \tstruct git_var *ptr;\n      -\tchar *val;\n     @@ builtin/var.c: static struct git_var git_vars[] = {\n      -\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n      -\t\t\t}\n      -\t\t\tfree(val);\n     -+\tchar delim = null_term ? '\\n' : '=';\n     -+\tchar eol = null_term ? '\\0' : '\\n';\n     ++\tchar delim = nul_term ? '\\n' : '=';\n     ++\tchar term = nul_term ? '\\0' : '\\n';\n      +\n      +\tfor (ptr = git_vars; ptr->read; ptr++) {\n      +\t\tchar *val = ptr->read(0);\n     @@ builtin/var.c: static struct git_var git_vars[] = {\n      +\t\tif (!val)\n      +\t\t\tcontinue;\n      +\n     -+\t\tif (ptr->multivalued && *val) {\n     -+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n     -+\n     -+\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n     -+\t\t\tfor (size_t i = 0; i < list.nr; i++)\n     -+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n     -+\t\t\t\t       list.items[i].string, eol);\n     -+\t\t\tstring_list_clear(&list, 0);\n     ++\t\tif (ptr->multivalued) {\n     ++\t\t\tfor (const char *s = val; *s; s += strlen(s) + 1)\n     ++\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, s, term);\n      +\t\t} else {\n     -+\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, eol);\n     ++\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n       \t\t}\n      +\t\tfree(val);\n      +\t}\n     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n       static int show_config(const char *var, const char *value,\n       \t\t       const struct config_context *ctx, void *cb)\n       {\n     -+\tint null_term = cb ? *(int *)cb : 0;\n     ++\tint *nul_term = cb;\n     ++\tchar delim = *nul_term ? '\\n' : '=';\n     ++\tchar term = *nul_term ? '\\0' : '\\n';\n      +\n       \tif (value)\n      -\t\tprintf(\"%s=%s\\n\", var, value);\n     -+\t\tprintf(\"%s%c%s%c\", var, null_term ? '\\n' : '=',\n     -+\t\t       value, null_term ? '\\0' : '\\n');\n     ++\t\tprintf(\"%s%c%s%c\", var, delim, value, term);\n       \telse\n      -\t\tprintf(\"%s\\n\", var);\n     -+\t\tprintf(\"%s%c\", var, null_term ? '\\0' : '\\n');\n     ++\t\tprintf(\"%s%c\", var, term);\n       \treturn git_default_config(var, value, ctx, cb);\n       }\n       \n     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n      -\tconst struct git_var *git_var;\n      -\tchar *val;\n      +\tint list = 0;\n     -+\tint null_term = 0;\n     ++\tint nul_term = 0;\n     ++\tint ret = 0;\n      +\tint i;\n     ++\tchar term;\n      +\tstruct option options[] = {\n      +\t\tOPT_BOOL('l', NULL, &list,\n      +\t\t\t N_(\"list all variables\")),\n     -+\t\tOPT_BOOL('z', NULL, &null_term,\n     ++\t\tOPT_BOOL('z', NULL, &nul_term,\n      +\t\t\t N_(\"terminate entries with NUL\")),\n      +\t\tOPT_END(),\n      +\t};\n     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n      +\tif (list) {\n      +\t\tif (argc)\n      +\t\t\tusage_with_options(var_usage, options);\n     -+\t\trepo_config(the_repository, show_config, &null_term);\n     -+\t\tlist_vars(null_term);\n     ++\t\trepo_config(the_repository, show_config, &nul_term);\n     ++\t\tlist_vars(nul_term);\n       \t\treturn 0;\n       \t}\n      +\n      +\tif (!argc)\n      +\t\tusage_with_options(var_usage, options);\n     -+\n     -+\tfor (i = 0; i < argc; i++) {\n     -+\t\tif (!get_git_var(argv[i]))\n     -+\t\t\tusage_with_options(var_usage, options);\n     -+\t}\n      +\n       \trepo_config(the_repository, git_default_config, NULL);\n       \n      -\tgit_var = get_git_var(argv[1]);\n      -\tif (!git_var)\n      -\t\tusage(var_usage);\n     ++\tterm = nul_term ? '\\0' : '\\n';\n     ++\n      +\tfor (i = 0; i < argc; i++) {\n      +\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n      +\t\tchar *val;\n     -+\n     -+\t\tval = git_var->read(IDENT_STRICT);\n     -+\t\tif (!val)\n     -+\t\t\treturn 1;\n       \n      -\tval = git_var->read(IDENT_STRICT);\n      -\tif (!val)\n      -\t\treturn 1;\n     -+\t\tif (git_var->multivalued && null_term && *val) {\n     -+\t\t\tstruct string_list values = STRING_LIST_INIT_DUP;\n     ++\t\tif (!git_var)\n     ++\t\t\tusage_with_options(var_usage, options);\n       \n      -\tprintf(\"%s\\n\", val);\n      -\tfree(val);\n     -+\t\t\tstring_list_split(&values, val, \"\\n\", -1);\n     -+\t\t\tfor (size_t j = 0; j < values.nr; j++) {\n     -+\t\t\t\tconst char *s = values.items[j].string;\n     ++\t\tval = git_var->read(IDENT_STRICT);\n     ++\t\tif (!val) {\n     ++\t\t\tif (argc == 1)\n     ++\t\t\t\treturn 1;\n     ++\t\t\tret = 1;\n     ++\t\t\tprintf(\"%c\", term);\n     ++\t\t\tcontinue;\n     ++\t\t}\n      +\n     -+\t\t\t\tprintf(\"%s%c\", s, '\\0');\n     -+\t\t\t}\n     -+\t\t\tstring_list_clear(&values, 0);\n     ++\t\tif (git_var->multivalued) {\n     ++\t\t\tfor (const char *s = val; *s; s += strlen(s) + 1)\n     ++\t\t\t\tprintf(\"%s%c\", s, term);\n     ++\t\t\tif (argc > 1)\n     ++\t\t\t\tprintf(\"%c\", term);\n      +\t\t} else {\n     -+\t\t\tprintf(\"%s%c\", val, null_term ? '\\0' : '\\n');\n     ++\t\t\tprintf(\"%s%c\", val, term);\n      +\t\t}\n      +\t\tfree(val);\n      +\t}\n       \n     - \treturn 0;\n     +-\treturn 0;\n     ++\treturn ret;\n       }\n      \n       ## t/t0007-git-var.sh ##\n     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '\n      +test_expect_success 'options must precede variable arguments' '\n      +\ttest_must_fail git var GIT_AUTHOR_NAME -z\n      +'\n     ++\n     ++test_expect_success 'get multiple variables with unset variable outputs blank record' '\n     ++\ttest_config user.signingkey \"\" &&\n     ++\tcat >expect <<-EOF &&\n     ++\t$GIT_AUTHOR_NAME\n     ++\n     ++\t$GIT_COMMITTER_NAME\n     ++\tEOF\n     ++\ttest_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'get multiple variables with -z and unset variable' '\n     ++\ttest_config user.signingkey \"\" &&\n     ++\tprintf \"%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_COMMITTER_NAME\" >expect &&\n     ++\ttest_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n     ++test_expect_success 'get multiple variables including multi-valued variable with -z' '\n     ++\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n     ++\tprintf \"%s\\0%s\\0%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \\\n     ++\t\t\"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" \\\n     ++\t\t\"$GIT_AUTHOR_EMAIL\" >expect &&\n     ++\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n     ++\t\tgit var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n      +\n       test_done\n\n\n Documentation/git-var.adoc |  67 ++++++++--\n builtin/var.c              | 249 ++++++++++++++++++++++++++++++-------\n t/t0007-git-var.sh         | 123 ++++++++++++++++++\n 3 files changed, 383 insertions(+), 56 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex 697c10aded..40f9f320ef 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -9,12 +9,16 @@ git-var - Show a Git logical variable\n SYNOPSIS\n --------\n [synopsis]\n-git var (-l | <variable>)\n+git var [-z] -l\n+git var [-z] <variable>...\n \n DESCRIPTION\n -----------\n-Prints a Git logical variable. Exits with code 1 if the variable has\n-no value.\n+Prints Git logical variables. Exits with code 1 if any requested\n+variable has no value. When multiple variables are requested, an empty\n+record (a blank line, or an empty NUL-terminated record when `-z` is given)\n+is printed for any variable that has no value, and the command continues\n+processing the remaining variables.\n \n OPTIONS\n -------\n@@ -24,19 +28,57 @@ OPTIONS\n \tas well. (However, the configuration variables listing functionality\n \tis deprecated in favor of `git config list`.)\n \n+`-z`::\n+\tTerminate entries with NUL instead of newline. When used with\n+\t`-l`, the variable name and its value are separated by a newline,\n+\tand each entry is terminated with a NUL byte.\n+\n EXAMPLES\n --------\n-\t$ git var GIT_AUTHOR_IDENT\n-\tEric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n-\n+* Get the author identity:\n++\n+------------\n+$ git var GIT_AUTHOR_IDENT\n+Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n+------------\n+\n+* Get the author name and email:\n++\n+------------\n+$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL\n+Eric W. Biederman\n+ebiederm@lnxi.com\n+------------\n \n VARIABLES\n ---------\n `GIT_AUTHOR_IDENT`::\n-    The author of a piece of code.\n+    The author.\n+\n+`GIT_AUTHOR_NAME`::\n+    The name of the author.\n+\n+`GIT_AUTHOR_EMAIL`::\n+    The email of the author.\n+\n+`GIT_AUTHOR_DATE`::\n+    The date and timezone of the author.\n \n `GIT_COMMITTER_IDENT`::\n-    The person who put a piece of code into Git.\n+    The committer.\n+\n+`GIT_COMMITTER_NAME`::\n+    The name of the committer.\n+\n+`GIT_COMMITTER_EMAIL`::\n+    The email of the committer.\n+\n+`GIT_COMMITTER_DATE`::\n+    The date and timezone of the committer.\n+\n+`GIT_SIGNING_KEY`::\n+    The key that would be used to sign the resulting commit if you were\n+    to run `git commit` right now.\n \n `GIT_EDITOR`::\n     Text editor for use by Git commands.  The value is meant to be\n@@ -85,9 +127,12 @@ endif::git-default-pager[]\n     The path to the global (per-user) configuration files, if any.\n \n Most path values contain only one value. However, some can contain multiple\n-values, which are separated by newlines, and are listed in order from highest to\n-lowest priority.  Callers should be prepared for any such path value to contain\n-multiple items.\n+values, which are separated by newlines (or NUL bytes if `-z` is given),\n+and are listed in order from highest to lowest priority. When querying\n+multiple variables, an extra newline (or an extra NUL byte if `-z` is\n+given) is printed after the values of a multi-valued variable to mark the\n+end of its list. Callers should be prepared for any such path value to\n+contain multiple items.\n \n Note that paths are printed even if they do not exist, but not if they are\n disabled by other environment variables.\ndiff --git a/builtin/var.c b/builtin/var.c\nindex cc3a43cde2..a6d14d7b35 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -12,25 +12,116 @@\n #include \"config.h\"\n #include \"editor.h\"\n #include \"environment.h\"\n+#include \"gpg-interface.h\"\n #include \"ident.h\"\n #include \"pager.h\"\n-#include \"refs.h\"\n+#include \"parse-options.h\"\n #include \"path.h\"\n-#include \"strbuf.h\"\n+#include \"refs.h\"\n #include \"run-command.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n \n-static const char var_usage[] = \"git var (-l | <variable>)\";\n+static const char * const var_usage[] = {\n+\tN_(\"git var [-z] -l\"),\n+\tN_(\"git var [-z] <variable>...\"),\n+\tNULL\n+};\n+\n+enum ident_part {\n+\tIDENT_NAME,\n+\tIDENT_MAIL,\n+\tIDENT_DATE,\n+};\n \n static char *committer(int ident_flag)\n {\n \treturn xstrdup_or_null(git_committer_info(ident_flag));\n }\n \n+static char *ident_part(const char *ident, enum ident_part part)\n+{\n+\tstruct ident_split split;\n+\n+\tif (!ident)\n+\t\treturn NULL;\n+\tif (split_ident_line(&split, ident, strlen(ident)))\n+\t\treturn NULL;\n+\n+\tswitch (part) {\n+\tcase IDENT_NAME:\n+\t\tif (!split.name_begin || !split.name_end)\n+\t\t\treturn NULL;\n+\t\treturn xmemdupz(split.name_begin,\n+\t\t\t\tsplit.name_end - split.name_begin);\n+\tcase IDENT_MAIL:\n+\t\tif (!split.mail_begin || !split.mail_end)\n+\t\t\treturn NULL;\n+\t\treturn xmemdupz(split.mail_begin,\n+\t\t\t\tsplit.mail_end - split.mail_begin);\n+\tcase IDENT_DATE:\n+\t\tif (!split.date_begin)\n+\t\t\treturn NULL;\n+\t\tif (split.tz_end)\n+\t\t\treturn xmemdupz(split.date_begin,\n+\t\t\t\t\tsplit.tz_end -\n+\t\t\t\t\tsplit.date_begin);\n+\t\tif (split.date_end)\n+\t\t\treturn xmemdupz(split.date_begin,\n+\t\t\t\t\tsplit.date_end -\n+\t\t\t\t\tsplit.date_begin);\n+\t\treturn NULL;\n+\tdefault:\n+\t\treturn NULL;\n+\t}\n+}\n+\n+static char *committer_name(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *committer_email(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *committer_date(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_DATE);\n+}\n+\n static char *author(int ident_flag)\n {\n \treturn xstrdup_or_null(git_author_info(ident_flag));\n }\n \n+static char *author_name(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *author_email(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *author_date(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_DATE);\n+}\n+\n+static char *git_signing_key(int ident_flag UNUSED)\n+{\n+\tchar *signing_key = get_signing_key();\n+\n+\tif (signing_key && !*signing_key) {\n+\t\tfree(signing_key);\n+\t\treturn NULL;\n+\t}\n+\treturn signing_key;\n+}\n+\n static char *editor(int ident_flag UNUSED)\n {\n \treturn xstrdup_or_null(git_editor());\n@@ -99,19 +190,21 @@ static char *git_config_val_global(int ident_flag UNUSED)\n \tgit_global_config_paths(&user, &xdg);\n \tif (xdg && *xdg) {\n \t\tnormalize_path_copy(xdg, xdg);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", xdg);\n+\t\tstrbuf_addstr(&buf, xdg);\n+\t\tstrbuf_addch(&buf, '\\0');\n \t}\n \tif (user && *user) {\n \t\tnormalize_path_copy(user, user);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", user);\n+\t\tstrbuf_addstr(&buf, user);\n+\t\tstrbuf_addch(&buf, '\\0');\n \t}\n \tfree(xdg);\n \tfree(user);\n-\tstrbuf_trim_trailing_newline(&buf);\n-\tif (buf.len == 0) {\n+\tif (!buf.len) {\n \t\tstrbuf_release(&buf);\n \t\treturn NULL;\n \t}\n+\tstrbuf_addch(&buf, '\\0');\n \treturn strbuf_detach(&buf, &unused);\n }\n \n@@ -125,10 +218,34 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_COMMITTER_IDENT\",\n \t\t.read = committer,\n \t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_NAME\",\n+\t\t.read = committer_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_EMAIL\",\n+\t\t.read = committer_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_DATE\",\n+\t\t.read = committer_date,\n+\t},\n \t{\n \t\t.name = \"GIT_AUTHOR_IDENT\",\n \t\t.read = author,\n \t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_NAME\",\n+\t\t.read = author_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_EMAIL\",\n+\t\t.read = author_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_DATE\",\n+\t\t.read = author_date,\n+\t},\n \t{\n \t\t.name = \"GIT_EDITOR\",\n \t\t.read = editor,\n@@ -145,6 +262,10 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_DEFAULT_BRANCH\",\n \t\t.read = default_branch,\n \t},\n+\t{\n+\t\t.name = \"GIT_SIGNING_KEY\",\n+\t\t.read = git_signing_key,\n+\t},\n \t{\n \t\t.name = \"GIT_SHELL_PATH\",\n \t\t.read = shell_path,\n@@ -172,34 +293,35 @@ static struct git_var git_vars[] = {\n \t},\n };\n \n-static void list_vars(void)\n+static void list_vars(int nul_term)\n {\n \tstruct git_var *ptr;\n-\tchar *val;\n-\n-\tfor (ptr = git_vars; ptr->read; ptr++)\n-\t\tif ((val = ptr->read(0))) {\n-\t\t\tif (ptr->multivalued && *val) {\n-\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n-\n-\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n-\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n-\t\t\t\tstring_list_clear(&list, 0);\n-\t\t\t} else {\n-\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n-\t\t\t}\n-\t\t\tfree(val);\n+\tchar delim = nul_term ? '\\n' : '=';\n+\tchar term = nul_term ? '\\0' : '\\n';\n+\n+\tfor (ptr = git_vars; ptr->read; ptr++) {\n+\t\tchar *val = ptr->read(0);\n+\n+\t\tif (!val)\n+\t\t\tcontinue;\n+\n+\t\tif (ptr->multivalued) {\n+\t\t\tfor (const char *s = val; *s; s += strlen(s) + 1)\n+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, s, term);\n+\t\t} else {\n+\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n \t\t}\n+\t\tfree(val);\n+\t}\n }\n \n static const struct git_var *get_git_var(const char *var)\n {\n \tstruct git_var *ptr;\n+\n \tfor (ptr = git_vars; ptr->read; ptr++) {\n-\t\tif (strcmp(var, ptr->name) == 0) {\n+\t\tif (!strcmp(var, ptr->name))\n \t\t\treturn ptr;\n-\t\t}\n \t}\n \treturn NULL;\n }\n@@ -207,42 +329,79 @@ static const struct git_var *get_git_var(const char *var)\n static int show_config(const char *var, const char *value,\n \t\t       const struct config_context *ctx, void *cb)\n {\n+\tint *nul_term = cb;\n+\tchar delim = *nul_term ? '\\n' : '=';\n+\tchar term = *nul_term ? '\\0' : '\\n';\n+\n \tif (value)\n-\t\tprintf(\"%s=%s\\n\", var, value);\n+\t\tprintf(\"%s%c%s%c\", var, delim, value, term);\n \telse\n-\t\tprintf(\"%s\\n\", var);\n+\t\tprintf(\"%s%c\", var, term);\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n int cmd_var(int argc,\n \t    const char **argv,\n-\t    const char *prefix UNUSED,\n+\t    const char *prefix,\n \t    struct repository *repo UNUSED)\n {\n-\tconst struct git_var *git_var;\n-\tchar *val;\n+\tint list = 0;\n+\tint nul_term = 0;\n+\tint ret = 0;\n+\tint i;\n+\tchar term;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL('l', NULL, &list,\n+\t\t\t N_(\"list all variables\")),\n+\t\tOPT_BOOL('z', NULL, &nul_term,\n+\t\t\t N_(\"terminate entries with NUL\")),\n+\t\tOPT_END(),\n+\t};\n \n-\tshow_usage_if_asked(argc, argv, var_usage);\n-\tif (argc != 2)\n-\t\tusage(var_usage);\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (strcmp(argv[1], \"-l\") == 0) {\n-\t\trepo_config(the_repository, show_config, NULL);\n-\t\tlist_vars();\n+\tif (list) {\n+\t\tif (argc)\n+\t\t\tusage_with_options(var_usage, options);\n+\t\trepo_config(the_repository, show_config, &nul_term);\n+\t\tlist_vars(nul_term);\n \t\treturn 0;\n \t}\n+\n+\tif (!argc)\n+\t\tusage_with_options(var_usage, options);\n+\n \trepo_config(the_repository, git_default_config, NULL);\n \n-\tgit_var = get_git_var(argv[1]);\n-\tif (!git_var)\n-\t\tusage(var_usage);\n+\tterm = nul_term ? '\\0' : '\\n';\n+\n+\tfor (i = 0; i < argc; i++) {\n+\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n+\t\tchar *val;\n \n-\tval = git_var->read(IDENT_STRICT);\n-\tif (!val)\n-\t\treturn 1;\n+\t\tif (!git_var)\n+\t\t\tusage_with_options(var_usage, options);\n \n-\tprintf(\"%s\\n\", val);\n-\tfree(val);\n+\t\tval = git_var->read(IDENT_STRICT);\n+\t\tif (!val) {\n+\t\t\tif (argc == 1)\n+\t\t\t\treturn 1;\n+\t\t\tret = 1;\n+\t\t\tprintf(\"%c\", term);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (git_var->multivalued) {\n+\t\t\tfor (const char *s = val; *s; s += strlen(s) + 1)\n+\t\t\t\tprintf(\"%s%c\", s, term);\n+\t\t\tif (argc > 1)\n+\t\t\t\tprintf(\"%c\", term);\n+\t\t} else {\n+\t\t\tprintf(\"%s%c\", val, term);\n+\t\t}\n+\t\tfree(val);\n+\t}\n \n-\treturn 0;\n+\treturn ret;\n }\ndiff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\nindex 2b60317758..92b68b9ab4 100755\n--- a/t/t0007-git-var.sh\n+++ b/t/t0007-git-var.sh\n@@ -276,4 +276,127 @@ test_expect_success '`git var -l` works even without HOME' '\n \t)\n '\n \n+test_expect_success 'get author identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n+\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n+\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n+\tgit var GIT_AUTHOR_NAME >actual.name &&\n+\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n+\tgit var GIT_AUTHOR_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get committer identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n+\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n+\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n+\tgit var GIT_COMMITTER_NAME >actual.name &&\n+\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n+\tgit var GIT_COMMITTER_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get multiple variables' '\n+\ttest_tick &&\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME\n+\t$GIT_AUTHOR_EMAIL\n+\t$GIT_COMMITTER_NAME\n+\t$GIT_COMMITTER_EMAIL\n+\tEOF\n+\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z' '\n+\ttest_tick &&\n+\tprintf \"%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual &&\n+\tprintf \"%s\\0\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expected &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git var -l -z' '\n+\tgit var -l -z >actual &&\n+\ttr \"\\0\" \"\\n\" <actual >actual.lines &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect &&\n+\tsed -n \"/^GIT_AUTHOR_NAME$/{n;p;}\" actual.lines >actual.author &&\n+\ttest_cmp expect actual.author &&\n+\techo false >expect &&\n+\tsed -n \"/^core\\.bare$/{n;p;}\" actual.lines >actual.bare &&\n+\ttest_cmp expect actual.bare\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY with user.signingkey configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\techo \"TEST_KEY_ID\" >expect &&\n+\tgit var GIT_SIGNING_KEY >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '\n+\ttest_config user.signingkey \"\" &&\n+\ttest_must_fail git var GIT_SIGNING_KEY\n+'\n+\n+test_expect_success 'git var -l lists new variables' '\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n+'\n+\n+test_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_SIGNING_KEY=TEST_KEY_ID\" actual\n+'\n+\n+test_expect_success 'options must precede variable arguments' '\n+\ttest_must_fail git var GIT_AUTHOR_NAME -z\n+'\n+\n+test_expect_success 'get multiple variables with unset variable outputs blank record' '\n+\ttest_config user.signingkey \"\" &&\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME\n+\n+\t$GIT_COMMITTER_NAME\n+\tEOF\n+\ttest_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z and unset variable' '\n+\ttest_config user.signingkey \"\" &&\n+\tprintf \"%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_COMMITTER_NAME\" >expect &&\n+\ttest_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables including multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tprintf \"%s\\0%s\\0%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \\\n+\t\t\"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" \\\n+\t\t\"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n+\t\tgit var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n-- \ngitgitgadget\n"},{"id":"552263","messageId":"xmqqpkyntnov.fsf@gitster.g","threadId":"66219","inReplyTo":"pull.2388.v5.git.git.1788900182711.gitgitgadget@gmail.com","subject":"Re: [PATCH v5] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-08T21:53:20Z","receivedAt":"2026-09-08T21:53:23Z","isPatch":true,"body":"\"Andrew Pleeter via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  VARIABLES\n>  ---------\n>  `GIT_AUTHOR_IDENT`::\n> +    The author.\n> +\n> +`GIT_AUTHOR_NAME`::\n> +    The name of the author.\n> +\n> +`GIT_AUTHOR_EMAIL`::\n> +    The email of the author.\n> +\n> +`GIT_AUTHOR_DATE`::\n> +    The date and timezone of the author.\n\nThe above (and the COMMITTER counterparts) gives almost no useful\ninformation.  On the other hand, the description used here ...\n\n> +`GIT_SIGNING_KEY`::\n> +    The key that would be used to sign the resulting commit if you were\n> +    to run `git commit` right now.\n\n... explains what significance this key has much better.\n\n> @@ -85,9 +127,12 @@ endif::git-default-pager[]\n>      The path to the global (per-user) configuration files, if any.\n>  \n>  Most path values contain only one value. However, some can contain multiple\n> -values, which are separated by newlines, and are listed in order from highest to\n> -lowest priority.  Callers should be prepared for any such path value to contain\n> -multiple items.\n> +values, which are separated by newlines (or NUL bytes if `-z` is given),\n> +and are listed in order from highest to lowest priority. When querying\n> +multiple variables, an extra newline (or an extra NUL byte if `-z` is\n> +given) is printed after the values of a multi-valued variable to mark the\n> +end of its list. Callers should be prepared for any such path value to\n> +contain multiple items.\n\nHmph.  The added bulk of this description is only because we can now\noptionally use NUL delimiting as opposed to LF?  Are we changing the\noutput in a backward incompatible way for callers that do not pass\nthe -z option?\n\nFor example, \"git var -l\" output ends like so for me in today's Git:\n\n    $ git var -l | tail -n 3\n    GIT_CONFIG_SYSTEM=/home/gitster/g/seen/etc/gitconfig\n    GIT_CONFIG_GLOBAL=/home/gitster/.config/git/config\n    GIT_CONFIG_GLOBAL=/home/gitster/.gitconfig\n\n    $ git var GIT_CONFIG_GLOBAL\n    /home/gitster/.config/git/config\n    /home/gitster/.gitconfig\n\nYou mention \"an extra newline\".  Will it appear in the above output\nwith this version?  If so, wouldn't that be a breaking change?\n\n> +static char *ident_part(const char *ident, enum ident_part part)\n> +{\n> +\tstruct ident_split split;\n> +\n> +\tif (!ident)\n> +\t\treturn NULL;\n> +\tif (split_ident_line(&split, ident, strlen(ident)))\n> +\t\treturn NULL;\n> +\n> +\tswitch (part) {\n> +\tcase IDENT_NAME:\n> +\t\tif (!split.name_begin || !split.name_end)\n> +\t\t\treturn NULL;\n> +\t\treturn xmemdupz(split.name_begin,\n> +\t\t\t\tsplit.name_end - split.name_begin);\n> +\tcase IDENT_MAIL:\n> +\t\tif (!split.mail_begin || !split.mail_end)\n> +\t\t\treturn NULL;\n> +\t\treturn xmemdupz(split.mail_begin,\n> +\t\t\t\tsplit.mail_end - split.mail_begin);\n> +\tcase IDENT_DATE:\n> +\t\tif (!split.date_begin)\n> +\t\t\treturn NULL;\n> +\t\tif (split.tz_end)\n> +\t\t\treturn xmemdupz(split.date_begin,\n> +\t\t\t\t\tsplit.tz_end -\n> +\t\t\t\t\tsplit.date_begin);\n> +\t\tif (split.date_end)\n> +\t\t\treturn xmemdupz(split.date_begin,\n> +\t\t\t\t\tsplit.date_end -\n> +\t\t\t\t\tsplit.date_begin);\n> +\t\treturn NULL;\n> +\tdefault:\n> +\t\treturn NULL;\n> +\t}\n> +}\n\nIn many cases in the above you punt and return NULL, but aren't\nthere some cases where it clearly is a data error that deserves\ndie() or a programming error that deserves BUG()?\n\nFor example, my quick read of split_ident_line() tells me that it is\nimpossible for split.name_begin or split.name_end to be NULL if the\nfunction signals success by returning 0.  If I am not misreading the\ncode, returning NULL when IDENT_NAME is requested in the above code\nis sweeping a programming error under the rug.\n\nIf the caller passed anything other than IDENT_{NAME,MAIL,DATE}, it\nsimilarly is a programming error.  The default: arm should not hide\nit underr the rug by returning NULL, but complain loudly with a\nBUG(), no?\n\n> @@ -99,19 +190,21 @@ static char *git_config_val_global(int ident_flag UNUSED)\n>  \tgit_global_config_paths(&user, &xdg);\n>  \tif (xdg && *xdg) {\n>  \t\tnormalize_path_copy(xdg, xdg);\n> -\t\tstrbuf_addf(&buf, \"%s\\n\", xdg);\n> +\t\tstrbuf_addstr(&buf, xdg);\n> +\t\tstrbuf_addch(&buf, '\\0');\n>  \t}\n>  \tif (user && *user) {\n>  \t\tnormalize_path_copy(user, user);\n> -\t\tstrbuf_addf(&buf, \"%s\\n\", user);\n> +\t\tstrbuf_addstr(&buf, user);\n> +\t\tstrbuf_addch(&buf, '\\0');\n>  \t}\n\nMental note: we used to use LF at the end, but in this version we\nadd NUL here.\n\n>  \tfree(xdg);\n>  \tfree(user);\n> -\tstrbuf_trim_trailing_newline(&buf);\n> -\tif (buf.len == 0) {\n> +\tif (!buf.len) {\n>  \t\tstrbuf_release(&buf);\n>  \t\treturn NULL;\n>  \t}\n> +\tstrbuf_addch(&buf, '\\0');\n\nAnd then we add an extra NUL after that.\n\n>  \treturn strbuf_detach(&buf, &unused);\n>  }\n\n> @@ -172,34 +293,35 @@ static struct git_var git_vars[] = {\n>  \t},\n>  };\n>  \n> -static void list_vars(void)\n> +static void list_vars(int nul_term)\n>  {\n>  \tstruct git_var *ptr;\n> -\tchar *val;\n> -\n> -\tfor (ptr = git_vars; ptr->read; ptr++)\n> -\t\tif ((val = ptr->read(0))) {\n> -\t\t\tif (ptr->multivalued && *val) {\n> -\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n> -\n> -\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n> -\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n> -\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n> -\t\t\t\tstring_list_clear(&list, 0);\n\nWe used to split at LF (because we used to concatenate with LF in\nthe git_config_val_globa() that grabs potentially multiple values)\nand then showed them one by one.\n\n> -\t\t\t} else {\n> -\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n> -\t\t\t}\n> -\t\t\tfree(val);\n> +\tchar delim = nul_term ? '\\n' : '=';\n> +\tchar term = nul_term ? '\\0' : '\\n';\n> +\n> +\tfor (ptr = git_vars; ptr->read; ptr++) {\n> +\t\tchar *val = ptr->read(0);\n> +\n> +\t\tif (!val)\n> +\t\t\tcontinue;\n> +\n> +\t\tif (ptr->multivalued) {\n> +\t\t\tfor (const char *s = val; *s; s += strlen(s) + 1)\n> +\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, s, term);\n\nNow we use each string pieces (s), skip the string we just showed by\nadvancing the pointer by strlen(s) + 1.  If multi-valued variable\nhas ever an empty string as one of the possible values, this scheme\nwould break down, but right now GIT_CONFIG_GLOBAL is the only thing\nthat is .multivalued, and neither the HOME or XDG path is likely to\nbe ever empty, so this may be OK, perhaps?  If xdg is defined to be\na non-empty string (i.e., \"if (xdg && *xdg)\" is taken) but if\ncalling normalize_path_copy(xdg, xdg) makes it an empty string, then\ngit_config_val_global() will give \"\\0/home/gitster/.gitconfig\\0\\0\"\nfor me (the first NUL is after the empty xdg value, the second NUL\nis terminating HOME value, and the third NUL concludes the whole\nthing), and then this loop will exit without showing anything (not\njust skipping an empty XDG, but hiding perfectly healthy HOME\nvalue).  Is that a concern?\n\nI wonder if we should correct how .multivalued field is handled\nbefore we add more of them.  For example, .multivalued = 1 item\nmay use something different from .read that uses a string-list\nto carry the information\n\n\t{\n\t\t.name = \"GIT_CONFIG_GLOBAL\",\n\t\t.multiread = git_config_val_global,\n\t},\n\n\tstatic int git_config_val_global(struct string_list *list)\n\t{\n\t\tgit_global_config_paths(...);\n\t\tif (xdg available)\n\t\t\tstring_list_append(list, xdg);\n\t\tif (user availble)\n\t\t\tstring_list_append(list, user);\n\t\treturn 0;\n\t}\n\t\nand then the above part of the code would look more like\n\n\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n\n\t\tif (ptr->read) {\n\t\t\t... single read as before ...\n\t\t} else (ptr->multiread) {\n\t\t\tstruct string_list list = STRING_LIST_INIT;\n\n\t\t\tptr->multiread(&list);\n\t\t\tfor (size_t i = 0; i < list.nr; i++)\n\t\t\t\t... use list.items[i].string ...\n\t\t}\n\n\t}\n\n>  static const struct git_var *get_git_var(const char *var)\n>  {\n>  \tstruct git_var *ptr;\n> +\n>  \tfor (ptr = git_vars; ptr->read; ptr++) {\n> -\t\tif (strcmp(var, ptr->name) == 0) {\n> +\t\tif (!strcmp(var, ptr->name))\n>  \t\t\treturn ptr;\n> -\t\t}\n>  \t}\n>  \treturn NULL;\n>  }\n\nAn unrelated change like this is distracting and makes it less\nlikely for your patch to succeed.  Leave such a clean-up out of a\npatch that is about a new feature, or fixing a bug.\n"},{"id":"552275","messageId":"pull.2388.v6.git.git.1788917076554.gitgitgadget@gmail.com","threadId":"66219","inReplyTo":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","subject":"[PATCH v6] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Andrew Pleeter via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-09T01:24:36Z","receivedAt":"2026-09-09T01:24:41Z","isPatch":true,"body":"From: Andrew Pleeter <andrewpleeter@gmail.com>\n\nWhile 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\nextracting individual components (name, email, or date) currently\nrequires callers to manually parse the composite string. Furthermore,\nthere is no way to query the resolved commit signing key through\n'git var', and the command only accepts a single variable at a time.\n\nTeach 'git var' to expose individual identity components and commit\nsigning configuration, and allow querying multiple variables with\noptional NUL-termination:\n\n- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n- Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n  the resulting commit if you were to run 'git commit' right now.\n- Allow passing multiple variable arguments (e.g., 'git var\n  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n  sequentially.\n- Support '-z' to terminate variable outputs with NUL bytes.\n- Format 'git var -l -z' using the same convention as 'git config\n  list -z' (newline separating key and value, NUL separating entries).\n- Delimit values of multi-valued variables with NUL when '-z' is given,\n  and output an extra delimiter after multi-valued variables when\n  querying multiple variables to disambiguate the stream.\n- When querying multiple variables, print an empty record for any\n  variable that has no value and continue processing remaining variables.\n- Use parse_options() to strictly require options before arguments.\n- Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n    var: support broken-down idents, signing key, multiple args, and -z\n    \n    Teach git var to expose individual identity components and commit\n    signing configuration, and allow querying multiple variables with\n    optional NUL-termination.\n    \n    \n    Changes since v5:\n    =================\n    \n     * Reverted cosmetic refactoring in get_git_var() to keep the diff\n       minimal and focused on adding ptr->multiread.\n     * Added BUG(\"unknown ident_part %d\", part) to default case in\n       ident_part() and removed redundant NULL checks on split.name_begin\n       and split.mail_begin.\n     * Adopted first-class int (*multiread)(struct string_list *) callback\n       in struct git_var for multi-valued variables like GIT_CONFIG_GLOBAL,\n       cleanly populating a struct string_list instead of relying on\n       embedded NUL buffers.\n     * Improved GIT_AUTHOR_* and GIT_COMMITTER_* documentation in\n       Documentation/git-var.adoc to describe the values that would be used\n       if you were to run git commit right now.\n     * Explicitly documented that single-variable queries and git var -l do\n       not print an extra delimiter after multi-valued variables.\n    \n    \n    Changes since v4:\n    =================\n    \n     * Simplified git_signing_key() to directly call get_signing_key() as\n       used throughout Git (in tag, send-pack, and sign_buffer()).\n     * Renamed null_term to nul_term across builtin/var.c, and simplified\n       show_config() callback handling.\n     * Replaced the redundant pre-validation loop in cmd_var() by validating\n       arguments directly in the main execution loop.\n     * When querying multiple variables, print an empty record (blank line\n       or \\0 with -z) for any variable that has no value, and continue\n       printing remaining variables instead of terminating prematurely.\n     * Switched multi-valued variable storage (git_config_val_global()) to\n       internal \\0 delimiters, iterating directly through string sequences\n       without allocating a temporary string_list.\n     * For multi-variable queries, output an extra delimiter (\\n or \\0)\n       after multi-valued variables to clearly mark the end of their list.\n     * Explicitly documented the git var -l -z format and multi-variable\n       handling in Documentation/git-var.adoc.\n     * Added comprehensive tests for unset variables and multi-valued stream\n       delimiters in t/t0007-git-var.sh.\n    \n    \n    Changes since v3:\n    =================\n    \n     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip\n       Wood and Junio C Hamano; dropped the alias mechanism and\n       commit.gpgsign check.\n     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in\n       builtin/var.c, strictly enforcing that options precede variable\n       arguments.\n     * Adopted git config list -z format (key\\nvalue\\0) for git var -l -z to\n       prevent ambiguity with = in config keys.\n     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with\n       NUL bytes under -z.\n     * Replaced char part in ident_part() with enum ident_part.\n     * Split synopsis in Documentation/git-var.adoc into separate lines for\n       -l and <variable>..., and removed awkward legacy phrasing (\"of a\n       piece of code\").\n     * Added tests in t/t0007-git-var.sh covering the new -z format,\n       multi-valued -z, and argument ordering.\n    \n    \n    Changes since v2:\n    =================\n    \n     * Drop git ident / git whoami subcommand entirely.\n     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n     * Add GIT_SIGNING_KEY to resolve commit signing keys.\n     * Teach git var to accept multiple variable arguments (git var <var1>\n       <var2> ...).\n     * Add -z option to terminate outputs with NUL bytes (including git var\n       -l -z).\n     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v6\nPull-Request: https://github.com/git/git/pull/2388\n\nRange-diff vs v5:\n\n 1:  04833673ea ! 1:  3b51f61a11 var: support broken-down idents, signing key, multiple args, and -z\n     @@ Documentation/git-var.adoc: OPTIONS\n       ---------\n       `GIT_AUTHOR_IDENT`::\n      -    The author of a piece of code.\n     -+    The author.\n     ++    The author name, email, and date that would be used if you were to\n     ++    run `git commit` right now.\n      +\n      +`GIT_AUTHOR_NAME`::\n     -+    The name of the author.\n     ++    The author name that would be used if you were to run `git commit`\n     ++    right now.\n      +\n      +`GIT_AUTHOR_EMAIL`::\n     -+    The email of the author.\n     ++    The author email that would be used if you were to run `git commit`\n     ++    right now.\n      +\n      +`GIT_AUTHOR_DATE`::\n     -+    The date and timezone of the author.\n     ++    The author date and timezone that would be used if you were to run\n     ++    `git commit` right now.\n       \n       `GIT_COMMITTER_IDENT`::\n      -    The person who put a piece of code into Git.\n     -+    The committer.\n     ++    The committer name, email, and date that would be used if you were\n     ++    to run `git commit` right now.\n      +\n      +`GIT_COMMITTER_NAME`::\n     -+    The name of the committer.\n     ++    The name of the committer that would be used if you were to run\n     ++    `git commit` right now.\n      +\n      +`GIT_COMMITTER_EMAIL`::\n     -+    The email of the committer.\n     ++    The email of the committer that would be used if you were to run\n     ++    `git commit` right now.\n      +\n      +`GIT_COMMITTER_DATE`::\n     -+    The date and timezone of the committer.\n     ++    The committer date and timezone that would be used if you were to run\n     ++    `git commit` right now.\n      +\n      +`GIT_SIGNING_KEY`::\n      +    The key that would be used to sign the resulting commit if you were\n     @@ Documentation/git-var.adoc: endif::git-default-pager[]\n      +and are listed in order from highest to lowest priority. When querying\n      +multiple variables, an extra newline (or an extra NUL byte if `-z` is\n      +given) is printed after the values of a multi-valued variable to mark the\n     -+end of its list. Callers should be prepared for any such path value to\n     ++end of its list. (Single-variable queries and `git var -l` do not print\n     ++an extra delimiter). Callers should be prepared for any such path value to\n      +contain multiple items.\n       \n       Note that paths are printed even if they do not exist, but not if they are\n     @@ builtin/var.c\n       #include \"run-command.h\"\n      +#include \"strbuf.h\"\n      +#include \"string-list.h\"\n     - \n     --static const char var_usage[] = \"git var (-l | <variable>)\";\n     ++\n      +static const char * const var_usage[] = {\n      +\tN_(\"git var [-z] -l\"),\n      +\tN_(\"git var [-z] <variable>...\"),\n      +\tNULL\n      +};\n     -+\n     + \n     +-static const char var_usage[] = \"git var (-l | <variable>)\";\n      +enum ident_part {\n      +\tIDENT_NAME,\n      +\tIDENT_MAIL,\n     @@ builtin/var.c\n      +\n      +\tswitch (part) {\n      +\tcase IDENT_NAME:\n     -+\t\tif (!split.name_begin || !split.name_end)\n     -+\t\t\treturn NULL;\n      +\t\treturn xmemdupz(split.name_begin,\n      +\t\t\t\tsplit.name_end - split.name_begin);\n      +\tcase IDENT_MAIL:\n     -+\t\tif (!split.mail_begin || !split.mail_end)\n     -+\t\t\treturn NULL;\n      +\t\treturn xmemdupz(split.mail_begin,\n      +\t\t\t\tsplit.mail_end - split.mail_begin);\n      +\tcase IDENT_DATE:\n     @@ builtin/var.c\n      +\t\t\t\t\tsplit.date_begin);\n      +\t\treturn NULL;\n      +\tdefault:\n     -+\t\treturn NULL;\n     ++\t\tBUG(\"unknown ident_part %d\", part);\n      +\t}\n      +}\n      +\n     @@ builtin/var.c\n       static char *editor(int ident_flag UNUSED)\n       {\n       \treturn xstrdup_or_null(git_editor());\n     -@@ builtin/var.c: static char *git_config_val_global(int ident_flag UNUSED)\n     +@@ builtin/var.c: static char *git_config_val_system(int ident_flag UNUSED)\n     + \treturn NULL;\n     + }\n     + \n     +-static char *git_config_val_global(int ident_flag UNUSED)\n     ++static int git_config_val_global(struct string_list *list)\n     + {\n     +-\tstruct strbuf buf = STRBUF_INIT;\n     + \tchar *user, *xdg;\n     +-\tsize_t unused;\n     + \n       \tgit_global_config_paths(&user, &xdg);\n       \tif (xdg && *xdg) {\n       \t\tnormalize_path_copy(xdg, xdg);\n      -\t\tstrbuf_addf(&buf, \"%s\\n\", xdg);\n     -+\t\tstrbuf_addstr(&buf, xdg);\n     -+\t\tstrbuf_addch(&buf, '\\0');\n     ++\t\tstring_list_append(list, xdg);\n       \t}\n       \tif (user && *user) {\n       \t\tnormalize_path_copy(user, user);\n      -\t\tstrbuf_addf(&buf, \"%s\\n\", user);\n     -+\t\tstrbuf_addstr(&buf, user);\n     -+\t\tstrbuf_addch(&buf, '\\0');\n     ++\t\tstring_list_append(list, user);\n       \t}\n       \tfree(xdg);\n       \tfree(user);\n      -\tstrbuf_trim_trailing_newline(&buf);\n      -\tif (buf.len == 0) {\n     -+\tif (!buf.len) {\n     - \t\tstrbuf_release(&buf);\n     - \t\treturn NULL;\n     - \t}\n     -+\tstrbuf_addch(&buf, '\\0');\n     - \treturn strbuf_detach(&buf, &unused);\n     +-\t\tstrbuf_release(&buf);\n     +-\t\treturn NULL;\n     +-\t}\n     +-\treturn strbuf_detach(&buf, &unused);\n     ++\treturn !list->nr;\n       }\n       \n     -@@ builtin/var.c: static struct git_var git_vars[] = {\n     + struct git_var {\n     + \tconst char *name;\n     + \tchar *(*read)(int);\n     +-\tint multivalued;\n     ++\tint (*multiread)(struct string_list *);\n     + };\n     + static struct git_var git_vars[] = {\n     + \t{\n       \t\t.name = \"GIT_COMMITTER_IDENT\",\n       \t\t.read = committer,\n       \t},\n     @@ builtin/var.c: static struct git_var git_vars[] = {\n       \t{\n       \t\t.name = \"GIT_SHELL_PATH\",\n       \t\t.read = shell_path,\n     +@@ builtin/var.c: static struct git_var git_vars[] = {\n     + \t},\n     + \t{\n     + \t\t.name = \"GIT_CONFIG_GLOBAL\",\n     +-\t\t.read = git_config_val_global,\n     +-\t\t.multivalued = 1,\n     ++\t\t.multiread = git_config_val_global,\n     + \t},\n     + \t{\n     + \t\t.name = \"\",\n      @@ builtin/var.c: static struct git_var git_vars[] = {\n       \t},\n       };\n     @@ builtin/var.c: static struct git_var git_vars[] = {\n       {\n       \tstruct git_var *ptr;\n      -\tchar *val;\n     --\n     ++\tchar delim = nul_term ? '\\n' : '=';\n     ++\tchar term = nul_term ? '\\0' : '\\n';\n     + \n      -\tfor (ptr = git_vars; ptr->read; ptr++)\n      -\t\tif ((val = ptr->read(0))) {\n      -\t\t\tif (ptr->multivalued && *val) {\n      -\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n     --\n     ++\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n     ++\t\tif (ptr->read) {\n     ++\t\t\tchar *val = ptr->read(0);\n     + \n      -\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n      -\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n      -\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n      -\t\t\t\tstring_list_clear(&list, 0);\n      -\t\t\t} else {\n      -\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n     --\t\t\t}\n     ++\t\t\tif (val) {\n     ++\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n     ++\t\t\t\tfree(val);\n     + \t\t\t}\n      -\t\t\tfree(val);\n     -+\tchar delim = nul_term ? '\\n' : '=';\n     -+\tchar term = nul_term ? '\\0' : '\\n';\n     -+\n     -+\tfor (ptr = git_vars; ptr->read; ptr++) {\n     -+\t\tchar *val = ptr->read(0);\n     -+\n     -+\t\tif (!val)\n     -+\t\t\tcontinue;\n     -+\n     -+\t\tif (ptr->multivalued) {\n     -+\t\t\tfor (const char *s = val; *s; s += strlen(s) + 1)\n     -+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, s, term);\n     -+\t\t} else {\n     -+\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n     ++\t\t} else if (ptr->multiread) {\n     ++\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n     ++\t\t\tsize_t i;\n     ++\n     ++\t\t\tif (!ptr->multiread(&list)) {\n     ++\t\t\t\tfor (i = 0; i < list.nr; i++)\n     ++\t\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n     ++\t\t\t\t\t       list.items[i].string, term);\n     ++\t\t\t}\n     ++\t\t\tstring_list_clear(&list, 0);\n       \t\t}\n     -+\t\tfree(val);\n      +\t}\n       }\n       \n       static const struct git_var *get_git_var(const char *var)\n       {\n       \tstruct git_var *ptr;\n     -+\n     - \tfor (ptr = git_vars; ptr->read; ptr++) {\n     --\t\tif (strcmp(var, ptr->name) == 0) {\n     -+\t\tif (!strcmp(var, ptr->name))\n     +-\tfor (ptr = git_vars; ptr->read; ptr++) {\n     ++\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n     + \t\tif (strcmp(var, ptr->name) == 0) {\n       \t\t\treturn ptr;\n     --\t\t}\n     - \t}\n     - \treturn NULL;\n     - }\n     + \t\t}\n      @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n       static int show_config(const char *var, const char *value,\n       \t\t       const struct config_context *ctx, void *cb)\n     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n      -\tif (!git_var)\n      -\t\tusage(var_usage);\n      +\tterm = nul_term ? '\\0' : '\\n';\n     -+\n     -+\tfor (i = 0; i < argc; i++) {\n     -+\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n     -+\t\tchar *val;\n       \n      -\tval = git_var->read(IDENT_STRICT);\n      -\tif (!val)\n      -\t\treturn 1;\n     -+\t\tif (!git_var)\n     -+\t\t\tusage_with_options(var_usage, options);\n     ++\tfor (i = 0; i < argc; i++) {\n     ++\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n       \n      -\tprintf(\"%s\\n\", val);\n      -\tfree(val);\n     -+\t\tval = git_var->read(IDENT_STRICT);\n     -+\t\tif (!val) {\n     -+\t\t\tif (argc == 1)\n     -+\t\t\t\treturn 1;\n     -+\t\t\tret = 1;\n     -+\t\t\tprintf(\"%c\", term);\n     -+\t\t\tcontinue;\n     -+\t\t}\n     ++\t\tif (!git_var)\n     ++\t\t\tusage_with_options(var_usage, options);\n     ++\n     ++\t\tif (git_var->read) {\n     ++\t\t\tchar *val = git_var->read(IDENT_STRICT);\n      +\n     -+\t\tif (git_var->multivalued) {\n     -+\t\t\tfor (const char *s = val; *s; s += strlen(s) + 1)\n     -+\t\t\t\tprintf(\"%s%c\", s, term);\n     -+\t\t\tif (argc > 1)\n     ++\t\t\tif (!val) {\n     ++\t\t\t\tif (argc == 1)\n     ++\t\t\t\t\treturn 1;\n     ++\t\t\t\tret = 1;\n      +\t\t\t\tprintf(\"%c\", term);\n     -+\t\t} else {\n     ++\t\t\t\tcontinue;\n     ++\t\t\t}\n      +\t\t\tprintf(\"%s%c\", val, term);\n     ++\t\t\tfree(val);\n     ++\t\t} else if (git_var->multiread) {\n     ++\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n     ++\t\t\tsize_t j;\n     ++\n     ++\t\t\tif (git_var->multiread(&list) || !list.nr) {\n     ++\t\t\t\tif (argc == 1) {\n     ++\t\t\t\t\tstring_list_clear(&list, 0);\n     ++\t\t\t\t\treturn 1;\n     ++\t\t\t\t}\n     ++\t\t\t\tret = 1;\n     ++\t\t\t\tprintf(\"%c\", term);\n     ++\t\t\t} else {\n     ++\t\t\t\tfor (j = 0; j < list.nr; j++)\n     ++\t\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n     ++\t\t\t\tif (argc > 1)\n     ++\t\t\t\t\tprintf(\"%c\", term);\n     ++\t\t\t}\n     ++\t\t\tstring_list_clear(&list, 0);\n      +\t\t}\n     -+\t\tfree(val);\n      +\t}\n       \n      -\treturn 0;\n\n\n Documentation/git-var.adoc |  76 +++++++++--\n builtin/var.c              | 265 ++++++++++++++++++++++++++++++-------\n t/t0007-git-var.sh         | 123 +++++++++++++++++\n 3 files changed, 402 insertions(+), 62 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex 697c10aded..2c1eaf3cf7 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -9,12 +9,16 @@ git-var - Show a Git logical variable\n SYNOPSIS\n --------\n [synopsis]\n-git var (-l | <variable>)\n+git var [-z] -l\n+git var [-z] <variable>...\n \n DESCRIPTION\n -----------\n-Prints a Git logical variable. Exits with code 1 if the variable has\n-no value.\n+Prints Git logical variables. Exits with code 1 if any requested\n+variable has no value. When multiple variables are requested, an empty\n+record (a blank line, or an empty NUL-terminated record when `-z` is given)\n+is printed for any variable that has no value, and the command continues\n+processing the remaining variables.\n \n OPTIONS\n -------\n@@ -24,19 +28,65 @@ OPTIONS\n \tas well. (However, the configuration variables listing functionality\n \tis deprecated in favor of `git config list`.)\n \n+`-z`::\n+\tTerminate entries with NUL instead of newline. When used with\n+\t`-l`, the variable name and its value are separated by a newline,\n+\tand each entry is terminated with a NUL byte.\n+\n EXAMPLES\n --------\n-\t$ git var GIT_AUTHOR_IDENT\n-\tEric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n-\n+* Get the author identity:\n++\n+------------\n+$ git var GIT_AUTHOR_IDENT\n+Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n+------------\n+\n+* Get the author name and email:\n++\n+------------\n+$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL\n+Eric W. Biederman\n+ebiederm@lnxi.com\n+------------\n \n VARIABLES\n ---------\n `GIT_AUTHOR_IDENT`::\n-    The author of a piece of code.\n+    The author name, email, and date that would be used if you were to\n+    run `git commit` right now.\n+\n+`GIT_AUTHOR_NAME`::\n+    The author name that would be used if you were to run `git commit`\n+    right now.\n+\n+`GIT_AUTHOR_EMAIL`::\n+    The author email that would be used if you were to run `git commit`\n+    right now.\n+\n+`GIT_AUTHOR_DATE`::\n+    The author date and timezone that would be used if you were to run\n+    `git commit` right now.\n \n `GIT_COMMITTER_IDENT`::\n-    The person who put a piece of code into Git.\n+    The committer name, email, and date that would be used if you were\n+    to run `git commit` right now.\n+\n+`GIT_COMMITTER_NAME`::\n+    The name of the committer that would be used if you were to run\n+    `git commit` right now.\n+\n+`GIT_COMMITTER_EMAIL`::\n+    The email of the committer that would be used if you were to run\n+    `git commit` right now.\n+\n+`GIT_COMMITTER_DATE`::\n+    The committer date and timezone that would be used if you were to run\n+    `git commit` right now.\n+\n+`GIT_SIGNING_KEY`::\n+    The key that would be used to sign the resulting commit if you were\n+    to run `git commit` right now.\n \n `GIT_EDITOR`::\n     Text editor for use by Git commands.  The value is meant to be\n@@ -85,9 +135,13 @@ endif::git-default-pager[]\n     The path to the global (per-user) configuration files, if any.\n \n Most path values contain only one value. However, some can contain multiple\n-values, which are separated by newlines, and are listed in order from highest to\n-lowest priority.  Callers should be prepared for any such path value to contain\n-multiple items.\n+values, which are separated by newlines (or NUL bytes if `-z` is given),\n+and are listed in order from highest to lowest priority. When querying\n+multiple variables, an extra newline (or an extra NUL byte if `-z` is\n+given) is printed after the values of a multi-valued variable to mark the\n+end of its list. (Single-variable queries and `git var -l` do not print\n+an extra delimiter). Callers should be prepared for any such path value to\n+contain multiple items.\n \n Note that paths are printed even if they do not exist, but not if they are\n disabled by other environment variables.\ndiff --git a/builtin/var.c b/builtin/var.c\nindex cc3a43cde2..decada1602 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -12,25 +12,112 @@\n #include \"config.h\"\n #include \"editor.h\"\n #include \"environment.h\"\n+#include \"gpg-interface.h\"\n #include \"ident.h\"\n #include \"pager.h\"\n-#include \"refs.h\"\n+#include \"parse-options.h\"\n #include \"path.h\"\n-#include \"strbuf.h\"\n+#include \"refs.h\"\n #include \"run-command.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n+\n+static const char * const var_usage[] = {\n+\tN_(\"git var [-z] -l\"),\n+\tN_(\"git var [-z] <variable>...\"),\n+\tNULL\n+};\n \n-static const char var_usage[] = \"git var (-l | <variable>)\";\n+enum ident_part {\n+\tIDENT_NAME,\n+\tIDENT_MAIL,\n+\tIDENT_DATE,\n+};\n \n static char *committer(int ident_flag)\n {\n \treturn xstrdup_or_null(git_committer_info(ident_flag));\n }\n \n+static char *ident_part(const char *ident, enum ident_part part)\n+{\n+\tstruct ident_split split;\n+\n+\tif (!ident)\n+\t\treturn NULL;\n+\tif (split_ident_line(&split, ident, strlen(ident)))\n+\t\treturn NULL;\n+\n+\tswitch (part) {\n+\tcase IDENT_NAME:\n+\t\treturn xmemdupz(split.name_begin,\n+\t\t\t\tsplit.name_end - split.name_begin);\n+\tcase IDENT_MAIL:\n+\t\treturn xmemdupz(split.mail_begin,\n+\t\t\t\tsplit.mail_end - split.mail_begin);\n+\tcase IDENT_DATE:\n+\t\tif (!split.date_begin)\n+\t\t\treturn NULL;\n+\t\tif (split.tz_end)\n+\t\t\treturn xmemdupz(split.date_begin,\n+\t\t\t\t\tsplit.tz_end -\n+\t\t\t\t\tsplit.date_begin);\n+\t\tif (split.date_end)\n+\t\t\treturn xmemdupz(split.date_begin,\n+\t\t\t\t\tsplit.date_end -\n+\t\t\t\t\tsplit.date_begin);\n+\t\treturn NULL;\n+\tdefault:\n+\t\tBUG(\"unknown ident_part %d\", part);\n+\t}\n+}\n+\n+static char *committer_name(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *committer_email(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *committer_date(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_DATE);\n+}\n+\n static char *author(int ident_flag)\n {\n \treturn xstrdup_or_null(git_author_info(ident_flag));\n }\n \n+static char *author_name(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *author_email(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *author_date(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_DATE);\n+}\n+\n+static char *git_signing_key(int ident_flag UNUSED)\n+{\n+\tchar *signing_key = get_signing_key();\n+\n+\tif (signing_key && !*signing_key) {\n+\t\tfree(signing_key);\n+\t\treturn NULL;\n+\t}\n+\treturn signing_key;\n+}\n+\n static char *editor(int ident_flag UNUSED)\n {\n \treturn xstrdup_or_null(git_editor());\n@@ -90,45 +177,62 @@ static char *git_config_val_system(int ident_flag UNUSED)\n \treturn NULL;\n }\n \n-static char *git_config_val_global(int ident_flag UNUSED)\n+static int git_config_val_global(struct string_list *list)\n {\n-\tstruct strbuf buf = STRBUF_INIT;\n \tchar *user, *xdg;\n-\tsize_t unused;\n \n \tgit_global_config_paths(&user, &xdg);\n \tif (xdg && *xdg) {\n \t\tnormalize_path_copy(xdg, xdg);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", xdg);\n+\t\tstring_list_append(list, xdg);\n \t}\n \tif (user && *user) {\n \t\tnormalize_path_copy(user, user);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", user);\n+\t\tstring_list_append(list, user);\n \t}\n \tfree(xdg);\n \tfree(user);\n-\tstrbuf_trim_trailing_newline(&buf);\n-\tif (buf.len == 0) {\n-\t\tstrbuf_release(&buf);\n-\t\treturn NULL;\n-\t}\n-\treturn strbuf_detach(&buf, &unused);\n+\treturn !list->nr;\n }\n \n struct git_var {\n \tconst char *name;\n \tchar *(*read)(int);\n-\tint multivalued;\n+\tint (*multiread)(struct string_list *);\n };\n static struct git_var git_vars[] = {\n \t{\n \t\t.name = \"GIT_COMMITTER_IDENT\",\n \t\t.read = committer,\n \t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_NAME\",\n+\t\t.read = committer_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_EMAIL\",\n+\t\t.read = committer_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_DATE\",\n+\t\t.read = committer_date,\n+\t},\n \t{\n \t\t.name = \"GIT_AUTHOR_IDENT\",\n \t\t.read = author,\n \t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_NAME\",\n+\t\t.read = author_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_EMAIL\",\n+\t\t.read = author_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_DATE\",\n+\t\t.read = author_date,\n+\t},\n \t{\n \t\t.name = \"GIT_EDITOR\",\n \t\t.read = editor,\n@@ -145,6 +249,10 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_DEFAULT_BRANCH\",\n \t\t.read = default_branch,\n \t},\n+\t{\n+\t\t.name = \"GIT_SIGNING_KEY\",\n+\t\t.read = git_signing_key,\n+\t},\n \t{\n \t\t.name = \"GIT_SHELL_PATH\",\n \t\t.read = shell_path,\n@@ -163,8 +271,7 @@ static struct git_var git_vars[] = {\n \t},\n \t{\n \t\t.name = \"GIT_CONFIG_GLOBAL\",\n-\t\t.read = git_config_val_global,\n-\t\t.multivalued = 1,\n+\t\t.multiread = git_config_val_global,\n \t},\n \t{\n \t\t.name = \"\",\n@@ -172,31 +279,38 @@ static struct git_var git_vars[] = {\n \t},\n };\n \n-static void list_vars(void)\n+static void list_vars(int nul_term)\n {\n \tstruct git_var *ptr;\n-\tchar *val;\n+\tchar delim = nul_term ? '\\n' : '=';\n+\tchar term = nul_term ? '\\0' : '\\n';\n \n-\tfor (ptr = git_vars; ptr->read; ptr++)\n-\t\tif ((val = ptr->read(0))) {\n-\t\t\tif (ptr->multivalued && *val) {\n-\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n+\t\tif (ptr->read) {\n+\t\t\tchar *val = ptr->read(0);\n \n-\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n-\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n-\t\t\t\tstring_list_clear(&list, 0);\n-\t\t\t} else {\n-\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n+\t\t\tif (val) {\n+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n+\t\t\t\tfree(val);\n \t\t\t}\n-\t\t\tfree(val);\n+\t\t} else if (ptr->multiread) {\n+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\t\t\tsize_t i;\n+\n+\t\t\tif (!ptr->multiread(&list)) {\n+\t\t\t\tfor (i = 0; i < list.nr; i++)\n+\t\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n+\t\t\t\t\t       list.items[i].string, term);\n+\t\t\t}\n+\t\t\tstring_list_clear(&list, 0);\n \t\t}\n+\t}\n }\n \n static const struct git_var *get_git_var(const char *var)\n {\n \tstruct git_var *ptr;\n-\tfor (ptr = git_vars; ptr->read; ptr++) {\n+\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n \t\tif (strcmp(var, ptr->name) == 0) {\n \t\t\treturn ptr;\n \t\t}\n@@ -207,42 +321,91 @@ static const struct git_var *get_git_var(const char *var)\n static int show_config(const char *var, const char *value,\n \t\t       const struct config_context *ctx, void *cb)\n {\n+\tint *nul_term = cb;\n+\tchar delim = *nul_term ? '\\n' : '=';\n+\tchar term = *nul_term ? '\\0' : '\\n';\n+\n \tif (value)\n-\t\tprintf(\"%s=%s\\n\", var, value);\n+\t\tprintf(\"%s%c%s%c\", var, delim, value, term);\n \telse\n-\t\tprintf(\"%s\\n\", var);\n+\t\tprintf(\"%s%c\", var, term);\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n int cmd_var(int argc,\n \t    const char **argv,\n-\t    const char *prefix UNUSED,\n+\t    const char *prefix,\n \t    struct repository *repo UNUSED)\n {\n-\tconst struct git_var *git_var;\n-\tchar *val;\n+\tint list = 0;\n+\tint nul_term = 0;\n+\tint ret = 0;\n+\tint i;\n+\tchar term;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL('l', NULL, &list,\n+\t\t\t N_(\"list all variables\")),\n+\t\tOPT_BOOL('z', NULL, &nul_term,\n+\t\t\t N_(\"terminate entries with NUL\")),\n+\t\tOPT_END(),\n+\t};\n \n-\tshow_usage_if_asked(argc, argv, var_usage);\n-\tif (argc != 2)\n-\t\tusage(var_usage);\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (strcmp(argv[1], \"-l\") == 0) {\n-\t\trepo_config(the_repository, show_config, NULL);\n-\t\tlist_vars();\n+\tif (list) {\n+\t\tif (argc)\n+\t\t\tusage_with_options(var_usage, options);\n+\t\trepo_config(the_repository, show_config, &nul_term);\n+\t\tlist_vars(nul_term);\n \t\treturn 0;\n \t}\n+\n+\tif (!argc)\n+\t\tusage_with_options(var_usage, options);\n+\n \trepo_config(the_repository, git_default_config, NULL);\n \n-\tgit_var = get_git_var(argv[1]);\n-\tif (!git_var)\n-\t\tusage(var_usage);\n+\tterm = nul_term ? '\\0' : '\\n';\n \n-\tval = git_var->read(IDENT_STRICT);\n-\tif (!val)\n-\t\treturn 1;\n+\tfor (i = 0; i < argc; i++) {\n+\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n \n-\tprintf(\"%s\\n\", val);\n-\tfree(val);\n+\t\tif (!git_var)\n+\t\t\tusage_with_options(var_usage, options);\n+\n+\t\tif (git_var->read) {\n+\t\t\tchar *val = git_var->read(IDENT_STRICT);\n+\n+\t\t\tif (!val) {\n+\t\t\t\tif (argc == 1)\n+\t\t\t\t\treturn 1;\n+\t\t\t\tret = 1;\n+\t\t\t\tprintf(\"%c\", term);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tprintf(\"%s%c\", val, term);\n+\t\t\tfree(val);\n+\t\t} else if (git_var->multiread) {\n+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\t\t\tsize_t j;\n+\n+\t\t\tif (git_var->multiread(&list) || !list.nr) {\n+\t\t\t\tif (argc == 1) {\n+\t\t\t\t\tstring_list_clear(&list, 0);\n+\t\t\t\t\treturn 1;\n+\t\t\t\t}\n+\t\t\t\tret = 1;\n+\t\t\t\tprintf(\"%c\", term);\n+\t\t\t} else {\n+\t\t\t\tfor (j = 0; j < list.nr; j++)\n+\t\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n+\t\t\t\tif (argc > 1)\n+\t\t\t\t\tprintf(\"%c\", term);\n+\t\t\t}\n+\t\t\tstring_list_clear(&list, 0);\n+\t\t}\n+\t}\n \n-\treturn 0;\n+\treturn ret;\n }\ndiff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\nindex 2b60317758..92b68b9ab4 100755\n--- a/t/t0007-git-var.sh\n+++ b/t/t0007-git-var.sh\n@@ -276,4 +276,127 @@ test_expect_success '`git var -l` works even without HOME' '\n \t)\n '\n \n+test_expect_success 'get author identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n+\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n+\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n+\tgit var GIT_AUTHOR_NAME >actual.name &&\n+\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n+\tgit var GIT_AUTHOR_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get committer identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n+\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n+\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n+\tgit var GIT_COMMITTER_NAME >actual.name &&\n+\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n+\tgit var GIT_COMMITTER_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get multiple variables' '\n+\ttest_tick &&\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME\n+\t$GIT_AUTHOR_EMAIL\n+\t$GIT_COMMITTER_NAME\n+\t$GIT_COMMITTER_EMAIL\n+\tEOF\n+\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z' '\n+\ttest_tick &&\n+\tprintf \"%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual &&\n+\tprintf \"%s\\0\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expected &&\n+\ttest_cmp expected actual\n+'\n+\n+test_expect_success 'git var -l -z' '\n+\tgit var -l -z >actual &&\n+\ttr \"\\0\" \"\\n\" <actual >actual.lines &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect &&\n+\tsed -n \"/^GIT_AUTHOR_NAME$/{n;p;}\" actual.lines >actual.author &&\n+\ttest_cmp expect actual.author &&\n+\techo false >expect &&\n+\tsed -n \"/^core\\.bare$/{n;p;}\" actual.lines >actual.bare &&\n+\ttest_cmp expect actual.bare\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY with user.signingkey configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\techo \"TEST_KEY_ID\" >expect &&\n+\tgit var GIT_SIGNING_KEY >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '\n+\ttest_config user.signingkey \"\" &&\n+\ttest_must_fail git var GIT_SIGNING_KEY\n+'\n+\n+test_expect_success 'git var -l lists new variables' '\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n+'\n+\n+test_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_SIGNING_KEY=TEST_KEY_ID\" actual\n+'\n+\n+test_expect_success 'options must precede variable arguments' '\n+\ttest_must_fail git var GIT_AUTHOR_NAME -z\n+'\n+\n+test_expect_success 'get multiple variables with unset variable outputs blank record' '\n+\ttest_config user.signingkey \"\" &&\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME\n+\n+\t$GIT_COMMITTER_NAME\n+\tEOF\n+\ttest_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z and unset variable' '\n+\ttest_config user.signingkey \"\" &&\n+\tprintf \"%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_COMMITTER_NAME\" >expect &&\n+\ttest_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables including multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tprintf \"%s\\0%s\\0%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \\\n+\t\t\"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" \\\n+\t\t\"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n+\t\tgit var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n-- \ngitgitgadget\n"},{"id":"552335","messageId":"07d1ef05-97df-481a-b647-cbe33a57f902@gmail.com","threadId":"66219","inReplyTo":"pull.2388.v6.git.git.1788917076554.gitgitgadget@gmail.com","subject":"Re: [PATCH v6] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-09T15:36:16Z","receivedAt":"2026-09-09T15:36:24Z","isPatch":true,"body":"Hi Andrew\n\nOn 09/09/2026 02:24, Andrew Pleeter via GitGitGadget wrote:\n> From: Andrew Pleeter <andrewpleeter@gmail.com>\n> \n> While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\n> extracting individual components (name, email, or date) currently\n> requires callers to manually parse the composite string. Furthermore,\n> there is no way to query the resolved commit signing key through\n> 'git var', and the command only accepts a single variable at a time.\n> \n> Teach 'git var' to expose individual identity components and commit\n> signing configuration, and allow querying multiple variables with\n> optional NUL-termination:\n> \n> - Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n> - Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n> - Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n>    the resulting commit if you were to run 'git commit' right now.\n\nI'm still curious what the use case for GIT_SIGNING_KEY is. Is the key \nalone enough for the caller to determine if they should be using gpg or \nssh? I've asked this twice already - when a reviewer asks a question it \nis helpful to rely rather than just sending a new version of the patch.\n> - Allow passing multiple variable arguments (e.g., 'git var\n>    GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n>    sequentially.\n> - Support '-z' to terminate variable outputs with NUL bytes.\n> - Format 'git var -l -z' using the same convention as 'git config\n>    list -z' (newline separating key and value, NUL separating entries).\n> - Delimit values of multi-valued variables with NUL when '-z' is given,\n>    and output an extra delimiter after multi-valued variables when\n>    querying multiple variables to disambiguate the stream.\n> - When querying multiple variables, print an empty record for any\n>    variable that has no value and continue processing remaining variables.\n> - Use parse_options() to strictly require options before arguments.\n> - Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n> \n> Signed-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n> ---\n\n> diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\n> index 697c10aded..2c1eaf3cf7 100644\n> --- a/Documentation/git-var.adoc\n> +++ b/Documentation/git-var.adoc\n> @@ -9,12 +9,16 @@ git-var - Show a Git logical variable\n>   SYNOPSIS\n>   --------\n>   [synopsis]\n> -git var (-l | <variable>)\n> +git var [-z] -l\n> +git var [-z] <variable>...\n>   \n>   DESCRIPTION\n>   -----------\n> -Prints a Git logical variable. Exits with code 1 if the variable has\n> -no value.\n> +Prints Git logical variables. Exits with code 1 if any requested\n> +variable has no value.\n\nI'm not sure that is very useful when the user asks for more than one \nvariable - they can see the value was empty by looking at the output and \nmeans callers cannot check for fatal errors such as an invalid variable \nname by simply looking for a non-zero exit code.\n\n> When multiple variables are requested, an empty\n> +record (a blank line, or an empty NUL-terminated record when `-z` is given)\n> +is printed for any variable that has no value, and the command continues\n> +processing the remaining variables.\n>   \n>   OPTIONS\n>   -------\n> @@ -24,19 +28,65 @@ OPTIONS\n>   \tas well. (However, the configuration variables listing functionality\n>   \tis deprecated in favor of `git config list`.)\n>   \n> +`-z`::\n> +\tTerminate entries with NUL instead of newline. When used with\n> +\t`-l`, the variable name and its value are separated by a newline,\n> +\tand each entry is terminated with a NUL byte.\n\nGood\n\n> @@ -85,9 +135,13 @@ endif::git-default-pager[]\n>       The path to the global (per-user) configuration files, if any.\n>   \n>   Most path values contain only one value. However, some can contain multiple\n> -values, which are separated by newlines, and are listed in order from highest to\n> -lowest priority.  Callers should be prepared for any such path value to contain\n> -multiple items.\n> +values, which are separated by newlines (or NUL bytes if `-z` is given),\n> +and are listed in order from highest to lowest priority. When querying\n> +multiple variables, an extra newline (or an extra NUL byte if `-z` is\n> +given) is printed after the values of a multi-valued variable to mark the\n> +end of its list.\n\nWe should mark each mult-valued varibale in its description so that \nusers know when to expect a list.\n\n> (Single-variable queries and `git var -l` do not print\n> +an extra delimiter). Callers should be prepared for any such path value to\n> +contain multiple items.\n>   \n>   Note that paths are printed even if they do not exist, but not if they are\n>   disabled by other environment variables.\n\n> -static char *git_config_val_global(int ident_flag UNUSED)\n> +static int git_config_val_global(struct string_list *list)\n>   {\n> -\tstruct strbuf buf = STRBUF_INIT;\n>   \tchar *user, *xdg;\n> -\tsize_t unused;\n>   \n>   \tgit_global_config_paths(&user, &xdg);\n>   \tif (xdg && *xdg) {\n>   \t\tnormalize_path_copy(xdg, xdg);\n> -\t\tstrbuf_addf(&buf, \"%s\\n\", xdg);\n> +\t\tstring_list_append(list, xdg);\n>   \t}\n>   \tif (user && *user) {\n>   \t\tnormalize_path_copy(user, user);\n> -\t\tstrbuf_addf(&buf, \"%s\\n\", user);\n> +\t\tstring_list_append(list, user);\n>   \t}\n>   \tfree(xdg);\n>   \tfree(user);\n> -\tstrbuf_trim_trailing_newline(&buf);\n> -\tif (buf.len == 0) {\n> -\t\tstrbuf_release(&buf);\n> -\t\treturn NULL;\n> -\t}\n> -\treturn strbuf_detach(&buf, &unused);\n> +\treturn !list->nr;\n>   }\n\nThis is a nice improvement that could perhaps be split out into a \nseparate preparatory change together with the change from a flag to a \ndifferent read function for multi-valued variables below.\n\n>   \n>   struct git_var {\n>   \tconst char *name;\n>   \tchar *(*read)(int);\n> -\tint multivalued;\n> +\tint (*multiread)(struct string_list *);\n>   };\n\n> -static void list_vars(void)\n> +static void list_vars(int nul_term)\n>   {\n>   \tstruct git_var *ptr;\n> -\tchar *val;\n> +\tchar delim = nul_term ? '\\n' : '=';\n> +\tchar term = nul_term ? '\\0' : '\\n';\n>   \n> -\tfor (ptr = git_vars; ptr->read; ptr++)\n> -\t\tif ((val = ptr->read(0))) {\n> -\t\t\tif (ptr->multivalued && *val) {\n> -\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n> +\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n> +\t\tif (ptr->read) {\n> +\t\t\tchar *val = ptr->read(0);\n>   \n> -\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n> -\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n> -\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n> -\t\t\t\tstring_list_clear(&list, 0);\n> -\t\t\t} else {\n> -\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n> +\t\t\tif (val) {\n> +\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n> +\t\t\t\tfree(val);\n>   \t\t\t}\n> -\t\t\tfree(val);\n> +\t\t} else if (ptr->multiread) {\n\nWe should just assume that ptr->multread is set when ptr->read is not, \nor possibly add an else clause that calls BUG().\n\n> +\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n> +\t\t\tsize_t i;\n> +\n> +\t\t\tif (!ptr->multiread(&list)) {\n> +\t\t\t\tfor (i = 0; i < list.nr; i++)\n> +\t\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n> +\t\t\t\t\t       list.items[i].string, term);\n> +\t\t\t}\n> +\t\t\tstring_list_clear(&list, 0);\n\n>   int cmd_var(int argc,\n>   \t    const char **argv,\n> -\t    const char *prefix UNUSED,\n> +\t    const char *prefix,\n>   \t    struct repository *repo UNUSED)\n > [...]\n> +\tfor (i = 0; i < argc; i++) {\n> +\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n>   \n> -\tprintf(\"%s\\n\", val);\n> -\tfree(val);\n> +\t\tif (!git_var)\n> +\t\t\tusage_with_options(var_usage, options);\n> +\n> +\t\tif (git_var->read) {\n> +\t\t\tchar *val = git_var->read(IDENT_STRICT);\n> +\n> +\t\t\tif (!val) {\n> +\t\t\t\tif (argc == 1)\n> +\t\t\t\t\treturn 1;\n> +\t\t\t\tret = 1;\n\nWhat's the benefit of this? The caller can see there was an empty value \nso why do we want a non-zero exit code as well. For example, if the \ncaller is asking for GIT_CONFIG_SYSTEM and GIT_CONFIG_GLOBAL but the \nuser ran the script with GIT_CONFIG_NOSYSTEM then that shouldn't be an \nerror - the caller should just not use the system config.\n\n> +\t\t\t\tprintf(\"%c\", term);\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n> +\t\t\tprintf(\"%s%c\", val, term);\n> +\t\t\tfree(val);\n> +\t\t} else if (git_var->multiread) {\n> +\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n> +\t\t\tsize_t j;\n> +\n> +\t\t\tif (git_var->multiread(&list) || !list.nr) {\n\nWhy are we checking the return value of the function and the list length \n- surely the list length tells us everything we need to know.\n\n> +\t\t\t\tif (argc == 1) {\n> +\t\t\t\t\tstring_list_clear(&list, 0);\n> +\t\t\t\t\treturn 1;\n> +\t\t\t\t}\n> +\t\t\t\tret = 1;\n> +\t\t\t\tprintf(\"%c\", term);\n> +\t\t\t} else {\n> +\t\t\t\tfor (j = 0; j < list.nr; j++)\n> +\t\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n> +\t\t\t\tif (argc > 1)\n> +\t\t\t\t\tprintf(\"%c\", term);\n> +\t\t\t}\n> +\t\t\tstring_list_clear(&list, 0);\n\nI think the above can be simplified to\n\n\t} else {\n\t\tstruct string_list list = STRING_LIST_INIT_NODUP;\n\t\t\n\t\tgit_var->multiread(&list);\n\t\tif (argc == 1 && !list.nr) {\n\t\t\treturn 1;\n\t\t}\n\t\tfor (j = 0; j < list.nr; j++)\n\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n\t\tif (argc > 1)\n\t\t\tputc(term);\n\n\t\tstring_list_clear(&list, 0);\n\t}\n\nI've not had time to look too closely at the tests, but I did notice \nthey use test_cmp() on files containing '\\0' which isn't a good idea \nbecause diff will see them as binary files. We have helpers like \nnul_to_q to translate nul to a printable character. I'm going to be off \nthe list from tomorrow until the middle of next week so it will be a few \ndays before I look at the next (and hopefully final) version.\n\nThanks\n\nPhillip\n\n> +\t\t}\n> +\t}\n>   \n> -\treturn 0;\n> +\treturn ret;\n>   }\n> diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\n> index 2b60317758..92b68b9ab4 100755\n> --- a/t/t0007-git-var.sh\n> +++ b/t/t0007-git-var.sh\n> @@ -276,4 +276,127 @@ test_expect_success '`git var -l` works even without HOME' '\n>   \t)\n>   '\n>   \n> +test_expect_success 'get author identity components' '\n> +\ttest_tick &&\n> +\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n> +\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n> +\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n> +\tgit var GIT_AUTHOR_NAME >actual.name &&\n> +\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n> +\tgit var GIT_AUTHOR_DATE >actual.date &&\n> +\ttest_cmp expect.name actual.name &&\n> +\ttest_cmp expect.email actual.email &&\n> +\ttest_cmp expect.date actual.date\n> +'\n> +\n> +test_expect_success 'get committer identity components' '\n> +\ttest_tick &&\n> +\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n> +\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n> +\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n> +\tgit var GIT_COMMITTER_NAME >actual.name &&\n> +\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n> +\tgit var GIT_COMMITTER_DATE >actual.date &&\n> +\ttest_cmp expect.name actual.name &&\n> +\ttest_cmp expect.email actual.email &&\n> +\ttest_cmp expect.date actual.date\n> +'\n> +\n> +test_expect_success 'get multiple variables' '\n> +\ttest_tick &&\n> +\tcat >expect <<-EOF &&\n> +\t$GIT_AUTHOR_NAME\n> +\t$GIT_AUTHOR_EMAIL\n> +\t$GIT_COMMITTER_NAME\n> +\t$GIT_COMMITTER_EMAIL\n> +\tEOF\n> +\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'get multiple variables with -z' '\n> +\ttest_tick &&\n> +\tprintf \"%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n> +\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'get multi-valued variable with -z' '\n> +\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n> +\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual &&\n> +\tprintf \"%s\\0\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expected &&\n> +\ttest_cmp expected actual\n> +'\n> +\n> +test_expect_success 'git var -l -z' '\n> +\tgit var -l -z >actual &&\n> +\ttr \"\\0\" \"\\n\" <actual >actual.lines &&\n> +\techo \"$GIT_AUTHOR_NAME\" >expect &&\n> +\tsed -n \"/^GIT_AUTHOR_NAME$/{n;p;}\" actual.lines >actual.author &&\n> +\ttest_cmp expect actual.author &&\n> +\techo false >expect &&\n> +\tsed -n \"/^core\\.bare$/{n;p;}\" actual.lines >actual.bare &&\n> +\ttest_cmp expect actual.bare\n> +'\n> +\n> +test_expect_success 'get GIT_SIGNING_KEY with user.signingkey configured' '\n> +\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n> +\techo \"TEST_KEY_ID\" >expect &&\n> +\tgit var GIT_SIGNING_KEY >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'get GIT_SIGNING_KEY fails when unset' '\n> +\ttest_config user.signingkey \"\" &&\n> +\ttest_must_fail git var GIT_SIGNING_KEY\n> +'\n> +\n> +test_expect_success 'git var -l lists new variables' '\n> +\tgit var -l >actual &&\n> +\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n> +\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n> +\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n> +\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n> +\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n> +\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n> +'\n> +\n> +test_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '\n> +\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n> +\tgit var -l >actual &&\n> +\ttest_grep \"^GIT_SIGNING_KEY=TEST_KEY_ID\" actual\n> +'\n> +\n> +test_expect_success 'options must precede variable arguments' '\n> +\ttest_must_fail git var GIT_AUTHOR_NAME -z\n> +'\n> +\n> +test_expect_success 'get multiple variables with unset variable outputs blank record' '\n> +\ttest_config user.signingkey \"\" &&\n> +\tcat >expect <<-EOF &&\n> +\t$GIT_AUTHOR_NAME\n> +\n> +\t$GIT_COMMITTER_NAME\n> +\tEOF\n> +\ttest_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'get multiple variables with -z and unset variable' '\n> +\ttest_config user.signingkey \"\" &&\n> +\tprintf \"%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_COMMITTER_NAME\" >expect &&\n> +\ttest_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'get multiple variables including multi-valued variable with -z' '\n> +\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n> +\tprintf \"%s\\0%s\\0%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \\\n> +\t\t\"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" \\\n> +\t\t\"$GIT_AUTHOR_EMAIL\" >expect &&\n> +\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n> +\t\tgit var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n>   test_done\n> \n> base-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n\n"},{"id":"552338","messageId":"xmqq4ifytlzk.fsf@gitster.g","threadId":"66219","inReplyTo":"pull.2388.v6.git.git.1788917076554.gitgitgadget@gmail.com","subject":"Re: [PATCH v6] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-09T16:42:23Z","receivedAt":"2026-09-09T16:42:26Z","isPatch":true,"body":"\"Andrew Pleeter via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  VARIABLES\n>  ---------\n>  `GIT_AUTHOR_IDENT`::\n> -    The author of a piece of code.\n> +    The author name, email, and date that would be used if you were to\n> +    run `git commit` right now.\n> +\n> +`GIT_AUTHOR_NAME`::\n> +    The author name that would be used if you were to run `git commit`\n> +    right now.\n> +\n> +`GIT_AUTHOR_EMAIL`::\n> +    The author email that would be used if you were to run `git commit`\n> +    right now.\n> +\n> +`GIT_AUTHOR_DATE`::\n> +    The author date and timezone that would be used if you were to run\n> +    `git commit` right now.\n\nThis is better than the previous iteration, but wastes reader's time\nwith full of repetitions.  Have you looked at the one suggested in\nhttps://lore.kernel.org/git/xmqqbjaecjxb.fsf@gitster.g/ for example\nto present more information in much more concise way?\n\n>  Most path values contain only one value. However, some can contain multiple\n> -values, which are separated by newlines, and are listed in order from highest to\n> -lowest priority.  Callers should be prepared for any such path value to contain\n> -multiple items.\n> +values, which are separated by newlines (or NUL bytes if `-z` is given),\n> +and are listed in order from highest to lowest priority. When querying\n> +multiple variables, an extra newline (or an extra NUL byte if `-z` is\n> +given) is printed after the values of a multi-valued variable to mark the\n> +end of its list. (Single-variable queries and `git var -l` do not print\n> +an extra delimiter). Callers should be prepared for any such path value to\n> +contain multiple items.\n\nThis makes it clear that the query forms we had before this change\nwill produce the same output, and that queries asking for more than\none value use a new format, which is good.\n\nHowever, I am not sure why we want an extra delimiter only after a\nmulti-valued variable.  Does it mean that the reading script needs\nto be aware of which variables are multi-valued and which are not?\nIt is not clear whether this extra delimiter is present only when a\npotentially multi-valued variable actually has multiple values, or if\nwe will have the extra delimiter even when such a variable happens to\nhave only a single (or perhaps zero) value.\n\nEspecially given that ...\n\n>  Note that paths are printed even if they do not exist, but not if they are\n>  disabled by other environment variables.\n\n... some paths may not be printed even when explicitly requested in\nthe new \"give me values of these variables\" form, it appears to me\nthat the extra delimiter, even with the reader's knowledge of which\nvariables are multi-valued, does not help identify which value\ncorresponds to which requested variable.  I can accept, to a limited\ndegree, the argument that a list of 'val' is less work to parse than\na list of 'var=val' simply because you do not have to strip 'var='\nfrom the front.  However, it looks to me that the proposed format\nmakes the wrong trade-off by making it harder to match a variable to\nits value(s).\n\n> diff --git a/builtin/var.c b/builtin/var.c\n> index cc3a43cde2..decada1602 100644\n> +static char *ident_part(const char *ident, enum ident_part part)\n> +{\n> +\tstruct ident_split split;\n> +\n> +\tif (!ident)\n> +\t\treturn NULL;\n> +\tif (split_ident_line(&split, ident, strlen(ident)))\n> +\t\treturn NULL;\n> +\n> +\tswitch (part) {\n> +\tcase IDENT_NAME:\n> +\t\treturn xmemdupz(split.name_begin,\n> +\t\t\t\tsplit.name_end - split.name_begin);\n> +\tcase IDENT_MAIL:\n> +\t\treturn xmemdupz(split.mail_begin,\n> +\t\t\t\tsplit.mail_end - split.mail_begin);\n\nThis is better in that it no longer returns NULL upon an impossible\ncondition like the previous iteration did.  Even better, we could\nprotect ourselves against breakage caused by careless updates to\nsplit_ident_line() and git_*_info() functions we rely on by keep the\ncheck but mark BUG(), e.g.,\n\n                case IDENT_NAME:\n        +\t\tif (!split.name_begin || !split.name_end)\n        +\t\t\tBUG(\"split_ident_line() gave NULL names???\");\n                        return xmemdupz(split.name_begin,\n                                        split.name_end - split.name_begin);\n\n> +\tcase IDENT_DATE:\n> +\t\tif (!split.date_begin)\n> +\t\t\treturn NULL;\n> +\t\tif (split.tz_end)\n> +\t\t\treturn xmemdupz(split.date_begin,\n> +\t\t\t\t\tsplit.tz_end -\n> +\t\t\t\t\tsplit.date_begin);\n> +\t\tif (split.date_end)\n> +\t\t\treturn xmemdupz(split.date_begin,\n> +\t\t\t\t\tsplit.date_end -\n> +\t\t\t\t\tsplit.date_begin);\n> +\t\treturn NULL;\n\nI gave \".name_begin/.name_end cannot be NULL with the way you call\nthe other routines\" in my previous response as a mere example, while\nhoping that you'd do similar due dilligence to other values.  With\nthe way committer_date() and author_date() are called (below), can\nfmt_ident() ever return an ident without datestamp and timezone,\nrequiring us to fall back on NULL returns like this?  You are not\npassing IDENT_NO_DATE flag anywhere, are you?\n\n>  struct git_var {\n>  \tconst char *name;\n>  \tchar *(*read)(int);\n> -\tint multivalued;\n> +\tint (*multiread)(struct string_list *);\n>  };\n> ...\n>  int cmd_var(int argc,\n>  \t    const char **argv,\n> -\t    const char *prefix UNUSED,\n> +\t    const char *prefix,\n>  \t    struct repository *repo UNUSED)\n>  {\n> +\tint list = 0;\n> +\tint nul_term = 0;\n> +\tint ret = 0;\n> +\tint i;\n> +\tchar term;\n> +\tstruct option options[] = {\n> +\t\tOPT_BOOL('l', NULL, &list,\n> +\t\t\t N_(\"list all variables\")),\n> +\t\tOPT_BOOL('z', NULL, &nul_term,\n> +\t\t\t N_(\"terminate entries with NUL\")),\n> +\t\tOPT_END(),\n> +\t};\n>  \n> +\targc = parse_options(argc, argv, prefix, options,\n> +\t\t\t     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n>  \n> +\tif (list) {\n> +\t\tif (argc)\n> +\t\t\tusage_with_options(var_usage, options);\n> +\t\trepo_config(the_repository, show_config, &nul_term);\n> +\t\tlist_vars(nul_term);\n>  \t\treturn 0;\n>  \t}\n\nOK.  Using \"-l\" and having named variables are incompatible.\n\n> +\n> +\tif (!argc)\n> +\t\tusage_with_options(var_usage, options);\n\nAnd not having named variables without \"-l\" invites the usage\nmessage.  We used to call usage() that exits with 129 and\nusage_with_options() does so, too.\n\n>  \trepo_config(the_repository, git_default_config, NULL);\n>  \n> +\tterm = nul_term ? '\\0' : '\\n';\n>  \n> +\tfor (i = 0; i < argc; i++) {\n> +\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n>  \n> +\t\tif (!git_var)\n> +\t\t\tusage_with_options(var_usage, options);\n> +\n> +\t\tif (git_var->read) {\n> +\t\t\tchar *val = git_var->read(IDENT_STRICT);\n> +\n> +\t\t\tif (!val) {\n> +\t\t\t\tif (argc == 1)\n> +\t\t\t\t\treturn 1;\n> +\t\t\t\tret = 1;\n> +\t\t\t\tprintf(\"%c\", term);\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n\nSo the idea is when there is a single var on the command line,\nmissing value gives no output and exit(1), but in the new \"more than\none variable\" mode, we note the fact that we had one failing\nvariable, emit a line terminator (NUL or LF) to help readers that\nexpect one \"line\" per request.\n\n> +\t\t\tprintf(\"%s%c\", val, term);\n> +\t\t\tfree(val);\n> +\t\t} else if (git_var->multiread) {\n> +\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n> +\t\t\tsize_t j;\n> +\n> +\t\t\tif (git_var->multiread(&list) || !list.nr) {\n> +\t\t\t\tif (argc == 1) {\n> +\t\t\t\t\tstring_list_clear(&list, 0);\n> +\t\t\t\t\treturn 1;\n> +\t\t\t\t}\n> +\t\t\t\tret = 1;\n> +\t\t\t\tprintf(\"%c\", term);\n\nThe same for variables that may have multiple values when they lack\nany value.\n\n> +\t\t\t} else {\n> +\t\t\t\tfor (j = 0; j < list.nr; j++)\n> +\t\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n> +\t\t\t\tif (argc > 1)\n> +\t\t\t\t\tprintf(\"%c\", term);\n\nSo this answers the question I had on ambiguous documentation.  A\nvariable that can have multiple values (including 0 values) will\nhave N \"lines\" of N values, plus an empty \"line\".\n\n\n> +\t\t\t}\n> +\t\t\tstring_list_clear(&list, 0);\n> +\t\t}\n> +\t}\n>  \n> +\treturn ret;\n>  }\n\nAnd we return \"ret\" that memorizes if we ever had a failure in the\nmiddle.  When there is no failure, we return 0 that is the value\n\"ret\" was initialized with.\n\nIf I were designing this, I'd rather (1) get rid of the \"empty line\"\nconvention for multi-valued variables, and (2) model multi-variable\nmode more after \"-l\" mode.  IOW, instead of thinking of the case\nwhere the user gave us two variables like two \"git var VARIBLE$N\"\ncalls given back to back, thinking it more like \"git var -l | grep\n-e VARIABLE1= -e VARIABLE2=\".\n\nThanks.\n\n"},{"id":"552389","messageId":"pull.2388.v7.git.git.1789009798902.gitgitgadget@gmail.com","threadId":"66219","inReplyTo":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","subject":"[PATCH v7] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Andrew Pleeter via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-10T03:09:58Z","receivedAt":"2026-09-10T03:10:03Z","isPatch":true,"body":"From: Andrew Pleeter <andrewpleeter@gmail.com>\n\nWhile 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\nextracting individual components (name, email, or date) currently\nrequires callers to manually parse the composite string. Furthermore,\nthere is no way to query the resolved commit signing key through\n'git var', and the command only accepts a single variable at a time.\n\nTeach 'git var' to expose individual identity components and commit\nsigning configuration, and allow querying multiple variables with\noptional NUL-termination:\n\n- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n- Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n  the resulting commit if you were to run 'git commit' right now.\n- Allow passing multiple variable arguments (e.g., 'git var\n  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n  sequentially.\n- Support '-z' to terminate variable outputs with NUL bytes.\n- Format 'git var -l -z' using the same convention as 'git config\n  list -z' (newline separating key and value, NUL separating entries).\n- Delimit values of multi-valued variables with NUL when '-z' is given,\n  and output an extra delimiter after multi-valued variables when\n  querying multiple variables to disambiguate the stream.\n- When querying multiple variables, print an empty record for any\n  variable that has no value and continue processing remaining variables.\n- Use parse_options() to strictly require options before arguments.\n- Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n    var: support broken-down idents, signing key, multiple args, and -z\n    \n    Teach git var to expose individual identity components and commit\n    signing configuration, and allow querying multiple variables with\n    optional NUL-termination.\n    \n    \n    Changes since v6:\n    =================\n    \n     * Grouped GIT_AUTHOR_* and GIT_COMMITTER_* entries together into\n       concise definitions in Documentation/git-var.adoc to avoid repetitive\n       descriptions, per feedback from Junio C. Hamano.\n     * Added explicit BUG() checks in ident_part() for NULL name/email\n       pointers and date/timezone to protect against unforeseen changes in\n       split_ident_line().\n     * Changed struct git_var member multiread to return void (void\n       (*multiread)(struct string_list *)) since list->nr communicates\n       length.\n     * Simplified list_vars() to use an unconditional else block for\n       ptr->multiread instead of redundant else if.\n     * In cmd_var(), do not set a non-zero exit status for missing variables\n       in multi-variable queries; callers can see the empty record (e.g.\n       GIT_CONFIG_SYSTEM under GIT_CONFIG_NOSYSTEM), matching Phillip Wood's\n       recommendation.\n     * Used putc(term, stdout) in cmd_var() when emitting delimiters.\n     * In t/t0007-git-var.sh, converted -z tests to pipe output through\n       nul_to_q so test_cmp produces clean diffs rather than binary\n       comparison errors.\n    \n    \n    Changes since v5:\n    =================\n    \n     * Reverted cosmetic refactoring in get_git_var() to keep the diff\n       minimal and focused on adding ptr->multiread.\n     * Added BUG(\"unknown ident_part %d\", part) to default case in\n       ident_part() and removed redundant NULL checks on split.name_begin\n       and split.mail_begin.\n     * Adopted first-class int (*multiread)(struct string_list *) callback\n       in struct git_var for multi-valued variables like GIT_CONFIG_GLOBAL,\n       cleanly populating a struct string_list instead of relying on\n       embedded NUL buffers.\n     * Improved GIT_AUTHOR_* and GIT_COMMITTER_* documentation in\n       Documentation/git-var.adoc to describe the values that would be used\n       if you were to run git commit right now.\n     * Explicitly documented that single-variable queries and git var -l do\n       not print an extra delimiter after multi-valued variables.\n    \n    \n    Changes since v4:\n    =================\n    \n     * Simplified git_signing_key() to directly call get_signing_key() as\n       used throughout Git (in tag, send-pack, and sign_buffer()).\n     * Renamed null_term to nul_term across builtin/var.c, and simplified\n       show_config() callback handling.\n     * Replaced the redundant pre-validation loop in cmd_var() by validating\n       arguments directly in the main execution loop.\n     * When querying multiple variables, print an empty record (blank line\n       or \\0 with -z) for any variable that has no value, and continue\n       printing remaining variables instead of terminating prematurely.\n     * Switched multi-valued variable storage (git_config_val_global()) to\n       internal \\0 delimiters, iterating directly through string sequences\n       without allocating a temporary string_list.\n     * For multi-variable queries, output an extra delimiter (\\n or \\0)\n       after multi-valued variables to clearly mark the end of their list.\n     * Explicitly documented the git var -l -z format and multi-variable\n       handling in Documentation/git-var.adoc.\n     * Added comprehensive tests for unset variables and multi-valued stream\n       delimiters in t/t0007-git-var.sh.\n    \n    \n    Changes since v3:\n    =================\n    \n     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip\n       Wood and Junio C Hamano; dropped the alias mechanism and\n       commit.gpgsign check.\n     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in\n       builtin/var.c, strictly enforcing that options precede variable\n       arguments.\n     * Adopted git config list -z format (key\\nvalue\\0) for git var -l -z to\n       prevent ambiguity with = in config keys.\n     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with\n       NUL bytes under -z.\n     * Replaced char part in ident_part() with enum ident_part.\n     * Split synopsis in Documentation/git-var.adoc into separate lines for\n       -l and <variable>..., and removed awkward legacy phrasing (\"of a\n       piece of code\").\n     * Added tests in t/t0007-git-var.sh covering the new -z format,\n       multi-valued -z, and argument ordering.\n    \n    \n    Changes since v2:\n    =================\n    \n     * Drop git ident / git whoami subcommand entirely.\n     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n     * Add GIT_SIGNING_KEY to resolve commit signing keys.\n     * Teach git var to accept multiple variable arguments (git var <var1>\n       <var2> ...).\n     * Add -z option to terminate outputs with NUL bytes (including git var\n       -l -z).\n     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v7\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v7\nPull-Request: https://github.com/git/git/pull/2388\n\nRange-diff vs v6:\n\n 1:  3b51f61a11 ! 1:  87f5f459ed var: support broken-down idents, signing key, multiple args, and -z\n     @@ Documentation/git-var.adoc: OPTIONS\n       ---------\n       `GIT_AUTHOR_IDENT`::\n      -    The author of a piece of code.\n     -+    The author name, email, and date that would be used if you were to\n     -+    run `git commit` right now.\n     -+\n      +`GIT_AUTHOR_NAME`::\n     -+    The author name that would be used if you were to run `git commit`\n     -+    right now.\n     -+\n      +`GIT_AUTHOR_EMAIL`::\n     -+    The author email that would be used if you were to run `git commit`\n     -+    right now.\n     -+\n      +`GIT_AUTHOR_DATE`::\n     -+    The author date and timezone that would be used if you were to run\n     -+    `git commit` right now.\n     ++    The authorship information that would be recorded in the\n     ++    resulting commit object if you ran `git commit` right now.\n     ++    `GIT_AUTHOR_IDENT` consists of the author's name, e-mail\n     ++    address, and timestamp+timezone. These three pieces of\n     ++    information are available separately as `GIT_AUTHOR_NAME`,\n     ++    `GIT_AUTHOR_EMAIL`, and `GIT_AUTHOR_DATE`.\n       \n       `GIT_COMMITTER_IDENT`::\n      -    The person who put a piece of code into Git.\n     -+    The committer name, email, and date that would be used if you were\n     -+    to run `git commit` right now.\n     -+\n      +`GIT_COMMITTER_NAME`::\n     -+    The name of the committer that would be used if you were to run\n     -+    `git commit` right now.\n     -+\n      +`GIT_COMMITTER_EMAIL`::\n     -+    The email of the committer that would be used if you were to run\n     -+    `git commit` right now.\n     -+\n      +`GIT_COMMITTER_DATE`::\n     -+    The committer date and timezone that would be used if you were to run\n     -+    `git commit` right now.\n     ++    The committer information that would be recorded in the\n     ++    resulting commit object if you ran `git commit` right now.\n     ++    `GIT_COMMITTER_IDENT` consists of the committer's name, e-mail\n     ++    address, and timestamp+timezone. These three pieces of\n     ++    information are available separately as `GIT_COMMITTER_NAME`,\n     ++    `GIT_COMMITTER_EMAIL`, and `GIT_COMMITTER_DATE`.\n      +\n      +`GIT_SIGNING_KEY`::\n      +    The key that would be used to sign the resulting commit if you were\n     @@ builtin/var.c\n       #include \"run-command.h\"\n      +#include \"strbuf.h\"\n      +#include \"string-list.h\"\n     -+\n     + \n     +-static const char var_usage[] = \"git var (-l | <variable>)\";\n      +static const char * const var_usage[] = {\n      +\tN_(\"git var [-z] -l\"),\n      +\tN_(\"git var [-z] <variable>...\"),\n      +\tNULL\n      +};\n     - \n     --static const char var_usage[] = \"git var (-l | <variable>)\";\n     ++\n      +enum ident_part {\n      +\tIDENT_NAME,\n      +\tIDENT_MAIL,\n     @@ builtin/var.c\n      +\n      +\tswitch (part) {\n      +\tcase IDENT_NAME:\n     ++\t\tif (!split.name_begin || !split.name_end)\n     ++\t\t\tBUG(\"split_ident_line() gave NULL names???\");\n      +\t\treturn xmemdupz(split.name_begin,\n      +\t\t\t\tsplit.name_end - split.name_begin);\n      +\tcase IDENT_MAIL:\n     ++\t\tif (!split.mail_begin || !split.mail_end)\n     ++\t\t\tBUG(\"split_ident_line() gave NULL mail???\");\n      +\t\treturn xmemdupz(split.mail_begin,\n      +\t\t\t\tsplit.mail_end - split.mail_begin);\n      +\tcase IDENT_DATE:\n     -+\t\tif (!split.date_begin)\n     -+\t\t\treturn NULL;\n     -+\t\tif (split.tz_end)\n     -+\t\t\treturn xmemdupz(split.date_begin,\n     -+\t\t\t\t\tsplit.tz_end -\n     -+\t\t\t\t\tsplit.date_begin);\n     -+\t\tif (split.date_end)\n     -+\t\t\treturn xmemdupz(split.date_begin,\n     -+\t\t\t\t\tsplit.date_end -\n     -+\t\t\t\t\tsplit.date_begin);\n     -+\t\treturn NULL;\n     ++\t\tif (!split.date_begin || !split.tz_end)\n     ++\t\t\tBUG(\"split_ident_line() gave NULL date/tz???\");\n     ++\t\treturn xmemdupz(split.date_begin,\n     ++\t\t\t\tsplit.tz_end - split.date_begin);\n      +\tdefault:\n      +\t\tBUG(\"unknown ident_part %d\", part);\n      +\t}\n     @@ builtin/var.c: static char *git_config_val_system(int ident_flag UNUSED)\n       }\n       \n      -static char *git_config_val_global(int ident_flag UNUSED)\n     -+static int git_config_val_global(struct string_list *list)\n     ++static void git_config_val_global(struct string_list *list)\n       {\n      -\tstruct strbuf buf = STRBUF_INIT;\n       \tchar *user, *xdg;\n     @@ builtin/var.c: static char *git_config_val_system(int ident_flag UNUSED)\n      -\t\treturn NULL;\n      -\t}\n      -\treturn strbuf_detach(&buf, &unused);\n     -+\treturn !list->nr;\n       }\n       \n       struct git_var {\n       \tconst char *name;\n       \tchar *(*read)(int);\n      -\tint multivalued;\n     -+\tint (*multiread)(struct string_list *);\n     ++\tvoid (*multiread)(struct string_list *);\n       };\n       static struct git_var git_vars[] = {\n       \t{\n     @@ builtin/var.c: static struct git_var git_vars[] = {\n      +\t\t\t\tfree(val);\n       \t\t\t}\n      -\t\t\tfree(val);\n     -+\t\t} else if (ptr->multiread) {\n     ++\t\t} else {\n      +\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n      +\t\t\tsize_t i;\n      +\n     -+\t\t\tif (!ptr->multiread(&list)) {\n     -+\t\t\t\tfor (i = 0; i < list.nr; i++)\n     -+\t\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n     -+\t\t\t\t\t       list.items[i].string, term);\n     -+\t\t\t}\n     ++\t\t\tptr->multiread(&list);\n     ++\t\t\tfor (i = 0; i < list.nr; i++)\n     ++\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n     ++\t\t\t\t       list.items[i].string, term);\n      +\t\t\tstring_list_clear(&list, 0);\n       \t\t}\n      +\t}\n     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n      -\tchar *val;\n      +\tint list = 0;\n      +\tint nul_term = 0;\n     -+\tint ret = 0;\n      +\tint i;\n      +\tchar term;\n      +\tstruct option options[] = {\n     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n      -\tif (!git_var)\n      -\t\tusage(var_usage);\n      +\tterm = nul_term ? '\\0' : '\\n';\n     ++\n     ++\tfor (i = 0; i < argc; i++) {\n     ++\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n       \n      -\tval = git_var->read(IDENT_STRICT);\n      -\tif (!val)\n      -\t\treturn 1;\n     -+\tfor (i = 0; i < argc; i++) {\n     -+\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n     ++\t\tif (!git_var)\n     ++\t\t\tusage_with_options(var_usage, options);\n       \n      -\tprintf(\"%s\\n\", val);\n      -\tfree(val);\n     -+\t\tif (!git_var)\n     -+\t\t\tusage_with_options(var_usage, options);\n     -+\n      +\t\tif (git_var->read) {\n      +\t\t\tchar *val = git_var->read(IDENT_STRICT);\n      +\n      +\t\t\tif (!val) {\n      +\t\t\t\tif (argc == 1)\n      +\t\t\t\t\treturn 1;\n     -+\t\t\t\tret = 1;\n     -+\t\t\t\tprintf(\"%c\", term);\n     ++\t\t\t\tputc(term, stdout);\n      +\t\t\t\tcontinue;\n      +\t\t\t}\n      +\t\t\tprintf(\"%s%c\", val, term);\n      +\t\t\tfree(val);\n     -+\t\t} else if (git_var->multiread) {\n     ++\t\t} else {\n      +\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n      +\t\t\tsize_t j;\n      +\n     -+\t\t\tif (git_var->multiread(&list) || !list.nr) {\n     -+\t\t\t\tif (argc == 1) {\n     -+\t\t\t\t\tstring_list_clear(&list, 0);\n     -+\t\t\t\t\treturn 1;\n     -+\t\t\t\t}\n     -+\t\t\t\tret = 1;\n     -+\t\t\t\tprintf(\"%c\", term);\n     -+\t\t\t} else {\n     -+\t\t\t\tfor (j = 0; j < list.nr; j++)\n     -+\t\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n     -+\t\t\t\tif (argc > 1)\n     -+\t\t\t\t\tprintf(\"%c\", term);\n     ++\t\t\tgit_var->multiread(&list);\n     ++\t\t\tif (argc == 1 && !list.nr) {\n     ++\t\t\t\tstring_list_clear(&list, 0);\n     ++\t\t\t\treturn 1;\n      +\t\t\t}\n     ++\t\t\tfor (j = 0; j < list.nr; j++)\n     ++\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n     ++\t\t\tif (argc > 1)\n     ++\t\t\t\tputc(term, stdout);\n      +\t\t\tstring_list_clear(&list, 0);\n      +\t\t}\n      +\t}\n       \n     --\treturn 0;\n     -+\treturn ret;\n     + \treturn 0;\n       }\n      \n       ## t/t0007-git-var.sh ##\n     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '\n      +\n      +test_expect_success 'get multiple variables with -z' '\n      +\ttest_tick &&\n     -+\tprintf \"%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n     -+\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual &&\n     ++\tprintf \"%sQ%sQ\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n     ++\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual.raw &&\n     ++\tnul_to_q <actual.raw >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'get multi-valued variable with -z' '\n      +\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n     -+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual &&\n     -+\tprintf \"%s\\0\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expected &&\n     -+\ttest_cmp expected actual\n     ++\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual.raw &&\n     ++\tprintf \"%sQ%sQ\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expect &&\n     ++\tnul_to_q <actual.raw >actual &&\n     ++\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'git var -l -z' '\n     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '\n      +\n      +\t$GIT_COMMITTER_NAME\n      +\tEOF\n     -+\ttest_must_fail git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n     ++\tgit var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'get multiple variables with -z and unset variable' '\n      +\ttest_config user.signingkey \"\" &&\n     -+\tprintf \"%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \"$GIT_COMMITTER_NAME\" >expect &&\n     -+\ttest_must_fail git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n     ++\tprintf \"%sQ%sQ\" \"$GIT_AUTHOR_NAME\" \"Q$GIT_COMMITTER_NAME\" >expect &&\n     ++\tgit var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&\n     ++\tnul_to_q <actual.raw >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'get multiple variables including multi-valued variable with -z' '\n      +\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n     -+\tprintf \"%s\\0%s\\0%s\\0\\0%s\\0\" \"$GIT_AUTHOR_NAME\" \\\n     ++\tprintf \"%sQ%sQ%sQQ%sQ\" \"$GIT_AUTHOR_NAME\" \\\n      +\t\t\"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" \\\n      +\t\t\"$GIT_AUTHOR_EMAIL\" >expect &&\n      +\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n     -+\t\tgit var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&\n     ++\t\tgit var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual.raw &&\n     ++\tnul_to_q <actual.raw >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n\n\n Documentation/git-var.adoc |  66 ++++++++--\n builtin/var.c              | 252 +++++++++++++++++++++++++++++--------\n t/t0007-git-var.sh         | 127 +++++++++++++++++++\n 3 files changed, 384 insertions(+), 61 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex 697c10aded..ccbb9f8e8d 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -9,12 +9,16 @@ git-var - Show a Git logical variable\n SYNOPSIS\n --------\n [synopsis]\n-git var (-l | <variable>)\n+git var [-z] -l\n+git var [-z] <variable>...\n \n DESCRIPTION\n -----------\n-Prints a Git logical variable. Exits with code 1 if the variable has\n-no value.\n+Prints Git logical variables. Exits with code 1 if any requested\n+variable has no value. When multiple variables are requested, an empty\n+record (a blank line, or an empty NUL-terminated record when `-z` is given)\n+is printed for any variable that has no value, and the command continues\n+processing the remaining variables.\n \n OPTIONS\n -------\n@@ -24,19 +28,55 @@ OPTIONS\n \tas well. (However, the configuration variables listing functionality\n \tis deprecated in favor of `git config list`.)\n \n+`-z`::\n+\tTerminate entries with NUL instead of newline. When used with\n+\t`-l`, the variable name and its value are separated by a newline,\n+\tand each entry is terminated with a NUL byte.\n+\n EXAMPLES\n --------\n-\t$ git var GIT_AUTHOR_IDENT\n-\tEric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n-\n+* Get the author identity:\n++\n+------------\n+$ git var GIT_AUTHOR_IDENT\n+Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n+------------\n+\n+* Get the author name and email:\n++\n+------------\n+$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL\n+Eric W. Biederman\n+ebiederm@lnxi.com\n+------------\n \n VARIABLES\n ---------\n `GIT_AUTHOR_IDENT`::\n-    The author of a piece of code.\n+`GIT_AUTHOR_NAME`::\n+`GIT_AUTHOR_EMAIL`::\n+`GIT_AUTHOR_DATE`::\n+    The authorship information that would be recorded in the\n+    resulting commit object if you ran `git commit` right now.\n+    `GIT_AUTHOR_IDENT` consists of the author's name, e-mail\n+    address, and timestamp+timezone. These three pieces of\n+    information are available separately as `GIT_AUTHOR_NAME`,\n+    `GIT_AUTHOR_EMAIL`, and `GIT_AUTHOR_DATE`.\n \n `GIT_COMMITTER_IDENT`::\n-    The person who put a piece of code into Git.\n+`GIT_COMMITTER_NAME`::\n+`GIT_COMMITTER_EMAIL`::\n+`GIT_COMMITTER_DATE`::\n+    The committer information that would be recorded in the\n+    resulting commit object if you ran `git commit` right now.\n+    `GIT_COMMITTER_IDENT` consists of the committer's name, e-mail\n+    address, and timestamp+timezone. These three pieces of\n+    information are available separately as `GIT_COMMITTER_NAME`,\n+    `GIT_COMMITTER_EMAIL`, and `GIT_COMMITTER_DATE`.\n+\n+`GIT_SIGNING_KEY`::\n+    The key that would be used to sign the resulting commit if you were\n+    to run `git commit` right now.\n \n `GIT_EDITOR`::\n     Text editor for use by Git commands.  The value is meant to be\n@@ -85,9 +125,13 @@ endif::git-default-pager[]\n     The path to the global (per-user) configuration files, if any.\n \n Most path values contain only one value. However, some can contain multiple\n-values, which are separated by newlines, and are listed in order from highest to\n-lowest priority.  Callers should be prepared for any such path value to contain\n-multiple items.\n+values, which are separated by newlines (or NUL bytes if `-z` is given),\n+and are listed in order from highest to lowest priority. When querying\n+multiple variables, an extra newline (or an extra NUL byte if `-z` is\n+given) is printed after the values of a multi-valued variable to mark the\n+end of its list. (Single-variable queries and `git var -l` do not print\n+an extra delimiter). Callers should be prepared for any such path value to\n+contain multiple items.\n \n Note that paths are printed even if they do not exist, but not if they are\n disabled by other environment variables.\ndiff --git a/builtin/var.c b/builtin/var.c\nindex cc3a43cde2..e8472ac722 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -12,25 +12,109 @@\n #include \"config.h\"\n #include \"editor.h\"\n #include \"environment.h\"\n+#include \"gpg-interface.h\"\n #include \"ident.h\"\n #include \"pager.h\"\n-#include \"refs.h\"\n+#include \"parse-options.h\"\n #include \"path.h\"\n-#include \"strbuf.h\"\n+#include \"refs.h\"\n #include \"run-command.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n \n-static const char var_usage[] = \"git var (-l | <variable>)\";\n+static const char * const var_usage[] = {\n+\tN_(\"git var [-z] -l\"),\n+\tN_(\"git var [-z] <variable>...\"),\n+\tNULL\n+};\n+\n+enum ident_part {\n+\tIDENT_NAME,\n+\tIDENT_MAIL,\n+\tIDENT_DATE,\n+};\n \n static char *committer(int ident_flag)\n {\n \treturn xstrdup_or_null(git_committer_info(ident_flag));\n }\n \n+static char *ident_part(const char *ident, enum ident_part part)\n+{\n+\tstruct ident_split split;\n+\n+\tif (!ident)\n+\t\treturn NULL;\n+\tif (split_ident_line(&split, ident, strlen(ident)))\n+\t\treturn NULL;\n+\n+\tswitch (part) {\n+\tcase IDENT_NAME:\n+\t\tif (!split.name_begin || !split.name_end)\n+\t\t\tBUG(\"split_ident_line() gave NULL names???\");\n+\t\treturn xmemdupz(split.name_begin,\n+\t\t\t\tsplit.name_end - split.name_begin);\n+\tcase IDENT_MAIL:\n+\t\tif (!split.mail_begin || !split.mail_end)\n+\t\t\tBUG(\"split_ident_line() gave NULL mail???\");\n+\t\treturn xmemdupz(split.mail_begin,\n+\t\t\t\tsplit.mail_end - split.mail_begin);\n+\tcase IDENT_DATE:\n+\t\tif (!split.date_begin || !split.tz_end)\n+\t\t\tBUG(\"split_ident_line() gave NULL date/tz???\");\n+\t\treturn xmemdupz(split.date_begin,\n+\t\t\t\tsplit.tz_end - split.date_begin);\n+\tdefault:\n+\t\tBUG(\"unknown ident_part %d\", part);\n+\t}\n+}\n+\n+static char *committer_name(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *committer_email(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *committer_date(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_DATE);\n+}\n+\n static char *author(int ident_flag)\n {\n \treturn xstrdup_or_null(git_author_info(ident_flag));\n }\n \n+static char *author_name(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *author_email(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *author_date(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_DATE);\n+}\n+\n+static char *git_signing_key(int ident_flag UNUSED)\n+{\n+\tchar *signing_key = get_signing_key();\n+\n+\tif (signing_key && !*signing_key) {\n+\t\tfree(signing_key);\n+\t\treturn NULL;\n+\t}\n+\treturn signing_key;\n+}\n+\n static char *editor(int ident_flag UNUSED)\n {\n \treturn xstrdup_or_null(git_editor());\n@@ -90,45 +174,61 @@ static char *git_config_val_system(int ident_flag UNUSED)\n \treturn NULL;\n }\n \n-static char *git_config_val_global(int ident_flag UNUSED)\n+static void git_config_val_global(struct string_list *list)\n {\n-\tstruct strbuf buf = STRBUF_INIT;\n \tchar *user, *xdg;\n-\tsize_t unused;\n \n \tgit_global_config_paths(&user, &xdg);\n \tif (xdg && *xdg) {\n \t\tnormalize_path_copy(xdg, xdg);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", xdg);\n+\t\tstring_list_append(list, xdg);\n \t}\n \tif (user && *user) {\n \t\tnormalize_path_copy(user, user);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", user);\n+\t\tstring_list_append(list, user);\n \t}\n \tfree(xdg);\n \tfree(user);\n-\tstrbuf_trim_trailing_newline(&buf);\n-\tif (buf.len == 0) {\n-\t\tstrbuf_release(&buf);\n-\t\treturn NULL;\n-\t}\n-\treturn strbuf_detach(&buf, &unused);\n }\n \n struct git_var {\n \tconst char *name;\n \tchar *(*read)(int);\n-\tint multivalued;\n+\tvoid (*multiread)(struct string_list *);\n };\n static struct git_var git_vars[] = {\n \t{\n \t\t.name = \"GIT_COMMITTER_IDENT\",\n \t\t.read = committer,\n \t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_NAME\",\n+\t\t.read = committer_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_EMAIL\",\n+\t\t.read = committer_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_DATE\",\n+\t\t.read = committer_date,\n+\t},\n \t{\n \t\t.name = \"GIT_AUTHOR_IDENT\",\n \t\t.read = author,\n \t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_NAME\",\n+\t\t.read = author_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_EMAIL\",\n+\t\t.read = author_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_DATE\",\n+\t\t.read = author_date,\n+\t},\n \t{\n \t\t.name = \"GIT_EDITOR\",\n \t\t.read = editor,\n@@ -145,6 +245,10 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_DEFAULT_BRANCH\",\n \t\t.read = default_branch,\n \t},\n+\t{\n+\t\t.name = \"GIT_SIGNING_KEY\",\n+\t\t.read = git_signing_key,\n+\t},\n \t{\n \t\t.name = \"GIT_SHELL_PATH\",\n \t\t.read = shell_path,\n@@ -163,8 +267,7 @@ static struct git_var git_vars[] = {\n \t},\n \t{\n \t\t.name = \"GIT_CONFIG_GLOBAL\",\n-\t\t.read = git_config_val_global,\n-\t\t.multivalued = 1,\n+\t\t.multiread = git_config_val_global,\n \t},\n \t{\n \t\t.name = \"\",\n@@ -172,31 +275,37 @@ static struct git_var git_vars[] = {\n \t},\n };\n \n-static void list_vars(void)\n+static void list_vars(int nul_term)\n {\n \tstruct git_var *ptr;\n-\tchar *val;\n+\tchar delim = nul_term ? '\\n' : '=';\n+\tchar term = nul_term ? '\\0' : '\\n';\n \n-\tfor (ptr = git_vars; ptr->read; ptr++)\n-\t\tif ((val = ptr->read(0))) {\n-\t\t\tif (ptr->multivalued && *val) {\n-\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n+\t\tif (ptr->read) {\n+\t\t\tchar *val = ptr->read(0);\n \n-\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n-\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n-\t\t\t\tstring_list_clear(&list, 0);\n-\t\t\t} else {\n-\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n+\t\t\tif (val) {\n+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n+\t\t\t\tfree(val);\n \t\t\t}\n-\t\t\tfree(val);\n+\t\t} else {\n+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\t\t\tsize_t i;\n+\n+\t\t\tptr->multiread(&list);\n+\t\t\tfor (i = 0; i < list.nr; i++)\n+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n+\t\t\t\t       list.items[i].string, term);\n+\t\t\tstring_list_clear(&list, 0);\n \t\t}\n+\t}\n }\n \n static const struct git_var *get_git_var(const char *var)\n {\n \tstruct git_var *ptr;\n-\tfor (ptr = git_vars; ptr->read; ptr++) {\n+\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n \t\tif (strcmp(var, ptr->name) == 0) {\n \t\t\treturn ptr;\n \t\t}\n@@ -207,42 +316,85 @@ static const struct git_var *get_git_var(const char *var)\n static int show_config(const char *var, const char *value,\n \t\t       const struct config_context *ctx, void *cb)\n {\n+\tint *nul_term = cb;\n+\tchar delim = *nul_term ? '\\n' : '=';\n+\tchar term = *nul_term ? '\\0' : '\\n';\n+\n \tif (value)\n-\t\tprintf(\"%s=%s\\n\", var, value);\n+\t\tprintf(\"%s%c%s%c\", var, delim, value, term);\n \telse\n-\t\tprintf(\"%s\\n\", var);\n+\t\tprintf(\"%s%c\", var, term);\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n int cmd_var(int argc,\n \t    const char **argv,\n-\t    const char *prefix UNUSED,\n+\t    const char *prefix,\n \t    struct repository *repo UNUSED)\n {\n-\tconst struct git_var *git_var;\n-\tchar *val;\n+\tint list = 0;\n+\tint nul_term = 0;\n+\tint i;\n+\tchar term;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL('l', NULL, &list,\n+\t\t\t N_(\"list all variables\")),\n+\t\tOPT_BOOL('z', NULL, &nul_term,\n+\t\t\t N_(\"terminate entries with NUL\")),\n+\t\tOPT_END(),\n+\t};\n \n-\tshow_usage_if_asked(argc, argv, var_usage);\n-\tif (argc != 2)\n-\t\tusage(var_usage);\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (strcmp(argv[1], \"-l\") == 0) {\n-\t\trepo_config(the_repository, show_config, NULL);\n-\t\tlist_vars();\n+\tif (list) {\n+\t\tif (argc)\n+\t\t\tusage_with_options(var_usage, options);\n+\t\trepo_config(the_repository, show_config, &nul_term);\n+\t\tlist_vars(nul_term);\n \t\treturn 0;\n \t}\n+\n+\tif (!argc)\n+\t\tusage_with_options(var_usage, options);\n+\n \trepo_config(the_repository, git_default_config, NULL);\n \n-\tgit_var = get_git_var(argv[1]);\n-\tif (!git_var)\n-\t\tusage(var_usage);\n+\tterm = nul_term ? '\\0' : '\\n';\n+\n+\tfor (i = 0; i < argc; i++) {\n+\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n \n-\tval = git_var->read(IDENT_STRICT);\n-\tif (!val)\n-\t\treturn 1;\n+\t\tif (!git_var)\n+\t\t\tusage_with_options(var_usage, options);\n \n-\tprintf(\"%s\\n\", val);\n-\tfree(val);\n+\t\tif (git_var->read) {\n+\t\t\tchar *val = git_var->read(IDENT_STRICT);\n+\n+\t\t\tif (!val) {\n+\t\t\t\tif (argc == 1)\n+\t\t\t\t\treturn 1;\n+\t\t\t\tputc(term, stdout);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tprintf(\"%s%c\", val, term);\n+\t\t\tfree(val);\n+\t\t} else {\n+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\t\t\tsize_t j;\n+\n+\t\t\tgit_var->multiread(&list);\n+\t\t\tif (argc == 1 && !list.nr) {\n+\t\t\t\tstring_list_clear(&list, 0);\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\tfor (j = 0; j < list.nr; j++)\n+\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n+\t\t\tif (argc > 1)\n+\t\t\t\tputc(term, stdout);\n+\t\t\tstring_list_clear(&list, 0);\n+\t\t}\n+\t}\n \n \treturn 0;\n }\ndiff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\nindex 2b60317758..75f688a1b1 100755\n--- a/t/t0007-git-var.sh\n+++ b/t/t0007-git-var.sh\n@@ -276,4 +276,131 @@ test_expect_success '`git var -l` works even without HOME' '\n \t)\n '\n \n+test_expect_success 'get author identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n+\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n+\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n+\tgit var GIT_AUTHOR_NAME >actual.name &&\n+\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n+\tgit var GIT_AUTHOR_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get committer identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n+\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n+\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n+\tgit var GIT_COMMITTER_NAME >actual.name &&\n+\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n+\tgit var GIT_COMMITTER_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get multiple variables' '\n+\ttest_tick &&\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME\n+\t$GIT_AUTHOR_EMAIL\n+\t$GIT_COMMITTER_NAME\n+\t$GIT_COMMITTER_EMAIL\n+\tEOF\n+\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z' '\n+\ttest_tick &&\n+\tprintf \"%sQ%sQ\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual.raw &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual.raw &&\n+\tprintf \"%sQ%sQ\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expect &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git var -l -z' '\n+\tgit var -l -z >actual &&\n+\ttr \"\\0\" \"\\n\" <actual >actual.lines &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect &&\n+\tsed -n \"/^GIT_AUTHOR_NAME$/{n;p;}\" actual.lines >actual.author &&\n+\ttest_cmp expect actual.author &&\n+\techo false >expect &&\n+\tsed -n \"/^core\\.bare$/{n;p;}\" actual.lines >actual.bare &&\n+\ttest_cmp expect actual.bare\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY with user.signingkey configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\techo \"TEST_KEY_ID\" >expect &&\n+\tgit var GIT_SIGNING_KEY >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '\n+\ttest_config user.signingkey \"\" &&\n+\ttest_must_fail git var GIT_SIGNING_KEY\n+'\n+\n+test_expect_success 'git var -l lists new variables' '\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n+'\n+\n+test_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_SIGNING_KEY=TEST_KEY_ID\" actual\n+'\n+\n+test_expect_success 'options must precede variable arguments' '\n+\ttest_must_fail git var GIT_AUTHOR_NAME -z\n+'\n+\n+test_expect_success 'get multiple variables with unset variable outputs blank record' '\n+\ttest_config user.signingkey \"\" &&\n+\tcat >expect <<-EOF &&\n+\t$GIT_AUTHOR_NAME\n+\n+\t$GIT_COMMITTER_NAME\n+\tEOF\n+\tgit var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z and unset variable' '\n+\ttest_config user.signingkey \"\" &&\n+\tprintf \"%sQ%sQ\" \"$GIT_AUTHOR_NAME\" \"Q$GIT_COMMITTER_NAME\" >expect &&\n+\tgit var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables including multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tprintf \"%sQ%sQ%sQQ%sQ\" \"$GIT_AUTHOR_NAME\" \\\n+\t\t\"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" \\\n+\t\t\"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n+\t\tgit var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual.raw &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n-- \ngitgitgadget\n"},{"id":"552725","messageId":"xmqq5x07630q.fsf@gitster.g","threadId":"66219","inReplyTo":"pull.2388.v7.git.git.1789009798902.gitgitgadget@gmail.com","subject":"Re: [PATCH v7] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-14T19:34:13Z","receivedAt":"2026-09-14T19:34:18Z","isPatch":true,"body":"\"Andrew Pleeter via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  DESCRIPTION\n>  -----------\n> +Prints Git logical variables. Exits with code 1 if any requested\n> +variable has no value. When multiple variables are requested, an empty\n> +record (a blank line, or an empty NUL-terminated record when `-z` is given)\n> +is printed for any variable that has no value, and the command continues\n> +processing the remaining variables.\n\nVery clearly described.  Although it makes it sound as if the\ncommand always notices a variable without any value and reports\nfailure with its exit value, no matter in what mode, but I do not\nthink that matches what the code does (below).\n\n>  int cmd_var(int argc,\n> ...\n> +\tterm = nul_term ? '\\0' : '\\n';\n> +\n> +\tfor (i = 0; i < argc; i++) {\n> +\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n>  \n> +\t\tif (!git_var)\n> +\t\t\tusage_with_options(var_usage, options);\n>  \n> +\t\tif (git_var->read) {\n> +\t\t\tchar *val = git_var->read(IDENT_STRICT);\n> +\n> +\t\t\tif (!val) {\n> +\t\t\t\tif (argc == 1)\n> +\t\t\t\t\treturn 1;\n> +\t\t\t\tputc(term, stdout);\n> +\t\t\t\tcontinue;\n> +\t\t\t}\n> +\t\t\tprintf(\"%s%c\", val, term);\n> +\t\t\tfree(val);\n> +\t\t} else {\n> +\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n> +\t\t\tsize_t j;\n> +\n> +\t\t\tgit_var->multiread(&list);\n> +\t\t\tif (argc == 1 && !list.nr) {\n> +\t\t\t\tstring_list_clear(&list, 0);\n> +\t\t\t\treturn 1;\n> +\t\t\t}\n> +\t\t\tfor (j = 0; j < list.nr; j++)\n> +\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n> +\t\t\tif (argc > 1)\n> +\t\t\t\tputc(term, stdout);\n> +\t\t\tstring_list_clear(&list, 0);\n> +\t\t}\n> +\t}\n>  \n>  \treturn 0;\n>  }\n\nWhen we ask for a single variable, 'argc' is 1 (and we never update\n'argc' in the loop, which is good), and we return 1 upon seeing a\nmissing value.  We also do the same when we receive a 0-element list\nback for a multi-valued variable.  Otherwise, nobody in the loop\nremembers that we had any such failure; the loop continues, and we\nreturn 0 unconditionally.  A \"missing value\" anomaly noticed during\nthe loop gets forgotten.\n\nEither the documentation or the code needs to be updated, I\nthink.\n\nI am still not convinced this output format is easy for scripts to\nhandle when multi-valued variables are involved.  It is also a bit\nunclear what exactly \"variable has no value\" means.  A variable\nwhose value is an empty string is not such a variable, right?  If a\nmulti-valued variable has an empty string and the string \"hello\" as\nits value, would the output from the command confuse the reading\nscript into thinking that the first blank line signals that the\nvariable has no value, for example?  Having to know which variables\nare multi-valued and which are not before parsing the output format\ndoes not help, either.\n\nWe could, of course, disambiguate by prefixing these lines with\nvariable names followed by '=' (or NUL), which would likely\neliminate the ambiguity.  But I understand that you are trying to\nallow the parsers to proceed without having to strip prefixes from\neach input, which is why the format tries to rely solely on the\ncorrespondence between command-line arguments and output lines.  I,\nhowever, doubt you succeeded in doing so without making the output\nambiguous.\n\nThanks.\n"},{"id":"552732","messageId":"pull.2388.v8.git.git.1789426226860.gitgitgadget@gmail.com","threadId":"66219","inReplyTo":"pull.2388.git.git.1787690802942.gitgitgadget@gmail.com","subject":"[PATCH v8] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Andrew Pleeter via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-14T22:50:26Z","receivedAt":"2026-09-14T22:50:29Z","isPatch":true,"body":"From: Andrew Pleeter <andrewpleeter@gmail.com>\n\nWhile 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\nextracting individual components (name, email, or date) currently\nrequires callers to manually parse the composite string. Furthermore,\nthere is no way to query the resolved commit signing key through\n'git var', and the command only accepts a single variable at a time.\n\nTeach 'git var' to expose individual identity components and commit\nsigning configuration, and allow querying multiple variables with\noptional NUL-termination:\n\n- Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n- Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n- Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n  the resulting commit if you were to run 'git commit' right now.\n- Allow passing multiple variable arguments (e.g., 'git var\n  GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL').\n- When a single variable is requested, print its bare value for backward\n  compatibility.\n- When multiple variables are requested, model the output after\n  'git var -l' by printing 'VARIABLE=value' pairs (or 'VARIABLE\\nvalue\\0'\n  when '-z' is given).\n- Format multi-valued variables in multi-variable mode as repeated\n  'VARIABLE=value' entries (or 'VARIABLE\\nvalue\\0' with '-z'),\n  eliminating stream ambiguity without extra trailing delimiters.\n- When querying multiple variables, omit any variable that has no value,\n  continue processing remaining variables, and exit with code 1.\n- Support '-z' to terminate variable outputs with NUL bytes.\n- Format 'git var -l -z' using the same convention as 'git config\n  list -z' (newline separating key and value, NUL separating entries).\n- Use parse_options() to strictly require options before arguments.\n- Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n    var: support broken-down idents, signing key, multiple args, and -z\n    \n    Teach git var to expose individual identity components and commit\n    signing configuration, and allow querying multiple variables with\n    optional NUL-termination.\n    \n    \n    Changes since v7:\n    =================\n    \n     * Modelled multi-variable querying after git var -l, printing\n       VARIABLE=value pairs (or VARIABLE\\nvalue\\0 when -z is given) instead\n       of bare values, per feedback from Junio C. Hamano.\n     * Formatted multi-valued variables in multi-variable mode as repeated\n       VARIABLE=value entries (or VARIABLE\\nvalue\\0 with -z), eliminating\n       stream ambiguity and the need for extra trailing delimiters.\n     * When querying multiple variables, unset variables are omitted from\n       output and the command terminates with exit code 1.\n     * Updated Documentation/git-var.adoc and t/t0007-git-var.sh\n       accordingly.\n    \n    \n    Changes since v6:\n    =================\n    \n     * Grouped GIT_AUTHOR_* and GIT_COMMITTER_* entries together into\n       concise definitions in Documentation/git-var.adoc to avoid repetitive\n       descriptions, per feedback from Junio C. Hamano.\n     * Added explicit BUG() checks in ident_part() for NULL name/email\n       pointers and date/timezone to protect against unforeseen changes in\n       split_ident_line().\n     * Changed struct git_var member multiread to return void (void\n       (*multiread)(struct string_list *)) since list->nr communicates\n       length.\n     * Simplified list_vars() to use an unconditional else block for\n       ptr->multiread instead of redundant else if.\n     * In cmd_var(), do not set a non-zero exit status for missing variables\n       in multi-variable queries; callers can see the empty record (e.g.\n       GIT_CONFIG_SYSTEM under GIT_CONFIG_NOSYSTEM), matching Phillip Wood's\n       recommendation.\n     * Used putc(term, stdout) in cmd_var() when emitting delimiters.\n     * In t/t0007-git-var.sh, converted -z tests to pipe output through\n       nul_to_q so test_cmp produces clean diffs rather than binary\n       comparison errors.\n    \n    \n    Changes since v5:\n    =================\n    \n     * Reverted cosmetic refactoring in get_git_var() to keep the diff\n       minimal and focused on adding ptr->multiread.\n     * Added BUG(\"unknown ident_part %d\", part) to default case in\n       ident_part() and removed redundant NULL checks on split.name_begin\n       and split.mail_begin.\n     * Adopted first-class int (*multiread)(struct string_list *) callback\n       in struct git_var for multi-valued variables like GIT_CONFIG_GLOBAL,\n       cleanly populating a struct string_list instead of relying on\n       embedded NUL buffers.\n     * Improved GIT_AUTHOR_* and GIT_COMMITTER_* documentation in\n       Documentation/git-var.adoc to describe the values that would be used\n       if you were to run git commit right now.\n     * Explicitly documented that single-variable queries and git var -l do\n       not print an extra delimiter after multi-valued variables.\n    \n    \n    Changes since v4:\n    =================\n    \n     * Simplified git_signing_key() to directly call get_signing_key() as\n       used throughout Git (in tag, send-pack, and sign_buffer()).\n     * Renamed null_term to nul_term across builtin/var.c, and simplified\n       show_config() callback handling.\n     * Replaced the redundant pre-validation loop in cmd_var() by validating\n       arguments directly in the main execution loop.\n     * When querying multiple variables, print an empty record (blank line\n       or \\0 with -z) for any variable that has no value, and continue\n       printing remaining variables instead of terminating prematurely.\n     * Switched multi-valued variable storage (git_config_val_global()) to\n       internal \\0 delimiters, iterating directly through string sequences\n       without allocating a temporary string_list.\n     * For multi-variable queries, output an extra delimiter (\\n or \\0)\n       after multi-valued variables to clearly mark the end of their list.\n     * Explicitly documented the git var -l -z format and multi-variable\n       handling in Documentation/git-var.adoc.\n     * Added comprehensive tests for unset variables and multi-valued stream\n       delimiters in t/t0007-git-var.sh.\n    \n    \n    Changes since v3:\n    =================\n    \n     * Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip\n       Wood and Junio C Hamano; dropped the alias mechanism and\n       commit.gpgsign check.\n     * Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in\n       builtin/var.c, strictly enforcing that options precede variable\n       arguments.\n     * Adopted git config list -z format (key\\nvalue\\0) for git var -l -z to\n       prevent ambiguity with = in config keys.\n     * Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with\n       NUL bytes under -z.\n     * Replaced char part in ident_part() with enum ident_part.\n     * Split synopsis in Documentation/git-var.adoc into separate lines for\n       -l and <variable>..., and removed awkward legacy phrasing (\"of a\n       piece of code\").\n     * Added tests in t/t0007-git-var.sh covering the new -z format,\n       multi-valued -z, and argument ordering.\n    \n    \n    Changes since v2:\n    =================\n    \n     * Drop git ident / git whoami subcommand entirely.\n     * Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n     * Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n     * Add GIT_SIGNING_KEY to resolve commit signing keys.\n     * Teach git var to accept multiple variable arguments (git var <var1>\n       <var2> ...).\n     * Add -z option to terminate outputs with NUL bytes (including git var\n       -l -z).\n     * Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2388%2Fanpl1623%2Fmaster-v8\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2388/anpl1623/master-v8\nPull-Request: https://github.com/git/git/pull/2388\n\nRange-diff vs v7:\n\n 1:  87f5f459ed ! 1:  b9c14a51f5 var: support broken-down idents, signing key, multiple args, and -z\n     @@ Commit message\n          - Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n            the resulting commit if you were to run 'git commit' right now.\n          - Allow passing multiple variable arguments (e.g., 'git var\n     -      GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL') to output each variable\n     -      sequentially.\n     +      GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL').\n     +    - When a single variable is requested, print its bare value for backward\n     +      compatibility.\n     +    - When multiple variables are requested, model the output after\n     +      'git var -l' by printing 'VARIABLE=value' pairs (or 'VARIABLE\\nvalue\\0'\n     +      when '-z' is given).\n     +    - Format multi-valued variables in multi-variable mode as repeated\n     +      'VARIABLE=value' entries (or 'VARIABLE\\nvalue\\0' with '-z'),\n     +      eliminating stream ambiguity without extra trailing delimiters.\n     +    - When querying multiple variables, omit any variable that has no value,\n     +      continue processing remaining variables, and exit with code 1.\n          - Support '-z' to terminate variable outputs with NUL bytes.\n          - Format 'git var -l -z' using the same convention as 'git config\n            list -z' (newline separating key and value, NUL separating entries).\n     -    - Delimit values of multi-valued variables with NUL when '-z' is given,\n     -      and output an extra delimiter after multi-valued variables when\n     -      querying multiple variables to disambiguate the stream.\n     -    - When querying multiple variables, print an empty record for any\n     -      variable that has no value and continue processing remaining variables.\n          - Use parse_options() to strictly require options before arguments.\n          - Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n      \n     @@ Documentation/git-var.adoc: git-var - Show a Git logical variable\n       -----------\n      -Prints a Git logical variable. Exits with code 1 if the variable has\n      -no value.\n     -+Prints Git logical variables. Exits with code 1 if any requested\n     -+variable has no value. When multiple variables are requested, an empty\n     -+record (a blank line, or an empty NUL-terminated record when `-z` is given)\n     -+is printed for any variable that has no value, and the command continues\n     -+processing the remaining variables.\n     ++Prints Git logical variables. When a single variable is requested, its\n     ++bare value is printed. When multiple variables are requested, they are\n     ++printed as `VARIABLE=value` pairs, separated by newlines.\n     ++\n     ++If `-z` is given, the output format changes depending on the mode:\n     ++\n     ++* With a single variable, the bare value is terminated by a NUL byte.\n     ++* With multiple variables or with `-l`, the variable name and value are\n     ++  separated by a newline, and each entry is terminated by a NUL byte\n     ++  (`VARIABLE\\nvalue\\0`).\n     ++\n     ++If any requested variable has no value, nothing is printed for that\n     ++variable, processing continues for the remaining variables, and the\n     ++command exits with code 1.\n       \n       OPTIONS\n       -------\n     @@ Documentation/git-var.adoc: OPTIONS\n       \n      +`-z`::\n      +\tTerminate entries with NUL instead of newline. When used with\n     -+\t`-l`, the variable name and its value are separated by a newline,\n     -+\tand each entry is terminated with a NUL byte.\n     ++\t`-l` or when multiple variables are requested, the variable name\n     ++\tand its value are separated by a newline, and each entry is\n     ++\tterminated with a NUL byte.\n      +\n       EXAMPLES\n       --------\n     @@ Documentation/git-var.adoc: OPTIONS\n      ++\n      +------------\n      +$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL\n     -+Eric W. Biederman\n     -+ebiederm@lnxi.com\n     ++GIT_AUTHOR_NAME=Eric W. Biederman\n     ++GIT_AUTHOR_EMAIL=ebiederm@lnxi.com\n      +------------\n       \n       VARIABLES\n     @@ Documentation/git-var.adoc: endif::git-default-pager[]\n      -multiple items.\n      +values, which are separated by newlines (or NUL bytes if `-z` is given),\n      +and are listed in order from highest to lowest priority. When querying\n     -+multiple variables, an extra newline (or an extra NUL byte if `-z` is\n     -+given) is printed after the values of a multi-valued variable to mark the\n     -+end of its list. (Single-variable queries and `git var -l` do not print\n     -+an extra delimiter). Callers should be prepared for any such path value to\n     -+contain multiple items.\n     ++multiple variables (or using `-l`), each value is output as a separate\n     ++`VARIABLE=value` entry (or `VARIABLE\\nvalue\\0` with `-z`). Callers should\n     ++be prepared for any such path value to contain multiple items.\n       \n       Note that paths are printed even if they do not exist, but not if they are\n       disabled by other environment variables.\n     @@ builtin/var.c: static struct git_var git_vars[] = {\n       {\n       \tstruct git_var *ptr;\n      -\tchar *val;\n     -+\tchar delim = nul_term ? '\\n' : '=';\n     -+\tchar term = nul_term ? '\\0' : '\\n';\n     - \n     +-\n      -\tfor (ptr = git_vars; ptr->read; ptr++)\n      -\t\tif ((val = ptr->read(0))) {\n      -\t\t\tif (ptr->multivalued && *val) {\n      -\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n     -+\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n     -+\t\tif (ptr->read) {\n     -+\t\t\tchar *val = ptr->read(0);\n     - \n     +-\n      -\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n      -\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n      -\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n      -\t\t\t\tstring_list_clear(&list, 0);\n      -\t\t\t} else {\n      -\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n     ++\tchar delim = nul_term ? '\\n' : '=';\n     ++\tchar term = nul_term ? '\\0' : '\\n';\n     ++\n     ++\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n     ++\t\tif (ptr->read) {\n     ++\t\t\tchar *val = ptr->read(0);\n     ++\n      +\t\t\tif (val) {\n      +\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n      +\t\t\t\tfree(val);\n     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n      -\tchar *val;\n      +\tint list = 0;\n      +\tint nul_term = 0;\n     ++\tint ret = 0;\n      +\tint i;\n     ++\tchar delim;\n      +\tchar term;\n      +\tstruct option options[] = {\n      +\t\tOPT_BOOL('l', NULL, &list,\n     @@ builtin/var.c: static const struct git_var *get_git_var(const char *var)\n      -\tgit_var = get_git_var(argv[1]);\n      -\tif (!git_var)\n      -\t\tusage(var_usage);\n     ++\tdelim = nul_term ? '\\n' : '=';\n      +\tterm = nul_term ? '\\0' : '\\n';\n     -+\n     -+\tfor (i = 0; i < argc; i++) {\n     -+\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n       \n      -\tval = git_var->read(IDENT_STRICT);\n      -\tif (!val)\n      -\t\treturn 1;\n     -+\t\tif (!git_var)\n     -+\t\t\tusage_with_options(var_usage, options);\n     ++\tfor (i = 0; i < argc; i++) {\n     ++\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n       \n      -\tprintf(\"%s\\n\", val);\n      -\tfree(val);\n     ++\t\tif (!git_var)\n     ++\t\t\tusage_with_options(var_usage, options);\n     ++\n      +\t\tif (git_var->read) {\n      +\t\t\tchar *val = git_var->read(IDENT_STRICT);\n      +\n      +\t\t\tif (!val) {\n     -+\t\t\t\tif (argc == 1)\n     -+\t\t\t\t\treturn 1;\n     -+\t\t\t\tputc(term, stdout);\n     ++\t\t\t\tret = 1;\n      +\t\t\t\tcontinue;\n      +\t\t\t}\n     -+\t\t\tprintf(\"%s%c\", val, term);\n     ++\t\t\tif (argc == 1)\n     ++\t\t\t\tprintf(\"%s%c\", val, term);\n     ++\t\t\telse\n     ++\t\t\t\tprintf(\"%s%c%s%c\", git_var->name, delim, val, term);\n      +\t\t\tfree(val);\n      +\t\t} else {\n      +\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n      +\t\t\tsize_t j;\n      +\n      +\t\t\tgit_var->multiread(&list);\n     -+\t\t\tif (argc == 1 && !list.nr) {\n     -+\t\t\t\tstring_list_clear(&list, 0);\n     -+\t\t\t\treturn 1;\n     ++\t\t\tif (!list.nr)\n     ++\t\t\t\tret = 1;\n     ++\t\t\tfor (j = 0; j < list.nr; j++) {\n     ++\t\t\t\tif (argc == 1)\n     ++\t\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n     ++\t\t\t\telse\n     ++\t\t\t\t\tprintf(\"%s%c%s%c\", git_var->name, delim,\n     ++\t\t\t\t\t       list.items[j].string, term);\n      +\t\t\t}\n     -+\t\t\tfor (j = 0; j < list.nr; j++)\n     -+\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n     -+\t\t\tif (argc > 1)\n     -+\t\t\t\tputc(term, stdout);\n      +\t\t\tstring_list_clear(&list, 0);\n      +\t\t}\n      +\t}\n       \n     - \treturn 0;\n     +-\treturn 0;\n     ++\treturn ret;\n       }\n      \n       ## t/t0007-git-var.sh ##\n     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '\n      +test_expect_success 'get multiple variables' '\n      +\ttest_tick &&\n      +\tcat >expect <<-EOF &&\n     -+\t$GIT_AUTHOR_NAME\n     -+\t$GIT_AUTHOR_EMAIL\n     -+\t$GIT_COMMITTER_NAME\n     -+\t$GIT_COMMITTER_EMAIL\n     ++\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n     ++\tGIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL\n     ++\tGIT_COMMITTER_NAME=$GIT_COMMITTER_NAME\n     ++\tGIT_COMMITTER_EMAIL=$GIT_COMMITTER_EMAIL\n      +\tEOF\n      +\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n      +\ttest_cmp expect actual\n     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '\n      +\n      +test_expect_success 'get multiple variables with -z' '\n      +\ttest_tick &&\n     -+\tprintf \"%sQ%sQ\" \"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n     ++\tprintf \"GIT_AUTHOR_NAME\\n%sQGIT_AUTHOR_EMAIL\\n%sQ\" \\\n     ++\t\t\"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n      +\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual.raw &&\n      +\tnul_to_q <actual.raw >actual &&\n      +\ttest_cmp expect actual\n     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '\n      +\ttest_must_fail git var GIT_AUTHOR_NAME -z\n      +'\n      +\n     -+test_expect_success 'get multiple variables with unset variable outputs blank record' '\n     ++test_expect_success 'get multiple variables with unset variable exits with 1 and omits unset' '\n      +\ttest_config user.signingkey \"\" &&\n      +\tcat >expect <<-EOF &&\n     -+\t$GIT_AUTHOR_NAME\n     -+\n     -+\t$GIT_COMMITTER_NAME\n     ++\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n     ++\tGIT_COMMITTER_NAME=$GIT_COMMITTER_NAME\n      +\tEOF\n     -+\tgit var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n     ++\ttest_expect_code 1 git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n      +test_expect_success 'get multiple variables with -z and unset variable' '\n      +\ttest_config user.signingkey \"\" &&\n     -+\tprintf \"%sQ%sQ\" \"$GIT_AUTHOR_NAME\" \"Q$GIT_COMMITTER_NAME\" >expect &&\n     -+\tgit var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&\n     ++\tprintf \"GIT_AUTHOR_NAME\\n%sQGIT_COMMITTER_NAME\\n%sQ\" \\\n     ++\t\t\"$GIT_AUTHOR_NAME\" \"$GIT_COMMITTER_NAME\" >expect &&\n     ++\ttest_expect_code 1 git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&\n      +\tnul_to_q <actual.raw >actual &&\n      +\ttest_cmp expect actual\n      +'\n      +\n     ++test_expect_success 'get multiple variables including multi-valued variable' '\n     ++\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n     ++\tcat >expect <<-EOF &&\n     ++\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n     ++\tGIT_CONFIG_GLOBAL=$TRASHDIR/foo/git/config\n     ++\tGIT_CONFIG_GLOBAL=$TRASHDIR/.gitconfig\n     ++\tGIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL\n     ++\tEOF\n     ++\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n     ++\t\tgit var GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n     ++\n      +test_expect_success 'get multiple variables including multi-valued variable with -z' '\n      +\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n     -+\tprintf \"%sQ%sQ%sQQ%sQ\" \"$GIT_AUTHOR_NAME\" \\\n     ++\tprintf \"GIT_AUTHOR_NAME\\n%sQGIT_CONFIG_GLOBAL\\n%sQGIT_CONFIG_GLOBAL\\n%sQGIT_AUTHOR_EMAIL\\n%sQ\" \\\n     ++\t\t\"$GIT_AUTHOR_NAME\" \\\n      +\t\t\"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" \\\n      +\t\t\"$GIT_AUTHOR_EMAIL\" >expect &&\n      +\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n     @@ t/t0007-git-var.sh: test_expect_success '`git var -l` works even without HOME' '\n      +\tnul_to_q <actual.raw >actual &&\n      +\ttest_cmp expect actual\n      +'\n     ++\n     ++test_expect_success 'get multiple variables with unset multi-valued variable' '\n     ++\tcat >expect <<-EOF &&\n     ++\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n     ++\tGIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL\n     ++\tEOF\n     ++\ttest_env GIT_CONFIG_GLOBAL= test_expect_code 1 git var GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&\n     ++\ttest_cmp expect actual\n     ++'\n      +\n       test_done\n\n\n Documentation/git-var.adoc |  74 +++++++++--\n builtin/var.c              | 263 +++++++++++++++++++++++++++++--------\n t/t0007-git-var.sh         | 151 +++++++++++++++++++++\n 3 files changed, 424 insertions(+), 64 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex 697c10aded..162d6d1722 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -9,12 +9,25 @@ git-var - Show a Git logical variable\n SYNOPSIS\n --------\n [synopsis]\n-git var (-l | <variable>)\n+git var [-z] -l\n+git var [-z] <variable>...\n \n DESCRIPTION\n -----------\n-Prints a Git logical variable. Exits with code 1 if the variable has\n-no value.\n+Prints Git logical variables. When a single variable is requested, its\n+bare value is printed. When multiple variables are requested, they are\n+printed as `VARIABLE=value` pairs, separated by newlines.\n+\n+If `-z` is given, the output format changes depending on the mode:\n+\n+* With a single variable, the bare value is terminated by a NUL byte.\n+* With multiple variables or with `-l`, the variable name and value are\n+  separated by a newline, and each entry is terminated by a NUL byte\n+  (`VARIABLE\\nvalue\\0`).\n+\n+If any requested variable has no value, nothing is printed for that\n+variable, processing continues for the remaining variables, and the\n+command exits with code 1.\n \n OPTIONS\n -------\n@@ -24,19 +37,56 @@ OPTIONS\n \tas well. (However, the configuration variables listing functionality\n \tis deprecated in favor of `git config list`.)\n \n+`-z`::\n+\tTerminate entries with NUL instead of newline. When used with\n+\t`-l` or when multiple variables are requested, the variable name\n+\tand its value are separated by a newline, and each entry is\n+\tterminated with a NUL byte.\n+\n EXAMPLES\n --------\n-\t$ git var GIT_AUTHOR_IDENT\n-\tEric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n-\n+* Get the author identity:\n++\n+------------\n+$ git var GIT_AUTHOR_IDENT\n+Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n+------------\n+\n+* Get the author name and email:\n++\n+------------\n+$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL\n+GIT_AUTHOR_NAME=Eric W. Biederman\n+GIT_AUTHOR_EMAIL=ebiederm@lnxi.com\n+------------\n \n VARIABLES\n ---------\n `GIT_AUTHOR_IDENT`::\n-    The author of a piece of code.\n+`GIT_AUTHOR_NAME`::\n+`GIT_AUTHOR_EMAIL`::\n+`GIT_AUTHOR_DATE`::\n+    The authorship information that would be recorded in the\n+    resulting commit object if you ran `git commit` right now.\n+    `GIT_AUTHOR_IDENT` consists of the author's name, e-mail\n+    address, and timestamp+timezone. These three pieces of\n+    information are available separately as `GIT_AUTHOR_NAME`,\n+    `GIT_AUTHOR_EMAIL`, and `GIT_AUTHOR_DATE`.\n \n `GIT_COMMITTER_IDENT`::\n-    The person who put a piece of code into Git.\n+`GIT_COMMITTER_NAME`::\n+`GIT_COMMITTER_EMAIL`::\n+`GIT_COMMITTER_DATE`::\n+    The committer information that would be recorded in the\n+    resulting commit object if you ran `git commit` right now.\n+    `GIT_COMMITTER_IDENT` consists of the committer's name, e-mail\n+    address, and timestamp+timezone. These three pieces of\n+    information are available separately as `GIT_COMMITTER_NAME`,\n+    `GIT_COMMITTER_EMAIL`, and `GIT_COMMITTER_DATE`.\n+\n+`GIT_SIGNING_KEY`::\n+    The key that would be used to sign the resulting commit if you were\n+    to run `git commit` right now.\n \n `GIT_EDITOR`::\n     Text editor for use by Git commands.  The value is meant to be\n@@ -85,9 +135,11 @@ endif::git-default-pager[]\n     The path to the global (per-user) configuration files, if any.\n \n Most path values contain only one value. However, some can contain multiple\n-values, which are separated by newlines, and are listed in order from highest to\n-lowest priority.  Callers should be prepared for any such path value to contain\n-multiple items.\n+values, which are separated by newlines (or NUL bytes if `-z` is given),\n+and are listed in order from highest to lowest priority. When querying\n+multiple variables (or using `-l`), each value is output as a separate\n+`VARIABLE=value` entry (or `VARIABLE\\nvalue\\0` with `-z`). Callers should\n+be prepared for any such path value to contain multiple items.\n \n Note that paths are printed even if they do not exist, but not if they are\n disabled by other environment variables.\ndiff --git a/builtin/var.c b/builtin/var.c\nindex cc3a43cde2..3b651a3eb6 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -12,25 +12,109 @@\n #include \"config.h\"\n #include \"editor.h\"\n #include \"environment.h\"\n+#include \"gpg-interface.h\"\n #include \"ident.h\"\n #include \"pager.h\"\n-#include \"refs.h\"\n+#include \"parse-options.h\"\n #include \"path.h\"\n-#include \"strbuf.h\"\n+#include \"refs.h\"\n #include \"run-command.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n \n-static const char var_usage[] = \"git var (-l | <variable>)\";\n+static const char * const var_usage[] = {\n+\tN_(\"git var [-z] -l\"),\n+\tN_(\"git var [-z] <variable>...\"),\n+\tNULL\n+};\n+\n+enum ident_part {\n+\tIDENT_NAME,\n+\tIDENT_MAIL,\n+\tIDENT_DATE,\n+};\n \n static char *committer(int ident_flag)\n {\n \treturn xstrdup_or_null(git_committer_info(ident_flag));\n }\n \n+static char *ident_part(const char *ident, enum ident_part part)\n+{\n+\tstruct ident_split split;\n+\n+\tif (!ident)\n+\t\treturn NULL;\n+\tif (split_ident_line(&split, ident, strlen(ident)))\n+\t\treturn NULL;\n+\n+\tswitch (part) {\n+\tcase IDENT_NAME:\n+\t\tif (!split.name_begin || !split.name_end)\n+\t\t\tBUG(\"split_ident_line() gave NULL names???\");\n+\t\treturn xmemdupz(split.name_begin,\n+\t\t\t\tsplit.name_end - split.name_begin);\n+\tcase IDENT_MAIL:\n+\t\tif (!split.mail_begin || !split.mail_end)\n+\t\t\tBUG(\"split_ident_line() gave NULL mail???\");\n+\t\treturn xmemdupz(split.mail_begin,\n+\t\t\t\tsplit.mail_end - split.mail_begin);\n+\tcase IDENT_DATE:\n+\t\tif (!split.date_begin || !split.tz_end)\n+\t\t\tBUG(\"split_ident_line() gave NULL date/tz???\");\n+\t\treturn xmemdupz(split.date_begin,\n+\t\t\t\tsplit.tz_end - split.date_begin);\n+\tdefault:\n+\t\tBUG(\"unknown ident_part %d\", part);\n+\t}\n+}\n+\n+static char *committer_name(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *committer_email(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *committer_date(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_DATE);\n+}\n+\n static char *author(int ident_flag)\n {\n \treturn xstrdup_or_null(git_author_info(ident_flag));\n }\n \n+static char *author_name(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *author_email(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *author_date(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_DATE);\n+}\n+\n+static char *git_signing_key(int ident_flag UNUSED)\n+{\n+\tchar *signing_key = get_signing_key();\n+\n+\tif (signing_key && !*signing_key) {\n+\t\tfree(signing_key);\n+\t\treturn NULL;\n+\t}\n+\treturn signing_key;\n+}\n+\n static char *editor(int ident_flag UNUSED)\n {\n \treturn xstrdup_or_null(git_editor());\n@@ -90,45 +174,61 @@ static char *git_config_val_system(int ident_flag UNUSED)\n \treturn NULL;\n }\n \n-static char *git_config_val_global(int ident_flag UNUSED)\n+static void git_config_val_global(struct string_list *list)\n {\n-\tstruct strbuf buf = STRBUF_INIT;\n \tchar *user, *xdg;\n-\tsize_t unused;\n \n \tgit_global_config_paths(&user, &xdg);\n \tif (xdg && *xdg) {\n \t\tnormalize_path_copy(xdg, xdg);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", xdg);\n+\t\tstring_list_append(list, xdg);\n \t}\n \tif (user && *user) {\n \t\tnormalize_path_copy(user, user);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", user);\n+\t\tstring_list_append(list, user);\n \t}\n \tfree(xdg);\n \tfree(user);\n-\tstrbuf_trim_trailing_newline(&buf);\n-\tif (buf.len == 0) {\n-\t\tstrbuf_release(&buf);\n-\t\treturn NULL;\n-\t}\n-\treturn strbuf_detach(&buf, &unused);\n }\n \n struct git_var {\n \tconst char *name;\n \tchar *(*read)(int);\n-\tint multivalued;\n+\tvoid (*multiread)(struct string_list *);\n };\n static struct git_var git_vars[] = {\n \t{\n \t\t.name = \"GIT_COMMITTER_IDENT\",\n \t\t.read = committer,\n \t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_NAME\",\n+\t\t.read = committer_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_EMAIL\",\n+\t\t.read = committer_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_DATE\",\n+\t\t.read = committer_date,\n+\t},\n \t{\n \t\t.name = \"GIT_AUTHOR_IDENT\",\n \t\t.read = author,\n \t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_NAME\",\n+\t\t.read = author_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_EMAIL\",\n+\t\t.read = author_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_DATE\",\n+\t\t.read = author_date,\n+\t},\n \t{\n \t\t.name = \"GIT_EDITOR\",\n \t\t.read = editor,\n@@ -145,6 +245,10 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_DEFAULT_BRANCH\",\n \t\t.read = default_branch,\n \t},\n+\t{\n+\t\t.name = \"GIT_SIGNING_KEY\",\n+\t\t.read = git_signing_key,\n+\t},\n \t{\n \t\t.name = \"GIT_SHELL_PATH\",\n \t\t.read = shell_path,\n@@ -163,8 +267,7 @@ static struct git_var git_vars[] = {\n \t},\n \t{\n \t\t.name = \"GIT_CONFIG_GLOBAL\",\n-\t\t.read = git_config_val_global,\n-\t\t.multivalued = 1,\n+\t\t.multiread = git_config_val_global,\n \t},\n \t{\n \t\t.name = \"\",\n@@ -172,31 +275,37 @@ static struct git_var git_vars[] = {\n \t},\n };\n \n-static void list_vars(void)\n+static void list_vars(int nul_term)\n {\n \tstruct git_var *ptr;\n-\tchar *val;\n-\n-\tfor (ptr = git_vars; ptr->read; ptr++)\n-\t\tif ((val = ptr->read(0))) {\n-\t\t\tif (ptr->multivalued && *val) {\n-\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n-\n-\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n-\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n-\t\t\t\tstring_list_clear(&list, 0);\n-\t\t\t} else {\n-\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n+\tchar delim = nul_term ? '\\n' : '=';\n+\tchar term = nul_term ? '\\0' : '\\n';\n+\n+\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n+\t\tif (ptr->read) {\n+\t\t\tchar *val = ptr->read(0);\n+\n+\t\t\tif (val) {\n+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n+\t\t\t\tfree(val);\n \t\t\t}\n-\t\t\tfree(val);\n+\t\t} else {\n+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\t\t\tsize_t i;\n+\n+\t\t\tptr->multiread(&list);\n+\t\t\tfor (i = 0; i < list.nr; i++)\n+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n+\t\t\t\t       list.items[i].string, term);\n+\t\t\tstring_list_clear(&list, 0);\n \t\t}\n+\t}\n }\n \n static const struct git_var *get_git_var(const char *var)\n {\n \tstruct git_var *ptr;\n-\tfor (ptr = git_vars; ptr->read; ptr++) {\n+\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n \t\tif (strcmp(var, ptr->name) == 0) {\n \t\t\treturn ptr;\n \t\t}\n@@ -207,42 +316,90 @@ static const struct git_var *get_git_var(const char *var)\n static int show_config(const char *var, const char *value,\n \t\t       const struct config_context *ctx, void *cb)\n {\n+\tint *nul_term = cb;\n+\tchar delim = *nul_term ? '\\n' : '=';\n+\tchar term = *nul_term ? '\\0' : '\\n';\n+\n \tif (value)\n-\t\tprintf(\"%s=%s\\n\", var, value);\n+\t\tprintf(\"%s%c%s%c\", var, delim, value, term);\n \telse\n-\t\tprintf(\"%s\\n\", var);\n+\t\tprintf(\"%s%c\", var, term);\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n int cmd_var(int argc,\n \t    const char **argv,\n-\t    const char *prefix UNUSED,\n+\t    const char *prefix,\n \t    struct repository *repo UNUSED)\n {\n-\tconst struct git_var *git_var;\n-\tchar *val;\n+\tint list = 0;\n+\tint nul_term = 0;\n+\tint ret = 0;\n+\tint i;\n+\tchar delim;\n+\tchar term;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL('l', NULL, &list,\n+\t\t\t N_(\"list all variables\")),\n+\t\tOPT_BOOL('z', NULL, &nul_term,\n+\t\t\t N_(\"terminate entries with NUL\")),\n+\t\tOPT_END(),\n+\t};\n \n-\tshow_usage_if_asked(argc, argv, var_usage);\n-\tif (argc != 2)\n-\t\tusage(var_usage);\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tif (strcmp(argv[1], \"-l\") == 0) {\n-\t\trepo_config(the_repository, show_config, NULL);\n-\t\tlist_vars();\n+\tif (list) {\n+\t\tif (argc)\n+\t\t\tusage_with_options(var_usage, options);\n+\t\trepo_config(the_repository, show_config, &nul_term);\n+\t\tlist_vars(nul_term);\n \t\treturn 0;\n \t}\n+\n+\tif (!argc)\n+\t\tusage_with_options(var_usage, options);\n+\n \trepo_config(the_repository, git_default_config, NULL);\n \n-\tgit_var = get_git_var(argv[1]);\n-\tif (!git_var)\n-\t\tusage(var_usage);\n+\tdelim = nul_term ? '\\n' : '=';\n+\tterm = nul_term ? '\\0' : '\\n';\n \n-\tval = git_var->read(IDENT_STRICT);\n-\tif (!val)\n-\t\treturn 1;\n+\tfor (i = 0; i < argc; i++) {\n+\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n \n-\tprintf(\"%s\\n\", val);\n-\tfree(val);\n+\t\tif (!git_var)\n+\t\t\tusage_with_options(var_usage, options);\n+\n+\t\tif (git_var->read) {\n+\t\t\tchar *val = git_var->read(IDENT_STRICT);\n+\n+\t\t\tif (!val) {\n+\t\t\t\tret = 1;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tif (argc == 1)\n+\t\t\t\tprintf(\"%s%c\", val, term);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s%c%s%c\", git_var->name, delim, val, term);\n+\t\t\tfree(val);\n+\t\t} else {\n+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\t\t\tsize_t j;\n+\n+\t\t\tgit_var->multiread(&list);\n+\t\t\tif (!list.nr)\n+\t\t\t\tret = 1;\n+\t\t\tfor (j = 0; j < list.nr; j++) {\n+\t\t\t\tif (argc == 1)\n+\t\t\t\t\tprintf(\"%s%c\", list.items[j].string, term);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%s%c%s%c\", git_var->name, delim,\n+\t\t\t\t\t       list.items[j].string, term);\n+\t\t\t}\n+\t\t\tstring_list_clear(&list, 0);\n+\t\t}\n+\t}\n \n-\treturn 0;\n+\treturn ret;\n }\ndiff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\nindex 2b60317758..da6cbaade9 100755\n--- a/t/t0007-git-var.sh\n+++ b/t/t0007-git-var.sh\n@@ -276,4 +276,155 @@ test_expect_success '`git var -l` works even without HOME' '\n \t)\n '\n \n+test_expect_success 'get author identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n+\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n+\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n+\tgit var GIT_AUTHOR_NAME >actual.name &&\n+\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n+\tgit var GIT_AUTHOR_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get committer identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n+\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n+\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n+\tgit var GIT_COMMITTER_NAME >actual.name &&\n+\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n+\tgit var GIT_COMMITTER_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get multiple variables' '\n+\ttest_tick &&\n+\tcat >expect <<-EOF &&\n+\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n+\tGIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL\n+\tGIT_COMMITTER_NAME=$GIT_COMMITTER_NAME\n+\tGIT_COMMITTER_EMAIL=$GIT_COMMITTER_EMAIL\n+\tEOF\n+\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z' '\n+\ttest_tick &&\n+\tprintf \"GIT_AUTHOR_NAME\\n%sQGIT_AUTHOR_EMAIL\\n%sQ\" \\\n+\t\t\"$GIT_AUTHOR_NAME\" \"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tgit var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL >actual.raw &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual.raw &&\n+\tprintf \"%sQ%sQ\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expect &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git var -l -z' '\n+\tgit var -l -z >actual &&\n+\ttr \"\\0\" \"\\n\" <actual >actual.lines &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect &&\n+\tsed -n \"/^GIT_AUTHOR_NAME$/{n;p;}\" actual.lines >actual.author &&\n+\ttest_cmp expect actual.author &&\n+\techo false >expect &&\n+\tsed -n \"/^core\\.bare$/{n;p;}\" actual.lines >actual.bare &&\n+\ttest_cmp expect actual.bare\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY with user.signingkey configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\techo \"TEST_KEY_ID\" >expect &&\n+\tgit var GIT_SIGNING_KEY >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get GIT_SIGNING_KEY fails when unset' '\n+\ttest_config user.signingkey \"\" &&\n+\ttest_must_fail git var GIT_SIGNING_KEY\n+'\n+\n+test_expect_success 'git var -l lists new variables' '\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n+'\n+\n+test_expect_success 'git var -l lists GIT_SIGNING_KEY when configured' '\n+\ttest_config user.signingkey \"TEST_KEY_ID\" &&\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_SIGNING_KEY=TEST_KEY_ID\" actual\n+'\n+\n+test_expect_success 'options must precede variable arguments' '\n+\ttest_must_fail git var GIT_AUTHOR_NAME -z\n+'\n+\n+test_expect_success 'get multiple variables with unset variable exits with 1 and omits unset' '\n+\ttest_config user.signingkey \"\" &&\n+\tcat >expect <<-EOF &&\n+\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n+\tGIT_COMMITTER_NAME=$GIT_COMMITTER_NAME\n+\tEOF\n+\ttest_expect_code 1 git var GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z and unset variable' '\n+\ttest_config user.signingkey \"\" &&\n+\tprintf \"GIT_AUTHOR_NAME\\n%sQGIT_COMMITTER_NAME\\n%sQ\" \\\n+\t\t\"$GIT_AUTHOR_NAME\" \"$GIT_COMMITTER_NAME\" >expect &&\n+\ttest_expect_code 1 git var -z GIT_AUTHOR_NAME GIT_SIGNING_KEY GIT_COMMITTER_NAME >actual.raw &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables including multi-valued variable' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tcat >expect <<-EOF &&\n+\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n+\tGIT_CONFIG_GLOBAL=$TRASHDIR/foo/git/config\n+\tGIT_CONFIG_GLOBAL=$TRASHDIR/.gitconfig\n+\tGIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL\n+\tEOF\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n+\t\tgit var GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables including multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tprintf \"GIT_AUTHOR_NAME\\n%sQGIT_CONFIG_GLOBAL\\n%sQGIT_CONFIG_GLOBAL\\n%sQGIT_AUTHOR_EMAIL\\n%sQ\" \\\n+\t\t\"$GIT_AUTHOR_NAME\" \\\n+\t\t\"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" \\\n+\t\t\"$GIT_AUTHOR_EMAIL\" >expect &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n+\t\tgit var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual.raw &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with unset multi-valued variable' '\n+\tcat >expect <<-EOF &&\n+\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n+\tGIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL\n+\tEOF\n+\ttest_env GIT_CONFIG_GLOBAL= test_expect_code 1 git var GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: 2c3adbb2c475981e340c79fdc5e7f4f9b5d9054e\n-- \ngitgitgadget\n"},{"id":"552763","messageId":"xmqq33va1lcg.fsf@gitster.g","threadId":"66219","inReplyTo":"pull.2388.v8.git.git.1789426226860.gitgitgadget@gmail.com","subject":"Re: [PATCH v8] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-15T17:21:51Z","receivedAt":"2026-09-15T17:21:54Z","isPatch":true,"body":"\"Andrew Pleeter via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Andrew Pleeter <andrewpleeter@gmail.com>\n>\n> While 'git var' exposes GIT_AUTHOR_IDENT and GIT_COMMITTER_IDENT,\n> extracting individual components (name, email, or date) currently\n> requires callers to manually parse the composite string. Furthermore,\n> there is no way to query the resolved commit signing key through\n> 'git var', and the command only accepts a single variable at a time.\n>\n> Teach 'git var' to expose individual identity components and commit\n> signing configuration, and allow querying multiple variables with\n> optional NUL-termination:\n\nThe huge laundary list (below) strongly tells us that this single\npatch is doing too many things at once and it is better done as a\nmulti-patch series.  Also, some descriptions seem to hint how the\nimplementation evolved during the development of this patch, which\nnobody is interested when they read \"git log\" output (which is the\nultimate target audience we write our commit log messages for).\n\nI would say this should be split into at least 3 patches.\n\n (1) Add \"-z\" output mode.\n\n     To allow reading scripts to unambiguously parse output from\n     \"git var (-l | <var>)\" command, implement a NUL terminated\n     output mode, similar to how \"git config -l -z\" shows list of\n     configuration variables and their values.  When showing the\n     value of a single variable this only makes difference for\n     variable with multiple values, but in the next step in this\n     series, we will introduce a mode where multiple variables are\n     queried.\n\n (2) Add (2 <= argc) mode that displays like \"var -l\" mode on top.\n\n     To allow reading values for multiple variables with a single\n     command invocation, teach \"var\" to take more than one variable,\n     and show output the same way as \"git var -l [-z]\", giving list\n     of \"var=value\" but only for variables requested by the user.\n\n (3) Add new variables.\n\n     Scripts reading from \"git var GIT_AUTHOR_IDENT\" needs to parse\n     the output if they want to extract only the author name.  \n\n     To allow scripts to easily access broken-out fields of\n     GIT_{AUTHOR,COMMITTER}_IDENT, add a GIT_AUTHOR_NAME variable\n     and its friends, as well as GIT_SIGNING_KEY.\n\nAfter you receive a review, you should respond and try to engage in\na dialog with reviewers, before sending a new iteration of a patch.\n\nWhen your new iteration is different from what reviewer suggested,\nwithout such an exchange beforehand, reviewers cannot tell if that\nis merely due to miscommunication, or you had a good reason to do\nthings differently.  Don't make reviewers feel as if they are\ntalking to silent machine that takes an earlier iteration of the\npatch with their input and spits out a new iteration.\n\nThanks.\n\n> - Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.\n> - Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.\n> - Add GIT_SIGNING_KEY to resolve the key that would be used to sign\n>   the resulting commit if you were to run 'git commit' right now.\n> - Allow passing multiple variable arguments (e.g., 'git var\n>   GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL').\n> - When a single variable is requested, print its bare value for backward\n>   compatibility.\n> - When multiple variables are requested, model the output after\n>   'git var -l' by printing 'VARIABLE=value' pairs (or 'VARIABLE\\nvalue\\0'\n>   when '-z' is given).\n> - Format multi-valued variables in multi-variable mode as repeated\n>   'VARIABLE=value' entries (or 'VARIABLE\\nvalue\\0' with '-z'),\n>   eliminating stream ambiguity without extra trailing delimiters.\n> - When querying multiple variables, omit any variable that has no value,\n>   continue processing remaining variables, and exit with code 1.\n> - Support '-z' to terminate variable outputs with NUL bytes.\n> - Format 'git var -l -z' using the same convention as 'git config\n>   list -z' (newline separating key and value, NUL separating entries).\n> - Use parse_options() to strictly require options before arguments.\n> - Update Documentation/git-var.adoc and t/t0007-git-var.sh.\n\n"},{"id":"552771","messageId":"20260915220228.42819-1-andrewpleeter@gmail.com","threadId":"66219","inReplyTo":"xmqq33va1lcg.fsf@gitster.g","subject":"Re: [PATCH v8] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Andrew Pleeter","fromEmail":"andrewpleeter@gmail.com","sentAt":"2026-09-15T22:02:28Z","receivedAt":"2026-09-15T22:02:30Z","isPatch":true,"body":"Hi Junio,\n\nThank you for the guidance, and I sincerely apologize for the lack of\ncommunication. I had posted replies on the GitHub pull request and did\nnot realize until now that GitGitGadget does not mirror PR comments back\nto the mailing list. I certainly did not intend to be silent or ignore\nyour reviews.\n\nI completely agree with your feedback. Packing all of these features into\na single commit makes the patch difficult to review and overcomplicates\nthe history.\n\nI will restructure the series for v9 into three separate patches as you\nsuggested:\n\n1. Add the -z output mode to git var (-l and single-variable).\n2. Teach git var to accept multiple variables (argc >= 2) with var=value\n   output.\n3. Expose the broken-out identity components and GIT_SIGNING_KEY.\n\nI will send v9 shortly. Thank you again for your patience and direction.\n\nBest regards,\nAndrew\n"},{"id":"552996","messageId":"ddfd5232-b832-476c-8614-268f8e4bbe28@gmail.com","threadId":"66219","inReplyTo":"20260915220228.42819-1-andrewpleeter@gmail.com","subject":"Re: [PATCH v8] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-22T14:57:01Z","receivedAt":"2026-09-22T14:57:09Z","isPatch":true,"body":"On 15/09/2026 23:02, Andrew Pleeter wrote:\n> Hi Junio,\n> \n> Thank you for the guidance, and I sincerely apologize for the lack of\n> communication. I had posted replies on the GitHub pull request and did\n> not realize until now that GitGitGadget does not mirror PR comments back\n> to the mailing list.\n\nAh, that explains the silence! I had a quick look at the PR comments but \ndid not notice any response to my question about the use of GIT_SIGNING_KEY.\n> I certainly did not intend to be silent or ignore\n> your reviews.\n> \n> I completely agree with your feedback. Packing all of these features into\n> a single commit makes the patch difficult to review and overcomplicates\n> the history.\n> \n> I will restructure the series for v9 into three separate patches as you\n> suggested:\n> \n> 1. Add the -z output mode to git var (-l and single-variable).\n> 2. Teach git var to accept multiple variables (argc >= 2) with var=value\n>     output.\n> 3. Expose the broken-out identity components and GIT_SIGNING_KEY.\n\nThat sounds like a good direction\n\nThanks\n\nPhillip\n\n> \n> I will send v9 shortly. Thank you again for your patience and direction.\n> \n> Best regards,\n> Andrew\n\n"},{"id":"553358","messageId":"20260926161951.30531-1-andrewpleeter@gmail.com","threadId":"66219","inReplyTo":"ddfd5232-b832-476c-8614-268f8e4bbe28@gmail.com","subject":"Re: [PATCH v8] var: support broken-down idents, signing key, multiple args, and -z","fromName":"Andrew Pleeter","fromEmail":"andrewpleeter@gmail.com","sentAt":"2026-09-26T16:19:51Z","receivedAt":"2026-09-26T16:19:57Z","isPatch":true,"body":"Hi Phillip,\n\nSorry for the silence. I was replying on the GitGitGadget PR and did not\nrealise those comments never reach the list. That is also why v6 switched\nto get_signing_key() without a word from me about your question.\n\nYou were right to keep asking about GIT_SIGNING_KEY. I am dropping it\nfrom the series. get_signing_key() returns a string whose meaning depends\non gpg.format, and the variable gives the caller no way to tell which\nkind it got:\n\n  gpg.format=openpgp, user.signingkey unset\n    $ git var GIT_SIGNING_KEY\n    C O Mitter <committer@example.com>\n\n  gpg.format=openpgp, user.signingkey=DEADBEEF\n    $ git var GIT_SIGNING_KEY\n    DEADBEEF\n\n  gpg.format=ssh, user.signingkey=/path/to/key.pub\n    $ git var GIT_SIGNING_KEY\n    /path/to/key.pub\n\nSo no, the key alone does not tell a caller whether to use gpg or ssh;\nthey would have to read gpg.format as well. And in the default case the\nvalue is a committer ident rather than a key, because that is what\nget_signing_key() hands to gpg to look up. Better to drop it than ship a\nGIT_SIGNING_KEY that sometimes isn't one. I will revisit it when I can\nname a caller it serves.\n\nOn the exit code, your GIT_CONFIG_NOSYSTEM example convinced me. In v9,\nasking for several variables omits the ones with no value and exits 0.\nNon-zero is reserved for real errors such as an unknown variable name, so\ncallers can detect those from the exit code alone. A single variable\nstill exits 1 when it has no value.\n\nThe string_list conversion is split out as you suggested:\n\n  1/4  var: represent multi-valued variables with a string_list\n  2/4  var: add \"-z\" output mode\n  3/4  var: accept more than one variable\n  4/4  var: add broken-out identity variables\n\nparse_options() sits in 2/4, since \"-z\" is what requires it.\nGIT_CONFIG_GLOBAL is now documented as multi-valued, and the tests use\nnul_to_q instead of test_cmp over files containing NUL.\n\nOne place I did not follow you: instead of a trailing delimiter for a\nmulti-valued variable when argc > 1, each value gets its own entry:\n\n  $ git var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL\n  GIT_AUTHOR_IDENT=A U Thor <author@example.com> 1112912053 -0700\n  GIT_CONFIG_GLOBAL=/home/me/.config/git/config\n  GIT_CONFIG_GLOBAL=/home/me/.gitconfig\n\nSame shape as \"git var -l\", so one parser handles both. Happy to switch\nif you prefer the trailing delimiter. I should have raised it last time\ninstead of just changing it.\n\nThanks,\nAndrew\n"},{"id":"553359","messageId":"20260926162048.30853-1-andrewpleeter@gmail.com","threadId":"66219","inReplyTo":"xmqq33va1lcg.fsf@gitster.g","subject":"[PATCH v9 0/4] var: -z output, multiple variables, and broken-out idents","fromName":"Andrew Pleeter","fromEmail":"andrewpleeter@gmail.com","sentAt":"2026-09-26T16:20:44Z","receivedAt":"2026-09-26T16:20:51Z","isPatch":true,"body":"This is a reroll of the single patch in v8, split up as Junio asked\nfor, with the changes Phillip asked for on the list.\n\nv8 was one commit doing four things at once. It is now four patches:\n\n  1/4 converts the internal representation of a multi-valued variable\n      from a newline-joined string to a string_list, with no change in\n      output. Phillip suggested doing this as its own preparatory step.\n\n  2/4 adds \"-z\", which is what forces the switch to parse_options(), so\n      that conversion lives here rather than in 1/4.\n\n  3/4 teaches \"git var\" to take more than one variable.\n\n  4/4 adds GIT_AUTHOR_NAME and friends.\n\nChanges since v8:\n\n * GIT_SIGNING_KEY is dropped. Phillip asked three times how it was\n   meant to be used, and once I looked properly the answer was that the\n   value cannot be interpreted without also reading gpg.format, and that\n   in the default configuration it is a committer ident rather than a\n   key at all. I would rather leave it out than define a variable I\n   cannot describe. Details are in my reply to him on this thread.\n\n * Asking for several variables no longer exits non-zero just because\n   one of them has no value; such a variable is left out of the output\n   and the rest are still shown. A non-zero status is now reserved for\n   real errors, such as naming a variable that does not exist, so\n   callers can detect those from the exit code. A single variable still\n   exits 1 when it has no value, as before. This is Phillip's\n   GIT_CONFIG_NOSYSTEM point.\n\n * GIT_CONFIG_GLOBAL is documented as a variable that can have more than\n   one value.\n\n * The commit messages are prose rather than a list of bullet points,\n   and no longer narrate how the patch was developed.\n\nThe tests use nul_to_q rather than running test_cmp over files\ncontaining NUL bytes, which Phillip pointed out in v6. Each patch builds\nand passes t0007 on its own.\n\nOne thing I did not do, and would like an opinion on: for a multi-valued\nvariable in multi-variable mode, rather than emitting a trailing\ndelimiter, each value is shown as its own \"VARIABLE=value\" entry, which\nmatches \"git var -l\". Phillip suggested the trailing delimiter and I am\nhappy to switch.\n\nAndrew Pleeter (4):\n  var: represent multi-valued variables with a string_list\n  var: add \"-z\" output mode\n  var: accept more than one variable\n  var: add broken-out identity variables\n\n Documentation/git-var.adoc |  68 +++++++++--\n builtin/var.c              | 242 +++++++++++++++++++++++++++++--------\n t/t0007-git-var.sh         | 153 +++++++++++++++++++++++\n 3 files changed, 403 insertions(+), 60 deletions(-)\n\n-- \n2.54.0 (Apple Git-157)\n\n"},{"id":"553360","messageId":"20260926162048.30853-2-andrewpleeter@gmail.com","threadId":"66219","inReplyTo":"xmqq33va1lcg.fsf@gitster.g","subject":"[PATCH v9 1/4] var: represent multi-valued variables with a string_list","fromName":"Andrew Pleeter","fromEmail":"andrewpleeter@gmail.com","sentAt":"2026-09-26T16:20:45Z","receivedAt":"2026-09-26T16:20:52Z","isPatch":true,"body":"A variable whose value is a list, of which GIT_CONFIG_GLOBAL is\ncurrently the only one, is read into a single string whose elements are\njoined with newlines, and \"git var -l\" splits that string apart again\nbefore showing each element.  Round-tripping through a delimiter that\ncan legitimately appear in a value is fragile, and it forces every\nfuture caller to know which variables need splitting.\n\nGive \"struct git_var\" a separate \"multiread\" function that appends the\nelements to a string_list, and let the single \"read\" function stay for\nthe variables that have exactly one value.  A variable uses one or the\nother, never both, so which one is set also says whether the variable\nis multi-valued.\n\nThe output is unchanged.  While at it, say in the documentation that\nGIT_CONFIG_GLOBAL can have multiple values, so that callers know when\nto expect a list.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n Documentation/git-var.adoc |  1 +\n builtin/var.c              | 79 +++++++++++++++++++++-----------------\n 2 files changed, 44 insertions(+), 36 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex 697c10aded..de3007732d 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -83,6 +83,7 @@ endif::git-default-pager[]\n \n `GIT_CONFIG_GLOBAL`::\n     The path to the global (per-user) configuration files, if any.\n+    This variable can have multiple values.\n \n Most path values contain only one value. However, some can contain multiple\n values, which are separated by newlines, and are listed in order from highest to\ndiff --git a/builtin/var.c b/builtin/var.c\nindex cc3a43cde2..9f7c8a6113 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -14,10 +14,11 @@\n #include \"environment.h\"\n #include \"ident.h\"\n #include \"pager.h\"\n-#include \"refs.h\"\n #include \"path.h\"\n-#include \"strbuf.h\"\n+#include \"refs.h\"\n #include \"run-command.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n \n static const char var_usage[] = \"git var (-l | <variable>)\";\n \n@@ -90,35 +91,27 @@ static char *git_config_val_system(int ident_flag UNUSED)\n \treturn NULL;\n }\n \n-static char *git_config_val_global(int ident_flag UNUSED)\n+static void git_config_val_global(struct string_list *list)\n {\n-\tstruct strbuf buf = STRBUF_INIT;\n \tchar *user, *xdg;\n-\tsize_t unused;\n \n \tgit_global_config_paths(&user, &xdg);\n \tif (xdg && *xdg) {\n \t\tnormalize_path_copy(xdg, xdg);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", xdg);\n+\t\tstring_list_append(list, xdg);\n \t}\n \tif (user && *user) {\n \t\tnormalize_path_copy(user, user);\n-\t\tstrbuf_addf(&buf, \"%s\\n\", user);\n+\t\tstring_list_append(list, user);\n \t}\n \tfree(xdg);\n \tfree(user);\n-\tstrbuf_trim_trailing_newline(&buf);\n-\tif (buf.len == 0) {\n-\t\tstrbuf_release(&buf);\n-\t\treturn NULL;\n-\t}\n-\treturn strbuf_detach(&buf, &unused);\n }\n \n struct git_var {\n \tconst char *name;\n \tchar *(*read)(int);\n-\tint multivalued;\n+\tvoid (*multiread)(struct string_list *);\n };\n static struct git_var git_vars[] = {\n \t{\n@@ -163,8 +156,7 @@ static struct git_var git_vars[] = {\n \t},\n \t{\n \t\t.name = \"GIT_CONFIG_GLOBAL\",\n-\t\t.read = git_config_val_global,\n-\t\t.multivalued = 1,\n+\t\t.multiread = git_config_val_global,\n \t},\n \t{\n \t\t.name = \"\",\n@@ -175,28 +167,30 @@ static struct git_var git_vars[] = {\n static void list_vars(void)\n {\n \tstruct git_var *ptr;\n-\tchar *val;\n-\n-\tfor (ptr = git_vars; ptr->read; ptr++)\n-\t\tif ((val = ptr->read(0))) {\n-\t\t\tif (ptr->multivalued && *val) {\n-\t\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n-\n-\t\t\t\tstring_list_split(&list, val, \"\\n\", -1);\n-\t\t\t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n-\t\t\t\tstring_list_clear(&list, 0);\n-\t\t\t} else {\n+\n+\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n+\t\tif (ptr->read) {\n+\t\t\tchar *val = ptr->read(0);\n+\n+\t\t\tif (val) {\n \t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n+\t\t\t\tfree(val);\n \t\t\t}\n-\t\t\tfree(val);\n+\t\t} else {\n+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\n+\t\t\tptr->multiread(&list);\n+\t\t\tfor (size_t i = 0; i < list.nr; i++)\n+\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n+\t\t\tstring_list_clear(&list, 0);\n \t\t}\n+\t}\n }\n \n static const struct git_var *get_git_var(const char *var)\n {\n \tstruct git_var *ptr;\n-\tfor (ptr = git_vars; ptr->read; ptr++) {\n+\tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n \t\tif (strcmp(var, ptr->name) == 0) {\n \t\t\treturn ptr;\n \t\t}\n@@ -220,7 +214,6 @@ int cmd_var(int argc,\n \t    struct repository *repo UNUSED)\n {\n \tconst struct git_var *git_var;\n-\tchar *val;\n \n \tshow_usage_if_asked(argc, argv, var_usage);\n \tif (argc != 2)\n@@ -237,12 +230,26 @@ int cmd_var(int argc,\n \tif (!git_var)\n \t\tusage(var_usage);\n \n-\tval = git_var->read(IDENT_STRICT);\n-\tif (!val)\n-\t\treturn 1;\n+\tif (git_var->read) {\n+\t\tchar *val = git_var->read(IDENT_STRICT);\n+\n+\t\tif (!val)\n+\t\t\treturn 1;\n \n-\tprintf(\"%s\\n\", val);\n-\tfree(val);\n+\t\tprintf(\"%s\\n\", val);\n+\t\tfree(val);\n+\t} else {\n+\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\n+\t\tgit_var->multiread(&list);\n+\t\tif (!list.nr) {\n+\t\t\tstring_list_clear(&list, 0);\n+\t\t\treturn 1;\n+\t\t}\n+\t\tfor (size_t i = 0; i < list.nr; i++)\n+\t\t\tprintf(\"%s\\n\", list.items[i].string);\n+\t\tstring_list_clear(&list, 0);\n+\t}\n \n \treturn 0;\n }\n-- \n2.54.0 (Apple Git-157)\n\n"},{"id":"553362","messageId":"20260926162048.30853-3-andrewpleeter@gmail.com","threadId":"66219","inReplyTo":"xmqq33va1lcg.fsf@gitster.g","subject":"[PATCH v9 2/4] var: add \"-z\" output mode","fromName":"Andrew Pleeter","fromEmail":"andrewpleeter@gmail.com","sentAt":"2026-09-26T16:20:46Z","receivedAt":"2026-09-26T16:20:53Z","isPatch":true,"body":"Scripts that read from \"git var\" have no unambiguous way to parse its\noutput.  A value that itself contains a newline, and a variable that\nhas more than one value, both run together with the newline that\nterminates each entry.\n\nTeach \"git var\" a \"-z\" option that terminates each entry with a NUL\nbyte instead.  With \"-l\", the variable name and its value are separated\nby a newline and each entry is terminated by NUL, which is the same\nconvention \"git config list -z\" already uses.  When a single variable\nis asked for, its bare value is terminated by NUL.\n\nParsing of the command line is switched over to parse_options() in\norder to accept the new option; as a side effect, options now have to\ncome before the variable name.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n Documentation/git-var.adoc | 19 ++++++++---\n builtin/var.c              | 67 +++++++++++++++++++++++++++-----------\n t/t0007-git-var.sh         | 30 +++++++++++++++++\n 3 files changed, 93 insertions(+), 23 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex de3007732d..9052fbc549 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -9,13 +9,19 @@ git-var - Show a Git logical variable\n SYNOPSIS\n --------\n [synopsis]\n-git var (-l | <variable>)\n+git var [-z] -l\n+git var [-z] <variable>\n \n DESCRIPTION\n -----------\n Prints a Git logical variable. Exits with code 1 if the variable has\n no value.\n \n+If `-z` is given, the value is terminated by a NUL byte instead of a\n+newline. With `-l`, the variable name and its value are separated by a\n+newline, and each entry is terminated by a NUL byte\n+(`VARIABLE\\nvalue\\0`), in the same way as `git config list -z`.\n+\n OPTIONS\n -------\n `-l`::\n@@ -24,6 +30,11 @@ OPTIONS\n \tas well. (However, the configuration variables listing functionality\n \tis deprecated in favor of `git config list`.)\n \n+`-z`::\n+\tTerminate entries with NUL instead of newline. When used with\n+\t`-l`, the variable name and its value are separated by a\n+\tnewline, and each entry is terminated with a NUL byte.\n+\n EXAMPLES\n --------\n \t$ git var GIT_AUTHOR_IDENT\n@@ -86,9 +97,9 @@ endif::git-default-pager[]\n     This variable can have multiple values.\n \n Most path values contain only one value. However, some can contain multiple\n-values, which are separated by newlines, and are listed in order from highest to\n-lowest priority.  Callers should be prepared for any such path value to contain\n-multiple items.\n+values, which are separated by newlines (or NUL bytes if `-z` is given), and are\n+listed in order from highest to lowest priority.  Callers should be prepared for\n+any such path value to contain multiple items.\n \n Note that paths are printed even if they do not exist, but not if they are\n disabled by other environment variables.\ndiff --git a/builtin/var.c b/builtin/var.c\nindex 9f7c8a6113..c9691070b8 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -14,13 +14,18 @@\n #include \"environment.h\"\n #include \"ident.h\"\n #include \"pager.h\"\n+#include \"parse-options.h\"\n #include \"path.h\"\n #include \"refs.h\"\n #include \"run-command.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n \n-static const char var_usage[] = \"git var (-l | <variable>)\";\n+static const char * const var_usage[] = {\n+\tN_(\"git var [-z] -l\"),\n+\tN_(\"git var [-z] <variable>\"),\n+\tNULL\n+};\n \n static char *committer(int ident_flag)\n {\n@@ -164,16 +169,18 @@ static struct git_var git_vars[] = {\n \t},\n };\n \n-static void list_vars(void)\n+static void list_vars(int nul_term)\n {\n \tstruct git_var *ptr;\n+\tchar delim = nul_term ? '\\n' : '=';\n+\tchar term = nul_term ? '\\0' : '\\n';\n \n \tfor (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {\n \t\tif (ptr->read) {\n \t\t\tchar *val = ptr->read(0);\n \n \t\t\tif (val) {\n-\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, val);\n+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim, val, term);\n \t\t\t\tfree(val);\n \t\t\t}\n \t\t} else {\n@@ -181,7 +188,8 @@ static void list_vars(void)\n \n \t\t\tptr->multiread(&list);\n \t\t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\t\tprintf(\"%s=%s\\n\", ptr->name, list.items[i].string);\n+\t\t\t\tprintf(\"%s%c%s%c\", ptr->name, delim,\n+\t\t\t\t       list.items[i].string, term);\n \t\t\tstring_list_clear(&list, 0);\n \t\t}\n \t}\n@@ -201,34 +209,55 @@ static const struct git_var *get_git_var(const char *var)\n static int show_config(const char *var, const char *value,\n \t\t       const struct config_context *ctx, void *cb)\n {\n+\tint *nul_term = cb;\n+\tchar delim = *nul_term ? '\\n' : '=';\n+\tchar term = *nul_term ? '\\0' : '\\n';\n+\n \tif (value)\n-\t\tprintf(\"%s=%s\\n\", var, value);\n+\t\tprintf(\"%s%c%s%c\", var, delim, value, term);\n \telse\n-\t\tprintf(\"%s\\n\", var);\n+\t\tprintf(\"%s%c\", var, term);\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n int cmd_var(int argc,\n \t    const char **argv,\n-\t    const char *prefix UNUSED,\n+\t    const char *prefix,\n \t    struct repository *repo UNUSED)\n {\n \tconst struct git_var *git_var;\n-\n-\tshow_usage_if_asked(argc, argv, var_usage);\n-\tif (argc != 2)\n-\t\tusage(var_usage);\n-\n-\tif (strcmp(argv[1], \"-l\") == 0) {\n-\t\trepo_config(the_repository, show_config, NULL);\n-\t\tlist_vars();\n+\tint list = 0;\n+\tint nul_term = 0;\n+\tchar term;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL('l', NULL, &list,\n+\t\t\t N_(\"list all variables\")),\n+\t\tOPT_BOOL('z', NULL, &nul_term,\n+\t\t\t N_(\"terminate entries with NUL\")),\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\tif (list) {\n+\t\tif (argc)\n+\t\t\tusage_with_options(var_usage, options);\n+\t\trepo_config(the_repository, show_config, &nul_term);\n+\t\tlist_vars(nul_term);\n \t\treturn 0;\n \t}\n+\n+\tif (argc != 1)\n+\t\tusage_with_options(var_usage, options);\n+\n \trepo_config(the_repository, git_default_config, NULL);\n \n-\tgit_var = get_git_var(argv[1]);\n+\tterm = nul_term ? '\\0' : '\\n';\n+\n+\tgit_var = get_git_var(argv[0]);\n \tif (!git_var)\n-\t\tusage(var_usage);\n+\t\tusage_with_options(var_usage, options);\n \n \tif (git_var->read) {\n \t\tchar *val = git_var->read(IDENT_STRICT);\n@@ -236,7 +265,7 @@ int cmd_var(int argc,\n \t\tif (!val)\n \t\t\treturn 1;\n \n-\t\tprintf(\"%s\\n\", val);\n+\t\tprintf(\"%s%c\", val, term);\n \t\tfree(val);\n \t} else {\n \t\tstruct string_list list = STRING_LIST_INIT_DUP;\n@@ -247,7 +276,7 @@ int cmd_var(int argc,\n \t\t\treturn 1;\n \t\t}\n \t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\tprintf(\"%s\\n\", list.items[i].string);\n+\t\t\tprintf(\"%s%c\", list.items[i].string, term);\n \t\tstring_list_clear(&list, 0);\n \t}\n \ndiff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\nindex 2b60317758..661d0539c0 100755\n--- a/t/t0007-git-var.sh\n+++ b/t/t0007-git-var.sh\n@@ -276,4 +276,34 @@ test_expect_success '`git var -l` works even without HOME' '\n \t)\n '\n \n+test_expect_success 'get a variable with -z' '\n+\tprintf \"%sQ\" \"$(git var GIT_AUTHOR_IDENT)\" >expect &&\n+\tgit var -z GIT_AUTHOR_IDENT >actual.raw &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multi-valued variable with -z' '\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" git var -z GIT_CONFIG_GLOBAL >actual.raw &&\n+\tprintf \"%sQ%sQ\" \"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expect &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git var -l -z' '\n+\tgit var -l -z >actual &&\n+\ttr \"\\0\" \"\\n\" <actual >actual.lines &&\n+\tgit var GIT_AUTHOR_IDENT >expect &&\n+\tsed -n \"/^GIT_AUTHOR_IDENT$/{n;p;}\" actual.lines >actual.author &&\n+\ttest_cmp expect actual.author &&\n+\techo false >expect &&\n+\tsed -n \"/^core\\.bare$/{n;p;}\" actual.lines >actual.bare &&\n+\ttest_cmp expect actual.bare\n+'\n+\n+test_expect_success 'options must precede variable arguments' '\n+\ttest_must_fail git var GIT_AUTHOR_IDENT -z\n+'\n+\n test_done\n-- \n2.54.0 (Apple Git-157)\n\n"},{"id":"553361","messageId":"20260926162048.30853-4-andrewpleeter@gmail.com","threadId":"66219","inReplyTo":"xmqq33va1lcg.fsf@gitster.g","subject":"[PATCH v9 3/4] var: accept more than one variable","fromName":"Andrew Pleeter","fromEmail":"andrewpleeter@gmail.com","sentAt":"2026-09-26T16:20:47Z","receivedAt":"2026-09-26T16:20:54Z","isPatch":true,"body":"\"git var\" takes exactly one variable and shows its value, so a script\nthat needs several of them has to run the command once per variable,\npaying process startup and configuration parsing each time.\n\nTeach \"git var\" to take any number of variables and show them the way\n\"git var -l\" does, i.e. as \"VARIABLE=value\" (or \"VARIABLE\\nvalue\\0\"\nunder \"-z\"), but limited to the variables the user asked for.  A single\nvariable still shows its bare value, and still exits with status 1 when\nit has no value, so existing users are unaffected.\n\nWhen more than one variable is asked for, a variable that has no value\nis simply left out of the output and the rest are still shown.  That a\nvariable is unset is not an error in itself: a caller asking for both\nGIT_CONFIG_SYSTEM and GIT_CONFIG_GLOBAL should not be told that\nsomething went wrong merely because the user set GIT_CONFIG_NOSYSTEM.\nKeeping a non-zero status for genuine errors, such as naming a variable\nthat does not exist, also lets callers detect those by looking at the\nexit code alone.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n Documentation/git-var.adoc | 32 +++++++++++-------\n builtin/var.c              | 56 +++++++++++++++++++------------\n t/t0007-git-var.sh         | 67 ++++++++++++++++++++++++++++++++++++++\n 3 files changed, 123 insertions(+), 32 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex 9052fbc549..d0772d9743 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -10,17 +10,22 @@ SYNOPSIS\n --------\n [synopsis]\n git var [-z] -l\n-git var [-z] <variable>\n+git var [-z] <variable>...\n \n DESCRIPTION\n -----------\n-Prints a Git logical variable. Exits with code 1 if the variable has\n-no value.\n+Prints Git logical variables. When a single variable is requested, its\n+bare value is printed, and the command exits with code 1 if it has no\n+value. When multiple variables are requested, they are printed as\n+`VARIABLE=value` pairs, separated by newlines; a variable that has no\n+value is omitted from the output, which is not by itself an error.\n \n-If `-z` is given, the value is terminated by a NUL byte instead of a\n-newline. With `-l`, the variable name and its value are separated by a\n-newline, and each entry is terminated by a NUL byte\n-(`VARIABLE\\nvalue\\0`), in the same way as `git config list -z`.\n+If `-z` is given, the output format changes depending on the mode:\n+\n+* With a single variable, the bare value is terminated by a NUL byte.\n+* With multiple variables or with `-l`, the variable name and value are\n+  separated by a newline, and each entry is terminated by a NUL byte\n+  (`VARIABLE\\nvalue\\0`), in the same way as `git config list -z`.\n \n OPTIONS\n -------\n@@ -32,8 +37,9 @@ OPTIONS\n \n `-z`::\n \tTerminate entries with NUL instead of newline. When used with\n-\t`-l`, the variable name and its value are separated by a\n-\tnewline, and each entry is terminated with a NUL byte.\n+\t`-l` or when multiple variables are requested, the variable name\n+\tand its value are separated by a newline, and each entry is\n+\tterminated with a NUL byte.\n \n EXAMPLES\n --------\n@@ -97,9 +103,11 @@ endif::git-default-pager[]\n     This variable can have multiple values.\n \n Most path values contain only one value. However, some can contain multiple\n-values, which are separated by newlines (or NUL bytes if `-z` is given), and are\n-listed in order from highest to lowest priority.  Callers should be prepared for\n-any such path value to contain multiple items.\n+values, which are separated by newlines (or NUL bytes if `-z` is given),\n+and are listed in order from highest to lowest priority. When querying\n+multiple variables (or using `-l`), each value is output as a separate\n+`VARIABLE=value` entry (or `VARIABLE\\nvalue\\0` with `-z`). Callers should\n+be prepared for any such path value to contain multiple items.\n \n Note that paths are printed even if they do not exist, but not if they are\n disabled by other environment variables.\ndiff --git a/builtin/var.c b/builtin/var.c\nindex c9691070b8..5fbebc62e2 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -23,7 +23,7 @@\n \n static const char * const var_usage[] = {\n \tN_(\"git var [-z] -l\"),\n-\tN_(\"git var [-z] <variable>\"),\n+\tN_(\"git var [-z] <variable>...\"),\n \tNULL\n };\n \n@@ -225,9 +225,9 @@ int cmd_var(int argc,\n \t    const char *prefix,\n \t    struct repository *repo UNUSED)\n {\n-\tconst struct git_var *git_var;\n \tint list = 0;\n \tint nul_term = 0;\n+\tchar delim;\n \tchar term;\n \tstruct option options[] = {\n \t\tOPT_BOOL('l', NULL, &list,\n@@ -248,36 +248,52 @@ int cmd_var(int argc,\n \t\treturn 0;\n \t}\n \n-\tif (argc != 1)\n+\tif (!argc)\n \t\tusage_with_options(var_usage, options);\n \n \trepo_config(the_repository, git_default_config, NULL);\n \n+\tdelim = nul_term ? '\\n' : '=';\n \tterm = nul_term ? '\\0' : '\\n';\n \n-\tgit_var = get_git_var(argv[0]);\n-\tif (!git_var)\n-\t\tusage_with_options(var_usage, options);\n+\tfor (int i = 0; i < argc; i++) {\n+\t\tconst struct git_var *git_var = get_git_var(argv[i]);\n \n-\tif (git_var->read) {\n-\t\tchar *val = git_var->read(IDENT_STRICT);\n+\t\tif (!git_var)\n+\t\t\tusage_with_options(var_usage, options);\n \n-\t\tif (!val)\n-\t\t\treturn 1;\n+\t\tif (git_var->read) {\n+\t\t\tchar *val = git_var->read(IDENT_STRICT);\n \n-\t\tprintf(\"%s%c\", val, term);\n-\t\tfree(val);\n-\t} else {\n-\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\t\t\tif (!val) {\n+\t\t\t\tif (argc == 1)\n+\t\t\t\t\treturn 1;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tif (argc == 1)\n+\t\t\t\tprintf(\"%s%c\", val, term);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s%c%s%c\", git_var->name, delim,\n+\t\t\t\t       val, term);\n+\t\t\tfree(val);\n+\t\t} else {\n+\t\t\tstruct string_list list = STRING_LIST_INIT_DUP;\n \n-\t\tgit_var->multiread(&list);\n-\t\tif (!list.nr) {\n+\t\t\tgit_var->multiread(&list);\n+\t\t\tif (argc == 1 && !list.nr) {\n+\t\t\t\tstring_list_clear(&list, 0);\n+\t\t\t\treturn 1;\n+\t\t\t}\n+\t\t\tfor (size_t j = 0; j < list.nr; j++) {\n+\t\t\t\tif (argc == 1)\n+\t\t\t\t\tprintf(\"%s%c\", list.items[j].string,\n+\t\t\t\t\t       term);\n+\t\t\t\telse\n+\t\t\t\t\tprintf(\"%s%c%s%c\", git_var->name, delim,\n+\t\t\t\t\t       list.items[j].string, term);\n+\t\t\t}\n \t\t\tstring_list_clear(&list, 0);\n-\t\t\treturn 1;\n \t\t}\n-\t\tfor (size_t i = 0; i < list.nr; i++)\n-\t\t\tprintf(\"%s%c\", list.items[i].string, term);\n-\t\tstring_list_clear(&list, 0);\n \t}\n \n \treturn 0;\ndiff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\nindex 661d0539c0..593610300a 100755\n--- a/t/t0007-git-var.sh\n+++ b/t/t0007-git-var.sh\n@@ -306,4 +306,71 @@ test_expect_success 'options must precede variable arguments' '\n \ttest_must_fail git var GIT_AUTHOR_IDENT -z\n '\n \n+test_expect_success 'get multiple variables' '\n+\ttest_tick &&\n+\tcat >expect <<-EOF &&\n+\tGIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE\n+\tGIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE\n+\tEOF\n+\tgit var GIT_AUTHOR_IDENT GIT_COMMITTER_IDENT >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables with -z' '\n+\ttest_tick &&\n+\tprintf \"GIT_AUTHOR_IDENT\\n%sQGIT_COMMITTER_IDENT\\n%sQ\" \\\n+\t\t\"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE\" \\\n+\t\t\"$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE\" >expect &&\n+\tgit var -z GIT_AUTHOR_IDENT GIT_COMMITTER_IDENT >actual.raw &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables including multi-valued variable' '\n+\ttest_tick &&\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tcat >expect <<-EOF &&\n+\tGIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE\n+\tGIT_CONFIG_GLOBAL=$TRASHDIR/foo/git/config\n+\tGIT_CONFIG_GLOBAL=$TRASHDIR/.gitconfig\n+\tGIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE\n+\tEOF\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n+\t\tgit var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL GIT_COMMITTER_IDENT >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'get multiple variables including multi-valued variable with -z' '\n+\ttest_tick &&\n+\tTRASHDIR=\"$(test-tool path-utils normalize_path_copy \"$(pwd)\")\" &&\n+\tprintf \"GIT_AUTHOR_IDENT\\n%sQGIT_CONFIG_GLOBAL\\n%sQGIT_CONFIG_GLOBAL\\n%sQ\" \\\n+\t\t\"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE\" \\\n+\t\t\"$TRASHDIR/foo/git/config\" \"$TRASHDIR/.gitconfig\" >expect &&\n+\tHOME=\"$TRASHDIR\" XDG_CONFIG_HOME=\"$TRASHDIR/foo\" \\\n+\t\tgit var -z GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL >actual.raw &&\n+\tnul_to_q <actual.raw >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'variable without a value is omitted but is not an error' '\n+\ttest_tick &&\n+\tcat >expect <<-EOF &&\n+\tGIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE\n+\tGIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE\n+\tEOF\n+\ttest_env GIT_CONFIG_GLOBAL= \\\n+\t\tgit var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL GIT_COMMITTER_IDENT >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'a single variable without a value still exits with 1' '\n+\ttest_env GIT_CONFIG_GLOBAL= test_expect_code 1 git var GIT_CONFIG_GLOBAL >out &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'unknown variable is a usage error' '\n+\ttest_must_fail git var GIT_AUTHOR_IDENT NO_SUCH_VARIABLE 2>err &&\n+\ttest_grep usage err\n+'\n+\n test_done\n-- \n2.54.0 (Apple Git-157)\n\n"},{"id":"553363","messageId":"20260926162048.30853-5-andrewpleeter@gmail.com","threadId":"66219","inReplyTo":"xmqq33va1lcg.fsf@gitster.g","subject":"[PATCH v9 4/4] var: add broken-out identity variables","fromName":"Andrew Pleeter","fromEmail":"andrewpleeter@gmail.com","sentAt":"2026-09-26T16:20:48Z","receivedAt":"2026-09-26T16:20:55Z","isPatch":true,"body":"A script that wants only the author's name has to ask for\nGIT_AUTHOR_IDENT and take the result apart itself, which means\nreimplementing the rules split_ident_line() already knows, and getting\nthem subtly wrong for names that contain an e-mail address or other\nawkward characters.\n\nAdd GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL and GIT_AUTHOR_DATE, together\nwith their GIT_COMMITTER_* counterparts, which give the individual\npieces of the corresponding *_IDENT variable.  They are derived from\nthe same ident string, so they are strict in the same way: asking for\none of them fails if the identity cannot be determined.\n\nSigned-off-by: Andrew Pleeter <andrewpleeter@gmail.com>\n---\n Documentation/git-var.adoc | 36 +++++++++++++--\n builtin/var.c              | 90 ++++++++++++++++++++++++++++++++++++++\n t/t0007-git-var.sh         | 56 ++++++++++++++++++++++++\n 3 files changed, 178 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc\nindex d0772d9743..6982228f97 100644\n--- a/Documentation/git-var.adoc\n+++ b/Documentation/git-var.adoc\n@@ -43,17 +43,45 @@ OPTIONS\n \n EXAMPLES\n --------\n-\t$ git var GIT_AUTHOR_IDENT\n-\tEric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n+* Get the author identity:\n++\n+------------\n+$ git var GIT_AUTHOR_IDENT\n+Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600\n+------------\n+\n+* Get the author name and email:\n++\n+------------\n+$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL\n+GIT_AUTHOR_NAME=Eric W. Biederman\n+GIT_AUTHOR_EMAIL=ebiederm@lnxi.com\n+------------\n \n \n VARIABLES\n ---------\n `GIT_AUTHOR_IDENT`::\n-    The author of a piece of code.\n+`GIT_AUTHOR_NAME`::\n+`GIT_AUTHOR_EMAIL`::\n+`GIT_AUTHOR_DATE`::\n+    The authorship information that would be recorded in the\n+    resulting commit object if you ran `git commit` right now.\n+    `GIT_AUTHOR_IDENT` consists of the author's name, e-mail\n+    address, and timestamp+timezone. These three pieces of\n+    information are available separately as `GIT_AUTHOR_NAME`,\n+    `GIT_AUTHOR_EMAIL`, and `GIT_AUTHOR_DATE`.\n \n `GIT_COMMITTER_IDENT`::\n-    The person who put a piece of code into Git.\n+`GIT_COMMITTER_NAME`::\n+`GIT_COMMITTER_EMAIL`::\n+`GIT_COMMITTER_DATE`::\n+    The committer information that would be recorded in the\n+    resulting commit object if you ran `git commit` right now.\n+    `GIT_COMMITTER_IDENT` consists of the committer's name, e-mail\n+    address, and timestamp+timezone. These three pieces of\n+    information are available separately as `GIT_COMMITTER_NAME`,\n+    `GIT_COMMITTER_EMAIL`, and `GIT_COMMITTER_DATE`.\n \n `GIT_EDITOR`::\n     Text editor for use by Git commands.  The value is meant to be\ndiff --git a/builtin/var.c b/builtin/var.c\nindex 5fbebc62e2..dd4325e5b5 100644\n--- a/builtin/var.c\n+++ b/builtin/var.c\n@@ -27,16 +27,82 @@ static const char * const var_usage[] = {\n \tNULL\n };\n \n+enum ident_part {\n+\tIDENT_NAME,\n+\tIDENT_MAIL,\n+\tIDENT_DATE,\n+};\n+\n+static char *ident_part(const char *ident, enum ident_part part)\n+{\n+\tstruct ident_split split;\n+\n+\tif (!ident)\n+\t\treturn NULL;\n+\tif (split_ident_line(&split, ident, strlen(ident)))\n+\t\treturn NULL;\n+\n+\tswitch (part) {\n+\tcase IDENT_NAME:\n+\t\tif (!split.name_begin || !split.name_end)\n+\t\t\tBUG(\"split_ident_line() gave NULL names???\");\n+\t\treturn xmemdupz(split.name_begin,\n+\t\t\t\tsplit.name_end - split.name_begin);\n+\tcase IDENT_MAIL:\n+\t\tif (!split.mail_begin || !split.mail_end)\n+\t\t\tBUG(\"split_ident_line() gave NULL mail???\");\n+\t\treturn xmemdupz(split.mail_begin,\n+\t\t\t\tsplit.mail_end - split.mail_begin);\n+\tcase IDENT_DATE:\n+\t\tif (!split.date_begin || !split.tz_end)\n+\t\t\tBUG(\"split_ident_line() gave NULL date/tz???\");\n+\t\treturn xmemdupz(split.date_begin,\n+\t\t\t\tsplit.tz_end - split.date_begin);\n+\tdefault:\n+\t\tBUG(\"unknown ident_part %d\", part);\n+\t}\n+}\n+\n static char *committer(int ident_flag)\n {\n \treturn xstrdup_or_null(git_committer_info(ident_flag));\n }\n \n+static char *committer_name(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *committer_email(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *committer_date(int ident_flag)\n+{\n+\treturn ident_part(git_committer_info(ident_flag), IDENT_DATE);\n+}\n+\n static char *author(int ident_flag)\n {\n \treturn xstrdup_or_null(git_author_info(ident_flag));\n }\n \n+static char *author_name(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_NAME);\n+}\n+\n+static char *author_email(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_MAIL);\n+}\n+\n+static char *author_date(int ident_flag)\n+{\n+\treturn ident_part(git_author_info(ident_flag), IDENT_DATE);\n+}\n+\n static char *editor(int ident_flag UNUSED)\n {\n \treturn xstrdup_or_null(git_editor());\n@@ -123,10 +189,34 @@ static struct git_var git_vars[] = {\n \t\t.name = \"GIT_COMMITTER_IDENT\",\n \t\t.read = committer,\n \t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_NAME\",\n+\t\t.read = committer_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_EMAIL\",\n+\t\t.read = committer_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_COMMITTER_DATE\",\n+\t\t.read = committer_date,\n+\t},\n \t{\n \t\t.name = \"GIT_AUTHOR_IDENT\",\n \t\t.read = author,\n \t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_NAME\",\n+\t\t.read = author_name,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_EMAIL\",\n+\t\t.read = author_email,\n+\t},\n+\t{\n+\t\t.name = \"GIT_AUTHOR_DATE\",\n+\t\t.read = author_date,\n+\t},\n \t{\n \t\t.name = \"GIT_EDITOR\",\n \t\t.read = editor,\ndiff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh\nindex 593610300a..e43becff1c 100755\n--- a/t/t0007-git-var.sh\n+++ b/t/t0007-git-var.sh\n@@ -373,4 +373,60 @@ test_expect_success 'unknown variable is a usage error' '\n \ttest_grep usage err\n '\n \n+test_expect_success 'get author identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n+\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n+\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n+\tgit var GIT_AUTHOR_NAME >actual.name &&\n+\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n+\tgit var GIT_AUTHOR_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success 'get committer identity components' '\n+\ttest_tick &&\n+\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n+\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n+\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n+\tgit var GIT_COMMITTER_NAME >actual.name &&\n+\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n+\tgit var GIT_COMMITTER_DATE >actual.date &&\n+\ttest_cmp expect.name actual.name &&\n+\ttest_cmp expect.email actual.email &&\n+\ttest_cmp expect.date actual.date\n+'\n+\n+test_expect_success !FAIL_PREREQS,!AUTOIDENT 'identity components are strict' '\n+\t(\n+\t\tsane_unset GIT_COMMITTER_NAME &&\n+\t\tsane_unset GIT_COMMITTER_EMAIL &&\n+\t\ttest_must_fail git var GIT_COMMITTER_NAME\n+\t)\n+'\n+\n+test_expect_success 'get several identity components at once' '\n+\ttest_tick &&\n+\tcat >expect <<-EOF &&\n+\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n+\tGIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL\n+\tGIT_COMMITTER_NAME=$GIT_COMMITTER_NAME\n+\tGIT_COMMITTER_EMAIL=$GIT_COMMITTER_EMAIL\n+\tEOF\n+\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'git var -l lists the identity components' '\n+\tgit var -l >actual &&\n+\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n+\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n+'\n+\n test_done\n-- \n2.54.0 (Apple Git-157)\n\n"},{"id":"553967","messageId":"3509a23e-9de1-442c-a64c-bc33110f92e7@gmail.com","threadId":"66219","inReplyTo":"20260926162048.30853-5-andrewpleeter@gmail.com","subject":"Re: [PATCH v9 4/4] var: add broken-out identity variables","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-02T14:11:21Z","receivedAt":"2026-10-02T14:11:34Z","isPatch":true,"body":"Hi Andrew\n\nThe implementation looks good, just one small comment on the tests.\n\nOn 26/09/2026 17:20, Andrew Pleeter wrote:\n> +test_expect_success 'get author identity components' '\n> +\ttest_tick &&\n> +\techo \"$GIT_AUTHOR_NAME\" >expect.name &&\n> +\techo \"$GIT_AUTHOR_EMAIL\" >expect.email &&\n> +\techo \"$GIT_AUTHOR_DATE\" >expect.date &&\n> +\tgit var GIT_AUTHOR_NAME >actual.name &&\n> +\tgit var GIT_AUTHOR_EMAIL >actual.email &&\n> +\tgit var GIT_AUTHOR_DATE >actual.date &&\n> +\ttest_cmp expect.name actual.name &&\n> +\ttest_cmp expect.email actual.email &&\n> +\ttest_cmp expect.date actual.date\n> +'\n\nI think it would have been sufficient just to list all the identity \ncomponents at once, rather than having separate tests for each one, but \nit is not worth re-rolling just for that.\n\nThanks\n\nPhillip\n\n> +test_expect_success 'get committer identity components' '\n> +\ttest_tick &&\n> +\techo \"$GIT_COMMITTER_NAME\" >expect.name &&\n> +\techo \"$GIT_COMMITTER_EMAIL\" >expect.email &&\n> +\techo \"$GIT_COMMITTER_DATE\" >expect.date &&\n> +\tgit var GIT_COMMITTER_NAME >actual.name &&\n> +\tgit var GIT_COMMITTER_EMAIL >actual.email &&\n> +\tgit var GIT_COMMITTER_DATE >actual.date &&\n> +\ttest_cmp expect.name actual.name &&\n> +\ttest_cmp expect.email actual.email &&\n> +\ttest_cmp expect.date actual.date\n> +'\n> +\n> +test_expect_success !FAIL_PREREQS,!AUTOIDENT 'identity components are strict' '\n> +\t(\n> +\t\tsane_unset GIT_COMMITTER_NAME &&\n> +\t\tsane_unset GIT_COMMITTER_EMAIL &&\n> +\t\ttest_must_fail git var GIT_COMMITTER_NAME\n> +\t)\n> +'\n> +\n> +test_expect_success 'get several identity components at once' '\n> +\ttest_tick &&\n> +\tcat >expect <<-EOF &&\n> +\tGIT_AUTHOR_NAME=$GIT_AUTHOR_NAME\n> +\tGIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL\n> +\tGIT_COMMITTER_NAME=$GIT_COMMITTER_NAME\n> +\tGIT_COMMITTER_EMAIL=$GIT_COMMITTER_EMAIL\n> +\tEOF\n> +\tgit var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'git var -l lists the identity components' '\n> +\tgit var -l >actual &&\n> +\ttest_grep \"^GIT_AUTHOR_NAME=\" actual &&\n> +\ttest_grep \"^GIT_AUTHOR_EMAIL=\" actual &&\n> +\ttest_grep \"^GIT_AUTHOR_DATE=\" actual &&\n> +\ttest_grep \"^GIT_COMMITTER_NAME=\" actual &&\n> +\ttest_grep \"^GIT_COMMITTER_EMAIL=\" actual &&\n> +\ttest_grep \"^GIT_COMMITTER_DATE=\" actual\n> +'\n> +\n>   test_done\n\n"},{"id":"553968","messageId":"33f24834-8e73-4230-bf0a-6809a85d9a86@gmail.com","threadId":"66219","inReplyTo":"20260926162048.30853-1-andrewpleeter@gmail.com","subject":"Re: [PATCH v9 0/4] var: -z output, multiple variables, and broken-out idents","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-02T14:11:31Z","receivedAt":"2026-10-02T14:11:43Z","isPatch":true,"body":"Hi Andrew\n\nOn 26/09/2026 17:20, Andrew Pleeter wrote:\n> This is a reroll of the single patch in v8, split up as Junio asked\n> for, with the changes Phillip asked for on the list.\n> \n> v8 was one commit doing four things at once. It is now four patches:\n> \n>    1/4 converts the internal representation of a multi-valued variable\n>        from a newline-joined string to a string_list, with no change in\n>        output. Phillip suggested doing this as its own preparatory step.\n> \n>    2/4 adds \"-z\", which is what forces the switch to parse_options(), so\n>        that conversion lives here rather than in 1/4.\n> \n>    3/4 teaches \"git var\" to take more than one variable.\n> \n>    4/4 adds GIT_AUTHOR_NAME and friends.\n\nThese patches look good to me. I've left a comment on the tests in \npatches 3 and 4, but I'm not sure they are enough to warrant a re-roll.\n\nThanks for working on this, supporting multiple values and NUL \ntermination are really useful improvements.\n\nPhillip\n\n> Changes since v8:\n> \n>   * GIT_SIGNING_KEY is dropped. Phillip asked three times how it was\n>     meant to be used, and once I looked properly the answer was that the\n>     value cannot be interpreted without also reading gpg.format, and that\n>     in the default configuration it is a committer ident rather than a\n>     key at all. I would rather leave it out than define a variable I\n>     cannot describe. Details are in my reply to him on this thread.\n> \n>   * Asking for several variables no longer exits non-zero just because\n>     one of them has no value; such a variable is left out of the output\n>     and the rest are still shown. A non-zero status is now reserved for\n>     real errors, such as naming a variable that does not exist, so\n>     callers can detect those from the exit code. A single variable still\n>     exits 1 when it has no value, as before. This is Phillip's\n>     GIT_CONFIG_NOSYSTEM point.\n> \n>   * GIT_CONFIG_GLOBAL is documented as a variable that can have more than\n>     one value.\n> \n>   * The commit messages are prose rather than a list of bullet points,\n>     and no longer narrate how the patch was developed.\n> \n> The tests use nul_to_q rather than running test_cmp over files\n> containing NUL bytes, which Phillip pointed out in v6. Each patch builds\n> and passes t0007 on its own.\n> \n> One thing I did not do, and would like an opinion on: for a multi-valued\n> variable in multi-variable mode, rather than emitting a trailing\n> delimiter, each value is shown as its own \"VARIABLE=value\" entry, which\n> matches \"git var -l\". Phillip suggested the trailing delimiter and I am\n> happy to switch.\n> \n> Andrew Pleeter (4):\n>    var: represent multi-valued variables with a string_list\n>    var: add \"-z\" output mode\n>    var: accept more than one variable\n>    var: add broken-out identity variables\n> \n>   Documentation/git-var.adoc |  68 +++++++++--\n>   builtin/var.c              | 242 +++++++++++++++++++++++++++++--------\n>   t/t0007-git-var.sh         | 153 +++++++++++++++++++++++\n>   3 files changed, 403 insertions(+), 60 deletions(-)\n> \n\n"},{"id":"553969","messageId":"cda2dd01-9dab-4842-86af-9b77767dfc52@gmail.com","threadId":"66219","inReplyTo":"20260926162048.30853-4-andrewpleeter@gmail.com","subject":"Re: [PATCH v9 3/4] var: accept more than one variable","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-02T14:11:38Z","receivedAt":"2026-10-02T14:11:50Z","isPatch":true,"body":"Hi Andrew\n\nThis all looks fine, though we typically avoid using test_env() because \nit introduces a hidden subshell. I've left a couple of suggestions \nbelow, but unless there is another reason to re-roll I wouldn't worry \ntoo much.\n\nOn 26/09/2026 17:20, Andrew Pleeter wrote:\n\n> +test_expect_success 'variable without a value is omitted but is not an error' '\n> +\ttest_tick &&\n> +\tcat >expect <<-EOF &&\n> +\tGIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE\n> +\tGIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE\n> +\tEOF\n> +\ttest_env GIT_CONFIG_GLOBAL= \\\n> +\t\tgit var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL GIT_COMMITTER_IDENT >actual &&\n\nThere is no need to use test_env here, \"GIT_CONFIG_GLOBAL= git var ...\" \nis all that's needed.\n\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'a single variable without a value still exits with 1' '\n> +\ttest_env GIT_CONFIG_GLOBAL= test_expect_code 1 git var GIT_CONFIG_GLOBAL >out &&\n\nHere test_env is also not needed, \"env GIT_CONFIG_GLOBAL= \ntest_expect_code 1 git var ...\" would be our typical style.\n\nThanks\n\nPhillip\n\n\n> +\ttest_must_be_empty out\n> +'\n> +\n> +test_expect_success 'unknown variable is a usage error' '\n> +\ttest_must_fail git var GIT_AUTHOR_IDENT NO_SUCH_VARIABLE 2>err &&\n> +\ttest_grep usage err\n> +'\n> +\n>   test_done\n\n"},{"id":"553991","messageId":"20261002174138.39096-1-andrewpleeter@gmail.com","threadId":"66219","inReplyTo":"33f24834-8e73-4230-bf0a-6809a85d9a86@gmail.com","subject":"Re: [PATCH v9 0/4] var: -z output, multiple variables, and broken-out idents","fromName":"Andrew Pleeter","fromEmail":"andrewpleeter@gmail.com","sentAt":"2026-10-02T17:41:38Z","receivedAt":"2026-10-02T17:41:46Z","isPatch":true,"body":"Hi Phillip,\n\nThanks for the review, and for the test_env pointer. Both nits are fair,\nand both of those tests are newer than the rest of the series:\n\n  - 3/4: \"GIT_CONFIG_GLOBAL= git var ...\" and \"env GIT_CONFIG_GLOBAL=\n    test_expect_code 1 git var ...\" are clearly better than dragging a\n    subshell in. I will use that style from here on.\n\n  - 4/4: the per-component tests collapse into \"get several identity\n    components at once\", which already covers the same ground.\n\nI will fold both in if anything else prompts a v10. If the series goes in\nas it stands, I can send them afterwards as a cleanup instead, whichever\nJunio prefers.\n\nThanks also for the GIT_SIGNING_KEY questions. Dropping it was the right\ncall, and I would not have got there if you had not asked three times.\n\nThanks,\nAndrew\n"}]}