test_filters.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. # test_filters.py -- Tests for filters
  2. # Copyright (C) 2024 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  5. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  6. # General Public License as public by the Free Software Foundation; version 2.0
  7. # or (at your option) any later version. You can redistribute it and/or
  8. # modify it under the terms of either of these two licenses.
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # You should have received a copy of the licenses; if not, see
  17. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  18. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  19. # License, Version 2.0.
  20. #
  21. """Tests for filters."""
  22. import os
  23. import tempfile
  24. import unittest
  25. from dulwich import porcelain
  26. from dulwich.repo import Repo
  27. from . import TestCase
  28. class GitAttributesFilterIntegrationTests(TestCase):
  29. """Test gitattributes integration with filter drivers."""
  30. def setUp(self) -> None:
  31. super().setUp()
  32. self.test_dir = tempfile.mkdtemp()
  33. self.addCleanup(self._cleanup_test_dir)
  34. self.repo = Repo.init(self.test_dir)
  35. def _cleanup_test_dir(self) -> None:
  36. """Clean up test directory."""
  37. import shutil
  38. shutil.rmtree(self.test_dir)
  39. def test_gitattributes_text_filter(self) -> None:
  40. """Test that text attribute triggers line ending conversion."""
  41. # Configure autocrlf first
  42. config = self.repo.get_config()
  43. config.set((b"core",), b"autocrlf", b"true")
  44. config.write_to_path()
  45. # Create .gitattributes with text attribute
  46. gitattributes_path = os.path.join(self.test_dir, ".gitattributes")
  47. with open(gitattributes_path, "wb") as f:
  48. f.write(b"*.txt text\n")
  49. f.write(b"*.bin -text\n")
  50. # Add .gitattributes
  51. porcelain.add(self.repo, paths=[".gitattributes"])
  52. porcelain.commit(self.repo, message=b"Add gitattributes")
  53. # Create text file with CRLF
  54. text_file = os.path.join(self.test_dir, "test.txt")
  55. with open(text_file, "wb") as f:
  56. f.write(b"line1\r\nline2\r\n")
  57. # Create binary file with CRLF
  58. bin_file = os.path.join(self.test_dir, "test.bin")
  59. with open(bin_file, "wb") as f:
  60. f.write(b"binary\r\ndata\r\n")
  61. # Add files
  62. porcelain.add(self.repo, paths=["test.txt", "test.bin"])
  63. # Check that text file was normalized
  64. index = self.repo.open_index()
  65. text_entry = index[b"test.txt"]
  66. text_blob = self.repo.object_store[text_entry.sha]
  67. self.assertEqual(text_blob.data, b"line1\nline2\n")
  68. # Check that binary file was not normalized
  69. bin_entry = index[b"test.bin"]
  70. bin_blob = self.repo.object_store[bin_entry.sha]
  71. self.assertEqual(bin_blob.data, b"binary\r\ndata\r\n")
  72. @unittest.skip("Custom process filters require external commands")
  73. def test_gitattributes_custom_filter(self) -> None:
  74. """Test custom filter specified in gitattributes."""
  75. # Create .gitattributes with custom filter
  76. gitattributes_path = os.path.join(self.test_dir, ".gitattributes")
  77. with open(gitattributes_path, "wb") as f:
  78. f.write(b"*.secret filter=redact\n")
  79. # Configure custom filter (use tr command for testing)
  80. config = self.repo.get_config()
  81. # This filter replaces all digits with X
  82. config.set((b"filter", b"redact"), b"clean", b"tr '0-9' 'X'")
  83. config.write_to_path()
  84. # Add .gitattributes
  85. porcelain.add(self.repo, paths=[".gitattributes"])
  86. # Create file with sensitive content
  87. secret_file = os.path.join(self.test_dir, "password.secret")
  88. with open(secret_file, "wb") as f:
  89. f.write(b"password123\ntoken456\n")
  90. # Add file
  91. porcelain.add(self.repo, paths=["password.secret"])
  92. # Check that content was filtered
  93. index = self.repo.open_index()
  94. entry = index[b"password.secret"]
  95. blob = self.repo.object_store[entry.sha]
  96. self.assertEqual(blob.data, b"passwordXXX\ntokenXXX\n")
  97. def test_gitattributes_from_tree(self) -> None:
  98. """Test that gitattributes from tree are used when no working tree exists."""
  99. # Create .gitattributes with text attribute
  100. gitattributes_path = os.path.join(self.test_dir, ".gitattributes")
  101. with open(gitattributes_path, "wb") as f:
  102. f.write(b"*.txt text\n")
  103. # Add and commit .gitattributes
  104. porcelain.add(self.repo, paths=[".gitattributes"])
  105. porcelain.commit(self.repo, message=b"Add gitattributes")
  106. # Remove .gitattributes from working tree
  107. os.remove(gitattributes_path)
  108. # Get gitattributes - should still work from tree
  109. gitattributes = self.repo.get_gitattributes()
  110. attrs = gitattributes.match_path(b"test.txt")
  111. self.assertEqual(attrs.get(b"text"), True)
  112. def test_gitattributes_info_attributes(self) -> None:
  113. """Test that .git/info/attributes is read."""
  114. # Create info/attributes
  115. info_dir = os.path.join(self.repo.controldir(), "info")
  116. if not os.path.exists(info_dir):
  117. os.makedirs(info_dir)
  118. info_attrs_path = os.path.join(info_dir, "attributes")
  119. with open(info_attrs_path, "wb") as f:
  120. f.write(b"*.log text\n")
  121. # Get gitattributes
  122. gitattributes = self.repo.get_gitattributes()
  123. attrs = gitattributes.match_path(b"debug.log")
  124. self.assertEqual(attrs.get(b"text"), True)
  125. @unittest.skip("Custom process filters require external commands")
  126. def test_filter_precedence(self) -> None:
  127. """Test that filter attribute takes precedence over text attribute."""
  128. # Create .gitattributes with both text and filter
  129. gitattributes_path = os.path.join(self.test_dir, ".gitattributes")
  130. with open(gitattributes_path, "wb") as f:
  131. f.write(b"*.txt text filter=custom\n")
  132. # Configure autocrlf and custom filter
  133. config = self.repo.get_config()
  134. config.set((b"core",), b"autocrlf", b"true")
  135. # This filter converts to uppercase
  136. config.set((b"filter", b"custom"), b"clean", b"tr '[:lower:]' '[:upper:]'")
  137. config.write_to_path()
  138. # Add .gitattributes
  139. porcelain.add(self.repo, paths=[".gitattributes"])
  140. # Create text file with lowercase and CRLF
  141. text_file = os.path.join(self.test_dir, "test.txt")
  142. with open(text_file, "wb") as f:
  143. f.write(b"hello\r\nworld\r\n")
  144. # Add file
  145. porcelain.add(self.repo, paths=["test.txt"])
  146. # Check that custom filter was applied (not just line ending conversion)
  147. index = self.repo.open_index()
  148. entry = index[b"test.txt"]
  149. blob = self.repo.object_store[entry.sha]
  150. # Should be uppercase with LF endings
  151. self.assertEqual(blob.data, b"HELLO\nWORLD\n")
  152. def test_blob_normalizer_integration(self) -> None:
  153. """Test that get_blob_normalizer returns a FilterBlobNormalizer."""
  154. normalizer = self.repo.get_blob_normalizer()
  155. # Check it's the right type
  156. from dulwich.filters import FilterBlobNormalizer
  157. self.assertIsInstance(normalizer, FilterBlobNormalizer)
  158. # Check it has access to gitattributes
  159. self.assertIsNotNone(normalizer.gitattributes)
  160. self.assertIsNotNone(normalizer.filter_registry)