Commit a00842c40a808d193e72bbd7bab1d8030b564447

Patrick Steinhardt 2019-06-29T09:59:14

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;