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/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; 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