From c7cf4d13b1ed0a2e70f2a1f3c6b4fc6c2652cf0b Mon Sep 17 00:00:00 2001
From: Byron <sebastian.thiel@icloud.com>
Date: Tue, 25 Aug 2026 04:57:29 +0200
Subject: [PATCH] fix: prefer `.git` during repository discovery

<!-- agent -->
GitPython considered worktree administration and bare-repository signatures
before a worktree's real .git entry. Align discovery with Git so .git files
and directories win, malformed .git files stop discovery, and candidate git
directories validate HEAD plus commondir-backed object and ref storage. This
addresses GHSA-239g-whfq-7xj9.

Regression coverage compares ambiguous layouts with git rev-parse and rejects
invalid HEAD/.git metadata.

Git baseline: 15c6308cf7ad276b306aa5b3ababfbdebfb1a917; setup.c
setup_git_directory_gently_1(), is_git_directory(), and validate_headref().

Repository validation can use GIT_COMMON_DIR for refs and objects. Preserve the
same value on Repo so later config, ref, and object access uses the directory
that made discovery succeed.

Capture GIT_COMMON_DIR before the first repository config read so bare-state
detection uses the same metadata location as discovery. Resolve relative
environment values immediately so later working-directory changes cannot
retarget the Repo.

Review feedback: relative GIT_COMMON_DIR left Git subprocesses resolving GIT_DIR
and GIT_COMMON_DIR from a different working directory; malformed commondir data,
empty GIT_OBJECT_DIRECTORY, and the linked-worktree signature were also handled
inconsistently.

Pin the repository environment to resolved paths, reject invalid
metadata without consulting the process working directory, and
restore HEAD-based linked-worktree detection. Keep the loose HEAD
and dangling .git behavior because both match Git setup.c at baseline
15c6308cf7ad276b306aa5b3ababfbdebfb1a917.

Review feedback identified that chained or self-referential .git pointers
recurse, filesystem-encoded metadata can fail text decoding, and a relative
GIT_DIR is not retained for later Git commands.

Parse one regular, size-bounded Gitfile exactly once, decode Gitfile and
commondir paths with the filesystem codec, and retain the resolved GIT_DIR
for subprocesses. This rejects cycles like Git instead of recursing and keeps
commands stable after working-directory changes.

Review feedback noted that a dangling commondir symlink was treated as absent,
allowing local objects and refs to validate the repository.

Distinguish a truly missing commondir from a dangling symlink. This follows
Git's get_common_dir_noenv(), whose file_exists check uses lstat before
attempting to read the entry.

Python 3.9 on Windows raised UnicodeDecodeError while the repository-discovery
regression test parsed invalid commondir bytes, causing the Python package test
(windows, 3.9) check to fail.

Treat UnicodeError like an unreadable metadata file in both commondir and
gitfile parsing. Invalid bytes now make discovery reject the candidate
repository, matching Git's behavior.

Review feedback noted that GIT_OBJECT_DIRECTORY made discovery succeed without
becoming the Repo ODB root, while a relative value could later be resolved from
the Git wrapper's different working directory.

Resolve the environment value against the construction directory, use it as the
ODB root, and preserve the absolute value for later Git commands. The regression
moves the only object store outside the git directory and verifies access
through both GitDB and git cat-file after the original environment and current
directory are restored.

Review feedback identified four setup mismatches: non-missing .git stat failures
could fall through to another repository, explicit GIT_DIR could be redirected
through a nested .git entry, Gitfile reads were not bounded to the stat-reported
size, and alternates ignored GIT_OBJECT_DIRECTORY.

Match Git setup.c by bypassing discovery for an environment-selected GIT_DIR,
stopping discovery when Gitfile stat fails for reasons other than a missing
path, and reading exactly the previously observed Gitfile size. Resolve
alternates below the active ODB root so custom object stores remain internally
consistent.

The commit review noted that treating every present GIT_DIR as explicit broke
two documented cases: an empty GIT_DIR must fall back to current-directory
discovery, while an empty Repo path must still use a nonempty GIT_DIR.

Assisted-by: GPT 5.6
Co-authored-by: GPT 5.6 <codex@openai.com>
---
 doc/source/changes.rst |   1 +
 git/repo/base.py       | 111 +++++++++++++----------
 git/repo/fun.py        | 110 +++++++++++++++--------
 test/test_repo.py      | 200 ++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 333 insertions(+), 89 deletions(-)

Index: GitPython-3.1.59/git/repo/base.py
===================================================================
--- GitPython-3.1.59.orig/git/repo/base.py
+++ GitPython-3.1.59/git/repo/base.py
@@ -45,7 +45,6 @@ from git.util import (
 
 from .fun import (
     find_submodule_git_dir,
-    find_worktree_git_dir,
     is_git_dir,
     rev_parse,
     touch,
@@ -232,6 +231,10 @@ class Repo:
     ) -> None:
         R"""Create a new :class:`Repo` instance.
 
+        .. note::
+            Repositories using reftable may be opened, but GitPython's direct reference
+            access does not support reftable.
+
         :param path:
             The path to either the worktree directory or the .git directory itself::
 
@@ -266,7 +269,11 @@ class Repo:
             :class:`Repo`
         """
 
-        epath = path or os.getenv("GIT_DIR")
+        git_dir_env = os.getenv("GIT_DIR")
+        object_dir_env = os.getenv("GIT_OBJECT_DIRECTORY")
+        if object_dir_env is not None:
+            object_dir_env = osp.abspath(object_dir_env)
+        epath = path or git_dir_env
         if not epath:
             epath = os.getcwd()
         epath = os.fspath(epath)
@@ -288,37 +295,48 @@ class Repo:
                 raise NoSuchPathError(epath)
 
         # Walk up the path to find the `.git` dir.
-        curpath = epath
-        git_dir = None
+        curpath = os.fspath(epath) if epath is not None else ""
+        git_dir: Optional[str] = None
+        explicit_git_dir = not path and bool(git_dir_env)
         while curpath:
             # ABOUT osp.NORMPATH
             # It's important to normalize the paths, as submodules will otherwise
             # initialize their repo instances with paths that depend on path-portions
             # that will not exist after being removed. It's just cleaner.
-            if (
-                osp.isfile(osp.join(curpath, "gitdir"))
-                and osp.isfile(osp.join(curpath, "commondir"))
-                and osp.isfile(osp.join(curpath, "HEAD"))
-            ):
-                git_dir = curpath
+            if not explicit_git_dir:
+                dotgit = osp.join(curpath, ".git")
+                try:
+                    sm_gitpath = find_submodule_git_dir(dotgit)
+                except OSError:
+                    break
+                if sm_gitpath is not None:
+                    # Worktrees can use relative paths as of Git 2.48, so join to curpath.
+                    git_dir = osp.normpath(osp.join(curpath, os.fspath(sm_gitpath)))
+                    self._working_tree_dir = curpath
+                    break
+
+                # Like Git, do not fall back to a bare repository or parent directory when
+                # a non-directory .git entry exists but is not a valid gitfile.
+                if osp.exists(dotgit) and not osp.isdir(dotgit):
+                    break
 
-                if "GIT_WORK_TREE" in os.environ:
-                    self._working_tree_dir = os.getenv("GIT_WORK_TREE")
-                else:
-                    # Linked worktree administrative directories store the path to the
-                    # worktree's .git file in their gitdir file (without "gitdir: " prefix).
-                    with open(osp.join(git_dir, "gitdir")) as fp:
-                        worktree_gitfile = fp.read().strip()
-
-                    if not osp.isabs(worktree_gitfile):
-                        worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile))
+            if is_git_dir(curpath):
+                git_dir = curpath
+                if osp.isfile(osp.join(curpath, "gitdir")) and osp.isfile(osp.join(curpath, "commondir")):
+                    if "GIT_WORK_TREE" in os.environ:
+                        self._working_tree_dir = os.getenv("GIT_WORK_TREE")
+                    else:
+                        # Linked worktree administrative directories store the path to
+                        # the worktree's .git file in gitdir (without a "gitdir: " prefix).
+                        with open(osp.join(git_dir, "gitdir")) as fp:
+                            worktree_gitfile = fp.read().strip()
 
-                    self._working_tree_dir = osp.dirname(worktree_gitfile)
+                        if not osp.isabs(worktree_gitfile):
+                            worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile))
 
-                break
+                        self._working_tree_dir = osp.dirname(worktree_gitfile)
+                    break
 
-            if is_git_dir(curpath):
-                git_dir = curpath
                 # from man git-config : core.worktree
                 # Set the path to the root of the working tree. If GIT_COMMON_DIR
                 # environment variable is set, core.worktree is ignored and not used for
@@ -338,22 +356,7 @@ class Repo:
                     self._working_tree_dir = os.getenv("GIT_WORK_TREE")
                 break
 
-            dotgit = osp.join(curpath, ".git")
-            sm_gitpath = find_submodule_git_dir(dotgit)
-            if sm_gitpath is not None:
-                git_dir = osp.normpath(sm_gitpath)
-
-            sm_gitpath = find_submodule_git_dir(dotgit)
-            if sm_gitpath is None:
-                sm_gitpath = find_worktree_git_dir(dotgit)
-
-            if sm_gitpath is not None:
-                # worktrees can use relative paths as of Git 2.48, so we join to curpath
-                git_dir = osp.normpath(osp.join(curpath, sm_gitpath))
-                self._working_tree_dir = curpath
-                break
-
-            if not search_parent_directories:
+            if explicit_git_dir or not search_parent_directories:
                 break
             curpath, tail = osp.split(curpath)
             if not tail:
@@ -364,6 +367,16 @@ class Repo:
             raise InvalidGitRepositoryError(epath)
         self.git_dir = git_dir
 
+        common_dir_env = os.getenv("GIT_COMMON_DIR")
+        if common_dir_env is not None:
+            self._common_dir = osp.abspath(common_dir_env)
+        else:
+            try:
+                common_dir = os.fsdecode((Path(self.git_dir) / "commondir").read_bytes()).rstrip("\r\n")
+                self._common_dir = osp.join(self.git_dir, common_dir)
+            except OSError:
+                self._common_dir = ""
+
         self._bare = False
         try:
             self._bare = self.config_reader("repository").getboolean("core", "bare")
@@ -371,12 +384,6 @@ class Repo:
             # Let's not assume the option exists, although it should.
             pass
 
-        try:
-            common_dir = (Path(self.git_dir) / "commondir").read_text().splitlines()[0].strip()
-            self._common_dir = osp.join(self.git_dir, common_dir)
-        except OSError:
-            self._common_dir = ""
-
         # Adjust the working directory in case we are actually bare - we didn't know
         # that in the first place.
         if self._bare:
@@ -385,9 +392,15 @@ class Repo:
 
         self.working_dir: PathLike = self._working_tree_dir or self.common_dir
         self.git = self.GitCommandWrapperType(self.working_dir)
+        if common_dir_env is not None:
+            self.git.update_environment(GIT_DIR=os.fspath(self.git_dir), GIT_COMMON_DIR=os.fspath(self.common_dir))
+        elif git_dir_env is not None:
+            self.git.update_environment(GIT_DIR=os.fspath(self.git_dir))
+        if object_dir_env is not None:
+            self.git.update_environment(GIT_OBJECT_DIRECTORY=object_dir_env)
 
         # Special handling, in special times.
-        rootpath = osp.join(self.common_dir, "objects")
+        rootpath = object_dir_env if object_dir_env is not None else osp.join(self.common_dir, "objects")
         if issubclass(odbt, GitCmdObjectDB):
             self.odb = odbt(rootpath, self.git)
         else:
@@ -988,7 +1001,7 @@ class Repo:
         :return:
             List of strings being pathnames of alternates
         """
-        alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
+        alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
 
         if osp.exists(alternates_path):
             with open(alternates_path, "rb") as f:
@@ -1009,7 +1022,7 @@ class Repo:
             The method does not check for the existence of the paths in `alts`, as the
             caller is responsible.
         """
-        alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
+        alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
         if not alts:
             if osp.isfile(alternates_path):
                 os.remove(alternates_path)
Index: GitPython-3.1.59/git/repo/fun.py
===================================================================
--- GitPython-3.1.59.orig/git/repo/fun.py
+++ GitPython-3.1.59/git/repo/fun.py
@@ -60,22 +60,61 @@ def touch(filename: str) -> str:
 def is_git_dir(d: PathLike) -> bool:
     """This is taken from the git setup.c:is_git_directory function.
 
+    .. note::
+        This function recognizes repositories using reftable through their
+        compatibility files, but GitPython's direct reference access does not support
+        reftable.
+
     :raise git.exc.WorkTreeRepositoryUnsupported:
         If it sees a worktree directory. It's quite hacky to do that here, but at least
         clearly indicates that we don't support it. There is the unlikely danger to
         throw if we see directories which just look like a worktree dir, but are none.
     """
     if osp.isdir(d):
-        if (osp.isdir(osp.join(d, "objects")) or "GIT_OBJECT_DIRECTORY" in os.environ) and osp.isdir(
-            osp.join(d, "refs")
-        ):
-            headref = osp.join(d, "HEAD")
-            return osp.isfile(headref) or (osp.islink(headref) and os.readlink(headref).startswith("refs"))
-        elif (
-            osp.isfile(osp.join(d, "gitdir"))
-            and osp.isfile(osp.join(d, "commondir"))
-            and osp.isfile(osp.join(d, "gitfile"))
-        ):
+        headref = osp.join(d, "HEAD")
+        if osp.islink(headref):
+            try:
+                valid_head = os.readlink(headref).startswith("refs/")
+            except OSError:
+                valid_head = False
+        else:
+            try:
+                with open(headref, "rb") as fp:
+                    head = fp.read(256)
+            except OSError:
+                valid_head = False
+            else:
+                valid_head = (head.startswith(b"ref:") and head[4:].lstrip().startswith(b"refs/")) or bool(
+                    re.match(rb"(?:[0-9A-Fa-f]{64}|[0-9A-Fa-f]{40})", head)
+                )
+
+        common_dir = os.getenv("GIT_COMMON_DIR")
+        if common_dir == "":
+            return False
+        if common_dir is None:
+            common_dir_file = Path(d) / "commondir"
+            try:
+                common_dir = os.fsdecode(common_dir_file.read_bytes()).rstrip("\r\n")
+            except FileNotFoundError:
+                if osp.lexists(common_dir_file):
+                    return False
+                common_dir = os.fspath(d)
+            except (OSError, UnicodeError):
+                return False
+            else:
+                if not common_dir:
+                    return False
+                try:
+                    common_dir = osp.realpath(osp.join(d, common_dir))
+                except (OSError, ValueError):
+                    return False
+
+        object_dir = os.getenv("GIT_OBJECT_DIRECTORY")
+        if object_dir is None:
+            object_dir = osp.join(common_dir, "objects")
+        if valid_head and osp.isdir(object_dir) and osp.isdir(osp.join(common_dir, "refs")):
+            return True
+        if osp.isfile(osp.join(d, "gitdir")) and osp.isfile(osp.join(d, "commondir")) and osp.isfile(headref):
             raise WorkTreeRepositoryUnsupported(d)
     return False
 
@@ -84,19 +123,20 @@ def find_worktree_git_dir(dotgit: PathLi
     """Search for a gitdir for this worktree."""
     try:
         statbuf = os.stat(dotgit)
-    except OSError:
+    except (FileNotFoundError, NotADirectoryError):
         return None
-    if not stat.S_ISREG(statbuf.st_mode):
+    if not stat.S_ISREG(statbuf.st_mode) or statbuf.st_size > (1 << 20):
         return None
 
     try:
-        lines = Path(dotgit).read_text().splitlines()
-        for key, value in [line.strip().split(": ") for line in lines]:
-            if key == "gitdir":
-                return value
-    except ValueError:
-        pass
-    return None
+        with open(dotgit, "rb") as fp:
+            content_bytes = fp.read(statbuf.st_size)
+        if len(content_bytes) != statbuf.st_size:
+            return None
+        content = os.fsdecode(content_bytes).rstrip("\r\n")
+    except (OSError, UnicodeError):
+        return None
+    return content[8:] if len(content) >= 9 and content.startswith("gitdir: ") else None
 
 
 def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
@@ -104,26 +144,18 @@ def find_submodule_git_dir(d: PathLike)
     if is_git_dir(d):
         return d
 
-    try:
-        with open(d) as fp:
-            content = fp.read().rstrip()
-    except IOError:
-        # It's probably not a file.
-        pass
-    else:
-        if content.startswith("gitdir: "):
-            path = content[8:]
-
-            if Git.is_cygwin():
-                # Cygwin creates submodules prefixed with `/cygdrive/...`.
-                # Cygwin git understands Cygwin paths much better than Windows ones.
-                # Also the Cygwin tests are assuming Cygwin paths.
-                path = cygpath(path)
-            if not osp.isabs(path):
-                path = osp.normpath(osp.join(osp.dirname(d), path))
-            return find_submodule_git_dir(path)
-    # END handle exception
-    return None
+    path = find_worktree_git_dir(d)
+    if path is None:
+        return None
+
+    if Git.is_cygwin():
+        # Cygwin creates submodules prefixed with `/cygdrive/...`.
+        # Cygwin git understands Cygwin paths much better than Windows ones.
+        # Also the Cygwin tests are assuming Cygwin paths.
+        path = cygpath(path)
+    if not osp.isabs(path):
+        path = osp.normpath(osp.join(osp.dirname(d), path))
+    return path if is_git_dir(path) else None
 
 
 def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
Index: GitPython-3.1.59/test/test_repo.py
===================================================================
--- GitPython-3.1.59.orig/test/test_repo.py
+++ GitPython-3.1.59/test/test_repo.py
@@ -40,7 +40,8 @@ from git import (
 from git.exc import UnsafeOptionError
 from git.exc import UnsafeProtocolError
 from git.exc import BadObject
-from git.repo.fun import touch
+from git.exc import WorkTreeRepositoryUnsupported
+from git.repo.fun import find_worktree_git_dir, touch
 from git.util import bin_to_hex, cwd, cygpath, join_path_native, rmfile, rmtree
 
 from test.lib import TestBase, fixture, requires_symlinks, with_rw_directory, with_rw_repo, PathLikeMock
@@ -122,6 +123,178 @@ class TestRepo(TestBase):
             nonexistent = osp.join(tdir, "foobar")
             self.assertRaises(NoSuchPathError, Repo, nonexistent)
 
+    def test_repo_discovery_prefers_dotgit(self):
+        layouts = {
+            "linked-worktree": {
+                "gitdir": ".git\n",
+                "commondir": ".git\n",
+                "HEAD": "ref: refs/heads/main\n",
+            },
+            "bare": {"objects": None, "refs": None, "HEAD": "ref: refs/heads/main\n"},
+        }
+
+        with tempfile.TemporaryDirectory() as tdir:
+            for name, entries in layouts.items():
+                path = Path(tdir) / name
+                Repo.init(path).close()
+                for entry, contents in entries.items():
+                    item = path / entry
+                    if contents is None:
+                        item.mkdir()
+                    else:
+                        item.write_text(contents)
+
+                with self.subTest(layout=name):
+                    expected_git_dir = Git(path).rev_parse("--absolute-git-dir")
+                    assert osp.samefile(Repo(path).git_dir, expected_git_dir)
+
+    def test_repo_discovery_honors_explicit_git_dir(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            git_dir = Path(tdir) / "repo.git"
+            Repo.init(git_dir, bare=True).close()
+            Repo.init(git_dir / ".git", bare=True).close()
+
+            with mock.patch.dict(os.environ, {"GIT_DIR": os.fspath(git_dir)}):
+                for path in (None, ""):
+                    with Repo(path) as repo, self.subTest(path=path):
+                        assert osp.samefile(repo.git_dir, git_dir)
+
+            worktree = Path(tdir) / "worktree"
+            Repo.init(worktree).close()
+            with cwd(worktree), mock.patch.dict(os.environ, {"GIT_DIR": ""}):
+                with Repo() as repo:
+                    assert osp.samefile(repo.git_dir, worktree / ".git")
+
+    def test_repo_discovery_rejects_invalid_metadata(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            path = Path(tdir)
+            (path / "objects").mkdir()
+            (path / "refs").mkdir()
+            (path / "HEAD").write_text("not a ref")
+
+            with self.subTest(metadata="HEAD"):
+                self.assertRaises(InvalidGitRepositoryError, Repo, path)
+
+            (path / "HEAD").write_text("ref: refs/heads/main\n")
+
+            for contents in (b"", b"\xff"):
+                (path / "commondir").write_bytes(contents)
+                with cwd(path), self.subTest(metadata="commondir", contents=contents):
+                    self.assertRaises(InvalidGitRepositoryError, Repo, path)
+
+            (path / "gitdir").write_text("../worktree/.git\n")
+            (path / "commondir").write_text("missing\n")
+            with self.subTest(metadata="linked-worktree"):
+                self.assertRaises(WorkTreeRepositoryUnsupported, Repo, path)
+
+            (path / "gitdir").unlink()
+            (path / "commondir").unlink()
+            for variable in ("GIT_COMMON_DIR", "GIT_OBJECT_DIRECTORY"):
+                with mock.patch.dict(os.environ, {variable: ""}), self.subTest(metadata=variable):
+                    self.assertRaises(InvalidGitRepositoryError, Repo, path)
+
+            for contents in (b"not a gitfile", b"gitdir: \n", b"gitdir: .git\n", b"\xff"):
+                (path / ".git").write_bytes(contents)
+                with self.subTest(metadata=".git", contents=contents):
+                    self.assertRaises(InvalidGitRepositoryError, Repo, path)
+
+    @requires_symlinks
+    def test_repo_discovery_rejects_dangling_commondir(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            path = Path(tdir)
+            (path / "objects").mkdir()
+            (path / "refs").mkdir()
+            (path / "HEAD").write_text("ref: refs/heads/main\n")
+            (path / "commondir").symlink_to("missing")
+
+            self.assertRaises(InvalidGitRepositoryError, Repo, path)
+
+    @requires_symlinks
+    def test_repo_discovery_rejects_dotgit_stat_errors(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            path = Path(tdir)
+            Repo.init(path).close()
+            child = path / "child"
+            child.mkdir()
+            (child / ".git").symlink_to(".git")
+
+            self.assertRaises(InvalidGitRepositoryError, Repo, child, search_parent_directories=True)
+
+    def test_gitfile_read_is_bounded(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            dotgit = Path(tdir) / ".git"
+            content = b"gitdir: target\n"
+            dotgit.write_bytes(content)
+            reader = mock.mock_open(read_data=b"")
+
+            with mock.patch("builtins.open", reader):
+                assert find_worktree_git_dir(dotgit) is None
+
+            reader().read.assert_called_once_with(len(content))
+
+    def test_repo_discovery_uses_storage_environment(self):
+        with tempfile.TemporaryDirectory() as tdir:
+            git_dir = Path(tdir) / "git"
+            common_dir = Path(tdir) / "common"
+            git_dir.mkdir()
+            common_dir.mkdir()
+            (git_dir / "HEAD").write_text("ref: refs/heads/main\n")
+            (common_dir / "objects").mkdir()
+            (common_dir / "refs").mkdir()
+            (common_dir / "config").write_text("[core]\n\tbare = true\n")
+
+            with cwd(tdir):
+                with mock.patch.dict(os.environ, {"GIT_DIR": "git", "GIT_COMMON_DIR": "common"}):
+                    repo = Repo()
+
+            assert osp.samefile(repo.common_dir, common_dir)
+            assert osp.samefile(repo.odb.root_path(), common_dir / "objects")
+            assert repo.bare
+            assert osp.samefile(repo.git.rev_parse("--absolute-git-dir"), git_dir)
+            assert osp.samefile(repo.git.rev_parse("--git-common-dir"), common_dir)
+
+            (git_dir / "commondir").write_text("../common\n")
+            environment = dict(os.environ)
+            environment["GIT_DIR"] = "git"
+            environment.pop("GIT_COMMON_DIR", None)
+            with cwd(tdir), mock.patch.dict(os.environ, environment, clear=True):
+                repo = Repo()
+
+            assert osp.samefile(repo.git.rev_parse("--absolute-git-dir"), git_dir)
+
+            if sys.platform.startswith("linux"):
+                byte_common_dir = Path(tdir) / os.fsdecode(b"common-\xff")
+                byte_common_dir.mkdir()
+                (byte_common_dir / "objects").mkdir()
+                (byte_common_dir / "refs").mkdir()
+                (git_dir / "commondir").write_bytes(b"../common-\xff\n")
+
+                assert osp.samefile(Repo(git_dir).common_dir, byte_common_dir)
+
+    @with_rw_directory
+    def test_repo_discovery_preserves_object_directory(self, tdir):
+        git_dir = Path(tdir) / "git"
+        payload = b"custom object database"
+        payload_file = Path(tdir) / "payload"
+        payload_file.write_bytes(payload)
+
+        source_repo = Repo.init(git_dir, bare=True)
+        blob_hexsha = source_repo.git.hash_object("-w", payload_file)
+        source_repo.close()
+        object_dir = Path(tdir) / "objects"
+        (git_dir / "objects").rename(object_dir)
+
+        with cwd(tdir), mock.patch.dict(os.environ, {"GIT_DIR": "git", "GIT_OBJECT_DIRECTORY": "objects"}):
+            repo = Repo(odbt=GitDB)
+
+        with repo:
+            assert osp.samefile(repo.odb.root_path(), object_dir)
+            assert repo.odb.has_object(bytes.fromhex(blob_hexsha))
+            assert repo.git.cat_file("blob", blob_hexsha) == payload.decode()
+            repo.alternates = ["other/location"]
+            assert repo.alternates == ["other/location"]
+            assert (object_dir / "info" / "alternates").is_file()
+
     @with_rw_repo("0.3.2.1")
     def test_repo_creation_from_different_paths(self, rw_repo):
         r_from_gitdir = Repo(rw_repo.git_dir)
@@ -389,6 +562,7 @@ class TestRepo(TestBase):
         os.makedirs(osp.join(common_dir, "objects", "info"))
         os.makedirs(osp.join(git_dir, "objects", "info"))
         repo = mock.Mock(common_dir=common_dir, git_dir=git_dir)
+        repo.odb.root_path.return_value = osp.join(common_dir, "objects")
 
         alts = ["other/location", "this/location"]
         Repo._set_alternates(repo, alts)
@@ -1148,6 +1322,30 @@ class TestRepo(TestBase):
         )
 
     @with_rw_directory
+    def test_reftable_repo_opens_but_direct_refs_are_unsupported(self, rw_dir):
+        git = Git(rw_dir)
+        try:
+            git.init(ref_format="reftable")
+        except GitCommandError as err:
+            if err.status == 129:
+                pytest.skip("git init --ref-format is not supported by this git version")
+            raise
+
+        git.update_environment(
+            GIT_AUTHOR_NAME="Test Author",
+            GIT_AUTHOR_EMAIL="author@example.com",
+            GIT_COMMITTER_NAME="Test Committer",
+            GIT_COMMITTER_EMAIL="committer@example.com",
+        )
+        git.commit(allow_empty=True, message="initial commit")
+        expected_head = git.rev_parse("HEAD")
+
+        repo = Repo(rw_dir)
+        assert repo.git.rev_parse("HEAD") == expected_head
+        assert repo.head.reference.name == ".invalid"
+        assert not repo.heads
+
+    @with_rw_directory
     def test_active_branch_raises_type_error_when_head_is_detached(self, rw_dir):
         repo = Repo.init(rw_dir)
         with open(osp.join(rw_dir, "a.txt"), "w") as f:
