Merge branch 'ds/background-maintenance-with-credential'

Background tasks "git maintenance" runs may need to use credential
information when going over the network, but a credential helper
may work only in an interactive environment, and end up blocking a
scheduled task waiting for UI.  Credential helpers can now behave
differently when they are not running interactively.

* ds/background-maintenance-with-credential:
  scalar: configure maintenance during 'reconfigure'
  maintenance: add custom config to background jobs
  credential: add new interactive config option
This commit is contained in:
Junio C Hamano 2024-09-30 16:16:16 -07:00
commit 4251403327
7 changed files with 114 additions and 12 deletions

View File

@ -9,6 +9,14 @@ credential.helper::
Note that multiple helpers may be defined. See linkgit:gitcredentials[7]
for details and examples.
credential.interactive::
By default, Git and any configured credential helpers will ask for
user input when new credentials are required. Many of these helpers
will succeed based on stored credentials if those credentials are
still valid. To avoid the possibility of user interactivity from
Git, set `credential.interactive=false`. Some credential helpers
respect this option as well.
credential.useHttpPath::
When acquiring credentials, consider the "path" component of an http
or https URL to be important. Defaults to false. See

View File

@ -1768,6 +1768,42 @@ static const char *get_frequency(enum schedule_priority schedule)
}
}
static const char *extraconfig[] = {
"credential.interactive=false",
"core.askPass=true", /* 'true' returns success, but no output. */
NULL
};
static const char *get_extra_config_parameters(void) {
static const char *result = NULL;
struct strbuf builder = STRBUF_INIT;
if (result)
return result;
for (const char **s = extraconfig; s && *s; s++)
strbuf_addf(&builder, "-c %s ", *s);
result = strbuf_detach(&builder, NULL);
return result;
}
static const char *get_extra_launchctl_strings(void) {
static const char *result = NULL;
struct strbuf builder = STRBUF_INIT;
if (result)
return result;
for (const char **s = extraconfig; s && *s; s++) {
strbuf_addstr(&builder, "<string>-c</string>\n");
strbuf_addf(&builder, "<string>%s</string>\n", *s);
}
result = strbuf_detach(&builder, NULL);
return result;
}
/*
* get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable
* to mock the schedulers that `git maintenance start` rely on.
@ -1974,6 +2010,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
"<array>\n"
"<string>%s/git</string>\n"
"<string>--exec-path=%s</string>\n"
"%s" /* For extra config parameters. */
"<string>for-each-repo</string>\n"
"<string>--keep-going</string>\n"
"<string>--config=maintenance.repo</string>\n"
@ -1983,7 +2020,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
"</array>\n"
"<key>StartCalendarInterval</key>\n"
"<array>\n";
strbuf_addf(&plist, preamble, name, exec_path, exec_path, frequency);
strbuf_addf(&plist, preamble, name, exec_path, exec_path,
get_extra_launchctl_strings(), frequency);
switch (schedule) {
case SCHEDULE_HOURLY:
@ -2218,11 +2256,12 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
"<Actions Context=\"Author\">\n"
"<Exec>\n"
"<Command>\"%s\\headless-git.exe\"</Command>\n"
"<Arguments>--exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
"<Arguments>--exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
"</Exec>\n"
"</Actions>\n"
"</Task>\n";
fprintf(tfile->fp, xml, exec_path, exec_path, frequency);
fprintf(tfile->fp, xml, exec_path, exec_path,
get_extra_config_parameters(), frequency);
strvec_split(&child.args, cmd);
strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml",
get_tempfile_path(tfile), NULL);
@ -2363,8 +2402,8 @@ static int crontab_update_schedule(int run_maintenance, int fd)
"# replaced in the future by a Git command.\n\n");
strbuf_addf(&line_format,
"%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n",
exec_path, exec_path);
"%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n",
exec_path, exec_path, get_extra_config_parameters());
fprintf(cron_in, line_format.buf, minute, "1-23", "*", "hourly");
fprintf(cron_in, line_format.buf, minute, "0", "1-6", "daily");
fprintf(cron_in, line_format.buf, minute, "0", "0", "weekly");
@ -2564,7 +2603,7 @@ static int systemd_timer_write_service_template(const char *exec_path)
"\n"
"[Service]\n"
"Type=oneshot\n"
"ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n"
"ExecStart=\"%s/git\" --exec-path=\"%s\" %s for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n"
"LockPersonality=yes\n"
"MemoryDenyWriteExecute=yes\n"
"NoNewPrivileges=yes\n"
@ -2574,7 +2613,7 @@ static int systemd_timer_write_service_template(const char *exec_path)
"RestrictSUIDSGID=yes\n"
"SystemCallArchitectures=native\n"
"SystemCallFilter=@system-service\n";
if (fprintf(file, unit, exec_path, exec_path) < 0) {
if (fprintf(file, unit, exec_path, exec_path, get_extra_config_parameters()) < 0) {
error(_("failed to write to '%s'"), filename);
fclose(file);
goto error;

View File

@ -13,6 +13,8 @@
#include "strbuf.h"
#include "urlmatch.h"
#include "git-compat-util.h"
#include "trace2.h"
#include "repository.h"
void credential_init(struct credential *c)
{
@ -251,14 +253,36 @@ static char *credential_ask_one(const char *what, struct credential *c,
return xstrdup(r);
}
static void credential_getpass(struct credential *c)
static int credential_getpass(struct credential *c)
{
int interactive;
char *value;
if (!git_config_get_maybe_bool("credential.interactive", &interactive) &&
!interactive) {
trace2_data_intmax("credential", the_repository,
"interactive/skipped", 1);
return -1;
}
if (!git_config_get_string("credential.interactive", &value)) {
int same = !strcmp(value, "never");
free(value);
if (same) {
trace2_data_intmax("credential", the_repository,
"interactive/skipped", 1);
return -1;
}
}
trace2_region_enter("credential", "interactive", the_repository);
if (!c->username)
c->username = credential_ask_one("Username", c,
PROMPT_ASKPASS|PROMPT_ECHO);
if (!c->password)
c->password = credential_ask_one("Password", c,
PROMPT_ASKPASS);
trace2_region_leave("credential", "interactive", the_repository);
return 0;
}
int credential_has_capability(const struct credential_capability *capa,
@ -501,8 +525,8 @@ void credential_fill(struct credential *c, int all_capabilities)
c->helpers.items[i].string);
}
credential_getpass(c);
if (!c->username && !c->password && !c->credential)
if (credential_getpass(c) ||
(!c->username && !c->password && !c->credential))
die("unable to get password from user");
}

View File

@ -733,6 +733,9 @@ static int cmd_reconfigure(int argc, const char **argv)
the_repository = old_repo;
if (toggle_maintenance(1) >= 0)
succeeded = 1;
loop_end:
if (!succeeded) {
res = -1;

View File

@ -186,6 +186,28 @@ test_expect_success 'clone from password-protected repository' '
test_cmp expect actual
'
test_expect_success 'credential.interactive=false skips askpass' '
set_askpass bogus nonsense &&
(
GIT_TRACE2_EVENT="$(pwd)/interactive-true" &&
export GIT_TRACE2_EVENT &&
test_must_fail git clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-true-dir &&
test_region credential interactive interactive-true &&
GIT_TRACE2_EVENT="$(pwd)/interactive-false" &&
export GIT_TRACE2_EVENT &&
test_must_fail git -c credential.interactive=false \
clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-false-dir &&
test_region ! credential interactive interactive-false &&
GIT_TRACE2_EVENT="$(pwd)/interactive-never" &&
export GIT_TRACE2_EVENT &&
test_must_fail git -c credential.interactive=never \
clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-never-dir &&
test_region ! credential interactive interactive-never
)
'
test_expect_success 'clone from auth-only-for-push repository' '
echo two >expect &&
set_askpass wrong &&

View File

@ -825,6 +825,9 @@ test_expect_success 'start and stop Linux/systemd maintenance' '
test_systemd_analyze_verify "systemd/user/git-maintenance@daily.service" &&
test_systemd_analyze_verify "systemd/user/git-maintenance@weekly.service" &&
grep "core.askPass=true" "systemd/user/git-maintenance@.service" &&
grep "credential.interactive=false" "systemd/user/git-maintenance@.service" &&
printf -- "--user enable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
test_cmp expect args &&

View File

@ -194,8 +194,11 @@ test_expect_success 'scalar reconfigure' '
scalar reconfigure one &&
test true = "$(git -C one/src config core.preloadIndex)" &&
git -C one/src config core.preloadIndex false &&
scalar reconfigure -a &&
test true = "$(git -C one/src config core.preloadIndex)"
rm one/src/cron.txt &&
GIT_TRACE2_EVENT="$(pwd)/reconfigure" scalar reconfigure -a &&
test_path_is_file one/src/cron.txt &&
test true = "$(git -C one/src config core.preloadIndex)" &&
test_subcommand git maintenance start <reconfigure
'
test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '