Explorar el Código

Fix Windows path handling regression in checkout operations

Fix a regression introduced in 0.23.1 where checkout operations on Windows
would fail with "Cannot create directory, parent path is a file: b'C:'" error.

The issue occurred in _ensure_parent_dir_exists when validating parent paths.
The original implementation manually split paths and reconstructed them, which
didn't correctly handle Windows drive letters or UNC paths.

The fix uses os.path.dirname() to properly walk up the directory tree, letting
Python's built-in path handling deal with all platform-specific cases including:
- Windows drive letters (C:\, D:\, etc.)
- UNC paths (\\server\share\...)
- Unix paths (/home/user/...)

This approach is more robust and eliminates the need for platform-specific
special cases.

Fixes #1751
Jelmer Vernooij hace 5 meses
padre
commit
5a176bf6af
Se han modificado 3 ficheros con 73 adiciones y 11 borrados
  1. 3 0
      NEWS
  2. 23 11
      dulwich/index.py
  3. 47 0
      tests/test_index.py

+ 3 - 0
NEWS

@@ -12,6 +12,9 @@
  * Add colorized diff support for the ``show`` command with ``--color``
    argument. (Jelmer Vernooij, #1741)
 
+ * Fix Windows path handling in ``_ensure_parent_dir_exists`` to correctly
+   handle drive letters during checkout operations. (Jelmer Vernooij, #1751)
+
 0.24.1	2025-08-01
 
  * Require ``typing_extensions`` on Python 3.10.

+ 23 - 11
dulwich/index.py

@@ -1595,19 +1595,31 @@ def _ensure_parent_dir_exists(full_path: bytes) -> None:
     """Ensure parent directory exists, checking no parent is a file."""
     parent_dir = os.path.dirname(full_path)
     if parent_dir and not os.path.exists(parent_dir):
-        # Check if any parent in the path is a file
-        parts = parent_dir.split(os_sep_bytes)
-        for i in range(len(parts)):
-            partial_path = os_sep_bytes.join(parts[: i + 1])
-            if (
-                partial_path
-                and os.path.exists(partial_path)
-                and not os.path.isdir(partial_path)
-            ):
-                # Parent path is a file, this is an error
+        # Walk up the directory tree to find the first existing parent
+        current = parent_dir
+        parents_to_check: list[bytes] = []
+
+        while current and not os.path.exists(current):
+            parents_to_check.insert(0, current)
+            new_parent = os.path.dirname(current)
+            if new_parent == current:
+                # Reached the root or can't go up further
+                break
+            current = new_parent
+
+        # Check if the existing parent (if any) is a directory
+        if current and os.path.exists(current) and not os.path.isdir(current):
+            raise OSError(
+                f"Cannot create directory, parent path is a file: {current!r}"
+            )
+
+        # Now check each parent we need to create isn't blocked by an existing file
+        for parent_path in parents_to_check:
+            if os.path.exists(parent_path) and not os.path.isdir(parent_path):
                 raise OSError(
-                    f"Cannot create directory, parent path is a file: {partial_path!r}"
+                    f"Cannot create directory, parent path is a file: {parent_path!r}"
                 )
+
         os.makedirs(parent_dir)
 
 

+ 47 - 0
tests/test_index.py

@@ -2908,3 +2908,50 @@ class TestUpdateWorkingTree(TestCase):
 
         # file2 should still be a directory
         self.assertTrue(os.path.isdir(file2_path))
+
+    def test_ensure_parent_dir_exists_windows_drive(self):
+        """Test that _ensure_parent_dir_exists handles Windows drive letters correctly."""
+        from dulwich.index import _ensure_parent_dir_exists
+
+        # Create a temporary directory to work with
+        with tempfile.TemporaryDirectory() as tmpdir:
+            # Test normal case (creates directory)
+            test_path = os.path.join(tmpdir, "subdir", "file.txt").encode()
+            _ensure_parent_dir_exists(test_path)
+            self.assertTrue(os.path.exists(os.path.dirname(test_path)))
+
+            # Test when parent is a file (should raise error)
+            file_path = os.path.join(tmpdir, "testfile").encode()
+            with open(file_path, "wb") as f:
+                f.write(b"test")
+
+            invalid_path = os.path.join(
+                tmpdir.encode(), b"testfile", b"subdir", b"file.txt"
+            )
+            with self.assertRaisesRegex(
+                OSError, "Cannot create directory, parent path is a file"
+            ):
+                _ensure_parent_dir_exists(invalid_path)
+
+            # Test with nested subdirectories
+            nested_path = os.path.join(tmpdir, "a", "b", "c", "d", "file.txt").encode()
+            _ensure_parent_dir_exists(nested_path)
+            self.assertTrue(os.path.exists(os.path.dirname(nested_path)))
+
+            # Test that various path formats are handled correctly by os.path.dirname
+            # This includes Windows drive letters, UNC paths, etc.
+            # The key is that we're using os.path.dirname which handles these correctly
+            import platform
+
+            if platform.system() == "Windows":
+                # Test Windows-specific paths only on Windows
+                test_cases = [
+                    b"C:\\temp\\test\\file.txt",
+                    b"D:\\file.txt",
+                    b"\\\\server\\share\\folder\\file.txt",
+                ]
+                for path in test_cases:
+                    # Just verify os.path.dirname handles these without errors
+                    parent = os.path.dirname(path)
+                    # We're not creating these directories, just testing the logic doesn't fail
+                    self.assertIsInstance(parent, bytes)