test_annotate.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. # test_annotate.py -- tests for porcelain annotate
  2. # Copyright (C) 2015 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 published 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 porcelain annotate and blame functions."""
  22. import os
  23. import tempfile
  24. from unittest import TestCase
  25. from dulwich.objects import Blob, Commit, Tree
  26. from dulwich.porcelain import annotate, blame
  27. from dulwich.repo import Repo
  28. class PorcelainAnnotateTestCase(TestCase):
  29. """Tests for the porcelain annotate function."""
  30. def setUp(self) -> None:
  31. self.temp_dir = tempfile.mkdtemp()
  32. self.repo = Repo.init(self.temp_dir)
  33. def tearDown(self) -> None:
  34. self.repo.close()
  35. import shutil
  36. shutil.rmtree(self.temp_dir)
  37. def _make_commit_with_file(
  38. self,
  39. filename: str,
  40. content: bytes,
  41. message: str,
  42. parent: bytes | None = None,
  43. ) -> bytes:
  44. """Helper to create a commit with a file."""
  45. # Create blob
  46. blob = Blob()
  47. blob.data = content
  48. self.repo.object_store.add_object(blob)
  49. # Create tree
  50. tree = Tree()
  51. tree.add(filename.encode(), 0o100644, blob.id)
  52. self.repo.object_store.add_object(tree)
  53. # Create commit
  54. commit = Commit()
  55. commit.tree = tree.id
  56. commit.author = commit.committer = b"Test Author <test@example.com>"
  57. commit.author_time = commit.commit_time = 1000000000
  58. commit.author_timezone = commit.commit_timezone = 0
  59. commit.encoding = b"UTF-8"
  60. commit.message = message.encode("utf-8")
  61. if parent:
  62. commit.parents = [parent]
  63. else:
  64. commit.parents = []
  65. self.repo.object_store.add_object(commit)
  66. # Update HEAD
  67. self.repo.refs[b"HEAD"] = commit.id
  68. return commit.id
  69. def test_porcelain_annotate(self) -> None:
  70. """Test the porcelain annotate function."""
  71. # Create commits
  72. commit1_id = self._make_commit_with_file(
  73. "file.txt", b"line1\nline2\n", "Initial commit"
  74. )
  75. self._make_commit_with_file(
  76. "file.txt", b"line1\nline2\nline3\n", "Add third line", parent=commit1_id
  77. )
  78. # Test annotate
  79. result = list(annotate(self.temp_dir, "file.txt"))
  80. self.assertEqual(3, len(result))
  81. # Check that each result has the right structure
  82. for (commit, entry), line in result:
  83. self.assertIsNotNone(commit)
  84. self.assertIsNotNone(entry)
  85. self.assertIn(line, [b"line1\n", b"line2\n", b"line3\n"])
  86. def test_porcelain_annotate_with_committish(self) -> None:
  87. """Test porcelain annotate with specific commit."""
  88. # Create commits
  89. commit1_id = self._make_commit_with_file(
  90. "file.txt", b"original\n", "Initial commit"
  91. )
  92. self._make_commit_with_file(
  93. "file.txt", b"modified\n", "Modify file", parent=commit1_id
  94. )
  95. # Annotate at first commit
  96. result = list(
  97. annotate(self.temp_dir, "file.txt", committish=commit1_id.decode())
  98. )
  99. self.assertEqual(1, len(result))
  100. self.assertEqual(b"original\n", result[0][1])
  101. # Annotate at HEAD (second commit)
  102. result = list(annotate(self.temp_dir, "file.txt"))
  103. self.assertEqual(1, len(result))
  104. self.assertEqual(b"modified\n", result[0][1])
  105. def test_blame_alias(self) -> None:
  106. """Test that blame is an alias for annotate."""
  107. self.assertIs(blame, annotate)
  108. class IntegrationTestCase(TestCase):
  109. """Integration tests with more complex scenarios."""
  110. def setUp(self) -> None:
  111. self.temp_dir = tempfile.mkdtemp()
  112. self.repo = Repo.init(self.temp_dir)
  113. def tearDown(self) -> None:
  114. self.repo.close()
  115. import shutil
  116. shutil.rmtree(self.temp_dir)
  117. def _create_file_commit(
  118. self,
  119. filename: str,
  120. content: bytes,
  121. message: str,
  122. parent: bytes | None = None,
  123. ) -> bytes:
  124. """Helper to create a commit with file content."""
  125. # Write file to working directory
  126. filepath = os.path.join(self.temp_dir, filename)
  127. with open(filepath, "wb") as f:
  128. f.write(content)
  129. # Stage file
  130. self.repo.get_worktree().stage([filename.encode()])
  131. # Create commit
  132. commit_id = self.repo.get_worktree().commit(
  133. message=message.encode(),
  134. committer=b"Test Committer <test@example.com>",
  135. author=b"Test Author <test@example.com>",
  136. commit_timestamp=1000000000,
  137. commit_timezone=0,
  138. author_timestamp=1000000000,
  139. author_timezone=0,
  140. )
  141. return commit_id
  142. def test_complex_file_history(self) -> None:
  143. """Test annotating a file with complex history."""
  144. # Initial commit with 3 lines
  145. self._create_file_commit(
  146. "complex.txt", b"First line\nSecond line\nThird line\n", "Initial commit"
  147. )
  148. # Add lines at the beginning and end
  149. self._create_file_commit(
  150. "complex.txt",
  151. b"New first line\nFirst line\nSecond line\nThird line\nNew last line\n",
  152. "Add lines at beginning and end",
  153. )
  154. # Modify middle line
  155. self._create_file_commit(
  156. "complex.txt",
  157. b"New first line\nFirst line\n"
  158. b"Modified second line\nThird line\n"
  159. b"New last line\n",
  160. "Modify middle line",
  161. )
  162. # Delete a line
  163. self._create_file_commit(
  164. "complex.txt",
  165. b"New first line\nFirst line\nModified second line\nNew last line\n",
  166. "Delete third line",
  167. )
  168. # Run annotate
  169. result = list(annotate(self.temp_dir, "complex.txt"))
  170. # Should have 4 lines
  171. self.assertEqual(4, len(result))
  172. # Verify each line comes from the correct commit
  173. lines = [line for (commit, entry), line in result]
  174. self.assertEqual(
  175. [
  176. b"New first line\n",
  177. b"First line\n",
  178. b"Modified second line\n",
  179. b"New last line\n",
  180. ],
  181. lines,
  182. )