Skip to content

Commit 908c6e5

Browse files
Maintenance locks and interactivity (#598)
* [x] This is an early version of work ~~already under review upstream~~. * [x] This change only applies to interactions with Azure DevOps and the GVFS Protocol. * [ ] This change only applies to the virtualization hook and VFS for Git. This set of patches has a little of both: 1. We need to revert a custom change around `maintenance.lock` files that was only in `microsoft/git`. The "fix" actually caused a worse situation where many background maintenance processes would pile up. 2. Create the `credential.interactive` config as an official Git config and use it in Git to prevent any chance of a foreground username/password request. (This is borrowed from GCM, which already respects this.) 3. To help avoid problems, especially when blocked by credentials, add configuration to the background schedule to avoid interactive prompts. 4. To make sure that the new config options are set up in the background schedule, update `scalar reconfigure` to execute `git maintenance start`. Points 2-4 could maybe to upstream, but we shouldn't wait for that. I've tested these features locally on Linux and Windows and double-checked that they will prevent the backup of maintenance processes when the credentials become invalid.
2 parents bc0006d + e5459ea commit 908c6e5

File tree

7 files changed

+120
-52
lines changed

7 files changed

+120
-52
lines changed

Documentation/config/credential.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ credential.helper::
99
Note that multiple helpers may be defined. See linkgit:gitcredentials[7]
1010
for details and examples.
1111

12+
credential.interactive::
13+
By default, Git and any configured credential helpers will ask for
14+
user input when new credentials are required. Many of these helpers
15+
will succeed based on stored credentials if those credentials are
16+
still valid. To avoid the possibility of user interactivity from
17+
Git, set `credential.interactive=false`. Some credential helpers
18+
respect this option as well.
19+
1220
credential.useHttpPath::
1321
When acquiring credentials, consider the "path" component of an http
1422
or https URL to be important. Defaults to false. See

builtin/gc.c

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,8 +1309,6 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts)
13091309
char *lock_path = xstrfmt("%s/maintenance", r->objects->odb->path);
13101310

13111311
if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {
1312-
struct stat st;
1313-
struct strbuf lock_dot_lock = STRBUF_INIT;
13141312
/*
13151313
* Another maintenance command is running.
13161314
*
@@ -1321,25 +1319,6 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts)
13211319
if (!opts->auto_flag && !opts->quiet)
13221320
warning(_("lock file '%s' exists, skipping maintenance"),
13231321
lock_path);
1324-
1325-
/*
1326-
* Check timestamp on .lock file to see if we should
1327-
* delete it to recover from a fail state.
1328-
*/
1329-
strbuf_addstr(&lock_dot_lock, lock_path);
1330-
strbuf_addstr(&lock_dot_lock, ".lock");
1331-
if (lstat(lock_dot_lock.buf, &st))
1332-
warning_errno(_("unable to stat '%s'"), lock_dot_lock.buf);
1333-
else {
1334-
if (st.st_mtime < time(NULL) - (6 * 60 * 60)) {
1335-
if (unlink(lock_dot_lock.buf))
1336-
warning_errno(_("unable to delete stale lock file"));
1337-
else
1338-
warning(_("deleted stale lock file"));
1339-
}
1340-
}
1341-
1342-
strbuf_release(&lock_dot_lock);
13431322
free(lock_path);
13441323
return 0;
13451324
}
@@ -1678,6 +1657,42 @@ static const char *get_frequency(enum schedule_priority schedule)
16781657
}
16791658
}
16801659

1660+
static const char *extraconfig[] = {
1661+
"credential.interactive=false",
1662+
"core.askPass=true", /* 'true' returns success, but no output. */
1663+
NULL
1664+
};
1665+
1666+
static const char *get_extra_config_parameters(void) {
1667+
static const char *result = NULL;
1668+
struct strbuf builder = STRBUF_INIT;
1669+
1670+
if (result)
1671+
return result;
1672+
1673+
for (const char **s = extraconfig; s && *s; s++)
1674+
strbuf_addf(&builder, "-c %s ", *s);
1675+
1676+
result = strbuf_detach(&builder, NULL);
1677+
return result;
1678+
}
1679+
1680+
static const char *get_extra_launchctl_strings(void) {
1681+
static const char *result = NULL;
1682+
struct strbuf builder = STRBUF_INIT;
1683+
1684+
if (result)
1685+
return result;
1686+
1687+
for (const char **s = extraconfig; s && *s; s++) {
1688+
strbuf_addstr(&builder, "<string>-c</string>\n");
1689+
strbuf_addf(&builder, "<string>%s</string>\n", *s);
1690+
}
1691+
1692+
result = strbuf_detach(&builder, NULL);
1693+
return result;
1694+
}
1695+
16811696
/*
16821697
* get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable
16831698
* to mock the schedulers that `git maintenance start` rely on.
@@ -1884,6 +1899,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
18841899
"<array>\n"
18851900
"<string>%s/git</string>\n"
18861901
"<string>--exec-path=%s</string>\n"
1902+
"%s" /* For extra config parameters. */
18871903
"<string>for-each-repo</string>\n"
18881904
"<string>--config=maintenance.repo</string>\n"
18891905
"<string>maintenance</string>\n"
@@ -1892,7 +1908,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
18921908
"</array>\n"
18931909
"<key>StartCalendarInterval</key>\n"
18941910
"<array>\n";
1895-
strbuf_addf(&plist, preamble, name, exec_path, exec_path, frequency);
1911+
strbuf_addf(&plist, preamble, name, exec_path, exec_path,
1912+
get_extra_launchctl_strings(), frequency);
18961913

18971914
switch (schedule) {
18981915
case SCHEDULE_HOURLY:
@@ -2127,11 +2144,12 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
21272144
"<Actions Context=\"Author\">\n"
21282145
"<Exec>\n"
21292146
"<Command>\"%s\\headless-git.exe\"</Command>\n"
2130-
"<Arguments>--exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
2147+
"<Arguments>--exec-path=\"%s\" %s for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
21312148
"</Exec>\n"
21322149
"</Actions>\n"
21332150
"</Task>\n";
2134-
fprintf(tfile->fp, xml, exec_path, exec_path, frequency);
2151+
fprintf(tfile->fp, xml, exec_path, exec_path,
2152+
get_extra_config_parameters(), frequency);
21352153
strvec_split(&child.args, cmd);
21362154
strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml",
21372155
get_tempfile_path(tfile), NULL);
@@ -2272,8 +2290,8 @@ static int crontab_update_schedule(int run_maintenance, int fd)
22722290
"# replaced in the future by a Git command.\n\n");
22732291

