public inbox for git@vger.kernel.org 
 help / color / mirror / Atom feed
From: "Karsten Blees via GitGitGadget" <gitgitgadget@gmail•com>
To: git@vger•kernel.org
Cc: Ben Knoble <ben.knoble@gmail•com>, Johannes Sixt <j6t@kdbg•org>,
	Karsten Blees <karsten.blees@gmail•com>,
	Johannes Schindelin <johannes.schindelin@gmx•de>,
	Karsten Blees <karsten.blees@gmail•com>
Subject: [PATCH v2 15/18] mingw: add support for symlinks to directories
Date: Fri, 09 Jan 2026 20:05:12 +0000	[thread overview]
Message-ID: <3d479fd47e68242b028e1bbfd0019dfb0ededac8.1767989115.git.gitgitgadget@gmail.com> (raw)
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


  parent reply	other threads:[~2026-01-09 20:05 UTC|newest]

Thread overview: 51+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-12-17 14:08 [PATCH 00/18] Support symbolic links on Windows Johannes Schindelin via GitGitGadget
2025-12-17 14:08 ` [PATCH 01/18] mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()` Karsten Blees via GitGitGadget
2025-12-18 10:34   ` Johannes Sixt
2025-12-17 14:08 ` [PATCH 02/18] mingw: implement `stat()` with symlink support Karsten Blees via GitGitGadget
2025-12-18 10:44   ` Johannes Sixt
2026-01-09 20:04     ` Johannes Schindelin
2025-12-17 14:08 ` [PATCH 03/18] mingw: drop the separate `do_lstat()` function Karsten Blees via GitGitGadget
2025-12-18 10:48   ` Johannes Sixt
2025-12-17 14:08 ` [PATCH 04/18] mingw: let `mingw_lstat()` error early upon problems with reparse points Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 05/18] mingw: teach dirent about symlinks Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 06/18] mingw: compute the correct size for symlinks in `mingw_lstat()` Bill Zissimopoulos via GitGitGadget
2025-12-17 14:08 ` [PATCH 07/18] mingw: factor out the retry logic Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 08/18] mingw: change default of `core.symlinks` to false Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 09/18] mingw: add symlink-specific error codes Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 10/18] mingw: handle symlinks to directories in `mingw_unlink()` Karsten Blees via GitGitGadget
2025-12-18  2:49   ` Ben Knoble
2026-01-09 20:04     ` Johannes Schindelin
2025-12-17 14:08 ` [PATCH 11/18] mingw: support renaming symlinks Karsten Blees via GitGitGadget
2025-12-18 17:44   ` Johannes Sixt
2026-01-09 20:04     ` Johannes Schindelin
2025-12-17 14:08 ` [PATCH 12/18] mingw: allow `mingw_chdir()` to change to symlink-resolved directories Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 13/18] mingw: implement `readlink()` Karsten Blees via GitGitGadget
2025-12-18 18:13   ` Johannes Sixt
2026-01-09 20:04     ` Johannes Schindelin
2025-12-17 14:08 ` [PATCH 14/18] mingw: implement basic `symlink()` functionality (file symlinks only) Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 15/18] mingw: add support for symlinks to directories Karsten Blees via GitGitGadget
2025-12-17 14:08 ` [PATCH 16/18] mingw: try to create symlinks without elevated permissions Johannes Schindelin via GitGitGadget
2025-12-17 14:08 ` [PATCH 17/18] mingw: emulate `stat()` a little more faithfully Johannes Schindelin via GitGitGadget
2025-12-17 14:08 ` [PATCH 18/18] mingw: special-case index entries for symlinks with buggy size Johannes Schindelin via GitGitGadget
2025-12-18  0:00 ` [PATCH 00/18] Support symbolic links on Windows Junio C Hamano
2025-12-18 18:51 ` Johannes Sixt
2025-12-18 19:33   ` Karsten Blees
2026-01-09 20:04 ` [PATCH v2 " Johannes Schindelin via GitGitGadget
2026-01-09 20:04   ` [PATCH v2 01/18] mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()` Karsten Blees via GitGitGadget
2026-01-09 20:04   ` [PATCH v2 02/18] mingw: implement `stat()` with symlink support Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 03/18] mingw: drop the separate `do_lstat()` function Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 04/18] mingw: let `mingw_lstat()` error early upon problems with reparse points Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 05/18] mingw: teach dirent about symlinks Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 06/18] mingw: compute the correct size for symlinks in `mingw_lstat()` Bill Zissimopoulos via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 07/18] mingw: factor out the retry logic Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 08/18] mingw: change default of `core.symlinks` to false Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 09/18] mingw: add symlink-specific error codes Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 10/18] mingw: handle symlinks to directories in `mingw_unlink()` Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 11/18] mingw: support renaming symlinks Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 12/18] mingw: allow `mingw_chdir()` to change to symlink-resolved directories Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 13/18] mingw: implement `readlink()` Karsten Blees via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 14/18] mingw: implement basic `symlink()` functionality (file symlinks only) Karsten Blees via GitGitGadget
2026-01-09 20:05   ` Karsten Blees via GitGitGadget [this message]
2026-01-09 20:05   ` [PATCH v2 16/18] mingw: try to create symlinks without elevated permissions Johannes Schindelin via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 17/18] mingw: emulate `stat()` a little more faithfully Johannes Schindelin via GitGitGadget
2026-01-09 20:05   ` [PATCH v2 18/18] mingw: special-case index entries for symlinks with buggy size Johannes Schindelin via GitGitGadget

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=3d479fd47e68242b028e1bbfd0019dfb0ededac8.1767989115.git.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail$(echo .)com \
    --cc=ben.knoble@gmail$(echo .)com \
    --cc=git@vger$(echo .)kernel.org \
    --cc=j6t@kdbg$(echo .)org \
    --cc=johannes.schindelin@gmx$(echo .)de \
    --cc=karsten.blees@gmail$(echo .)com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox