mingw: factor out the retry logic

In several places, Git's Windows-specific code follows the pattern where it tries to perform an operation, and retries several times when that operation fails, sleeping an increasing amount of time, before finally giving up and asking the user whether to rety (after, say, closing an editor that held a handle to a file, preventing the operation from succeeding). This logic is a bit hard to use, and inconsistent: `mingw_unlink()` and `mingw_rmdir()` duplicate the code to retry, and both of them do so incompletely. They also do not restore `errno` if the user answers 'no'. Introduce a `retry_ask_yes_no()` helper function that handles retry with small delay, asking the user, and restoring `errno`. Note that in `mingw_unlink()`, we include the `_wchmod()` call in the retry loop (which may fail if the file is locked exclusively). In `mingw_rmdir()`, we include special error handling in the retry loop. Signed-off-by: Karsten Blees <karsten.blees@gmail.com> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Karsten Blees committed Jan 9, 2026 at 20:05 UTC b0b32ff16ffc0448b4522997667086e31715424f
1 file changed +46 -58
compat/mingw.c
+46 -58
@@ -28,8 +28,6 @@
28
29 #define HCAST(type, handle) ((type)(intptr_t)handle)
30
31 -static const int delay[] = { 0, 1, 10, 20, 40 };
32 -
31 void open_in_gdb(void)
32 {
33 static struct child_process cp = CHILD_PROCESS_INIT;
@@ -205,15 +203,12 @@ static int read_yes_no_answer(void)
203 return -1;
204 }
205
208 -static int ask_yes_no_if_possible(const char *format, ...)
206 +static int ask_yes_no_if_possible(const char *format, va_list args)
207 {
208 char question[4096];
209 const char *retry_hook;
212 - va_list args;
210
214 - va_start(args, format);
211 vsnprintf(question, sizeof(question), format, args);
216 - va_end(args);
212
213 retry_hook = mingw_getenv("GIT_ASK_YESNO");
214 if (retry_hook) {
@@ -238,6 +233,31 @@ static int ask_yes_no_if_possible(const char *format, ...)
233 }
234 }
235
236 +static int retry_ask_yes_no(int *tries, const char *format, ...)
237 +{
238 + static const int delay[] = { 0, 1, 10, 20, 40 };
239 + va_list args;
240 + int result, saved_errno = errno;
241 +
242 + if ((*tries) < ARRAY_SIZE(delay)) {
243 + /*
244 + * We assume that some other process had the file open at the wrong
245 + * moment and retry. In order to give the other process a higher
246 + * chance to complete its operation, we give up our time slice now.
247 + * If we have to retry again, we do sleep a bit.
248 + */
249 + Sleep(delay[*tries]);
250 + (*tries)++;
251 + return 1;
252 + }
253 +
254 + va_start(args, format);
255 + result = ask_yes_no_if_possible(format, args);
256 + va_end(args);
257 + errno = saved_errno;
258 + return result;
259 +}
260 +
261 /* Windows only */
262 enum hide_dotfiles_type {
263 HIDE_DOTFILES_FALSE = 0,
@@ -298,7 +318,7 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)
318
319 int mingw_unlink(const char *pathname, int handle_in_use_error)
320 {
301 - int ret, tries = 0;
321 + int tries = 0;
322 wchar_t wpathname[MAX_PATH];
323 if (xutftowcs_path(wpathname, pathname) < 0)
324 return -1;
@@ -306,29 +326,19 @@ int mingw_unlink(const char *pathname, int handle_in_use_error)
326 if (DeleteFileW(wpathname))
327 return 0;
328
309 - /* read-only files cannot be removed */
310 - _wchmod(wpathname, 0666);
311 - while ((ret = _wunlink(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
329 + do {
330 + /* read-only files cannot be removed */
331 + _wchmod(wpathname, 0666);
332 + if (!_wunlink(wpathname))
333 + return 0;
334 if (!is_file_in_use_error(GetLastError()))
335 break;
336 if (!handle_in_use_error)
315 - return ret;
337 + return -1;
338
317 - /*
318 - * We assume that some other process had the source or
319 - * destination file open at the wrong moment and retry.
320 - * In order to give the other process a higher chance to
321 - * complete its operation, we give up our time slice now.
322 - * If we have to retry again, we do sleep a bit.
323 - */
324 - Sleep(delay[tries]);
325 - tries++;
326 - }
327 - while (ret == -1 && is_file_in_use_error(GetLastError()) &&
328 - ask_yes_no_if_possible("Unlink of file '%s' failed. "
329 - "Should I try again?", pathname))
330 - ret = _wunlink(wpathname);
331 - return ret;
339 + } while (retry_ask_yes_no(&tries, "Unlink of file '%s' failed. "
340 + "Should I try again?", pathname));
341 + return -1;
342 }
343
344 static int is_dir_empty(const wchar_t *wpath)
@@ -355,7 +365,7 @@ static int is_dir_empty(const wchar_t *wpath)
365
366 int mingw_rmdir(const char *pathname)
367 {
358 - int ret, tries = 0;
368 + int tries = 0;
369 wchar_t wpathname[MAX_PATH];
370 struct stat st;
371
@@ -381,7 +391,11 @@ int mingw_rmdir(const char *pathname)
391 if (xutftowcs_path(wpathname, pathname) < 0)
392 return -1;
393
384 - while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
394 + do {
395 + if (!_wrmdir(wpathname)) {
396 + invalidate_lstat_cache();
397 + return 0;
398 + }
399 if (!is_file_in_use_error(GetLastError()))
400 errno = err_win_to_posix(GetLastError());
401 if (errno != EACCES)
@@ -390,23 +404,9 @@ int mingw_rmdir(const char *pathname)
404 errno = ENOTEMPTY;
405 break;
406 }
393 - /*
394 - * We assume that some other process had the source or
395 - * destination file open at the wrong moment and retry.
396 - * In order to give the other process a higher chance to
397 - * complete its operation, we give up our time slice now.
398 - * If we have to retry again, we do sleep a bit.
399 - */
400 - Sleep(delay[tries]);
401 - tries++;
402 - }
403 - while (ret == -1 && errno == EACCES && is_file_in_use_error(GetLastError()) &&
404 - ask_yes_no_if_possible("Deletion of directory '%s' failed. "
405 - "Should I try again?", pathname))
406 - ret = _wrmdir(wpathname);
407 - if (!ret)
408 - invalidate_lstat_cache();
409 - return ret;
407 + } while (retry_ask_yes_no(&tries, "Deletion of directory '%s' failed. "
408 + "Should I try again?", pathname));
409 + return -1;
410 }
411
412 static inline int needs_hiding(const char *path)
@@ -2384,20 +2384,8 @@ repeat:
2384 SetFileAttributesW(wpnew, attrs);
2385 }
2386 }
2387 - if (tries < ARRAY_SIZE(delay) && gle == ERROR_ACCESS_DENIED) {
2388 - /*
2389 - * We assume that some other process had the source or
2390 - * destination file open at the wrong moment and retry.
2391 - * In order to give the other process a higher chance to
2392 - * complete its operation, we give up our time slice now.
2393 - * If we have to retry again, we do sleep a bit.
2394 - */
2395 - Sleep(delay[tries]);
2396 - tries++;
2397 - goto repeat;
2398 - }
2387 if (gle == ERROR_ACCESS_DENIED &&
2400 - ask_yes_no_if_possible("Rename from '%s' to '%s' failed. "
2388 + retry_ask_yes_no(&tries, "Rename from '%s' to '%s' failed. "
2389 "Should I try again?", pold, pnew))
2390 goto repeat;
2391