Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion git/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ def _included_paths(self) -> List[Tuple[str, str]]:
value,
)
if self._repo.git_dir:
if fnmatch.fnmatchcase(str(self._repo.git_dir), value):
if fnmatch.fnmatchcase(os.fspath(self._repo.git_dir), value):
paths += self.items(section)

elif keyword == "onbranch":
Expand Down
12 changes: 6 additions & 6 deletions git/index/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ def raise_exc(e: Exception) -> NoReturn:
r = str(self.repo.working_tree_dir)
rs = r + os.sep
for path in paths:
abs_path = str(path)
abs_path = os.fspath(path)
if not osp.isabs(abs_path):
abs_path = osp.join(r, path)
# END make absolute path
Expand Down Expand Up @@ -656,10 +656,10 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
return path
if self.repo.bare:
raise InvalidGitRepositoryError("require non-bare repository")
if not osp.normpath(str(path)).startswith(str(self.repo.working_tree_dir)):
if not osp.normpath(path).startswith(str(self.repo.working_tree_dir)):
raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
result = os.path.relpath(path, self.repo.working_tree_dir)
if str(path).endswith(os.sep) and not result.endswith(os.sep):
if os.fspath(path).endswith(os.sep) and not result.endswith(os.sep):
result += os.sep
return result

Expand Down Expand Up @@ -1036,7 +1036,7 @@ def remove(
args.append("--")

# Preprocess paths.
paths = self._items_to_rela_paths(items)
paths = list(map(os.fspath, self._items_to_rela_paths(items))) # type: ignore[arg-type]
removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()

# Process output to gain proper paths.
Expand Down Expand Up @@ -1359,11 +1359,11 @@ def make_exc() -> GitCommandError:
try:
self.entries[(co_path, 0)]
except KeyError:
folder = str(co_path)
folder = co_path
if not folder.endswith("/"):
folder += "/"
for entry in self.entries.values():
if str(entry.path).startswith(folder):
if os.fspath(entry.path).startswith(folder):
p = entry.path
self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False)
checked_out_files.append(p)
Expand Down
2 changes: 1 addition & 1 deletion git/index/fun.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
return

env = os.environ.copy()
env["GIT_INDEX_FILE"] = safe_decode(str(index.path))
env["GIT_INDEX_FILE"] = safe_decode(os.fspath(index.path))
env["GIT_EDITOR"] = ":"
cmd = [hp]
try:
Expand Down
4 changes: 2 additions & 2 deletions git/index/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

# typing ----------------------------------------------------------------------

from typing import Any, Callable, TYPE_CHECKING, Optional, Type
from typing import Any, Callable, TYPE_CHECKING, Optional, Type, cast

from git.types import Literal, PathLike, _T

