123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- # test_annotate.py -- tests for annotate
- # Copyright (C) 2015 Jelmer Vernooij <jelmer@jelmer.uk>
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License
- # as published by the Free Software Foundation; version 2
- # of the License or (at your option) a later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- # MA 02110-1301, USA.
- """Tests for annotate support."""
- import os
- import tempfile
- import unittest
- from unittest import TestCase
- from dulwich.annotate import annotate_lines, update_lines
- from dulwich.objects import Blob, Commit, Tree
- from dulwich.porcelain import annotate, blame
- from dulwich.repo import Repo
- class UpdateLinesTestCase(TestCase):
- """Tests for update_lines function."""
- def test_update_lines_equal(self):
- """Test update_lines when all lines are equal."""
- old_lines = [
- (("commit1", "entry1"), b"line1"),
- (("commit2", "entry2"), b"line2"),
- ]
- new_blob = b"line1\nline2"
- new_history_data = ("commit3", "entry3")
- result = update_lines(old_lines, new_history_data, new_blob)
- self.assertEqual(old_lines, result)
- def test_update_lines_insert(self):
- """Test update_lines when new lines are inserted."""
- old_lines = [
- (("commit1", "entry1"), b"line1"),
- (("commit2", "entry2"), b"line3"),
- ]
- new_blob = b"line1\nline2\nline3"
- new_history_data = ("commit3", "entry3")
- result = update_lines(old_lines, new_history_data, new_blob)
- expected = [
- (("commit1", "entry1"), b"line1"),
- (("commit3", "entry3"), b"line2"),
- (("commit2", "entry2"), b"line3"),
- ]
- self.assertEqual(expected, result)
- def test_update_lines_delete(self):
- """Test update_lines when lines are deleted."""
- old_lines = [
- (("commit1", "entry1"), b"line1"),
- (("commit2", "entry2"), b"line2"),
- (("commit3", "entry3"), b"line3"),
- ]
- new_blob = b"line1\nline3"
- new_history_data = ("commit4", "entry4")
- result = update_lines(old_lines, new_history_data, new_blob)
- expected = [
- (("commit1", "entry1"), b"line1"),
- (("commit3", "entry3"), b"line3"),
- ]
- self.assertEqual(expected, result)
- def test_update_lines_replace(self):
- """Test update_lines when lines are replaced."""
- old_lines = [
- (("commit1", "entry1"), b"line1"),
- (("commit2", "entry2"), b"line2"),
- ]
- new_blob = b"line1\nline2_modified"
- new_history_data = ("commit3", "entry3")
- result = update_lines(old_lines, new_history_data, new_blob)
- expected = [
- (("commit1", "entry1"), b"line1"),
- (("commit3", "entry3"), b"line2_modified"),
- ]
- self.assertEqual(expected, result)
- def test_update_lines_empty_old(self):
- """Test update_lines with empty old lines."""
- old_lines = []
- new_blob = b"line1\nline2"
- new_history_data = ("commit1", "entry1")
- result = update_lines(old_lines, new_history_data, new_blob)
- expected = [
- (("commit1", "entry1"), b"line1"),
- (("commit1", "entry1"), b"line2"),
- ]
- self.assertEqual(expected, result)
- def test_update_lines_empty_new(self):
- """Test update_lines with empty new blob."""
- old_lines = [(("commit1", "entry1"), b"line1")]
- new_blob = b""
- new_history_data = ("commit2", "entry2")
- result = update_lines(old_lines, new_history_data, new_blob)
- self.assertEqual([], result)
- class AnnotateLinesTestCase(TestCase):
- """Tests for annotate_lines function."""
- def setUp(self):
- self.temp_dir = tempfile.mkdtemp()
- self.repo = Repo.init(self.temp_dir)
- def tearDown(self):
- self.repo.close()
- import shutil
- shutil.rmtree(self.temp_dir)
- def _make_commit(self, blob_content, message, parent=None):
- """Helper to create a commit with a single file."""
- # Create blob
- blob = Blob()
- blob.data = blob_content
- self.repo.object_store.add_object(blob)
- # Create tree
- tree = Tree()
- tree.add(b"test.txt", 0o100644, blob.id)
- self.repo.object_store.add_object(tree)
- # Create commit
- commit = Commit()
- commit.tree = tree.id
- commit.author = commit.committer = b"Test Author <test@example.com>"
- commit.author_time = commit.commit_time = 1000000000
- commit.author_timezone = commit.commit_timezone = 0
- commit.encoding = b"UTF-8"
- commit.message = message.encode("utf-8")
- if parent:
- commit.parents = [parent]
- else:
- commit.parents = []
- self.repo.object_store.add_object(commit)
- return commit.id
- def test_annotate_lines_single_commit(self):
- """Test annotating a file with a single commit."""
- commit_id = self._make_commit(b"line1\nline2\nline3\n", "Initial commit")
- result = annotate_lines(self.repo.object_store, commit_id, b"test.txt")
- self.assertEqual(3, len(result))
- for (commit, entry), line in result:
- self.assertEqual(commit_id, commit.id)
- self.assertIn(line, [b"line1\n", b"line2\n", b"line3\n"])
- def test_annotate_lines_multiple_commits(self):
- """Test annotating a file with multiple commits."""
- # First commit
- commit1_id = self._make_commit(b"line1\nline2\n", "Initial commit")
- # Second commit - add a line
- commit2_id = self._make_commit(
- b"line1\nline1.5\nline2\n", "Add line between", parent=commit1_id
- )
- # Third commit - modify a line
- commit3_id = self._make_commit(
- b"line1_modified\nline1.5\nline2\n", "Modify first line", parent=commit2_id
- )
- result = annotate_lines(self.repo.object_store, commit3_id, b"test.txt")
- self.assertEqual(3, len(result))
- # First line should be from commit3
- self.assertEqual(commit3_id, result[0][0][0].id)
- self.assertEqual(b"line1_modified\n", result[0][1])
- # Second line should be from commit2
- self.assertEqual(commit2_id, result[1][0][0].id)
- self.assertEqual(b"line1.5\n", result[1][1])
- # Third line should be from commit1
- self.assertEqual(commit1_id, result[2][0][0].id)
- self.assertEqual(b"line2\n", result[2][1])
- def test_annotate_lines_nonexistent_path(self):
- """Test annotating a nonexistent file."""
- commit_id = self._make_commit(b"content\n", "Initial commit")
- result = annotate_lines(self.repo.object_store, commit_id, b"nonexistent.txt")
- self.assertEqual([], result)
- class PorcelainAnnotateTestCase(TestCase):
- """Tests for the porcelain annotate function."""
- def setUp(self):
- self.temp_dir = tempfile.mkdtemp()
- self.repo = Repo.init(self.temp_dir)
- def tearDown(self):
- self.repo.close()
- import shutil
- shutil.rmtree(self.temp_dir)
- def _make_commit_with_file(self, filename, content, message, parent=None):
- """Helper to create a commit with a file."""
- # Create blob
- blob = Blob()
- blob.data = content
- self.repo.object_store.add_object(blob)
- # Create tree
- tree = Tree()
- tree.add(filename.encode(), 0o100644, blob.id)
- self.repo.object_store.add_object(tree)
- # Create commit
- commit = Commit()
- commit.tree = tree.id
- commit.author = commit.committer = b"Test Author <test@example.com>"
- commit.author_time = commit.commit_time = 1000000000
- commit.author_timezone = commit.commit_timezone = 0
- commit.encoding = b"UTF-8"
- commit.message = message.encode("utf-8")
- if parent:
- commit.parents = [parent]
- else:
- commit.parents = []
- self.repo.object_store.add_object(commit)
- # Update HEAD
- self.repo.refs[b"HEAD"] = commit.id
- return commit.id
- def test_porcelain_annotate(self):
- """Test the porcelain annotate function."""
- # Create commits
- commit1_id = self._make_commit_with_file(
- "file.txt", b"line1\nline2\n", "Initial commit"
- )
- self._make_commit_with_file(
- "file.txt", b"line1\nline2\nline3\n", "Add third line", parent=commit1_id
- )
- # Test annotate
- result = list(annotate(self.temp_dir, "file.txt"))
- self.assertEqual(3, len(result))
- # Check that each result has the right structure
- for (commit, entry), line in result:
- self.assertIsNotNone(commit)
- self.assertIsNotNone(entry)
- self.assertIn(line, [b"line1\n", b"line2\n", b"line3\n"])
- def test_porcelain_annotate_with_committish(self):
- """Test porcelain annotate with specific commit."""
- # Create commits
- commit1_id = self._make_commit_with_file(
- "file.txt", b"original\n", "Initial commit"
- )
- self._make_commit_with_file(
- "file.txt", b"modified\n", "Modify file", parent=commit1_id
- )
- # Annotate at first commit
- result = list(
- annotate(self.temp_dir, "file.txt", committish=commit1_id.decode())
- )
- self.assertEqual(1, len(result))
- self.assertEqual(b"original\n", result[0][1])
- # Annotate at HEAD (second commit)
- result = list(annotate(self.temp_dir, "file.txt"))
- self.assertEqual(1, len(result))
- self.assertEqual(b"modified\n", result[0][1])
- def test_blame_alias(self):
- """Test that blame is an alias for annotate."""
- self.assertIs(blame, annotate)
- class IntegrationTestCase(TestCase):
- """Integration tests with more complex scenarios."""
- def setUp(self):
- self.temp_dir = tempfile.mkdtemp()
- self.repo = Repo.init(self.temp_dir)
- def tearDown(self):
- self.repo.close()
- import shutil
- shutil.rmtree(self.temp_dir)
- def _create_file_commit(self, filename, content, message, parent=None):
- """Helper to create a commit with file content."""
- # Write file to working directory
- filepath = os.path.join(self.temp_dir, filename)
- with open(filepath, "wb") as f:
- f.write(content)
- # Stage file
- self.repo.stage([filename.encode()])
- # Create commit
- commit_id = self.repo.do_commit(
- message.encode(),
- committer=b"Test Committer <test@example.com>",
- author=b"Test Author <test@example.com>",
- commit_timestamp=1000000000,
- commit_timezone=0,
- author_timestamp=1000000000,
- author_timezone=0,
- )
- return commit_id
- def test_complex_file_history(self):
- """Test annotating a file with complex history."""
- # Initial commit with 3 lines
- self._create_file_commit(
- "complex.txt", b"First line\nSecond line\nThird line\n", "Initial commit"
- )
- # Add lines at the beginning and end
- self._create_file_commit(
- "complex.txt",
- b"New first line\nFirst line\nSecond line\nThird line\nNew last line\n",
- "Add lines at beginning and end",
- )
- # Modify middle line
- self._create_file_commit(
- "complex.txt",
- b"New first line\nFirst line\n"
- b"Modified second line\nThird line\n"
- b"New last line\n",
- "Modify middle line",
- )
- # Delete a line
- self._create_file_commit(
- "complex.txt",
- b"New first line\nFirst line\nModified second line\nNew last line\n",
- "Delete third line",
- )
- # Run annotate
- result = list(annotate(self.temp_dir, "complex.txt"))
- # Should have 4 lines
- self.assertEqual(4, len(result))
- # Verify each line comes from the correct commit
- lines = [line for (commit, entry), line in result]
- self.assertEqual(
- [
- b"New first line\n",
- b"First line\n",
- b"Modified second line\n",
- b"New last line\n",
- ],
- lines,
- )
- if __name__ == "__main__":
- unittest.main()
|