22742292
strbuf_addf(&line_format,
2275-
"%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%s\n",
2276-
exec_path, exec_path);
2293+
"%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" %s for-each-repo --config=maintenance.repo maintenance run --schedule=%%s\n",
2294+
exec_path, exec_path, get_extra_config_parameters());
22772295
fprintf(cron_in, line_format.buf, minute, "1-23", "*", "hourly");
22782296
fprintf(cron_in, line_format.buf, minute, "0", "1-6", "daily");
22792297
fprintf(cron_in, line_format.buf, minute, "0", "0", "weekly");
@@ -2473,7 +2491,7 @@ static int systemd_timer_write_service_template(const char *exec_path)
24732491
"\n"
24742492
"[Service]\n"
24752493
"Type=oneshot\n"
2476-
"ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n"
2494+
"ExecStart=\"%s/git\" --exec-path=\"%s\" %s for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n"
24772495
"LockPersonality=yes\n"
24782496
"MemoryDenyWriteExecute=yes\n"
24792497
"NoNewPrivileges=yes\n"
@@ -2483,7 +2501,7 @@ static int systemd_timer_write_service_template(const char *exec_path)
24832501
"RestrictSUIDSGID=yes\n"
24842502
"SystemCallArchitectures=native\n"
24852503
"SystemCallFilter=@system-service\n";
2486-
if (fprintf(file, unit, exec_path, exec_path) < 0) {
2504+
if (fprintf(file, unit, exec_path, exec_path, get_extra_config_parameters()) < 0) {
24872505
error(_("failed to write to '%s'"), filename);
24882506
fclose(file);
24892507
goto error;

credential.c

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include "strbuf.h"
1212
#include "urlmatch.h"
1313
#include "git-compat-util.h"
14+
#include "trace2.h"
15+
#include "repository.h"
1416

1517
void credential_init(struct credential *c)
1618
{
@@ -196,14 +198,36 @@ static char *credential_ask_one(const char *what, struct credential *c,
196198
return xstrdup(r);
197199
}
198200

199-
static void credential_getpass(struct credential *c)
201+
static int credential_getpass(struct credential *c)
200202
{
203+
int interactive;
204+
char *value;
205+
if (!git_config_get_maybe_bool("credential.interactive", &interactive) &&
206+
!interactive) {
207+
trace2_data_intmax("credential", the_repository,
208+
"interactive/skipped", 1);
209+
return -1;
210+
}
211+
if (!git_config_get_string("credential.interactive", &value)) {
212+
int same = !strcmp(value, "never");
213+
free(value);
214+
if (same) {
215+
trace2_data_intmax("credential", the_repository,
216+
"interactive/skipped", 1);
217+
return -1;
218+
}
219+
}
220+
221+
trace2_region_enter("credential", "interactive", the_repository);
201222
if (!c->username)
202223
c->username = credential_ask_one("Username", c,
203224
PROMPT_ASKPASS|PROMPT_ECHO);
204225
if (!c->password)
205226
c->password = credential_ask_one("Password", c,
206227
PROMPT_ASKPASS);
228+
trace2_region_leave("credential", "interactive", the_repository);
229+
230+
return 0;
207231
}
208232

209233
int credential_read(struct credential *c, FILE *fp)
@@ -381,8 +405,8 @@ void credential_fill(struct credential *c)
381405
c->helpers.items[i].string);
382406
}
383407

384-
credential_getpass(c);
385-
if (!c->username && !c->password)
408+
if (credential_getpass(c) ||
409+
(!c->username && !c->password))
386410
die("unable to get password from user");
387411
}
388412

scalar.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,9 @@ static int cmd_reconfigure(int argc, const char **argv)
10981098
if (set_recommended_config(1) < 0)
10991099
failed = -1;
11001100

1101+
if (toggle_maintenance(1) < 0)
1102+
failed = -1;
1103+
11011104
loop_end:
11021105
if (failed) {
11031106
res = failed;
@@ -1400,7 +1403,7 @@ int cmd_main(int argc, const char **argv)
14001403
if (is_unattended()) {
14011404
setenv("GIT_ASKPASS", "", 0);
14021405
setenv("GIT_TERMINAL_PROMPT", "false", 0);
1403-
git_config_push_parameter("credential.interactive=never");
1406+
git_config_push_parameter("credential.interactive=false");
14041407
}
14051408

14061409
while (argc > 1 && *argv[1] == '-') {

t/t5551-http-fetch-smart.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,28 @@ test_expect_success 'clone from password-protected repository' '
186186
test_cmp expect actual
187187
'
188188

189+
test_expect_success 'credential.interactive=false skips askpass' '
190+
set_askpass bogus nonsense &&
191+
(
192+
GIT_TRACE2_EVENT="$(pwd)/interactive-true" &&
193+
export GIT_TRACE2_EVENT &&
194+
test_must_fail git clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-true-dir &&
195+
test_region credential interactive interactive-true &&
196+
197+
GIT_TRACE2_EVENT="$(pwd)/interactive-false" &&
198+
export GIT_TRACE2_EVENT &&
199+
test_must_fail git -c credential.interactive=false \
200+
clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-false-dir &&
201+
test_region ! credential interactive interactive-false &&
202+
203+
GIT_TRACE2_EVENT="$(pwd)/interactive-never" &&
204+
export GIT_TRACE2_EVENT &&
205+
test_must_fail git -c credential.interactive=never \
206+
clone --bare "$HTTPD_URL/auth/smart/repo.git" interactive-never-dir &&
207+
test_region ! credential interactive interactive-never
208+
)
209+
'
210+
189211
test_expect_success 'clone from auth-only-for-push repository' '
190212
echo two >expect &&
191213
set_askpass wrong &&

t/t7900-maintenance.sh

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,23 +54,6 @@ test_expect_success 'run [--auto|--quiet]' '
5454
test_subcommand git gc --no-quiet <run-no-quiet.txt
5555
'
5656

57-
test_expect_success 'lock file behavior' '
58-
test_when_finished git config --unset maintenance.commit-graph.schedule &&
59-
git config maintenance.commit-graph.schedule hourly &&
60-
61-
touch .git/objects/maintenance.lock &&
62-
git maintenance run --schedule=hourly --no-quiet 2>err &&
63-
grep "lock file .* exists, skipping maintenance" err &&
64-
65-
test-tool chmtime =-22000 .git/objects/maintenance.lock &&
66-
git maintenance run --schedule=hourly --no-quiet 2>err &&
67-
grep "deleted stale lock file" err &&
68-
test_path_is_missing .git/objects/maintenance.lock &&
69-
70-
git maintenance run --schedule=hourly 2>err &&
71-
test_must_be_empty err
72-
'
73-
7457
test_expect_success 'maintenance.auto config option' '
7558
GIT_TRACE2_EVENT="$(pwd)/default" git commit --quiet --allow-empty -m 1 &&
7659
test_subcommand git maintenance run --auto --quiet <default &&
@@ -771,6 +754,9 @@ test_expect_success 'start and stop Linux/systemd maintenance' '
771754
test_systemd_analyze_verify "systemd/user/[email protected]" &&
772755
test_systemd_analyze_verify "systemd/user/[email protected]" &&
773756
757+
grep "core.askPass=true" "systemd/user/[email protected]" &&
758+
grep "credential.interactive=false" "systemd/user/[email protected]" &&
759+
774760
printf -- "--user enable --now git-maintenance@%s.timer\n" hourly daily weekly >expect &&
775761
test_cmp expect args &&
776762

t/t9210-scalar.sh

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,11 @@ test_expect_success 'scalar reconfigure' '
176176
scalar reconfigure one &&
177177
test true = "$(git -C one/src config core.preloadIndex)" &&
178178
git -C one/src config core.preloadIndex false &&
179-
scalar reconfigure -a &&
180-
test true = "$(git -C one/src config core.preloadIndex)"
179+
rm one/src/cron.txt &&
180+
GIT_TRACE2_EVENT="$(pwd)/reconfigure" scalar reconfigure -a &&
181+
test_path_is_file one/src/cron.txt &&
182+
test true = "$(git -C one/src config core.preloadIndex)" &&
183+
test_subcommand git maintenance start <reconfigure
181184
'
182185

183186
test_expect_success '`reconfigure -a` removes stale config entries' '
@@ -300,7 +303,11 @@ test_expect_success 'start GVFS-enabled server' '
300303
test_expect_success '`scalar clone` with GVFS-enabled server' '
301304
: the fake cache server requires fake authentication &&
302305
git config --global core.askPass true &&
303-
scalar clone --single-branch -- http://$HOST_PORT/ using-gvfs &&
306+
307+
# We must set credential.interactive=true to bypass a setting
308+
# in "scalar clone" that disables interactive credentials during
309+
# an unattended command.
310+
scalar -c credential.interactive=true clone --single-branch -- http://$HOST_PORT/ using-gvfs &&
304311
305312
: verify that the shared cache has been configured &&
306313
cache_key="url_$(printf "%s" http://$HOST_PORT/ |

0 commit comments

Comments
 (0)