Skip to content

var: support broken-down idents, signing key, multiple args, and -z - #2388

Open
anpl1623 wants to merge 1 commit into
git:masterfrom
anpl1623:master
Open

anpl1623 wants to merge 1 commit into
git:masterfrom
anpl1623:master

Conversation

@anpl1623

@anpl1623 anpl1623 commented Aug 24, 2026 •

Copy link
Copy Markdown

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

Changes since v7:

  • Modelled multi-variable querying after git var -l, printing VARIABLE=value pairs (or VARIABLE\nvalue\0 when -z is given) instead of bare values, per feedback from Junio C. Hamano.
  • Formatted multi-valued variables in multi-variable mode as repeated VARIABLE=value entries (or VARIABLE\nvalue\0 with -z), eliminating stream ambiguity and the need for extra trailing delimiters.
  • When querying multiple variables, unset variables are omitted from output and the command terminates with exit code 1.
  • Updated Documentation/git-var.adoc and t/t0007-git-var.sh accordingly.

Changes since v6:

  • Grouped GIT_AUTHOR_* and GIT_COMMITTER_* entries together into concise definitions in Documentation/git-var.adoc to avoid repetitive descriptions, per feedback from Junio C. Hamano.
  • Added explicit BUG() checks in ident_part() for NULL name/email pointers and date/timezone to protect against unforeseen changes in split_ident_line().
  • Changed struct git_var member multiread to return void (void (*multiread)(struct string_list *)) since list->nr communicates length.
  • Simplified list_vars() to use an unconditional else block for ptr->multiread instead of redundant else if.
  • In cmd_var(), do not set a non-zero exit status for missing variables in multi-variable queries; callers can see the empty record (e.g. GIT_CONFIG_SYSTEM under GIT_CONFIG_NOSYSTEM), matching Phillip Wood's recommendation.
  • Used putc(term, stdout) in cmd_var() when emitting delimiters.
  • In t/t0007-git-var.sh, converted -z tests to pipe output through nul_to_q so test_cmp produces clean diffs rather than binary comparison errors.

Changes since v5:

  • Reverted cosmetic refactoring in get_git_var() to keep the diff minimal and focused on adding ptr->multiread.
  • Added BUG("unknown ident_part %d", part) to default case in ident_part() and removed redundant NULL checks on split.name_begin and split.mail_begin.
  • Adopted first-class int (*multiread)(struct string_list *) callback in struct git_var for multi-valued variables like GIT_CONFIG_GLOBAL, cleanly populating a struct string_list instead of relying on embedded NUL buffers.
  • Improved GIT_AUTHOR_* and GIT_COMMITTER_* documentation in Documentation/git-var.adoc to describe the values that would be used if you were to run git commit right now.
  • Explicitly documented that single-variable queries and git var -l do not print an extra delimiter after multi-valued variables.

Changes since v4:

  • Simplified git_signing_key() to directly call get_signing_key() as used throughout Git (in tag, send-pack, and sign_buffer()).
  • Renamed null_term to nul_term across builtin/var.c, and simplified show_config() callback handling.
  • Replaced the redundant pre-validation loop in cmd_var() by validating arguments directly in the main execution loop.
  • When querying multiple variables, print an empty record (blank line or \0 with -z) for any variable that has no value, and continue printing remaining variables instead of terminating prematurely.
  • Switched multi-valued variable storage (git_config_val_global()) to internal \0 delimiters, iterating directly through string sequences without allocating a temporary string_list.
  • For multi-variable queries, output an extra delimiter (\n or \0) after multi-valued variables to clearly mark the end of their list.
  • Explicitly documented the git var -l -z format and multi-variable handling in Documentation/git-var.adoc.
  • Added comprehensive tests for unset variables and multi-valued stream delimiters in t/t0007-git-var.sh.

Changes since v3:

  • Renamed GIT_DEFAULT_KEY to GIT_SIGNING_KEY per feedback from Phillip Wood and Junio C Hamano; dropped the alias mechanism and commit.gpgsign check.
  • Used parse_options() with PARSE_OPT_STOP_AT_NON_OPTION in builtin/var.c, strictly enforcing that options precede variable arguments.
  • Adopted git config list -z format (key\nvalue\0) for git var -l -z to prevent ambiguity with = in config keys.
  • Delimited multi-valued variable outputs (e.g. GIT_CONFIG_GLOBAL) with NUL bytes under -z.
  • Replaced char part in ident_part() with enum ident_part.
  • Split synopsis in Documentation/git-var.adoc into separate lines for -l and <variable>..., and removed awkward legacy phrasing ("of a piece of code").
  • Added tests in t/t0007-git-var.sh covering the new -z format, multi-valued -z, and argument ordering.

Changes since v2:

  • Drop git ident / git whoami subcommand entirely.
  • Add GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and GIT_AUTHOR_DATE.
  • Add GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, and GIT_COMMITTER_DATE.
  • Add GIT_SIGNING_KEY to resolve commit signing keys.
  • Teach git var to accept multiple variable arguments (git var <var1> <var2> ...).
  • Add -z option to terminate outputs with NUL bytes (including git var -l -z).
  • Update Documentation/git-var.adoc and t/t0007-git-var.sh.

CC: "brian m. carlson" [email protected], Jeff King [email protected], Junio C Hamano [email protected]
cc: Ben Knoble [email protected]
cc: Phillip Wood [email protected]

@gitgitgadget-git

Copy link
Copy Markdown

Welcome to GitGitGadget

Hi @anpl1623, and welcome to GitGitGadget, the GitHub App to send patch series to the Git mailing list from GitHub Pull Requests.

Please make sure that either:

  • Your Pull Request has a good description, if it consists of multiple commits, as it will be used as cover letter.
  • Your Pull Request description is empty, if it consists of a single commit, as the commit message should be descriptive enough by itself.

You can CC potential reviewers by adding a footer to the PR description with the following syntax:

CC: Revi Ewer <[email protected]>, Ill Takalook <[email protected]>

NOTE: DO NOT copy/paste your CC list from a previous GGG PR's description,
because it will result in a malformed CC list on the mailing list. See
example.

Also, it is a good idea to review the commit messages one last time, as the Git project expects them in a quite specific form:

  • the lines should not exceed 76 columns,
  • the first line should be like a header and typically start with a prefix like "tests:" or "revisions:" to state which subsystem the change is about, and
  • the commit messages' body should be describing the "why?" of the change.
  • Finally, the commit messages should end in a Signed-off-by: line matching the commits' author.

It is in general a good idea to await the automated test ("Checks") in this Pull Request before contributing the patches, e.g. to avoid trivial issues such as unportable code.

Contributing the patches

Before you can contribute the patches, your GitHub username needs to be added to the list of permitted users. Any already-permitted user can do that, by adding a comment to your PR of the form /allow. A good way to find other contributors is to locate recent pull requests where someone has been /allowed:

Both the person who commented /allow and the PR author are able to /allow you.

An alternative is the channel #git-devel on the Libera Chat IRC network:

<newcontributor> I've just created my first PR, could someone please /allow me? https://github.com/gitgitgadget/git/pull/12345
<veteran> newcontributor: it is done
<newcontributor> thanks!

Once on the list of permitted usernames, you can contribute the patches to the Git mailing list by adding a PR comment /submit.

If you want to see what email(s) would be sent for a /submit request, add a PR comment /preview to have the email(s) sent to you. You must have a public GitHub email address for this. Note that any reviewers CC'd via the list in the PR description will not actually be sent emails.

After you submit, GitGitGadget will respond with another comment that contains the link to the cover letter mail in the Git mailing list archive. Please make sure to monitor the discussion in that thread and to address comments and suggestions (while the comments and suggestions will be mirrored into the PR by GitGitGadget, you will still want to reply via mail).

