From f62abd00db3618afff5eb4fd1c23bd7259d8e6d8 Mon Sep 17 00:00:00 2001 From: u_quark Date: Mon, 18 Dec 2023 15:26:55 +0000 Subject: [PATCH] Use environment variables when creating signatures When creating an action signature (e.g. for a commit author and committer) read the following environment variables that can override the configuration options: * `GIT_AUTHOR_NAME` is the human-readable name in the "author" field. * `GIT_AUTHOR_EMAIL` is the email for the "author" field. * `GIT_AUTHOR_DATE` is the timestamp used for the "author" field. * `GIT_COMMITTER_NAME` sets the human name for the "committer" field. * `GIT_COMMITTER_EMAIL` is the email address for the "committer" field. * `GIT_COMMITTER_DATE` is used for the timestamp in the "committer" field. * `EMAIL` is the fallback email address in case the user.email configuration value isn't set. If this isn't set, Git falls back to the system user and host names. This is taken from the git documentation chapter "10.8 Environment Variables": https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables This PR adds support for reading these environment variables by adding two new functions `git_signature_default_author` and `git_signature_default_committer` and deprecates the `git_signature_default` function. Fixes: https://github.com/libgit2/libgit2/issues/3751 Prior work: * https://github.com/libgit2/libgit2/pull/4409 * https://github.com/libgit2/libgit2/pull/5479 * https://github.com/libgit2/libgit2/pull/6290 --- examples/commit.c | 14 ++++-- examples/init.c | 10 ++-- examples/stash.c | 2 +- examples/tag.c | 2 +- include/git2/signature.h | 48 +++++++++++++++++++ src/libgit2/rebase.c | 2 +- src/libgit2/refs.c | 2 +- src/libgit2/signature.c | 73 +++++++++++++++++++++++++++++ src/libgit2/signature.h | 2 +- src/util/date.c | 10 +++- src/util/date.h | 12 +++++ tests/libgit2/commit/signature.c | 80 ++++++++++++++++++++++++++++++++ tests/libgit2/date/date.c | 8 ++++ tests/libgit2/remote/fetch.c | 2 +- tests/libgit2/repo/init.c | 12 +++-- 15 files changed, 258 insertions(+), 21 deletions(-) diff --git a/examples/commit.c b/examples/commit.c index aedc1af7e..1ba4739f0 100644 --- a/examples/commit.c +++ b/examples/commit.c @@ -39,7 +39,7 @@ int lg2_commit(git_repository *repo, int argc, char **argv) git_index *index; git_object *parent = NULL; git_reference *ref = NULL; - git_signature *signature; + git_signature *author_signature, *committer_signature; /* Validate args */ if (argc < 3 || strcmp(opt, "-m") != 0) { @@ -63,21 +63,25 @@ int lg2_commit(git_repository *repo, int argc, char **argv) check_lg2(git_tree_lookup(&tree, repo, &tree_oid), "Error looking up tree", NULL); - check_lg2(git_signature_default(&signature, repo), "Error creating signature", NULL); + check_lg2(git_signature_default_author(&author_signature, repo), + "Error creating author signature", NULL); + check_lg2(git_signature_default_committer(&committer_signature, repo), + "Error creating committer signature", NULL); check_lg2(git_commit_create_v( &commit_oid, repo, "HEAD", - signature, - signature, + author_signature, + committer_signature, NULL, comment, tree, parent ? 1 : 0, parent), "Error creating commit", NULL); git_index_free(index); - git_signature_free(signature); + git_signature_free(author_signature); + git_signature_free(committer_signature); git_tree_free(tree); git_object_free(parent); git_reference_free(ref); diff --git a/examples/init.c b/examples/init.c index 2f681c5ae..4cd55abad 100644 --- a/examples/init.c +++ b/examples/init.c @@ -123,14 +123,15 @@ int lg2_init(git_repository *repo, int argc, char *argv[]) */ static void create_initial_commit(git_repository *repo) { - git_signature *sig; + git_signature *author_sig, *committer_sig; git_index *index; git_oid tree_id, commit_id; git_tree *tree; /** First use the config to initialize a commit signature for the user. */ - if (git_signature_default(&sig, repo) < 0) + if ((git_signature_default_author(&author_sig, repo) < 0) || + (git_signature_default_committer(&committer_sig, repo) < 0)) fatal("Unable to create a commit signature.", "Perhaps 'user.name' and 'user.email' are not set"); @@ -162,14 +163,15 @@ static void create_initial_commit(git_repository *repo) */ if (git_commit_create_v( - &commit_id, repo, "HEAD", sig, sig, + &commit_id, repo, "HEAD", author_sig, committer_sig, NULL, "Initial commit", tree, 0) < 0) fatal("Could not create the initial commit", NULL); /** Clean up so we don't leak memory. */ git_tree_free(tree); - git_signature_free(sig); + git_signature_free(author_sig); + git_signature_free(committer_sig); } static void usage(const char *error, const char *arg) diff --git a/examples/stash.c b/examples/stash.c index 8142439c7..c330cbce1 100644 --- a/examples/stash.c +++ b/examples/stash.c @@ -108,7 +108,7 @@ static int cmd_push(git_repository *repo, struct opts *opts) if (opts->argc) usage("push does not accept any parameters"); - check_lg2(git_signature_default(&signature, repo), + check_lg2(git_signature_default_author(&signature, repo), "Unable to get signature", NULL); check_lg2(git_stash_save(&stashid, repo, signature, NULL, GIT_STASH_DEFAULT), "Unable to save stash", NULL); diff --git a/examples/tag.c b/examples/tag.c index e4f71ae62..9bebcd1e6 100644 --- a/examples/tag.c +++ b/examples/tag.c @@ -226,7 +226,7 @@ static void action_create_tag(tag_state *state) check_lg2(git_revparse_single(&target, repo, opts->target), "Unable to resolve spec", opts->target); - check_lg2(git_signature_default(&tagger, repo), + check_lg2(git_signature_default_author(&tagger, repo), "Unable to create signature", NULL); check_lg2(git_tag_create(&oid, repo, opts->tag_name, diff --git a/include/git2/signature.h b/include/git2/signature.h index 849998e66..31aa9676d 100644 --- a/include/git2/signature.h +++ b/include/git2/signature.h @@ -48,6 +48,52 @@ GIT_EXTERN(int) git_signature_new(git_signature **out, const char *name, const c */ GIT_EXTERN(int) git_signature_now(git_signature **out, const char *name, const char *email); +/** Create a new author action signature with default information based on the + * configuration and environment variables. + * + * If GIT_AUTHOR_NAME environment variable is set it uses that over the + * user.name value from the configuration. + * + * If GIT_AUTHOR_EMAIL environment variable is set it uses that over the + * user.email value from the configuration. The EMAIL environment variable is + * the fallback email address in case the user.email configuration value isn't + * set. + * + * If GIT_AUTHOR_DATE is set it uses that, otherwise it uses the current time + * as the timestamp. + * + * It will return GIT_ENOTFOUND if either the user.name or user.email are not + * set and there is no fallback from an environment variable. + * + * @param out new signature + * @param repo repository pointer + * @return 0 on success, GIT_ENOTFOUND if config is missing, or error code + */ +GIT_EXTERN(int) git_signature_default_author(git_signature **out, git_repository *repo); + +/** Create a new committer action signature with default information based on + * the configuration and environment variables. + * + * If GIT_COMMITTER_NAME environment variable is set it uses that over the + * user.name value from the configuration. + * + * If GIT_COMMITTER_EMAIL environment variable is set it uses that over the + * user.email value from the configuration. The EMAIL environment variable is + * the fallback email address in case the user.email configuration value isn't + * set. + * + * If GIT_COMMITTER_DATE is set it uses that, otherwise it uses the current + * time as the timestamp. + * + * It will return GIT_ENOTFOUND if either the user.name or user.email are not + * set and there is no fallback from an environment variable. + * + * @param out new signature @param repo repository pointer @return 0 on + * success, GIT_ENOTFOUND if config is missing, or error code + */ +GIT_EXTERN(int) git_signature_default_committer(git_signature **out, git_repository *repo); + +#ifndef GIT_DEPRECATE_HARD /** * Create a new action signature with default user and now timestamp. * @@ -56,11 +102,13 @@ GIT_EXTERN(int) git_signature_now(git_signature **out, const char *name, const c * based on that information. It will return GIT_ENOTFOUND if either the * user.name or user.email are not set. * + * @deprecated use git_signature_default_author or git_signature_default_committer instead * @param out new signature * @param repo repository pointer * @return 0 on success, GIT_ENOTFOUND if config is missing, or error code */ GIT_EXTERN(int) git_signature_default(git_signature **out, git_repository *repo); +#endif /** * Create a new signature by parsing the given buffer, which is diff --git a/src/libgit2/rebase.c b/src/libgit2/rebase.c index 77e442e98..e9de62652 100644 --- a/src/libgit2/rebase.c +++ b/src/libgit2/rebase.c @@ -1268,7 +1268,7 @@ static int rebase_copy_note( } if (!committer) { - if((error = git_signature_default(&who, rebase->repo)) < 0) { + if((error = git_signature_default_committer(&who, rebase->repo)) < 0) { if (error != GIT_ENOTFOUND || (error = git_signature_now(&who, "unknown", "unknown")) < 0) goto done; diff --git a/src/libgit2/refs.c b/src/libgit2/refs.c index c1ed04d23..8b553d40a 100644 --- a/src/libgit2/refs.c +++ b/src/libgit2/refs.c @@ -451,7 +451,7 @@ int git_reference__log_signature(git_signature **out, git_repository *repo) git_signature *who; if(((error = refs_configured_ident(&who, repo)) < 0) && - ((error = git_signature_default(&who, repo)) < 0) && + ((error = git_signature_default_author(&who, repo)) < 0) && ((error = git_signature_now(&who, "unknown", "unknown")) < 0)) return error; diff --git a/src/libgit2/signature.c b/src/libgit2/signature.c index 12d2b5f8d..1a694c4cf 100644 --- a/src/libgit2/signature.c +++ b/src/libgit2/signature.c @@ -10,6 +10,7 @@ #include "repository.h" #include "git2/common.h" #include "posix.h" +#include "date.h" void git_signature_free(git_signature *sig) { @@ -201,6 +202,78 @@ int git_signature_default(git_signature **out, git_repository *repo) return error; } +int git_signature__default_from_env(const char *name_env_var, const char *email_env_var, + const char *date_env_var, git_signature **out, git_repository *repo) +{ + int error; + git_config *cfg; + const char *name, *email, *date; + git_time_t timestamp; + int offset; + git_str name_env = GIT_STR_INIT; + git_str email_env = GIT_STR_INIT; + git_str date_env = GIT_STR_INIT; + int have_email_env = -1; + + if ((error = git_repository_config_snapshot(&cfg, repo)) < 0) + return error; + + /* Check if the environment variable for the name is set */ + if (!(git__getenv(&name_env, name_env_var))) + name = git_str_cstr(&name_env); + else + /* or else read the configuration value. */ + if ((error = git_config_get_string(&name, cfg, "user.name")) < 0) + goto done; + + /* Check if the environment variable for the email is set. */ + if (!(git__getenv(&email_env, email_env_var))) + email = git_str_cstr(&email_env); + else { + /* Check if the fallback EMAIL environment variable is set + * before we check the configuration so that we preserve the + * error message if the configuration value is missing. */ + git_str_dispose(&email_env); + have_email_env = !git__getenv(&email_env, "EMAIL"); + if ((error = git_config_get_string(&email, cfg, "user.email")) < 0) { + if (have_email_env) { + email = git_str_cstr(&email_env); + error = 0; + } else + goto done; + } + } + + /* Check if the environment variable for the timestamp is set */ + if (!(git__getenv(&date_env, date_env_var))) { + date = git_str_cstr(&date_env); + if ((error = git_date_offset_parse(×tamp, &offset, date)) < 0) + goto done; + error = git_signature_new(out, name, email, timestamp, offset); + } else + /* or else default to the current timestamp. */ + error = git_signature_now(out, name, email); + +done: + git_config_free(cfg); + git_str_dispose(&name_env); + git_str_dispose(&email_env); + git_str_dispose(&date_env); + return error; +} + +int git_signature_default_author(git_signature **out, git_repository *repo) +{ + return git_signature__default_from_env("GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", + "GIT_AUTHOR_DATE", out, repo); +} + +int git_signature_default_committer(git_signature **out, git_repository *repo) +{ + return git_signature__default_from_env("GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL", + "GIT_COMMITTER_DATE", out, repo); +} + int git_signature__parse(git_signature *sig, const char **buffer_out, const char *buffer_end, const char *header, char ender) { diff --git a/src/libgit2/signature.h b/src/libgit2/signature.h index 5c8270954..23356161e 100644 --- a/src/libgit2/signature.h +++ b/src/libgit2/signature.h @@ -17,7 +17,7 @@ int git_signature__parse(git_signature *sig, const char **buffer_out, const char *buffer_end, const char *header, char ender); void git_signature__writebuf(git_str *buf, const char *header, const git_signature *sig); bool git_signature__equal(const git_signature *one, const git_signature *two); - int git_signature__pdup(git_signature **dest, const git_signature *source, git_pool *pool); +int git_signature__default_from_env(const char *name_env_var, const char *email_env_var, const char *date_env_var, git_signature **out, git_repository *repo); #endif diff --git a/src/util/date.c b/src/util/date.c index 4d757e21a..161712e16 100644 --- a/src/util/date.c +++ b/src/util/date.c @@ -858,7 +858,7 @@ static git_time_t approxidate_str(const char *date, return update_tm(&tm, &now, 0); } -int git_date_parse(git_time_t *out, const char *date) +int git_date_offset_parse(git_time_t *out, int * out_offset, const char *date) { time_t time_sec; git_time_t timestamp; @@ -866,6 +866,7 @@ int git_date_parse(git_time_t *out, const char *date) if (!parse_date_basic(date, ×tamp, &offset)) { *out = timestamp; + *out_offset = offset; return 0; } @@ -876,6 +877,13 @@ int git_date_parse(git_time_t *out, const char *date) return error_ret; } +int git_date_parse(git_time_t *out, const char *date) +{ + int offset; + + return git_date_offset_parse(out, &offset, date); +} + int git_date_rfc2822_fmt(git_str *out, git_time_t time, int offset) { time_t t; diff --git a/src/util/date.h b/src/util/date.h index 7ebd3c30e..785fc064b 100644 --- a/src/util/date.h +++ b/src/util/date.h @@ -10,9 +10,21 @@ #include "util.h" #include "str.h" +/* + * Parse a string into a value as a git_time_t with a timezone offset. + * + * Sample valid input: + * - "yesterday" + * - "July 17, 2003" + * - "2003-7-17 08:23i+03" + */ +extern int git_date_offset_parse(git_time_t *out, int *out_offset, const char *date); + /* * Parse a string into a value as a git_time_t. * + * Timezone offset is ignored. + * * Sample valid input: * - "yesterday" * - "July 17, 2003" diff --git a/tests/libgit2/commit/signature.c b/tests/libgit2/commit/signature.c index fddd5076e..b41182ce6 100644 --- a/tests/libgit2/commit/signature.c +++ b/tests/libgit2/commit/signature.c @@ -153,3 +153,83 @@ void test_commit_signature__pos_and_neg_zero_offsets_dont_match(void) git_signature_free((git_signature *)with_neg_zero); git_signature_free((git_signature *)with_pos_zero); } + +static git_repository *g_repo; + +void test_commit_signature__initialize(void) +{ + g_repo = cl_git_sandbox_init("empty_standard_repo"); +} + +void test_commit_signature__cleanup(void) +{ + cl_git_sandbox_cleanup(); + g_repo = NULL; +} + +void test_commit_signature__signature_default(void) +{ + git_signature *author_sign, *committer_sign; + git_config *cfg, *local; + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass(git_config_open_level(&local, cfg, GIT_CONFIG_LEVEL_LOCAL)); + /* No configuration value is set and no environment variable */ + cl_git_fail(git_signature_default_author(&author_sign, g_repo)); + cl_git_fail(git_signature_default_committer(&committer_sign, g_repo)); + /* Name is read from configuration and email is read from fallback EMAIL + * environment variable */ + cl_git_pass(git_config_set_string(local, "user.name", "Name (config)")); + cl_setenv("EMAIL", "email-envvar@example.com"); + cl_git_pass(git_signature_default_author(&author_sign, g_repo)); + cl_git_pass(git_signature_default_committer(&committer_sign, g_repo)); + cl_assert_equal_s("Name (config)", author_sign->name); + cl_assert_equal_s("email-envvar@example.com", author_sign->email); + cl_assert_equal_s("Name (config)", committer_sign->name); + cl_assert_equal_s("email-envvar@example.com", committer_sign->email); + cl_setenv("EMAIL", NULL); + git_signature_free(author_sign); + git_signature_free(committer_sign); + /* Environment variables have precedence over configuration */ + cl_git_pass(git_config_set_string(local, "user.email", "config@example.com")); + cl_setenv("GIT_AUTHOR_NAME", "Author (envvar)"); + cl_setenv("GIT_AUTHOR_EMAIL", "author-envvar@example.com"); + cl_setenv("GIT_COMMITTER_NAME", "Committer (envvar)"); + cl_setenv("GIT_COMMITTER_EMAIL", "committer-envvar@example.com"); + cl_git_pass(git_signature_default_author(&author_sign, g_repo)); + cl_git_pass(git_signature_default_committer(&committer_sign, g_repo)); + cl_assert_equal_s("Author (envvar)", author_sign->name); + cl_assert_equal_s("author-envvar@example.com", author_sign->email); + cl_assert_equal_s("Committer (envvar)", committer_sign->name); + cl_assert_equal_s("committer-envvar@example.com", committer_sign->email); + git_signature_free(author_sign); + git_signature_free(committer_sign); + /* When environment variables are not set we can still read from + * configuration */ + cl_setenv("GIT_AUTHOR_NAME", NULL); + cl_setenv("GIT_AUTHOR_EMAIL", NULL); + cl_setenv("GIT_COMMITTER_NAME", NULL); + cl_setenv("GIT_COMMITTER_EMAIL", NULL); + cl_git_pass(git_signature_default_author(&author_sign, g_repo)); + cl_git_pass(git_signature_default_committer(&committer_sign, g_repo)); + cl_assert_equal_s("Name (config)", author_sign->name); + cl_assert_equal_s("config@example.com", author_sign->email); + cl_assert_equal_s("Name (config)", committer_sign->name); + cl_assert_equal_s("config@example.com", committer_sign->email); + git_signature_free(author_sign); + git_signature_free(committer_sign); + /* We can also override the timestamp with an environment variable */ + cl_setenv("GIT_AUTHOR_DATE", "1971-02-03 04:05:06+01"); + cl_setenv("GIT_COMMITTER_DATE", "1988-09-10 11:12:13-01"); + cl_git_pass(git_signature_default_author(&author_sign, g_repo)); + cl_git_pass(git_signature_default_committer(&committer_sign, g_repo)); + cl_assert_equal_i(34398306, author_sign->when.time); /* 1971-02-03 03:05:06 UTC */ + cl_assert_equal_i(60, author_sign->when.offset); + cl_assert_equal_i(589896733, committer_sign->when.time); /* 1988-09-10 12:12:13 UTC */ + cl_assert_equal_i(-60, committer_sign->when.offset); + git_signature_free(author_sign); + git_signature_free(committer_sign); + cl_setenv("GIT_AUTHOR_DATE", NULL); + cl_setenv("GIT_COMMITTER_DATE", NULL); + git_config_free(local); + git_config_free(cfg); +} diff --git a/tests/libgit2/date/date.c b/tests/libgit2/date/date.c index 82b5c6728..b5796861c 100644 --- a/tests/libgit2/date/date.c +++ b/tests/libgit2/date/date.c @@ -20,3 +20,11 @@ void test_date_date__invalid_date(void) cl_git_fail(git_date_parse(&d, "")); cl_git_fail(git_date_parse(&d, "NEITHER_INTEGER_NOR_DATETIME")); } + +void test_date_date__offset(void) +{ + git_time_t d; + int offset; + cl_git_pass(git_date_offset_parse(&d, &offset, "1970-1-1 01:00:00+03")); + cl_assert_equal_i(offset, 3*60); +} diff --git a/tests/libgit2/remote/fetch.c b/tests/libgit2/remote/fetch.c index a5d3272c5..c24ec5a01 100644 --- a/tests/libgit2/remote/fetch.c +++ b/tests/libgit2/remote/fetch.c @@ -82,7 +82,7 @@ static void do_time_travelling_fetch(git_oid *commit1id, git_oid *commit2id, cl_git_pass(git_treebuilder_new(&tb, repo1, NULL)); cl_git_pass(git_treebuilder_write(&empty_tree_id, tb)); cl_git_pass(git_tree_lookup(&empty_tree, repo1, &empty_tree_id)); - cl_git_pass(git_signature_default(&sig, repo1)); + cl_git_pass(git_signature_default_author(&sig, repo1)); cl_git_pass(git_commit_create(commit1id, repo1, REPO1_REFNAME, sig, sig, NULL, "one", empty_tree, 0, NULL)); cl_git_pass(git_commit_lookup(&commit1, repo1, commit1id)); diff --git a/tests/libgit2/repo/init.c b/tests/libgit2/repo/init.c index d78ec063c..bb26e5443 100644 --- a/tests/libgit2/repo/init.c +++ b/tests/libgit2/repo/init.c @@ -581,7 +581,7 @@ void test_repo_init__init_with_initial_commit(void) * made to a repository... */ - /* Make sure we're ready to use git_signature_default :-) */ + /* Make sure we're ready to use git_signature_default_author :-) */ { git_config *cfg, *local; cl_git_pass(git_repository_config(&cfg, g_repo)); @@ -594,20 +594,22 @@ void test_repo_init__init_with_initial_commit(void) /* Create a commit with the new contents of the index */ { - git_signature *sig; + git_signature *author_sig, *committer_sig; git_oid tree_id, commit_id; git_tree *tree; - cl_git_pass(git_signature_default(&sig, g_repo)); + cl_git_pass(git_signature_default_author(&author_sig, g_repo)); + cl_git_pass(git_signature_default_committer(&committer_sig, g_repo)); cl_git_pass(git_index_write_tree(&tree_id, index)); cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id)); cl_git_pass(git_commit_create_v( - &commit_id, g_repo, "HEAD", sig, sig, + &commit_id, g_repo, "HEAD", author_sig, committer_sig, NULL, "First", tree, 0)); git_tree_free(tree); - git_signature_free(sig); + git_signature_free(author_sig); + git_signature_free(committer_sig); } git_index_free(index);