# test_sha256.py -- Compatibility tests for SHA256 support # Copyright (C) 2024 The Dulwich contributors # # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU # General Public License as public by the Free Software Foundation; version 2.0 # or (at your option) any later version. You can redistribute it and/or # modify it under the terms of either of these two licenses. # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # You should have received a copy of the licenses; if not, see # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Compatibility tests for SHA256 support with git command line tools.""" import os import tempfile from dulwich.object_format import SHA256 from dulwich.objects import Blob, Commit, Tree from dulwich.repo import Repo from .utils import CompatTestCase, run_git_or_fail class GitSHA256CompatibilityTests(CompatTestCase): """Test SHA256 compatibility with git command line tools.""" min_git_version = (2, 29, 0) def _run_git(self, args, cwd=None): """Run git command in the specified directory.""" return run_git_or_fail(args, cwd=cwd) def test_sha256_repo_creation_compat(self): """Test that dulwich-created SHA256 repos are readable by git.""" # Create SHA256 repo with dulwich repo_path = tempfile.mkdtemp() self.addCleanup(lambda: __import__("shutil").rmtree(repo_path)) repo = Repo.init(repo_path, mkdir=False, object_format="sha256") # Add a blob and tree using dulwich blob = Blob.from_string(b"Hello SHA256 world!") tree = Tree() tree.add(b"hello.txt", 0o100644, blob.get_id(SHA256)) # Create objects in the repository object_store = repo.object_store object_store.add_object(blob) object_store.add_object(tree) repo.close() # Verify git can read the repository config_output = self._run_git( ["config", "--get", "extensions.objectformat"], cwd=repo_path ) self.assertEqual(config_output.strip(), b"sha256") # Verify git recognizes it as a SHA256 repository rev_parse_output = self._run_git( ["rev-parse", "--show-object-format"], cwd=repo_path ) self.assertEqual(rev_parse_output.strip(), b"sha256") def test_git_created_sha256_repo_readable(self): """Test that git-created SHA256 repos are readable by dulwich.""" # Create SHA256 repo with git repo_path = tempfile.mkdtemp() self.addCleanup(lambda: __import__("shutil").rmtree(repo_path)) self._run_git(["init", "--object-format=sha256", repo_path]) # Create a file and commit with git test_file = os.path.join(repo_path, "test.txt") with open(test_file, "w") as f: f.write("Test SHA256 content") self._run_git(["add", "test.txt"], cwd=repo_path) self._run_git(["commit", "-m", "Test SHA256 commit"], cwd=repo_path) # Read with dulwich repo = Repo(repo_path) # Verify dulwich detects SHA256 self.assertEqual(repo.object_format, SHA256) # Verify dulwich can read objects # Try both main and master branches (git default changed over time) try: head_ref = repo.refs[b"refs/heads/main"] except KeyError: head_ref = repo.refs[b"refs/heads/master"] self.assertEqual(len(head_ref), 64) # SHA256 length # Read the commit object commit = repo[head_ref] self.assertIsInstance(commit, Commit) self.assertEqual(len(commit.tree), 64) # SHA256 tree ID repo.close() def test_object_hashing_consistency(self): """Test that object hashing is consistent between dulwich and git.""" # Create SHA256 repo with git repo_path = tempfile.mkdtemp() self.addCleanup(lambda: __import__("shutil").rmtree(repo_path)) self._run_git(["init", "--object-format=sha256", repo_path]) # Create a test file with known content test_content = b"Test content for SHA256 hashing consistency" test_file = os.path.join(repo_path, "test.txt") with open(test_file, "wb") as f: f.write(test_content) # Get git's hash for the content git_hash = self._run_git(["hash-object", "test.txt"], cwd=repo_path) git_hash = git_hash.strip().decode("ascii") # Create same blob with dulwich blob = Blob.from_string(test_content) dulwich_hash = blob.get_id(SHA256).decode("ascii") # Hashes should match self.assertEqual(git_hash, dulwich_hash) def test_tree_hashing_consistency(self): """Test that tree hashing is consistent between dulwich and git.""" # Create SHA256 repo with git repo_path = tempfile.mkdtemp() self.addCleanup(lambda: __import__("shutil").rmtree(repo_path)) self._run_git(["init", "--object-format=sha256", repo_path]) # Create a test file and add to index test_content = b"Tree test content" test_file = os.path.join(repo_path, "tree_test.txt") with open(test_file, "wb") as f: f.write(test_content) self._run_git(["add", "tree_test.txt"], cwd=repo_path) # Get git's tree hash git_tree_hash = self._run_git(["write-tree"], cwd=repo_path) git_tree_hash = git_tree_hash.strip().decode("ascii") # Create same tree with dulwich blob = Blob.from_string(test_content) tree = Tree() tree.add(b"tree_test.txt", 0o100644, blob.get_id(SHA256)) dulwich_tree_hash = tree.get_id(SHA256).decode("ascii") # Tree hashes should match self.assertEqual(git_tree_hash, dulwich_tree_hash) def test_commit_creation_interop(self): """Test commit creation interoperability between dulwich and git.""" # Create SHA256 repo with dulwich repo_path = tempfile.mkdtemp() self.addCleanup(lambda: __import__("shutil").rmtree(repo_path)) repo = Repo.init(repo_path, mkdir=False, object_format="sha256") # Create objects with dulwich blob = Blob.from_string(b"Interop test content") tree = Tree() tree.add(b"interop.txt", 0o100644, blob.get_id(SHA256)) commit = Commit() commit.tree = tree.get_id(SHA256) commit.author = commit.committer = b"Test User " commit.commit_time = commit.author_time = 1234567890 commit.commit_timezone = commit.author_timezone = 0 commit.message = b"Test SHA256 commit from dulwich" # Add objects to repo object_store = repo.object_store object_store.add_object(blob) object_store.add_object(tree) object_store.add_object(commit) # Update HEAD commit_id = commit.get_id(SHA256) repo.refs[b"refs/heads/master"] = commit_id repo.close() # Verify git can read the commit commit_hash = self._run_git(["rev-parse", "HEAD"], cwd=repo_path) commit_hash = commit_hash.strip().decode("ascii") self.assertEqual(len(commit_hash), 64) # SHA256 length # Verify git can show the commit commit_message = self._run_git(["log", "--format=%s", "-n", "1"], cwd=repo_path) self.assertEqual(commit_message.strip(), b"Test SHA256 commit from dulwich") # Verify git can list the tree tree_content = self._run_git(["ls-tree", "HEAD"], cwd=repo_path) self.assertIn(b"interop.txt", tree_content) def test_ref_updates_interop(self): """Test that ref updates work between dulwich and git.""" # Create repo with git repo_path = tempfile.mkdtemp() self.addCleanup(lambda: __import__("shutil").rmtree(repo_path)) self._run_git(["init", "--object-format=sha256", repo_path]) # Create initial commit with git test_file = os.path.join(repo_path, "initial.txt") with open(test_file, "w") as f: f.write("Initial content") self._run_git(["add", "initial.txt"], cwd=repo_path) self._run_git(["commit", "-m", "Initial commit"], cwd=repo_path) initial_commit = self._run_git(["rev-parse", "HEAD"], cwd=repo_path) initial_commit = initial_commit.strip() # Update ref with dulwich repo = Repo(repo_path) # Create new commit with dulwich blob = Blob.from_string(b"New content from dulwich") tree = Tree() tree.add(b"dulwich.txt", 0o100644, blob.get_id(SHA256)) commit = Commit() commit.tree = tree.get_id(SHA256) commit.parents = [initial_commit] commit.author = commit.committer = b"Dulwich User " commit.commit_time = commit.author_time = 1234567891 commit.commit_timezone = commit.author_timezone = 0 commit.message = b"Commit from dulwich" # Add objects and update ref object_store = repo.object_store object_store.add_object(blob) object_store.add_object(tree) object_store.add_object(commit) new_commit_hash = commit.get_id(SHA256) repo.refs[b"refs/heads/master"] = new_commit_hash repo.close() # Verify git sees the update current_commit = self._run_git(["rev-parse", "HEAD"], cwd=repo_path) current_commit = current_commit.strip().decode("ascii") self.assertEqual(current_commit, new_commit_hash.decode("ascii")) # Verify git can access the new tree tree_listing = self._run_git(["ls-tree", "HEAD"], cwd=repo_path) self.assertIn(b"dulwich.txt", tree_listing) def test_clone_sha256_repo_git_to_dulwich(self): """Test cloning a git SHA256 repository with dulwich.""" # Create source repo with git source_path = tempfile.mkdtemp() self.addCleanup(lambda: __import__("shutil").rmtree(source_path)) self._run_git(["init", "--object-format=sha256", source_path]) # Add content test_file = os.path.join(source_path, "clone_test.txt") with open(test_file, "w") as f: f.write("Content to be cloned") self._run_git(["add", "clone_test.txt"], cwd=source_path) self._run_git(["commit", "-m", "Initial commit"], cwd=source_path) # Clone with dulwich target_path = tempfile.mkdtemp() self.addCleanup(lambda: __import__("shutil").rmtree(target_path)) target_repo = Repo.init(target_path, mkdir=False, object_format="sha256") # Copy objects (simplified clone) source_repo = Repo(source_path) # Copy all objects for obj_id in source_repo.object_store: obj = source_repo.object_store[obj_id] target_repo.object_store.add_object(obj) # Copy refs for ref_name in source_repo.refs.keys(): ref_id = source_repo.refs[ref_name] target_repo.refs[ref_name] = ref_id # Set HEAD target_repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master") source_repo.close() target_repo.close() # Verify with git output = self._run_git(["rev-parse", "--show-object-format"], cwd=target_path) self.assertEqual(output.strip(), b"sha256") # Verify content self._run_git(["checkout", "HEAD", "--", "."], cwd=target_path) cloned_file = os.path.join(target_path, "clone_test.txt") with open(cloned_file) as f: content = f.read() self.assertEqual(content, "Content to be cloned") def test_fsck_sha256_repo(self): """Test that git fsck works on dulwich-created SHA256 repos.""" # Create repo with dulwich repo_path = tempfile.mkdtemp() self.addCleanup(lambda: __import__("shutil").rmtree(repo_path)) repo = Repo.init(repo_path, mkdir=False, object_format="sha256") # Create a more complex object graph # Multiple blobs blobs = [] for i in range(5): blob = Blob.from_string(f"Blob content {i}".encode()) repo.object_store.add_object(blob) blobs.append(blob) # Multiple trees subtree = Tree() subtree.add(b"subfile1.txt", 0o100644, blobs[0].get_id(SHA256)) subtree.add(b"subfile2.txt", 0o100644, blobs[1].get_id(SHA256)) repo.object_store.add_object(subtree) main_tree = Tree() main_tree.add(b"file1.txt", 0o100644, blobs[2].get_id(SHA256)) main_tree.add(b"file2.txt", 0o100644, blobs[3].get_id(SHA256)) main_tree.add(b"subdir", 0o040000, subtree.get_id(SHA256)) repo.object_store.add_object(main_tree) # Create commits commit1 = Commit() commit1.tree = main_tree.get_id(SHA256) commit1.author = commit1.committer = b"Test " commit1.commit_time = commit1.author_time = 1234567890 commit1.commit_timezone = commit1.author_timezone = 0 commit1.message = b"First commit" repo.object_store.add_object(commit1) commit2 = Commit() commit2.tree = main_tree.get_id(SHA256) commit2.parents = [commit1.get_id(SHA256)] commit2.author = commit2.committer = b"Test " commit2.commit_time = commit2.author_time = 1234567891 commit2.commit_timezone = commit2.author_timezone = 0 commit2.message = b"Second commit" repo.object_store.add_object(commit2) # Set refs repo.refs[b"refs/heads/master"] = commit2.get_id(SHA256) repo.refs[b"refs/heads/branch1"] = commit1.get_id(SHA256) repo.close() # Run git fsck fsck_output = self._run_git(["fsck", "--full"], cwd=repo_path) # fsck should not report any errors (empty output or success message) self.assertNotIn(b"error", fsck_output.lower()) self.assertNotIn(b"missing", fsck_output.lower()) self.assertNotIn(b"broken", fsck_output.lower())