Sfoglia il codice sorgente

Add GitAttributes modification and serialization methods

Jelmer Vernooij 1 mese fa
parent
commit
33e3136833
2 ha cambiato i file con 266 aggiunte e 0 eliminazioni
  1. 86 0
      dulwich/attrs.py
  2. 180 0
      tests/test_attrs.py

+ 86 - 0
dulwich/attrs.py

@@ -334,3 +334,89 @@ class GitAttributes:
         """
         patterns = read_gitattributes(path)
         return cls(patterns)
+
+    def set_attribute(self, pattern: bytes, name: bytes, value: AttributeValue) -> None:
+        """Set an attribute for a pattern.
+
+        Args:
+            pattern: The file pattern
+            name: Attribute name
+            value: Attribute value (bytes, True, False, or None)
+        """
+        # Find existing pattern
+        pattern_obj = None
+        attrs_dict: Optional[dict[bytes, AttributeValue]] = None
+        for i, (p, attrs) in enumerate(self._patterns):
+            if p.pattern == pattern:
+                pattern_obj = p
+                # Convert to mutable dict
+                attrs_dict = dict(attrs)
+                break
+
+        if pattern_obj is None:
+            # Create new pattern
+            pattern_obj = Pattern(pattern)
+            attrs_dict = {}
+            self._patterns.append((pattern_obj, attrs_dict))
+
+        # Update the attribute
+        if attrs_dict is None:
+            raise AssertionError("attrs_dict should not be None at this point")
+        attrs_dict[name] = value
+
+        # Update the pattern in the list
+        for i, (p, _) in enumerate(self._patterns):
+            if p.pattern == pattern:
+                self._patterns[i] = (p, attrs_dict)
+                break
+
+    def remove_pattern(self, pattern: bytes) -> None:
+        """Remove all attributes for a pattern.
+
+        Args:
+            pattern: The file pattern to remove
+        """
+        self._patterns = [
+            (p, attrs) for p, attrs in self._patterns if p.pattern != pattern
+        ]
+
+    def to_bytes(self) -> bytes:
+        """Convert GitAttributes to bytes format suitable for writing to file.
+
+        Returns:
+            Bytes representation of the gitattributes file
+        """
+        lines = []
+        for pattern_obj, attrs in self._patterns:
+            pattern = pattern_obj.pattern
+            attr_strs = []
+
+            for name, value in sorted(attrs.items()):
+                if value is True:
+                    attr_strs.append(name)
+                elif value is False:
+                    attr_strs.append(b"-" + name)
+                elif value is None:
+                    attr_strs.append(b"!" + name)
+                else:
+                    # value is bytes
+                    attr_strs.append(name + b"=" + value)
+
+            if attr_strs:
+                line = pattern + b" " + b" ".join(attr_strs)
+                lines.append(line)
+
+        return b"\n".join(lines) + b"\n" if lines else b""
+
+    def write_to_file(self, filename: Union[str, bytes]) -> None:
+        """Write GitAttributes to a file.
+
+        Args:
+            filename: Path to write the .gitattributes file
+        """
+        if isinstance(filename, str):
+            filename = filename.encode("utf-8")
+
+        content = self.to_bytes()
+        with open(filename, "wb") as f:
+            f.write(content)

+ 180 - 0
tests/test_attrs.py

@@ -456,3 +456,183 @@ class GitAttributesTests(TestCase):
 
             attrs = ga.match_path(b"lib.rs")
             self.assertEqual(attrs, {b"diff": b"rust"})
+
+    def test_set_attribute(self):
+        """Test setting attributes."""
+        ga = GitAttributes()
+
+        # Set attribute for a new pattern
+        ga.set_attribute(b"*.txt", b"text", True)
+        attrs = ga.match_path(b"file.txt")
+        self.assertEqual(attrs, {b"text": True})
+
+        # Update existing pattern
+        ga.set_attribute(b"*.txt", b"diff", b"plain")
+        attrs = ga.match_path(b"file.txt")
+        self.assertEqual(attrs, {b"text": True, b"diff": b"plain"})
+
+        # Unset attribute
+        ga.set_attribute(b"*.txt", b"binary", False)
+        attrs = ga.match_path(b"file.txt")
+        self.assertEqual(attrs, {b"text": True, b"diff": b"plain", b"binary": False})
+
+        # Remove attribute (unspecified)
+        ga.set_attribute(b"*.txt", b"text", None)
+        attrs = ga.match_path(b"file.txt")
+        self.assertEqual(attrs, {b"diff": b"plain", b"binary": False})
+
+    def test_remove_pattern(self):
+        """Test removing patterns."""
+        patterns = [
+            (Pattern(b"*.txt"), {b"text": True}),
+            (Pattern(b"*.jpg"), {b"binary": True}),
+            (Pattern(b"*.py"), {b"diff": b"python"}),
+        ]
+        ga = GitAttributes(patterns)
+        self.assertEqual(len(ga), 3)
+
+        # Remove middle pattern
+        ga.remove_pattern(b"*.jpg")
+        self.assertEqual(len(ga), 2)
+
+        # Check remaining patterns
+        attrs = ga.match_path(b"file.txt")
+        self.assertEqual(attrs, {b"text": True})
+
+        attrs = ga.match_path(b"image.jpg")
+        self.assertEqual(attrs, {})  # No match anymore
+
+        attrs = ga.match_path(b"script.py")
+        self.assertEqual(attrs, {b"diff": b"python"})
+
+    def test_to_bytes_empty(self):
+        """Test converting empty GitAttributes to bytes."""
+        ga = GitAttributes()
+        content = ga.to_bytes()
+        self.assertEqual(content, b"")
+
+    def test_to_bytes_single_pattern(self):
+        """Test converting single pattern to bytes."""
+        ga = GitAttributes()
+        ga.set_attribute(b"*.txt", b"text", True)
+        content = ga.to_bytes()
+        self.assertEqual(content, b"*.txt text\n")
+
+    def test_to_bytes_multiple_attributes(self):
+        """Test converting pattern with multiple attributes to bytes."""
+        ga = GitAttributes()
+        ga.set_attribute(b"*.jpg", b"text", False)
+        ga.set_attribute(b"*.jpg", b"diff", False)
+        ga.set_attribute(b"*.jpg", b"binary", True)
+        content = ga.to_bytes()
+        # Attributes should be sorted
+        self.assertEqual(content, b"*.jpg binary -diff -text\n")
+
+    def test_to_bytes_multiple_patterns(self):
+        """Test converting multiple patterns to bytes."""
+        ga = GitAttributes()
+        ga.set_attribute(b"*.txt", b"text", True)
+        ga.set_attribute(b"*.jpg", b"binary", True)
+        ga.set_attribute(b"*.jpg", b"text", False)
+        content = ga.to_bytes()
+        expected = b"*.txt text\n*.jpg binary -text\n"
+        self.assertEqual(content, expected)
+
+    def test_to_bytes_with_values(self):
+        """Test converting attributes with values to bytes."""
+        ga = GitAttributes()
+        ga.set_attribute(b"*.c", b"filter", b"indent")
+        ga.set_attribute(b"*.c", b"diff", b"cpp")
+        ga.set_attribute(b"*.c", b"text", True)
+        content = ga.to_bytes()
+        # Attributes should be sorted
+        self.assertEqual(content, b"*.c diff=cpp filter=indent text\n")
+
+    def test_to_bytes_unspecified(self):
+        """Test converting unspecified attributes to bytes."""
+        ga = GitAttributes()
+        ga.set_attribute(b"*.bin", b"text", None)
+        ga.set_attribute(b"*.bin", b"diff", None)
+        content = ga.to_bytes()
+        # Unspecified attributes use !
+        self.assertEqual(content, b"*.bin !diff !text\n")
+
+    def test_write_to_file(self):
+        """Test writing GitAttributes to file."""
+        ga = GitAttributes()
+        ga.set_attribute(b"*.txt", b"text", True)
+        ga.set_attribute(b"*.jpg", b"binary", True)
+        ga.set_attribute(b"*.jpg", b"text", False)
+
+        with tempfile.NamedTemporaryFile(delete=False) as f:
+            temp_path = f.name
+
+        try:
+            ga.write_to_file(temp_path)
+
+            # Read back the file
+            with open(temp_path, "rb") as f:
+                content = f.read()
+
+            expected = b"*.txt text\n*.jpg binary -text\n"
+            self.assertEqual(content, expected)
+        finally:
+            os.unlink(temp_path)
+
+    def test_write_to_file_string_path(self):
+        """Test writing GitAttributes to file with string path."""
+        ga = GitAttributes()
+        ga.set_attribute(b"*.py", b"diff", b"python")
+
+        with tempfile.NamedTemporaryFile(delete=False) as f:
+            temp_path = f.name
+
+        try:
+            # Pass string path instead of bytes
+            ga.write_to_file(temp_path)
+
+            # Read back the file
+            with open(temp_path, "rb") as f:
+                content = f.read()
+
+            self.assertEqual(content, b"*.py diff=python\n")
+        finally:
+            os.unlink(temp_path)
+
+    def test_roundtrip_gitattributes(self):
+        """Test reading and writing gitattributes preserves content."""
+        original_content = b"""*.txt text
+*.jpg -text binary
+*.c filter=indent diff=cpp
+*.bin !text !diff
+"""
+
+        with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
+            f.write(original_content)
+            temp_path = f.name
+
+        try:
+            # Read the file
+            ga = GitAttributes.from_file(temp_path)
+
+            # Write to a new file
+            with tempfile.NamedTemporaryFile(delete=False) as f2:
+                temp_path2 = f2.name
+
+            ga.write_to_file(temp_path2)
+
+            # The content should be equivalent (though order might differ for attributes)
+            ga2 = GitAttributes.from_file(temp_path2)
+
+            # Compare patterns
+            patterns1 = list(ga)
+            patterns2 = list(ga2)
+            self.assertEqual(len(patterns1), len(patterns2))
+
+            for (p1, attrs1), (p2, attrs2) in zip(patterns1, patterns2):
+                self.assertEqual(p1.pattern, p2.pattern)
+                self.assertEqual(attrs1, attrs2)
+
+            os.unlink(temp_path2)
+        finally:
+            os.unlink(temp_path)