{"thread":{"id":"64639","subject":"[PATCH 00/18] Support symbolic links on Windows","startedAt":"2025-12-17T14:08:58Z","lastAt":"2026-01-09T20:05:39Z","messageCount":51,"participants":["Johannes Schindelin via GitGitGadget","Karsten Blees via GitGitGadget","Bill Zissimopoulos via GitGitGadget","Junio C Hamano","Ben Knoble","Johannes Sixt","Karsten Blees","Johannes Schindelin"],"isPatch":true,"patchVersion":1,"patchTotal":18},"messages":[{"id":"532331","messageId":"pull.2018.git.1765980535.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":null,"subject":"[PATCH 00/18] Support symbolic links on Windows","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:37Z","receivedAt":"2025-12-17T14:08:58Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"This finally upstreams Git for Windows' support for Windows' branch of\nsymbolic links, which has been maturing since 2015. It is based off of\njs/prep-symlink-windows.\n\nBill Zissimopoulos (1):\n  mingw: compute the correct size for symlinks in `mingw_lstat()`\n\nJohannes Schindelin (3):\n  mingw: try to create symlinks without elevated permissions\n  mingw: emulate `stat()` a little more faithfully\n  mingw: special-case index entries for symlinks with buggy size\n\nKarsten Blees (14):\n  mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`\n  mingw: implement `stat()` with symlink support\n  mingw: drop the separate `do_lstat()` function\n  mingw: let `mingw_lstat()` error early upon problems with reparse\n    points\n  mingw: teach dirent about symlinks\n  mingw: factor out the retry logic\n  mingw: change default of `core.symlinks` to false\n  mingw: add symlink-specific error codes\n  mingw: handle symlinks to directories in `mingw_unlink()`\n  mingw: support renaming symlinks\n  mingw: allow `mingw_chdir()` to change to symlink-resolved directories\n  mingw: implement `readlink()`\n  mingw: implement basic `symlink()` functionality (file symlinks only)\n  mingw: add support for symlinks to directories\n\n compat/mingw-posix.h  |   6 +-\n compat/mingw.c        | 635 ++++++++++++++++++++++++++++++++----------\n compat/win32.h        |   6 +-\n compat/win32/dirent.c |   5 +-\n read-cache.c          |  11 +\n 5 files changed, 507 insertions(+), 156 deletions(-)\n\n\nbase-commit: 6f6fe02f5fe587ec9788f8a5a34281949d7b2ca1\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2018%2Fdscho%2Fsymlinks-next-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2018/dscho/symlinks-next-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2018\n-- \ngitgitgadget\n"},{"id":"532332","messageId":"dae450dd0e995b26d0140cf3753d103f5470f44d.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 01/18] mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:38Z","receivedAt":"2025-12-17T14:08:59Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nThe Win32 API function `GetFileAttributes()` cannot handle paths with\ntrailing dir separators. The current `mingw_stat()`/`mingw_lstat()`\nimplementation calls `GetFileAttributes()` twice if the path has\ntrailing slashes (first with the original path that was passed as\nfunction parameter, and and a second time with a path copy with trailing\n'/' removed).\n\nWith the conversion to wide Unicode, we get the length of the path for\nfree, and also have a (wide char) buffer that can be modified. This\nmakes it easy to avoid that extraneous Win32 API call.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 48 ++++++++++++------------------------------------\n 1 file changed, 12 insertions(+), 36 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex cf4f3c92e7..f5a0fe3325 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -928,9 +928,18 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)\n {\n \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n \twchar_t wfilename[MAX_PATH];\n-\tif (xutftowcs_path(wfilename, file_name) < 0)\n+\tint wlen = xutftowcs_path(wfilename, file_name);\n+\tif (wlen < 0)\n \t\treturn -1;\n \n+\t/* strip trailing '/', or GetFileAttributes will fail */\n+\twhile (wlen && is_dir_sep(wfilename[wlen - 1]))\n+\t\twfilename[--wlen] = 0;\n+\tif (!wlen) {\n+\t\terrno = ENOENT;\n+\t\treturn -1;\n+\t}\n+\n \tif (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {\n \t\tbuf->st_ino = 0;\n \t\tbuf->st_gid = 0;\n@@ -990,39 +999,6 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)\n \treturn -1;\n }\n \n-/* We provide our own lstat/fstat functions, since the provided\n- * lstat/fstat functions are so slow. These stat functions are\n- * tailored for Git's usage (read: fast), and are not meant to be\n- * complete. Note that Git stat()s are redirected to mingw_lstat()\n- * too, since Windows doesn't really handle symlinks that well.\n- */\n-static int do_stat_internal(int follow, const char *file_name, struct stat *buf)\n-{\n-\tsize_t namelen;\n-\tchar alt_name[PATH_MAX];\n-\n-\tif (!do_lstat(follow, file_name, buf))\n-\t\treturn 0;\n-\n-\t/* if file_name ended in a '/', Windows returned ENOENT;\n-\t * try again without trailing slashes\n-\t */\n-\tif (errno != ENOENT)\n-\t\treturn -1;\n-\n-\tnamelen = strlen(file_name);\n-\tif (namelen && file_name[namelen-1] != '/')\n-\t\treturn -1;\n-\twhile (namelen && file_name[namelen-1] == '/')\n-\t\t--namelen;\n-\tif (!namelen || namelen >= PATH_MAX)\n-\t\treturn -1;\n-\n-\tmemcpy(alt_name, file_name, namelen);\n-\talt_name[namelen] = 0;\n-\treturn do_lstat(follow, alt_name, buf);\n-}\n-\n static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)\n {\n \tBY_HANDLE_FILE_INFORMATION fdata;\n@@ -1048,11 +1024,11 @@ static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)\n \n int mingw_lstat(const char *file_name, struct stat *buf)\n {\n-\treturn do_stat_internal(0, file_name, buf);\n+\treturn do_lstat(0, file_name, buf);\n }\n int mingw_stat(const char *file_name, struct stat *buf)\n {\n-\treturn do_stat_internal(1, file_name, buf);\n+\treturn do_lstat(1, file_name, buf);\n }\n \n int mingw_fstat(int fd, struct stat *buf)\n-- \ngitgitgadget\n\n"},{"id":"532333","messageId":"c36848eda76742f7a7c203868a077b790301344f.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 02/18] mingw: implement `stat()` with symlink support","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:39Z","receivedAt":"2025-12-17T14:09:01Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nWith respect to symlinks, the current `mingw_stat()` implementation is\nalmost identical to `mingw_lstat()`: except for the file type (`st_mode\n& S_IFMT`), it returns information about the link rather than the target.\n\nImplement `mingw_stat()` by opening the file handle requesting minimal\npermissions, and then calling `GetFileInformationByHandle()` on it. This\nway, all links are resolved by the Windows file system layer.\n\nIf symlinks are disabled, use `mingw_lstat()` as before, but fail with\n`ELOOP` if a symlink would have to be resolved.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 19 ++++++++++++++++++-\n 1 file changed, 18 insertions(+), 1 deletion(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex f5a0fe3325..59afd69686 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -1026,9 +1026,26 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n {\n \treturn do_lstat(0, file_name, buf);\n }\n+\n int mingw_stat(const char *file_name, struct stat *buf)\n {\n-\treturn do_lstat(1, file_name, buf);\n+\twchar_t wfile_name[MAX_PATH];\n+\tHANDLE hnd;\n+\tint result;\n+\n+\t/* open the file and let Windows resolve the links */\n+\tif (xutftowcs_path(wfile_name, file_name) < 0)\n+\t\treturn -1;\n+\thnd = CreateFileW(wfile_name, 0,\n+\t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n+\t\t\tOPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);\n+\tif (hnd == INVALID_HANDLE_VALUE) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n+\t}\n+\tresult = get_file_info_by_handle(hnd, buf);\n+\tCloseHandle(hnd);\n+\treturn result;\n }\n \n int mingw_fstat(int fd, struct stat *buf)\n-- \ngitgitgadget\n\n"},{"id":"532334","messageId":"aa0ca80bbb00ec53f359c79719b95bcc141ac85c.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 03/18] mingw: drop the separate `do_lstat()` function","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:40Z","receivedAt":"2025-12-17T14:09:02Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nWith the new `mingw_stat()` implementation, `do_lstat()` is only called\nfrom `mingw_lstat()` (with the function parameter `follow == 0`). Remove\nthe extra function and the old `mingw_stat()`-specific (`follow == 1`)\nlogic.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 22 ++--------------------\n 1 file changed, 2 insertions(+), 20 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 59afd69686..ec6c2801d3 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -917,14 +917,7 @@ static int has_valid_directory_prefix(wchar_t *wfilename)\n \treturn 1;\n }\n \n-/* We keep the do_lstat code in a separate function to avoid recursion.\n- * When a path ends with a slash, the stat will fail with ENOENT. In\n- * this case, we strip the trailing slashes and stat again.\n- *\n- * If follow is true then act like stat() and report on the link\n- * target. Otherwise report on the link itself.\n- */\n-static int do_lstat(int follow, const char *file_name, struct stat *buf)\n+int mingw_lstat(const char *file_name, struct stat *buf)\n {\n \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n \twchar_t wfilename[MAX_PATH];\n@@ -958,13 +951,7 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)\n \t\t\tif (handle != INVALID_HANDLE_VALUE) {\n \t\t\t\tif ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&\n \t\t\t\t\t\t(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {\n-\t\t\t\t\tif (follow) {\n-\t\t\t\t\t\tchar buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];\n-\t\t\t\t\t\tbuf->st_size = readlink(file_name, buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE);\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\tbuf->st_mode = S_IFLNK;\n-\t\t\t\t\t}\n-\t\t\t\t\tbuf->st_mode |= S_IREAD;\n+\t\t\t\t\tbuf->st_mode = S_IFLNK | S_IREAD;\n \t\t\t\t\tif (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))\n \t\t\t\t\t\tbuf->st_mode |= S_IWRITE;\n \t\t\t\t}\n@@ -1022,11 +1009,6 @@ static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)\n \treturn 0;\n }\n \n-int mingw_lstat(const char *file_name, struct stat *buf)\n-{\n-\treturn do_lstat(0, file_name, buf);\n-}\n-\n int mingw_stat(const char *file_name, struct stat *buf)\n {\n \twchar_t wfile_name[MAX_PATH];\n-- \ngitgitgadget\n\n"},{"id":"532335","messageId":"886044373be1bd4d2e7ecf758c42474e9efbe067.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 04/18] mingw: let `mingw_lstat()` error early upon problems with reparse points","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:41Z","receivedAt":"2025-12-17T14:09:03Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nWhen obtaining lstat information for reparse points, we need to call\n`FindFirstFile()` in addition to `GetFileInformationEx()` to obtain\nthe type of the reparse point (symlink, mount point etc.). However,\ncurrently there is no error handling whatsoever if `FindFirstFile()`\nfails.\n\nCall `FindFirstFile()` before modifying the `stat *buf` output parameter\nand error out if the call fails.\n\nNote: The `FindFirstFile()` return value includes all the data\nthat we get from `GetFileAttributesEx()`, so we could replace\n`GetFileAttributesEx()` with `FindFirstFile()`. We don't do that because\n`GetFileAttributesEx()` is about twice as fast for single files. I.e.\nwe only pay the extra cost of calling `FindFirstFile()` in the rare case\nthat we encounter a reparse point.\n\nPlease also note that the indentation the remaining reparse point\ncode changed, and hence the best way to look at this diff is with\n`--color-moved -w`. That code was _not_ moved because a subsequent\ncommit will move it to an altogether different function, anyway.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 24 ++++++++++++++----------\n 1 file changed, 14 insertions(+), 10 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex ec6c2801d3..23a926c7d1 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -920,6 +920,7 @@ static int has_valid_directory_prefix(wchar_t *wfilename)\n int mingw_lstat(const char *file_name, struct stat *buf)\n {\n \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n+\tWIN32_FIND_DATAW findbuf = { 0 };\n \twchar_t wfilename[MAX_PATH];\n \tint wlen = xutftowcs_path(wfilename, file_name);\n \tif (wlen < 0)\n@@ -934,6 +935,13 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n \t}\n \n \tif (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {\n+\t\t/* for reparse points, use FindFirstFile to get the reparse tag */\n+\t\tif (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {\n+\t\t\tHANDLE handle = FindFirstFileW(wfilename, &findbuf);\n+\t\t\tif (handle == INVALID_HANDLE_VALUE)\n+\t\t\t\tgoto error;\n+\t\t\tFindClose(handle);\n+\t\t}\n \t\tbuf->st_ino = 0;\n \t\tbuf->st_gid = 0;\n \t\tbuf->st_uid = 0;\n@@ -946,20 +954,16 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n \t\tfiletime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim));\n \t\tfiletime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim));\n \t\tif (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {\n-\t\t\tWIN32_FIND_DATAW findbuf;\n-\t\t\tHANDLE handle = FindFirstFileW(wfilename, &findbuf);\n-\t\t\tif (handle != INVALID_HANDLE_VALUE) {\n-\t\t\t\tif ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&\n-\t\t\t\t\t\t(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {\n-\t\t\t\t\tbuf->st_mode = S_IFLNK | S_IREAD;\n-\t\t\t\t\tif (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))\n-\t\t\t\t\t\tbuf->st_mode |= S_IWRITE;\n-\t\t\t\t}\n-\t\t\t\tFindClose(handle);\n+\t\t\tif ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&\n+\t\t\t\t\t(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {\n+\t\t\t\tbuf->st_mode = S_IFLNK | S_IREAD;\n+\t\t\t\tif (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))\n+\t\t\t\t\tbuf->st_mode |= S_IWRITE;\n \t\t\t}\n \t\t}\n \t\treturn 0;\n \t}\n+error:\n \tswitch (GetLastError()) {\n \tcase ERROR_ACCESS_DENIED:\n \tcase ERROR_SHARING_VIOLATION:\n-- \ngitgitgadget\n\n"},{"id":"532336","messageId":"db1d156aa01826c8371e1ebdfeafa65744b66010.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 05/18] mingw: teach dirent about symlinks","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:42Z","receivedAt":"2025-12-17T14:09:04Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nMove the `S_IFLNK` detection to `file_attr_to_st_mode()`.\n\nImplement `DT_LNK` detection in dirent.c's `readdir()` function.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c        | 13 +++----------\n compat/win32.h        |  6 ++++--\n compat/win32/dirent.c |  5 ++++-\n 3 files changed, 11 insertions(+), 13 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 23a926c7d1..a3a48db581 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -946,21 +946,14 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n \t\tbuf->st_gid = 0;\n \t\tbuf->st_uid = 0;\n \t\tbuf->st_nlink = 1;\n-\t\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes);\n+\t\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes,\n+\t\t\t\tfindbuf.dwReserved0);\n \t\tbuf->st_size = fdata.nFileSizeLow |\n \t\t\t(((off_t)fdata.nFileSizeHigh)<<32);\n \t\tbuf->st_dev = buf->st_rdev = 0; /* not used by Git */\n \t\tfiletime_to_timespec(&(fdata.ftLastAccessTime), &(buf->st_atim));\n \t\tfiletime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim));\n \t\tfiletime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim));\n-\t\tif (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {\n-\t\t\tif ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&\n-\t\t\t\t\t(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {\n-\t\t\t\tbuf->st_mode = S_IFLNK | S_IREAD;\n-\t\t\t\tif (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))\n-\t\t\t\t\tbuf->st_mode |= S_IWRITE;\n-\t\t\t}\n-\t\t}\n \t\treturn 0;\n \t}\n error:\n@@ -1003,7 +996,7 @@ static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)\n \tbuf->st_gid = 0;\n \tbuf->st_uid = 0;\n \tbuf->st_nlink = 1;\n-\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes);\n+\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes, 0);\n \tbuf->st_size = fdata.nFileSizeLow |\n \t\t(((off_t)fdata.nFileSizeHigh)<<32);\n \tbuf->st_dev = buf->st_rdev = 0; /* not used by Git */\ndiff --git a/compat/win32.h b/compat/win32.h\nindex a97e880757..671bcc81f9 100644\n--- a/compat/win32.h\n+++ b/compat/win32.h\n@@ -6,10 +6,12 @@\n #include <windows.h>\n #endif\n \n-static inline int file_attr_to_st_mode (DWORD attr)\n+static inline int file_attr_to_st_mode (DWORD attr, DWORD tag)\n {\n \tint fMode = S_IREAD;\n-\tif (attr & FILE_ATTRIBUTE_DIRECTORY)\n+\tif ((attr & FILE_ATTRIBUTE_REPARSE_POINT) && tag == IO_REPARSE_TAG_SYMLINK)\n+\t\tfMode |= S_IFLNK;\n+\telse if (attr & FILE_ATTRIBUTE_DIRECTORY)\n \t\tfMode |= S_IFDIR;\n \telse\n \t\tfMode |= S_IFREG;\ndiff --git a/compat/win32/dirent.c b/compat/win32/dirent.c\nindex 52420ec7d4..24ee9b814d 100644\n--- a/compat/win32/dirent.c\n+++ b/compat/win32/dirent.c\n@@ -12,7 +12,10 @@ static inline void finddata2dirent(struct dirent *ent, WIN32_FIND_DATAW *fdata)\n \txwcstoutf(ent->d_name, fdata->cFileName, sizeof(ent->d_name));\n \n \t/* Set file type, based on WIN32_FIND_DATA */\n-\tif (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)\n+\tif ((fdata->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)\n+\t\t\t&& fdata->dwReserved0 == IO_REPARSE_TAG_SYMLINK)\n+\t\tent->d_type = DT_LNK;\n+\telse if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)\n \t\tent->d_type = DT_DIR;\n \telse\n \t\tent->d_type = DT_REG;\n-- \ngitgitgadget\n\n"},{"id":"532337","messageId":"4c49a3d9bf57415ea31ac357452b646c75b64d82.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 06/18] mingw: compute the correct size for symlinks in `mingw_lstat()`","fromName":"Bill Zissimopoulos via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:43Z","receivedAt":"2025-12-17T14:09:06Z","isPatch":true,"sender":{"key":"name:Bill Zissimopoulos","avatar":null},"body":"From: Bill Zissimopoulos <billziss@navimatics.com>\n\nPOSIX specifies that upon successful return from `lstat()`: \"the\nvalue of the st_size member shall be set to the length of the pathname\ncontained in the symbolic link not including any terminating null byte\".\n\nGit typically doesn't trust the `stat.st_size` member of symlinks (e.g.\nsee `strbuf_readlink()`). Therefore, it is tempting to save on the extra\noverhead of opening and reading the reparse point merely to calculate\nthe exact size of the link target.\n\nThis is, in fact, what Git for Windows did, from May 2015 to May 2020.\nAt least almost: some functions take shortcuts if `st_size` is 0 (e.g.\n`diff_populate_filespec()`), hence Git for Windows hard-coded the length\nof all symlinks to MAX_PATH.\n\nThis did cause problems, though, specifically in Git repositories\nthat were also accessed by Git for Cygwin or Git for WSL. For example,\ndoing `git reset --hard` using Git for Windows would update the size of\nsymlinks in the index to be MAX_PATH; at a later time Git for Cygwin\nor Git for WSL would find that symlinks have changed size during `git\nstatus` and update the index. And then Git for Windows would think that\nthe index needs to be updated. Even if the symlinks did not, in fact,\nchange. To avoid that, the correct size must be determined.\n\nSigned-off-by: Bill Zissimopoulos <billziss@navimatics.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 114 ++++++++++++++++++++++++++++++++++++++++++++-----\n 1 file changed, 104 insertions(+), 10 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex a3a48db581..c7571951dc 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -21,6 +21,7 @@\n #define SECURITY_WIN32\n #include <sspi.h>\n #include <wchar.h>\n+#include <winioctl.h>\n #include <winternl.h>\n \n #define STATUS_DELETE_PENDING ((NTSTATUS) 0xC0000056)\n@@ -917,10 +918,102 @@ static int has_valid_directory_prefix(wchar_t *wfilename)\n \treturn 1;\n }\n \n+#ifndef _WINNT_H\n+/*\n+ * The REPARSE_DATA_BUFFER structure is defined in the Windows DDK (in\n+ * ntifs.h) and in MSYS1's winnt.h (which defines _WINNT_H). So define\n+ * it ourselves if we are on MSYS2 (whose winnt.h defines _WINNT_).\n+ */\n+typedef struct _REPARSE_DATA_BUFFER {\n+\tDWORD  ReparseTag;\n+\tWORD   ReparseDataLength;\n+\tWORD   Reserved;\n+#ifndef _MSC_VER\n+\t_ANONYMOUS_UNION\n+#endif\n+\tunion {\n+\t\tstruct {\n+\t\t\tWORD   SubstituteNameOffset;\n+\t\t\tWORD   SubstituteNameLength;\n+\t\t\tWORD   PrintNameOffset;\n+\t\t\tWORD   PrintNameLength;\n+\t\t\tULONG  Flags;\n+\t\t\tWCHAR PathBuffer[1];\n+\t\t} SymbolicLinkReparseBuffer;\n+\t\tstruct {\n+\t\t\tWORD   SubstituteNameOffset;\n+\t\t\tWORD   SubstituteNameLength;\n+\t\t\tWORD   PrintNameOffset;\n+\t\t\tWORD   PrintNameLength;\n+\t\t\tWCHAR PathBuffer[1];\n+\t\t} MountPointReparseBuffer;\n+\t\tstruct {\n+\t\t\tBYTE   DataBuffer[1];\n+\t\t} GenericReparseBuffer;\n+\t} DUMMYUNIONNAME;\n+} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;\n+#endif\n+\n+static int read_reparse_point(const WCHAR *wpath, BOOL fail_on_unknown_tag,\n+\t\t\t      char *tmpbuf, int *plen, DWORD *ptag)\n+{\n+\tHANDLE handle;\n+\tWCHAR *wbuf;\n+\tREPARSE_DATA_BUFFER *b = alloca(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);\n+\tDWORD dummy;\n+\n+\t/* read reparse point data */\n+\thandle = CreateFileW(wpath, 0,\n+\t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n+\t\t\tOPEN_EXISTING,\n+\t\t\tFILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);\n+\tif (handle == INVALID_HANDLE_VALUE) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n+\t}\n+\tif (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, b,\n+\t\t\tMAXIMUM_REPARSE_DATA_BUFFER_SIZE, &dummy, NULL)) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\tCloseHandle(handle);\n+\t\treturn -1;\n+\t}\n+\tCloseHandle(handle);\n+\n+\t/* get target path for symlinks or mount points (aka 'junctions') */\n+\tswitch ((*ptag = b->ReparseTag)) {\n+\tcase IO_REPARSE_TAG_SYMLINK:\n+\t\twbuf = (WCHAR*) (((char*) b->SymbolicLinkReparseBuffer.PathBuffer)\n+\t\t\t\t+ b->SymbolicLinkReparseBuffer.SubstituteNameOffset);\n+\t\t*(WCHAR*) (((char*) wbuf)\n+\t\t\t\t+ b->SymbolicLinkReparseBuffer.SubstituteNameLength) = 0;\n+\t\tbreak;\n+\tcase IO_REPARSE_TAG_MOUNT_POINT:\n+\t\twbuf = (WCHAR*) (((char*) b->MountPointReparseBuffer.PathBuffer)\n+\t\t\t\t+ b->MountPointReparseBuffer.SubstituteNameOffset);\n+\t\t*(WCHAR*) (((char*) wbuf)\n+\t\t\t\t+ b->MountPointReparseBuffer.SubstituteNameLength) = 0;\n+\t\tbreak;\n+\tdefault:\n+\t\tif (fail_on_unknown_tag) {\n+\t\t\terrno = EINVAL;\n+\t\t\treturn -1;\n+\t\t} else {\n+\t\t\t*plen = MAX_PATH;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\tif ((*plen =\n+\t     xwcstoutf(tmpbuf, normalize_ntpath(wbuf), MAX_PATH)) <  0)\n+\t\treturn -1;\n+\treturn 0;\n+}\n+\n int mingw_lstat(const char *file_name, struct stat *buf)\n {\n \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n-\tWIN32_FIND_DATAW findbuf = { 0 };\n+\tDWORD reparse_tag = 0;\n+\tint link_len = 0;\n \twchar_t wfilename[MAX_PATH];\n \tint wlen = xutftowcs_path(wfilename, file_name);\n \tif (wlen < 0)\n@@ -935,28 +1028,29 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n \t}\n \n \tif (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {\n-\t\t/* for reparse points, use FindFirstFile to get the reparse tag */\n+\t\t/* for reparse points, get the link tag and length */\n \t\tif (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {\n-\t\t\tHANDLE handle = FindFirstFileW(wfilename, &findbuf);\n-\t\t\tif (handle == INVALID_HANDLE_VALUE)\n-\t\t\t\tgoto error;\n-\t\t\tFindClose(handle);\n+\t\t\tchar tmpbuf[MAX_PATH];\n+\n+\t\t\tif (read_reparse_point(wfilename, FALSE, tmpbuf,\n+\t\t\t\t\t       &link_len, &reparse_tag) < 0)\n+\t\t\t\treturn -1;\n \t\t}\n \t\tbuf->st_ino = 0;\n \t\tbuf->st_gid = 0;\n \t\tbuf->st_uid = 0;\n \t\tbuf->st_nlink = 1;\n \t\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes,\n-\t\t\t\tfindbuf.dwReserved0);\n-\t\tbuf->st_size = fdata.nFileSizeLow |\n-\t\t\t(((off_t)fdata.nFileSizeHigh)<<32);\n+\t\t\t\treparse_tag);\n+\t\tbuf->st_size = S_ISLNK(buf->st_mode) ? link_len :\n+\t\t\tfdata.nFileSizeLow | (((off_t) fdata.nFileSizeHigh) << 32);\n \t\tbuf->st_dev = buf->st_rdev = 0; /* not used by Git */\n \t\tfiletime_to_timespec(&(fdata.ftLastAccessTime), &(buf->st_atim));\n \t\tfiletime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim));\n \t\tfiletime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim));\n \t\treturn 0;\n \t}\n-error:\n+\n \tswitch (GetLastError()) {\n \tcase ERROR_ACCESS_DENIED:\n \tcase ERROR_SHARING_VIOLATION:\n-- \ngitgitgadget\n\n"},{"id":"532338","messageId":"ad74d540f2f6bd19f248606e29ae45d226c264f9.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 07/18] mingw: factor out the retry logic","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:44Z","receivedAt":"2025-12-17T14:09:06Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nIn several places, Git's Windows-specific code follows the pattern where\nit tries to perform an operation, and retries several times when that\noperation fails, sleeping an increasing amount of time, before finally\ngiving up and asking the user whether to rety (after, say, closing an\neditor that held a handle to a file, preventing the operation from\nsucceeding).\n\nThis logic is a bit hard to use, and inconsistent:\n`mingw_unlink()` and `mingw_rmdir()` duplicate the code to retry,\nand both of them do so incompletely. They also do not restore `errno` if the\nuser answers 'no'.\n\nIntroduce a `retry_ask_yes_no()` helper function that handles retry with\nsmall delay, asking the user, and restoring `errno`.\n\nNote that in `mingw_unlink()`, we include the `_wchmod()` call in the\nretry loop (which may fail if the file is locked exclusively).\n\nIn `mingw_rmdir()`, we include special error handling in the retry loop.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 104 ++++++++++++++++++++++---------------------------\n 1 file changed, 46 insertions(+), 58 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex c7571951dc..26e64c6a5a 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -28,8 +28,6 @@\n \n #define HCAST(type, handle) ((type)(intptr_t)handle)\n \n-static const int delay[] = { 0, 1, 10, 20, 40 };\n-\n void open_in_gdb(void)\n {\n \tstatic struct child_process cp = CHILD_PROCESS_INIT;\n@@ -205,15 +203,12 @@ static int read_yes_no_answer(void)\n \treturn -1;\n }\n \n-static int ask_yes_no_if_possible(const char *format, ...)\n+static int ask_yes_no_if_possible(const char *format, va_list args)\n {\n \tchar question[4096];\n \tconst char *retry_hook;\n-\tva_list args;\n \n-\tva_start(args, format);\n \tvsnprintf(question, sizeof(question), format, args);\n-\tva_end(args);\n \n \tretry_hook = mingw_getenv(\"GIT_ASK_YESNO\");\n \tif (retry_hook) {\n@@ -238,6 +233,31 @@ static int ask_yes_no_if_possible(const char *format, ...)\n \t}\n }\n \n+static int retry_ask_yes_no(int *tries, const char *format, ...)\n+{\n+\tstatic const int delay[] = { 0, 1, 10, 20, 40 };\n+\tva_list args;\n+\tint result, saved_errno = errno;\n+\n+\tif ((*tries) < ARRAY_SIZE(delay)) {\n+\t\t/*\n+\t\t * We assume that some other process had the file open at the wrong\n+\t\t * moment and retry. In order to give the other process a higher\n+\t\t * chance to complete its operation, we give up our time slice now.\n+\t\t * If we have to retry again, we do sleep a bit.\n+\t\t */\n+\t\tSleep(delay[*tries]);\n+\t\t(*tries)++;\n+\t\treturn 1;\n+\t}\n+\n+\tva_start(args, format);\n+\tresult = ask_yes_no_if_possible(format, args);\n+\tva_end(args);\n+\terrno = saved_errno;\n+\treturn result;\n+}\n+\n /* Windows only */\n enum hide_dotfiles_type {\n \tHIDE_DOTFILES_FALSE = 0,\n@@ -298,7 +318,7 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)\n \n int mingw_unlink(const char *pathname, int handle_in_use_error)\n {\n-\tint ret, tries = 0;\n+\tint tries = 0;\n \twchar_t wpathname[MAX_PATH];\n \tif (xutftowcs_path(wpathname, pathname) < 0)\n \t\treturn -1;\n@@ -306,29 +326,19 @@ int mingw_unlink(const char *pathname, int handle_in_use_error)\n \tif (DeleteFileW(wpathname))\n \t\treturn 0;\n \n-\t/* read-only files cannot be removed */\n-\t_wchmod(wpathname, 0666);\n-\twhile ((ret = _wunlink(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {\n+\tdo {\n+\t\t/* read-only files cannot be removed */\n+\t\t_wchmod(wpathname, 0666);\n+\t\tif (!_wunlink(wpathname))\n+\t\t\treturn 0;\n \t\tif (!is_file_in_use_error(GetLastError()))\n \t\t\tbreak;\n \t\tif (!handle_in_use_error)\n-\t\t\treturn ret;\n+\t\t\treturn -1;\n \n-\t\t/*\n-\t\t * We assume that some other process had the source or\n-\t\t * destination file open at the wrong moment and retry.\n-\t\t * In order to give the other process a higher chance to\n-\t\t * complete its operation, we give up our time slice now.\n-\t\t * If we have to retry again, we do sleep a bit.\n-\t\t */\n-\t\tSleep(delay[tries]);\n-\t\ttries++;\n-\t}\n-\twhile (ret == -1 && is_file_in_use_error(GetLastError()) &&\n-\t       ask_yes_no_if_possible(\"Unlink of file '%s' failed. \"\n-\t\t\t\"Should I try again?\", pathname))\n-\t       ret = _wunlink(wpathname);\n-\treturn ret;\n+\t} while (retry_ask_yes_no(&tries, \"Unlink of file '%s' failed. \"\n+\t\t\t\"Should I try again?\", pathname));\n+\treturn -1;\n }\n \n static int is_dir_empty(const wchar_t *wpath)\n@@ -355,7 +365,7 @@ static int is_dir_empty(const wchar_t *wpath)\n \n int mingw_rmdir(const char *pathname)\n {\n-\tint ret, tries = 0;\n+\tint tries = 0;\n \twchar_t wpathname[MAX_PATH];\n \tstruct stat st;\n \n@@ -381,7 +391,11 @@ int mingw_rmdir(const char *pathname)\n \tif (xutftowcs_path(wpathname, pathname) < 0)\n \t\treturn -1;\n \n-\twhile ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {\n+\tdo {\n+\t\tif (!_wrmdir(wpathname)) {\n+\t\t\tinvalidate_lstat_cache();\n+\t\t\treturn 0;\n+\t\t}\n \t\tif (!is_file_in_use_error(GetLastError()))\n \t\t\terrno = err_win_to_posix(GetLastError());\n \t\tif (errno != EACCES)\n@@ -390,23 +404,9 @@ int mingw_rmdir(const char *pathname)\n \t\t\terrno = ENOTEMPTY;\n \t\t\tbreak;\n \t\t}\n-\t\t/*\n-\t\t * We assume that some other process had the source or\n-\t\t * destination file open at the wrong moment and retry.\n-\t\t * In order to give the other process a higher chance to\n-\t\t * complete its operation, we give up our time slice now.\n-\t\t * If we have to retry again, we do sleep a bit.\n-\t\t */\n-\t\tSleep(delay[tries]);\n-\t\ttries++;\n-\t}\n-\twhile (ret == -1 && errno == EACCES && is_file_in_use_error(GetLastError()) &&\n-\t       ask_yes_no_if_possible(\"Deletion of directory '%s' failed. \"\n-\t\t\t\"Should I try again?\", pathname))\n-\t       ret = _wrmdir(wpathname);\n-\tif (!ret)\n-\t\tinvalidate_lstat_cache();\n-\treturn ret;\n+\t} while (retry_ask_yes_no(&tries, \"Deletion of directory '%s' failed. \"\n+\t\t\t\"Should I try again?\", pathname));\n+\treturn -1;\n }\n \n static inline int needs_hiding(const char *path)\n@@ -2384,20 +2384,8 @@ repeat:\n \t\t\tSetFileAttributesW(wpnew, attrs);\n \t\t}\n \t}\n-\tif (tries < ARRAY_SIZE(delay) && gle == ERROR_ACCESS_DENIED) {\n-\t\t/*\n-\t\t * We assume that some other process had the source or\n-\t\t * destination file open at the wrong moment and retry.\n-\t\t * In order to give the other process a higher chance to\n-\t\t * complete its operation, we give up our time slice now.\n-\t\t * If we have to retry again, we do sleep a bit.\n-\t\t */\n-\t\tSleep(delay[tries]);\n-\t\ttries++;\n-\t\tgoto repeat;\n-\t}\n \tif (gle == ERROR_ACCESS_DENIED &&\n-\t       ask_yes_no_if_possible(\"Rename from '%s' to '%s' failed. \"\n+\t       retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n \t\t       \"Should I try again?\", pold, pnew))\n \t\tgoto repeat;\n \n-- \ngitgitgadget\n\n"},{"id":"532341","messageId":"25313cea76e059d01a0607d0d5291b62011f4526.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 08/18] mingw: change default of `core.symlinks` to false","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:45Z","receivedAt":"2025-12-17T14:09:08Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nSymlinks on Windows don't work the same way as on Unix systems. For\nexample, there are different types of symlinks for directories and\nfiles, and unless using a recent-ish Windows version in Developer Mode,\ncreating symlinks requires administrative privileges.\n\nBy default, disable symlink support on Windows. That is, users\nexplicitly have to enable it with `git config [--system|--global]\ncore.symlinks true`; For convenience, `git init` (and `git clone`)\nwill perform a test whether the current setup allows creating symlinks\nand will configure that setting in the repository config.\n\nThe test suite ignores system / global config files. Allow\ntesting *with* symlink support by checking if native symlinks are\nenabled in MSYS2 (via setting the special environment variable\n`MSYS=winsymlinks:nativestrict` to ask the MSYS2 runtime to enable\ncreating symlinks).\n\nNote: This assumes that Git's test suite is run in MSYS2's Bash, which\nis true for the time being (an experiment to switch to BusyBox-w32\nfailed due to the experimental nature of BusyBox-w32).\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 26e64c6a5a..0fe00a5b70 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -2862,6 +2862,15 @@ static void setup_windows_environment(void)\n \t\tif (!tmp && (tmp = getenv(\"USERPROFILE\")))\n \t\t\tsetenv(\"HOME\", tmp, 1);\n \t}\n+\n+\t/*\n+\t * Change 'core.symlinks' default to false, unless native symlinks are\n+\t * enabled in MSys2 (via 'MSYS=winsymlinks:nativestrict'). Thus we can\n+\t * run the test suite (which doesn't obey config files) with or without\n+\t * symlink support.\n+\t */\n+\tif (!(tmp = getenv(\"MSYS\")) || !strstr(tmp, \"winsymlinks:nativestrict\"))\n+\t\thas_symlinks = 0;\n }\n \n static void get_current_user_sid(PSID *sid, HANDLE *linked_token)\n-- \ngitgitgadget\n\n"},{"id":"532339","messageId":"b698f4a002a29e1342760fa8cc5d31a3e4a857f6.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 09/18] mingw: add symlink-specific error codes","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:46Z","receivedAt":"2025-12-17T14:09:09Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nThe Win32 API calls do not set `errno`; Instead, error codes for failed\noperations must be obtained via the `GetLastError()` function. Git would\nnot know what to do with those error values, though, which is why Git's\nWindows compatibility layer translates them to `errno` values.\n\nLet's handle a couple of symlink-related error codes that will become\nrelevant with the upcoming support for symlinks on Windows.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 0fe00a5b70..0e8807196f 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -102,6 +102,7 @@ int err_win_to_posix(DWORD winerr)\n \tcase ERROR_INVALID_PARAMETER: error = EINVAL; break;\n \tcase ERROR_INVALID_PASSWORD: error = EPERM; break;\n \tcase ERROR_INVALID_PRIMARY_GROUP: error = EINVAL; break;\n+\tcase ERROR_INVALID_REPARSE_DATA: error = EINVAL; break;\n \tcase ERROR_INVALID_SIGNAL_NUMBER: error = EINVAL; break;\n \tcase ERROR_INVALID_TARGET_HANDLE: error = EIO; break;\n \tcase ERROR_INVALID_WORKSTATION: error = EACCES; break;\n@@ -116,6 +117,7 @@ int err_win_to_posix(DWORD winerr)\n \tcase ERROR_NEGATIVE_SEEK: error = ESPIPE; break;\n \tcase ERROR_NOACCESS: error = EFAULT; break;\n \tcase ERROR_NONE_MAPPED: error = EINVAL; break;\n+\tcase ERROR_NOT_A_REPARSE_POINT: error = EINVAL; break;\n \tcase ERROR_NOT_ENOUGH_MEMORY: error = ENOMEM; break;\n \tcase ERROR_NOT_READY: error = EAGAIN; break;\n \tcase ERROR_NOT_SAME_DEVICE: error = EXDEV; break;\n@@ -136,6 +138,9 @@ int err_win_to_posix(DWORD winerr)\n \tcase ERROR_PIPE_NOT_CONNECTED: error = EPIPE; break;\n \tcase ERROR_PRIVILEGE_NOT_HELD: error = EACCES; break;\n \tcase ERROR_READ_FAULT: error = EIO; break;\n+\tcase ERROR_REPARSE_ATTRIBUTE_CONFLICT: error = EINVAL; break;\n+\tcase ERROR_REPARSE_TAG_INVALID: error = EINVAL; break;\n+\tcase ERROR_REPARSE_TAG_MISMATCH: error = EINVAL; break;\n \tcase ERROR_SEEK: error = EIO; break;\n \tcase ERROR_SEEK_ON_DEVICE: error = ESPIPE; break;\n \tcase ERROR_SHARING_BUFFER_EXCEEDED: error = ENFILE; break;\n-- \ngitgitgadget\n\n"},{"id":"532340","messageId":"282aba42e8aeed336318614523ff8b36d3131e5f.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 10/18] mingw: handle symlinks to directories in `mingw_unlink()`","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:47Z","receivedAt":"2025-12-17T14:09:10Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nThe `_wunlink()` and `DeleteFileW()` functions refuse to delete symlinks\nto directories on Windows; The error code woutl be `ERROR_ACCESS_DENIED`\nin that case. Take that error code as an indicator that we need to try\n`_wrmdir()` as well. In the best case, it will remove a symlink. In the\nworst case, it will fail with the same error code again.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 9 ++++++++-\n 1 file changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 0e8807196f..b1cc30d0f1 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -338,9 +338,16 @@ int mingw_unlink(const char *pathname, int handle_in_use_error)\n \t\t\treturn 0;\n \t\tif (!is_file_in_use_error(GetLastError()))\n \t\t\tbreak;\n+\t\t/*\n+\t\t * _wunlink() / DeleteFileW() for directory symlinks fails with\n+\t\t * ERROR_ACCESS_DENIED (EACCES), so try _wrmdir() as well. This is the\n+\t\t * same error we get if a file is in use (already checked above).\n+\t\t */\n+\t\tif (!_wrmdir(wpathname))\n+\t\t\treturn 0;\n+\n \t\tif (!handle_in_use_error)\n \t\t\treturn -1;\n-\n \t} while (retry_ask_yes_no(&tries, \"Unlink of file '%s' failed. \"\n \t\t\t\"Should I try again?\", pathname));\n \treturn -1;\n-- \ngitgitgadget\n\n"},{"id":"532342","messageId":"5cb3b10500339fec403b84e30721e4f6a68e42f6.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 11/18] mingw: support renaming symlinks","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:48Z","receivedAt":"2025-12-17T14:09:14Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nOlder MSVCRT's `_wrename()` function cannot rename symlinks over\nexisting files: it returns success without doing anything. Newer\nMSVCR*.dll versions probably do not share this problem: according to CRT\nsources, they just call `MoveFileEx()` with the `MOVEFILE_COPY_ALLOWED`\nflag.\n\nAvoid the `_wrename()` call, and go with directly calling\n`MoveFileEx()`, with proper error handling of course.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 38 ++++++++++++++++----------------------\n 1 file changed, 16 insertions(+), 22 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex b1cc30d0f1..55f0bb478e 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -2275,7 +2275,7 @@ int mingw_accept(int sockfd1, struct sockaddr *sa, socklen_t *sz)\n int mingw_rename(const char *pold, const char *pnew)\n {\n \tstatic int supports_file_rename_info_ex = 1;\n-\tDWORD attrs, gle;\n+\tDWORD attrs = INVALID_FILE_ATTRIBUTES, gle;\n \tint tries = 0;\n \twchar_t wpold[MAX_PATH], wpnew[MAX_PATH];\n \tint wpnew_len;\n@@ -2286,15 +2286,6 @@ int mingw_rename(const char *pold, const char *pnew)\n \tif (wpnew_len < 0)\n \t\treturn -1;\n \n-\t/*\n-\t * Try native rename() first to get errno right.\n-\t * It is based on MoveFile(), which cannot overwrite existing files.\n-\t */\n-\tif (!_wrename(wpold, wpnew))\n-\t\treturn 0;\n-\tif (errno != EEXIST)\n-\t\treturn -1;\n-\n repeat:\n \tif (supports_file_rename_info_ex) {\n \t\t/*\n@@ -2370,13 +2361,22 @@ repeat:\n \t\t * to retry.\n \t\t */\n \t} else {\n-\t\tif (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))\n+\t\tif (MoveFileExW(wpold, wpnew,\n+\t\t\t\tMOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED))\n \t\t\treturn 0;\n \t\tgle = GetLastError();\n \t}\n \n-\t/* TODO: translate more errors */\n-\tif (gle == ERROR_ACCESS_DENIED &&\n+\t/* revert file attributes on failure */\n+\tif (attrs != INVALID_FILE_ATTRIBUTES)\n+\t\tSetFileAttributesW(wpnew, attrs);\n+\n+\tif (!is_file_in_use_error(gle)) {\n+\t\terrno = err_win_to_posix(gle);\n+\t\treturn -1;\n+\t}\n+\n+\tif (attrs == INVALID_FILE_ATTRIBUTES &&\n \t    (attrs = GetFileAttributesW(wpnew)) != INVALID_FILE_ATTRIBUTES) {\n \t\tif (attrs & FILE_ATTRIBUTE_DIRECTORY) {\n \t\t\tDWORD attrsold = GetFileAttributesW(wpold);\n@@ -2388,16 +2388,10 @@ repeat:\n \t\t\treturn -1;\n \t\t}\n \t\tif ((attrs & FILE_ATTRIBUTE_READONLY) &&\n-\t\t    SetFileAttributesW(wpnew, attrs & ~FILE_ATTRIBUTE_READONLY)) {\n-\t\t\tif (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))\n-\t\t\t\treturn 0;\n-\t\t\tgle = GetLastError();\n-\t\t\t/* revert file attributes on failure */\n-\t\t\tSetFileAttributesW(wpnew, attrs);\n-\t\t}\n+\t\t    SetFileAttributesW(wpnew, attrs & ~FILE_ATTRIBUTE_READONLY))\n+\t\t\tgoto repeat;\n \t}\n-\tif (gle == ERROR_ACCESS_DENIED &&\n-\t       retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n+\tif (retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n \t\t       \"Should I try again?\", pold, pnew))\n \t\tgoto repeat;\n \n-- \ngitgitgadget\n\n"},{"id":"532343","messageId":"49920839bb2f9e21d1cf91e54024ecd986fe32a9.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 12/18] mingw: allow `mingw_chdir()` to change to symlink-resolved directories","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:49Z","receivedAt":"2025-12-17T14:09:15Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nIf symlinks are enabled, resolve all symlinks when changing directories,\nas required by POSIX.\n\nNote: Git's `real_path()` function bases its link resolution algorithm\non this property of `chdir()`. Unfortunately, the current directory on\nWindows is limited to only MAX_PATH (260) characters. Therefore using\nsymlinks and long paths in combination may be problematic.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 20 +++++++++++++++++++-\n 1 file changed, 19 insertions(+), 1 deletion(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 55f0bb478e..5d2a8c247c 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -866,9 +866,27 @@ int mingw_access(const char *filename, int mode)\n int mingw_chdir(const char *dirname)\n {\n \twchar_t wdirname[MAX_PATH];\n+\n \tif (xutftowcs_path(wdirname, dirname) < 0)\n \t\treturn -1;\n-\treturn _wchdir(wdirname);\n+\n+\tif (has_symlinks) {\n+\t\tHANDLE hnd = CreateFileW(wdirname, 0,\n+\t\t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n+\t\t\t\tOPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);\n+\t\tif (hnd == INVALID_HANDLE_VALUE) {\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\t\treturn -1;\n+\t\t}\n+\t\tif (!GetFinalPathNameByHandleW(hnd, wdirname, ARRAY_SIZE(wdirname), 0)) {\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\t\tCloseHandle(hnd);\n+\t\t\treturn -1;\n+\t\t}\n+\t\tCloseHandle(hnd);\n+\t}\n+\n+\treturn _wchdir(normalize_ntpath(wdirname));\n }\n \n int mingw_chmod(const char *filename, int mode)\n-- \ngitgitgadget\n\n"},{"id":"532344","messageId":"8fef8220f4d60b48358882fbc961c4604eab3abf.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 13/18] mingw: implement `readlink()`","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:50Z","receivedAt":"2025-12-17T14:09:16Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nImplement `readlink()` by reading NTFS reparse points via the\n`read_reparse_point()` function that was introduced earlier to determine\nthe length of symlink targets. Works for symlinks and directory\njunctions. If symlinks are disabled, fail with `ENOSYS`.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw-posix.h |  3 +--\n compat/mingw.c       | 24 ++++++++++++++++++++++++\n 2 files changed, 25 insertions(+), 2 deletions(-)\n\ndiff --git a/compat/mingw-posix.h b/compat/mingw-posix.h\nindex 0939feff27..896aa976b1 100644\n--- a/compat/mingw-posix.h\n+++ b/compat/mingw-posix.h\n@@ -121,8 +121,6 @@ struct utsname {\n  * trivial stubs\n  */\n \n-static inline int readlink(const char *path UNUSED, char *buf UNUSED, size_t bufsiz UNUSED)\n-{ errno = ENOSYS; return -1; }\n static inline int symlink(const char *oldpath UNUSED, const char *newpath UNUSED)\n { errno = ENOSYS; return -1; }\n static inline int fchmod(int fildes UNUSED, mode_t mode UNUSED)\n@@ -197,6 +195,7 @@ int setitimer(int type, struct itimerval *in, struct itimerval *out);\n int sigaction(int sig, struct sigaction *in, struct sigaction *out);\n int link(const char *oldpath, const char *newpath);\n int uname(struct utsname *buf);\n+int readlink(const char *path, char *buf, size_t bufsiz);\n \n /*\n  * replacements of existing functions\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 5d2a8c247c..b407a2ac07 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -2698,6 +2698,30 @@ int link(const char *oldpath, const char *newpath)\n \treturn 0;\n }\n \n+int readlink(const char *path, char *buf, size_t bufsiz)\n+{\n+\tWCHAR wpath[MAX_PATH];\n+\tchar tmpbuf[MAX_PATH];\n+\tint len;\n+\tDWORD tag;\n+\n+\tif (xutftowcs_path(wpath, path) < 0)\n+\t\treturn -1;\n+\n+\tif (read_reparse_point(wpath, TRUE, tmpbuf, &len, &tag) < 0)\n+\t\treturn -1;\n+\n+\t/*\n+\t * Adapt to strange readlink() API: Copy up to bufsiz *bytes*, potentially\n+\t * cutting off a UTF-8 sequence. Insufficient bufsize is *not* a failure\n+\t * condition. There is no conversion function that produces invalid UTF-8,\n+\t * so convert to a (hopefully large enough) temporary buffer, then memcpy\n+\t * the requested number of bytes (including '\\0' for robustness).\n+\t */\n+\tmemcpy(buf, tmpbuf, min(bufsiz, len + 1));\n+\treturn min(bufsiz, len);\n+}\n+\n pid_t waitpid(pid_t pid, int *status, int options)\n {\n \tHANDLE h = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION,\n-- \ngitgitgadget\n\n"},{"id":"532345","messageId":"1dd5f9d6cdad0f9a1fb8139274c99df008dd03ad.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 14/18] mingw: implement basic `symlink()` functionality (file symlinks only)","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:51Z","receivedAt":"2025-12-17T14:09:17Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nImplement `symlink()`. This implementation always creates _file_\nsymlinks (remember: Windows discerns between symlinks pointing to\ndirectories and those pointing to files). Support for directory symlinks\nwill be added in a subseqeuent commit.\n\nThis implementation fails with `ENOSYS` if symlinks are disabled or\nunsupported.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw-posix.h |  3 +--\n compat/mingw.c       | 28 ++++++++++++++++++++++++++++\n 2 files changed, 29 insertions(+), 2 deletions(-)\n\ndiff --git a/compat/mingw-posix.h b/compat/mingw-posix.h\nindex 896aa976b1..2d989fd762 100644\n--- a/compat/mingw-posix.h\n+++ b/compat/mingw-posix.h\n@@ -121,8 +121,6 @@ struct utsname {\n  * trivial stubs\n  */\n \n-static inline int symlink(const char *oldpath UNUSED, const char *newpath UNUSED)\n-{ errno = ENOSYS; return -1; }\n static inline int fchmod(int fildes UNUSED, mode_t mode UNUSED)\n { errno = ENOSYS; return -1; }\n #ifndef __MINGW64_VERSION_MAJOR\n@@ -195,6 +193,7 @@ int setitimer(int type, struct itimerval *in, struct itimerval *out);\n int sigaction(int sig, struct sigaction *in, struct sigaction *out);\n int link(const char *oldpath, const char *newpath);\n int uname(struct utsname *buf);\n+int symlink(const char *target, const char *link);\n int readlink(const char *path, char *buf, size_t bufsiz);\n \n /*\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex b407a2ac07..8d366794c4 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -2698,6 +2698,34 @@ int link(const char *oldpath, const char *newpath)\n \treturn 0;\n }\n \n+int symlink(const char *target, const char *link)\n+{\n+\twchar_t wtarget[MAX_PATH], wlink[MAX_PATH];\n+\tint len;\n+\n+\t/* fail if symlinks are disabled or API is not supported (WinXP) */\n+\tif (!has_symlinks) {\n+\t\terrno = ENOSYS;\n+\t\treturn -1;\n+\t}\n+\n+\tif ((len = xutftowcs_path(wtarget, target)) < 0\n+\t\t\t|| xutftowcs_path(wlink, link) < 0)\n+\t\treturn -1;\n+\n+\t/* convert target dir separators to backslashes */\n+\twhile (len--)\n+\t\tif (wtarget[len] == '/')\n+\t\t\twtarget[len] = '\\\\';\n+\n+\t/* create file symlink */\n+\tif (!CreateSymbolicLinkW(wlink, wtarget, 0)) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n int readlink(const char *path, char *buf, size_t bufsiz)\n {\n \tWCHAR wpath[MAX_PATH];\n-- \ngitgitgadget\n\n"},{"id":"532346","messageId":"7b6dbc73f74e2fb6539034ab54f8d1a098d398da.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 15/18] mingw: add support for symlinks to directories","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:52Z","receivedAt":"2025-12-17T14:09:18Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <blees@dcon.de>\n\nSymlinks on Windows have a flag that indicates whether the target is a\nfile or a directory. Symlinks of wrong type simply don't work. This even\naffects core Win32 APIs (e.g. `DeleteFile()` refuses to delete directory\nsymlinks).\n\nHowever, `CreateFile()` with FILE_FLAG_BACKUP_SEMANTICS does work. Check\nthe target type by first creating a tentative file symlink, opening it,\nand checking the type of the resulting handle. If it is a directory,\nrecreate the symlink with the directory flag set.\n\nIt is possible to create symlinks before the target exists (or in case\nof symlinks to symlinks: before the target type is known). If this\nhappens, create a tentative file symlink and postpone the directory\ndecision: keep a list of phantom symlinks to be processed whenever a new\ndirectory is created in `mingw_mkdir()`.\n\nLimitations: This algorithm may fail if a link target changes from file\nto directory or vice versa, or if the target directory is created in\nanother process. It's the best Git can do, though.\n\nSigned-off-by: Karsten Blees <blees@dcon.de>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 164 +++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 164 insertions(+)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 8d366794c4..59a32e454e 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -296,6 +296,131 @@ int mingw_core_config(const char *var, const char *value,\n \treturn 0;\n }\n \n+static inline int is_wdir_sep(wchar_t wchar)\n+{\n+\treturn wchar == L'/' || wchar == L'\\\\';\n+}\n+\n+static const wchar_t *make_relative_to(const wchar_t *path,\n+\t\t\t\t       const wchar_t *relative_to, wchar_t *out,\n+\t\t\t\t       size_t size)\n+{\n+\tsize_t i = wcslen(relative_to), len;\n+\n+\t/* Is `path` already absolute? */\n+\tif (is_wdir_sep(path[0]) ||\n+\t    (iswalpha(path[0]) && path[1] == L':' && is_wdir_sep(path[2])))\n+\t\treturn path;\n+\n+\twhile (i > 0 && !is_wdir_sep(relative_to[i - 1]))\n+\t\ti--;\n+\n+\t/* Is `relative_to` in the current directory? */\n+\tif (!i)\n+\t\treturn path;\n+\n+\tlen = wcslen(path);\n+\tif (i + len + 1 > size) {\n+\t\terror(\"Could not make '%ls' relative to '%ls' (too large)\",\n+\t\t      path, relative_to);\n+\t\treturn NULL;\n+\t}\n+\n+\tmemcpy(out, relative_to, i * sizeof(wchar_t));\n+\twcscpy(out + i, path);\n+\treturn out;\n+}\n+\n+enum phantom_symlink_result {\n+\tPHANTOM_SYMLINK_RETRY,\n+\tPHANTOM_SYMLINK_DONE,\n+\tPHANTOM_SYMLINK_DIRECTORY\n+};\n+\n+/*\n+ * Changes a file symlink to a directory symlink if the target exists and is a\n+ * directory.\n+ */\n+static enum phantom_symlink_result\n+process_phantom_symlink(const wchar_t *wtarget, const wchar_t *wlink)\n+{\n+\tHANDLE hnd;\n+\tBY_HANDLE_FILE_INFORMATION fdata;\n+\twchar_t relative[MAX_PATH];\n+\tconst wchar_t *rel;\n+\n+\t/* check that wlink is still a file symlink */\n+\tif ((GetFileAttributesW(wlink)\n+\t\t\t& (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))\n+\t\t\t!= FILE_ATTRIBUTE_REPARSE_POINT)\n+\t\treturn PHANTOM_SYMLINK_DONE;\n+\n+\t/* make it relative, if necessary */\n+\trel = make_relative_to(wtarget, wlink, relative, ARRAY_SIZE(relative));\n+\tif (!rel)\n+\t\treturn PHANTOM_SYMLINK_DONE;\n+\n+\t/* let Windows resolve the link by opening it */\n+\thnd = CreateFileW(rel, 0,\n+\t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n+\t\t\tOPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);\n+\tif (hnd == INVALID_HANDLE_VALUE) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn PHANTOM_SYMLINK_RETRY;\n+\t}\n+\n+\tif (!GetFileInformationByHandle(hnd, &fdata)) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\tCloseHandle(hnd);\n+\t\treturn PHANTOM_SYMLINK_RETRY;\n+\t}\n+\tCloseHandle(hnd);\n+\n+\t/* if target exists and is a file, we're done */\n+\tif (!(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))\n+\t\treturn PHANTOM_SYMLINK_DONE;\n+\n+\t/* otherwise recreate the symlink with directory flag */\n+\tif (DeleteFileW(wlink) && CreateSymbolicLinkW(wlink, wtarget, 1))\n+\t\treturn PHANTOM_SYMLINK_DIRECTORY;\n+\n+\terrno = err_win_to_posix(GetLastError());\n+\treturn PHANTOM_SYMLINK_RETRY;\n+}\n+\n+/* keep track of newly created symlinks to non-existing targets */\n+struct phantom_symlink_info {\n+\tstruct phantom_symlink_info *next;\n+\twchar_t *wlink;\n+\twchar_t *wtarget;\n+};\n+\n+static struct phantom_symlink_info *phantom_symlinks = NULL;\n+static CRITICAL_SECTION phantom_symlinks_cs;\n+\n+static void process_phantom_symlinks(void)\n+{\n+\tstruct phantom_symlink_info *current, **psi;\n+\tEnterCriticalSection(&phantom_symlinks_cs);\n+\t/* process phantom symlinks list */\n+\tpsi = &phantom_symlinks;\n+\twhile ((current = *psi)) {\n+\t\tenum phantom_symlink_result result = process_phantom_symlink(\n+\t\t\t\tcurrent->wtarget, current->wlink);\n+\t\tif (result == PHANTOM_SYMLINK_RETRY) {\n+\t\t\tpsi = &current->next;\n+\t\t} else {\n+\t\t\t/* symlink was processed, remove from list */\n+\t\t\t*psi = current->next;\n+\t\t\tfree(current);\n+\t\t\t/* if symlink was a directory, start over */\n+\t\t\tif (result == PHANTOM_SYMLINK_DIRECTORY)\n+\t\t\t\tpsi = &phantom_symlinks;\n+\t\t}\n+\t}\n+\tLeaveCriticalSection(&phantom_symlinks_cs);\n+}\n+\n /* Normalizes NT paths as returned by some low-level APIs. */\n static wchar_t *normalize_ntpath(wchar_t *wbuf)\n {\n@@ -479,6 +604,8 @@ int mingw_mkdir(const char *path, int mode UNUSED)\n \tif (xutftowcs_path(wpath, path) < 0)\n \t\treturn -1;\n \tret = _wmkdir(wpath);\n+\tif (!ret)\n+\t\tprocess_phantom_symlinks();\n \tif (!ret && needs_hiding(path))\n \t\treturn set_hidden_flag(wpath, 1);\n \treturn ret;\n@@ -2723,6 +2850,42 @@ int symlink(const char *target, const char *link)\n \t\terrno = err_win_to_posix(GetLastError());\n \t\treturn -1;\n \t}\n+\n+\t/* convert to directory symlink if target exists */\n+\tswitch (process_phantom_symlink(wtarget, wlink)) {\n+\tcase PHANTOM_SYMLINK_RETRY:\t{\n+\t\t/* if target doesn't exist, add to phantom symlinks list */\n+\t\twchar_t wfullpath[MAX_PATH];\n+\t\tstruct phantom_symlink_info *psi;\n+\n+\t\t/* convert to absolute path to be independent of cwd */\n+\t\tlen = GetFullPathNameW(wlink, MAX_PATH, wfullpath, NULL);\n+\t\tif (!len || len >= MAX_PATH) {\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/* over-allocate and fill phantom_symlink_info structure */\n+\t\tpsi = xmalloc(sizeof(struct phantom_symlink_info)\n+\t\t\t+ sizeof(wchar_t) * (len + wcslen(wtarget) + 2));\n+\t\tpsi->wlink = (wchar_t *)(psi + 1);\n+\t\twcscpy(psi->wlink, wfullpath);\n+\t\tpsi->wtarget = psi->wlink + len + 1;\n+\t\twcscpy(psi->wtarget, wtarget);\n+\n+\t\tEnterCriticalSection(&phantom_symlinks_cs);\n+\t\tpsi->next = phantom_symlinks;\n+\t\tphantom_symlinks = psi;\n+\t\tLeaveCriticalSection(&phantom_symlinks_cs);\n+\t\tbreak;\n+\t}\n+\tcase PHANTOM_SYMLINK_DIRECTORY:\n+\t\t/* if we created a dir symlink, process other phantom symlinks */\n+\t\tprocess_phantom_symlinks();\n+\t\tbreak;\n+\tdefault:\n+\t\tbreak;\n+\t}\n \treturn 0;\n }\n \n@@ -3424,6 +3587,7 @@ int wmain(int argc, const wchar_t **wargv)\n \n \t/* initialize critical section for waitpid pinfo_t list */\n \tInitializeCriticalSection(&pinfo_cs);\n+\tInitializeCriticalSection(&phantom_symlinks_cs);\n \n \t/* set up default file mode and file modes for stdin/out/err */\n \t_fmode = _O_BINARY;\n-- \ngitgitgadget\n\n"},{"id":"532347","messageId":"d3b89c29b01e5ed19b40f45714b3a1db800afa5d.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 16/18] mingw: try to create symlinks without elevated permissions","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:53Z","receivedAt":"2025-12-17T14:09:19Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nAs of Windows 10 Build 14972 in Developer Mode, a new flag is supported\nby `CreateSymbolicLink()` to create symbolic links even when running\noutside of an elevated session (which was previously required).\n\nThis new flag is called `SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE`\nand has the numeric value 0x02.\n\nPrevious Windows 10 versions will not understand that flag and return\nan `ERROR_INVALID_PARAMETER`, therefore we have to be careful to try\npassing that flag only when the build number indicates that it is\nsupported.\n\nFor more information about the new flag, see this blog post:\nhttps://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 26 ++++++++++++++++++++++++--\n 1 file changed, 24 insertions(+), 2 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 59a32e454e..3e2110a87a 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -331,6 +331,8 @@ static const wchar_t *make_relative_to(const wchar_t *path,\n \treturn out;\n }\n \n+static DWORD symlink_file_flags = 0, symlink_directory_flags = 1;\n+\n enum phantom_symlink_result {\n \tPHANTOM_SYMLINK_RETRY,\n \tPHANTOM_SYMLINK_DONE,\n@@ -381,7 +383,8 @@ process_phantom_symlink(const wchar_t *wtarget, const wchar_t *wlink)\n \t\treturn PHANTOM_SYMLINK_DONE;\n \n \t/* otherwise recreate the symlink with directory flag */\n-\tif (DeleteFileW(wlink) && CreateSymbolicLinkW(wlink, wtarget, 1))\n+\tif (DeleteFileW(wlink) &&\n+\t    CreateSymbolicLinkW(wlink, wtarget, symlink_directory_flags))\n \t\treturn PHANTOM_SYMLINK_DIRECTORY;\n \n \terrno = err_win_to_posix(GetLastError());\n@@ -2846,7 +2849,7 @@ int symlink(const char *target, const char *link)\n \t\t\twtarget[len] = '\\\\';\n \n \t/* create file symlink */\n-\tif (!CreateSymbolicLinkW(wlink, wtarget, 0)) {\n+\tif (!CreateSymbolicLinkW(wlink, wtarget, symlink_file_flags)) {\n \t\terrno = err_win_to_posix(GetLastError());\n \t\treturn -1;\n \t}\n@@ -3523,6 +3526,24 @@ static void maybe_redirect_std_handles(void)\n \t\t\t\t  GENERIC_WRITE, FILE_FLAG_NO_BUFFERING);\n }\n \n+static void adjust_symlink_flags(void)\n+{\n+\t/*\n+\t * Starting with Windows 10 Build 14972, symbolic links can be created\n+\t * using CreateSymbolicLink() without elevation by passing the flag\n+\t * SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE (0x02) as last\n+\t * parameter, provided the Developer Mode has been enabled. Some\n+\t * earlier Windows versions complain about this flag with an\n+\t * ERROR_INVALID_PARAMETER, hence we have to test the build number\n+\t * specifically.\n+\t */\n+\tif (GetVersion() >= 14972 << 16) {\n+\t\tsymlink_file_flags |= 2;\n+\t\tsymlink_directory_flags |= 2;\n+\t}\n+\n+}\n+\n #ifdef _MSC_VER\n #ifdef _DEBUG\n #include <crtdbg.h>\n@@ -3558,6 +3579,7 @@ int wmain(int argc, const wchar_t **wargv)\n #endif\n \n \tmaybe_redirect_std_handles();\n+\tadjust_symlink_flags();\n \n \t/* determine size of argv and environ conversion buffer */\n \tmaxlen = wcslen(wargv[0]);\n-- \ngitgitgadget\n\n"},{"id":"532348","messageId":"2e73ab42217496739adc5881d900552bce006c05.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 17/18] mingw: emulate `stat()` a little more faithfully","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:54Z","receivedAt":"2025-12-17T14:09:21Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen creating directories via `safe_create_leading_directories()`, we\nmight encounter an already-existing directory which is not\nreadable by the current user. To handle that situation, Git's code calls\n`stat()` to determine whether we're looking at a directory.\n\nIn such a case, `CreateFile()` will fail, though, no matter what, and\nconsequently `mingw_stat()` will fail, too. But POSIX semantics seem to\nstill allow `stat()` to go forward.\n\nSo let's call `mingw_lstat()` to the rescue if we fail to get a file\nhandle due to denied permission in `mingw_stat()`, and fill the stat\ninfo that way.\n\nWe need to be careful to not allow this to go forward in case that we're\nlooking at a symbolic link: to resolve the link, we would still have to\ncreate a file handle, and we just found out that we cannot. Therefore,\n`stat()` still needs to fail with `EACCES` in that case.\n\nThis fixes https://github.com/git-for-windows/git/issues/2531.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 14 +++++++++++++-\n 1 file changed, 13 insertions(+), 1 deletion(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 3e2110a87a..628a3941d2 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -1273,7 +1273,19 @@ int mingw_stat(const char *file_name, struct stat *buf)\n \t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n \t\t\tOPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);\n \tif (hnd == INVALID_HANDLE_VALUE) {\n-\t\terrno = err_win_to_posix(GetLastError());\n+\t\tDWORD err = GetLastError();\n+\n+\t\tif (err == ERROR_ACCESS_DENIED &&\n+\t\t    !mingw_lstat(file_name, buf) &&\n+\t\t    !S_ISLNK(buf->st_mode))\n+\t\t\t/*\n+\t\t\t * POSIX semantics state to still try to fill\n+\t\t\t * information, even if permission is denied to create\n+\t\t\t * a file handle.\n+\t\t\t */\n+\t\t\treturn 0;\n+\n+\t\terrno = err_win_to_posix(err);\n \t\treturn -1;\n \t}\n \tresult = get_file_info_by_handle(hnd, buf);\n-- \ngitgitgadget\n\n"},{"id":"532349","messageId":"817f488523099b784432742328ea69646c6abafc.1765980535.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH 18/18] mingw: special-case index entries for symlinks with buggy size","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T14:08:55Z","receivedAt":"2025-12-17T14:09:22Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIn https://github.com/git-for-windows/git/pull/2637, we fixed a bug\nwhere symbolic links' target path sizes were recorded incorrectly in the\nindex. The downside of this fix was that every user with tracked\nsymbolic links in their checkouts would see them as modified in `git\nstatus`, but not in `git diff`, and only a `git add <path>` (or `git add\n-u`) would \"fix\" this.\n\nLet's do better than that: we can detect that situation and simply\npretend that a symbolic link with a known bad size (or a size that just\nhappens to be that bad size, a _very_ unlikely scenario because it would\noverflow our buffers due to the trailing NUL byte) means that it needs\nto be re-checked as if we had just checked it out.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n read-cache.c | 11 +++++++++++\n 1 file changed, 11 insertions(+)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex 990d4ead0d..260f4b3b2f 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -470,6 +470,17 @@ int ie_modified(struct index_state *istate,\n \t * then we know it is.\n \t */\n \tif ((changed & DATA_CHANGED) &&\n+#ifdef GIT_WINDOWS_NATIVE\n+\t    /*\n+\t     * Work around Git for Windows v2.27.0 fixing a bug where symlinks'\n+\t     * target path lengths were not read at all, and instead recorded\n+\t     * as 4096: now, all symlinks would appear as modified.\n+\t     *\n+\t     * So let's just special-case symlinks with a target path length\n+\t     * (i.e. `sd_size`) of 4096 and force them to be re-checked.\n+\t     */\n+\t    (!S_ISLNK(st->st_mode) || ce->ce_stat_data.sd_size != MAX_PATH) &&\n+#endif\n \t    (S_ISGITLINK(ce->ce_mode) || ce->ce_stat_data.sd_size != 0))\n \t\treturn changed;\n \n-- \ngitgitgadget\n"},{"id":"532393","messageId":"xmqqpl8czmke.fsf@gitster.g","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"Re: [PATCH 00/18] Support symbolic links on Windows","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-18T00:00:01Z","receivedAt":"2025-12-18T00:00:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Johannes Schindelin via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> This finally upstreams Git for Windows' support for Windows' branch of\n> symbolic links, which has been maturing since 2015. It is based off of\n> js/prep-symlink-windows.\n\nThe three topics taken together touch 19 paths in total, about a\nhalf of which are t/ test files.\n\nI've read the changes to generic parts (i.e., outside compat/) and\nsaw nothing questionable.  Very nicely done.\n\nThanks.\n\n apply.c                             |   2 +-\n compat/mingw-posix.h                |   6 +-\n compat/mingw.c                      | 667 +++++++++++++++++++++++++++---------\n compat/win32.h                      |   6 +-\n compat/win32/dirent.c               |   5 +-\n environment.c                       |   4 +-\n environment.h                       |   2 +\n lockfile.c                          |   4 +-\n read-cache.c                        |  11 +\n setup.c                             |   2 +-\n strbuf.c                            |  10 +-\n t/t0001-init.sh                     |   6 +-\n t/t0301-credential-cache.sh         |   3 +-\n t/t0600-reffiles-backend.sh         |   2 +-\n t/t1006-cat-file.sh                 |  24 +-\n t/t1305-config-include.sh           |   4 +-\n t/t6423-merge-rename-directories.sh |   9 +-\n t/t7800-difftool.sh                 |   8 +-\n t/t9700/test.pl                     |   9 +-\n 19 files changed, 585 insertions(+), 199 deletions(-)\n"},{"id":"532403","messageId":"20488887-14B7-4471-9241-9CDBC3EAB2E8@gmail.com","threadId":"64639","inReplyTo":"282aba42e8aeed336318614523ff8b36d3131e5f.1765980535.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 10/18] mingw: handle symlinks to directories in `mingw_unlink()`","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-12-18T02:49:47Z","receivedAt":"2025-12-18T02:50:00Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 17 déc. 2025 à 09:17, Karsten Blees via GitGitGadget <gitgitgadget@gmail.com> a écrit :\n> \n> ﻿From: Karsten Blees <blees@dcon.de>\n> \n> The `_wunlink()` and `DeleteFileW()` functions refuse to delete symlinks\n> to directories on Windows; The error code woutl be `ERROR_ACCESS_DENIED`\n\nCasually reading; spotted “woutl.” Presumably would?"},{"id":"532450","messageId":"44db571b-4fb1-43da-a7c2-eb7cc15048ab@kdbg.org","threadId":"64639","inReplyTo":"dae450dd0e995b26d0140cf3753d103f5470f44d.1765980535.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 01/18] mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2025-12-18T10:34:57Z","receivedAt":"2025-12-18T10:35:07Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:\n> From: Karsten Blees <blees@dcon.de>\n> \n> The Win32 API function `GetFileAttributes()` cannot handle paths with\n> trailing dir separators. The current `mingw_stat()`/`mingw_lstat()`\n> implementation calls `GetFileAttributes()` twice if the path has\n> trailing slashes (first with the original path that was passed as\n> function parameter, and and a second time with a path copy with trailing\n> '/' removed).\nA comment above do_lstat() mentions this procedure. This patch doesn't\nchange the comment, but it should.\n\n-- Hannes\n\n"},{"id":"532451","messageId":"46b69027-90b4-439a-a14d-61d1bb739b7b@kdbg.org","threadId":"64639","inReplyTo":"c36848eda76742f7a7c203868a077b790301344f.1765980535.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 02/18] mingw: implement `stat()` with symlink support","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2025-12-18T10:44:39Z","receivedAt":"2025-12-18T10:44:48Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:\n> From: Karsten Blees <blees@dcon.de>\n> \n> With respect to symlinks, the current `mingw_stat()` implementation is\n> almost identical to `mingw_lstat()`: except for the file type (`st_mode\n> & S_IFMT`), it returns information about the link rather than the target.\n> \n> Implement `mingw_stat()` by opening the file handle requesting minimal\n> permissions, and then calling `GetFileInformationByHandle()` on it. This\n> way, all links are resolved by the Windows file system layer.\n> \n> If symlinks are disabled, use `mingw_lstat()` as before, but fail with\n> `ELOOP` if a symlink would have to be resolved.\n\nThis last paragraph is disconnected from the patch text. I can't find a\nuse of ELOOP anywhere in the code that has something to do with the goal\nof this patch. Is this a remnant from early times where symbolic links\nwere optional?\n\nThe patch text looks good.\n\n> \n> Signed-off-by: Karsten Blees <blees@dcon.de>\n> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n> ---\n>  compat/mingw.c | 19 ++++++++++++++++++-\n>  1 file changed, 18 insertions(+), 1 deletion(-)\n> \n> diff --git a/compat/mingw.c b/compat/mingw.c\n> index f5a0fe3325..59afd69686 100644\n> --- a/compat/mingw.c\n> +++ b/compat/mingw.c\n> @@ -1026,9 +1026,26 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n>  {\n>  \treturn do_lstat(0, file_name, buf);\n>  }\n> +\n>  int mingw_stat(const char *file_name, struct stat *buf)\n>  {\n> -\treturn do_lstat(1, file_name, buf);\n> +\twchar_t wfile_name[MAX_PATH];\n> +\tHANDLE hnd;\n> +\tint result;\n> +\n> +\t/* open the file and let Windows resolve the links */\n> +\tif (xutftowcs_path(wfile_name, file_name) < 0)\n> +\t\treturn -1;\n> +\thnd = CreateFileW(wfile_name, 0,\n> +\t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n> +\t\t\tOPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);\n> +\tif (hnd == INVALID_HANDLE_VALUE) {\n> +\t\terrno = err_win_to_posix(GetLastError());\n> +\t\treturn -1;\n> +\t}\n> +\tresult = get_file_info_by_handle(hnd, buf);\n> +\tCloseHandle(hnd);\n> +\treturn result;\n>  }\n>  \n>  int mingw_fstat(int fd, struct stat *buf)\n\n-- Hannes\n\n"},{"id":"532452","messageId":"52d0ef24-ae21-400c-836b-aa43dd5899d2@kdbg.org","threadId":"64639","inReplyTo":"aa0ca80bbb00ec53f359c79719b95bcc141ac85c.1765980535.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 03/18] mingw: drop the separate `do_lstat()` function","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2025-12-18T10:48:44Z","receivedAt":"2025-12-18T10:48:47Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:\n> From: Karsten Blees <blees@dcon.de>\n> \n> With the new `mingw_stat()` implementation, `do_lstat()` is only called\n> from `mingw_lstat()` (with the function parameter `follow == 0`). Remove\n> the extra function and the old `mingw_stat()`-specific (`follow == 1`)\n> logic.\n> \n> Signed-off-by: Karsten Blees <blees@dcon.de>\n> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n> ---\n>  compat/mingw.c | 22 ++--------------------\n>  1 file changed, 2 insertions(+), 20 deletions(-)\n> \n> diff --git a/compat/mingw.c b/compat/mingw.c\n> index 59afd69686..ec6c2801d3 100644\n> --- a/compat/mingw.c\n> +++ b/compat/mingw.c\n> @@ -917,14 +917,7 @@ static int has_valid_directory_prefix(wchar_t *wfilename)\n>  \treturn 1;\n>  }\n>  \n> -/* We keep the do_lstat code in a separate function to avoid recursion.\n> - * When a path ends with a slash, the stat will fail with ENOENT. In\n> - * this case, we strip the trailing slashes and stat again.\n> - *\n> - * If follow is true then act like stat() and report on the link\n> - * target. Otherwise report on the link itself.\n> - */\n> -static int do_lstat(int follow, const char *file_name, struct stat *buf)\n> +int mingw_lstat(const char *file_name, struct stat *buf)\n\nOh, here goes the entire function including the comment. Fine, then.\nDisregard my comment on 01/18.\n\n>  {\n>  \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n>  \twchar_t wfilename[MAX_PATH];\n> @@ -958,13 +951,7 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)\n>  \t\t\tif (handle != INVALID_HANDLE_VALUE) {\n>  \t\t\t\tif ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&\n>  \t\t\t\t\t\t(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {\n> -\t\t\t\t\tif (follow) {\n> -\t\t\t\t\t\tchar buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];\n> -\t\t\t\t\t\tbuf->st_size = readlink(file_name, buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE);\n> -\t\t\t\t\t} else {\n> -\t\t\t\t\t\tbuf->st_mode = S_IFLNK;\n> -\t\t\t\t\t}\n> -\t\t\t\t\tbuf->st_mode |= S_IREAD;\n> +\t\t\t\t\tbuf->st_mode = S_IFLNK | S_IREAD;\n>  \t\t\t\t\tif (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))\n>  \t\t\t\t\t\tbuf->st_mode |= S_IWRITE;\n>  \t\t\t\t}\n> @@ -1022,11 +1009,6 @@ static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)\n>  \treturn 0;\n>  }\n>  \n> -int mingw_lstat(const char *file_name, struct stat *buf)\n> -{\n> -\treturn do_lstat(0, file_name, buf);\n> -}\n> -\n>  int mingw_stat(const char *file_name, struct stat *buf)\n>  {\n>  \twchar_t wfile_name[MAX_PATH];\n\nAn obviously correct rewrite.\n\n-- Hannes\n\n"},{"id":"532492","messageId":"02fa15af-5a57-4557-b016-fd14b9107c6e@kdbg.org","threadId":"64639","inReplyTo":"5cb3b10500339fec403b84e30721e4f6a68e42f6.1765980535.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 11/18] mingw: support renaming symlinks","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2025-12-18T17:44:37Z","receivedAt":"2025-12-18T17:44:41Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:\n> From: Karsten Blees <blees@dcon.de>\n> \n> Older MSVCRT's `_wrename()` function cannot rename symlinks over\n> existing files: it returns success without doing anything. Newer\n> MSVCR*.dll versions probably do not share this problem: according to CRT\n> sources, they just call `MoveFileEx()` with the `MOVEFILE_COPY_ALLOWED`\n> flag.\n> \n> Avoid the `_wrename()` call, and go with directly calling\n> `MoveFileEx()`, with proper error handling of course.\n> \n> Signed-off-by: Karsten Blees <blees@dcon.de>\n> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n> ---\n>  compat/mingw.c | 38 ++++++++++++++++----------------------\n>  1 file changed, 16 insertions(+), 22 deletions(-)\n> \n> diff --git a/compat/mingw.c b/compat/mingw.c\n> index b1cc30d0f1..55f0bb478e 100644\n> --- a/compat/mingw.c\n> +++ b/compat/mingw.c\n> @@ -2275,7 +2275,7 @@ int mingw_accept(int sockfd1, struct sockaddr *sa, socklen_t *sz)\n>  int mingw_rename(const char *pold, const char *pnew)\n>  {\n>  \tstatic int supports_file_rename_info_ex = 1;\n> -\tDWORD attrs, gle;\n> +\tDWORD attrs = INVALID_FILE_ATTRIBUTES, gle;\n>  \tint tries = 0;\n>  \twchar_t wpold[MAX_PATH], wpnew[MAX_PATH];\n>  \tint wpnew_len;\n> @@ -2286,15 +2286,6 @@ int mingw_rename(const char *pold, const char *pnew)\n>  \tif (wpnew_len < 0)\n>  \t\treturn -1;\n>  \n> -\t/*\n> -\t * Try native rename() first to get errno right.\n> -\t * It is based on MoveFile(), which cannot overwrite existing files.\n> -\t */\n> -\tif (!_wrename(wpold, wpnew))\n> -\t\treturn 0;\n> -\tif (errno != EEXIST)\n> -\t\treturn -1;\n> -\n>  repeat:\n>  \tif (supports_file_rename_info_ex) {\n>  \t\t/*\n> @@ -2370,13 +2361,22 @@ repeat:\n>  \t\t * to retry.\n>  \t\t */\n>  \t} else {\n> -\t\tif (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))\n> +\t\tif (MoveFileExW(wpold, wpnew,\n> +\t\t\t\tMOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED))\n>  \t\t\treturn 0;\n>  \t\tgle = GetLastError();\n>  \t}\n>  \n> -\t/* TODO: translate more errors */\n> -\tif (gle == ERROR_ACCESS_DENIED &&\n> +\t/* revert file attributes on failure */\n> +\tif (attrs != INVALID_FILE_ATTRIBUTES)\n> +\t\tSetFileAttributesW(wpnew, attrs);\n> +\n> +\tif (!is_file_in_use_error(gle)) {\n> +\t\terrno = err_win_to_posix(gle);\n> +\t\treturn -1;\n> +\t}\n> +\n> +\tif (attrs == INVALID_FILE_ATTRIBUTES &&\n>  \t    (attrs = GetFileAttributesW(wpnew)) != INVALID_FILE_ATTRIBUTES) {\n>  \t\tif (attrs & FILE_ATTRIBUTE_DIRECTORY) {\n>  \t\t\tDWORD attrsold = GetFileAttributesW(wpold);\n> @@ -2388,16 +2388,10 @@ repeat:\n>  \t\t\treturn -1;\n>  \t\t}\n>  \t\tif ((attrs & FILE_ATTRIBUTE_READONLY) &&\n> -\t\t    SetFileAttributesW(wpnew, attrs & ~FILE_ATTRIBUTE_READONLY)) {\n> -\t\t\tif (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))\n> -\t\t\t\treturn 0;\n> -\t\t\tgle = GetLastError();\n> -\t\t\t/* revert file attributes on failure */\n> -\t\t\tSetFileAttributesW(wpnew, attrs);\n> -\t\t}\n> +\t\t    SetFileAttributesW(wpnew, attrs & ~FILE_ATTRIBUTE_READONLY))\n> +\t\t\tgoto repeat;\n>  \t}\n> -\tif (gle == ERROR_ACCESS_DENIED &&\n> -\t       retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n> +\tif (retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n>  \t\t       \"Should I try again?\", pold, pnew))\n>  \t\tgoto repeat;\n>  \n\nThe logic in this function is incredibly convoluted. It does look\nsomewhat reasonable, at least on the non-error path, but whether the\nvariable attr is changed and reset as needed after 'goto repeat' and the\nvarious failure modes, I cannot tell. I give up and trust that this code\nhas been battle-tested during the past decade and works as desired.\n\n-- Hannes\n\n"},{"id":"532494","messageId":"8826825b-79ad-4700-aeb5-71e7847ca5dc@kdbg.org","threadId":"64639","inReplyTo":"8fef8220f4d60b48358882fbc961c4604eab3abf.1765980535.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 13/18] mingw: implement `readlink()`","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2025-12-18T18:13:22Z","receivedAt":"2025-12-18T18:13:25Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:\n> From: Karsten Blees <blees@dcon.de>\n> \n> Implement `readlink()` by reading NTFS reparse points via the\n> `read_reparse_point()` function that was introduced earlier to determine\n> the length of symlink targets. Works for symlinks and directory\n> junctions. If symlinks are disabled, fail with `ENOSYS`.\n\nThis last sentence is obsolete, I think, because I cannot see how the\npatch achieves a failure with ENOSYS.\n\n> \n> Signed-off-by: Karsten Blees <blees@dcon.de>\n> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n> ---\n>  compat/mingw-posix.h |  3 +--\n>  compat/mingw.c       | 24 ++++++++++++++++++++++++\n>  2 files changed, 25 insertions(+), 2 deletions(-)\n> \n> diff --git a/compat/mingw-posix.h b/compat/mingw-posix.h\n> index 0939feff27..896aa976b1 100644\n> --- a/compat/mingw-posix.h\n> +++ b/compat/mingw-posix.h\n> @@ -121,8 +121,6 @@ struct utsname {\n>   * trivial stubs\n>   */\n>  \n> -static inline int readlink(const char *path UNUSED, char *buf UNUSED, size_t bufsiz UNUSED)\n> -{ errno = ENOSYS; return -1; }\n>  static inline int symlink(const char *oldpath UNUSED, const char *newpath UNUSED)\n>  { errno = ENOSYS; return -1; }\n>  static inline int fchmod(int fildes UNUSED, mode_t mode UNUSED)\n> @@ -197,6 +195,7 @@ int setitimer(int type, struct itimerval *in, struct itimerval *out);\n>  int sigaction(int sig, struct sigaction *in, struct sigaction *out);\n>  int link(const char *oldpath, const char *newpath);\n>  int uname(struct utsname *buf);\n> +int readlink(const char *path, char *buf, size_t bufsiz);\n>  \n>  /*\n>   * replacements of existing functions\n> diff --git a/compat/mingw.c b/compat/mingw.c\n> index 5d2a8c247c..b407a2ac07 100644\n> --- a/compat/mingw.c\n> +++ b/compat/mingw.c\n> @@ -2698,6 +2698,30 @@ int link(const char *oldpath, const char *newpath)\n>  \treturn 0;\n>  }\n>  \n> +int readlink(const char *path, char *buf, size_t bufsiz)\n> +{\n> +\tWCHAR wpath[MAX_PATH];\n> +\tchar tmpbuf[MAX_PATH];\n> +\tint len;\n> +\tDWORD tag;\n> +\n> +\tif (xutftowcs_path(wpath, path) < 0)\n> +\t\treturn -1;\n> +\n> +\tif (read_reparse_point(wpath, TRUE, tmpbuf, &len, &tag) < 0)\n> +\t\treturn -1;\n> +\n> +\t/*\n> +\t * Adapt to strange readlink() API: Copy up to bufsiz *bytes*, potentially\n> +\t * cutting off a UTF-8 sequence. Insufficient bufsize is *not* a failure\n> +\t * condition. There is no conversion function that produces invalid UTF-8,\n> +\t * so convert to a (hopefully large enough) temporary buffer, then memcpy\n> +\t * the requested number of bytes (including '\\0' for robustness).\n> +\t */\n> +\tmemcpy(buf, tmpbuf, min(bufsiz, len + 1));\n> +\treturn min(bufsiz, len);\n> +}\n> +\n>  pid_t waitpid(pid_t pid, int *status, int options)\n>  {\n>  \tHANDLE h = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION,\n\n-- Hannes\n\n"},{"id":"532498","messageId":"19030791-dafa-4a63-985b-9cdf423bfd1b@kdbg.org","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"Re: [PATCH 00/18] Support symbolic links on Windows","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2025-12-18T18:51:41Z","receivedAt":"2025-12-18T18:51:44Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 17.12.25 um 15:08 schrieb Johannes Schindelin via GitGitGadget:\n> This finally upstreams Git for Windows' support for Windows' branch of\n> symbolic links, which has been maturing since 2015. It is based off of\n> js/prep-symlink-windows.\n> \n> Bill Zissimopoulos (1):\n>   mingw: compute the correct size for symlinks in `mingw_lstat()`\n> \n> Johannes Schindelin (3):\n>   mingw: try to create symlinks without elevated permissions\n>   mingw: emulate `stat()` a little more faithfully\n>   mingw: special-case index entries for symlinks with buggy size\n> \n> Karsten Blees (14):\n>   mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`\n>   mingw: implement `stat()` with symlink support\n>   mingw: drop the separate `do_lstat()` function\n>   mingw: let `mingw_lstat()` error early upon problems with reparse\n>     points\n>   mingw: teach dirent about symlinks\n>   mingw: factor out the retry logic\n>   mingw: change default of `core.symlinks` to false\n>   mingw: add symlink-specific error codes\n>   mingw: handle symlinks to directories in `mingw_unlink()`\n>   mingw: support renaming symlinks\n>   mingw: allow `mingw_chdir()` to change to symlink-resolved directories\n>   mingw: implement `readlink()`\n>   mingw: implement basic `symlink()` functionality (file symlinks only)\n>   mingw: add support for symlinks to directories\n> \n>  compat/mingw-posix.h  |   6 +-\n>  compat/mingw.c        | 635 ++++++++++++++++++++++++++++++++----------\n>  compat/win32.h        |   6 +-\n>  compat/win32/dirent.c |   5 +-\n>  read-cache.c          |  11 +\n>  5 files changed, 507 insertions(+), 156 deletions(-)\n> \n> \n> base-commit: 6f6fe02f5fe587ec9788f8a5a34281949d7b2ca1\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2018%2Fdscho%2Fsymlinks-next-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2018/dscho/symlinks-next-v1\n> Pull-Request: https://github.com/gitgitgadget/git/pull/2018\n\nI've reviewed this series and had a few comments on some of them.\n\nAll others look good, with one caveat though: symbolic links on Windows\naren't exactly an itch of mine, and I'm unfamiliar with the\ncorresponding API. That said, I didn't spot anything unusual at a\nsuperficial level.\n\nI notice that Karsten's emails bounce. Would it be appropriate to\nredirect authorship and sign-off to the other email that is registered\nin .mailmap?\n\n-- Hannes\n\n"},{"id":"532499","messageId":"77e768b2-6693-454f-9e11-fb0acdec703c@gmail.com","threadId":"64639","inReplyTo":"19030791-dafa-4a63-985b-9cdf423bfd1b@kdbg.org","subject":"Re: [PATCH 00/18] Support symbolic links on Windows","fromName":"Karsten Blees","fromEmail":"karsten.blees@gmail.com","sentAt":"2025-12-18T19:33:01Z","receivedAt":"2025-12-18T19:33:06Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"Am 18.12.2025 um 19:51 schrieb Johannes Sixt:\n> I notice that Karsten's emails bounce. Would it be appropriate to\n> redirect authorship and sign-off to the other email that is registered\n> in .mailmap?\n>\n> -- Hannes\n\nHi,\n\nindeed, the @dcon.de address that I used to sign my patches no longer \nworks, as I'm no longer working for that company. Feel free to change to \nmy current address.\n\nCheers,\n\nKarsten\n\n"},{"id":"533404","messageId":"704e952d-7924-00ce-b8b0-ad355e659335@gmx.de","threadId":"64639","inReplyTo":"46b69027-90b4-439a-a14d-61d1bb739b7b@kdbg.org","subject":"Re: [PATCH 02/18] mingw: implement `stat()` with symlink support","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2026-01-09T20:04:26Z","receivedAt":"2026-01-09T20:04:29Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Hannes,\n\nOn Thu, 18 Dec 2025, Johannes Sixt wrote:\n\n> Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:\n> > From: Karsten Blees <blees@dcon.de>\n> > \n> > With respect to symlinks, the current `mingw_stat()` implementation is\n> > almost identical to `mingw_lstat()`: except for the file type (`st_mode\n> > & S_IFMT`), it returns information about the link rather than the target.\n> > \n> > Implement `mingw_stat()` by opening the file handle requesting minimal\n> > permissions, and then calling `GetFileInformationByHandle()` on it. This\n> > way, all links are resolved by the Windows file system layer.\n> > \n> > If symlinks are disabled, use `mingw_lstat()` as before, but fail with\n> > `ELOOP` if a symlink would have to be resolved.\n> \n> This last paragraph is disconnected from the patch text. I can't find a\n> use of ELOOP anywhere in the code that has something to do with the goal\n> of this patch. Is this a remnant from early times where symbolic links\n> were optional?\n\nYou're right. Sharp eyes, by the way, I cannot count how often I glanced\nover this paragraph while pre-reviewing.\n\nAs to the reason why this paragraph is there: This comes from the initial\nversion of this patch:\nhttps://github.com/git-for-windows/git/commit/b908441ea594f022e862c04cefe8ac73bb8c0ab0\n\nI can only try to reconstruct why I skipped the ELOOP logic in the rebased\nversion at\nhttps://github.com/git-for-windows/git/commit/0181eb0c78d04f5fb065cbe2f3346077b0f9930e\n(my guess is that I realized that returning ELOOP when symlink support was\ndisabled via `core.symlinks = false` was undesirable: In particular with\nWindows 7 semantics, where symlinks could be read and used, but required\nadministrator permissions to create, that flag was meant to turn off\nsymlink _creation_, but reading existing symlinks should work\nnevertheless).\n\nI'll simply remove this paragraph from the commit message.\n\nCiao,\nJohannes\n"},{"id":"533405","messageId":"14388349-d1b5-fc8f-b6c7-4a7b43e64494@gmx.de","threadId":"64639","inReplyTo":"20488887-14B7-4471-9241-9CDBC3EAB2E8@gmail.com","subject":"Re: [PATCH 10/18] mingw: handle symlinks to directories in `mingw_unlink()`","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2026-01-09T20:04:33Z","receivedAt":"2026-01-09T20:04:36Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Ben,\n\nOn Wed, 17 Dec 2025, Ben Knoble wrote:\n\n> \n> > Le 17 déc. 2025 à 09:17, Karsten Blees via GitGitGadget <gitgitgadget@gmail.com> a écrit :\n> > \n> > ﻿From: Karsten Blees <blees@dcon.de>\n> > \n> > The `_wunlink()` and `DeleteFileW()` functions refuse to delete symlinks\n> > to directories on Windows; The error code woutl be `ERROR_ACCESS_DENIED`\n> \n> Casually reading; spotted “woutl.” Presumably would?\n\nThank you for spotting! Fixed,\nJohannes\n"},{"id":"533406","messageId":"99570ae3-d34d-c917-e218-ebbf3f9d7b6c@gmx.de","threadId":"64639","inReplyTo":"02fa15af-5a57-4557-b016-fd14b9107c6e@kdbg.org","subject":"Re: [PATCH 11/18] mingw: support renaming symlinks","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2026-01-09T20:04:40Z","receivedAt":"2026-01-09T20:04:44Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Hannes,\n\nOn Thu, 18 Dec 2025, Johannes Sixt wrote:\n\n> Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:\n> > From: Karsten Blees <blees@dcon.de>\n> > \n> > Older MSVCRT's `_wrename()` function cannot rename symlinks over\n> > existing files: it returns success without doing anything. Newer\n> > MSVCR*.dll versions probably do not share this problem: according to CRT\n> > sources, they just call `MoveFileEx()` with the `MOVEFILE_COPY_ALLOWED`\n> > flag.\n> > \n> > Avoid the `_wrename()` call, and go with directly calling\n> > `MoveFileEx()`, with proper error handling of course.\n> > \n> > Signed-off-by: Karsten Blees <blees@dcon.de>\n> > Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n> > ---\n> >  compat/mingw.c | 38 ++++++++++++++++----------------------\n> >  1 file changed, 16 insertions(+), 22 deletions(-)\n> > \n> > diff --git a/compat/mingw.c b/compat/mingw.c\n> > index b1cc30d0f1..55f0bb478e 100644\n> > --- a/compat/mingw.c\n> > +++ b/compat/mingw.c\n> > @@ -2275,7 +2275,7 @@ int mingw_accept(int sockfd1, struct sockaddr *sa, socklen_t *sz)\n> >  int mingw_rename(const char *pold, const char *pnew)\n> >  {\n> >  \tstatic int supports_file_rename_info_ex = 1;\n> > -\tDWORD attrs, gle;\n> > +\tDWORD attrs = INVALID_FILE_ATTRIBUTES, gle;\n> >  \tint tries = 0;\n> >  \twchar_t wpold[MAX_PATH], wpnew[MAX_PATH];\n> >  \tint wpnew_len;\n> > @@ -2286,15 +2286,6 @@ int mingw_rename(const char *pold, const char *pnew)\n> >  \tif (wpnew_len < 0)\n> >  \t\treturn -1;\n> >  \n> > -\t/*\n> > -\t * Try native rename() first to get errno right.\n> > -\t * It is based on MoveFile(), which cannot overwrite existing files.\n> > -\t */\n> > -\tif (!_wrename(wpold, wpnew))\n> > -\t\treturn 0;\n> > -\tif (errno != EEXIST)\n> > -\t\treturn -1;\n> > -\n> >  repeat:\n> >  \tif (supports_file_rename_info_ex) {\n> >  \t\t/*\n> > @@ -2370,13 +2361,22 @@ repeat:\n> >  \t\t * to retry.\n> >  \t\t */\n> >  \t} else {\n> > -\t\tif (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))\n> > +\t\tif (MoveFileExW(wpold, wpnew,\n> > +\t\t\t\tMOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED))\n> >  \t\t\treturn 0;\n> >  \t\tgle = GetLastError();\n> >  \t}\n> >  \n> > -\t/* TODO: translate more errors */\n> > -\tif (gle == ERROR_ACCESS_DENIED &&\n> > +\t/* revert file attributes on failure */\n> > +\tif (attrs != INVALID_FILE_ATTRIBUTES)\n> > +\t\tSetFileAttributesW(wpnew, attrs);\n> > +\n> > +\tif (!is_file_in_use_error(gle)) {\n> > +\t\terrno = err_win_to_posix(gle);\n> > +\t\treturn -1;\n> > +\t}\n> > +\n> > +\tif (attrs == INVALID_FILE_ATTRIBUTES &&\n> >  \t    (attrs = GetFileAttributesW(wpnew)) != INVALID_FILE_ATTRIBUTES) {\n> >  \t\tif (attrs & FILE_ATTRIBUTE_DIRECTORY) {\n> >  \t\t\tDWORD attrsold = GetFileAttributesW(wpold);\n> > @@ -2388,16 +2388,10 @@ repeat:\n> >  \t\t\treturn -1;\n> >  \t\t}\n> >  \t\tif ((attrs & FILE_ATTRIBUTE_READONLY) &&\n> > -\t\t    SetFileAttributesW(wpnew, attrs & ~FILE_ATTRIBUTE_READONLY)) {\n> > -\t\t\tif (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))\n> > -\t\t\t\treturn 0;\n> > -\t\t\tgle = GetLastError();\n> > -\t\t\t/* revert file attributes on failure */\n> > -\t\t\tSetFileAttributesW(wpnew, attrs);\n> > -\t\t}\n> > +\t\t    SetFileAttributesW(wpnew, attrs & ~FILE_ATTRIBUTE_READONLY))\n> > +\t\t\tgoto repeat;\n> >  \t}\n> > -\tif (gle == ERROR_ACCESS_DENIED &&\n> > -\t       retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n> > +\tif (retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n> >  \t\t       \"Should I try again?\", pold, pnew))\n> >  \t\tgoto repeat;\n> >  \n> \n> The logic in this function is incredibly convoluted. It does look\n> somewhat reasonable, at least on the non-error path, but whether the\n> variable attr is changed and reset as needed after 'goto repeat' and the\n> various failure modes, I cannot tell. I give up and trust that this code\n> has been battle-tested during the past decade and works as desired.\n\nI do agree that the logic is quite convoluted. Historically grown, like.\nBut as you suspect: This has been battle-hardened, and I am loathe to\nintroduce a regression by making it prettier at this point. This is tried\nand tested code, and that counts for something.\n\nCiao,\nJohannes\n\n"},{"id":"533407","messageId":"5fe64b77-d10b-b66e-8622-14bec1e96f4a@gmx.de","threadId":"64639","inReplyTo":"8826825b-79ad-4700-aeb5-71e7847ca5dc@kdbg.org","subject":"Re: [PATCH 13/18] mingw: implement `readlink()`","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2026-01-09T20:04:48Z","receivedAt":"2026-01-09T20:04:51Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Hannes,\n\nOn Thu, 18 Dec 2025, Johannes Sixt wrote:\n\n> Am 17.12.25 um 15:08 schrieb Karsten Blees via GitGitGadget:\n> > From: Karsten Blees <blees@dcon.de>\n> > \n> > Implement `readlink()` by reading NTFS reparse points via the\n> > `read_reparse_point()` function that was introduced earlier to determine\n> > the length of symlink targets. Works for symlinks and directory\n> > junctions. If symlinks are disabled, fail with `ENOSYS`.\n> \n> This last sentence is obsolete, I think, because I cannot see how the\n> patch achieves a failure with ENOSYS.\n\nIndeed, this is obsolete. Just like with the ELOOP commit message comment\nof 02/18, I must have dropped this because reading symlinks should work\neven if creating symlinks has been disabled via `core.symlinks`. Here is\nthe range-diff between the last version of the patch that still had the\nENOSYS logic and the first version that lacked it (Git for Windows-only\ncommits):\n\n1:  4f353d988de4 ! 1:  1d079621427c Win32: implement readlink()\n\n   @@ compat/mingw.c: int link(const char *oldpath, const char *newpath)\n    +\tchar tmpbuf[MAX_LONG_PATH];\n    +\tint len;\n    +\n   -+\t/* fail if symlinks are disabled */\n   -+\tif (!has_symlinks) {\n   -+\t\terrno = ENOSYS;\n   -+\t\treturn -1;\n   -+\t}\n   -+\n    +\tif (xutftowcs_long_path(wpath, path) < 0)\n    +\t\treturn -1;\n    +\n\nSo: Unfortunately I have no record that I can readily produce that would\nmotivate that change. Given that it happened during the same v2.19.2\ntimeframe as the ELOOP change, there must have been some broader\ndiscussion about this, but I could not find it, not even in the release\nnotes of that version:\nhttps://github.com/git-for-windows/git/releases/tag/v2.19.2.windows.1\n\nAll I can present is the reconstructed rationale that just because Git is\nnot allowed (or able) to create symlinks does not mean that they cannot\nexist, and therefore Git should at least read and parse them as expected,\nindependent of the value of `core.symlinks`.\n\nSo yes, this part of the commit message is just simply confusing at this\npoint, so I'll drop it.\n\nCiao,\nJohannes\n"},{"id":"533408","messageId":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.git.1765980535.gitgitgadget@gmail.com","subject":"[PATCH v2 00/18] Support symbolic links on Windows","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:04:57Z","receivedAt":"2026-01-09T20:05:18Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"This finally upstreams Git for Windows' support for Windows' branch of\nsymbolic links, which has been maturing since 2015. It is based off of\njs/prep-symlink-windows.\n\nChanges since v1:\n\n * Changed Karsten's email address to the current one.\n * The changes to do_lstat() are now reflected by the comment preceding that\n   function.\n * The commit message mentioning some ELOOP logic when symlink support is\n   disabled, which had no corresponding part in the commit's diff, was\n   adjusted to no longer mention that long gone change.\n * Fixed the typo \"woutl\".\n * Dropped a misleading, stale comment about ENOSYS in the readlink()\n   implementation (talking about a long-dropped part of the original patch).\n\nBill Zissimopoulos (1):\n  mingw: compute the correct size for symlinks in `mingw_lstat()`\n\nJohannes Schindelin (3):\n  mingw: try to create symlinks without elevated permissions\n  mingw: emulate `stat()` a little more faithfully\n  mingw: special-case index entries for symlinks with buggy size\n\nKarsten Blees (14):\n  mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`\n  mingw: implement `stat()` with symlink support\n  mingw: drop the separate `do_lstat()` function\n  mingw: let `mingw_lstat()` error early upon problems with reparse\n    points\n  mingw: teach dirent about symlinks\n  mingw: factor out the retry logic\n  mingw: change default of `core.symlinks` to false\n  mingw: add symlink-specific error codes\n  mingw: handle symlinks to directories in `mingw_unlink()`\n  mingw: support renaming symlinks\n  mingw: allow `mingw_chdir()` to change to symlink-resolved directories\n  mingw: implement `readlink()`\n  mingw: implement basic `symlink()` functionality (file symlinks only)\n  mingw: add support for symlinks to directories\n\n compat/mingw-posix.h  |   6 +-\n compat/mingw.c        | 635 ++++++++++++++++++++++++++++++++----------\n compat/win32.h        |   6 +-\n compat/win32/dirent.c |   5 +-\n read-cache.c          |  11 +\n 5 files changed, 507 insertions(+), 156 deletions(-)\n\n\nbase-commit: 1887b3dd06823575e37ad19b5827d467e126c6ed\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2018%2Fdscho%2Fsymlinks-next-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2018/dscho/symlinks-next-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2018\n\nRange-diff vs v1:\n\n  1:  dae450dd0e !  1:  6ec4ff7457 mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`\n     @@ Commit message\n          free, and also have a (wide char) buffer that can be modified. This\n          makes it easy to avoid that extraneous Win32 API call.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n     +@@ compat/mingw.c: static int has_valid_directory_prefix(wchar_t *wfilename)\n     + }\n     + \n     + /* We keep the do_lstat code in a separate function to avoid recursion.\n     +- * When a path ends with a slash, the stat will fail with ENOENT. In\n     +- * this case, we strip the trailing slashes and stat again.\n     ++ * When a path ends with a slash, the call to `GetFileAttributedExW()`\n     ++ * would fail. To prevent this, we strip any trailing slashes before that\n     ++ * call.\n     +  *\n     +  * If follow is true then act like stat() and report on the link\n     +  * target. Otherwise report on the link itself.\n      @@ compat/mingw.c: static int do_lstat(int follow, const char *file_name, struct stat *buf)\n       {\n       \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n  2:  c36848eda7 !  2:  5d83a8ab76 mingw: implement `stat()` with symlink support\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: implement `stat()` with symlink support\n     @@ Commit message\n          permissions, and then calling `GetFileInformationByHandle()` on it. This\n          way, all links are resolved by the Windows file system layer.\n      \n     -    If symlinks are disabled, use `mingw_lstat()` as before, but fail with\n     -    `ELOOP` if a symlink would have to be resolved.\n     -\n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n  3:  aa0ca80bbb !  3:  d3953f1826 mingw: drop the separate `do_lstat()` function\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: drop the separate `do_lstat()` function\n     @@ Commit message\n          the extra function and the old `mingw_stat()`-specific (`follow == 1`)\n          logic.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n     @@ compat/mingw.c: static int has_valid_directory_prefix(wchar_t *wfilename)\n       }\n       \n      -/* We keep the do_lstat code in a separate function to avoid recursion.\n     -- * When a path ends with a slash, the stat will fail with ENOENT. In\n     -- * this case, we strip the trailing slashes and stat again.\n     +- * When a path ends with a slash, the call to `GetFileAttributedExW()`\n     +- * would fail. To prevent this, we strip any trailing slashes before that\n     +- * call.\n      - *\n      - * If follow is true then act like stat() and report on the link\n      - * target. Otherwise report on the link itself.\n  4:  886044373b !  4:  a6355789c3 mingw: let `mingw_lstat()` error early upon problems with reparse points\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: let `mingw_lstat()` error early upon problems with reparse points\n     @@ Commit message\n          `--color-moved -w`. That code was _not_ moved because a subsequent\n          commit will move it to an altogether different function, anyway.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n  5:  db1d156aa0 !  5:  8b7f5a8fc7 mingw: teach dirent about symlinks\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: teach dirent about symlinks\n     @@ Commit message\n      \n          Implement `DT_LNK` detection in dirent.c's `readdir()` function.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n  6:  4c49a3d9bf =  6:  1159e86dc4 mingw: compute the correct size for symlinks in `mingw_lstat()`\n  7:  ad74d540f2 !  7:  4aeccd6656 mingw: factor out the retry logic\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: factor out the retry logic\n     @@ Commit message\n      \n          In `mingw_rmdir()`, we include special error handling in the retry loop.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n  8:  25313cea76 !  8:  86c0742748 mingw: change default of `core.symlinks` to false\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: change default of `core.symlinks` to false\n     @@ Commit message\n          is true for the time being (an experiment to switch to BusyBox-w32\n          failed due to the experimental nature of BusyBox-w32).\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n  9:  b698f4a002 !  9:  9a0093d34b mingw: add symlink-specific error codes\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: add symlink-specific error codes\n     @@ Commit message\n          Let's handle a couple of symlink-related error codes that will become\n          relevant with the upcoming support for symlinks on Windows.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n 10:  282aba42e8 ! 10:  5dc90f9785 mingw: handle symlinks to directories in `mingw_unlink()`\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: handle symlinks to directories in `mingw_unlink()`\n      \n          The `_wunlink()` and `DeleteFileW()` functions refuse to delete symlinks\n     -    to directories on Windows; The error code woutl be `ERROR_ACCESS_DENIED`\n     +    to directories on Windows; The error code would be `ERROR_ACCESS_DENIED`\n          in that case. Take that error code as an indicator that we need to try\n          `_wrmdir()` as well. In the best case, it will remove a symlink. In the\n          worst case, it will fail with the same error code again.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n 11:  5cb3b10500 ! 11:  3670a0a181 mingw: support renaming symlinks\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: support renaming symlinks\n     @@ Commit message\n          Avoid the `_wrename()` call, and go with directly calling\n          `MoveFileEx()`, with proper error handling of course.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n 12:  49920839bb ! 12:  f79cf31dab mingw: allow `mingw_chdir()` to change to symlink-resolved directories\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: allow `mingw_chdir()` to change to symlink-resolved directories\n     @@ Commit message\n          Windows is limited to only MAX_PATH (260) characters. Therefore using\n          symlinks and long paths in combination may be problematic.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n 13:  8fef8220f4 ! 13:  1ae9c63a89 mingw: implement `readlink()`\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: implement `readlink()`\n     @@ Commit message\n          Implement `readlink()` by reading NTFS reparse points via the\n          `read_reparse_point()` function that was introduced earlier to determine\n          the length of symlink targets. Works for symlinks and directory\n     -    junctions. If symlinks are disabled, fail with `ENOSYS`.\n     +    junctions.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw-posix.h ##\n 14:  1dd5f9d6cd ! 14:  4e0ac43ef6 mingw: implement basic `symlink()` functionality (file symlinks only)\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: implement basic `symlink()` functionality (file symlinks only)\n     @@ Commit message\n          This implementation fails with `ENOSYS` if symlinks are disabled or\n          unsupported.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw-posix.h ##\n 15:  7b6dbc73f7 ! 15:  3d479fd47e mingw: add support for symlinks to directories\n     @@\n       ## Metadata ##\n     -Author: Karsten Blees <blees@dcon.de>\n     +Author: Karsten Blees <karsten.blees@gmail.com>\n      \n       ## Commit message ##\n          mingw: add support for symlinks to directories\n     @@ Commit message\n          to directory or vice versa, or if the target directory is created in\n          another process. It's the best Git can do, though.\n      \n     -    Signed-off-by: Karsten Blees <blees@dcon.de>\n     +    Signed-off-by: Karsten Blees <karsten.blees@gmail.com>\n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n       ## compat/mingw.c ##\n 16:  d3b89c29b0 = 16:  fb6aa461da mingw: try to create symlinks without elevated permissions\n 17:  2e73ab4221 = 17:  40c3f7f36e mingw: emulate `stat()` a little more faithfully\n 18:  817f488523 = 18:  afcf2bbfcb mingw: special-case index entries for symlinks with buggy size\n\n-- \ngitgitgadget\n"},{"id":"533409","messageId":"6ec4ff74577f3690a11160812666e5ca800d9d65.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 01/18] mingw: don't call `GetFileAttributes()` twice in `mingw_lstat()`","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:04:58Z","receivedAt":"2026-01-09T20:05:19Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nThe Win32 API function `GetFileAttributes()` cannot handle paths with\ntrailing dir separators. The current `mingw_stat()`/`mingw_lstat()`\nimplementation calls `GetFileAttributes()` twice if the path has\ntrailing slashes (first with the original path that was passed as\nfunction parameter, and and a second time with a path copy with trailing\n'/' removed).\n\nWith the conversion to wide Unicode, we get the length of the path for\nfree, and also have a (wide char) buffer that can be modified. This\nmakes it easy to avoid that extraneous Win32 API call.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 53 ++++++++++++++------------------------------------\n 1 file changed, 15 insertions(+), 38 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex cf4f3c92e7..ae6826948e 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -918,8 +918,9 @@ static int has_valid_directory_prefix(wchar_t *wfilename)\n }\n \n /* We keep the do_lstat code in a separate function to avoid recursion.\n- * When a path ends with a slash, the stat will fail with ENOENT. In\n- * this case, we strip the trailing slashes and stat again.\n+ * When a path ends with a slash, the call to `GetFileAttributedExW()`\n+ * would fail. To prevent this, we strip any trailing slashes before that\n+ * call.\n  *\n  * If follow is true then act like stat() and report on the link\n  * target. Otherwise report on the link itself.\n@@ -928,9 +929,18 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)\n {\n \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n \twchar_t wfilename[MAX_PATH];\n-\tif (xutftowcs_path(wfilename, file_name) < 0)\n+\tint wlen = xutftowcs_path(wfilename, file_name);\n+\tif (wlen < 0)\n \t\treturn -1;\n \n+\t/* strip trailing '/', or GetFileAttributes will fail */\n+\twhile (wlen && is_dir_sep(wfilename[wlen - 1]))\n+\t\twfilename[--wlen] = 0;\n+\tif (!wlen) {\n+\t\terrno = ENOENT;\n+\t\treturn -1;\n+\t}\n+\n \tif (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {\n \t\tbuf->st_ino = 0;\n \t\tbuf->st_gid = 0;\n@@ -990,39 +1000,6 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)\n \treturn -1;\n }\n \n-/* We provide our own lstat/fstat functions, since the provided\n- * lstat/fstat functions are so slow. These stat functions are\n- * tailored for Git's usage (read: fast), and are not meant to be\n- * complete. Note that Git stat()s are redirected to mingw_lstat()\n- * too, since Windows doesn't really handle symlinks that well.\n- */\n-static int do_stat_internal(int follow, const char *file_name, struct stat *buf)\n-{\n-\tsize_t namelen;\n-\tchar alt_name[PATH_MAX];\n-\n-\tif (!do_lstat(follow, file_name, buf))\n-\t\treturn 0;\n-\n-\t/* if file_name ended in a '/', Windows returned ENOENT;\n-\t * try again without trailing slashes\n-\t */\n-\tif (errno != ENOENT)\n-\t\treturn -1;\n-\n-\tnamelen = strlen(file_name);\n-\tif (namelen && file_name[namelen-1] != '/')\n-\t\treturn -1;\n-\twhile (namelen && file_name[namelen-1] == '/')\n-\t\t--namelen;\n-\tif (!namelen || namelen >= PATH_MAX)\n-\t\treturn -1;\n-\n-\tmemcpy(alt_name, file_name, namelen);\n-\talt_name[namelen] = 0;\n-\treturn do_lstat(follow, alt_name, buf);\n-}\n-\n static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)\n {\n \tBY_HANDLE_FILE_INFORMATION fdata;\n@@ -1048,11 +1025,11 @@ static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)\n \n int mingw_lstat(const char *file_name, struct stat *buf)\n {\n-\treturn do_stat_internal(0, file_name, buf);\n+\treturn do_lstat(0, file_name, buf);\n }\n int mingw_stat(const char *file_name, struct stat *buf)\n {\n-\treturn do_stat_internal(1, file_name, buf);\n+\treturn do_lstat(1, file_name, buf);\n }\n \n int mingw_fstat(int fd, struct stat *buf)\n-- \ngitgitgadget\n\n"},{"id":"533410","messageId":"5d83a8ab760c7fcbffe43ed82cfebd3d8eefda96.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 02/18] mingw: implement `stat()` with symlink support","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:04:59Z","receivedAt":"2026-01-09T20:05:20Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nWith respect to symlinks, the current `mingw_stat()` implementation is\nalmost identical to `mingw_lstat()`: except for the file type (`st_mode\n& S_IFMT`), it returns information about the link rather than the target.\n\nImplement `mingw_stat()` by opening the file handle requesting minimal\npermissions, and then calling `GetFileInformationByHandle()` on it. This\nway, all links are resolved by the Windows file system layer.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 19 ++++++++++++++++++-\n 1 file changed, 18 insertions(+), 1 deletion(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex ae6826948e..13970ae729 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -1027,9 +1027,26 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n {\n \treturn do_lstat(0, file_name, buf);\n }\n+\n int mingw_stat(const char *file_name, struct stat *buf)\n {\n-\treturn do_lstat(1, file_name, buf);\n+\twchar_t wfile_name[MAX_PATH];\n+\tHANDLE hnd;\n+\tint result;\n+\n+\t/* open the file and let Windows resolve the links */\n+\tif (xutftowcs_path(wfile_name, file_name) < 0)\n+\t\treturn -1;\n+\thnd = CreateFileW(wfile_name, 0,\n+\t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n+\t\t\tOPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);\n+\tif (hnd == INVALID_HANDLE_VALUE) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n+\t}\n+\tresult = get_file_info_by_handle(hnd, buf);\n+\tCloseHandle(hnd);\n+\treturn result;\n }\n \n int mingw_fstat(int fd, struct stat *buf)\n-- \ngitgitgadget\n\n"},{"id":"533412","messageId":"d3953f182658bf0935ff7962bb5f04f51bef7d51.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 03/18] mingw: drop the separate `do_lstat()` function","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:00Z","receivedAt":"2026-01-09T20:05:21Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nWith the new `mingw_stat()` implementation, `do_lstat()` is only called\nfrom `mingw_lstat()` (with the function parameter `follow == 0`). Remove\nthe extra function and the old `mingw_stat()`-specific (`follow == 1`)\nlogic.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 23 ++---------------------\n 1 file changed, 2 insertions(+), 21 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 13970ae729..ec6c2801d3 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -917,15 +917,7 @@ static int has_valid_directory_prefix(wchar_t *wfilename)\n \treturn 1;\n }\n \n-/* We keep the do_lstat code in a separate function to avoid recursion.\n- * When a path ends with a slash, the call to `GetFileAttributedExW()`\n- * would fail. To prevent this, we strip any trailing slashes before that\n- * call.\n- *\n- * If follow is true then act like stat() and report on the link\n- * target. Otherwise report on the link itself.\n- */\n-static int do_lstat(int follow, const char *file_name, struct stat *buf)\n+int mingw_lstat(const char *file_name, struct stat *buf)\n {\n \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n \twchar_t wfilename[MAX_PATH];\n@@ -959,13 +951,7 @@ static int do_lstat(int follow, const char *file_name, struct stat *buf)\n \t\t\tif (handle != INVALID_HANDLE_VALUE) {\n \t\t\t\tif ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&\n \t\t\t\t\t\t(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {\n-\t\t\t\t\tif (follow) {\n-\t\t\t\t\t\tchar buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];\n-\t\t\t\t\t\tbuf->st_size = readlink(file_name, buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE);\n-\t\t\t\t\t} else {\n-\t\t\t\t\t\tbuf->st_mode = S_IFLNK;\n-\t\t\t\t\t}\n-\t\t\t\t\tbuf->st_mode |= S_IREAD;\n+\t\t\t\t\tbuf->st_mode = S_IFLNK | S_IREAD;\n \t\t\t\t\tif (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))\n \t\t\t\t\t\tbuf->st_mode |= S_IWRITE;\n \t\t\t\t}\n@@ -1023,11 +1009,6 @@ static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)\n \treturn 0;\n }\n \n-int mingw_lstat(const char *file_name, struct stat *buf)\n-{\n-\treturn do_lstat(0, file_name, buf);\n-}\n-\n int mingw_stat(const char *file_name, struct stat *buf)\n {\n \twchar_t wfile_name[MAX_PATH];\n-- \ngitgitgadget\n\n"},{"id":"533413","messageId":"a6355789c359f3631d5a8de7cbd1cbdbb719f8d1.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 04/18] mingw: let `mingw_lstat()` error early upon problems with reparse points","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:01Z","receivedAt":"2026-01-09T20:05:22Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nWhen obtaining lstat information for reparse points, we need to call\n`FindFirstFile()` in addition to `GetFileInformationEx()` to obtain\nthe type of the reparse point (symlink, mount point etc.). However,\ncurrently there is no error handling whatsoever if `FindFirstFile()`\nfails.\n\nCall `FindFirstFile()` before modifying the `stat *buf` output parameter\nand error out if the call fails.\n\nNote: The `FindFirstFile()` return value includes all the data\nthat we get from `GetFileAttributesEx()`, so we could replace\n`GetFileAttributesEx()` with `FindFirstFile()`. We don't do that because\n`GetFileAttributesEx()` is about twice as fast for single files. I.e.\nwe only pay the extra cost of calling `FindFirstFile()` in the rare case\nthat we encounter a reparse point.\n\nPlease also note that the indentation the remaining reparse point\ncode changed, and hence the best way to look at this diff is with\n`--color-moved -w`. That code was _not_ moved because a subsequent\ncommit will move it to an altogether different function, anyway.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 24 ++++++++++++++----------\n 1 file changed, 14 insertions(+), 10 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex ec6c2801d3..23a926c7d1 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -920,6 +920,7 @@ static int has_valid_directory_prefix(wchar_t *wfilename)\n int mingw_lstat(const char *file_name, struct stat *buf)\n {\n \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n+\tWIN32_FIND_DATAW findbuf = { 0 };\n \twchar_t wfilename[MAX_PATH];\n \tint wlen = xutftowcs_path(wfilename, file_name);\n \tif (wlen < 0)\n@@ -934,6 +935,13 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n \t}\n \n \tif (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {\n+\t\t/* for reparse points, use FindFirstFile to get the reparse tag */\n+\t\tif (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {\n+\t\t\tHANDLE handle = FindFirstFileW(wfilename, &findbuf);\n+\t\t\tif (handle == INVALID_HANDLE_VALUE)\n+\t\t\t\tgoto error;\n+\t\t\tFindClose(handle);\n+\t\t}\n \t\tbuf->st_ino = 0;\n \t\tbuf->st_gid = 0;\n \t\tbuf->st_uid = 0;\n@@ -946,20 +954,16 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n \t\tfiletime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim));\n \t\tfiletime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim));\n \t\tif (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {\n-\t\t\tWIN32_FIND_DATAW findbuf;\n-\t\t\tHANDLE handle = FindFirstFileW(wfilename, &findbuf);\n-\t\t\tif (handle != INVALID_HANDLE_VALUE) {\n-\t\t\t\tif ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&\n-\t\t\t\t\t\t(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {\n-\t\t\t\t\tbuf->st_mode = S_IFLNK | S_IREAD;\n-\t\t\t\t\tif (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))\n-\t\t\t\t\t\tbuf->st_mode |= S_IWRITE;\n-\t\t\t\t}\n-\t\t\t\tFindClose(handle);\n+\t\t\tif ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&\n+\t\t\t\t\t(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {\n+\t\t\t\tbuf->st_mode = S_IFLNK | S_IREAD;\n+\t\t\t\tif (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))\n+\t\t\t\t\tbuf->st_mode |= S_IWRITE;\n \t\t\t}\n \t\t}\n \t\treturn 0;\n \t}\n+error:\n \tswitch (GetLastError()) {\n \tcase ERROR_ACCESS_DENIED:\n \tcase ERROR_SHARING_VIOLATION:\n-- \ngitgitgadget\n\n"},{"id":"533415","messageId":"8b7f5a8fc7eb6e9e2cc704b5458161952d83666a.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 05/18] mingw: teach dirent about symlinks","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:02Z","receivedAt":"2026-01-09T20:05:24Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nMove the `S_IFLNK` detection to `file_attr_to_st_mode()`.\n\nImplement `DT_LNK` detection in dirent.c's `readdir()` function.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c        | 13 +++----------\n compat/win32.h        |  6 ++++--\n compat/win32/dirent.c |  5 ++++-\n 3 files changed, 11 insertions(+), 13 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 23a926c7d1..a3a48db581 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -946,21 +946,14 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n \t\tbuf->st_gid = 0;\n \t\tbuf->st_uid = 0;\n \t\tbuf->st_nlink = 1;\n-\t\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes);\n+\t\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes,\n+\t\t\t\tfindbuf.dwReserved0);\n \t\tbuf->st_size = fdata.nFileSizeLow |\n \t\t\t(((off_t)fdata.nFileSizeHigh)<<32);\n \t\tbuf->st_dev = buf->st_rdev = 0; /* not used by Git */\n \t\tfiletime_to_timespec(&(fdata.ftLastAccessTime), &(buf->st_atim));\n \t\tfiletime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim));\n \t\tfiletime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim));\n-\t\tif (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {\n-\t\t\tif ((findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) &&\n-\t\t\t\t\t(findbuf.dwReserved0 == IO_REPARSE_TAG_SYMLINK)) {\n-\t\t\t\tbuf->st_mode = S_IFLNK | S_IREAD;\n-\t\t\t\tif (!(findbuf.dwFileAttributes & FILE_ATTRIBUTE_READONLY))\n-\t\t\t\t\tbuf->st_mode |= S_IWRITE;\n-\t\t\t}\n-\t\t}\n \t\treturn 0;\n \t}\n error:\n@@ -1003,7 +996,7 @@ static int get_file_info_by_handle(HANDLE hnd, struct stat *buf)\n \tbuf->st_gid = 0;\n \tbuf->st_uid = 0;\n \tbuf->st_nlink = 1;\n-\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes);\n+\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes, 0);\n \tbuf->st_size = fdata.nFileSizeLow |\n \t\t(((off_t)fdata.nFileSizeHigh)<<32);\n \tbuf->st_dev = buf->st_rdev = 0; /* not used by Git */\ndiff --git a/compat/win32.h b/compat/win32.h\nindex a97e880757..671bcc81f9 100644\n--- a/compat/win32.h\n+++ b/compat/win32.h\n@@ -6,10 +6,12 @@\n #include <windows.h>\n #endif\n \n-static inline int file_attr_to_st_mode (DWORD attr)\n+static inline int file_attr_to_st_mode (DWORD attr, DWORD tag)\n {\n \tint fMode = S_IREAD;\n-\tif (attr & FILE_ATTRIBUTE_DIRECTORY)\n+\tif ((attr & FILE_ATTRIBUTE_REPARSE_POINT) && tag == IO_REPARSE_TAG_SYMLINK)\n+\t\tfMode |= S_IFLNK;\n+\telse if (attr & FILE_ATTRIBUTE_DIRECTORY)\n \t\tfMode |= S_IFDIR;\n \telse\n \t\tfMode |= S_IFREG;\ndiff --git a/compat/win32/dirent.c b/compat/win32/dirent.c\nindex 52420ec7d4..24ee9b814d 100644\n--- a/compat/win32/dirent.c\n+++ b/compat/win32/dirent.c\n@@ -12,7 +12,10 @@ static inline void finddata2dirent(struct dirent *ent, WIN32_FIND_DATAW *fdata)\n \txwcstoutf(ent->d_name, fdata->cFileName, sizeof(ent->d_name));\n \n \t/* Set file type, based on WIN32_FIND_DATA */\n-\tif (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)\n+\tif ((fdata->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)\n+\t\t\t&& fdata->dwReserved0 == IO_REPARSE_TAG_SYMLINK)\n+\t\tent->d_type = DT_LNK;\n+\telse if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)\n \t\tent->d_type = DT_DIR;\n \telse\n \t\tent->d_type = DT_REG;\n-- \ngitgitgadget\n\n"},{"id":"533416","messageId":"1159e86dc42510297599676efee98057be857878.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 06/18] mingw: compute the correct size for symlinks in `mingw_lstat()`","fromName":"Bill Zissimopoulos via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:03Z","receivedAt":"2026-01-09T20:05:25Z","isPatch":true,"sender":{"key":"name:Bill Zissimopoulos","avatar":null},"body":"From: Bill Zissimopoulos <billziss@navimatics.com>\n\nPOSIX specifies that upon successful return from `lstat()`: \"the\nvalue of the st_size member shall be set to the length of the pathname\ncontained in the symbolic link not including any terminating null byte\".\n\nGit typically doesn't trust the `stat.st_size` member of symlinks (e.g.\nsee `strbuf_readlink()`). Therefore, it is tempting to save on the extra\noverhead of opening and reading the reparse point merely to calculate\nthe exact size of the link target.\n\nThis is, in fact, what Git for Windows did, from May 2015 to May 2020.\nAt least almost: some functions take shortcuts if `st_size` is 0 (e.g.\n`diff_populate_filespec()`), hence Git for Windows hard-coded the length\nof all symlinks to MAX_PATH.\n\nThis did cause problems, though, specifically in Git repositories\nthat were also accessed by Git for Cygwin or Git for WSL. For example,\ndoing `git reset --hard` using Git for Windows would update the size of\nsymlinks in the index to be MAX_PATH; at a later time Git for Cygwin\nor Git for WSL would find that symlinks have changed size during `git\nstatus` and update the index. And then Git for Windows would think that\nthe index needs to be updated. Even if the symlinks did not, in fact,\nchange. To avoid that, the correct size must be determined.\n\nSigned-off-by: Bill Zissimopoulos <billziss@navimatics.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 114 ++++++++++++++++++++++++++++++++++++++++++++-----\n 1 file changed, 104 insertions(+), 10 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex a3a48db581..c7571951dc 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -21,6 +21,7 @@\n #define SECURITY_WIN32\n #include <sspi.h>\n #include <wchar.h>\n+#include <winioctl.h>\n #include <winternl.h>\n \n #define STATUS_DELETE_PENDING ((NTSTATUS) 0xC0000056)\n@@ -917,10 +918,102 @@ static int has_valid_directory_prefix(wchar_t *wfilename)\n \treturn 1;\n }\n \n+#ifndef _WINNT_H\n+/*\n+ * The REPARSE_DATA_BUFFER structure is defined in the Windows DDK (in\n+ * ntifs.h) and in MSYS1's winnt.h (which defines _WINNT_H). So define\n+ * it ourselves if we are on MSYS2 (whose winnt.h defines _WINNT_).\n+ */\n+typedef struct _REPARSE_DATA_BUFFER {\n+\tDWORD  ReparseTag;\n+\tWORD   ReparseDataLength;\n+\tWORD   Reserved;\n+#ifndef _MSC_VER\n+\t_ANONYMOUS_UNION\n+#endif\n+\tunion {\n+\t\tstruct {\n+\t\t\tWORD   SubstituteNameOffset;\n+\t\t\tWORD   SubstituteNameLength;\n+\t\t\tWORD   PrintNameOffset;\n+\t\t\tWORD   PrintNameLength;\n+\t\t\tULONG  Flags;\n+\t\t\tWCHAR PathBuffer[1];\n+\t\t} SymbolicLinkReparseBuffer;\n+\t\tstruct {\n+\t\t\tWORD   SubstituteNameOffset;\n+\t\t\tWORD   SubstituteNameLength;\n+\t\t\tWORD   PrintNameOffset;\n+\t\t\tWORD   PrintNameLength;\n+\t\t\tWCHAR PathBuffer[1];\n+\t\t} MountPointReparseBuffer;\n+\t\tstruct {\n+\t\t\tBYTE   DataBuffer[1];\n+\t\t} GenericReparseBuffer;\n+\t} DUMMYUNIONNAME;\n+} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;\n+#endif\n+\n+static int read_reparse_point(const WCHAR *wpath, BOOL fail_on_unknown_tag,\n+\t\t\t      char *tmpbuf, int *plen, DWORD *ptag)\n+{\n+\tHANDLE handle;\n+\tWCHAR *wbuf;\n+\tREPARSE_DATA_BUFFER *b = alloca(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);\n+\tDWORD dummy;\n+\n+\t/* read reparse point data */\n+\thandle = CreateFileW(wpath, 0,\n+\t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n+\t\t\tOPEN_EXISTING,\n+\t\t\tFILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);\n+\tif (handle == INVALID_HANDLE_VALUE) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n+\t}\n+\tif (!DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0, b,\n+\t\t\tMAXIMUM_REPARSE_DATA_BUFFER_SIZE, &dummy, NULL)) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\tCloseHandle(handle);\n+\t\treturn -1;\n+\t}\n+\tCloseHandle(handle);\n+\n+\t/* get target path for symlinks or mount points (aka 'junctions') */\n+\tswitch ((*ptag = b->ReparseTag)) {\n+\tcase IO_REPARSE_TAG_SYMLINK:\n+\t\twbuf = (WCHAR*) (((char*) b->SymbolicLinkReparseBuffer.PathBuffer)\n+\t\t\t\t+ b->SymbolicLinkReparseBuffer.SubstituteNameOffset);\n+\t\t*(WCHAR*) (((char*) wbuf)\n+\t\t\t\t+ b->SymbolicLinkReparseBuffer.SubstituteNameLength) = 0;\n+\t\tbreak;\n+\tcase IO_REPARSE_TAG_MOUNT_POINT:\n+\t\twbuf = (WCHAR*) (((char*) b->MountPointReparseBuffer.PathBuffer)\n+\t\t\t\t+ b->MountPointReparseBuffer.SubstituteNameOffset);\n+\t\t*(WCHAR*) (((char*) wbuf)\n+\t\t\t\t+ b->MountPointReparseBuffer.SubstituteNameLength) = 0;\n+\t\tbreak;\n+\tdefault:\n+\t\tif (fail_on_unknown_tag) {\n+\t\t\terrno = EINVAL;\n+\t\t\treturn -1;\n+\t\t} else {\n+\t\t\t*plen = MAX_PATH;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\tif ((*plen =\n+\t     xwcstoutf(tmpbuf, normalize_ntpath(wbuf), MAX_PATH)) <  0)\n+\t\treturn -1;\n+\treturn 0;\n+}\n+\n int mingw_lstat(const char *file_name, struct stat *buf)\n {\n \tWIN32_FILE_ATTRIBUTE_DATA fdata;\n-\tWIN32_FIND_DATAW findbuf = { 0 };\n+\tDWORD reparse_tag = 0;\n+\tint link_len = 0;\n \twchar_t wfilename[MAX_PATH];\n \tint wlen = xutftowcs_path(wfilename, file_name);\n \tif (wlen < 0)\n@@ -935,28 +1028,29 @@ int mingw_lstat(const char *file_name, struct stat *buf)\n \t}\n \n \tif (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {\n-\t\t/* for reparse points, use FindFirstFile to get the reparse tag */\n+\t\t/* for reparse points, get the link tag and length */\n \t\tif (fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {\n-\t\t\tHANDLE handle = FindFirstFileW(wfilename, &findbuf);\n-\t\t\tif (handle == INVALID_HANDLE_VALUE)\n-\t\t\t\tgoto error;\n-\t\t\tFindClose(handle);\n+\t\t\tchar tmpbuf[MAX_PATH];\n+\n+\t\t\tif (read_reparse_point(wfilename, FALSE, tmpbuf,\n+\t\t\t\t\t       &link_len, &reparse_tag) < 0)\n+\t\t\t\treturn -1;\n \t\t}\n \t\tbuf->st_ino = 0;\n \t\tbuf->st_gid = 0;\n \t\tbuf->st_uid = 0;\n \t\tbuf->st_nlink = 1;\n \t\tbuf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes,\n-\t\t\t\tfindbuf.dwReserved0);\n-\t\tbuf->st_size = fdata.nFileSizeLow |\n-\t\t\t(((off_t)fdata.nFileSizeHigh)<<32);\n+\t\t\t\treparse_tag);\n+\t\tbuf->st_size = S_ISLNK(buf->st_mode) ? link_len :\n+\t\t\tfdata.nFileSizeLow | (((off_t) fdata.nFileSizeHigh) << 32);\n \t\tbuf->st_dev = buf->st_rdev = 0; /* not used by Git */\n \t\tfiletime_to_timespec(&(fdata.ftLastAccessTime), &(buf->st_atim));\n \t\tfiletime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim));\n \t\tfiletime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim));\n \t\treturn 0;\n \t}\n-error:\n+\n \tswitch (GetLastError()) {\n \tcase ERROR_ACCESS_DENIED:\n \tcase ERROR_SHARING_VIOLATION:\n-- \ngitgitgadget\n\n"},{"id":"533418","messageId":"4aeccd6656a106e537bad73c0f1c8c8c2c34992a.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 07/18] mingw: factor out the retry logic","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:04Z","receivedAt":"2026-01-09T20:05:27Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nIn several places, Git's Windows-specific code follows the pattern where\nit tries to perform an operation, and retries several times when that\noperation fails, sleeping an increasing amount of time, before finally\ngiving up and asking the user whether to rety (after, say, closing an\neditor that held a handle to a file, preventing the operation from\nsucceeding).\n\nThis logic is a bit hard to use, and inconsistent:\n`mingw_unlink()` and `mingw_rmdir()` duplicate the code to retry,\nand both of them do so incompletely. They also do not restore `errno` if the\nuser answers 'no'.\n\nIntroduce a `retry_ask_yes_no()` helper function that handles retry with\nsmall delay, asking the user, and restoring `errno`.\n\nNote that in `mingw_unlink()`, we include the `_wchmod()` call in the\nretry loop (which may fail if the file is locked exclusively).\n\nIn `mingw_rmdir()`, we include special error handling in the retry loop.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 104 ++++++++++++++++++++++---------------------------\n 1 file changed, 46 insertions(+), 58 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex c7571951dc..26e64c6a5a 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -28,8 +28,6 @@\n \n #define HCAST(type, handle) ((type)(intptr_t)handle)\n \n-static const int delay[] = { 0, 1, 10, 20, 40 };\n-\n void open_in_gdb(void)\n {\n \tstatic struct child_process cp = CHILD_PROCESS_INIT;\n@@ -205,15 +203,12 @@ static int read_yes_no_answer(void)\n \treturn -1;\n }\n \n-static int ask_yes_no_if_possible(const char *format, ...)\n+static int ask_yes_no_if_possible(const char *format, va_list args)\n {\n \tchar question[4096];\n \tconst char *retry_hook;\n-\tva_list args;\n \n-\tva_start(args, format);\n \tvsnprintf(question, sizeof(question), format, args);\n-\tva_end(args);\n \n \tretry_hook = mingw_getenv(\"GIT_ASK_YESNO\");\n \tif (retry_hook) {\n@@ -238,6 +233,31 @@ static int ask_yes_no_if_possible(const char *format, ...)\n \t}\n }\n \n+static int retry_ask_yes_no(int *tries, const char *format, ...)\n+{\n+\tstatic const int delay[] = { 0, 1, 10, 20, 40 };\n+\tva_list args;\n+\tint result, saved_errno = errno;\n+\n+\tif ((*tries) < ARRAY_SIZE(delay)) {\n+\t\t/*\n+\t\t * We assume that some other process had the file open at the wrong\n+\t\t * moment and retry. In order to give the other process a higher\n+\t\t * chance to complete its operation, we give up our time slice now.\n+\t\t * If we have to retry again, we do sleep a bit.\n+\t\t */\n+\t\tSleep(delay[*tries]);\n+\t\t(*tries)++;\n+\t\treturn 1;\n+\t}\n+\n+\tva_start(args, format);\n+\tresult = ask_yes_no_if_possible(format, args);\n+\tva_end(args);\n+\terrno = saved_errno;\n+\treturn result;\n+}\n+\n /* Windows only */\n enum hide_dotfiles_type {\n \tHIDE_DOTFILES_FALSE = 0,\n@@ -298,7 +318,7 @@ static wchar_t *normalize_ntpath(wchar_t *wbuf)\n \n int mingw_unlink(const char *pathname, int handle_in_use_error)\n {\n-\tint ret, tries = 0;\n+\tint tries = 0;\n \twchar_t wpathname[MAX_PATH];\n \tif (xutftowcs_path(wpathname, pathname) < 0)\n \t\treturn -1;\n@@ -306,29 +326,19 @@ int mingw_unlink(const char *pathname, int handle_in_use_error)\n \tif (DeleteFileW(wpathname))\n \t\treturn 0;\n \n-\t/* read-only files cannot be removed */\n-\t_wchmod(wpathname, 0666);\n-\twhile ((ret = _wunlink(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {\n+\tdo {\n+\t\t/* read-only files cannot be removed */\n+\t\t_wchmod(wpathname, 0666);\n+\t\tif (!_wunlink(wpathname))\n+\t\t\treturn 0;\n \t\tif (!is_file_in_use_error(GetLastError()))\n \t\t\tbreak;\n \t\tif (!handle_in_use_error)\n-\t\t\treturn ret;\n+\t\t\treturn -1;\n \n-\t\t/*\n-\t\t * We assume that some other process had the source or\n-\t\t * destination file open at the wrong moment and retry.\n-\t\t * In order to give the other process a higher chance to\n-\t\t * complete its operation, we give up our time slice now.\n-\t\t * If we have to retry again, we do sleep a bit.\n-\t\t */\n-\t\tSleep(delay[tries]);\n-\t\ttries++;\n-\t}\n-\twhile (ret == -1 && is_file_in_use_error(GetLastError()) &&\n-\t       ask_yes_no_if_possible(\"Unlink of file '%s' failed. \"\n-\t\t\t\"Should I try again?\", pathname))\n-\t       ret = _wunlink(wpathname);\n-\treturn ret;\n+\t} while (retry_ask_yes_no(&tries, \"Unlink of file '%s' failed. \"\n+\t\t\t\"Should I try again?\", pathname));\n+\treturn -1;\n }\n \n static int is_dir_empty(const wchar_t *wpath)\n@@ -355,7 +365,7 @@ static int is_dir_empty(const wchar_t *wpath)\n \n int mingw_rmdir(const char *pathname)\n {\n-\tint ret, tries = 0;\n+\tint tries = 0;\n \twchar_t wpathname[MAX_PATH];\n \tstruct stat st;\n \n@@ -381,7 +391,11 @@ int mingw_rmdir(const char *pathname)\n \tif (xutftowcs_path(wpathname, pathname) < 0)\n \t\treturn -1;\n \n-\twhile ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {\n+\tdo {\n+\t\tif (!_wrmdir(wpathname)) {\n+\t\t\tinvalidate_lstat_cache();\n+\t\t\treturn 0;\n+\t\t}\n \t\tif (!is_file_in_use_error(GetLastError()))\n \t\t\terrno = err_win_to_posix(GetLastError());\n \t\tif (errno != EACCES)\n@@ -390,23 +404,9 @@ int mingw_rmdir(const char *pathname)\n \t\t\terrno = ENOTEMPTY;\n \t\t\tbreak;\n \t\t}\n-\t\t/*\n-\t\t * We assume that some other process had the source or\n-\t\t * destination file open at the wrong moment and retry.\n-\t\t * In order to give the other process a higher chance to\n-\t\t * complete its operation, we give up our time slice now.\n-\t\t * If we have to retry again, we do sleep a bit.\n-\t\t */\n-\t\tSleep(delay[tries]);\n-\t\ttries++;\n-\t}\n-\twhile (ret == -1 && errno == EACCES && is_file_in_use_error(GetLastError()) &&\n-\t       ask_yes_no_if_possible(\"Deletion of directory '%s' failed. \"\n-\t\t\t\"Should I try again?\", pathname))\n-\t       ret = _wrmdir(wpathname);\n-\tif (!ret)\n-\t\tinvalidate_lstat_cache();\n-\treturn ret;\n+\t} while (retry_ask_yes_no(&tries, \"Deletion of directory '%s' failed. \"\n+\t\t\t\"Should I try again?\", pathname));\n+\treturn -1;\n }\n \n static inline int needs_hiding(const char *path)\n@@ -2384,20 +2384,8 @@ repeat:\n \t\t\tSetFileAttributesW(wpnew, attrs);\n \t\t}\n \t}\n-\tif (tries < ARRAY_SIZE(delay) && gle == ERROR_ACCESS_DENIED) {\n-\t\t/*\n-\t\t * We assume that some other process had the source or\n-\t\t * destination file open at the wrong moment and retry.\n-\t\t * In order to give the other process a higher chance to\n-\t\t * complete its operation, we give up our time slice now.\n-\t\t * If we have to retry again, we do sleep a bit.\n-\t\t */\n-\t\tSleep(delay[tries]);\n-\t\ttries++;\n-\t\tgoto repeat;\n-\t}\n \tif (gle == ERROR_ACCESS_DENIED &&\n-\t       ask_yes_no_if_possible(\"Rename from '%s' to '%s' failed. \"\n+\t       retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n \t\t       \"Should I try again?\", pold, pnew))\n \t\tgoto repeat;\n \n-- \ngitgitgadget\n\n"},{"id":"533421","messageId":"86c07427485f6c73904fe1b637fa262468a38125.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 08/18] mingw: change default of `core.symlinks` to false","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:05Z","receivedAt":"2026-01-09T20:05:28Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nSymlinks on Windows don't work the same way as on Unix systems. For\nexample, there are different types of symlinks for directories and\nfiles, and unless using a recent-ish Windows version in Developer Mode,\ncreating symlinks requires administrative privileges.\n\nBy default, disable symlink support on Windows. That is, users\nexplicitly have to enable it with `git config [--system|--global]\ncore.symlinks true`; For convenience, `git init` (and `git clone`)\nwill perform a test whether the current setup allows creating symlinks\nand will configure that setting in the repository config.\n\nThe test suite ignores system / global config files. Allow\ntesting *with* symlink support by checking if native symlinks are\nenabled in MSYS2 (via setting the special environment variable\n`MSYS=winsymlinks:nativestrict` to ask the MSYS2 runtime to enable\ncreating symlinks).\n\nNote: This assumes that Git's test suite is run in MSYS2's Bash, which\nis true for the time being (an experiment to switch to BusyBox-w32\nfailed due to the experimental nature of BusyBox-w32).\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 9 +++++++++\n 1 file changed, 9 insertions(+)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 26e64c6a5a..0fe00a5b70 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -2862,6 +2862,15 @@ static void setup_windows_environment(void)\n \t\tif (!tmp && (tmp = getenv(\"USERPROFILE\")))\n \t\t\tsetenv(\"HOME\", tmp, 1);\n \t}\n+\n+\t/*\n+\t * Change 'core.symlinks' default to false, unless native symlinks are\n+\t * enabled in MSys2 (via 'MSYS=winsymlinks:nativestrict'). Thus we can\n+\t * run the test suite (which doesn't obey config files) with or without\n+\t * symlink support.\n+\t */\n+\tif (!(tmp = getenv(\"MSYS\")) || !strstr(tmp, \"winsymlinks:nativestrict\"))\n+\t\thas_symlinks = 0;\n }\n \n static void get_current_user_sid(PSID *sid, HANDLE *linked_token)\n-- \ngitgitgadget\n\n"},{"id":"533419","messageId":"9a0093d34bb87e4e6d5731acf39265d5bbd19431.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 09/18] mingw: add symlink-specific error codes","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:06Z","receivedAt":"2026-01-09T20:05:29Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nThe Win32 API calls do not set `errno`; Instead, error codes for failed\noperations must be obtained via the `GetLastError()` function. Git would\nnot know what to do with those error values, though, which is why Git's\nWindows compatibility layer translates them to `errno` values.\n\nLet's handle a couple of symlink-related error codes that will become\nrelevant with the upcoming support for symlinks on Windows.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 5 +++++\n 1 file changed, 5 insertions(+)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 0fe00a5b70..0e8807196f 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -102,6 +102,7 @@ int err_win_to_posix(DWORD winerr)\n \tcase ERROR_INVALID_PARAMETER: error = EINVAL; break;\n \tcase ERROR_INVALID_PASSWORD: error = EPERM; break;\n \tcase ERROR_INVALID_PRIMARY_GROUP: error = EINVAL; break;\n+\tcase ERROR_INVALID_REPARSE_DATA: error = EINVAL; break;\n \tcase ERROR_INVALID_SIGNAL_NUMBER: error = EINVAL; break;\n \tcase ERROR_INVALID_TARGET_HANDLE: error = EIO; break;\n \tcase ERROR_INVALID_WORKSTATION: error = EACCES; break;\n@@ -116,6 +117,7 @@ int err_win_to_posix(DWORD winerr)\n \tcase ERROR_NEGATIVE_SEEK: error = ESPIPE; break;\n \tcase ERROR_NOACCESS: error = EFAULT; break;\n \tcase ERROR_NONE_MAPPED: error = EINVAL; break;\n+\tcase ERROR_NOT_A_REPARSE_POINT: error = EINVAL; break;\n \tcase ERROR_NOT_ENOUGH_MEMORY: error = ENOMEM; break;\n \tcase ERROR_NOT_READY: error = EAGAIN; break;\n \tcase ERROR_NOT_SAME_DEVICE: error = EXDEV; break;\n@@ -136,6 +138,9 @@ int err_win_to_posix(DWORD winerr)\n \tcase ERROR_PIPE_NOT_CONNECTED: error = EPIPE; break;\n \tcase ERROR_PRIVILEGE_NOT_HELD: error = EACCES; break;\n \tcase ERROR_READ_FAULT: error = EIO; break;\n+\tcase ERROR_REPARSE_ATTRIBUTE_CONFLICT: error = EINVAL; break;\n+\tcase ERROR_REPARSE_TAG_INVALID: error = EINVAL; break;\n+\tcase ERROR_REPARSE_TAG_MISMATCH: error = EINVAL; break;\n \tcase ERROR_SEEK: error = EIO; break;\n \tcase ERROR_SEEK_ON_DEVICE: error = ESPIPE; break;\n \tcase ERROR_SHARING_BUFFER_EXCEEDED: error = ENFILE; break;\n-- \ngitgitgadget\n\n"},{"id":"533426","messageId":"3670a0a181f8c1198ffea6560ae81a4b4f949be2.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 11/18] mingw: support renaming symlinks","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:08Z","receivedAt":"2026-01-09T20:05:31Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nOlder MSVCRT's `_wrename()` function cannot rename symlinks over\nexisting files: it returns success without doing anything. Newer\nMSVCR*.dll versions probably do not share this problem: according to CRT\nsources, they just call `MoveFileEx()` with the `MOVEFILE_COPY_ALLOWED`\nflag.\n\nAvoid the `_wrename()` call, and go with directly calling\n`MoveFileEx()`, with proper error handling of course.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 38 ++++++++++++++++----------------------\n 1 file changed, 16 insertions(+), 22 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex b1cc30d0f1..55f0bb478e 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -2275,7 +2275,7 @@ int mingw_accept(int sockfd1, struct sockaddr *sa, socklen_t *sz)\n int mingw_rename(const char *pold, const char *pnew)\n {\n \tstatic int supports_file_rename_info_ex = 1;\n-\tDWORD attrs, gle;\n+\tDWORD attrs = INVALID_FILE_ATTRIBUTES, gle;\n \tint tries = 0;\n \twchar_t wpold[MAX_PATH], wpnew[MAX_PATH];\n \tint wpnew_len;\n@@ -2286,15 +2286,6 @@ int mingw_rename(const char *pold, const char *pnew)\n \tif (wpnew_len < 0)\n \t\treturn -1;\n \n-\t/*\n-\t * Try native rename() first to get errno right.\n-\t * It is based on MoveFile(), which cannot overwrite existing files.\n-\t */\n-\tif (!_wrename(wpold, wpnew))\n-\t\treturn 0;\n-\tif (errno != EEXIST)\n-\t\treturn -1;\n-\n repeat:\n \tif (supports_file_rename_info_ex) {\n \t\t/*\n@@ -2370,13 +2361,22 @@ repeat:\n \t\t * to retry.\n \t\t */\n \t} else {\n-\t\tif (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))\n+\t\tif (MoveFileExW(wpold, wpnew,\n+\t\t\t\tMOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED))\n \t\t\treturn 0;\n \t\tgle = GetLastError();\n \t}\n \n-\t/* TODO: translate more errors */\n-\tif (gle == ERROR_ACCESS_DENIED &&\n+\t/* revert file attributes on failure */\n+\tif (attrs != INVALID_FILE_ATTRIBUTES)\n+\t\tSetFileAttributesW(wpnew, attrs);\n+\n+\tif (!is_file_in_use_error(gle)) {\n+\t\terrno = err_win_to_posix(gle);\n+\t\treturn -1;\n+\t}\n+\n+\tif (attrs == INVALID_FILE_ATTRIBUTES &&\n \t    (attrs = GetFileAttributesW(wpnew)) != INVALID_FILE_ATTRIBUTES) {\n \t\tif (attrs & FILE_ATTRIBUTE_DIRECTORY) {\n \t\t\tDWORD attrsold = GetFileAttributesW(wpold);\n@@ -2388,16 +2388,10 @@ repeat:\n \t\t\treturn -1;\n \t\t}\n \t\tif ((attrs & FILE_ATTRIBUTE_READONLY) &&\n-\t\t    SetFileAttributesW(wpnew, attrs & ~FILE_ATTRIBUTE_READONLY)) {\n-\t\t\tif (MoveFileExW(wpold, wpnew, MOVEFILE_REPLACE_EXISTING))\n-\t\t\t\treturn 0;\n-\t\t\tgle = GetLastError();\n-\t\t\t/* revert file attributes on failure */\n-\t\t\tSetFileAttributesW(wpnew, attrs);\n-\t\t}\n+\t\t    SetFileAttributesW(wpnew, attrs & ~FILE_ATTRIBUTE_READONLY))\n+\t\t\tgoto repeat;\n \t}\n-\tif (gle == ERROR_ACCESS_DENIED &&\n-\t       retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n+\tif (retry_ask_yes_no(&tries, \"Rename from '%s' to '%s' failed. \"\n \t\t       \"Should I try again?\", pold, pnew))\n \t\tgoto repeat;\n \n-- \ngitgitgadget\n\n"},{"id":"533424","messageId":"5dc90f9785d8f83f39d79788a3f1b9ed8fa539dc.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 10/18] mingw: handle symlinks to directories in `mingw_unlink()`","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:07Z","receivedAt":"2026-01-09T20:05:32Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nThe `_wunlink()` and `DeleteFileW()` functions refuse to delete symlinks\nto directories on Windows; The error code would be `ERROR_ACCESS_DENIED`\nin that case. Take that error code as an indicator that we need to try\n`_wrmdir()` as well. In the best case, it will remove a symlink. In the\nworst case, it will fail with the same error code again.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 9 ++++++++-\n 1 file changed, 8 insertions(+), 1 deletion(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 0e8807196f..b1cc30d0f1 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -338,9 +338,16 @@ int mingw_unlink(const char *pathname, int handle_in_use_error)\n \t\t\treturn 0;\n \t\tif (!is_file_in_use_error(GetLastError()))\n \t\t\tbreak;\n+\t\t/*\n+\t\t * _wunlink() / DeleteFileW() for directory symlinks fails with\n+\t\t * ERROR_ACCESS_DENIED (EACCES), so try _wrmdir() as well. This is the\n+\t\t * same error we get if a file is in use (already checked above).\n+\t\t */\n+\t\tif (!_wrmdir(wpathname))\n+\t\t\treturn 0;\n+\n \t\tif (!handle_in_use_error)\n \t\t\treturn -1;\n-\n \t} while (retry_ask_yes_no(&tries, \"Unlink of file '%s' failed. \"\n \t\t\t\"Should I try again?\", pathname));\n \treturn -1;\n-- \ngitgitgadget\n\n"},{"id":"533425","messageId":"f79cf31dab8fab373978a382d71877803d89c8f7.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 12/18] mingw: allow `mingw_chdir()` to change to symlink-resolved directories","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:09Z","receivedAt":"2026-01-09T20:05:33Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nIf symlinks are enabled, resolve all symlinks when changing directories,\nas required by POSIX.\n\nNote: Git's `real_path()` function bases its link resolution algorithm\non this property of `chdir()`. Unfortunately, the current directory on\nWindows is limited to only MAX_PATH (260) characters. Therefore using\nsymlinks and long paths in combination may be problematic.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 20 +++++++++++++++++++-\n 1 file changed, 19 insertions(+), 1 deletion(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 55f0bb478e..5d2a8c247c 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -866,9 +866,27 @@ int mingw_access(const char *filename, int mode)\n int mingw_chdir(const char *dirname)\n {\n \twchar_t wdirname[MAX_PATH];\n+\n \tif (xutftowcs_path(wdirname, dirname) < 0)\n \t\treturn -1;\n-\treturn _wchdir(wdirname);\n+\n+\tif (has_symlinks) {\n+\t\tHANDLE hnd = CreateFileW(wdirname, 0,\n+\t\t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n+\t\t\t\tOPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);\n+\t\tif (hnd == INVALID_HANDLE_VALUE) {\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\t\treturn -1;\n+\t\t}\n+\t\tif (!GetFinalPathNameByHandleW(hnd, wdirname, ARRAY_SIZE(wdirname), 0)) {\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\t\tCloseHandle(hnd);\n+\t\t\treturn -1;\n+\t\t}\n+\t\tCloseHandle(hnd);\n+\t}\n+\n+\treturn _wchdir(normalize_ntpath(wdirname));\n }\n \n int mingw_chmod(const char *filename, int mode)\n-- \ngitgitgadget\n\n"},{"id":"533428","messageId":"1ae9c63a89e6b35d1658042685fdec0a572b375a.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 13/18] mingw: implement `readlink()`","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:10Z","receivedAt":"2026-01-09T20:05:34Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nImplement `readlink()` by reading NTFS reparse points via the\n`read_reparse_point()` function that was introduced earlier to determine\nthe length of symlink targets. Works for symlinks and directory\njunctions.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw-posix.h |  3 +--\n compat/mingw.c       | 24 ++++++++++++++++++++++++\n 2 files changed, 25 insertions(+), 2 deletions(-)\n\ndiff --git a/compat/mingw-posix.h b/compat/mingw-posix.h\nindex 0939feff27..896aa976b1 100644\n--- a/compat/mingw-posix.h\n+++ b/compat/mingw-posix.h\n@@ -121,8 +121,6 @@ struct utsname {\n  * trivial stubs\n  */\n \n-static inline int readlink(const char *path UNUSED, char *buf UNUSED, size_t bufsiz UNUSED)\n-{ errno = ENOSYS; return -1; }\n static inline int symlink(const char *oldpath UNUSED, const char *newpath UNUSED)\n { errno = ENOSYS; return -1; }\n static inline int fchmod(int fildes UNUSED, mode_t mode UNUSED)\n@@ -197,6 +195,7 @@ int setitimer(int type, struct itimerval *in, struct itimerval *out);\n int sigaction(int sig, struct sigaction *in, struct sigaction *out);\n int link(const char *oldpath, const char *newpath);\n int uname(struct utsname *buf);\n+int readlink(const char *path, char *buf, size_t bufsiz);\n \n /*\n  * replacements of existing functions\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 5d2a8c247c..b407a2ac07 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -2698,6 +2698,30 @@ int link(const char *oldpath, const char *newpath)\n \treturn 0;\n }\n \n+int readlink(const char *path, char *buf, size_t bufsiz)\n+{\n+\tWCHAR wpath[MAX_PATH];\n+\tchar tmpbuf[MAX_PATH];\n+\tint len;\n+\tDWORD tag;\n+\n+\tif (xutftowcs_path(wpath, path) < 0)\n+\t\treturn -1;\n+\n+\tif (read_reparse_point(wpath, TRUE, tmpbuf, &len, &tag) < 0)\n+\t\treturn -1;\n+\n+\t/*\n+\t * Adapt to strange readlink() API: Copy up to bufsiz *bytes*, potentially\n+\t * cutting off a UTF-8 sequence. Insufficient bufsize is *not* a failure\n+\t * condition. There is no conversion function that produces invalid UTF-8,\n+\t * so convert to a (hopefully large enough) temporary buffer, then memcpy\n+\t * the requested number of bytes (including '\\0' for robustness).\n+\t */\n+\tmemcpy(buf, tmpbuf, min(bufsiz, len + 1));\n+\treturn min(bufsiz, len);\n+}\n+\n pid_t waitpid(pid_t pid, int *status, int options)\n {\n \tHANDLE h = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION,\n-- \ngitgitgadget\n\n"},{"id":"533427","messageId":"4e0ac43ef6e04f645d501f8f46514c0dfc3f68a8.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 14/18] mingw: implement basic `symlink()` functionality (file symlinks only)","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:11Z","receivedAt":"2026-01-09T20:05:35Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nImplement `symlink()`. This implementation always creates _file_\nsymlinks (remember: Windows discerns between symlinks pointing to\ndirectories and those pointing to files). Support for directory symlinks\nwill be added in a subseqeuent commit.\n\nThis implementation fails with `ENOSYS` if symlinks are disabled or\nunsupported.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw-posix.h |  3 +--\n compat/mingw.c       | 28 ++++++++++++++++++++++++++++\n 2 files changed, 29 insertions(+), 2 deletions(-)\n\ndiff --git a/compat/mingw-posix.h b/compat/mingw-posix.h\nindex 896aa976b1..2d989fd762 100644\n--- a/compat/mingw-posix.h\n+++ b/compat/mingw-posix.h\n@@ -121,8 +121,6 @@ struct utsname {\n  * trivial stubs\n  */\n \n-static inline int symlink(const char *oldpath UNUSED, const char *newpath UNUSED)\n-{ errno = ENOSYS; return -1; }\n static inline int fchmod(int fildes UNUSED, mode_t mode UNUSED)\n { errno = ENOSYS; return -1; }\n #ifndef __MINGW64_VERSION_MAJOR\n@@ -195,6 +193,7 @@ int setitimer(int type, struct itimerval *in, struct itimerval *out);\n int sigaction(int sig, struct sigaction *in, struct sigaction *out);\n int link(const char *oldpath, const char *newpath);\n int uname(struct utsname *buf);\n+int symlink(const char *target, const char *link);\n int readlink(const char *path, char *buf, size_t bufsiz);\n \n /*\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex b407a2ac07..8d366794c4 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -2698,6 +2698,34 @@ int link(const char *oldpath, const char *newpath)\n \treturn 0;\n }\n \n+int symlink(const char *target, const char *link)\n+{\n+\twchar_t wtarget[MAX_PATH], wlink[MAX_PATH];\n+\tint len;\n+\n+\t/* fail if symlinks are disabled or API is not supported (WinXP) */\n+\tif (!has_symlinks) {\n+\t\terrno = ENOSYS;\n+\t\treturn -1;\n+\t}\n+\n+\tif ((len = xutftowcs_path(wtarget, target)) < 0\n+\t\t\t|| xutftowcs_path(wlink, link) < 0)\n+\t\treturn -1;\n+\n+\t/* convert target dir separators to backslashes */\n+\twhile (len--)\n+\t\tif (wtarget[len] == '/')\n+\t\t\twtarget[len] = '\\\\';\n+\n+\t/* create file symlink */\n+\tif (!CreateSymbolicLinkW(wlink, wtarget, 0)) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n+\n int readlink(const char *path, char *buf, size_t bufsiz)\n {\n \tWCHAR wpath[MAX_PATH];\n-- \ngitgitgadget\n\n"},{"id":"533431","messageId":"3d479fd47e68242b028e1bbfd0019dfb0ededac8.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 15/18] mingw: add support for symlinks to directories","fromName":"Karsten Blees via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:12Z","receivedAt":"2026-01-09T20:05:36Z","isPatch":true,"sender":{"key":"karsten.blees@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1111200?v=4"},"body":"From: Karsten Blees <karsten.blees@gmail.com>\n\nSymlinks on Windows have a flag that indicates whether the target is a\nfile or a directory. Symlinks of wrong type simply don't work. This even\naffects core Win32 APIs (e.g. `DeleteFile()` refuses to delete directory\nsymlinks).\n\nHowever, `CreateFile()` with FILE_FLAG_BACKUP_SEMANTICS does work. Check\nthe target type by first creating a tentative file symlink, opening it,\nand checking the type of the resulting handle. If it is a directory,\nrecreate the symlink with the directory flag set.\n\nIt is possible to create symlinks before the target exists (or in case\nof symlinks to symlinks: before the target type is known). If this\nhappens, create a tentative file symlink and postpone the directory\ndecision: keep a list of phantom symlinks to be processed whenever a new\ndirectory is created in `mingw_mkdir()`.\n\nLimitations: This algorithm may fail if a link target changes from file\nto directory or vice versa, or if the target directory is created in\nanother process. It's the best Git can do, though.\n\nSigned-off-by: Karsten Blees <karsten.blees@gmail.com>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 164 +++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 164 insertions(+)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 8d366794c4..59a32e454e 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -296,6 +296,131 @@ int mingw_core_config(const char *var, const char *value,\n \treturn 0;\n }\n \n+static inline int is_wdir_sep(wchar_t wchar)\n+{\n+\treturn wchar == L'/' || wchar == L'\\\\';\n+}\n+\n+static const wchar_t *make_relative_to(const wchar_t *path,\n+\t\t\t\t       const wchar_t *relative_to, wchar_t *out,\n+\t\t\t\t       size_t size)\n+{\n+\tsize_t i = wcslen(relative_to), len;\n+\n+\t/* Is `path` already absolute? */\n+\tif (is_wdir_sep(path[0]) ||\n+\t    (iswalpha(path[0]) && path[1] == L':' && is_wdir_sep(path[2])))\n+\t\treturn path;\n+\n+\twhile (i > 0 && !is_wdir_sep(relative_to[i - 1]))\n+\t\ti--;\n+\n+\t/* Is `relative_to` in the current directory? */\n+\tif (!i)\n+\t\treturn path;\n+\n+\tlen = wcslen(path);\n+\tif (i + len + 1 > size) {\n+\t\terror(\"Could not make '%ls' relative to '%ls' (too large)\",\n+\t\t      path, relative_to);\n+\t\treturn NULL;\n+\t}\n+\n+\tmemcpy(out, relative_to, i * sizeof(wchar_t));\n+\twcscpy(out + i, path);\n+\treturn out;\n+}\n+\n+enum phantom_symlink_result {\n+\tPHANTOM_SYMLINK_RETRY,\n+\tPHANTOM_SYMLINK_DONE,\n+\tPHANTOM_SYMLINK_DIRECTORY\n+};\n+\n+/*\n+ * Changes a file symlink to a directory symlink if the target exists and is a\n+ * directory.\n+ */\n+static enum phantom_symlink_result\n+process_phantom_symlink(const wchar_t *wtarget, const wchar_t *wlink)\n+{\n+\tHANDLE hnd;\n+\tBY_HANDLE_FILE_INFORMATION fdata;\n+\twchar_t relative[MAX_PATH];\n+\tconst wchar_t *rel;\n+\n+\t/* check that wlink is still a file symlink */\n+\tif ((GetFileAttributesW(wlink)\n+\t\t\t& (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))\n+\t\t\t!= FILE_ATTRIBUTE_REPARSE_POINT)\n+\t\treturn PHANTOM_SYMLINK_DONE;\n+\n+\t/* make it relative, if necessary */\n+\trel = make_relative_to(wtarget, wlink, relative, ARRAY_SIZE(relative));\n+\tif (!rel)\n+\t\treturn PHANTOM_SYMLINK_DONE;\n+\n+\t/* let Windows resolve the link by opening it */\n+\thnd = CreateFileW(rel, 0,\n+\t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n+\t\t\tOPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);\n+\tif (hnd == INVALID_HANDLE_VALUE) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn PHANTOM_SYMLINK_RETRY;\n+\t}\n+\n+\tif (!GetFileInformationByHandle(hnd, &fdata)) {\n+\t\terrno = err_win_to_posix(GetLastError());\n+\t\tCloseHandle(hnd);\n+\t\treturn PHANTOM_SYMLINK_RETRY;\n+\t}\n+\tCloseHandle(hnd);\n+\n+\t/* if target exists and is a file, we're done */\n+\tif (!(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))\n+\t\treturn PHANTOM_SYMLINK_DONE;\n+\n+\t/* otherwise recreate the symlink with directory flag */\n+\tif (DeleteFileW(wlink) && CreateSymbolicLinkW(wlink, wtarget, 1))\n+\t\treturn PHANTOM_SYMLINK_DIRECTORY;\n+\n+\terrno = err_win_to_posix(GetLastError());\n+\treturn PHANTOM_SYMLINK_RETRY;\n+}\n+\n+/* keep track of newly created symlinks to non-existing targets */\n+struct phantom_symlink_info {\n+\tstruct phantom_symlink_info *next;\n+\twchar_t *wlink;\n+\twchar_t *wtarget;\n+};\n+\n+static struct phantom_symlink_info *phantom_symlinks = NULL;\n+static CRITICAL_SECTION phantom_symlinks_cs;\n+\n+static void process_phantom_symlinks(void)\n+{\n+\tstruct phantom_symlink_info *current, **psi;\n+\tEnterCriticalSection(&phantom_symlinks_cs);\n+\t/* process phantom symlinks list */\n+\tpsi = &phantom_symlinks;\n+\twhile ((current = *psi)) {\n+\t\tenum phantom_symlink_result result = process_phantom_symlink(\n+\t\t\t\tcurrent->wtarget, current->wlink);\n+\t\tif (result == PHANTOM_SYMLINK_RETRY) {\n+\t\t\tpsi = &current->next;\n+\t\t} else {\n+\t\t\t/* symlink was processed, remove from list */\n+\t\t\t*psi = current->next;\n+\t\t\tfree(current);\n+\t\t\t/* if symlink was a directory, start over */\n+\t\t\tif (result == PHANTOM_SYMLINK_DIRECTORY)\n+\t\t\t\tpsi = &phantom_symlinks;\n+\t\t}\n+\t}\n+\tLeaveCriticalSection(&phantom_symlinks_cs);\n+}\n+\n /* Normalizes NT paths as returned by some low-level APIs. */\n static wchar_t *normalize_ntpath(wchar_t *wbuf)\n {\n@@ -479,6 +604,8 @@ int mingw_mkdir(const char *path, int mode UNUSED)\n \tif (xutftowcs_path(wpath, path) < 0)\n \t\treturn -1;\n \tret = _wmkdir(wpath);\n+\tif (!ret)\n+\t\tprocess_phantom_symlinks();\n \tif (!ret && needs_hiding(path))\n \t\treturn set_hidden_flag(wpath, 1);\n \treturn ret;\n@@ -2723,6 +2850,42 @@ int symlink(const char *target, const char *link)\n \t\terrno = err_win_to_posix(GetLastError());\n \t\treturn -1;\n \t}\n+\n+\t/* convert to directory symlink if target exists */\n+\tswitch (process_phantom_symlink(wtarget, wlink)) {\n+\tcase PHANTOM_SYMLINK_RETRY:\t{\n+\t\t/* if target doesn't exist, add to phantom symlinks list */\n+\t\twchar_t wfullpath[MAX_PATH];\n+\t\tstruct phantom_symlink_info *psi;\n+\n+\t\t/* convert to absolute path to be independent of cwd */\n+\t\tlen = GetFullPathNameW(wlink, MAX_PATH, wfullpath, NULL);\n+\t\tif (!len || len >= MAX_PATH) {\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\t/* over-allocate and fill phantom_symlink_info structure */\n+\t\tpsi = xmalloc(sizeof(struct phantom_symlink_info)\n+\t\t\t+ sizeof(wchar_t) * (len + wcslen(wtarget) + 2));\n+\t\tpsi->wlink = (wchar_t *)(psi + 1);\n+\t\twcscpy(psi->wlink, wfullpath);\n+\t\tpsi->wtarget = psi->wlink + len + 1;\n+\t\twcscpy(psi->wtarget, wtarget);\n+\n+\t\tEnterCriticalSection(&phantom_symlinks_cs);\n+\t\tpsi->next = phantom_symlinks;\n+\t\tphantom_symlinks = psi;\n+\t\tLeaveCriticalSection(&phantom_symlinks_cs);\n+\t\tbreak;\n+\t}\n+\tcase PHANTOM_SYMLINK_DIRECTORY:\n+\t\t/* if we created a dir symlink, process other phantom symlinks */\n+\t\tprocess_phantom_symlinks();\n+\t\tbreak;\n+\tdefault:\n+\t\tbreak;\n+\t}\n \treturn 0;\n }\n \n@@ -3424,6 +3587,7 @@ int wmain(int argc, const wchar_t **wargv)\n \n \t/* initialize critical section for waitpid pinfo_t list */\n \tInitializeCriticalSection(&pinfo_cs);\n+\tInitializeCriticalSection(&phantom_symlinks_cs);\n \n \t/* set up default file mode and file modes for stdin/out/err */\n \t_fmode = _O_BINARY;\n-- \ngitgitgadget\n\n"},{"id":"533429","messageId":"fb6aa461da7c60db07950d19a453cc75d8ddefcd.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 16/18] mingw: try to create symlinks without elevated permissions","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:13Z","receivedAt":"2026-01-09T20:05:37Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nAs of Windows 10 Build 14972 in Developer Mode, a new flag is supported\nby `CreateSymbolicLink()` to create symbolic links even when running\noutside of an elevated session (which was previously required).\n\nThis new flag is called `SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE`\nand has the numeric value 0x02.\n\nPrevious Windows 10 versions will not understand that flag and return\nan `ERROR_INVALID_PARAMETER`, therefore we have to be careful to try\npassing that flag only when the build number indicates that it is\nsupported.\n\nFor more information about the new flag, see this blog post:\nhttps://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 26 ++++++++++++++++++++++++--\n 1 file changed, 24 insertions(+), 2 deletions(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 59a32e454e..3e2110a87a 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -331,6 +331,8 @@ static const wchar_t *make_relative_to(const wchar_t *path,\n \treturn out;\n }\n \n+static DWORD symlink_file_flags = 0, symlink_directory_flags = 1;\n+\n enum phantom_symlink_result {\n \tPHANTOM_SYMLINK_RETRY,\n \tPHANTOM_SYMLINK_DONE,\n@@ -381,7 +383,8 @@ process_phantom_symlink(const wchar_t *wtarget, const wchar_t *wlink)\n \t\treturn PHANTOM_SYMLINK_DONE;\n \n \t/* otherwise recreate the symlink with directory flag */\n-\tif (DeleteFileW(wlink) && CreateSymbolicLinkW(wlink, wtarget, 1))\n+\tif (DeleteFileW(wlink) &&\n+\t    CreateSymbolicLinkW(wlink, wtarget, symlink_directory_flags))\n \t\treturn PHANTOM_SYMLINK_DIRECTORY;\n \n \terrno = err_win_to_posix(GetLastError());\n@@ -2846,7 +2849,7 @@ int symlink(const char *target, const char *link)\n \t\t\twtarget[len] = '\\\\';\n \n \t/* create file symlink */\n-\tif (!CreateSymbolicLinkW(wlink, wtarget, 0)) {\n+\tif (!CreateSymbolicLinkW(wlink, wtarget, symlink_file_flags)) {\n \t\terrno = err_win_to_posix(GetLastError());\n \t\treturn -1;\n \t}\n@@ -3523,6 +3526,24 @@ static void maybe_redirect_std_handles(void)\n \t\t\t\t  GENERIC_WRITE, FILE_FLAG_NO_BUFFERING);\n }\n \n+static void adjust_symlink_flags(void)\n+{\n+\t/*\n+\t * Starting with Windows 10 Build 14972, symbolic links can be created\n+\t * using CreateSymbolicLink() without elevation by passing the flag\n+\t * SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE (0x02) as last\n+\t * parameter, provided the Developer Mode has been enabled. Some\n+\t * earlier Windows versions complain about this flag with an\n+\t * ERROR_INVALID_PARAMETER, hence we have to test the build number\n+\t * specifically.\n+\t */\n+\tif (GetVersion() >= 14972 << 16) {\n+\t\tsymlink_file_flags |= 2;\n+\t\tsymlink_directory_flags |= 2;\n+\t}\n+\n+}\n+\n #ifdef _MSC_VER\n #ifdef _DEBUG\n #include <crtdbg.h>\n@@ -3558,6 +3579,7 @@ int wmain(int argc, const wchar_t **wargv)\n #endif\n \n \tmaybe_redirect_std_handles();\n+\tadjust_symlink_flags();\n \n \t/* determine size of argv and environ conversion buffer */\n \tmaxlen = wcslen(wargv[0]);\n-- \ngitgitgadget\n\n"},{"id":"533430","messageId":"40c3f7f36e945c49cb8c86a431cdcca2d728ec82.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 17/18] mingw: emulate `stat()` a little more faithfully","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:14Z","receivedAt":"2026-01-09T20:05:38Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen creating directories via `safe_create_leading_directories()`, we\nmight encounter an already-existing directory which is not\nreadable by the current user. To handle that situation, Git's code calls\n`stat()` to determine whether we're looking at a directory.\n\nIn such a case, `CreateFile()` will fail, though, no matter what, and\nconsequently `mingw_stat()` will fail, too. But POSIX semantics seem to\nstill allow `stat()` to go forward.\n\nSo let's call `mingw_lstat()` to the rescue if we fail to get a file\nhandle due to denied permission in `mingw_stat()`, and fill the stat\ninfo that way.\n\nWe need to be careful to not allow this to go forward in case that we're\nlooking at a symbolic link: to resolve the link, we would still have to\ncreate a file handle, and we just found out that we cannot. Therefore,\n`stat()` still needs to fail with `EACCES` in that case.\n\nThis fixes https://github.com/git-for-windows/git/issues/2531.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n compat/mingw.c | 14 +++++++++++++-\n 1 file changed, 13 insertions(+), 1 deletion(-)\n\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 3e2110a87a..628a3941d2 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -1273,7 +1273,19 @@ int mingw_stat(const char *file_name, struct stat *buf)\n \t\t\tFILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,\n \t\t\tOPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);\n \tif (hnd == INVALID_HANDLE_VALUE) {\n-\t\terrno = err_win_to_posix(GetLastError());\n+\t\tDWORD err = GetLastError();\n+\n+\t\tif (err == ERROR_ACCESS_DENIED &&\n+\t\t    !mingw_lstat(file_name, buf) &&\n+\t\t    !S_ISLNK(buf->st_mode))\n+\t\t\t/*\n+\t\t\t * POSIX semantics state to still try to fill\n+\t\t\t * information, even if permission is denied to create\n+\t\t\t * a file handle.\n+\t\t\t */\n+\t\t\treturn 0;\n+\n+\t\terrno = err_win_to_posix(err);\n \t\treturn -1;\n \t}\n \tresult = get_file_info_by_handle(hnd, buf);\n-- \ngitgitgadget\n\n"},{"id":"533432","messageId":"afcf2bbfcb83c542ff8e6d6d2ceef15d16e25be7.1767989115.git.gitgitgadget@gmail.com","threadId":"64639","inReplyTo":"pull.2018.v2.git.1767989115.gitgitgadget@gmail.com","subject":"[PATCH v2 18/18] mingw: special-case index entries for symlinks with buggy size","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-09T20:05:15Z","receivedAt":"2026-01-09T20:05:39Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIn https://github.com/git-for-windows/git/pull/2637, we fixed a bug\nwhere symbolic links' target path sizes were recorded incorrectly in the\nindex. The downside of this fix was that every user with tracked\nsymbolic links in their checkouts would see them as modified in `git\nstatus`, but not in `git diff`, and only a `git add <path>` (or `git add\n-u`) would \"fix\" this.\n\nLet's do better than that: we can detect that situation and simply\npretend that a symbolic link with a known bad size (or a size that just\nhappens to be that bad size, a _very_ unlikely scenario because it would\noverflow our buffers due to the trailing NUL byte) means that it needs\nto be re-checked as if we had just checked it out.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n read-cache.c | 11 +++++++++++\n 1 file changed, 11 insertions(+)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex 990d4ead0d..260f4b3b2f 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -470,6 +470,17 @@ int ie_modified(struct index_state *istate,\n \t * then we know it is.\n \t */\n \tif ((changed & DATA_CHANGED) &&\n+#ifdef GIT_WINDOWS_NATIVE\n+\t    /*\n+\t     * Work around Git for Windows v2.27.0 fixing a bug where symlinks'\n+\t     * target path lengths were not read at all, and instead recorded\n+\t     * as 4096: now, all symlinks would appear as modified.\n+\t     *\n+\t     * So let's just special-case symlinks with a target path length\n+\t     * (i.e. `sd_size`) of 4096 and force them to be re-checked.\n+\t     */\n+\t    (!S_ISLNK(st->st_mode) || ce->ce_stat_data.sd_size != MAX_PATH) &&\n+#endif\n \t    (S_ISGITLINK(ce->ce_mode) || ce->ce_stat_data.sd_size != 0))\n \t\treturn changed;\n \n-- \ngitgitgadget\n"}]}