Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Check joined short-option values before Git execution
<!-- agent -->
GHSA-wvpp-8hx9-p66j reports that unsafe-option checks omitted the value
joined to a one-character option when split_single_char_options was false. A
regression test reproduced the mismatch: GitPython checked only -n even though
it emitted a joined -nVALUE token that Git parses as clustered short options.

Collect the exact joined token for unsplit one-character keyword arguments so
the existing clustered-short-option validation sees every option character.
The split form and long-option behavior remain unchanged.

A broader audit confirmed that all guarded keyword-forwarding APIs use
_option_candidates, including clone, ls-remote, fetch, pull, push, archive,
revision, diff, checkout-index, and tag paths. Git cf5497b14 confirms repeated
short-option parsing within a joined token. Focused candidate and unsafe-option
tests pass.

Assisted-by: GPT 5.6
Co-authored-by: GPT 5.6 <codex@openai.com>
  • Loading branch information
Byron and codex committed Aug 4, 2026
commit 96a888f4d782cb2f80452148e48e60ce4af6d541
12 changes: 10 additions & 2 deletions git/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -1044,13 +1044,21 @@ def _option_candidates(cls, args: Sequence[Any] = (), kwargs: Optional[Mapping[s
values = value if isinstance(value, (list, tuple)) else (value,)
if any(value is True or (value is not False and value is not None) for value in values):
key = str(key)
options.append(f"-{key}" if len(key) == 1 else f"--{dashify(key)}")
if len(key) == 1 and split_single_char_options:
if len(key) != 1:
options.append(f"--{dashify(key)}")
elif split_single_char_options:
options.append(f"-{key}")
options.extend(
str(value)
for value in values
if value is not True and value not in (False, None) and str(value).startswith("-")
)
else:
options.extend(
f"-{key}" if value is True else f"-{key}{value}"
for value in values
if value is True or (value is not False and value is not None)
)
Comment thread
Byron marked this conversation as resolved.
return options

AutoInterrupt: TypeAlias = _AutoInterrupt
Expand Down
10 changes: 9 additions & 1 deletion test/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,15 @@ def test_option_candidates_include_split_single_char_option_values(self):

unsplit_kwargs = {"n": "--upload-pack=helper", "split_single_char_options": False}
self.assertEqual(self.git.transform_kwargs(**unsplit_kwargs), ["-n--upload-pack=helper"])
self.assertEqual(Git._option_candidates(kwargs=unsplit_kwargs), ["-n"])
self.assertEqual(Git._option_candidates(kwargs=unsplit_kwargs), ["-n--upload-pack=helper"])

def test_option_candidates_include_joined_single_char_option_values(self):
kwargs = {"n": "uhelper", "split_single_char_options": False}
candidates = Git._option_candidates(kwargs=kwargs)

self.assertEqual(candidates, ["-nuhelper"])
with self.assertRaises(UnsafeOptionError):
Git.check_unsafe_options(options=candidates, unsafe_options=["-u"])

_shell_cases = (
# value_in_call, value_from_class, expected_popen_arg
Expand Down