signature: add git_signature_default_from_env

People who are doing a commit expect a unified timestamp between
author and committer information when we're using the current timestamp.
Provide a single function that returns both author and committer
information so that they can have an identical timestamp when none is
specified in the environment.
This commit is contained in:
Edward Thomson
2024-06-14 12:50:40 +02:00
parent 24d9fe1339
commit 649ef1cca6
7 changed files with 131 additions and 108 deletions

View File

@@ -63,10 +63,8 @@ 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_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_signature_default_from_env(&author_signature, &committer_signature, repo),
"Error creating signature", NULL);
check_lg2(git_commit_create_v(
&commit_oid,

View File

@@ -130,8 +130,7 @@ static void create_initial_commit(git_repository *repo)
/** First use the config to initialize a commit signature for the user. */
if ((git_signature_default_author(&author_sig, repo) < 0) ||
(git_signature_default_committer(&committer_sig, repo) < 0))
if ((git_signature_default_from_env(&author_sig, &committer_sig, repo) < 0))
fatal("Unable to create a commit signature.",
"Perhaps 'user.name' and 'user.email' are not set");

View File

@@ -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_author(&signature, repo),
check_lg2(git_signature_default_from_env(&signature, NULL, repo),
"Unable to get signature", NULL);
check_lg2(git_stash_save(&stashid, repo, signature, NULL, GIT_STASH_DEFAULT),
"Unable to save stash", NULL);

View File

@@ -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_author(&tagger, repo),
check_lg2(git_signature_default_from_env(&tagger, NULL, repo),
"Unable to create signature", NULL);
check_lg2(git_tag_create(&oid, repo, opts->tag_name,

View File

@@ -49,65 +49,53 @@ 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.
* Create a new author and/or committer signatures 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 `author_out` is set, it will be populated with the author
* information. The `GIT_AUTHOR_NAME` and `GIT_AUTHOR_EMAIL`
* environment variables will be honored, and `user.name` and
* `user.email` configuration options will be honored if the
* environment variables are unset. For timestamps, `GIT_AUTHOR_DATE`
* will be used, otherwise the current time will be used.
*
* 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 `committer_out` is set, it will be populated with the
* committer information. The `GIT_COMMITTER_NAME` and
* `GIT_COMMITTER_EMAIL` environment variables will be honored,
* and `user.name` and `user.email` configuration options will
* be honored if the environment variables are unset. For timestamps,
* `GIT_COMMITTER_DATE` will be used, otherwise the current time will
* be used.
*
* If GIT_AUTHOR_DATE is set it uses that, otherwise it uses the current time
* as the timestamp.
* If neither `GIT_AUTHOR_DATE` nor `GIT_COMMITTER_DATE` are set,
* both timestamps will be set to the same time.
*
* 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.
* 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. One of `author_out` or `committer_out` must be set.
*
* @param out new signature
* @param author_out pointer to set the author signature, or NULL
* @param committer_out pointer to set the committer signature, or NULL
* @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);
GIT_EXTERN(int) git_signature_default_from_env(
git_signature **author_out,
git_signature **committer_out,
git_repository *repo);
/**
* Create a new action signature with default user and now timestamp.
*
* Warning: This function may be deprecated in the future. Use one of
* git_signature_default_author or git_signature_default_committer instead.
* These are more compliant with how git constructs default signatures.
*
* This looks up the user.name and user.email from the configuration and
* uses the current time as the timestamp, and creates a new signature
* based on that information. It will return GIT_ENOTFOUND if either the
* user.name or user.email are not set.
*
* Note that these do not examine environment variables, only the
* configuration files. Use `git_signature_default_from_env` to
* consider the environment variables.
*
* @param out new signature
* @param repo repository pointer
* @return 0 on success, GIT_ENOTFOUND if config is missing, or error code

View File

@@ -153,15 +153,10 @@ int git_signature__pdup(git_signature **dest, const git_signature *source, git_p
return 0;
}
int git_signature_now(git_signature **sig_out, const char *name, const char *email)
static void current_time(time_t *now_out, int *offset_out)
{
time_t now;
time_t offset;
struct tm *utc_tm;
git_signature *sig;
struct tm _utc;
*sig_out = NULL;
struct tm _utc, *utc_tm;
/*
* Get the current time as seconds since the epoch and
@@ -171,21 +166,28 @@ int git_signature_now(git_signature **sig_out, const char *name, const char *ema
* us that time as seconds since the epoch. The difference
* between its return value and 'now' is our offset to UTC.
*/
time(&now);
utc_tm = p_gmtime_r(&now, &_utc);
time(now_out);
utc_tm = p_gmtime_r(now_out, &_utc);
utc_tm->tm_isdst = -1;
offset = (time_t)difftime(now, mktime(utc_tm));
offset = (time_t)difftime(*now_out, mktime(utc_tm));
offset /= 60;
if (git_signature_new(&sig, name, email, now, (int)offset) < 0)
return -1;
*sig_out = sig;
return 0;
*offset_out = (int)offset;
}
int git_signature_now(
git_signature **sig_out,
const char *name,
const char *email)
{
time_t now;
int offset;
current_time(&now, &offset);
return git_signature_new(sig_out, name, email, now, offset);
}
#ifndef GIT_DEPRECATE_HARD
int git_signature_default(git_signature **out, git_repository *repo)
{
int error;
@@ -202,10 +204,15 @@ int git_signature_default(git_signature **out, git_repository *repo)
git_config_free(cfg);
return error;
}
#endif
static 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)
static int user_from_env(
git_signature **out,
git_repository *repo,
const char *name_env_var,
const char *email_env_var,
const char *date_env_var,
time_t default_time,
int default_offset)
{
int error;
git_config *cfg;
@@ -215,46 +222,53 @@ static int git_signature__default_from_env(const char *name_env_var, const char
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)))
if (!(git__getenv(&name_env, name_env_var))) {
name = git_str_cstr(&name_env);
else
} 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)))
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
} else {
if ((error = git_config_get_string(&email, cfg, "user.email")) == GIT_ENOTFOUND) {
git_error *last_error;
git_error_save(&last_error);
if ((error = git__getenv(&email_env, "EMAIL")) < 0) {
git_error_restore(last_error);
error = GIT_ENOTFOUND;
goto done;
}
email = git_str_cstr(&email_env);
git_error_free(last_error);
} else if (error < 0) {
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(&timestamp, &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);
} else {
timestamp = default_time;
offset = default_offset;
}
error = git_signature_new(out, name, email, timestamp, offset);
done:
git_config_free(cfg);
@@ -264,16 +278,45 @@ done:
return error;
}
int git_signature_default_author(git_signature **out, git_repository *repo)
int git_signature_default_from_env(
git_signature **author_out,
git_signature **committer_out,
git_repository *repo)
{
return git_signature__default_from_env("GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL",
"GIT_AUTHOR_DATE", out, repo);
}
git_signature *author = NULL, *committer = NULL;
time_t now;
int offset;
int error;
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);
GIT_ASSERT_ARG(author_out || committer_out);
GIT_ASSERT_ARG(repo);
current_time(&now, &offset);
if (author_out &&
(error = user_from_env(&author, repo, "GIT_AUTHOR_NAME",
"GIT_AUTHOR_EMAIL", "GIT_AUTHOR_DATE",
now, offset)) < 0)
goto on_error;
if (committer_out &&
(error = user_from_env(&committer, repo, "GIT_COMMITTER_NAME",
"GIT_COMMITTER_EMAIL", "GIT_COMMITTER_DATE",
now, offset)) < 0)
goto on_error;
if (author_out)
*author_out = author;
if (committer_out)
*committer_out = committer;
return 0;
on_error:
git__free(author);
git__free(committer);
return error;
}
int git_signature__parse(git_signature *sig, const char **buffer_out,

View File

@@ -167,7 +167,7 @@ void test_commit_signature__cleanup(void)
g_repo = NULL;
}
void test_commit_signature__signature_default(void)
void test_commit_signature__from_env(void)
{
git_signature *author_sign, *committer_sign;
git_config *cfg, *local;
@@ -179,14 +179,12 @@ void test_commit_signature__signature_default(void)
cl_setenv("GIT_AUTHOR_EMAIL", NULL);
cl_setenv("GIT_COMMITTER_NAME", NULL);
cl_setenv("GIT_COMMITTER_EMAIL", NULL);
cl_git_fail(git_signature_default_author(&author_sign, g_repo));
cl_git_fail(git_signature_default_committer(&committer_sign, g_repo));
cl_git_fail(git_signature_default_from_env(&author_sign, &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_git_pass(git_signature_default_from_env(&author_sign, &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);
@@ -200,8 +198,7 @@ void test_commit_signature__signature_default(void)
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_git_pass(git_signature_default_from_env(&author_sign, &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);
@@ -214,8 +211,7 @@ void test_commit_signature__signature_default(void)
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_git_pass(git_signature_default_from_env(&author_sign, &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);
@@ -225,8 +221,7 @@ void test_commit_signature__signature_default(void)
/* 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_git_pass(git_signature_default_from_env(&author_sign, &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 */