diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index 013b097ae..8dd108355 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -895,22 +895,6 @@ "lineCount": 1 } }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 22, - "endColumn": 37, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 22, - "endColumn": 37, - "lineCount": 1 - } - }, { "code": "reportReturnType", "range": { diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 88099f15c..7dfa2e8f2 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -25,16 +25,6 @@ jobs: python-version: "3.14t" - os-type: macos python-version: "3.15t" - - os-type: windows - python-version: "3.13" # FIXME: Fix and enable Python 3.13-3.15 on Windows (#1955). - - os-type: windows - python-version: "3.14" - - os-type: windows - python-version: "3.14t" - - os-type: windows - python-version: "3.15" - - os-type: windows - python-version: "3.15t" include: - os-ver: latest - os-type: ubuntu diff --git a/git/config.py b/git/config.py index 821710ea3..8b26be722 100644 --- a/git/config.py +++ b/git/config.py @@ -577,8 +577,12 @@ def _all_items(section: str) -> List[Tuple[str, str]]: if keyword in ["gitdir", "gitdir/i"]: value = osp.expanduser(value) + git_dir = os.fspath(self._repo.git_dir) if self._repo.git_dir else None + if sys.platform == "win32": + git_dir = git_dir.replace("\\", "/") if git_dir else None - if not any(value.startswith(s) for s in ["./", "/"]): + drive, _tail = osp.splitdrive(value) + if not drive and not any(value.startswith(s) for s in ["./", "/"]): value = "**/" + value if value.endswith("/"): value += "**" @@ -590,9 +594,8 @@ def _all_items(section: str) -> List[Tuple[str, str]]: lambda m: f"[{m.group().lower()!r}{m.group().upper()!r}]", value, ) - if self._repo.git_dir: - if fnmatch.fnmatchcase(os.fspath(self._repo.git_dir), value): - paths += _all_items(section) + if git_dir and fnmatch.fnmatchcase(git_dir, value): + paths += _all_items(section) elif keyword == "onbranch": try: diff --git a/git/index/base.py b/git/index/base.py index e5b1e72f8..248a7f10a 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -34,6 +34,8 @@ LockedFD, join_path_native, file_contents_ro, + _is_path_rooted, + _to_relative_path, to_native_path_linux, unbare_repo, to_bin_sha, @@ -58,6 +60,7 @@ Any, BinaryIO, Callable, + cast, Dict, Generator, IO, @@ -655,16 +658,12 @@ def _to_relative_path(self, path: PathLike) -> PathLike: :raise ValueError: """ - if not osp.isabs(path): - return path if self.repo.bare: - raise InvalidGitRepositoryError("require non-bare repository") - 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 os.fspath(path).endswith(os.sep) and not result.endswith(os.sep): - result += os.sep - return result + drive, _tail = osp.splitdrive(os.fspath(path)) + if drive or _is_path_rooted(path): + raise InvalidGitRepositoryError("paths with a drive or root require a non-bare repository") + return path + return _to_relative_path(cast(PathLike, self.repo.working_tree_dir), path) def _preprocess_add_items( self, items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]] diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 97fc9e111..e20bc1994 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -28,6 +28,7 @@ from git.util import ( IterableList, RemoteProgress, + _to_relative_path, join_path_native, rmtree, to_native_path_linux, @@ -379,23 +380,14 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike: :raise ValueError: If path is not contained in the parent repository's working tree. """ - path = to_native_path_linux(path) + if parent_repo.working_tree_dir: + path = _to_relative_path(parent_repo.working_tree_dir, path) + else: + path = to_native_path_linux(path) if path.endswith("/"): path = path[:-1] - # END handle trailing slash - - if osp.isabs(path) and parent_repo.working_tree_dir: - working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir) - if not path.startswith(working_tree_linux): - raise ValueError( - "Submodule checkout path '%s' needs to be within the parents repository at '%s'" - % (working_tree_linux, path) - ) - path = path[len(working_tree_linux.rstrip("/")) + 1 :] - if not path: - raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path) - # END verify converted relative path makes sense - # END convert to a relative path + if not path or path == ".": + raise ValueError("Submodule checkout path must not be the repository root") return path diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 020de5e13..824d0c46c 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -119,7 +119,7 @@ def _get_validated_path(base: PathLike, path: PathLike) -> str: common_path = os.path.commonpath([base_path, abs_path]) except ValueError as e: raise ValueError("Reference path %r escapes the repository" % path) from e - if os.path.normcase(common_path) != os.path.normcase(base_path): + if common_path != base_path: raise ValueError("Reference path %r escapes the repository" % path) return abs_path diff --git a/git/repo/base.py b/git/repo/base.py index 6594101f3..dfd361747 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -961,8 +961,7 @@ def _get_alternates(self) -> List[str]: :return: List of strings being pathnames of alternates """ - if self.git_dir: - alternates_path = osp.join(self.git_dir, "objects", "info", "alternates") + alternates_path = osp.join(self.common_dir, "objects", "info", "alternates") if osp.exists(alternates_path): with open(alternates_path, "rb") as f: diff --git a/git/util.py b/git/util.py index 6f002d98a..02f57c132 100644 --- a/git/util.py +++ b/git/util.py @@ -315,6 +315,68 @@ def join_path_native(a: PathLike, *p: PathLike) -> PathLike: return to_native_path(join_path(a, *p)) +def _is_path_rooted(path: PathLike) -> bool: + r"""Whether ``path`` has a root, including one encoded in a UNC drive. + + On Windows, ``\directory`` is rooted on the current drive without being + absolute, while ``C:\directory`` has both a drive and a root. In contrast, + ``directory`` and the drive-relative ``C:directory`` have no root. + UNC paths are rooted: ``\\server\share`` stores the share in the drive + returned by :func:`os.path.splitdrive`, while ``\\server\share\directory`` + additionally has a rooted tail. + On POSIX, which has no drive concept, this simply distinguishes absolute + paths such as ``/directory`` from relative paths such as ``directory``. + """ + drive, tail = osp.splitdrive(os.fspath(path)) + separators = (os.sep,) if os.altsep is None else (os.sep, os.altsep) + return tail.startswith(separators) or drive.startswith(separators) + + +def _to_relative_path(root: PathLike, path: PathLike) -> str: + r"""Return a normalized Git-style path confined to ``root``. + + A Windows path such as ``\directory`` is rooted but not absolute. Resolve it + against the drive of ``root`` rather than treating it as relative to ``root``. + Drive-relative paths such as ``C:directory`` are rejected because their meaning + depends on process-global per-drive state. + + For example, with ``root`` set to ``C:\repo`` on Windows: + + * ``directory\file`` -> ``directory/file`` + * ``directory\`` -> ``directory/`` + * ``C:\repo\directory\file`` -> ``directory/file`` + * ``\repo\directory\file`` -> ``directory/file`` + * ``C:directory\file`` -> :exc:`ValueError` + * ``C:\other\file`` -> :exc:`ValueError` + + On POSIX, ``/repo/directory/file`` under ``/repo`` similarly becomes + ``directory/file``. A trailing separator is preserved as a Git-style ``/``. + """ + path_str = os.fspath(path) + if not path_str: + return path_str + + drive, _tail = osp.splitdrive(path_str) + rooted = _is_path_rooted(path_str) + if drive and not rooted: + raise ValueError("Drive-relative path %r is not supported" % path_str) + + root_abs = osp.abspath(os.fspath(root)) + path_abs = osp.abspath(osp.join(root_abs, path_str)) + try: + common_path = osp.commonpath([root_abs, path_abs]) + except ValueError as e: + raise ValueError("Path %r is not in repository at %r" % (path_str, root_abs)) from e + if common_path != root_abs: + raise ValueError("Path %r is not in repository at %r" % (path_str, root_abs)) + + relative_path = to_native_path_linux(osp.relpath(path_abs, root_abs)) + separators = (os.sep,) if os.altsep is None else (os.sep, os.altsep) + if path_str.endswith(separators) and relative_path != "." and not relative_path.endswith("/"): + relative_path += "/" + return relative_path + + def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool: """Make sure that the directory pointed to by path exists. diff --git a/test/test_commit.py b/test/test_commit.py index cb0427740..b9ceecf07 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -276,14 +276,14 @@ def test_iteration(self): assert ltd_commits and len(ltd_commits) < len(all_commits) # Show commits of multiple paths, resulting in a union of commits. - less_ltd_commits = list(Commit.iter_items(self.rorepo, "master", paths=("CHANGES", "AUTHORS"))) + less_ltd_commits = list(Commit.iter_items(self.rorepo, "HEAD", paths=("CHANGES", "AUTHORS"))) assert len(ltd_commits) < len(less_ltd_commits) class Child(Commit): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - child_commits = list(Child.iter_items(self.rorepo, "master", paths=("CHANGES", "AUTHORS"))) + child_commits = list(Child.iter_items(self.rorepo, "HEAD", paths=("CHANGES", "AUTHORS"))) assert type(child_commits[0]) is Child def test_iter_items(self): @@ -536,7 +536,7 @@ def test_trailers(self): ), ] for msg in msgs: - commit = copy.copy(self.rorepo.commit("master")) + commit = copy.copy(self.rorepo.commit("HEAD")) commit.message = msg assert commit.trailers_list == [ (KEY_1, VALUE_1_1), @@ -559,13 +559,13 @@ def test_trailers(self): ] for msg in msgs: - commit = copy.copy(self.rorepo.commit("master")) + commit = copy.copy(self.rorepo.commit("HEAD")) commit.message = msg assert commit.trailers_list == [] assert commit.trailers_dict == {} # Check that only the last key value paragraph is evaluated. - commit = copy.copy(self.rorepo.commit("master")) + commit = copy.copy(self.rorepo.commit("HEAD")) commit.message = f"Subject\n\nMultiline\nBody\n\n{KEY_1}: {VALUE_1_1}\n\n{KEY_2}: {VALUE_2}\n" assert commit.trailers_list == [(KEY_2, VALUE_2)] assert commit.trailers_dict == {KEY_2: [VALUE_2]} diff --git a/test/test_config.py b/test/test_config.py index 361a51fa9..f5316296b 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -14,7 +14,7 @@ from git import GitConfigParser from git.config import _OMD, cp -from git.util import rmfile +from git.util import cwd, rmfile from test.lib import SkipTest, TestCase, fixture_path, with_rw_directory @@ -374,6 +374,22 @@ def test_config_relative_path_include(self, rw_dir): with GitConfigParser(relative_config_path, read_only=True) as cr: assert cr.get_value("included", "value") == "included" + @pytest.mark.skipif(os.name != "nt", reason="Specifically for Windows drive-rooted paths.") + @with_rw_directory + def test_config_drive_rooted_path_include(self, rw_dir): + with cwd(rw_dir): + included_path = osp.join(rw_dir, "included") + with GitConfigParser(included_path, read_only=False) as cw: + cw.set_value("included", "value", "included") + + _drive, rooted_included_path = osp.splitdrive(included_path) + config_path = osp.join(rw_dir, "config") + with GitConfigParser(config_path, read_only=False) as cw: + cw.set_value("include", "path", rooted_included_path) + + with GitConfigParser(config_path, read_only=True) as cr: + assert cr.get_value("included", "value") == "included" + @with_rw_directory def test_multiple_include_paths_with_same_key(self, rw_dir): """Test that multiple 'path' entries under [include] are all respected. @@ -411,15 +427,11 @@ def test_multiple_include_paths_with_same_key(self, rw_dir): assert cr.get_value("user", "name") == "from-inc1" assert cr.get_value("core", "bar") == "from-inc2" - @pytest.mark.xfail( - sys.platform == "win32", - reason='Second config._has_includes() assertion fails (for "config is included if path is matching git_dir")', - raises=AssertionError, - ) @with_rw_directory def test_conditional_includes_from_git_dir(self, rw_dir): # Initiate repository path. git_dir = osp.join(rw_dir, "target1", "repo1") + git_dir_pattern = git_dir.replace("\\", "/") os.makedirs(git_dir) # Initiate mocked repository. @@ -431,6 +443,7 @@ def test_conditional_includes_from_git_dir(self, rw_dir): template = '[includeIf "{}:{}"]\n path={}\n' with open(path1, "w") as stream: + # on Windows, this writes a backslash pattern. stream.write(template.format("gitdir", git_dir, path2)) # Ensure that config is ignored if no repo is set. @@ -438,14 +451,26 @@ def test_conditional_includes_from_git_dir(self, rw_dir): assert not config._has_includes() assert config._included_paths() == [] - # Ensure that config is included if path is matching git_dir. + # Git uses forward slashes in gitdir patterns on every platform: + # backslashes escape the next pattern character rather than separate + # path components. On Windows, GitPython therefore normalizes git_dir + # to forward slashes but leaves this backslash pattern unchanged, so + # the two do not match and no path is included. + with GitConfigParser(path1, repo=repo, merge_includes=False) as config: + expected_paths = [] if sys.platform == "win32" else [("path", path2)] + assert config._included_paths() == expected_paths + + # Ensure that Git's forward-slash syntax matches native Windows paths. + with open(path1, "w") as stream: + stream.write(template.format("gitdir", git_dir_pattern, path2)) + with GitConfigParser(path1, repo=repo) as config: assert config._has_includes() assert config._included_paths() == [("path", path2)] # Ensure that config is ignored if case is incorrect. with open(path1, "w") as stream: - stream.write(template.format("gitdir", git_dir.upper(), path2)) + stream.write(template.format("gitdir", git_dir_pattern.upper(), path2)) with GitConfigParser(path1, repo=repo) as config: assert not config._has_includes() @@ -453,7 +478,7 @@ def test_conditional_includes_from_git_dir(self, rw_dir): # Ensure that config is included if case is ignored. with open(path1, "w") as stream: - stream.write(template.format("gitdir/i", git_dir.upper(), path2)) + stream.write(template.format("gitdir/i", git_dir_pattern.upper(), path2)) with GitConfigParser(path1, repo=repo) as config: assert config._has_includes() @@ -483,6 +508,20 @@ def test_conditional_includes_from_git_dir(self, rw_dir): assert config._has_includes() assert config._included_paths() == [("path", path2)] + @with_rw_directory + def test_conditional_includes_do_not_treat_backslashes_as_separators(self, rw_dir): + git_dir = osp.join(rw_dir, "target", "repo") + repo = mock.Mock(git_dir=git_dir) + config_path = osp.join(rw_dir, "config") + included_path = osp.join(rw_dir, "included") + pattern = git_dir.replace("\\", "/").replace("/target/repo", R"/target\repo") + + with open(config_path, "w") as stream: + stream.write(f'[includeIf "gitdir:{pattern}"]\n path={included_path}\n') + + with GitConfigParser(config_path, repo=repo, merge_includes=False) as config: + assert config._included_paths() == [] + @with_rw_directory def test_conditional_includes_from_branch_name(self, rw_dir): # Initiate mocked branch. diff --git a/test/test_docs.py b/test/test_docs.py index c3cfec3e0..0810d954d 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -72,21 +72,20 @@ def test_init_repo_object(self, rw_dir): # heads, tags and references # heads are branches in git-speak # [8-test_init_repo_object] - self.assertEqual( - repo.head.ref, - repo.heads.master, # head is a sym-ref pointing to master. - "It's ok if TC not running from `master`.", - ) + active_branch = repo.active_branch + self.assertEqual(repo.head.ref, active_branch) # HEAD is a sym-ref pointing to the active branch. self.assertEqual(repo.tags["0.3.5"], repo.tag("refs/tags/0.3.5")) # You can access tags in various ways too. - self.assertEqual(repo.refs.master, repo.heads["master"]) # .refs provides all refs, i.e. heads... + self.assertEqual(repo.refs[active_branch.name], repo.heads[active_branch.name]) # .refs provides all refs... if "TRAVIS" not in os.environ: - self.assertEqual(repo.refs["origin/master"], repo.remotes.origin.refs.master) # ... remotes ... + remote_branch = next(ref for ref in repo.remotes.origin.refs if ref.remote_head != "HEAD") + self.assertEqual(repo.refs[remote_branch.name], remote_branch) # ...remotes... self.assertEqual(repo.refs["0.3.5"], repo.tags["0.3.5"]) # ... and tags. # ![8-test_init_repo_object] # Create a new head/branch. # [9-test_init_repo_object] + original_branch = cloned_repo.active_branch new_branch = cloned_repo.create_head("feature") # Create a new branch ... assert cloned_repo.active_branch != new_branch # which wasn't checked out yet ... self.assertEqual(new_branch.commit, cloned_repo.active_branch.commit) # pointing to the checked-out commit. @@ -146,10 +145,10 @@ def update(self, op_code, cur_count, max_count=None, message=""): assert origin.exists() for fetch_info in origin.fetch(progress=MyProgressPrinter()): print("Updated %s to %s" % (fetch_info.ref, fetch_info.commit)) - # Create a local branch at the latest fetched master. We specify the name - # statically, but you have all information to do it programmatically as well. - bare_master = bare_repo.create_head("master", origin.refs.master) - bare_repo.head.set_reference(bare_master) + # Create a local branch at one of the remote's branches. + remote_branch = next(ref for ref in origin.refs if ref.remote_head != "HEAD") + bare_branch = bare_repo.create_head(remote_branch.remote_head, remote_branch) + bare_repo.head.set_reference(bare_branch) assert not bare_repo.delete_remote(origin).exists() # push and pull behave very similarly. # ![12-test_init_repo_object] @@ -162,35 +161,39 @@ def update(self, op_code, cur_count, max_count=None, message=""): new_file_path = os.path.join(cloned_repo.working_tree_dir, "my-new-file") open(new_file_path, "wb").close() # Create new file in working tree. cloned_repo.index.add([new_file_path]) # Add it to the index. - # Commit the changes to deviate masters history. + # Commit the changes to deviate from the original branch's history. cloned_repo.index.commit("Added a new file in the past - for later merge") # Prepare a merge. - master = cloned_repo.heads.master # Right-hand side is ahead of us, in the future. - merge_base = cloned_repo.merge_base(new_branch, master) # Allows for a three-way merge. - cloned_repo.index.merge_tree(master, base=merge_base) # Write the merge result into index. + merge_base = cloned_repo.merge_base(new_branch, original_branch) # Allows for a three-way merge. + cloned_repo.index.merge_tree(original_branch, base=merge_base) # Write the merge result into index. cloned_repo.index.commit( "Merged past and now into future ;)", - parent_commits=(new_branch.commit, master.commit), + parent_commits=(new_branch.commit, original_branch.commit), ) - # Now new_branch is ahead of master, which probably should be checked out and reset softly. + # Now new_branch is ahead of the original branch, which probably should be checked out and reset softly. # Note that all these operations didn't touch the working tree, as we managed it ourselves. # This definitely requires you to know what you are doing! :) assert os.path.basename(new_file_path) in new_branch.commit.tree # New file is now in tree. - master.commit = new_branch.commit # Let master point to most recent commit. - cloned_repo.head.reference = master # We adjusted just the reference, not the working tree or index. + original_branch.commit = new_branch.commit # Let the original branch point to the most recent commit. + cloned_repo.head.reference = original_branch # We adjusted just the reference, not the working tree or index. # ![13-test_init_repo_object] # submodules # [14-test_init_repo_object] - # Create a new submodule and check it out on the spot, setup to track master - # branch of `bare_repo`. As our GitPython repository has submodules already that - # point to GitHub, make sure we don't interact with them. + # Create a new submodule and check it out on the spot, set up to track the + # default branch of `bare_repo`. As our GitPython repository has submodules + # already that point to GitHub, make sure we don't interact with them. for sm in cloned_repo.submodules: assert not sm.remove().exists() # after removal, the sm doesn't exist anymore - sm = cloned_repo.create_submodule("mysubrepo", "path/to/subrepo", url=bare_repo.git_dir, branch="master") + sm = cloned_repo.create_submodule( + "mysubrepo", + "path/to/subrepo", + url=bare_repo.git_dir, + branch=bare_branch.name, + ) # .gitmodules was written and added to the index, which is now being committed. cloned_repo.index.commit("Added submodule") diff --git a/test/test_index.py b/test/test_index.py index 3ad5a457f..2d2e47d20 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -1086,14 +1086,52 @@ class Mocked: path = os.path.join(repo_root, f"directory2{os.sep}") index = IndexFile(repo) - expected_path = f"directory2{os.sep}" + expected_path = "directory2/" actual_path = index._to_relative_path(path) self.assertEqual(expected_path, actual_path) - with mock.patch("git.index.base.os.path") as ospath_mock: - ospath_mock.relpath.return_value = f"directory2{os.sep}" - actual_path = index._to_relative_path(path) - self.assertEqual(expected_path, actual_path) + @pytest.mark.skipif(sys.platform != "win32", reason="Specifically for Windows.") + def test__to_relative_path_windows_unc_share_root(self): + for repo_root in [R"\\server\share", R"\\?\UNC\server\share"]: + with self.subTest(repo_root=repo_root): + repo = mock.Mock(bare=False, git_dir=repo_root, working_tree_dir=repo_root) + index = IndexFile(repo) + + self.assertEqual(index._to_relative_path(repo_root), ".") + + @pytest.mark.skipif(sys.platform != "win32", reason="Specifically for Windows.") + @with_rw_directory + def test__to_relative_path_windows_path_kinds(self, rw_dir): + repo_root = osp.join(rw_dir, "repo") + os.makedirs(osp.join(repo_root, "nested")) + + class Mocked: + bare = False + git_dir = osp.join(repo_root, ".git") + working_tree_dir = repo_root + + index = IndexFile(Mocked()) + inside_path = osp.join(repo_root, "nested", "file") + _drive, rooted_inside_path = osp.splitdrive(inside_path) + + self.assertEqual(index._to_relative_path(inside_path), "nested/file") + self.assertEqual(index._to_relative_path(rooted_inside_path), "nested/file") + self.assertEqual(index._to_relative_path(rooted_inside_path.replace("\\", "/")), "nested/file") + self.assertEqual(index._to_relative_path(PathLikeMock(inside_path)), "nested/file") + self.assertEqual(index._to_relative_path(inside_path.upper()), "NESTED/FILE") + self.assertRaises(ValueError, index._to_relative_path, osp.join(repo_root + "-other", "file")) + self.assertRaises(ValueError, index._to_relative_path, osp.join("..", "outside")) + self.assertRaises(ValueError, index._to_relative_path, f"{osp.splitdrive(repo_root)[0]}relative") + self.assertRaises(ValueError, index._to_relative_path, R"Z:\outside") + self.assertRaises(ValueError, index._to_relative_path, R"\\server\share\outside") + self.assertRaises(ValueError, index._to_relative_path, R"\\?\C:\outside") + + Mocked.bare = True + bare_index = IndexFile(Mocked()) + self.assertRaises(InvalidGitRepositoryError, bare_index._to_relative_path, rooted_inside_path) + self.assertRaises( + InvalidGitRepositoryError, bare_index._to_relative_path, f"{osp.splitdrive(repo_root)[0]}relative" + ) @pytest.mark.xfail( type(_win_bash_status) is WinBashStatus.Absent, diff --git a/test/test_refs.py b/test/test_refs.py index 6481b54a8..38e15cfa1 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -258,7 +258,7 @@ def test_orig_head(self): @with_rw_repo("0.1.6") def test_head_checkout_detached_head(self, rw_repo): - res = rw_repo.remotes.origin.refs.master.checkout() + res = rw_repo.remotes.origin.refs.HEAD.reference.checkout() assert isinstance(res, SymbolicReference) assert res.name == "HEAD" @@ -661,7 +661,7 @@ def test_dereference_recursive(self): assert SymbolicReference.dereference_recursive(self.rorepo, "HEAD") def test_reflog(self): - assert isinstance(self.rorepo.heads.master.log(), RefLog) + assert isinstance(self.rorepo.active_branch.log(), RefLog) def test_refs_outside_repo(self): # Create a file containing a valid reference outside the repository. Attempting diff --git a/test/test_repo.py b/test/test_repo.py index 7c7f1dd34..84336a39b 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -128,8 +128,9 @@ def test_heads_should_populate_head_data(self): self.assertIsInstance(head.commit, Commit) # END for each head - self.assertIsInstance(self.rorepo.heads.master, Head) - self.assertIsInstance(self.rorepo.heads["master"], Head) + active_branch = self.rorepo.active_branch + self.assertIsInstance(self.rorepo.heads[active_branch.name], Head) + self.assertEqual(self.rorepo.heads[active_branch.name], active_branch) def test_tree_from_revision(self): tree = self.rorepo.tree("0.1.6") @@ -233,6 +234,18 @@ def test_clone_from_keeps_env(self, rw_dir): self.assertEqual(environment, cloned.git.environment()) + @pytest.mark.skipif(os.name != "nt", reason="Specifically for Windows drive-rooted paths.") + @with_rw_directory + def test_clone_from_drive_rooted_destination(self, rw_dir): + original_repo = Repo.init(osp.join(rw_dir, "repo")) + with cwd(rw_dir): + destination = osp.join(rw_dir, "clone") + _drive, rooted_destination = osp.splitdrive(destination) + + cloned = Repo.clone_from(original_repo.git_dir, rooted_destination) + + assert osp.samefile(cloned.working_tree_dir, destination) + @with_rw_directory def test_date_format(self, rw_dir): repo = Repo.init(osp.join(rw_dir, "repo")) @@ -326,13 +339,29 @@ def test_daemon_export(self): def test_alternates(self): cur_alternates = self.rorepo.alternates - # empty alternates - self.rorepo.alternates = [] - self.assertEqual(self.rorepo.alternates, []) + try: + # Empty alternates. + self.rorepo.alternates = [] + self.assertEqual(self.rorepo.alternates, []) + alts = ["other/location", "this/location"] + self.rorepo.alternates = alts + self.assertEqual(alts, self.rorepo.alternates) + finally: + self.rorepo.alternates = cur_alternates + + @with_rw_directory + def test_alternates_use_common_dir(self, rw_dir): + common_dir = osp.join(rw_dir, "common") + git_dir = osp.join(rw_dir, "worktrees", "linked") + 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) + alts = ["other/location", "this/location"] - self.rorepo.alternates = alts - self.assertEqual(alts, self.rorepo.alternates) - self.rorepo.alternates = cur_alternates + Repo._set_alternates(repo, alts) + + self.assertEqual(Repo._get_alternates(repo), alts) + self.assertFalse(osp.exists(osp.join(git_dir, "objects", "info", "alternates"))) def test_repr(self): assert repr(self.rorepo).startswith("=4 -env_list = py{37,38,39,310,311,312}, ruff, format, mypy, html, misc +env_list = py{37,38,39,310,311,312,313,314,315}, ruff, format, mypy, html, misc [testenv] description = Run unit tests