win32: correctly unlink symlinks to directories When deleting a symlink on Windows, then the way to delete it depends on whether it is a directory symlink or a file symlink. In the first case, we need to use `DeleteFile`, in the second `RemoveDirectory`. Right now, `p_unlink` will only ever try to use `DeleteFile`, though, and thus fail to remove directory symlinks. This mismatches how unlink(3P) is expected to behave, though, as it shall remove any symlink disregarding whether it is a file or directory symlink. In order to correctly unlink a symlink, we thus need to check what kind of file this is. If we were to first query file attributes of every file upon calling `p_unlink`, then this would penalize the common case though. Instead, we can try to first delete the file with `DeleteFile` and only if the error returned is `ERROR_ACCESS_DENIED` will we query file attributes and determine whether it is a directory symlink to use `RemoveDirectory` instead.
diff --git a/src/win32/posix_w32.c b/src/win32/posix_w32.c
index 2d1fa90..4de3217 100644
--- a/src/win32/posix_w32.c
+++ b/src/win32/posix_w32.c
@@ -251,9 +251,25 @@ int p_link(const char *old, const char *new)
GIT_INLINE(int) unlink_once(const wchar_t *path)
{
+ DWORD error;
+
if (DeleteFileW(path))
return 0;
+ if ((error = GetLastError()) == ERROR_ACCESS_DENIED) {
+ WIN32_FILE_ATTRIBUTE_DATA fdata;
+ if (!GetFileAttributesExW(path, GetFileExInfoStandard, &fdata) ||
+ !(fdata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) ||
+ !(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
+ goto out;
+
+ if (RemoveDirectoryW(path))
+ return 0;
+ }
+
+out:
+ SetLastError(error);
+
if (last_error_retryable())
return GIT_RETRY;