Commit Diff


commit - aa67e876cf7e27d67a6f9baf186bf2b2a70cad4e
commit + a3704312cc36f6bf5e9cbf89b37f62973f1d9c06
blob - 0afceda1a36907e148b0bb4841a8c8d6bc6b4fa8
blob + da39847fd9a87b55175e2b7890d58abed6abe2b3
--- dulwich/client.py
+++ dulwich/client.py
@@ -57,7 +57,9 @@ from urllib.parse import (
     urlunsplit,
     urlunparse,
 )
+from urllib.request import url2pathname
 
+
 import dulwich
 from dulwich.config import get_xdg_config_home_path
 from dulwich.errors import (
@@ -2236,6 +2238,33 @@ class Urllib3HttpGitClient(AbstractHttpGitClient):
 HttpGitClient = Urllib3HttpGitClient
 
 
+def _win32_url_to_path(parsed) -> str:
+    """
+    Convert a file: URL to a path.
+
+    https://datatracker.ietf.org/doc/html/rfc8089
+    """
+    assert sys.platform == "win32" or os.name == "nt"
+    assert parsed.scheme == "file"
+
+    _, netloc, path, _, _, _ = parsed
+
+    if netloc == "localhost" or not netloc:
+        netloc = ""
+    elif (
+        netloc
+        and len(netloc) >= 2
+        and netloc[0].isalpha()
+        and netloc[1:2] in (":", ":/")
+    ):
+        # file://C:/foo.bar/baz or file://C://foo.bar//baz
+        netloc = netloc[:2]
+    else:
+        raise NotImplementedError("Non-local file URLs are not supported")
+
+    return url2pathname(netloc + path)
+
+
 def get_transport_and_path_from_url(url, config=None, **kwargs):
     """Obtain a git client from a URL.
 
@@ -2261,6 +2290,8 @@ def get_transport_and_path_from_url(url, config=None, 
             parsed.path,
         )
     elif parsed.scheme == "file":
+        if sys.platform == "win32" or os.name == "nt":
+            return default_local_git_client_cls(**kwargs), _win32_url_to_path(parsed)
         return (
             default_local_git_client_cls.from_parsedurl(parsed, **kwargs),
             parsed.path,
blob - 9937d034e4255379f79e52cb269ba287f22e5c30
blob + 12f62d15e3ef2f408dc60a906e06e474720ff45a
--- dulwich/tests/test_client.py
+++ dulwich/tests/test_client.py
@@ -30,6 +30,8 @@ from urllib.parse import (
     quote as urlquote,
     urlparse,
 )
+
+from unittest.mock import patch
 
 import dulwich
 from dulwich import (
@@ -682,12 +684,42 @@ class TestGetTransportAndPathFromUrl(TestCase):
         self.assertIsInstance(c, HttpGitClient)
         self.assertEqual("/jelmer/dulwich", path)
 
+    @patch("os.name", "posix")
+    @patch("sys.platform", "linux")
     def test_file(self):
         c, path = get_transport_and_path_from_url("file:///home/jelmer/foo")
         self.assertIsInstance(c, LocalGitClient)
         self.assertEqual("/home/jelmer/foo", path)
+
+    @patch("os.name", "nt")
+    @patch("sys.platform", "win32")
+    def test_file_win(self):
+        # `_win32_url_to_path` uses urllib.request.url2pathname, which is set to
+        # `ntutl2path.url2pathname`  when `os.name==nt`
+        from nturl2path import url2pathname
+
+        with patch("dulwich.client.url2pathname", url2pathname):
+            expected = "C:\\foo.bar\\baz"
+            for file_url in [
+                "file:C:/foo.bar/baz",
+                "file:/C:/foo.bar/baz",
+                "file://C:/foo.bar/baz",
+                "file://C://foo.bar//baz",
+                "file:///C:/foo.bar/baz",
+            ]:
+                c, path = get_transport_and_path(file_url)
+                self.assertIsInstance(c, LocalGitClient)
+                self.assertEqual(path, expected)
 
+            for remote_url in [
+                "file://host.example.com/C:/foo.bar/baz"
+                "file://host.example.com/C:/foo.bar/baz"
+                "file:////host.example/foo.bar/baz",
+            ]:
+                with self.assertRaises(NotImplementedError):
+                    c, path = get_transport_and_path(remote_url)
 
+
 class TestSSHVendor(object):
     def __init__(self):
         self.host = None