| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| 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 | |
|---|---|---|---|
@@ -9,6 +9,7 @@ | |||
| 9 | 9 | import ntpath | |
| 10 | 10 | import os | |
| 11 | 11 | import os.path as osp | |
| 12 | + from pathlib import Path | ||
| 12 | 13 | import shlex | |
| 13 | 14 | import stat | |
| 14 | 15 | import sys | |
@@ -255,7 +256,7 @@ def _config_parser( | |||
| 255 | 256 | # END handle parent_commit | |
| 256 | 257 | fp_module: Union[str, BytesIO] | |
| 257 | 258 | if not repo.bare and parent_matches_head and repo.working_tree_dir: | |
| 258 | - fp_module = osp.join(repo.working_tree_dir, cls.k_modules_file) | ||
| 259 | + fp_module = cls._checked_abspath(repo.working_tree_dir, cls.k_modules_file) | ||
| 259 | 260 | else: | |
| 260 | 261 | assert parent_commit is not None, "need valid parent_commit in bare repositories" | |
| 261 | 262 | try: | |
@@ -322,7 +323,7 @@ def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> Path | |||
| 322 | 323 | if cls._need_gitfile_submodules(parent_repo.git): | |
| 323 | 324 | return osp.join(parent_repo.git_dir, "modules", name) | |
| 324 | 325 | if parent_repo.working_tree_dir: | |
| 325 | - return osp.join(parent_repo.working_tree_dir, path) | ||
| 326 | + return cls._checked_abspath(parent_repo.working_tree_dir, cls._to_relative_path(parent_repo, path)) | ||
| 326 | 327 | raise NotADirectoryError() | |
| 327 | 328 | ||
| 328 | 329 | @classmethod | |
@@ -361,8 +362,11 @@ def _clone_repo( | |||
| 361 | 362 | :param kwargs: | |
| 362 | 363 | Additional arguments given to :manpage:`git-clone(1)`. | |
| 363 | 364 | """ | |
| 365 | + path = cls._to_relative_path(repo, path) | ||
| 366 | + if repo.working_tree_dir is None: | ||
| 367 | + raise NotADirectoryError("Submodules require a working tree") | ||
| 368 | + module_checkout_path = cls._checked_abspath(repo.working_tree_dir, path) | ||
| 364 | 369 | module_abspath = cls._module_abspath(repo, path, name) | |
| 365 | - module_checkout_path = module_abspath | ||
| 366 | 370 | if cls._need_gitfile_submodules(repo.git): | |
| 367 | 371 | if not allow_unsafe_options: | |
| 368 | 372 | Git.check_unsafe_options(Git._option_candidates([], kwargs), repo.unsafe_git_clone_options) | |
@@ -373,11 +377,22 @@ def _clone_repo( | |||
| 373 | 377 | repo.unsafe_git_clone_options, | |
| 374 | 378 | ) | |
| 375 | 379 | allow_unsafe_options = True | |
| 380 | + if osp.islink(module_abspath): | ||
| 381 | + # Clone into the target while retaining the metadata alias. Git for | ||
| 382 | + # Windows cannot initialize through a dangling directory symlink. | ||
| 383 | + # Read the link explicitly for Python 3.7, and remove the Windows | ||
| 384 | + # namespace prefix returned by newer Python versions for Git. | ||
| 385 | + target = os.readlink(module_abspath) | ||
| 386 | + if sys.platform == "win32": | ||
| 387 | + if target.startswith("\\\\?\\UNC\\"): | ||
| 388 | + target = "\\\\" + target[8:] | ||
| 389 | + elif target.startswith("\\\\?\\"): | ||
| 390 | + target = target[4:] | ||
| 391 | + module_abspath = to_native_path_linux(osp.join(osp.dirname(module_abspath), target)) | ||
| 376 | 392 | kwargs["separate_git_dir"] = module_abspath | |
| 377 | 393 | module_abspath_dir = osp.dirname(module_abspath) | |
| 378 | 394 | if not osp.isdir(module_abspath_dir): | |
| 379 | 395 | os.makedirs(module_abspath_dir) | |
| 380 | - module_checkout_path = osp.join(repo.working_tree_dir, path) # type: ignore[arg-type] | ||
| 381 | 396 | ||
| 382 | 397 | if url.startswith("../"): | |
| 383 | 398 | remote_name = cast("RemoteReference", repo.active_branch.tracking_branch()).remote_name | |
@@ -419,13 +434,43 @@ def abspath(self) -> PathLike: | |||
| 419 | 434 | root = self.repo.working_tree_dir | |
| 420 | 435 | if root is None: | |
| 421 | 436 | return super().abspath | |
| 422 | - path = root | ||
| 423 | - for component in os.fspath(self._to_relative_path(self.repo, self.path)).split("/"): | ||
| 424 | - path = join_path_native(path, component) | ||
| 437 | + return self._checkout_abspath(self._to_relative_path(self.repo, self.path)) | ||
| 438 | + | ||
| 439 | + def _checkout_abspath(self, relative_path: PathLike, allow_final_symlink: bool = False) -> PathLike: | ||
| 440 | + """Check a checkout path already normalized by :meth:`_to_relative_path`.""" | ||
| 441 | + return self._checked_abspath(self.repo.working_tree_dir, relative_path, allow_final_symlink) | ||
| 442 | + | ||
| 443 | + @classmethod | ||
| 444 | + def _checked_abspath( | ||
| 445 | + cls, root: Union[PathLike, None], relative_path: PathLike, allow_final_symlink: bool = False | ||
| 446 | + ) -> str: | ||
| 447 | + """Reject symlinks below a trusted root before accessing submodule paths.""" | ||
| 448 | + if root is None: | ||
| 449 | + raise NotADirectoryError("Submodules require a working tree") | ||
| 450 | + path = os.fspath(root) | ||
| 451 | + components = to_native_path_linux(relative_path).split("/") | ||
| 452 | + for index, component in enumerate(components): | ||
| 453 | + path = os.fspath(join_path_native(path, component)) | ||
| 454 | + if allow_final_symlink and index == len(components) - 1: | ||
| 455 | + break | ||
| 425 | 456 | if osp.islink(path): | |
| 426 | - raise ValueError("Submodule checkout path %r contains a symbolic link" % self.path) | ||
| 457 | + raise ValueError("Submodule path %r contains a symbolic link" % relative_path) | ||
| 427 | 458 | return path | |
| 428 | 459 | ||
| 460 | + @staticmethod | ||
| 461 | + def _renames(source: PathLike, destination: PathLike) -> None: | ||
| 462 | + os.makedirs(osp.dirname(destination), exist_ok=True) | ||
| 463 | + os.rename(source, destination) | ||
| 464 | + # Match renames() cleanup, but stop before directory symlinks: Windows | ||
| 465 | + # rmdir() removes the link even when its target is nonempty. | ||
| 466 | + parent = osp.dirname(source) | ||
| 467 | + while parent and not osp.islink(parent): | ||
| 468 | + try: | ||
| 469 | + os.rmdir(parent) | ||
| 470 | + except OSError: | ||
| 471 | + break | ||
| 472 | + parent = osp.dirname(parent) | ||
| 473 | + | ||
| 429 | 474 | @classmethod | |
| 430 | 475 | def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None: | |
| 431 | 476 | """Write a ``.git`` file containing a (preferably) relative path to the actual | |
@@ -449,14 +494,19 @@ def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_ab | |||
| 449 | 494 | :param module_abspath: | |
| 450 | 495 | Absolute path to the bare repository. | |
| 451 | 496 | """ | |
| 497 | + # Git resolves metadata symlinks before interpreting core.worktree. | ||
| 498 | + # Path.resolve() also handles Windows symlinks on Python 3.7. | ||
| 499 | + module_abspath = str(Path(module_abspath).resolve()) | ||
| 500 | + working_tree_dir = str(Path(working_tree_dir).resolve()) | ||
| 452 | 501 | git_file = osp.join(working_tree_dir, ".git") | |
| 502 | + module_config = osp.join(module_abspath, "config") | ||
| 453 | 503 | rela_path = osp.relpath(module_abspath, start=working_tree_dir) | |
| 454 | 504 | if sys.platform == "win32" and osp.isfile(git_file): | |
| 455 | 505 | os.remove(git_file) | |
| 456 | 506 | with open(git_file, "wb") as fp: | |
| 457 | 507 | fp.write(("gitdir: %s" % rela_path).encode(defenc)) | |
| 458 | 508 | ||
| 459 | - with GitConfigParser(osp.join(module_abspath, "config"), read_only=False, merge_includes=False) as writer: | ||
| 509 | + with GitConfigParser(module_config, read_only=False, merge_includes=False) as writer: | ||
| 460 | 510 | writer.set_value( | |
| 461 | 511 | "core", | |
| 462 | 512 | "worktree", | |
@@ -567,6 +617,8 @@ def add( | |||
| 567 | 617 | name, | |
| 568 | 618 | url="invalid-temporary", | |
| 569 | 619 | ) | |
| 620 | + cls._checked_abspath(repo.working_tree_dir, cls.k_modules_file) | ||
| 621 | + sm._checkout_abspath(path) | ||
| 570 | 622 | if sm.exists(): | |
| 571 | 623 | # Reretrieve submodule from tree. | |
| 572 | 624 | try: | |
@@ -661,7 +713,9 @@ def add( | |||
| 661 | 713 | ||
| 662 | 714 | # We deliberately assume that our head matches our index! | |
| 663 | 715 | if mrepo: | |
| 664 | - sm.binsha = mrepo.head.commit.binsha | ||
| 716 | + # Release cat-file processes before callers move the checkout on Windows. | ||
| 717 | + with mrepo: | ||
| 718 | + sm.binsha = mrepo.head.commit.binsha | ||
| 665 | 719 | index.add([sm], write=True) | |
| 666 | 720 | ||
| 667 | 721 | return sm | |
@@ -1039,7 +1093,8 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = | |||
| 1039 | 1093 | self | |
| 1040 | 1094 | ||
| 1041 | 1095 | :raise ValueError: | |
| 1042 | - If the module path existed and was not empty, or was a file. | ||
| 1096 | + If the module path existed and was not empty, was a file, or had a | ||
| 1097 | + symbolic link in an intermediate component. | ||
| 1043 | 1098 | ||
| 1044 | 1099 | :note: | |
| 1045 | 1100 | Currently the method is not atomic, and it could leave the repository in an | |
@@ -1057,7 +1112,11 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = | |||
| 1057 | 1112 | return self | |
| 1058 | 1113 | # END handle no change | |
| 1059 | 1114 | ||
| 1060 | - module_checkout_abspath = join_path_native(str(self.repo.working_tree_dir), module_checkout_path) | ||
| 1115 | + if configuration: | ||
| 1116 | + self._checked_abspath(self.repo.working_tree_dir, self.k_modules_file) | ||
| 1117 | + # Validate the source before removing the destination. | ||
| 1118 | + cur_path = self.abspath | ||
| 1119 | + module_checkout_abspath = self._checkout_abspath(module_checkout_path, allow_final_symlink=True) | ||
| 1061 | 1120 | if osp.isfile(module_checkout_abspath): | |
| 1062 | 1121 | raise ValueError("Cannot move repository onto a file: %s" % module_checkout_abspath) | |
| 1063 | 1122 | # END handle target files | |
@@ -1089,10 +1148,9 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = | |||
| 1089 | 1148 | # END handle module | |
| 1090 | 1149 | ||
| 1091 | 1150 | # Move the module into place if possible. | |
| 1092 | - cur_path = self.abspath | ||
| 1093 | 1151 | renamed_module = False | |
| 1094 | 1152 | if module and osp.exists(cur_path): | |
| 1095 | - os.renames(cur_path, module_checkout_abspath) | ||
| 1153 | + self._renames(cur_path, module_checkout_abspath) | ||
| 1096 | 1154 | renamed_module = True | |
| 1097 | 1155 | ||
| 1098 | 1156 | if osp.isfile(osp.join(module_checkout_abspath, ".git")): | |
@@ -1123,7 +1181,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = | |||
| 1123 | 1181 | # END handle configuration flag | |
| 1124 | 1182 | except Exception: | |
| 1125 | 1183 | if renamed_module: | |
| 1126 | - os.renames(module_checkout_abspath, cur_path) | ||
| 1184 | + self._renames(module_checkout_abspath, cur_path) | ||
| 1127 | 1185 | # END undo module renaming | |
| 1128 | 1186 | raise | |
| 1129 | 1187 | # END handle undo rename | |
@@ -1180,6 +1238,12 @@ def remove( | |||
| 1180 | 1238 | Doesn't work atomically, as failure to remove any part of the submodule will | |
| 1181 | 1239 | leave an inconsistent state. | |
| 1182 | 1240 | ||
| 1241 | + :note: | ||
| 1242 | + Metadata-directory aliases under ``.git/modules`` are retained. A link | ||
| 1243 | + directly to the deleted repository becomes dangling; adding or initializing | ||
| 1244 | + the submodule again recreates its target. Linked parent directories remain | ||
| 1245 | + available to sibling submodules. | ||
| 1246 | + | ||
| 1183 | 1247 | :raise git.exc.InvalidGitRepositoryError: | |
| 1184 | 1248 | Thrown if the repository cannot be deleted. | |
| 1185 | 1249 | ||
@@ -1191,6 +1255,8 @@ def remove( | |||
| 1191 | 1255 | # END handle parameters | |
| 1192 | 1256 | ||
| 1193 | 1257 | self._validated_name(self.name) | |
| 1258 | + if configuration: | ||
| 1259 | + self._checked_abspath(self.repo.working_tree_dir, self.k_modules_file) | ||
| 1194 | 1260 | # Recursively remove children of this submodule. | |
| 1195 | 1261 | nc = 0 | |
| 1196 | 1262 | for csm in self.children(): | |
@@ -1209,7 +1275,7 @@ def remove( | |||
| 1209 | 1275 | ################################ | |
| 1210 | 1276 | if module and self.module_exists(): | |
| 1211 | 1277 | mod = self.module() | |
| 1212 | - git_dir = mod.git_dir | ||
| 1278 | + git_dir = str(Path(mod.git_dir).resolve()) | ||
| 1213 | 1279 | if force: | |
| 1214 | 1280 | # Take the fast lane and just delete everything in our module path. | |
| 1215 | 1281 | # TODO: If we run into permission problems, we have a highly | |
@@ -1450,6 +1516,9 @@ def rename(self, new_name: str) -> "Submodule": | |||
| 1450 | 1516 | ||
| 1451 | 1517 | self._validated_name(self.name) | |
| 1452 | 1518 | self._validated_name(new_name) | |
| 1519 | + destination_module_abspath = self._module_abspath(self.repo, self.path, new_name) | ||
| 1520 | + mod = self.module() | ||
| 1521 | + self._checked_abspath(self.repo.working_tree_dir, self.k_modules_file) | ||
| 1453 | 1522 | ||
| 1454 | 1523 | # .git/config | |
| 1455 | 1524 | with self.repo.config_writer() as pw: | |
@@ -1466,17 +1535,15 @@ def rename(self, new_name: str) -> "Submodule": | |||
| 1466 | 1535 | self._name = new_name | |
| 1467 | 1536 | ||
| 1468 | 1537 | # .git/modules | |
| 1469 | - mod = self.module() | ||
| 1470 | 1538 | if mod.has_separate_working_tree(): | |
| 1471 | - destination_module_abspath = self._module_abspath(self.repo, self.path, new_name) | ||
| 1472 | 1539 | source_dir = mod.git_dir | |
| 1473 | 1540 | # Let's be sure the submodule name is not so obviously tied to a directory. | |
| 1474 | 1541 | if str(destination_module_abspath).startswith(str(mod.git_dir)): | |
| 1475 | 1542 | tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4())) | |
| 1476 | - os.renames(source_dir, tmp_dir) | ||
| 1543 | + self._renames(source_dir, tmp_dir) | ||
| 1477 | 1544 | source_dir = tmp_dir | |
| 1478 | 1545 | # END handle self-containment | |
| 1479 | - os.renames(source_dir, destination_module_abspath) | ||
| 1546 | + self._renames(source_dir, destination_module_abspath) | ||
| 1480 | 1547 | if mod.working_tree_dir: | |
| 1481 | 1548 | self._write_git_file_and_module_config(mod.working_tree_dir, destination_module_abspath) | |
| 1482 | 1549 | # END move separate git repository | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1090,14 +1090,16 @@ def test_fetch_unsafe_branch_name(self, rw_repo, remote_repo): | |||
| 1090 | 1090 | class TestTimeouts(TestBase): | |
| 1091 | 1091 | @with_rw_repo("HEAD", bare=False) | |
| 1092 | 1092 | def test_timeout_funcs(self, repo): | |
| 1093 | + # Maintenance may outlive a timed-out fetch and race with fixture cleanup. | ||
| 1094 | + with repo.config_writer() as config: | ||
| 1095 | + config.set_value("maintenance", "auto", False) | ||
| 1096 | + config.set_value("gc", "auto", 0) # Older Git versions use auto-gc. | ||
| 1097 | + | ||
| 1093 | 1098 | # Force error code to prevent a race condition if the python thread is slow. | |
| 1094 | - default = Git.AutoInterrupt._status_code_if_terminate | ||
| 1095 | - Git.AutoInterrupt._status_code_if_terminate = -15 | ||
| 1096 | - for function in ["pull", "fetch"]: # Can't get push to time out. | ||
| 1097 | - f = getattr(repo.remotes.origin, function) | ||
| 1098 | - assert f is not None # Make sure these functions exist. | ||
| 1099 | - _ = f() # Make sure the function runs. | ||
| 1100 | - with pytest.raises(GitCommandError, match="kill_after_timeout=0 s"): | ||
| 1101 | - f(kill_after_timeout=0) | ||
| 1102 | - | ||
| 1103 | - Git.AutoInterrupt._status_code_if_terminate = default | ||
| 1099 | + with mock.patch.object(Git.AutoInterrupt, "_status_code_if_terminate", -15): | ||
| 1100 | + for function in ["pull", "fetch"]: # Can't get push to time out. | ||
| 1101 | + f = getattr(repo.remotes.origin, function) | ||
| 1102 | + assert f is not None # Make sure these functions exist. | ||
| 1103 | + _ = f() # Make sure the function runs. | ||
| 1104 | + with pytest.raises(GitCommandError, match="kill_after_timeout=0 s"): | ||
| 1105 | + f(kill_after_timeout=0) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments