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 = ¤t->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
next prev 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.com \
--cc=ben.knoble@gmail.com \
--cc=git@vger.kernel.org \
--cc=j6t@kdbg.org \
--cc=johannes.schindelin@gmx.de \
--cc=karsten.blees@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.