If you do not want to subscribe to the Git mailing list just to be able to respond to a mail, you can download the mbox from the Git mailing list archive (click the (raw) link), then import it into your mail program. If you use GMail, you can do this via:

curl -g --user "<EMailAddress>:<Password>" \
    --url "imaps://imap.gmail.com/INBOX" -T /path/to/raw.txt

To iterate on your change, i.e. send a revised patch or patch series, you will first want to (force-)push to the same branch. You probably also want to modify your Pull Request description (or title). It is a good idea to summarize the revision by adding something like this to the cover letter (read: by editing the first comment on the PR, i.e. the PR description):

Changes since v1:
- Fixed a typo in the commit message (found by ...)
- Added a code comment to ... as suggested by ...
...

To send a new iteration, just add another PR comment with the contents: /submit.

Need help?

New contributors who want advice are encouraged to join [email protected], where volunteers who regularly contribute to Git are willing to answer newbie questions, give advice, or otherwise provide mentoring to interested contributors. You must join in order to post or view messages, but anyone can join.

You may also be able to find help in real time in the developer IRC channel, #git-devel on Libera Chat. Remember that IRC does not support offline messaging, so if you send someone a private message and log out, they cannot respond to you. The scrollback of #git-devel is archived, though.

@gitgitgadget-git

Copy link
Copy Markdown

There is an issue in commit d12cc4a:
builtin/whoami: add new 'whoami' command

  • Commit not signed off

@gitgitgadget-git

Copy link
Copy Markdown

There is an issue in commit e1bb825:
builtin/whoami: add new 'whoami' command

  • Commit not signed off

@anpl1623
anpl1623 force-pushed the master branch 2 times, most recently from 8c4bcfb to 263aebc Compare August 25, 2026 00:22
@Ikke

Ikke commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

/allow

@gitgitgadget-git

Copy link
Copy Markdown

User anpl1623 is now allowed to use GitGitGadget.

@Timo7670

Copy link
Copy Markdown

Sleepwalk

@anpl1623

Copy link
Copy Markdown
Author

/submit

@gitgitgadget-git

Copy link
Copy Markdown

Submitted as [email protected]

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-git-2388/anpl1623/master-v1

To fetch this version to local tag pr-git-2388/anpl1623/master-v1:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-git-2388/anpl1623/master-v1

@gitgitgadget-git

Copy link
Copy Markdown

There is a merge commit in this Pull Request:

c3159a51d21dacce0660e84e2659ff2afb0cf3f8

Please rebase the branch and force-push.

@gitgitgadget-git

Copy link
Copy Markdown

There is a merge commit in this Pull Request:

c3159a51d21dacce0660e84e2659ff2afb0cf3f8

Please rebase the branch and force-push.

@gitgitgadget-git

Copy link
Copy Markdown

"brian m. carlson" wrote on the Git mailing list (how to reply to this email):

On 2026-08-25 at 20:46:42, Andrew Pleeter via GitGitGadget wrote:
> From: anpl1623 <[email protected]>

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

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

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

user.author.name=A U Thor
user.author.email=[email protected]

Other formats are possible, though.

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

> Include documentation in Documentation/git-whoami.adoc and regression
> tests in t/t0015-whoami.sh.
> 
> Signed-off-by: anpl1623 <[email protected]>

Again, you'll want to sign this off with your real name.

>     MOTIVATION
>     
>     Users often work across multiple environments, profiles, or repositories
>     with different global/local configs and signing keys. Currently,
>     verifying what identity and signing key will be attached to a new commit
>     requires checking several individual git config and git var settings.
>     git whoami provides a simple, direct porcelain command to verify this in
>     one step.

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

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

> +	repo_config(the_repository, git_default_config, NULL);

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

> +	repo_config_get_bool(the_repository, "commit.gpgsign", &gpgsign);
> +	repo_config_get_string(the_repository, "user.signingkey", &signing_key);
> +	repo_config_get_string(the_repository, "gpg.format", &gpg_format);
> +	repo_config_get_string(the_repository, "gpg.ssh.defaultkeycommand", &ssh_default_key_cmd);
> +
> +	is_ssh = gpg_format && !strcmp(gpg_format, "ssh");
> +
> +	if (signing_key && *signing_key) {
> +		resolved_key = xstrdup(signing_key);
> +	} else if (is_ssh) {
> +		if (ssh_default_key_cmd && *ssh_default_key_cmd)
> +			resolved_key = get_signing_key_id();
> +	} else if (gpgsign) {
> +		resolved_key = get_signing_key_id();
> +	}

Should this also do something useful for X.509 keys?

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

Perhaps others think this is compelling, though, so I'm interested to
hear other opinions about the utility of the command.
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

@gitgitgadget-git

Copy link
Copy Markdown

User "brian m. carlson" <[email protected]> has been added to the cc: list.

@anpl1623

Copy link
Copy Markdown
Author

"brian m. carlson" wrote on the Git mailing list (how to reply to this email):

On 2026-08-25 at 20:46:42, Andrew Pleeter via GitGitGadget wrote:
> From: anpl1623 <[email protected]>

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

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

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

user.author.name=A U Thor
[email protected]

Other formats are possible, though.

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

> Include documentation in Documentation/git-whoami.adoc and regression
> tests in t/t0015-whoami.sh.
> 
> Signed-off-by: anpl1623 <[email protected]>

Again, you'll want to sign this off with your real name.

>     MOTIVATION
>     
>     Users often work across multiple environments, profiles, or repositories
>     with different global/local configs and signing keys. Currently,
>     verifying what identity and signing key will be attached to a new commit
>     requires checking several individual git config and git var settings.
>     git whoami provides a simple, direct porcelain command to verify this in
>     one step.

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

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

> +	repo_config(the_repository, git_default_config, NULL);

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

> +	repo_config_get_bool(the_repository, "commit.gpgsign", &gpgsign);
> +	repo_config_get_string(the_repository, "user.signingkey", &signing_key);
> +	repo_config_get_string(the_repository, "gpg.format", &gpg_format);
> +	repo_config_get_string(the_repository, "gpg.ssh.defaultkeycommand", &ssh_default_key_cmd);
> +
> +	is_ssh = gpg_format && !strcmp(gpg_format, "ssh");
> +
> +	if (signing_key && *signing_key) {
> +		resolved_key = xstrdup(signing_key);
> +	} else if (is_ssh) {
> +		if (ssh_default_key_cmd && *ssh_default_key_cmd)
> +			resolved_key = get_signing_key_id();
> +	} else if (gpgsign) {
> +		resolved_key = get_signing_key_id();
> +	}

Should this also do something useful for X.509 keys?

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

Perhaps others think this is compelling, though, so I'm interested to
hear other opinions about the utility of the command.
-- 
brian m. carlson (they/them)
Toronto, Ontario, CA

Hi Brian,

Thank you for the detailed feedback! I have updated the patch to address all of your points:

  1. Real Name & Sign-off:
    Updated the commit author and Signed-off-by trailer to use my real name:
    "Andrew Pleeter [email protected]".

  2. Machine-Readable & NUL-Terminated Output:
    Added a --porcelain option that outputs key-value pairs in the user.author.*,
    user.committer.*, user.signingkey, gpg.format, and commit.gpgsign namespace,
    as well as -z (--null) for NUL-terminated output across --porcelain and single-field
    selectors.

  3. Motivation in Commit Message:
    Moved the motivation into the commit message, detailing why 'git whoami' provides
    a cleaner, unified porcelain interface compared to querying 'git var' / 'git config'
    separately.

  4. Removal of the_repository:
    Removed '#define USE_THE_REPOSITORY_VARIABLE' and refactored 'builtin/whoami.c' to use
    the 'repo' argument with 'repo_config(repo, ...)', cleanly handling non-repository
    invocations.

  5. Signing Key Resolution & X.509:
    Documented the explicit resolution order across OpenPGP, X.509, and SSH.

All regression tests in t/t0015-whoami.sh and documentation linters pass.

@gitgitgadget-git

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Andrew Pleeter via GitGitGadget" <[email protected]> writes:

>     builtin/whoami: add new 'whoami' command

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

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

This pair hints the users they are equals.

But the code tells us otherwise:

> +	if (show_name) {
> +		if (show_author)
> +			puts(author_name.buf);
> +		else
> +			puts(committer_name.buf);
> +		goto cleanup;
> +	}
> +

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

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

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

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

So confusing.

> +`-n`::
> +`--name`::
> +	Show name only.
> +
> +`-e`::
> +`--email`::
> +	Show email only.

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

    $ git ident -a -e -n
    Andrew Pleeter <[email protected]>
    $ git ident -a -n -v
    Author: Andrew Pleeter
    $ git ident -a -c -e
    <[email protected]>
    <[email protected]>
    $ git ident -a -c -e -v
    Author: <[email protected]>
    Committer: <[email protected]>

@anpl1623
anpl1623 force-pushed the master branch 2 times, most recently from dae4823 to f322e7f Compare August 31, 2026 22:43
@gitgitgadget-git

Copy link
Copy Markdown

There is a merge commit in this Pull Request:

f47f45b72d0dfa22846e815dfc44a3e7cfc87dde

Please rebase the branch and force-push.

@gitgitgadget-git

Copy link
Copy Markdown

There is a merge commit in this Pull Request:

f47f45b72d0dfa22846e815dfc44a3e7cfc87dde

Please rebase the branch and force-push.

@anpl1623

Copy link
Copy Markdown
Author

/submit

@gitgitgadget-git

Copy link
Copy Markdown

Submitted as [email protected]

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-git-2388/anpl1623/master-v2

To fetch this version to local tag pr-git-2388/anpl1623/master-v2:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-git-2388/anpl1623/master-v2

@anpl1623

Copy link
Copy Markdown
Author

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

Hi Junio,

Thank you for the review and guidance!

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

My apologies for not adopting your grouped suggestion sooner. In v7, we have replaced the individual entries with your grouped definitions in Documentation/git-var.adoc:

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

(and similarly for `GIT_COMMITTER_*`).

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

With the way committer_date() and author_date() are called (below), can
fmt_ident() ever return an ident without datestamp and timezone,
requiring us to fall back on NULL returns like this? You are not
passing IDENT_NO_DATE flag anywhere, are you?

Following up on your due diligence suggestion: we audited fmt_ident() and split_ident_line(). When called without IDENT_NO_DATE (as is done for all ident variables here), fmt_ident() always produces date and timezone fields.

In v7, we added explicit defensive assertions:

  • In IDENT_NAME: BUG("split_ident_line() gave NULL names???") if either pointer is NULL.
  • In IDENT_MAIL: BUG("split_ident_line() gave NULL mail???") if either pointer is NULL.
  • In IDENT_DATE: asserted !split.date_begin || !split.tz_end with BUG("split_ident_line() gave NULL date/tz???"), and simplified extraction to xmemdupz(split.date_begin, split.tz_end - split.date_begin).

However, I am not sure why we want an extra delimiter only after a
multi-valued variable. Does it mean that the reading script needs
to be aware of which variables are multi-valued and which are not?
...
If I were designing this, I'd rather (1) get rid of the "empty line"
convention for multi-valued variables, and (2) model multi-variable
mode more after "-l" mode. IOW, instead of thinking of the case
where the user gave us two variables like two "git var VARIBLE$N"
calls given back to back, thinking it more like "git var -l | grep
-e VARIABLE1= -e VARIABLE2=".

Regarding the multi-variable design and delimiters:

  1. Why values-only instead of key=val:
    The primary motivation for querying multiple variables (e.g. git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL) was to allow scripts to easily unpack values directly without stripping variable names, e.g.:

    read -r name email < <(git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL)
    

    or with -z:

    { IFS= read -r -d '' name; IFS= read -r -d '' email; } < <(git var -z GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL)
    

    If git var <var1> <var2> emitted VARIABLE=value, callers would need an extra stripping step, reducing the convenience over git var -l.

  2. Stream disambiguation for multi-valued variables:
    Because single-valued variables emit exactly one delimiter, a reader querying only single-valued variables knows that line 1 is variable 1, line 2 is variable 2, etc.

    When multi-valued variables are mixed in, an extra delimiter is appended after the list so that the stream remains deterministic:

    • A variable with 0 values emits an empty record (a lone delimiter).
    • A multi-valued variable with N values emits N delimited records followed by an extra delimiter marking the end of that list.

    A consumer querying git var -z GIT_AUTHOR_NAME GIT_CONFIG_GLOBAL GIT_AUTHOR_EMAIL can thus read:

    • 1 record for author name.
    • 0 or more records until an empty record marks the end of GIT_CONFIG_GLOBAL.
    • 1 record for author email.

    git var -l (and git var -l -z) remains the canonical interface if callers want key=value pairs. However, if the project consensus is that multi-argument queries should instead act as filters on git var -l (e.g., git var -l VAR1 VAR2 printing VAR1=val1\nVAR2=val2\n), we are happy to adapt to that model!

Thanks,
Andrew

@anpl1623

Copy link
Copy Markdown
Author

/submit

@gitgitgadget-git

Copy link
Copy Markdown

Submitted as [email protected]

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-git-2388/anpl1623/master-v7

To fetch this version to local tag pr-git-2388/anpl1623/master-v7:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-git-2388/anpl1623/master-v7

@dscho

dscho commented Sep 10, 2026

Copy link
Copy Markdown
Member

@anpl1623 please note that none of your replies in this PR will ever reach the intended recipient, as the welcome message said:

Please make sure to monitor the discussion in that thread and to address comments and suggestions (while the comments and suggestions will be mirrored into the PR by GitGitGadget, you will still want to reply via mail).

Your PR comments are not mirrored to the Git mailing list. Neither Phillip nor Junio got your messages.

@gitgitgadget-git

Copy link
Copy Markdown

This patch series was integrated into seen via a6f2bea.

@gitgitgadget-git

Copy link
Copy Markdown

This branch is now known as ap/var-broken-down-idents.

@gitgitgadget-git

Copy link
Copy Markdown

There was a status update in the "Cooking" section about the branch ap/var-broken-down-idents on the Git mailing list:

Needs review.
source: <[email protected]>

@gitgitgadget-git

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Andrew Pleeter via GitGitGadget" <[email protected]> writes:

>  DESCRIPTION
>  -----------
> +Prints Git logical variables. Exits with code 1 if any requested
> +variable has no value. When multiple variables are requested, an empty
> +record (a blank line, or an empty NUL-terminated record when `-z` is given)
> +is printed for any variable that has no value, and the command continues
> +processing the remaining variables.

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

>  int cmd_var(int argc,
> ...
> +	term = nul_term ? '\0' : '\n';
> +
> +	for (i = 0; i < argc; i++) {
> +		const struct git_var *git_var = get_git_var(argv[i]);
>  
> +		if (!git_var)
> +			usage_with_options(var_usage, options);
>  
> +		if (git_var->read) {
> +			char *val = git_var->read(IDENT_STRICT);
> +
> +			if (!val) {
> +				if (argc == 1)
> +					return 1;
> +				putc(term, stdout);
> +				continue;
> +			}
> +			printf("%s%c", val, term);
> +			free(val);
> +		} else {
> +			struct string_list list = STRING_LIST_INIT_DUP;
> +			size_t j;
> +
> +			git_var->multiread(&list);
> +			if (argc == 1 && !list.nr) {
> +				string_list_clear(&list, 0);
> +				return 1;
> +			}
> +			for (j = 0; j < list.nr; j++)
> +				printf("%s%c", list.items[j].string, term);
> +			if (argc > 1)
> +				putc(term, stdout);
> +			string_list_clear(&list, 0);
> +		}
> +	}
>  
>  	return 0;
>  }

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

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

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

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

Thanks.

@gitgitgadget-git

Copy link
Copy Markdown

There was a status update in the "Cooking" section about the branch ap/var-broken-down-idents on the Git mailing list:

The 'git var' command has been extended to expose individual
identity components ('GIT_AUTHOR_NAME', etc.) and the commit
signing key, and can now accept multiple variables to query at
once, safely formatting the output with a new '-z' option.

Needs review.
source: <[email protected]>

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

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

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

Signed-off-by: Andrew Pleeter <[email protected]>
@anpl1623

Copy link
Copy Markdown
Author

/submit

@gitgitgadget-git

Copy link
Copy Markdown

Submitted as [email protected]

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-git-2388/anpl1623/master-v8

To fetch this version to local tag pr-git-2388/anpl1623/master-v8:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-git-2388/anpl1623/master-v8

@gitgitgadget-git

Copy link
Copy Markdown

Junio C Hamano wrote on the Git mailing list (how to reply to this email):

"Andrew Pleeter via GitGitGadget" <[email protected]> writes:

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

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

I would say this should be split into at least 3 patches.

 (1) Add "-z" output mode.

     To allow reading scripts to unambiguously parse output from
     "git var (-l | <var>)" command, implement a NUL terminated
     output mode, similar to how "git config -l -z" shows list of
     configuration variables and their values.  When showing the
     value of a single variable this only makes difference for
     variable with multiple values, but in the next step in this
     series, we will introduce a mode where multiple variables are
     queried.

 (2) Add (2 <= argc) mode that displays like "var -l" mode on top.

     To allow reading values for multiple variables with a single
     command invocation, teach "var" to take more than one variable,
     and show output the same way as "git var -l [-z]", giving list
     of "var=value" but only for variables requested by the user.

 (3) Add new variables.

     Scripts reading from "git var GIT_AUTHOR_IDENT" needs to parse
     the output if they want to extract only the author name.  

     To allow scripts to easily access broken-out fields of
     GIT_{AUTHOR,COMMITTER}_IDENT, add a GIT_AUTHOR_NAME variable
     and its friends, as well as GIT_SIGNING_KEY.

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

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

Thanks.

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

@gitgitgadget-git

Copy link
Copy Markdown

This patch series is no longer integrated into seen.

@gitgitgadget-git gitgitgadget-git Bot removed the seen label Sep 15, 2026
@gitgitgadget-git

Copy link
Copy Markdown

This patch series was integrated into seen via 94eff80.

@gitgitgadget-git

Copy link
Copy Markdown

Andrew Pleeter wrote on the Git mailing list (how to reply to this email):

Hi Junio,

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

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

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

1. Add the -z output mode to git var (-l and single-variable).
2. Teach git var to accept multiple variables (argc >= 2) with var=value
   output.
3. Expose the broken-out identity components and GIT_SIGNING_KEY.

I will send v9 shortly. Thank you again for your patience and direction.

Best regards,
Andrew

@gitgitgadget-git

Copy link
Copy Markdown

There was a status update in the "Cooking" section about the branch ap/var-broken-down-idents on the Git mailing list:

The 'git var' command has been extended to expose individual
identity components ('GIT_AUTHOR_NAME', etc.) and the commit
signing key, and can now accept multiple variables to query at
once, safely formatting the output with a new '-z' option.

Expecting a reroll.
cf. <[email protected]>
source: <[email protected]>

@gitgitgadget-git

Copy link
Copy Markdown

There was a status update in the "Cooking" section about the branch ap/var-broken-down-idents on the Git mailing list:

The 'git var' command has been extended to expose individual
identity components ('GIT_AUTHOR_NAME', etc.) and the commit
signing key, and can now accept multiple variables to query at
once, safely formatting the output with a new '-z' option.

Expecting a reroll.
cf. <[email protected]>
source: <[email protected]>

@gitgitgadget-git

Copy link
Copy Markdown

Phillip Wood wrote on the Git mailing list (how to reply to this email):

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

Ah, that explains the silence! I had a quick look at the PR comments but did not notice any response to my question about the use of GIT_SIGNING_KEY.
> I certainly did not intend to be silent or ignore
> your reviews.
> > I completely agree with your feedback. Packing all of these features into
> a single commit makes the patch difficult to review and overcomplicates
> the history.
> > I will restructure the series for v9 into three separate patches as you
> suggested:
> > 1. Add the -z output mode to git var (-l and single-variable).
> 2. Teach git var to accept multiple variables (argc >= 2) with var=value
>     output.
> 3. Expose the broken-out identity components and GIT_SIGNING_KEY.

That sounds like a good direction

Thanks

Phillip

> > I will send v9 shortly. Thank you again for your patience and direction.
> > Best regards,
> Andrew

@gitgitgadget-git

Copy link
Copy Markdown

There was a status update in the "Cooking" section about the branch ap/var-broken-down-idents on the Git mailing list:

The 'git var' command has been extended to expose individual
identity components ('GIT_AUTHOR_NAME', etc.) and the commit
signing key, and can now accept multiple variables to query at
once, safely formatting the output with a new '-z' option.

Expecting a reroll.
cf. <[email protected]>
source: <[email protected]>

@gitgitgadget-git

Copy link
Copy Markdown

Andrew Pleeter wrote on the Git mailing list (how to reply to this email):

Hi Phillip,

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

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

  gpg.format=openpgp, user.signingkey unset
    $ git var GIT_SIGNING_KEY
    C O Mitter <[email protected]>

  gpg.format=openpgp, user.signingkey=DEADBEEF
    $ git var GIT_SIGNING_KEY
    DEADBEEF

  gpg.format=ssh, user.signingkey=/path/to/key.pub
    $ git var GIT_SIGNING_KEY
    /path/to/key.pub

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

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

The string_list conversion is split out as you suggested:

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

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

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

  $ git var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL
  GIT_AUTHOR_IDENT=A U Thor <[email protected]> 1112912053 -0700
  GIT_CONFIG_GLOBAL=/home/me/.config/git/config
  GIT_CONFIG_GLOBAL=/home/me/.gitconfig

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

Thanks,
Andrew

@gitgitgadget-git

Copy link
Copy Markdown

Andrew Pleeter wrote on the Git mailing list (how to reply to this email):

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

v8 was one commit doing four things at once. It is now four patches:

  1/4 converts the internal representation of a multi-valued variable
      from a newline-joined string to a string_list, with no change in
      output. Phillip suggested doing this as its own preparatory step.

  2/4 adds "-z", which is what forces the switch to parse_options(), so
      that conversion lives here rather than in 1/4.

  3/4 teaches "git var" to take more than one variable.

  4/4 adds GIT_AUTHOR_NAME and friends.

Changes since v8:

 * GIT_SIGNING_KEY is dropped. Phillip asked three times how it was
   meant to be used, and once I looked properly the answer was that the
   value cannot be interpreted without also reading gpg.format, and that
   in the default configuration it is a committer ident rather than a
   key at all. I would rather leave it out than define a variable I
   cannot describe. Details are in my reply to him on this thread.

 * Asking for several variables no longer exits non-zero just because
   one of them has no value; such a variable is left out of the output
   and the rest are still shown. A non-zero status is now reserved for
   real errors, such as naming a variable that does not exist, so
   callers can detect those from the exit code. A single variable still
   exits 1 when it has no value, as before. This is Phillip's
   GIT_CONFIG_NOSYSTEM point.

 * GIT_CONFIG_GLOBAL is documented as a variable that can have more than
   one value.

 * The commit messages are prose rather than a list of bullet points,
   and no longer narrate how the patch was developed.

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

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

Andrew Pleeter (4):
  var: represent multi-valued variables with a string_list
  var: add "-z" output mode
  var: accept more than one variable
  var: add broken-out identity variables

 Documentation/git-var.adoc |  68 +++++++++--
 builtin/var.c              | 242 +++++++++++++++++++++++++++++--------
 t/t0007-git-var.sh         | 153 +++++++++++++++++++++++
 3 files changed, 403 insertions(+), 60 deletions(-)

-- 
2.54.0 (Apple Git-157)

@gitgitgadget-git

Copy link
Copy Markdown

Andrew Pleeter wrote on the Git mailing list (how to reply to this email):

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

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

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

Signed-off-by: Andrew Pleeter <[email protected]>
---
 Documentation/git-var.adoc |  1 +
 builtin/var.c              | 79 +++++++++++++++++++++-----------------
 2 files changed, 44 insertions(+), 36 deletions(-)

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index 697c10aded..de3007732d 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -83,6 +83,7 @@ endif::git-default-pager[]
 
 `GIT_CONFIG_GLOBAL`::
     The path to the global (per-user) configuration files, if any.
+    This variable can have multiple values.
 
 Most path values contain only one value. However, some can contain multiple
 values, which are separated by newlines, and are listed in order from highest to
diff --git a/builtin/var.c b/builtin/var.c
index cc3a43cde2..9f7c8a6113 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -14,10 +14,11 @@
 #include "environment.h"
 #include "ident.h"
 #include "pager.h"
-#include "refs.h"
 #include "path.h"
-#include "strbuf.h"
+#include "refs.h"
 #include "run-command.h"
+#include "strbuf.h"
+#include "string-list.h"
 
 static const char var_usage[] = "git var (-l | <variable>)";
 
@@ -90,35 +91,27 @@ static char *git_config_val_system(int ident_flag UNUSED)
 	return NULL;
 }
 
-static char *git_config_val_global(int ident_flag UNUSED)
+static void git_config_val_global(struct string_list *list)
 {
-	struct strbuf buf = STRBUF_INIT;
 	char *user, *xdg;
-	size_t unused;
 
 	git_global_config_paths(&user, &xdg);
 	if (xdg && *xdg) {
 		normalize_path_copy(xdg, xdg);
-		strbuf_addf(&buf, "%s\n", xdg);
+		string_list_append(list, xdg);
 	}
 	if (user && *user) {
 		normalize_path_copy(user, user);
-		strbuf_addf(&buf, "%s\n", user);
+		string_list_append(list, user);
 	}
 	free(xdg);
 	free(user);
-	strbuf_trim_trailing_newline(&buf);
-	if (buf.len == 0) {
-		strbuf_release(&buf);
-		return NULL;
-	}
-	return strbuf_detach(&buf, &unused);
 }
 
 struct git_var {
 	const char *name;
 	char *(*read)(int);
-	int multivalued;
+	void (*multiread)(struct string_list *);
 };
 static struct git_var git_vars[] = {
 	{
@@ -163,8 +156,7 @@ static struct git_var git_vars[] = {
 	},
 	{
 		.name = "GIT_CONFIG_GLOBAL",
-		.read = git_config_val_global,
-		.multivalued = 1,
+		.multiread = git_config_val_global,
 	},
 	{
 		.name = "",
@@ -175,28 +167,30 @@ static struct git_var git_vars[] = {
 static void list_vars(void)
 {
 	struct git_var *ptr;
-	char *val;
-
-	for (ptr = git_vars; ptr->read; ptr++)
-		if ((val = ptr->read(0))) {
-			if (ptr->multivalued && *val) {
-				struct string_list list = STRING_LIST_INIT_DUP;
-
-				string_list_split(&list, val, "\n", -1);
-				for (size_t i = 0; i < list.nr; i++)
-					printf("%s=%s\n", ptr->name, list.items[i].string);
-				string_list_clear(&list, 0);
-			} else {
+
+	for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
+		if (ptr->read) {
+			char *val = ptr->read(0);
+
+			if (val) {
 				printf("%s=%s\n", ptr->name, val);
+				free(val);
 			}
-			free(val);
+		} else {
+			struct string_list list = STRING_LIST_INIT_DUP;
+
+			ptr->multiread(&list);
+			for (size_t i = 0; i < list.nr; i++)
+				printf("%s=%s\n", ptr->name, list.items[i].string);
+			string_list_clear(&list, 0);
 		}
+	}
 }
 
 static const struct git_var *get_git_var(const char *var)
 {
 	struct git_var *ptr;
-	for (ptr = git_vars; ptr->read; ptr++) {
+	for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
 		if (strcmp(var, ptr->name) == 0) {
 			return ptr;
 		}
@@ -220,7 +214,6 @@ int cmd_var(int argc,
 	    struct repository *repo UNUSED)
 {
 	const struct git_var *git_var;
-	char *val;
 
 	show_usage_if_asked(argc, argv, var_usage);
 	if (argc != 2)
@@ -237,12 +230,26 @@ int cmd_var(int argc,
 	if (!git_var)
 		usage(var_usage);
 
-	val = git_var->read(IDENT_STRICT);
-	if (!val)
-		return 1;
+	if (git_var->read) {
+		char *val = git_var->read(IDENT_STRICT);
+
+		if (!val)
+			return 1;
 
-	printf("%s\n", val);
-	free(val);
+		printf("%s\n", val);
+		free(val);
+	} else {
+		struct string_list list = STRING_LIST_INIT_DUP;
+
+		git_var->multiread(&list);
+		if (!list.nr) {
+			string_list_clear(&list, 0);
+			return 1;
+		}
+		for (size_t i = 0; i < list.nr; i++)
+			printf("%s\n", list.items[i].string);
+		string_list_clear(&list, 0);
+	}
 
 	return 0;
 }
-- 
2.54.0 (Apple Git-157)

@gitgitgadget-git

Copy link
Copy Markdown

Andrew Pleeter wrote on the Git mailing list (how to reply to this email):

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

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

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

Signed-off-by: Andrew Pleeter <[email protected]>
---
 Documentation/git-var.adoc | 32 +++++++++++-------
 builtin/var.c              | 56 +++++++++++++++++++------------
 t/t0007-git-var.sh         | 67 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 123 insertions(+), 32 deletions(-)

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index 9052fbc549..d0772d9743 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -10,17 +10,22 @@ SYNOPSIS
 --------
 [synopsis]
 git var [-z] -l
-git var [-z] <variable>
+git var [-z] <variable>...
 
 DESCRIPTION
 -----------
-Prints a Git logical variable. Exits with code 1 if the variable has
-no value.
+Prints Git logical variables. When a single variable is requested, its
+bare value is printed, and the command exits with code 1 if it has no
+value. When multiple variables are requested, they are printed as
+`VARIABLE=value` pairs, separated by newlines; a variable that has no
+value is omitted from the output, which is not by itself an error.
 
-If `-z` is given, the value is terminated by a NUL byte instead of a
-newline. With `-l`, the variable name and its value are separated by a
-newline, and each entry is terminated by a NUL byte
-(`VARIABLE\nvalue\0`), in the same way as `git config list -z`.
+If `-z` is given, the output format changes depending on the mode:
+
+* With a single variable, the bare value is terminated by a NUL byte.
+* With multiple variables or with `-l`, the variable name and value are
+  separated by a newline, and each entry is terminated by a NUL byte
+  (`VARIABLE\nvalue\0`), in the same way as `git config list -z`.
 
 OPTIONS
 -------
@@ -32,8 +37,9 @@ OPTIONS
 
 `-z`::
 	Terminate entries with NUL instead of newline. When used with
-	`-l`, the variable name and its value are separated by a
-	newline, and each entry is terminated with a NUL byte.
+	`-l` or when multiple variables are requested, the variable name
+	and its value are separated by a newline, and each entry is
+	terminated with a NUL byte.
 
 EXAMPLES
 --------
@@ -97,9 +103,11 @@ endif::git-default-pager[]
     This variable can have multiple values.
 
 Most path values contain only one value. However, some can contain multiple
-values, which are separated by newlines (or NUL bytes if `-z` is given), and are
-listed in order from highest to lowest priority.  Callers should be prepared for
-any such path value to contain multiple items.
+values, which are separated by newlines (or NUL bytes if `-z` is given),
+and are listed in order from highest to lowest priority. When querying
+multiple variables (or using `-l`), each value is output as a separate
+`VARIABLE=value` entry (or `VARIABLE\nvalue\0` with `-z`). Callers should
+be prepared for any such path value to contain multiple items.
 
 Note that paths are printed even if they do not exist, but not if they are
 disabled by other environment variables.
diff --git a/builtin/var.c b/builtin/var.c
index c9691070b8..5fbebc62e2 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -23,7 +23,7 @@
 
 static const char * const var_usage[] = {
 	N_("git var [-z] -l"),
-	N_("git var [-z] <variable>"),
+	N_("git var [-z] <variable>..."),
 	NULL
 };
 
@@ -225,9 +225,9 @@ int cmd_var(int argc,
 	    const char *prefix,
 	    struct repository *repo UNUSED)
 {
-	const struct git_var *git_var;
 	int list = 0;
 	int nul_term = 0;
+	char delim;
 	char term;
 	struct option options[] = {
 		OPT_BOOL('l', NULL, &list,
@@ -248,36 +248,52 @@ int cmd_var(int argc,
 		return 0;
 	}
 
-	if (argc != 1)
+	if (!argc)
 		usage_with_options(var_usage, options);
 
 	repo_config(the_repository, git_default_config, NULL);
 
+	delim = nul_term ? '\n' : '=';
 	term = nul_term ? '\0' : '\n';
 
-	git_var = get_git_var(argv[0]);
-	if (!git_var)
-		usage_with_options(var_usage, options);
+	for (int i = 0; i < argc; i++) {
+		const struct git_var *git_var = get_git_var(argv[i]);
 
-	if (git_var->read) {
-		char *val = git_var->read(IDENT_STRICT);
+		if (!git_var)
+			usage_with_options(var_usage, options);
 
-		if (!val)
-			return 1;
+		if (git_var->read) {
+			char *val = git_var->read(IDENT_STRICT);
 
-		printf("%s%c", val, term);
-		free(val);
-	} else {
-		struct string_list list = STRING_LIST_INIT_DUP;
+			if (!val) {
+				if (argc == 1)
+					return 1;
+				continue;
+			}
+			if (argc == 1)
+				printf("%s%c", val, term);
+			else
+				printf("%s%c%s%c", git_var->name, delim,
+				       val, term);
+			free(val);
+		} else {
+			struct string_list list = STRING_LIST_INIT_DUP;
 
-		git_var->multiread(&list);
-		if (!list.nr) {
+			git_var->multiread(&list);
+			if (argc == 1 && !list.nr) {
+				string_list_clear(&list, 0);
+				return 1;
+			}
+			for (size_t j = 0; j < list.nr; j++) {
+				if (argc == 1)
+					printf("%s%c", list.items[j].string,
+					       term);
+				else
+					printf("%s%c%s%c", git_var->name, delim,
+					       list.items[j].string, term);
+			}
 			string_list_clear(&list, 0);
-			return 1;
 		}
-		for (size_t i = 0; i < list.nr; i++)
-			printf("%s%c", list.items[i].string, term);
-		string_list_clear(&list, 0);
 	}
 
 	return 0;
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 661d0539c0..593610300a 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -306,4 +306,71 @@ test_expect_success 'options must precede variable arguments' '
 	test_must_fail git var GIT_AUTHOR_IDENT -z
 '
 
+test_expect_success 'get multiple variables' '
+	test_tick &&
+	cat >expect <<-EOF &&
+	GIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE
+	GIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+	EOF
+	git var GIT_AUTHOR_IDENT GIT_COMMITTER_IDENT >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables with -z' '
+	test_tick &&
+	printf "GIT_AUTHOR_IDENT\n%sQGIT_COMMITTER_IDENT\n%sQ" \
+		"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE" \
+		"$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE" >expect &&
+	git var -z GIT_AUTHOR_IDENT GIT_COMMITTER_IDENT >actual.raw &&
+	nul_to_q <actual.raw >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables including multi-valued variable' '
+	test_tick &&
+	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
+	cat >expect <<-EOF &&
+	GIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE
+	GIT_CONFIG_GLOBAL=$TRASHDIR/foo/git/config
+	GIT_CONFIG_GLOBAL=$TRASHDIR/.gitconfig
+	GIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+	EOF
+	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
+		git var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL GIT_COMMITTER_IDENT >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multiple variables including multi-valued variable with -z' '
+	test_tick &&
+	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
+	printf "GIT_AUTHOR_IDENT\n%sQGIT_CONFIG_GLOBAL\n%sQGIT_CONFIG_GLOBAL\n%sQ" \
+		"$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE" \
+		"$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expect &&
+	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" \
+		git var -z GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL >actual.raw &&
+	nul_to_q <actual.raw >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'variable without a value is omitted but is not an error' '
+	test_tick &&
+	cat >expect <<-EOF &&
+	GIT_AUTHOR_IDENT=$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> $GIT_AUTHOR_DATE
+	GIT_COMMITTER_IDENT=$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
+	EOF
+	test_env GIT_CONFIG_GLOBAL= \
+		git var GIT_AUTHOR_IDENT GIT_CONFIG_GLOBAL GIT_COMMITTER_IDENT >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'a single variable without a value still exits with 1' '
+	test_env GIT_CONFIG_GLOBAL= test_expect_code 1 git var GIT_CONFIG_GLOBAL >out &&
+	test_must_be_empty out
+'
+
+test_expect_success 'unknown variable is a usage error' '
+	test_must_fail git var GIT_AUTHOR_IDENT NO_SUCH_VARIABLE 2>err &&
+	test_grep usage err
+'
+
 test_done
-- 
2.54.0 (Apple Git-157)

@gitgitgadget-git

Copy link
Copy Markdown

Andrew Pleeter wrote on the Git mailing list (how to reply to this email):

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

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

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

Signed-off-by: Andrew Pleeter <[email protected]>
---
 Documentation/git-var.adoc | 19 ++++++++---
 builtin/var.c              | 67 +++++++++++++++++++++++++++-----------
 t/t0007-git-var.sh         | 30 +++++++++++++++++
 3 files changed, 93 insertions(+), 23 deletions(-)

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index de3007732d..9052fbc549 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -9,13 +9,19 @@ git-var - Show a Git logical variable
 SYNOPSIS
 --------
 [synopsis]
-git var (-l | <variable>)
+git var [-z] -l
+git var [-z] <variable>
 
 DESCRIPTION
 -----------
 Prints a Git logical variable. Exits with code 1 if the variable has
 no value.
 
+If `-z` is given, the value is terminated by a NUL byte instead of a
+newline. With `-l`, the variable name and its value are separated by a
+newline, and each entry is terminated by a NUL byte
+(`VARIABLE\nvalue\0`), in the same way as `git config list -z`.
+
 OPTIONS
 -------
 `-l`::
@@ -24,6 +30,11 @@ OPTIONS
 	as well. (However, the configuration variables listing functionality
 	is deprecated in favor of `git config list`.)
 
+`-z`::
+	Terminate entries with NUL instead of newline. When used with
+	`-l`, the variable name and its value are separated by a
+	newline, and each entry is terminated with a NUL byte.
+
 EXAMPLES
 --------
 	$ git var GIT_AUTHOR_IDENT
@@ -86,9 +97,9 @@ endif::git-default-pager[]
     This variable can have multiple values.
 
 Most path values contain only one value. However, some can contain multiple
-values, which are separated by newlines, and are listed in order from highest to
-lowest priority.  Callers should be prepared for any such path value to contain
-multiple items.
+values, which are separated by newlines (or NUL bytes if `-z` is given), and are
+listed in order from highest to lowest priority.  Callers should be prepared for
+any such path value to contain multiple items.
 
 Note that paths are printed even if they do not exist, but not if they are
 disabled by other environment variables.
diff --git a/builtin/var.c b/builtin/var.c
index 9f7c8a6113..c9691070b8 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -14,13 +14,18 @@
 #include "environment.h"
 #include "ident.h"
 #include "pager.h"
+#include "parse-options.h"
 #include "path.h"
 #include "refs.h"
 #include "run-command.h"
 #include "strbuf.h"
 #include "string-list.h"
 
-static const char var_usage[] = "git var (-l | <variable>)";
+static const char * const var_usage[] = {
+	N_("git var [-z] -l"),
+	N_("git var [-z] <variable>"),
+	NULL
+};
 
 static char *committer(int ident_flag)
 {
@@ -164,16 +169,18 @@ static struct git_var git_vars[] = {
 	},
 };
 
-static void list_vars(void)
+static void list_vars(int nul_term)
 {
 	struct git_var *ptr;
+	char delim = nul_term ? '\n' : '=';
+	char term = nul_term ? '\0' : '\n';
 
 	for (ptr = git_vars; ptr->read || ptr->multiread; ptr++) {
 		if (ptr->read) {
 			char *val = ptr->read(0);
 
 			if (val) {
-				printf("%s=%s\n", ptr->name, val);
+				printf("%s%c%s%c", ptr->name, delim, val, term);
 				free(val);
 			}
 		} else {
@@ -181,7 +188,8 @@ static void list_vars(void)
 
 			ptr->multiread(&list);
 			for (size_t i = 0; i < list.nr; i++)
-				printf("%s=%s\n", ptr->name, list.items[i].string);
+				printf("%s%c%s%c", ptr->name, delim,
+				       list.items[i].string, term);
 			string_list_clear(&list, 0);
 		}
 	}
@@ -201,34 +209,55 @@ static const struct git_var *get_git_var(const char *var)
 static int show_config(const char *var, const char *value,
 		       const struct config_context *ctx, void *cb)
 {
+	int *nul_term = cb;
+	char delim = *nul_term ? '\n' : '=';
+	char term = *nul_term ? '\0' : '\n';
+
 	if (value)
-		printf("%s=%s\n", var, value);
+		printf("%s%c%s%c", var, delim, value, term);
 	else
-		printf("%s\n", var);
+		printf("%s%c", var, term);
 	return git_default_config(var, value, ctx, cb);
 }
 
 int cmd_var(int argc,
 	    const char **argv,
-	    const char *prefix UNUSED,
+	    const char *prefix,
 	    struct repository *repo UNUSED)
 {
 	const struct git_var *git_var;
-
-	show_usage_if_asked(argc, argv, var_usage);
-	if (argc != 2)
-		usage(var_usage);
-
-	if (strcmp(argv[1], "-l") == 0) {
-		repo_config(the_repository, show_config, NULL);
-		list_vars();
+	int list = 0;
+	int nul_term = 0;
+	char term;
+	struct option options[] = {
+		OPT_BOOL('l', NULL, &list,
+			 N_("list all variables")),
+		OPT_BOOL('z', NULL, &nul_term,
+			 N_("terminate entries with NUL")),
+		OPT_END(),
+	};
+
+	argc = parse_options(argc, argv, prefix, options,
+			     var_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+
+	if (list) {
+		if (argc)
+			usage_with_options(var_usage, options);
+		repo_config(the_repository, show_config, &nul_term);
+		list_vars(nul_term);
 		return 0;
 	}
+
+	if (argc != 1)
+		usage_with_options(var_usage, options);
+
 	repo_config(the_repository, git_default_config, NULL);
 
-	git_var = get_git_var(argv[1]);
+	term = nul_term ? '\0' : '\n';
+
+	git_var = get_git_var(argv[0]);
 	if (!git_var)
-		usage(var_usage);
+		usage_with_options(var_usage, options);
 
 	if (git_var->read) {
 		char *val = git_var->read(IDENT_STRICT);
@@ -236,7 +265,7 @@ int cmd_var(int argc,
 		if (!val)
 			return 1;
 
-		printf("%s\n", val);
+		printf("%s%c", val, term);
 		free(val);
 	} else {
 		struct string_list list = STRING_LIST_INIT_DUP;
@@ -247,7 +276,7 @@ int cmd_var(int argc,
 			return 1;
 		}
 		for (size_t i = 0; i < list.nr; i++)
-			printf("%s\n", list.items[i].string);
+			printf("%s%c", list.items[i].string, term);
 		string_list_clear(&list, 0);
 	}
 
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 2b60317758..661d0539c0 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -276,4 +276,34 @@ test_expect_success '`git var -l` works even without HOME' '
 	)
 '
 
+test_expect_success 'get a variable with -z' '
+	printf "%sQ" "$(git var GIT_AUTHOR_IDENT)" >expect &&
+	git var -z GIT_AUTHOR_IDENT >actual.raw &&
+	nul_to_q <actual.raw >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'get multi-valued variable with -z' '
+	TRASHDIR="$(test-tool path-utils normalize_path_copy "$(pwd)")" &&
+	HOME="$TRASHDIR" XDG_CONFIG_HOME="$TRASHDIR/foo" git var -z GIT_CONFIG_GLOBAL >actual.raw &&
+	printf "%sQ%sQ" "$TRASHDIR/foo/git/config" "$TRASHDIR/.gitconfig" >expect &&
+	nul_to_q <actual.raw >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git var -l -z' '
+	git var -l -z >actual &&
+	tr "\0" "\n" <actual >actual.lines &&
+	git var GIT_AUTHOR_IDENT >expect &&
+	sed -n "/^GIT_AUTHOR_IDENT$/{n;p;}" actual.lines >actual.author &&
+	test_cmp expect actual.author &&
+	echo false >expect &&
+	sed -n "/^core\.bare$/{n;p;}" actual.lines >actual.bare &&
+	test_cmp expect actual.bare
+'
+
+test_expect_success 'options must precede variable arguments' '
+	test_must_fail git var GIT_AUTHOR_IDENT -z
+'
+
 test_done
-- 
2.54.0 (Apple Git-157)

@gitgitgadget-git

Copy link
Copy Markdown

Andrew Pleeter wrote on the Git mailing list (how to reply to this email):

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

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

Signed-off-by: Andrew Pleeter <[email protected]>
---
 Documentation/git-var.adoc | 36 +++++++++++++--
 builtin/var.c              | 90 ++++++++++++++++++++++++++++++++++++++
 t/t0007-git-var.sh         | 56 ++++++++++++++++++++++++
 3 files changed, 178 insertions(+), 4 deletions(-)

diff --git a/Documentation/git-var.adoc b/Documentation/git-var.adoc
index d0772d9743..6982228f97 100644
--- a/Documentation/git-var.adoc
+++ b/Documentation/git-var.adoc
@@ -43,17 +43,45 @@ OPTIONS
 
 EXAMPLES
 --------
-	$ git var GIT_AUTHOR_IDENT
-	Eric W. Biederman <[email protected]> 1121223278 -0600
+* Get the author identity:
++
+------------
+$ git var GIT_AUTHOR_IDENT
+Eric W. Biederman <[email protected]> 1121223278 -0600
+------------
+
+* Get the author name and email:
++
+------------
+$ git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
+GIT_AUTHOR_NAME=Eric W. Biederman
+GIT_AUTHOR_EMAIL=[email protected]
+------------
 
 
 VARIABLES
 ---------
 `GIT_AUTHOR_IDENT`::
-    The author of a piece of code.
+`GIT_AUTHOR_NAME`::
+`GIT_AUTHOR_EMAIL`::
+`GIT_AUTHOR_DATE`::
+    The authorship information that would be recorded in the
+    resulting commit object if you ran `git commit` right now.
+    `GIT_AUTHOR_IDENT` consists of the author's name, e-mail
+    address, and timestamp+timezone. These three pieces of
+    information are available separately as `GIT_AUTHOR_NAME`,
+    `GIT_AUTHOR_EMAIL`, and `GIT_AUTHOR_DATE`.
 
 `GIT_COMMITTER_IDENT`::
-    The person who put a piece of code into Git.
+`GIT_COMMITTER_NAME`::
+`GIT_COMMITTER_EMAIL`::
+`GIT_COMMITTER_DATE`::
+    The committer information that would be recorded in the
+    resulting commit object if you ran `git commit` right now.
+    `GIT_COMMITTER_IDENT` consists of the committer's name, e-mail
+    address, and timestamp+timezone. These three pieces of
+    information are available separately as `GIT_COMMITTER_NAME`,
+    `GIT_COMMITTER_EMAIL`, and `GIT_COMMITTER_DATE`.
 
 `GIT_EDITOR`::
     Text editor for use by Git commands.  The value is meant to be
diff --git a/builtin/var.c b/builtin/var.c
index 5fbebc62e2..dd4325e5b5 100644
--- a/builtin/var.c
+++ b/builtin/var.c
@@ -27,16 +27,82 @@ static const char * const var_usage[] = {
 	NULL
 };
 
+enum ident_part {
+	IDENT_NAME,
+	IDENT_MAIL,
+	IDENT_DATE,
+};
+
+static char *ident_part(const char *ident, enum ident_part part)
+{
+	struct ident_split split;
+
+	if (!ident)
+		return NULL;
+	if (split_ident_line(&split, ident, strlen(ident)))
+		return NULL;
+
+	switch (part) {
+	case IDENT_NAME:
+		if (!split.name_begin || !split.name_end)
+			BUG("split_ident_line() gave NULL names???");
+		return xmemdupz(split.name_begin,
+				split.name_end - split.name_begin);
+	case IDENT_MAIL:
+		if (!split.mail_begin || !split.mail_end)
+			BUG("split_ident_line() gave NULL mail???");
+		return xmemdupz(split.mail_begin,
+				split.mail_end - split.mail_begin);
+	case IDENT_DATE:
+		if (!split.date_begin || !split.tz_end)
+			BUG("split_ident_line() gave NULL date/tz???");
+		return xmemdupz(split.date_begin,
+				split.tz_end - split.date_begin);
+	default:
+		BUG("unknown ident_part %d", part);
+	}
+}
+
 static char *committer(int ident_flag)
 {
 	return xstrdup_or_null(git_committer_info(ident_flag));
 }
 
+static char *committer_name(int ident_flag)
+{
+	return ident_part(git_committer_info(ident_flag), IDENT_NAME);
+}
+
+static char *committer_email(int ident_flag)
+{
+	return ident_part(git_committer_info(ident_flag), IDENT_MAIL);
+}
+
+static char *committer_date(int ident_flag)
+{
+	return ident_part(git_committer_info(ident_flag), IDENT_DATE);
+}
+
 static char *author(int ident_flag)
 {
 	return xstrdup_or_null(git_author_info(ident_flag));
 }
 
+static char *author_name(int ident_flag)
+{
+	return ident_part(git_author_info(ident_flag), IDENT_NAME);
+}
+
+static char *author_email(int ident_flag)
+{
+	return ident_part(git_author_info(ident_flag), IDENT_MAIL);
+}
+
+static char *author_date(int ident_flag)
+{
+	return ident_part(git_author_info(ident_flag), IDENT_DATE);
+}
+
 static char *editor(int ident_flag UNUSED)
 {
 	return xstrdup_or_null(git_editor());
@@ -123,10 +189,34 @@ static struct git_var git_vars[] = {
 		.name = "GIT_COMMITTER_IDENT",
 		.read = committer,
 	},
+	{
+		.name = "GIT_COMMITTER_NAME",
+		.read = committer_name,
+	},
+	{
+		.name = "GIT_COMMITTER_EMAIL",
+		.read = committer_email,
+	},
+	{
+		.name = "GIT_COMMITTER_DATE",
+		.read = committer_date,
+	},
 	{
 		.name = "GIT_AUTHOR_IDENT",
 		.read = author,
 	},
+	{
+		.name = "GIT_AUTHOR_NAME",
+		.read = author_name,
+	},
+	{
+		.name = "GIT_AUTHOR_EMAIL",
+		.read = author_email,
+	},
+	{
+		.name = "GIT_AUTHOR_DATE",
+		.read = author_date,
+	},
 	{
 		.name = "GIT_EDITOR",
 		.read = editor,
diff --git a/t/t0007-git-var.sh b/t/t0007-git-var.sh
index 593610300a..e43becff1c 100755
--- a/t/t0007-git-var.sh
+++ b/t/t0007-git-var.sh
@@ -373,4 +373,60 @@ test_expect_success 'unknown variable is a usage error' '
 	test_grep usage err
 '
 
+test_expect_success 'get author identity components' '
+	test_tick &&
+	echo "$GIT_AUTHOR_NAME" >expect.name &&
+	echo "$GIT_AUTHOR_EMAIL" >expect.email &&
+	echo "$GIT_AUTHOR_DATE" >expect.date &&
+	git var GIT_AUTHOR_NAME >actual.name &&
+	git var GIT_AUTHOR_EMAIL >actual.email &&
+	git var GIT_AUTHOR_DATE >actual.date &&
+	test_cmp expect.name actual.name &&
+	test_cmp expect.email actual.email &&
+	test_cmp expect.date actual.date
+'
+
+test_expect_success 'get committer identity components' '
+	test_tick &&
+	echo "$GIT_COMMITTER_NAME" >expect.name &&
+	echo "$GIT_COMMITTER_EMAIL" >expect.email &&
+	echo "$GIT_COMMITTER_DATE" >expect.date &&
+	git var GIT_COMMITTER_NAME >actual.name &&
+	git var GIT_COMMITTER_EMAIL >actual.email &&
+	git var GIT_COMMITTER_DATE >actual.date &&
+	test_cmp expect.name actual.name &&
+	test_cmp expect.email actual.email &&
+	test_cmp expect.date actual.date
+'
+
+test_expect_success !FAIL_PREREQS,!AUTOIDENT 'identity components are strict' '
+	(
+		sane_unset GIT_COMMITTER_NAME &&
+		sane_unset GIT_COMMITTER_EMAIL &&
+		test_must_fail git var GIT_COMMITTER_NAME
+	)
+'
+
+test_expect_success 'get several identity components at once' '
+	test_tick &&
+	cat >expect <<-EOF &&
+	GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME
+	GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL
+	GIT_COMMITTER_NAME=$GIT_COMMITTER_NAME
+	GIT_COMMITTER_EMAIL=$GIT_COMMITTER_EMAIL
+	EOF
+	git var GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'git var -l lists the identity components' '
+	git var -l >actual &&
+	test_grep "^GIT_AUTHOR_NAME=" actual &&
+	test_grep "^GIT_AUTHOR_EMAIL=" actual &&
+	test_grep "^GIT_AUTHOR_DATE=" actual &&
+	test_grep "^GIT_COMMITTER_NAME=" actual &&
+	test_grep "^GIT_COMMITTER_EMAIL=" actual &&
+	test_grep "^GIT_COMMITTER_DATE=" actual
+'
+
 test_done
-- 
2.54.0 (Apple Git-157)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants