Skip to content

Commit 3481da9

Browse files
authored
Merge pull request #2218 from gitpython-developers/fix-repo-open
Fix repository discovery precedence
2 parents d160fb4 + 56636c3 commit 3481da9

5 files changed

Lines changed: 334 additions & 90 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.1.59
1+
3.1.60

doc/source/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Security fixes for
99

1010
* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-g5vv-9gxw-82hx
1111
* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-whh4-5q6c-9v3x
12+
* https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-239g-whfq-7xj9
1213

1314
If you can, also try and provide feedback on the upcoming v4 branch
1415
https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome.

git/repo/base.py

Lines changed: 62 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545

4646
from .fun import (
4747
find_submodule_git_dir,
48-
find_worktree_git_dir,
4948
is_git_dir,
5049
rev_parse,
5150
touch,
@@ -234,6 +233,10 @@ def __init__(
234233
) -> None:
235234
R"""Create a new :class:`Repo` instance.
236235
236+
.. note::
237+
Repositories using reftable may be opened, but GitPython's direct reference
238+
access does not support reftable.
239+
237240
:param path:
238241
The path to either the worktree directory or the .git directory itself::
239242
@@ -268,7 +271,11 @@ def __init__(
268271
:class:`Repo`
269272
"""
270273

271-
epath = path or os.getenv("GIT_DIR")
274+
git_dir_env = os.getenv("GIT_DIR")
275+
object_dir_env = os.getenv("GIT_OBJECT_DIRECTORY")
276+
if object_dir_env is not None:
277+
object_dir_env = osp.abspath(object_dir_env)
278+
epath = path or git_dir_env
272279
if not epath:
273280
epath = os.getcwd()
274281
epath = os.fspath(epath)
@@ -290,37 +297,48 @@ def __init__(
290297
raise NoSuchPathError(epath)
291298

292299
# Walk up the path to find the `.git` dir.
293-
curpath = epath
294-
git_dir = None
300+
curpath = os.fspath(epath) if epath is not None else ""
301+
git_dir: Optional[str] = None
302+
explicit_git_dir = not path and bool(git_dir_env)
295303
while curpath:
296304
# ABOUT osp.NORMPATH
297305
# It's important to normalize the paths, as submodules will otherwise
298306
# initialize their repo instances with paths that depend on path-portions
299307
# that will not exist after being removed. It's just cleaner.
300-
if (
301-
osp.isfile(osp.join(curpath, "gitdir"))
302-
and osp.isfile(osp.join(curpath, "commondir"))
303-
and osp.isfile(osp.join(curpath, "HEAD"))
304-
):
305-
git_dir = curpath
306-
307-
if "GIT_WORK_TREE" in os.environ:
308-
self._working_tree_dir = os.getenv("GIT_WORK_TREE")
309-
else:
310-
# Linked worktree administrative directories store the path to the
311-
# worktree's .git file in their gitdir file (without "gitdir: " prefix).
312-
with open(osp.join(git_dir, "gitdir")) as fp:
313-
worktree_gitfile = fp.read().strip()
308+
if not explicit_git_dir:
309+
dotgit = osp.join(curpath, ".git")
310+
try:
311+
sm_gitpath = find_submodule_git_dir(dotgit)
312+
except OSError:
313+
break
314+
if sm_gitpath is not None:
315+
# Worktrees can use relative paths as of Git 2.48, so join to curpath.
316+
git_dir = osp.normpath(osp.join(curpath, os.fspath(sm_gitpath)))
317+
self._working_tree_dir = curpath
318+
break
319+
320+
# Like Git, do not fall back to a bare repository or parent directory when
321+
# a non-directory .git entry exists but is not a valid gitfile.
322+
if osp.exists(dotgit) and not osp.isdir(dotgit):
323+
break
314324

315-
if not osp.isabs(worktree_gitfile):
316-
worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile))
325+
if is_git_dir(curpath):
326+
git_dir = curpath
327+
if osp.isfile(osp.join(curpath, "gitdir")) and osp.isfile(osp.join(curpath, "commondir")):
328+
if "GIT_WORK_TREE" in os.environ:
329+
self._working_tree_dir = os.getenv("GIT_WORK_TREE")
330+
else:
331+
# Linked worktree administrative directories store the path to
332+
# the worktree's .git file in gitdir (without a "gitdir: " prefix).
333+
with open(osp.join(git_dir, "gitdir")) as fp:
334+
worktree_gitfile = fp.read().strip()
317335

318-
self._working_tree_dir = osp.dirname(worktree_gitfile)
336+
if not osp.isabs(worktree_gitfile):
337+
worktree_gitfile = osp.normpath(osp.join(git_dir, worktree_gitfile))
319338

320-
break
339+
self._working_tree_dir = osp.dirname(worktree_gitfile)
340+
break
321341

322-
if is_git_dir(curpath):
323-
git_dir = curpath
324342
# from man git-config : core.worktree
325343
# Set the path to the root of the working tree. If GIT_COMMON_DIR
326344
# environment variable is set, core.worktree is ignored and not used for
@@ -340,22 +358,7 @@ def __init__(
340358
self._working_tree_dir = os.getenv("GIT_WORK_TREE")
341359
break
342360

343-
dotgit = osp.join(curpath, ".git")
344-
sm_gitpath = find_submodule_git_dir(dotgit)
345-
if sm_gitpath is not None:
346-
git_dir = osp.normpath(sm_gitpath)
347-
348-
sm_gitpath = find_submodule_git_dir(dotgit)
349-
if sm_gitpath is None:
350-
sm_gitpath = find_worktree_git_dir(dotgit)
351-
352-
if sm_gitpath is not None:
353-
# worktrees can use relative paths as of Git 2.48, so we join to curpath
354-
git_dir = osp.normpath(osp.join(curpath, sm_gitpath))
355-
self._working_tree_dir = curpath
356-
break
357-
358-
if not search_parent_directories:
361+
if explicit_git_dir or not search_parent_directories:
359362
break
360363
curpath, tail = osp.split(curpath)
361364
if not tail:
@@ -366,19 +369,23 @@ def __init__(
366369
raise InvalidGitRepositoryError(epath)
367370
self.git_dir = git_dir
368371

372+
common_dir_env = os.getenv("GIT_COMMON_DIR")
373+
if common_dir_env is not None:
374+
self._common_dir = osp.abspath(common_dir_env)
375+
else:
376+
try:
377+
common_dir = os.fsdecode((Path(self.git_dir) / "commondir").read_bytes()).rstrip("\r\n")
378+
self._common_dir = osp.join(self.git_dir, common_dir)
379+
except OSError:
380+
self._common_dir = ""
381+
369382
self._bare = False
370383
try:
371384
self._bare = self.config_reader("repository").getboolean("core", "bare")
372385
except Exception:
373386
# Let's not assume the option exists, although it should.
374387
pass
375388

376-
try:
377-
common_dir = (Path(self.git_dir) / "commondir").read_text().splitlines()[0].strip()
378-
self._common_dir = osp.join(self.git_dir, common_dir)
379-
except OSError:
380-
self._common_dir = ""
381-
382389
# Adjust the working directory in case we are actually bare - we didn't know
383390
# that in the first place.
384391
if self._bare:
@@ -387,9 +394,15 @@ def __init__(
387394

388395
self.working_dir: PathLike = self._working_tree_dir or self.common_dir
389396
self.git = self.GitCommandWrapperType(self.working_dir)
397+
if common_dir_env is not None:
398+
self.git.update_environment(GIT_DIR=os.fspath(self.git_dir), GIT_COMMON_DIR=os.fspath(self.common_dir))
399+
elif git_dir_env is not None:
400+
self.git.update_environment(GIT_DIR=os.fspath(self.git_dir))
401+
if object_dir_env is not None:
402+
self.git.update_environment(GIT_OBJECT_DIRECTORY=object_dir_env)
390403

391404
# Special handling, in special times.
392-
rootpath = osp.join(self.common_dir, "objects")
405+
rootpath = object_dir_env if object_dir_env is not None else osp.join(self.common_dir, "objects")
393406
if issubclass(odbt, GitCmdObjectDB):
394407
self.odb = odbt(rootpath, self.git)
395408
else:
@@ -990,7 +1003,7 @@ def _get_alternates(self) -> List[str]:
9901003
:return:
9911004
List of strings being pathnames of alternates
9921005
"""
993-
alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
1006+
alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
9941007

9951008
if osp.exists(alternates_path):
9961009
with open(alternates_path, "rb") as f:
@@ -1011,7 +1024,7 @@ def _set_alternates(self, alts: List[str]) -> None:
10111024
The method does not check for the existence of the paths in `alts`, as the
10121025
caller is responsible.
10131026
"""
1014-
alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
1027+
alternates_path = osp.join(self.odb.root_path(), "info", "alternates")
10151028
if not alts:
10161029
if osp.isfile(alternates_path):
10171030
os.remove(alternates_path)

git/repo/fun.py

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,61 @@ def touch(filename: str) -> str:
6060
def is_git_dir(d: PathLike) -> bool:
6161
"""This is taken from the git setup.c:is_git_directory function.
6262
63+
.. note::
64+
This function recognizes repositories using reftable through their
65+
compatibility files, but GitPython's direct reference access does not support
66+
reftable.
67+
6368
:raise git.exc.WorkTreeRepositoryUnsupported:
6469
If it sees a worktree directory. It's quite hacky to do that here, but at least
6570
clearly indicates that we don't support it. There is the unlikely danger to
6671
throw if we see directories which just look like a worktree dir, but are none.
6772
"""
6873
if osp.isdir(d):
69-
if (osp.isdir(osp.join(d, "objects")) or "GIT_OBJECT_DIRECTORY" in os.environ) and osp.isdir(
70-
osp.join(d, "refs")
71-
):
72-
headref = osp.join(d, "HEAD")
73-
return osp.isfile(headref) or (osp.islink(headref) and os.readlink(headref).startswith("refs"))
74-
elif (
75-
osp.isfile(osp.join(d, "gitdir"))
76-
and osp.isfile(osp.join(d, "commondir"))
77-
and osp.isfile(osp.join(d, "gitfile"))
78-
):
74+
headref = osp.join(d, "HEAD")
75+
if osp.islink(headref):
76+
try:
77+
valid_head = os.readlink(headref).startswith("refs/")
78+
except OSError:
79+
valid_head = False
80+
else:
81+
try:
82+
with open(headref, "rb") as fp:
83+
head = fp.read(256)
84+
except OSError:
85+
valid_head = False
86+
else:
87+
valid_head = (head.startswith(b"ref:") and head[4:].lstrip().startswith(b"refs/")) or bool(
88+
re.match(rb"(?:[0-9A-Fa-f]{64}|[0-9A-Fa-f]{40})", head)
89+
)
90+
91+
common_dir = os.getenv("GIT_COMMON_DIR")
92+
if common_dir == "":
93+
return False
94+
if common_dir is None:
95+
common_dir_file = Path(d) / "commondir"
96+
try:
97+
common_dir = os.fsdecode(common_dir_file.read_bytes()).rstrip("\r\n")
98+
except FileNotFoundError:
99+
if osp.lexists(common_dir_file):
100+
return False
101+
common_dir = os.fspath(d)
102+
except (OSError, UnicodeError):
103+
return False
104+
else:
105+
if not common_dir:
106+
return False
107+
try:
108+
common_dir = osp.realpath(osp.join(d, common_dir))
109+
except (OSError, ValueError):
110+
return False
111+
112+
object_dir = os.getenv("GIT_OBJECT_DIRECTORY")
113+
if object_dir is None:
114+
object_dir = osp.join(common_dir, "objects")
115+
if valid_head and osp.isdir(object_dir) and osp.isdir(osp.join(common_dir, "refs")):
116+
return True
117+
if osp.isfile(osp.join(d, "gitdir")) and osp.isfile(osp.join(d, "commondir")) and osp.isfile(headref):
79118
raise WorkTreeRepositoryUnsupported(d)
80119
return False
81120

@@ -84,46 +123,39 @@ def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]:
84123
"""Search for a gitdir for this worktree."""
85124
try:
86125
statbuf = os.stat(dotgit)
87-
except OSError:
126+
except (FileNotFoundError, NotADirectoryError):
88127
return None
89-
if not stat.S_ISREG(statbuf.st_mode):
128+
if not stat.S_ISREG(statbuf.st_mode) or statbuf.st_size > (1 << 20):
90129
return None
91130

92131
try:
93-
lines = Path(dotgit).read_text().splitlines()
94-
for key, value in [line.strip().split(": ") for line in lines]:
95-
if key == "gitdir":
96-
return value
97-
except ValueError:
98-
pass
99-
return None
132+
with open(dotgit, "rb") as fp:
133+
content_bytes = fp.read(statbuf.st_size)
134+
if len(content_bytes) != statbuf.st_size:
135+
return None
136+
content = os.fsdecode(content_bytes).rstrip("\r\n")
137+
except (OSError, UnicodeError):
138+
return None
139+
return content[8:] if len(content) >= 9 and content.startswith("gitdir: ") else None
100140

101141

102142
def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
103143
"""Search for a submodule repo."""
104144
if is_git_dir(d):
105145
return d
106146

107-
try:
108-
with open(d) as fp:
109-
content = fp.read().rstrip()
110-
except IOError:
111-
# It's probably not a file.
112-
pass
113-
else:
114-
if content.startswith("gitdir: "):
115-
path = content[8:]
116-
117-
if Git.is_cygwin():
118-
# Cygwin creates submodules prefixed with `/cygdrive/...`.
119-
# Cygwin git understands Cygwin paths much better than Windows ones.
120-
# Also the Cygwin tests are assuming Cygwin paths.
121-
path = cygpath(path)
122-
if not osp.isabs(path):
123-
path = osp.normpath(osp.join(osp.dirname(d), path))
124-
return find_submodule_git_dir(path)
125-
# END handle exception
126-
return None
147+
path = find_worktree_git_dir(d)
148+
if path is None:
149+
return None
150+
151+
if Git.is_cygwin():
152+
# Cygwin creates submodules prefixed with `/cygdrive/...`.
153+
# Cygwin git understands Cygwin paths much better than Windows ones.
154+
# Also the Cygwin tests are assuming Cygwin paths.
155+
path = cygpath(path)
156+
if not osp.isabs(path):
157+
path = osp.normpath(osp.join(osp.dirname(d), path))
158+
return path if is_git_dir(path) else None
127159

128160

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

0 commit comments

Comments
 (0)