@@ -960,18 +960,89 @@ def _canonicalize_option_name(cls, option: str) -> str:
960960 return dashify (option_tokens [0 ])
961961
962962 @classmethod
963- def check_unsafe_options (cls , options : List [str ], unsafe_options : List [str ]) -> None :
963+ def _option_token (cls , option : str ) -> str :
964+ option_name = option .split ("=" , 1 )[0 ]
965+ option_tokens = option_name .split (None , 1 )
966+ if not option_tokens :
967+ return ""
968+ return option_tokens [0 ]
969+
970+ @classmethod
971+ def _is_long_option (cls , option : str , bare_options_are_long : bool = True ) -> bool :
972+ """Return whether an option is a long option before canonicalization."""
973+ option_token = cls ._option_token (option )
974+ if not option_token :
975+ return False
976+
977+ if option_token .startswith ("--" ):
978+ return True
979+ if option_token .startswith ("-" ):
980+ return False
981+ return bare_options_are_long and len (option_token ) > 1
982+
983+ @classmethod
984+ def _matches_unsafe_short_option (
985+ cls , option : str , canonical_unsafe_option : str , unsafe_option_is_long : bool
986+ ) -> bool :
987+ """Return whether a single-dash option matches an unsafe short option."""
988+ if unsafe_option_is_long or len (canonical_unsafe_option ) != 1 :
989+ return False
990+
991+ option_token = cls ._option_token (option )
992+ if not option_token :
993+ return False
994+
995+ if not option_token .startswith ("-" ) or option_token .startswith ("--" ):
996+ return False
997+
998+ clusterable_flags = frozenset ("46flnqsv" )
999+ for option_char in option_token [1 :]:
1000+ if option_char == canonical_unsafe_option :
1001+ return True
1002+ if option_char not in clusterable_flags :
1003+ return False
1004+ return False
1005+
1006+ @classmethod
1007+ def check_unsafe_options (
1008+ cls , options : List [str ], unsafe_options : List [str ], bare_options_are_long : bool = True
1009+ ) -> None :
9641010 """Check for unsafe options.
9651011
9661012 Some options that are passed to ``git <command>`` can be used to execute
9671013 arbitrary commands. These are blocked by default.
9681014 """
9691015 # Options can be of the form `foo`, `--foo`, `--foo bar`, or `--foo=bar`.
970- canonical_unsafe_options = {cls ._canonicalize_option_name (option ): option for option in unsafe_options }
1016+ # We also treat long-option prefix forms as unsafe, matching Git's option
1017+ # parser behavior for long options.
1018+ canonical_unsafe_options = [
1019+ (cls ._canonicalize_option_name (option ), option , cls ._is_long_option (option )) for option in unsafe_options
1020+ ]
9711021 for option in options :
972- unsafe_option = canonical_unsafe_options .get (cls ._canonicalize_option_name (option ))
973- if unsafe_option is not None :
974- raise UnsafeOptionError (f"{ unsafe_option } is not allowed, use `allow_unsafe_options=True` to allow it." )
1022+ option_token = cls ._option_token (option )
1023+ if not bare_options_are_long and not option_token .startswith ("-" ):
1024+ continue
1025+ canonical_option = cls ._canonicalize_option_name (option )
1026+ if not canonical_option :
1027+ continue
1028+ option_is_long = cls ._is_long_option (option , bare_options_are_long = bare_options_are_long )
1029+ for (
1030+ canonical_unsafe_option ,
1031+ unsafe_option ,
1032+ unsafe_option_is_long ,
1033+ ) in canonical_unsafe_options :
1034+ if (
1035+ canonical_option == canonical_unsafe_option
1036+ or (
1037+ option_is_long
1038+ and unsafe_option_is_long
1039+ and canonical_unsafe_option .startswith (canonical_option )
1040+ )
1041+ or cls ._matches_unsafe_short_option (option , canonical_unsafe_option , unsafe_option_is_long )
1042+ ):
1043+ raise UnsafeOptionError (
1044+ f"{ unsafe_option } is not allowed, use `allow_unsafe_options=True` to allow it."
1045+ )
9751046
9761047 AutoInterrupt : TypeAlias = _AutoInterrupt
9771048
0 commit comments