| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent b62e91b commit 24b6f95
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,19 @@ | |||
| 2 | 2 | Changelog | |
| 3 | 3 | ========= | |
| 4 | 4 | ||
| 5 | + 3.1.63 | ||
| 6 | + ====== | ||
| 7 | + | ||
| 8 | + Security fixes for | ||
| 9 | + | ||
| 10 | + * https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-gq48-pqfc-9p58 | ||
| 11 | + | ||
| 12 | + If you can, also try and provide feedback on the upcoming v4 branch | ||
| 13 | + https://github.com/gitpython-developers/GitPython/pull/2177 - patches welcome. | ||
| 14 | + | ||
| 15 | + See the following for all changes. | ||
| 16 | + https://github.com/gitpython-developers/GitPython/releases/tag/3.1.63 | ||
| 17 | + | ||
| 5 | 18 | 3.1.62 | |
| 6 | 19 | ====== | |
| 7 | 20 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -419,11 +419,20 @@ def abspath(self) -> PathLike: | |||
| 419 | 419 | root = self.repo.working_tree_dir | |
| 420 | 420 | if root is None: | |
| 421 | 421 | return super().abspath | |
| 422 | - path = root | ||
| 423 | - for component in os.fspath(self._to_relative_path(self.repo, self.path)).split("/"): | ||
| 422 | + return self._checkout_abspath(self._to_relative_path(self.repo, self.path)) | ||
| 423 | + | ||
| 424 | + def _checkout_abspath(self, relative_path: PathLike, allow_final_symlink: bool = False) -> PathLike: | ||
| 425 | + """Check a checkout path already normalized by :meth:`_to_relative_path`.""" | ||
| 426 | + path = self.repo.working_tree_dir | ||
| 427 | + if path is None: | ||
| 428 | + raise NotADirectoryError("Submodules require a working tree") | ||
| 429 | + components = os.fspath(relative_path).split("/") | ||
| 430 | + for index, component in enumerate(components): | ||
| 424 | 431 | path = join_path_native(path, component) | |
| 432 | + if allow_final_symlink and index == len(components) - 1: | ||
| 433 | + break | ||
| 425 | 434 | if osp.islink(path): | |
| 426 | - raise ValueError("Submodule checkout path %r contains a symbolic link" % self.path) | ||
| 435 | + raise ValueError("Submodule checkout path %r contains a symbolic link" % relative_path) | ||
| 427 | 436 | return path | |
| 428 | 437 | ||
| 429 | 438 | @classmethod | |
@@ -1039,7 +1048,8 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = | |||
| 1039 | 1048 | self | |
| 1040 | 1049 | ||
| 1041 | 1050 | :raise ValueError: | |
| 1042 | - If the module path existed and was not empty, or was a file. | ||
| 1051 | + If the module path existed and was not empty, was a file, or had a | ||
| 1052 | + symbolic link in an intermediate component. | ||
| 1043 | 1053 | ||
| 1044 | 1054 | :note: | |
| 1045 | 1055 | Currently the method is not atomic, and it could leave the repository in an | |
@@ -1057,7 +1067,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = | |||
| 1057 | 1067 | return self | |
| 1058 | 1068 | # END handle no change | |
| 1059 | 1069 | ||
| 1060 | - module_checkout_abspath = join_path_native(str(self.repo.working_tree_dir), module_checkout_path) | ||
| 1070 | + module_checkout_abspath = self._checkout_abspath(module_checkout_path, allow_final_symlink=True) | ||
| 1061 | 1071 | if osp.isfile(module_checkout_abspath): | |
| 1062 | 1072 | raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath) | |
| 1063 | 1073 | # END handle target files | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -51,6 +51,131 @@ def _patch_git_config(name, value): | |||
| 51 | 51 | yield | |
| 52 | 52 | ||
| 53 | 53 | ||
| 54 | + @pytest.fixture | ||
| 55 | + def movable_submodule(tmp_path): | ||
| 56 | + """Create a committed local submodule whose logical name stays fixed when moved.""" | ||
| 57 | + with git.Repo.init(tmp_path / "source") as source, git.Repo.init(tmp_path / "parent") as parent: | ||
| 58 | + (tmp_path / "source" / "file").write_text("content", encoding="utf-8") | ||
| 59 | + source.index.add(["file"]) | ||
| 60 | + source.index.commit("Create source") | ||
| 61 | + with _patch_git_config("protocol.file.allow", "always"): | ||
| 62 | + submodule = parent.create_submodule("logical-name", "module", source.working_tree_dir) | ||
| 63 | + parent.index.commit("Create submodule") | ||
| 64 | + # Release clone handles before Windows moves the checkout. | ||
| 65 | + submodule.module().close() | ||
| 66 | + yield submodule | ||
| 67 | + | ||
| 68 | + | ||
| 69 | + def _move_snapshot(submodule): | ||
| 70 | + """Capture index, configuration, and path state to detect side effects of rejected moves.""" | ||
| 71 | + parent = submodule.repo | ||
| 72 | + with submodule.module() as module: | ||
| 73 | + config = Path(module.git_dir, "config").read_bytes() | ||
| 74 | + return ( | ||
| 75 | + Path(parent.index.path).read_bytes(), | ||
| 76 | + Path(parent.working_tree_dir, ".gitmodules").read_bytes(), | ||
| 77 | + Path(submodule.abspath, ".git").read_bytes(), | ||
| 78 | + config, | ||
| 79 | + submodule.path, | ||
| 80 | + ) | ||
| 81 | + | ||
| 82 | + | ||
| 83 | + @pytest.mark.parametrize("target_kind", ["relative", "absolute", "internal", "dangling"]) | ||
| 84 | + @pytest.mark.parametrize("configuration,module", [(True, True), (False, True), (True, False)]) | ||
| 85 | + @pytest.mark.parametrize("absolute_path", [False, True]) | ||
| 86 | + def test_move_rejects_intermediate_symlink( | ||
| 87 | + movable_submodule, tmp_path, target_kind, configuration, module, absolute_path | ||
| 88 | + ): | ||
| 89 | + """Reject intermediate symlinks before changing repository state or their targets. | ||
| 90 | + | ||
| 91 | + Cover relative and absolute destinations in every move mode, including links | ||
| 92 | + within the repository and dangling links, which must also be rejected. | ||
| 93 | + """ | ||
| 94 | + submodule = movable_submodule | ||
| 95 | + parent = submodule.repo | ||
| 96 | + root = Path(parent.working_tree_dir) | ||
| 97 | + target = root / "target" if target_kind == "internal" else tmp_path / "outside" | ||
| 98 | + if target_kind != "dangling": | ||
| 99 | + target.mkdir() | ||
| 100 | + (root / "nested").mkdir() | ||
| 101 | + link = root / "nested" / "link" | ||
| 102 | + link.symlink_to( | ||
| 103 | + target if target_kind == "absolute" else os.path.relpath(target, link.parent), target_is_directory=True | ||
| 104 | + ) | ||
| 105 | + parent.index.add(["nested/link"]) | ||
| 106 | + parent.index.commit("Record layout") | ||
| 107 | + tree = parent.git.write_tree() | ||
| 108 | + before = _move_snapshot(submodule) | ||
| 109 | + destination = root / "nested/link/new/moved" if absolute_path else "nested/link/new/moved" | ||
| 110 | + | ||
| 111 | + with pytest.raises(ValueError, match="contains a symbolic link"): | ||
| 112 | + submodule.move(destination, configuration=configuration, module=module) | ||
| 113 | + | ||
| 114 | + assert _move_snapshot(submodule) == before | ||
| 115 | + assert parent.git.write_tree() == tree | ||
| 116 | + assert Path(submodule.abspath, "file").read_text(encoding="utf-8") == "content" | ||
| 117 | + if target_kind == "dangling": | ||
| 118 | + assert not target.exists() | ||
| 119 | + else: | ||
| 120 | + assert list(target.iterdir()) == [] | ||
| 121 | + | ||
| 122 | + | ||
| 123 | + @pytest.mark.parametrize("absolute_path", [False, True]) | ||
| 124 | + def test_move_normal_destination(movable_submodule, absolute_path): | ||
| 125 | + """Allow ordinary relative and absolute moves, and make a repeated move a no-op.""" | ||
| 126 | + submodule = movable_submodule | ||
| 127 | + root = Path(submodule.repo.working_tree_dir) | ||
| 128 | + destination = root / "nested/moved" if absolute_path else "nested/moved" | ||
| 129 | + assert submodule.move(destination) is submodule | ||
| 130 | + assert Path(submodule.abspath, "file").read_text(encoding="utf-8") == "content" | ||
| 131 | + assert not (root / "module").exists() | ||
| 132 | + assert submodule.path == "nested/moved" | ||
| 133 | + submodule.repo.git.write_tree() | ||
| 134 | + before = _move_snapshot(submodule) | ||
| 135 | + assert submodule.move(destination) is submodule | ||
| 136 | + assert _move_snapshot(submodule) == before | ||
| 137 | + | ||
| 138 | + | ||
| 139 | + @pytest.mark.parametrize("kind", ["empty", "nonempty", "file", "dangling"]) | ||
| 140 | + def test_move_leaf_symlink_compatibility(movable_submodule, tmp_path, kind): | ||
| 141 | + """Preserve leaf-symlink replacement without modifying the external target. | ||
| 142 | + | ||
| 143 | + Moving onto a link to an empty directory replaces the link; nonempty, file, | ||
| 144 | + and dangling targets fail without changing repository state. Direct checkout | ||
| 145 | + path access must still reject every leaf symlink. | ||
| 146 | + """ | ||
| 147 | + submodule = movable_submodule | ||
| 148 | + root = Path(submodule.repo.working_tree_dir) | ||
| 149 | + target = tmp_path / "outside" | ||
| 150 | + if kind in ("empty", "nonempty"): | ||
| 151 | + target.mkdir() | ||
| 152 | + if kind == "nonempty": | ||
| 153 | + (target / "keep").write_text("keep", encoding="utf-8") | ||
| 154 | + if kind == "file": | ||
| 155 | + target.write_text("keep", encoding="utf-8") | ||
| 156 | + destination = root / "destination" | ||
| 157 | + destination.symlink_to(target, target_is_directory=kind != "file") | ||
| 158 | + with pytest.raises(ValueError, match="contains a symbolic link"): | ||
| 159 | + Submodule(submodule.repo, Submodule.NULL_BIN_SHA, name="unused", path="destination").abspath | ||
| 160 | + before = _move_snapshot(submodule) | ||
| 161 | + if kind == "empty": | ||
| 162 | + assert submodule.move("destination") is submodule | ||
| 163 | + assert not destination.is_symlink() | ||
| 164 | + assert (destination / "file").is_file() | ||
| 165 | + assert list(target.iterdir()) == [] | ||
| 166 | + else: | ||
| 167 | + with pytest.raises(OSError if kind == "dangling" else ValueError): | ||
| 168 | + submodule.move("destination") | ||
| 169 | + assert _move_snapshot(submodule) == before | ||
| 170 | + assert destination.is_symlink() | ||
| 171 | + if kind == "nonempty": | ||
| 172 | + assert (target / "keep").read_text(encoding="utf-8") == "keep" | ||
| 173 | + elif kind == "file": | ||
| 174 | + assert target.read_text(encoding="utf-8") == "keep" | ||
| 175 | + else: | ||
| 176 | + assert not target.exists() | ||
| 177 | + | ||
| 178 | + | ||
| 54 | 179 | class TestRootProgress(RootUpdateProgress): | |
| 55 | 180 | """Just prints messages, for now without checking the correctness of the states""" | |
| 56 | 181 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments