Skip to content

Commit b8c000e

Browse files
codexByron
authored andcommitted
Address review feedback about Gitfile handling
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. Git baseline: 15c6308cf7ad276b306aa5b3ababfbdebfb1a917, setup.c read_gitfile_gently() and get_common_dir_noenv(). Validation: 7 focused tests and 12 subtests; Ruff check and format; mypy; compileall; git diff --check.
1 parent 32baeab commit b8c000e

3 files changed

Lines changed: 45 additions & 34 deletions

File tree

git/repo/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ def __init__(
267267
:class:`Repo`
268268
"""
269269

270-
epath = path or os.getenv("GIT_DIR")
270+
git_dir_env = os.getenv("GIT_DIR")
271+
epath = path or git_dir_env
271272
if not epath:
272273
epath = os.getcwd()
273274
epath = os.fspath(epath)
@@ -361,7 +362,7 @@ def __init__(
361362
self._common_dir = osp.abspath(common_dir_env)
362363
else:
363364
try:
364-
common_dir = (Path(self.git_dir) / "commondir").read_text().splitlines()[0].strip()
365+
common_dir = os.fsdecode((Path(self.git_dir) / "commondir").read_bytes()).rstrip("\r\n")
365366
self._common_dir = osp.join(self.git_dir, common_dir)
366367
except OSError:
367368
self._common_dir = ""
@@ -383,6 +384,8 @@ def __init__(
383384
self.git = self.GitCommandWrapperType(self.working_dir)
384385
if common_dir_env is not None:
385386
self.git.update_environment(GIT_DIR=os.fspath(self.git_dir), GIT_COMMON_DIR=os.fspath(self.common_dir))
387+
elif git_dir_env is not None:
388+
self.git.update_environment(GIT_DIR=os.fspath(self.git_dir))
386389

387390
# Special handling, in special times.
388391
rootpath = osp.join(self.common_dir, "objects")

git/repo/fun.py

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -117,44 +117,33 @@ def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]:
117117
statbuf = os.stat(dotgit)
118118
except OSError:
119119
return None
120-
if not stat.S_ISREG(statbuf.st_mode):
120+
if not stat.S_ISREG(statbuf.st_mode) or statbuf.st_size > (1 << 20):
121121
return None
122122

123123
try:
124-
lines = Path(dotgit).read_text().splitlines()
125-
for key, value in [line.strip().split(": ") for line in lines]:
126-
if key == "gitdir":
127-
return value
128-
except ValueError:
129-
pass
130-
return None
124+
content = os.fsdecode(Path(dotgit).read_bytes()).rstrip("\r\n")
125+
except OSError:
126+
return None
127+
return content[8:] if len(content) >= 9 and content.startswith("gitdir: ") else None
131128

132129

133130
def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
134131
"""Search for a submodule repo."""
135132
if is_git_dir(d):
136133
return d
137134

138-
try:
139-
with open(d) as fp:
140-
content = fp.read().rstrip()
141-
except IOError:
142-
# It's probably not a file.
143-
pass
144-
else:
145-
if content.startswith("gitdir: "):
146-
path = content[8:]
147-
148-
if Git.is_cygwin():
149-
# Cygwin creates submodules prefixed with `/cygdrive/...`.
150-
# Cygwin git understands Cygwin paths much better than Windows ones.
151-
# Also the Cygwin tests are assuming Cygwin paths.
152-
path = cygpath(path)
153-
if not osp.isabs(path):
154-
path = osp.normpath(osp.join(osp.dirname(d), path))
155-
return find_submodule_git_dir(path)
156-
# END handle exception
157-
return None
135+
path = find_worktree_git_dir(d)
136+
if path is None:
137+
return None
138+
139+
if Git.is_cygwin():
140+
# Cygwin creates submodules prefixed with `/cygdrive/...`.
141+
# Cygwin git understands Cygwin paths much better than Windows ones.
142+
# Also the Cygwin tests are assuming Cygwin paths.
143+
path = cygpath(path)
144+
if not osp.isabs(path):
145+
path = osp.normpath(osp.join(osp.dirname(d), path))
146+
return path if is_git_dir(path) else None
158147

159148

160149
def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:

test/test_repo.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,12 @@ def test_repo_discovery_rejects_invalid_metadata(self):
176176
with mock.patch.dict(os.environ, {variable: ""}), self.subTest(metadata=variable):
177177
self.assertRaises(InvalidGitRepositoryError, Repo, path)
178178

179-
(path / ".git").write_text("not a gitfile")
180-
with self.subTest(metadata=".git"):
181-
self.assertRaises(InvalidGitRepositoryError, Repo, path)
179+
for contents in (b"not a gitfile", b"gitdir: \n", b"gitdir: .git\n", b"\xff"):
180+
(path / ".git").write_bytes(contents)
181+
with self.subTest(metadata=".git", contents=contents):
182+
self.assertRaises(InvalidGitRepositoryError, Repo, path)
182183

183-
def test_repo_discovery_uses_git_common_dir(self):
184+
def test_repo_discovery_uses_storage_environment(self):
184185
with tempfile.TemporaryDirectory() as tdir:
185186
git_dir = Path(tdir) / "git"
186187
common_dir = Path(tdir) / "common"
@@ -201,6 +202,24 @@ def test_repo_discovery_uses_git_common_dir(self):
201202
assert osp.samefile(repo.git.rev_parse("--absolute-git-dir"), git_dir)
202203
assert osp.samefile(repo.git.rev_parse("--git-common-dir"), common_dir)
203204

205+
(git_dir / "commondir").write_text("../common\n")
206+
environment = dict(os.environ)
207+
environment["GIT_DIR"] = "git"
208+
environment.pop("GIT_COMMON_DIR", None)
209+
with cwd(tdir), mock.patch.dict(os.environ, environment, clear=True):
210+
repo = Repo()
211+
212+
assert osp.samefile(repo.git.rev_parse("--absolute-git-dir"), git_dir)
213+
214+
if sys.platform.startswith("linux"):
215+
byte_common_dir = Path(tdir) / os.fsdecode(b"common-\xff")
216+
byte_common_dir.mkdir()
217+
(byte_common_dir / "objects").mkdir()
218+
(byte_common_dir / "refs").mkdir()
219+
(git_dir / "commondir").write_bytes(b"../common-\xff\n")
220+
221+
assert osp.samefile(Repo(git_dir).common_dir, byte_common_dir)
222+
204223
@with_rw_repo("0.3.2.1")
205224
def test_repo_creation_from_different_paths(self, rw_repo):
206225
r_from_gitdir = Repo(rw_repo.git_dir)

0 commit comments

Comments
 (0)