mingw: add support for symlinks to directories

Symlinks on Windows have a flag that indicates whether the target is a file or a directory. Symlinks of wrong type simply don't work. This even affects core Win32 APIs (e.g. `DeleteFile()` refuses to delete directory symlinks). However, `CreateFile()` with FILE_FLAG_BACKUP_SEMANTICS does work. Check the target type by first creating a tentative file symlink, opening it, and checking the type of the resulting handle. If it is a directory, recreate the symlink with the directory flag set. It is possible to create symlinks before the target exists (or in case of symlinks to symlinks: before the target type is known). If this happens, create a tentative file symlink and postpone the directory decision: keep a list of phantom symlinks to be processed whenever a new directory is created in `mingw_mkdir()`. Limitations: This algorithm may fail if a link target changes from file to directory or vice versa, or if the target directory is created in another process. It's the best Git can do, though. 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 97be7aa43a711646e66e1b11b43624e0940fe278
1 file changed +164
compat/mingw.c
+164
@@ -296,6 +296,131 @@ int mingw_core_config(const char *var, const char *value,
296 return 0;
297 }
298
299 +static inline int is_wdir_sep(wchar_t wchar)
300 +{
301 + return wchar == L'/' || wchar == L'\\';
302 +}
303 +
304 +static const wchar_t *make_relative_to(const wchar_t *path,
305 + const wchar_t *relative_to, wchar_t *out,
306 + size_t size)
307 +{
308 + size_t i = wcslen(relative_to), len;
309 +
310 + /* Is `path` already absolute? */
311 + if (is_wdir_sep(path[0]) ||
312 + (iswalpha(path[0]) && path[1] == L':' && is_wdir_sep(path[2])))
313 + return path;
314 +
315 + while (i > 0 && !is_wdir_sep(relative_to[i - 1]))
316 + i--;
317 +
318 + /* Is `relative_to` in the current directory? */
319 + if (!i)
320 + return path;
321 +
322 + len = wcslen(path);
323 + if (i + len + 1 > size) {
324 + error("Could not make '%ls' relative to '%ls' (too large)",
325 + path, relative_to);
326 + return NULL;
327 + }
328 +
329 + memcpy(out, relative_to, i * sizeof(wchar_t));
330 + wcscpy(out + i, path);
331 + return out;
332 +}
333 +
334 +enum phantom_symlink_result {
335 + PHANTOM_SYMLINK_RETRY,
336 + PHANTOM_SYMLINK_DONE,
337 + PHANTOM_SYMLINK_DIRECTORY
338 +};
339 +
340 +/*
341 + * Changes a file symlink to a directory symlink if the target exists and is a
342 + * directory.
343 + */
344 +static enum phantom_symlink_result
345 +process_phantom_symlink(const wchar_t *wtarget, const wchar_t *wlink)
346 +{
347 + HANDLE hnd;
348 + BY_HANDLE_FILE_INFORMATION fdata;
349 + wchar_t relative[MAX_PATH];
350 + const wchar_t *rel;
351 +
352 + /* check that wlink is still a file symlink */
353 + if ((GetFileAttributesW(wlink)
354 + & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))
355 + != FILE_ATTRIBUTE_REPARSE_POINT)
356 + return PHANTOM_SYMLINK_DONE;
357 +
358 + /* make it relative, if necessary */
359 + rel = make_relative_to(wtarget, wlink, relative, ARRAY_SIZE(relative));
360 + if (!rel)
361 + return PHANTOM_SYMLINK_DONE;
362 +
363 + /* let Windows resolve the link by opening it */
364 + hnd = CreateFileW(rel, 0,
365 + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
366 + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
367 + if (hnd == INVALID_HANDLE_VALUE) {
368 + errno = err_win_to_posix(GetLastError());
369 + return PHANTOM_SYMLINK_RETRY;
370 + }
371 +
372 + if (!GetFileInformationByHandle(hnd, &fdata)) {
373 + errno = err_win_to_posix(GetLastError());
374 + CloseHandle(hnd);
375 + return PHANTOM_SYMLINK_RETRY;
376 + }
377 + CloseHandle(hnd);
378 +
379 + /* if target exists and is a file, we're done */
380 + if (!(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
381 + return PHANTOM_SYMLINK_DONE;
382 +
383 + /* otherwise recreate the symlink with directory flag */
384 + if (DeleteFileW(wlink) && CreateSymbolicLinkW(wlink, wtarget, 1))
385 + return PHANTOM_SYMLINK_DIRECTORY;
386 +
387 + errno = err_win_to_posix(GetLastError());
388 + return PHANTOM_SYMLINK_RETRY;
389 +}
390 +
391 +/* keep track of newly created symlinks to non-existing targets */
392 +struct phantom_symlink_info {
393 + struct phantom_symlink_info *next;
394 + wchar_t *wlink;
395 + wchar_t *wtarget;
396 +};
397 +
398 +static struct phantom_symlink_info *phantom_symlinks = NULL;
399 +static CRITICAL_SECTION phantom_symlinks_cs;
400 +
401 +static void process_phantom_symlinks(void)
402 +{
403 + struct phantom_symlink_info *current, **psi;
404 + EnterCriticalSection(&phantom_symlinks_cs);
405 + /* process phantom symlinks list */
406 + psi = &phantom_symlinks;
407 + while ((current = *psi)) {
408 + enum phantom_symlink_result result = process_phantom_symlink(
409 + current->wtarget, current->wlink);
410 + if (result == PHANTOM_SYMLINK_RETRY) {
411 + psi = &current->next;
412 + } else {
413 + /* symlink was processed, remove from list */
414 + *psi = current->next;
415 + free(current);
416 + /* if symlink was a directory, start over */
417 + if (result == PHANTOM_SYMLINK_DIRECTORY)
418 + psi = &phantom_symlinks;
419 + }
420 + }
421 + LeaveCriticalSection(&phantom_symlinks_cs);
422 +}
423 +
424 /* Normalizes NT paths as returned by some low-level APIs. */
425 static wchar_t *normalize_ntpath(wchar_t *wbuf)
426 {
@@ -479,6 +604,8 @@ int mingw_mkdir(const char *path, int mode UNUSED)
604 if (xutftowcs_path(wpath, path) < 0)
605 return -1;
606 ret = _wmkdir(wpath);
607 + if (!ret)
608 + process_phantom_symlinks();
609 if (!ret && needs_hiding(path))
610 return set_hidden_flag(wpath, 1);
611 return ret;
@@ -2723,6 +2850,42 @@ int symlink(const char *target, const char *link)
2850 errno = err_win_to_posix(GetLastError());
2851 return -1;
2852 }
2853 +
2854 + /* convert to directory symlink if target exists */
2855 + switch (process_phantom_symlink(wtarget, wlink)) {
2856 + case PHANTOM_SYMLINK_RETRY: {
2857 + /* if target doesn't exist, add to phantom symlinks list */
2858 + wchar_t wfullpath[MAX_PATH];
2859 + struct phantom_symlink_info *psi;
2860 +
2861 + /* convert to absolute path to be independent of cwd */
2862 + len = GetFullPathNameW(wlink, MAX_PATH, wfullpath, NULL);
2863 + if (!len || len >= MAX_PATH) {
2864 + errno = err_win_to_posix(GetLastError());
2865 + return -1;
2866 + }
2867 +
2868 + /* over-allocate and fill phantom_symlink_info structure */
2869 + psi = xmalloc(sizeof(struct phantom_symlink_info)
2870 + + sizeof(wchar_t) * (len + wcslen(wtarget) + 2));
2871 + psi->wlink = (wchar_t *)(psi + 1);
2872 + wcscpy(psi->wlink, wfullpath);
2873 + psi->wtarget = psi->wlink + len + 1;
2874 + wcscpy(psi->wtarget, wtarget);
2875 +
2876 + EnterCriticalSection(&phantom_symlinks_cs);
2877 + psi->next = phantom_symlinks;
2878 + phantom_symlinks = psi;
2879 + LeaveCriticalSection(&phantom_symlinks_cs);
2880 + break;
2881 + }
2882 + case PHANTOM_SYMLINK_DIRECTORY:
2883 + /* if we created a dir symlink, process other phantom symlinks */
2884 + process_phantom_symlinks();
2885 + break;
2886 + default:
2887 + break;
2888 + }
2889 return 0;
2890 }
2891
@@ -3424,6 +3587,7 @@ int wmain(int argc, const wchar_t **wargv)
3587
3588 /* initialize critical section for waitpid pinfo_t list */
3589 InitializeCriticalSection(&pinfo_cs);
3590 + InitializeCriticalSection(&phantom_symlinks_cs);
3591
3592 /* set up default file mode and file modes for stdin/out/err */
3593 _fmode = _O_BINARY;