From 97508e91b62c91b77447dbba39a84770682591a8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 28 Apr 2026 01:55:49 +0200 Subject: [PATCH 1/2] mingw: optionally use legacy (non-POSIX) delete semantics At some point between Windows 10 Build 17134.1304 and Build 18363.657, the default behavior of `DeleteFileW()` was changed to use POSIX semantics (https://stackoverflow.com/a/60512798). Under those semantics, a file can be deleted even when another process holds an active `MapViewOfFile` view on it: the directory entry is removed immediately, but the underlying data persists until the last handle is closed. On older Windows versions (and Windows 10 builds before that change), `DeleteFileW()` uses legacy semantics where deletion fails outright if any process holds a file mapping. To allow testing code paths that depend on the legacy behavior, introduce a `GIT_TEST_LEGACY_DELETE` environment variable. When set, `mingw_unlink()` uses `SetFileInformationByHandle()` with `FileDispositionInfo` (the non-POSIX variant) instead of `DeleteFileW()`, forcing legacy delete semantics regardless of the Windows version. Signed-off-by: Johannes Schindelin --- compat/mingw.c | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index 2023c16db65742..04f9aa3922d4bb 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -449,20 +449,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_PATH]; if (xutftowcs_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; From 12ebd5c56ff02cdbae87b0af9404bb666133bb04 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 28 Apr 2026 00:10:41 +0200 Subject: [PATCH 2/2] maintenance(geometric): do release the `.idx` files before repacking As is done for all the other maintenance tasks, let's release the ODB also before starting the geometric repacking. That way, the `.idx` files won't be `mmap()`ed when they are to be deleted (which does not work on Windows because you cannot delete files on that platform as long as they are kept open by a process). This regression was introduced by 9bc151850c1c (builtin/maintenance: introduce "geometric-repack" task, 2025-10-24), but was only noticed once geometric repacking was made the default in 452b12c2e0fe (builtin/ maintenance: use "geometric" strategy by default, 2026-02-24). The fix recapitulates my work from df76ee7b77f0 (run-command: offer to close the object store before running, 2021-09-09) & friends. To guard against future regressions of this kind, add a check to `run_and_verify_geometric_pack()` in `t7900` that detects orphaned `.idx` files left behind after repacking. Contrary to interactive calls, the `git maintenance` call in that test case would _not_ block on Windows, asking whether to retry deleting that file, which is the reason why this bug was not caught earlier. Furthermore, since the default behavior of `DeleteFileW()` was changed at some point between Windows 10 Build 17134.1304 and Build 18363.657 to use POSIX semantics (see https://stackoverflow.com/a/60512798), the added orphaned-`.idx` check would be insufficient to catch this regression on modern Windows without emulating legacy delete semantics via `GIT_TEST_LEGACY_DELETE=1`. This fixes https://github.com/git-for-windows/git/issues/6210. Signed-off-by: Johannes Schindelin --- builtin/gc.c | 1 + t/t7900-maintenance.sh | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/builtin/gc.c b/builtin/gc.c index 3a71e314c975af..84a66d32404e4d 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -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) diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh index 4700beacc18281..f497f51b2348c8 100755 --- a/t/t7900-maintenance.sh +++ b/t/t7900-maintenance.sh @@ -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