git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v2 15/18] mingw: add support for symlinks to directories

From
Karsten Blees via GitGitGadget <gitgitgadget@gmail.com>
Date
Jan 9, 2026, 20:05 UTC
Message-ID
<3d479fd47e68242b028e1bbfd0019dfb0ededac8.1767989115.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.2018.v2.git.1767989115.gitgitgadget@gmail.com>
From: Karsten Blees <karsten.blees@gmail.com>

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>
---
 compat/mingw.c | 164 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 164 insertions(+)
diff --git a/compat/mingw.c b/compat/mingw.c
index 8d366794c4..59a32e454e 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -296,6 +296,131 @@ int mingw_core_config(const char *var, const char *value,
 	return 0;
 }
 
+static inline int is_wdir_sep(wchar_t wchar)
+{
+	return wchar == L'/' || wchar == L'\\';
+}
+
+static const wchar_t *make_relative_to(const wchar_t *path,
+				       const wchar_t *relative_to, wchar_t *out,
+				       size_t size)
+{
+	size_t i = wcslen(relative_to), len;
+
+	/* Is `path` already absolute? */
+	if (is_wdir_sep(path[0]) ||
+	    (iswalpha(path[0]) && path[1] == L':' && is_wdir_sep(path[2])))
+		return path;
+
+	while (i > 0 && !is_wdir_sep(relative_to[i - 1]))
+		i--;
+
+	/* Is `relative_to` in the current directory? */
+	if (!i)
+		return path;
+
+	len = wcslen(path);
+	if (i + len + 1 > size) {
+		error("Could not make '%ls' relative to '%ls' (too large)",
+		      path, relative_to);
+		return NULL;
+	}
+
+	memcpy(out, relative_to, i * sizeof(wchar_t));
+	wcscpy(out + i, path);
+	return out;
+}
+
+enum phantom_symlink_result {
+	PHANTOM_SYMLINK_RETRY,
+	PHANTOM_SYMLINK_DONE,
+	PHANTOM_SYMLINK_DIRECTORY
+};
+
+/*
+ * Changes a file symlink to a directory symlink if the target exists and is a
+ * directory.
+ */
+static enum phantom_symlink_result
+process_phantom_symlink(const wchar_t *wtarget, const wchar_t *wlink)
+{
+	HANDLE hnd;
+	BY_HANDLE_FILE_INFORMATION fdata;
+	wchar_t relative[MAX_PATH];
+	const wchar_t *rel;
+
+	/* check that wlink is still a file symlink */
+	if ((GetFileAttributesW(wlink)
+			& (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))
+			!= FILE_ATTRIBUTE_REPARSE_POINT)
+		return PHANTOM_SYMLINK_DONE;
+
+	/* make it relative, if necessary */
+	rel = make_relative_to(wtarget, wlink, relative, ARRAY_SIZE(relative));
+	if (!rel)
+		return PHANTOM_SYMLINK_DONE;
+
+	/* let Windows resolve the link by opening it */
+	hnd = CreateFileW(rel, 0,
+			FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
+			OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
+	if (hnd == INVALID_HANDLE_VALUE) {
+		errno = err_win_to_posix(GetLastError());
+		return PHANTOM_SYMLINK_RETRY;
+	}
+
+	if (!GetFileInformationByHandle(hnd, &fdata)) {
+		errno = err_win_to_posix(GetLastError());
+		CloseHandle(hnd);
+		return PHANTOM_SYMLINK_RETRY;
+	}
+	CloseHandle(hnd);
+
+	/* if target exists and is a file, we're done */
+	if (!(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
+		return PHANTOM_SYMLINK_DONE;
+
+	/* otherwise recreate the symlink with directory flag */
+	if (DeleteFileW(wlink) && CreateSymbolicLinkW(wlink, wtarget, 1))
+		return PHANTOM_SYMLINK_DIRECTORY;
+
+	errno = err_win_to_posix(GetLastError());
+	return PHANTOM_SYMLINK_RETRY;
+}
+
+/* keep track of newly created symlinks to non-existing targets */
+struct phantom_symlink_info {
+	struct phantom_symlink_info *next;
+	wchar_t *wlink;
+	wchar_t *wtarget;
+};
+
+static struct phantom_symlink_info *phantom_symlinks = NULL;
+static CRITICAL_SECTION phantom_symlinks_cs;
+
+static void process_phantom_symlinks(void)
+{
+	struct phantom_symlink_info *current, **psi;
+	EnterCriticalSection(&phantom_symlinks_cs);
+	/* process phantom symlinks list */
+	psi = &phantom_symlinks;
+	while ((current = *psi)) {
+		enum phantom_symlink_result result = process_phantom_symlink(
+				current->wtarget, current->wlink);
+		if (result == PHANTOM_SYMLINK_RETRY) {
+			psi = &current->next;
+		} else {
+			/* symlink was processed, remove from list */
+			*psi = current->next;
+			free(current);
+			/* if symlink was a directory, start over */
+			if (result == PHANTOM_SYMLINK_DIRECTORY)
+				psi = &phantom_symlinks;
+		}
+	}
+	LeaveCriticalSection(&phantom_symlinks_cs);
+}
+
 /* Normalizes NT paths as returned by some low-level APIs. */
 static wchar_t *normalize_ntpath(wchar_t *wbuf)
 {
@@ -479,6 +604,8 @@ int mingw_mkdir(const char *path, int mode UNUSED)
 	if (xutftowcs_path(wpath, path) < 0)
 		return -1;
 	ret = _wmkdir(wpath);
+	if (!ret)
+		process_phantom_symlinks();
 	if (!ret && needs_hiding(path))
 		return set_hidden_flag(wpath, 1);
 	return ret;
@@ -2723,6 +2850,42 @@ int symlink(const char *target, const char *link)
 		errno = err_win_to_posix(GetLastError());
 		return -1;
 	}
+
+	/* convert to directory symlink if target exists */
+	switch (process_phantom_symlink(wtarget, wlink)) {
+	case PHANTOM_SYMLINK_RETRY:	{
+		/* if target doesn't exist, add to phantom symlinks list */
+		wchar_t wfullpath[MAX_PATH];
+		struct phantom_symlink_info *psi;
+
+		/* convert to absolute path to be independent of cwd */
+		len = GetFullPathNameW(wlink, MAX_PATH, wfullpath, NULL);
+		if (!len || len >= MAX_PATH) {
+			errno = err_win_to_posix(GetLastError());
+			return -1;
+		}
+
+		/* over-allocate and fill phantom_symlink_info structure */
+		psi = xmalloc(sizeof(struct phantom_symlink_info)
+			+ sizeof(wchar_t) * (len + wcslen(wtarget) + 2));
+		psi->wlink = (wchar_t *)(psi + 1);
+		wcscpy(psi->wlink, wfullpath);
+		psi->wtarget = psi->wlink + len + 1;
+		wcscpy(psi->wtarget, wtarget);
+
+		EnterCriticalSection(&phantom_symlinks_cs);
+		psi->next = phantom_symlinks;
+		phantom_symlinks = psi;
+		LeaveCriticalSection(&phantom_symlinks_cs);
+		break;
+	}
+	case PHANTOM_SYMLINK_DIRECTORY:
+		/* if we created a dir symlink, process other phantom symlinks */
+		process_phantom_symlinks();
+		break;
+	default:
+		break;
+	}
 	return 0;
 }
 
@@ -3424,6 +3587,7 @@ int wmain(int argc, const wchar_t **wargv)
 
 	/* initialize critical section for waitpid pinfo_t list */
 	InitializeCriticalSection(&pinfo_cs);
+	InitializeCriticalSection(&phantom_symlinks_cs);
 
 	/* set up default file mode and file modes for stdin/out/err */
 	_fmode = _O_BINARY;
-- 
gitgitgadget
Previous: Karsten Blees via GitGitGadgetNext: Johannes Schindelin via GitGitGadget
Message 48 of 51 in “Support symbolic links on Windows”
  1. 00/18 Support symbolic links on WindowsJohannes Schindelin via GitGitGadget, Dec 17, 2025
  2. 01/18 mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`Karsten Blees via GitGitGadget, Dec 17, 2025
  3. Johannes SixtDec 18, 2025
  4. 02/18 mingw: implement `stat()` with symlink supportKarsten Blees via GitGitGadget, Dec 17, 2025
  5. Johannes SixtDec 18, 2025
  6. Johannes SchindelinJan 9, 2026
  7. 03/18 mingw: drop the separate `do_lstat()` functionKarsten Blees via GitGitGadget, Dec 17, 2025
  8. Johannes SixtDec 18, 2025
  9. 04/18 mingw: let `mingw_lstat()` error early upon problems with reparse pointsKarsten Blees via GitGitGadget, Dec 17, 2025
  10. 05/18 mingw: teach dirent about symlinksKarsten Blees via GitGitGadget, Dec 17, 2025
  11. 06/18 mingw: compute the correct size for symlinks in `mingw_lstat()`Bill Zissimopoulos via GitGitGadget, Dec 17, 2025
  12. 07/18 mingw: factor out the retry logicKarsten Blees via GitGitGadget, Dec 17, 2025
  13. 08/18 mingw: change default of `core.symlinks` to falseKarsten Blees via GitGitGadget, Dec 17, 2025
  14. 09/18 mingw: add symlink-specific error codesKarsten Blees via GitGitGadget, Dec 17, 2025
  15. 10/18 mingw: handle symlinks to directories in `mingw_unlink()`Karsten Blees via GitGitGadget, Dec 17, 2025
  16. Ben KnobleDec 18, 2025
  17. Johannes SchindelinJan 9, 2026
  18. 11/18 mingw: support renaming symlinksKarsten Blees via GitGitGadget, Dec 17, 2025
  19. Johannes SixtDec 18, 2025
  20. Johannes SchindelinJan 9, 2026
  21. 12/18 mingw: allow `mingw_chdir()` to change to symlink-resolved directoriesKarsten Blees via GitGitGadget, Dec 17, 2025
  22. 13/18 mingw: implement `readlink()`Karsten Blees via GitGitGadget, Dec 17, 2025
  23. Johannes SixtDec 18, 2025
  24. Johannes SchindelinJan 9, 2026
  25. 14/18 mingw: implement basic `symlink()` functionality (file symlinks only)Karsten Blees via GitGitGadget, Dec 17, 2025
  26. 15/18 mingw: add support for symlinks to directoriesKarsten Blees via GitGitGadget, Dec 17, 2025
  27. 16/18 mingw: try to create symlinks without elevated permissionsJohannes Schindelin via GitGitGadget, Dec 17, 2025
  28. 17/18 mingw: emulate `stat()` a little more faithfullyJohannes Schindelin via GitGitGadget, Dec 17, 2025
  29. 18/18 mingw: special-case index entries for symlinks with buggy sizeJohannes Schindelin via GitGitGadget, Dec 17, 2025
  30. Junio C HamanoDec 18, 2025
  31. Johannes SixtDec 18, 2025
  32. Karsten BleesDec 18, 2025
  33. 00/18 Support symbolic links on WindowsJohannes Schindelin via GitGitGadget, Jan 9, 2026
  34. 01/18 mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`Karsten Blees via GitGitGadget, Jan 9, 2026
  35. 02/18 mingw: implement `stat()` with symlink supportKarsten Blees via GitGitGadget, Jan 9, 2026
  36. 03/18 mingw: drop the separate `do_lstat()` functionKarsten Blees via GitGitGadget, Jan 9, 2026
  37. 04/18 mingw: let `mingw_lstat()` error early upon problems with reparse pointsKarsten Blees via GitGitGadget, Jan 9, 2026
  38. 05/18 mingw: teach dirent about symlinksKarsten Blees via GitGitGadget, Jan 9, 2026
  39. 06/18 mingw: compute the correct size for symlinks in `mingw_lstat()`Bill Zissimopoulos via GitGitGadget, Jan 9, 2026
  40. 07/18 mingw: factor out the retry logicKarsten Blees via GitGitGadget, Jan 9, 2026
  41. 08/18 mingw: change default of `core.symlinks` to falseKarsten Blees via GitGitGadget, Jan 9, 2026
  42. 09/18 mingw: add symlink-specific error codesKarsten Blees via GitGitGadget, Jan 9, 2026
  43. 11/18 mingw: support renaming symlinksKarsten Blees via GitGitGadget, Jan 9, 2026
  44. 10/18 mingw: handle symlinks to directories in `mingw_unlink()`Karsten Blees via GitGitGadget, Jan 9, 2026
  45. 12/18 mingw: allow `mingw_chdir()` to change to symlink-resolved directoriesKarsten Blees via GitGitGadget, Jan 9, 2026
  46. 13/18 mingw: implement `readlink()`Karsten Blees via GitGitGadget, Jan 9, 2026
  47. 14/18 mingw: implement basic `symlink()` functionality (file symlinks only)Karsten Blees via GitGitGadget, Jan 9, 2026
  48. 15/18 mingw: add support for symlinks to directoriesKarsten Blees via GitGitGadget, Jan 9, 2026
  49. 16/18 mingw: try to create symlinks without elevated permissionsJohannes Schindelin via GitGitGadget, Jan 9, 2026
  50. 17/18 mingw: emulate `stat()` a little more faithfullyJohannes Schindelin via GitGitGadget, Jan 9, 2026
  51. 18/18 mingw: special-case index entries for symlinks with buggy sizeJohannes Schindelin via GitGitGadget, Jan 9, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.