Expand Down Expand Up @@ -106,7 +106,7 @@ def git_working_dir(func: Callable[..., _T]) -> Callable[..., _T]:
@wraps(func)
def set_git_working_dir(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
cur_wd = os.getcwd()
os.chdir(str(self.repo.working_tree_dir))
os.chdir(cast(PathLike, self.repo.working_tree_dir))
try:
return func(self, *args, **kwargs)
finally:
Expand Down
3 changes: 2 additions & 1 deletion git/objects/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
__all__ = ["Blob"]

from mimetypes import guess_type
import os
import sys

if sys.version_info >= (3, 8):
Expand Down Expand Up @@ -44,5 +45,5 @@ def mime_type(self) -> str:
"""
guesses = None
if self.path:
guesses = guess_type(str(self.path))
guesses = guess_type(os.fspath(self.path))
return guesses and guesses[0] or self.DEFAULT_MIME_TYPE
4 changes: 2 additions & 2 deletions git/objects/submodule/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def _clone_repo(
module_abspath_dir = osp.dirname(module_abspath)
if not osp.isdir(module_abspath_dir):
os.makedirs(module_abspath_dir)
module_checkout_path = osp.join(str(repo.working_tree_dir), path)
module_checkout_path = osp.join(repo.working_tree_dir, path) # type: ignore[arg-type]

if url.startswith("../"):
remote_name = cast("RemoteReference", repo.active_branch.tracking_branch()).remote_name
Expand Down Expand Up @@ -541,7 +541,7 @@ def add(
if sm.exists():
# Reretrieve submodule from tree.
try:
sm = repo.head.commit.tree[str(path)]
sm = repo.head.commit.tree[os.fspath(path)]
sm._name = name
return sm
except KeyError:
Expand Down
3 changes: 2 additions & 1 deletion git/refs/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

__all__ = ["Reference"]

import os
from git.util import IterableObj, LazyMixin

from .symbolic import SymbolicReference, T_References
Expand Down Expand Up @@ -65,7 +66,7 @@ def __init__(self, repo: "Repo", path: PathLike, check_path: bool = True) -> Non
If ``False``, you can provide any path.
Otherwise the path must start with the default path prefix of this type.
"""
if check_path and not str(path).startswith(self._common_path_default + "/"):
if check_path and not os.fspath(path).startswith(self._common_path_default + "/"):
raise ValueError(f"Cannot instantiate {self.__class__.__name__!r} from path {path}")
self.path: str # SymbolicReference converts to string at the moment.
super().__init__(repo, path)
Expand Down
21 changes: 11 additions & 10 deletions git/refs/symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
__all__ = ["SymbolicReference"]

import os
from pathlib import Path

from gitdb.exc import BadName, BadObject

Expand Down Expand Up @@ -76,10 +77,10 @@ class SymbolicReference:

def __init__(self, repo: "Repo", path: PathLike, check_path: bool = False) -> None:
self.repo = repo
self.path = path
self.path: PathLike = path

def __str__(self) -> str:
return str(self.path)
return os.fspath(self.path)

def __repr__(self) -> str:
return '<git.%s "%s">' % (self.__class__.__name__, self.path)
Expand All @@ -103,7 +104,7 @@ def name(self) -> str:
In case of symbolic references, the shortest assumable name is the path
itself.
"""
return str(self.path)
return os.fspath(self.path)

@property
def abspath(self) -> PathLike:
Expand Down Expand Up @@ -178,7 +179,7 @@ def _check_ref_name_valid(ref_path: PathLike) -> None:
"""
previous: Union[str, None] = None
one_before_previous: Union[str, None] = None
for c in str(ref_path):
for c in os.fspath(ref_path):
if c in " ~^:?*[\\":
raise ValueError(
f"Invalid reference '{ref_path}': references cannot contain spaces, tildes (~), carets (^),"
Expand Down Expand Up @@ -212,7 +213,7 @@ def _check_ref_name_valid(ref_path: PathLike) -> None:
raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a forward slash (/)")
elif previous == "@" and one_before_previous is None:
raise ValueError(f"Invalid reference '{ref_path}': references cannot be '@'")
elif any(component.endswith(".lock") for component in str(ref_path).split("/")):
elif any(component.endswith(".lock") for component in Path(ref_path).parts):
raise ValueError(
f"Invalid reference '{ref_path}': references cannot have slash-separated components that end with"
" '.lock'"
Expand All @@ -235,7 +236,7 @@ def _get_ref_info_helper(
tokens: Union[None, List[str], Tuple[str, str]] = None
repodir = _git_dir(repo, ref_path)
try:
with open(os.path.join(repodir, str(ref_path)), "rt", encoding="UTF-8") as fp:
with open(os.path.join(repodir, ref_path), "rt", encoding="UTF-8") as fp: # type: ignore[arg-type]
value = fp.read().rstrip()
# Don't only split on spaces, but on whitespace, which allows to parse lines like:
# 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo
Expand Down Expand Up @@ -614,7 +615,7 @@ def to_full_path(cls, path: Union[PathLike, "SymbolicReference"]) -> PathLike:
full_ref_path = path
if not cls._common_path_default:
return full_ref_path
if not str(path).startswith(cls._common_path_default + "/"):
if not os.fspath(path).startswith(cls._common_path_default + "/"):
full_ref_path = "%s/%s" % (cls._common_path_default, path)
return full_ref_path

Expand Down Expand Up @@ -706,7 +707,7 @@ def _create(
if not force and os.path.isfile(abs_ref_path):
target_data = str(target)
if isinstance(target, SymbolicReference):
target_data = str(target.path)
target_data = os.fspath(target.path)
if not resolve:
target_data = "ref: " + target_data
with open(abs_ref_path, "rb") as fd:
Expand Down Expand Up @@ -842,7 +843,7 @@ def _iter_items(

# Read packed refs.
for _sha, rela_path in cls._iter_packed_refs(repo):
if rela_path.startswith(str(common_path)):
if rela_path.startswith(os.fspath(common_path)):
rela_paths.add(rela_path)
# END relative path matches common path
# END packed refs reading
Expand Down Expand Up @@ -930,4 +931,4 @@ def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_Refere

def is_remote(self) -> bool:
""":return: True if this symbolic reference points to a remote branch"""
return str(self.path).startswith(self._remote_common_path_default + "/")
return os.fspath(self.path).startswith(self._remote_common_path_default + "/")
19 changes: 9 additions & 10 deletions git/repo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class Repo:
working_dir: PathLike
"""The working directory of the git command."""

# stored as string for easier processing, but annotated as path for clearer intention
_working_tree_dir: Optional[PathLike] = None

git_dir: PathLike
Expand Down Expand Up @@ -215,15 +216,13 @@ def __init__(
epath = path or os.getenv("GIT_DIR")
if not epath:
epath = os.getcwd()
epath = os.fspath(epath)
if Git.is_cygwin():
# Given how the tests are written, this seems more likely to catch Cygwin
# git used from Windows than Windows git used from Cygwin. Therefore
# changing to Cygwin-style paths is the relevant operation.
epath = cygpath(str(epath))
epath = cygpath(epath)

epath = epath or path or os.getcwd()
if not isinstance(epath, str):
epath = str(epath)
if expand_vars and re.search(self.re_envvars, epath):
warnings.warn(
"The use of environment variables in paths is deprecated"
Expand Down Expand Up @@ -957,7 +956,7 @@ def is_dirty(
if not submodules:
default_args.append("--ignore-submodules")
if path:
default_args.extend(["--", str(path)])
default_args.extend(["--", os.fspath(path)])
if index:
# diff index against HEAD.
if osp.isfile(self.index.path) and len(self.git.diff("--cached", *default_args)):
Expand Down Expand Up @@ -1357,9 +1356,9 @@ def _clone(
) -> "Repo":
odbt = kwargs.pop("odbt", odb_default_type)

# When pathlib.Path or other class-based path is passed
if not isinstance(path, str):
path = str(path)
# url may be a path and this has no effect if it is a string
url = os.fspath(url)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL is not a path though.

Copy link
Contributor Author

@George-Ogden George-Ogden Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, url can be a path if you're cloning a local repo, and if it's not os.fspath will leave strings alone.

path = os.fspath(path)

## A bug win cygwin's Git, when `--bare` or `--separate-git-dir`
# it prepends the cwd or(?) the `url` into the `path, so::
Expand All @@ -1376,7 +1375,7 @@ def _clone(
multi = shlex.split(" ".join(multi_options))

if not allow_unsafe_protocols:
Git.check_unsafe_protocols(str(url))
Git.check_unsafe_protocols(url)
if not allow_unsafe_options:
Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options)
if not allow_unsafe_options and multi_options:
Expand All @@ -1385,7 +1384,7 @@ def _clone(
proc = git.clone(
multi,
"--",
Git.polish_url(str(url)),
Git.polish_url(url),
clone_path,
with_extended_output=True,
as_process=True,
Expand Down
24 changes: 12 additions & 12 deletions git/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
import logging
import os
import os.path as osp
import pathlib
from pathlib import Path
import platform
import re
import shutil
Expand Down Expand Up @@ -272,9 +272,9 @@ def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 *
def join_path(a: PathLike, *p: PathLike) -> PathLike:
R"""Join path tokens together similar to osp.join, but always use ``/`` instead of
possibly ``\`` on Windows."""
path = str(a)
path = os.fspath(a)
for b in p:
b = str(b)
b = os.fspath(b)
if not b:
continue
if b.startswith("/"):
Expand All @@ -290,18 +290,18 @@ def join_path(a: PathLike, *p: PathLike) -> PathLike:
if sys.platform == "win32":

def to_native_path_windows(path: PathLike) -> PathLike:
path = str(path)
path = os.fspath(path)
return path.replace("/", "\\")

def to_native_path_linux(path: PathLike) -> str:
path = str(path)
path = os.fspath(path)
return path.replace("\\", "/")

to_native_path = to_native_path_windows
else:
# No need for any work on Linux.
def to_native_path_linux(path: PathLike) -> str:
return str(path)
return os.fspath(path)

to_native_path = to_native_path_linux

Expand Down Expand Up @@ -372,7 +372,7 @@ def is_exec(fpath: str) -> bool:
progs = []
if not path:
path = os.environ["PATH"]
for folder in str(path).split(os.pathsep):
for folder in os.fspath(path).split(os.pathsep):
folder = folder.strip('"')
if folder:
exe_path = osp.join(folder, program)
Expand All @@ -397,7 +397,7 @@ def _cygexpath(drive: Optional[str], path: str) -> str:
p = cygpath(p)
elif drive:
p = "/proc/cygdrive/%s/%s" % (drive.lower(), p)
p_str = str(p) # ensure it is a str and not AnyPath
p_str = os.fspath(p) # ensure it is a str and not AnyPath
return p_str.replace("\\", "/")


Expand All @@ -418,7 +418,7 @@ def _cygexpath(drive: Optional[str], path: str) -> str:

def cygpath(path: str) -> str:
"""Use :meth:`git.cmd.Git.polish_url` instead, that works on any environment."""
path = str(path) # Ensure is str and not AnyPath.
path = os.fspath(path) # Ensure is str and not AnyPath.
# Fix to use Paths when 3.5 dropped. Or to be just str if only for URLs?
if not path.startswith(("/cygdrive", "//", "/proc/cygdrive")):
for regex, parser, recurse in _cygpath_parsers:
Expand All @@ -438,7 +438,7 @@ def cygpath(path: str) -> str:


def decygpath(path: PathLike) -> str:
path = str(path)
path = os.fspath(path)
m = _decygpath_regex.match(path)
if m:
drive, rest_path = m.groups()
Expand All @@ -465,7 +465,7 @@ def _is_cygwin_git(git_executable: str) -> bool:
# Just a name given, not a real path.
uname_cmd = osp.join(git_dir, "uname")

if not (pathlib.Path(uname_cmd).is_file() and os.access(uname_cmd, os.X_OK)):
if not (Path(uname_cmd).is_file() and os.access(uname_cmd, os.X_OK)):
_logger.debug(f"Failed checking if running in CYGWIN: {uname_cmd} is not an executable")
_is_cygwin_cache[git_executable] = is_cygwin
return is_cygwin
Expand Down Expand Up @@ -523,7 +523,7 @@ def expand_path(p: PathLike, expand_vars: bool = ...) -> str:


def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[PathLike]:
if isinstance(p, pathlib.Path):
if isinstance(p, Path):
return p.resolve()
try:
p = osp.expanduser(p) # type: ignore[arg-type]
Expand Down
Loading
Loading