maintenance(geometric): avoid deadlocks on Windows 10 (#6215)

This PR is a companion of https://github.com/gitgitgadget/git/pull/2103.

On Windows, `maintenance_task_geometric_repack()` opens pack index files
via `pack_geometry_init()` (which `mmap()`s the `.idx` files), then
spawns `git repack` as a child process without setting
`child.odb_to_close`. The parent's `mmap()`s prevent the child from
deleting old `.idx` files.

On Windows 10 builds before the POSIX delete semantics change (between
Build 17134.1304 and 18363.657, see
https://stackoverflow.com/a/60512798), this results in `Unlink of file
'.git/objects/pack/pack-<hash>.idx' failed. Should I try again?` during
fetch-triggered auto-maintenance with the geometric strategy.

The fix adds the missing `child.odb_to_close = the_repository->objects`
line, matching all other maintenance tasks.

The first commit introduces a `GIT_TEST_LEGACY_DELETE` environment
variable to simulate legacy (pre-POSIX) delete semantics on modern
Windows, so the regression test can verify the fix even on Windows 11.

This fixes https://github.com/git-for-windows/git/issues/6210.

Tested-by: Patryk Miś <foss@patrykmis.com>
This commit is contained in:
Johannes Schindelin
2026-04-28 19:13:34 +02:00
committed by GitHub
3 changed files with 67 additions and 3 deletions

View File

@@ -1590,6 +1590,7 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts,
pack_geometry_split(&geometry);
child.git_cmd = 1;
child.odb_to_close = the_repository->objects;
strvec_pushl(&child.args, "repack", "-d", "-l", NULL);
if (geometry.split < geometry.pack_nr)

View File

@@ -554,20 +554,63 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
return wbuf;
}
/*
* Use SetFileInformationByHandle(FileDispositionInfo) to force legacy
* (non-POSIX) delete semantics. On Windows 11, DeleteFileW() uses POSIX
* delete semantics internally, allowing deletion even with active
* MapViewOfFile views. This helper simulates Windows 10 behavior where
* deletion fails if a file mapping exists.
*
* Returns nonzero on success (like DeleteFileW), 0 on failure.
*/
static int legacy_delete_file(const wchar_t *wpathname)
{
FILE_DISPOSITION_INFO fdi = { TRUE };
DWORD gle;
HANDLE h = CreateFileW(wpathname, DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE |
FILE_SHARE_DELETE,
NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT, NULL);
if (h == INVALID_HANDLE_VALUE)
return 0;
if (SetFileInformationByHandle(h, FileDispositionInfo,
&fdi, sizeof(fdi))) {
CloseHandle(h);
return 1;
}
gle = GetLastError();
CloseHandle(h);
SetLastError(gle);
return 0;
}
static int try_delete_file(const wchar_t *wpathname, int use_legacy)
{
if (use_legacy)
return legacy_delete_file(wpathname);
return DeleteFileW(wpathname);
}
int mingw_unlink(const char *pathname, int handle_in_use_error)
{
static int use_legacy_delete = -1;
int tries = 0;
wchar_t wpathname[MAX_LONG_PATH];
if (xutftowcs_long_path(wpathname, pathname) < 0)
return -1;
if (DeleteFileW(wpathname))
if (use_legacy_delete < 0)
use_legacy_delete = !!getenv("GIT_TEST_LEGACY_DELETE");
if (try_delete_file(wpathname, use_legacy_delete))
return 0;
do {
/* read-only files cannot be removed */
_wchmod(wpathname, 0666);
if (!_wunlink(wpathname))
if (try_delete_file(wpathname, use_legacy_delete))
return 0;
if (!is_file_in_use_error(GetLastError()))
break;

View File

@@ -532,7 +532,16 @@ run_and_verify_geometric_pack () {
# And verify that there are no loose objects anymore.
git count-objects -v >count &&
test_grep '^count: 0$' count
test_grep '^count: 0$' count &&
# Verify that no orphaned .idx files were left behind. On
# Windows, a missing odb_to_close causes the parent to hold
# mmap handles on .idx files, silently preventing their
# deletion by the child git-repack process.
ls .git/objects/pack/pack-*.idx .git/objects/pack/pack-*.pack |
sed "s/\.pack$/.idx/" |
sort | uniq -u >orphaned-idx &&
test_must_be_empty orphaned-idx
}
test_expect_success 'geometric repacking task' '
@@ -580,8 +589,19 @@ test_expect_success 'geometric repacking task' '
# And these two small packs should now be merged via the
# geometric repack. The large packfile should remain intact.
cp -R .git/objects .git/objects.save &&
run_and_verify_geometric_pack 2 &&
# On Windows, verify the same with legacy delete semantics
# that reject deletion of mmap-held .idx files.
if test_have_prereq MINGW
then
rm -rf .git/objects &&
mv .git/objects.save .git/objects &&
test_env GIT_TEST_LEGACY_DELETE=1 \
run_and_verify_geometric_pack 2
fi &&
# If we now add two more objects and repack twice we should
# then see another all-into-one repack. This time around
# though, as we have unreachable objects, we should also see a