| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent d9ddb55 commit f2550b6
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -654,6 +654,12 @@ class Git(metaclass=_GitMeta): | |||
| 654 | 654 | "--upload-pack", | |
| 655 | 655 | ] | |
| 656 | 656 | ||
| 657 | + unsafe_git_pathspec_from_file_options = [ | ||
| 658 | + # Reads pathspecs from a caller-controlled file. Some commands include an | ||
| 659 | + # unmatched pathspec in their error output, which can disclose the file. | ||
| 660 | + "--pathspec-from-file", | ||
| 661 | + ] | ||
| 662 | + | ||
| 657 | 663 | def __getstate__(self) -> Dict[str, Any]: | |
| 658 | 664 | return slots_to_dict(self, exclude=self._excluded_) | |
| 659 | 665 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1024,6 +1024,7 @@ def remove( | |||
| 1024 | 1024 | self, | |
| 1025 | 1025 | items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]], | |
| 1026 | 1026 | working_tree: bool = False, | |
| 1027 | + allow_unsafe_options: bool = False, | ||
| 1027 | 1028 | **kwargs: Any, | |
| 1028 | 1029 | ) -> List[str]: | |
| 1029 | 1030 | R"""Remove the given items from the index and optionally from the working tree | |
@@ -1054,6 +1055,10 @@ def remove( | |||
| 1054 | 1055 | physically removing the respective file. This may fail if there are | |
| 1055 | 1056 | uncommitted changes in it. | |
| 1056 | 1057 | ||
| 1058 | + :param allow_unsafe_options: | ||
| 1059 | + Allow unsafe options such as ``--pathspec-from-file`` to be passed to | ||
| 1060 | + :manpage:`git-rm(1)`. | ||
| 1061 | + | ||
| 1057 | 1062 | :param kwargs: | |
| 1058 | 1063 | Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as | |
| 1059 | 1064 | ``r`` to allow recursive removal. | |
@@ -1065,6 +1070,11 @@ def remove( | |||
| 1065 | 1070 | This is interesting to know in case you have provided a directory or globs. | |
| 1066 | 1071 | Paths are relative to the repository. | |
| 1067 | 1072 | """ | |
| 1073 | + if not allow_unsafe_options: | ||
| 1074 | + Git.check_unsafe_options( | ||
| 1075 | + options=Git._option_candidates([], kwargs), | ||
| 1076 | + unsafe_options=Git.unsafe_git_pathspec_from_file_options, | ||
| 1077 | + ) | ||
| 1068 | 1078 | args = [] | |
| 1069 | 1079 | if not working_tree: | |
| 1070 | 1080 | args.append("--cached") | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,6 +19,7 @@ | |||
| 19 | 19 | ||
| 20 | 20 | from typing import Any, Sequence, TYPE_CHECKING, Union | |
| 21 | 21 | ||
| 22 | + from git.cmd import Git | ||
| 22 | 23 | from git.types import Commit_ish, PathLike | |
| 23 | 24 | ||
| 24 | 25 | if TYPE_CHECKING: | |
@@ -62,6 +63,7 @@ def reset( | |||
| 62 | 63 | index: bool = True, | |
| 63 | 64 | working_tree: bool = False, | |
| 64 | 65 | paths: Union[PathLike, Sequence[PathLike], None] = None, | |
| 66 | + allow_unsafe_options: bool = False, | ||
| 65 | 67 | **kwargs: Any, | |
| 66 | 68 | ) -> "HEAD": | |
| 67 | 69 | """Reset our HEAD to the given commit optionally synchronizing the index and | |
@@ -84,12 +86,21 @@ def reset( | |||
| 84 | 86 | Single path or list of paths relative to the git root directory | |
| 85 | 87 | that are to be reset. This allows to partially reset individual files. | |
| 86 | 88 | ||
| 89 | + :param allow_unsafe_options: | ||
| 90 | + Allow unsafe options such as ``--pathspec-from-file`` to be passed to | ||
| 91 | + :manpage:`git-reset(1)`. | ||
| 92 | + | ||
| 87 | 93 | :param kwargs: | |
| 88 | 94 | Additional arguments passed to :manpage:`git-reset(1)`. | |
| 89 | 95 | ||
| 90 | 96 | :return: | |
| 91 | 97 | self | |
| 92 | 98 | """ | |
| 99 | + if not allow_unsafe_options: | ||
| 100 | + Git.check_unsafe_options( | ||
| 101 | + options=Git._option_candidates([commit], kwargs), | ||
| 102 | + unsafe_options=Git.unsafe_git_pathspec_from_file_options, | ||
| 103 | + ) | ||
| 93 | 104 | mode: Union[str, None] | |
| 94 | 105 | mode = "--soft" | |
| 95 | 106 | if index: | |
@@ -234,7 +245,12 @@ def rename(self, new_path: PathLike, force: bool = False) -> "Head": | |||
| 234 | 245 | self.path = "%s/%s" % (self._common_path_default, new_path) | |
| 235 | 246 | return self | |
| 236 | 247 | ||
| 237 | - def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]: | ||
| 248 | + def checkout( | ||
| 249 | + self, | ||
| 250 | + force: bool = False, | ||
| 251 | + allow_unsafe_options: bool = False, | ||
| 252 | + **kwargs: Any, | ||
| 253 | + ) -> Union["HEAD", "Head"]: | ||
| 238 | 254 | """Check out this head by setting the HEAD to this reference, by updating the | |
| 239 | 255 | index to reflect the tree we point to and by updating the working tree to | |
| 240 | 256 | reflect the latest index. | |
@@ -246,6 +262,10 @@ def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]: | |||
| 246 | 262 | If ``False``, :exc:`~git.exc.GitCommandError` will be raised in that | |
| 247 | 263 | situation. | |
| 248 | 264 | ||
| 265 | + :param allow_unsafe_options: | ||
| 266 | + Allow unsafe options such as ``--pathspec-from-file`` to be passed to | ||
| 267 | + :manpage:`git-checkout(1)`. | ||
| 268 | + | ||
| 249 | 269 | :param kwargs: | |
| 250 | 270 | Additional keyword arguments to be passed to git checkout, e.g. | |
| 251 | 271 | ``b="new_branch"`` to create a new branch at the given spot. | |
@@ -261,6 +281,11 @@ def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]: | |||
| 261 | 281 | the HEAD detached which is allowed and possible, but remains a special state | |
| 262 | 282 | that some tools might not be able to handle. | |
| 263 | 283 | """ | |
| 284 | + if not allow_unsafe_options: | ||
| 285 | + Git.check_unsafe_options( | ||
| 286 | + options=Git._option_candidates([], kwargs), | ||
| 287 | + unsafe_options=Git.unsafe_git_pathspec_from_file_options, | ||
| 288 | + ) | ||
| 264 | 289 | kwargs["f"] = force | |
| 265 | 290 | if kwargs["f"] is False: | |
| 266 | 291 | kwargs.pop("f") | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -215,6 +215,18 @@ def test_option_candidates_ignore_untransformed_kwargs(self): | |||
| 215 | 215 | ||
| 216 | 216 | self.assertEqual(options, ["--max-count"]) | |
| 217 | 217 | ||
| 218 | + def test_option_candidates_include_falsey_non_boolean_values(self): | ||
| 219 | + kwargs = {"pathspec_from_file": 0} | ||
| 220 | + candidates = Git._option_candidates(kwargs=kwargs) | ||
| 221 | + | ||
| 222 | + self.assertEqual(candidates, ["--pathspec-from-file"]) | ||
| 223 | + self.assertEqual(self.git.transform_kwargs(**kwargs), ["--pathspec-from-file=0"]) | ||
| 224 | + with self.assertRaises(UnsafeOptionError): | ||
| 225 | + Git.check_unsafe_options( | ||
| 226 | + options=candidates, | ||
| 227 | + unsafe_options=Git.unsafe_git_pathspec_from_file_options, | ||
| 228 | + ) | ||
| 229 | + | ||
| 218 | 230 | def test_option_candidates_include_split_single_char_option_values(self): | |
| 219 | 231 | cases = [ | |
| 220 | 232 | ({"n": "--upload-pack=helper"}, ["-n", "--upload-pack=helper"], ["--upload-pack"]), | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -212,6 +212,32 @@ def test_checkout_rejects_unsafe_prefix(self, rw_repo): | |||
| 212 | 212 | rw_repo.index.checkout(prefix=f"{target}/", allow_unsafe_options=True) | |
| 213 | 213 | self.assertTrue(osp.isfile(osp.join(target, "CHANGES"))) | |
| 214 | 214 | ||
| 215 | + @with_rw_repo("HEAD") | ||
| 216 | + def test_remove_rejects_pathspec_from_file(self, rw_repo): | ||
| 217 | + with tempfile.TemporaryDirectory() as tdir: | ||
| 218 | + pathspecs = Path(tdir) / "pathspecs" | ||
| 219 | + pathspecs.write_bytes(b"unmatched-path-one\nunmatched-path-two") | ||
| 220 | + for option_name in ("pathspec_from_file", "pathspec_from"): | ||
| 221 | + with self.assertRaises(UnsafeOptionError): | ||
| 222 | + rw_repo.index.remove( | ||
| 223 | + [], | ||
| 224 | + pathspec_file_nul=True, | ||
| 225 | + **{option_name: str(pathspecs)}, | ||
| 226 | + ) | ||
| 227 | + | ||
| 228 | + @with_rw_repo("HEAD") | ||
| 229 | + def test_remove_allows_explicit_pathspec_from_file(self, rw_repo): | ||
| 230 | + with tempfile.TemporaryDirectory() as tdir: | ||
| 231 | + pathspecs = Path(tdir) / "pathspecs" | ||
| 232 | + pathspecs.write_bytes(b"CHANGES\0") | ||
| 233 | + removed = rw_repo.index.remove( | ||
| 234 | + [], | ||
| 235 | + pathspec_from_file=str(pathspecs), | ||
| 236 | + pathspec_file_nul=True, | ||
| 237 | + allow_unsafe_options=True, | ||
| 238 | + ) | ||
| 239 | + assert "CHANGES" in removed | ||
| 240 | + | ||
| 215 | 241 | def __init__(self, *args): | |
| 216 | 242 | super().__init__(*args) | |
| 217 | 243 | self._reset_progress() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -262,6 +262,51 @@ def test_head_checkout_detached_head(self, rw_repo): | |||
| 262 | 262 | assert isinstance(res, SymbolicReference) | |
| 263 | 263 | assert res.name == "HEAD" | |
| 264 | 264 | ||
| 265 | + @with_rw_repo("HEAD") | ||
| 266 | + def test_head_checkout_rejects_pathspec_from_file(self, rw_repo): | ||
| 267 | + with tempfile.TemporaryDirectory() as tdir: | ||
| 268 | + pathspecs = Path(tdir) / "pathspecs" | ||
| 269 | + pathspecs.write_bytes(b"unmatched-path-one\nunmatched-path-two") | ||
| 270 | + for option_name in ("pathspec_from_file", "pathspec_from"): | ||
| 271 | + with self.assertRaises(UnsafeOptionError): | ||
| 272 | + rw_repo.active_branch.checkout( | ||
| 273 | + pathspec_file_nul=True, | ||
| 274 | + **{option_name: str(pathspecs)}, | ||
| 275 | + ) | ||
| 276 | + | ||
| 277 | + @with_rw_repo("HEAD") | ||
| 278 | + def test_head_reset_rejects_pathspec_from_file(self, rw_repo): | ||
| 279 | + with tempfile.TemporaryDirectory() as tdir: | ||
| 280 | + pathspecs = Path(tdir) / "pathspecs" | ||
| 281 | + pathspecs.write_bytes(b"unmatched-path-one\nunmatched-path-two") | ||
| 282 | + for option_name in ("pathspec_from_file", "pathspec_from"): | ||
| 283 | + with self.assertRaises(UnsafeOptionError): | ||
| 284 | + rw_repo.head.reset( | ||
| 285 | + pathspec_file_nul=True, | ||
| 286 | + **{option_name: str(pathspecs)}, | ||
| 287 | + ) | ||
| 288 | + for option_name in ("--pathspec-from-file", "--pathspec-from"): | ||
| 289 | + with self.assertRaises(UnsafeOptionError): | ||
| 290 | + rw_repo.head.reset( | ||
| 291 | + f"{option_name}={pathspecs}", | ||
| 292 | + pathspec_file_nul=True, | ||
| 293 | + ) | ||
| 294 | + | ||
| 295 | + @with_rw_repo("HEAD") | ||
| 296 | + def test_head_commands_allow_explicit_pathspec_from_file(self, rw_repo): | ||
| 297 | + with tempfile.TemporaryDirectory() as tdir: | ||
| 298 | + pathspecs = Path(tdir) / "pathspecs" | ||
| 299 | + pathspecs.write_bytes(b"CHANGES\0") | ||
| 300 | + options = { | ||
| 301 | + "pathspec_from_file": str(pathspecs), | ||
| 302 | + "pathspec_file_nul": True, | ||
| 303 | + "allow_unsafe_options": True, | ||
| 304 | + } | ||
| 305 | + head = rw_repo.head | ||
| 306 | + branch = rw_repo.active_branch | ||
| 307 | + assert head.reset(**options) is head | ||
| 308 | + assert branch.checkout(**options) == branch | ||
| 309 | + | ||
| 265 | 310 | @with_rw_repo("0.1.6") | |
| 266 | 311 | def test_head_reset(self, rw_repo): | |
| 267 | 312 | cur_head = rw_repo.head | |
| Back | FazBrowse Home | New Git URL |
0 commit comments