Repository: gitpython-developers/GitPython
Pull request #1267 (2021-06-05 10:32:25)

Adds repo.is_valid_object() check.
As discussed in #1266.

Unfortunately `git cat-file --batch-check` doesn't allow for specific object limits like `git cat-file commit --batch-check`,
I did not know this at first and added `cat_file_blob_header` parallel to `cat_file_header`, to basically check a smaller subset of objects.

An improvement that could be made, if the check takes too long on big repos, would be to split the cat_file_header object collection into object collections per type. That way only a subset of objects would be queried after the initial query for the cat_file_header collection.


Patch:
diff --git a/git/repo/base.py b/git/repo/base.py
index 55682411..6cc56031 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -3,12 +3,13 @@
 #
 # This module is part of GitPython and is released under
 # the BSD License: http://www.opensource.org/licenses/bsd-license.php
-
 import logging
 import os
 import re
 import warnings
 
+from gitdb.exc import BadObject
+
 from git.cmd import (
     Git,
     handle_process_output
@@ -618,6 +619,23 @@ class Repo(object):
             raise
         return True
 
+    def is_valid_object(self, sha: str, object_type: str = None) -> bool:
+        try:
+            complete_sha = self.odb.partial_to_complete_sha_hex(sha)
+            object_info = self.odb.info(complete_sha)
+            if object_type:
+                if object_info.type == object_type.encode():
+                    return True
+                else:
+                    log.debug("Commit hash points to an object of type '%s'. Requested were objects of type '%s'",
+                              object_info.type.decode(), object_type)
+                    return False
+            else:
+                return True
+        except BadObject:
+            log.debug("Commit hash is invalid.")
+            return False
+
     def _get_daemon_export(self) -> bool:
         if self.git_dir:
             filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)


Test patch:
diff --git a/test/test_repo.py b/test/test_repo.py
index 04102b01..8aced94d 100644
--- a/test/test_repo.py
+++ b/test/test_repo.py
@@ -989,6 +989,34 @@ class TestRepo(TestBase):
         for i, j in itertools.permutations([c1, 'ffffff', ''], r=2):
             self.assertRaises(GitCommandError, repo.is_ancestor, i, j)
 
+    def test_is_valid_object(self):
+        repo = self.rorepo
+        commit_sha = 'f6aa8d1'
+        blob_sha = '1fbe3e4375'
+        tree_sha = '960b40fe36'
+        tag_sha = '42c2f60c43'
+
+        # Check for valid objects
+        self.assertTrue(repo.is_valid_object(commit_sha))
+        self.assertTrue(repo.is_valid_object(blob_sha))
+        self.assertTrue(repo.is_valid_object(tree_sha))
+        self.assertTrue(repo.is_valid_object(tag_sha))
+
+        # Check for valid objects of specific type
+        self.assertTrue(repo.is_valid_object(commit_sha, 'commit'))
+        self.assertTrue(repo.is_valid_object(blob_sha, 'blob'))
+        self.assertTrue(repo.is_valid_object(tree_sha, 'tree'))
+        self.assertTrue(repo.is_valid_object(tag_sha, 'tag'))
+
+        # Check for invalid objects
+        self.assertFalse(repo.is_valid_object(b'1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a', 'blob'))
+
+        # Check for invalid objects of specific type
+        self.assertFalse(repo.is_valid_object(commit_sha, 'blob'))
+        self.assertFalse(repo.is_valid_object(blob_sha, 'commit'))
+        self.assertFalse(repo.is_valid_object(tree_sha, 'commit'))
+        self.assertFalse(repo.is_valid_object(tag_sha, 'commit'))
+
     @with_rw_directory
     def test_git_work_tree_dotgit(self, rw_dir):
         """Check that we find .git as a worktree file and find the worktree