浏览代码

Fix porcelain.add() to handle symlinks pointing outside repository

Previously, porcelain.add() would fail when trying to add a symlink that
points outside the repository because path.resolve() would fully resolve
the symlink, causing a ValueError when the resolved path was outside the
repo boundaries.

This fix changes the path resolution logic to only resolve the parent
directory for symlinks, keeping the symlink itself unresolved. This
matches Git's behavior which allows adding symlinks regardless of where
they point.

Fixes #789
Jelmer Vernooij 2 月之前
父节点
当前提交
d659270286
共有 3 个文件被更改,包括 40 次插入1 次删除
  1. 6 0
      NEWS
  2. 17 1
      dulwich/porcelain.py
  3. 17 0
      tests/test_porcelain.py

+ 6 - 0
NEWS

@@ -1,5 +1,11 @@
 0.22.9	UNRELEASED
 
+ * Fix ``porcelain.add()`` symlink handling to allow adding symlinks that point
+   outside the repository. Previously, the function would fail when trying to
+   add a symlink pointing outside the repo due to aggressive path resolution.
+   Now only resolves the parent directory for symlinks, matching Git's behavior.
+   (Jelmer Vernooij, #789)
+
  * Fix gitignore pattern matching for directory negation patterns. Patterns like
    ``!data/*/`` now correctly unignore direct subdirectories while still ignoring
    files in the parent directory, matching Git's behavior. The ``is_ignored()`` method

+ 17 - 1
dulwich/porcelain.py

@@ -619,7 +619,23 @@ def add(repo=".", paths=None):
             if not path.is_absolute():
                 # Make relative paths relative to the repo directory
                 path = repo_path / path
-            relpath = str(path.resolve().relative_to(repo_path))
+
+            try:
+                # Don't resolve symlinks completely - only resolve the parent directory
+                # to avoid issues when symlinks point outside the repository
+                if path.is_symlink():
+                    # For symlinks, resolve only the parent directory
+                    parent_resolved = path.parent.resolve()
+                    resolved_path = parent_resolved / path.name
+                else:
+                    # For regular files/dirs, resolve normally
+                    resolved_path = path.resolve()
+
+                relpath = str(resolved_path.relative_to(repo_path))
+            except ValueError:
+                # Path is not within the repository
+                raise ValueError(f"Path {p} is not within repository {repo_path}")
+
             # FIXME: Support patterns
             if path.is_dir():
                 relpath = os.path.join(relpath, "")

+ 17 - 0
tests/test_porcelain.py

@@ -1027,6 +1027,23 @@ class AddTests(PorcelainTestCase):
         blob = self.repo[entry.sha]
         self.assertEqual(blob.data, b"line1\nline2")
 
+    def test_add_symlink_outside_repo(self) -> None:
+        """Test adding a symlink that points outside the repository."""
+        # Create a symlink pointing outside the repository
+        symlink_path = os.path.join(self.repo.path, "symlink_to_nowhere")
+        os.symlink("/nonexistent/path", symlink_path)
+
+        # Adding the symlink should succeed (matching Git's behavior)
+        added, ignored = porcelain.add(self.repo.path, paths=[symlink_path])
+
+        # Should successfully add the symlink
+        self.assertIn("symlink_to_nowhere", added)
+        self.assertEqual(len(ignored), 0)
+
+        # Verify symlink is actually staged
+        index = self.repo.open_index()
+        self.assertIn(b"symlink_to_nowhere", index)
+
 
 class RemoveTests(PorcelainTestCase):
     def test_remove_file(self) -> None: