Skip to content

Commit fe2ae00

Browse files
codexByron
authored andcommitted
Address review feedback about repository setup
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 regressions cover all four cases. This follows Git baseline 15c6308cf7ad276b306aa5b3ababfbdebfb1a917 in setup_git_directory_gently_1() and read_gitfile_gently(). Validation: 11 focused tests plus 12 subtests; Ruff check and format; mypy; compileall; git diff --check.
1 parent ad38516 commit fe2ae00

3 files changed

Lines changed: 63 additions & 17 deletions

File tree

git/repo/base.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -295,23 +295,28 @@ def __init__(
295295
# Walk up the path to find the `.git` dir.
296296
curpath = os.fspath(epath) if epath is not None else ""
297297
git_dir: Optional[str] = None
298+
explicit_git_dir = path is None and git_dir_env is not None
298299
while curpath:
299300
# ABOUT osp.NORMPATH
300301
# It's important to normalize the paths, as submodules will otherwise
301302
# initialize their repo instances with paths that depend on path-portions
302303
# that will not exist after being removed. It's just cleaner.
303-
dotgit = osp.join(curpath, ".git")
304-
sm_gitpath = find_submodule_git_dir(dotgit)
305-
if sm_gitpath is not None:
306-
# Worktrees can use relative paths as of Git 2.48, so join to curpath.
307-
git_dir = osp.normpath(osp.join(curpath, os.fspath(sm_gitpath)))
308-
self._working_tree_dir = curpath
309-
break
304+
if not explicit_git_dir:
305+
dotgit = osp.join(curpath, ".git")
306+
try:
307+
sm_gitpath = find_submodule_git_dir(dotgit)
308+
except OSError:
309+
break
310+
if sm_gitpath is not None:
311+
# Worktrees can use relative paths as of Git 2.48, so join to curpath.
312+
git_dir = osp.normpath(osp.join(curpath, os.fspath(sm_gitpath)))
313+
self._working_tree_dir = curpath
314+
break
310315

311-
# Like Git, do not fall back to a bare repository or parent directory when
312-
# a non-directory .git entry exists but is not a valid gitfile.
313-
if osp.exists(dotgit) and not osp.isdir(dotgit):
314-
break
316+
# Like Git, do not fall back to a bare repository or parent directory when
317+
# a non-directory .git entry exists but is not a valid gitfile.
318+
if osp.exists(dotgit) and not osp.isdir(dotgit):
319+
break
315320

316321
if is_git_dir(curpath):
317322
git_dir = curpath
@@ -349,7 +354,7 @@ def __init__(
349354
self._working_tree_dir = os.getenv("GIT_WORK_TREE")
350355
break
351356

352-
if not search_parent_directories:
357+
if explicit_git_dir or not search_parent_directories:
353358
break
354359
curpath, tail = osp.split(curpath)
355360
if not tail:
@@ -994,7 +999,7 @@ def _get_alternates(self) -> List[str]:
994999
:return:
9951000
List of strings being pathnames of alternates
9961001
"""
997-
alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
1002+
alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
9981003

9991004
if osp.exists(alternates_path):
10001005
with open(alternates_path, "rb") as f:
@@ -1015,7 +1020,7 @@ def _set_alternates(self, alts: List[str]) -> None:
10151020
The method does not check for the existence of the paths in `alts`, as the
10161021
caller is responsible.
10171022
"""
1018-
alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
1023+
alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
10191024
if not alts:
10201025
if osp.isfile(alternates_path):
10211026
os.remove(alternates_path)

git/repo/fun.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,17 @@ def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]:
118118
"""Search for a gitdir for this worktree."""
119119
try:
120120
statbuf = os.stat(dotgit)
121-
except OSError:
121+
except (FileNotFoundError, NotADirectoryError):
122122
return None
123123
if not stat.S_ISREG(statbuf.st_mode) or statbuf.st_size > (1 << 20):
124124
return None
125125

126126
try:
127-
content = os.fsdecode(Path(dotgit).read_bytes()).rstrip("\r\n")
127+
with open(dotgit, "rb") as fp:
128+
content_bytes = fp.read(statbuf.st_size)
129+
if len(content_bytes) != statbuf.st_size:
130+
return None
131+
content = os.fsdecode(content_bytes).rstrip("\r\n")
128132
except (OSError, UnicodeError):
129133
return None
130134
return content[8:] if len(content) >= 9 and content.startswith("gitdir: ") else None

test/test_repo.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from git.exc import UnsafeProtocolError
4242
from git.exc import BadObject
4343
from git.exc import WorkTreeRepositoryUnsupported
44-
from git.repo.fun import touch
44+
from git.repo.fun import find_worktree_git_dir, touch
4545
from git.util import bin_to_hex, cwd, cygpath, join_path_native, rmfile, rmtree
4646

4747
from test.lib import TestBase, fixture, requires_symlinks, with_rw_directory, with_rw_repo, PathLikeMock
@@ -148,6 +148,16 @@ def test_repo_discovery_prefers_dotgit(self):
148148
expected_git_dir = Git(path).rev_parse("--absolute-git-dir")
149149
assert osp.samefile(Repo(path).git_dir, expected_git_dir)
150150

151+
def test_repo_discovery_honors_explicit_git_dir(self):
152+
with tempfile.TemporaryDirectory() as tdir:
153+
git_dir = Path(tdir) / "repo.git"
154+
Repo.init(git_dir, bare=True).close()
155+
Repo.init(git_dir / ".git", bare=True).close()
156+
157+
with mock.patch.dict(os.environ, {"GIT_DIR": os.fspath(git_dir)}):
158+
with Repo() as repo:
159+
assert osp.samefile(repo.git_dir, git_dir)
160+
151161
def test_repo_discovery_rejects_invalid_metadata(self):
152162
with tempfile.TemporaryDirectory() as tdir:
153163
path = Path(tdir)
@@ -192,6 +202,29 @@ def test_repo_discovery_rejects_dangling_commondir(self):
192202

193203
self.assertRaises(InvalidGitRepositoryError, Repo, path)
194204

205+
@requires_symlinks
206+
def test_repo_discovery_rejects_dotgit_stat_errors(self):
207+
with tempfile.TemporaryDirectory() as tdir:
208+
path = Path(tdir)
209+
Repo.init(path).close()
210+
child = path / "child"
211+
child.mkdir()
212+
(child / ".git").symlink_to(".git")
213+
214+
self.assertRaises(InvalidGitRepositoryError, Repo, child, search_parent_directories=True)
215+
216+
def test_gitfile_read_is_bounded(self):
217+
with tempfile.TemporaryDirectory() as tdir:
218+
dotgit = Path(tdir) / ".git"
219+
content = b"gitdir: target\n"
220+
dotgit.write_bytes(content)
221+
reader = mock.mock_open(read_data=b"")
222+
223+
with mock.patch("builtins.open", reader):
224+
assert find_worktree_git_dir(dotgit) is None
225+
226+
reader().read.assert_called_once_with(len(content))
227+
195228
def test_repo_discovery_uses_storage_environment(self):
196229
with tempfile.TemporaryDirectory() as tdir:
197230
git_dir = Path(tdir) / "git"
@@ -251,6 +284,9 @@ def test_repo_discovery_preserves_object_directory(self):
251284
assert osp.samefile(repo.odb.root_path(), object_dir)
252285
assert repo.odb.info(bytes.fromhex(blob_hexsha)).size == len(payload)
253286
assert repo.git.cat_file("blob", blob_hexsha) == payload.decode()
287+
repo.alternates = ["other/location"]
288+
assert repo.alternates == ["other/location"]
289+
assert (object_dir / "info" / "alternates").is_file()
254290

255291
@with_rw_repo("0.3.2.1")
256292
def test_repo_creation_from_different_paths(self, rw_repo):
@@ -519,6 +555,7 @@ def test_alternates_use_common_dir(self, rw_dir):
519555
os.makedirs(osp.join(common_dir, "objects", "info"))
520556
os.makedirs(osp.join(git_dir, "objects", "info"))
521557
repo = mock.Mock(common_dir=common_dir, git_dir=git_dir)
558+
repo.odb.root_path.return_value = osp.join(common_dir, "objects")
522559

523560
alts = ["other/location", "this/location"]
524561
Repo._set_alternates(repo, alts)

0 commit comments

Comments
 (0)