Skip to content

Commit 4ed855f

Browse files
jdclaude
andcommitted
fix(utils): decode CommandError stdout safely so __str__ never raises
`CommandError.__str__` decoded captured stdout as strict UTF-8, so any non-UTF-8 bytes from a subprocess (legacy locales, binary diff fragments, truncated multi-byte sequences) would turn `str(error)` into a `UnicodeDecodeError`. Every error-formatting site is affected — the top-level CLI handler in `cli.py:104`, log messages, etc. — converting the failure into an unhandled traceback instead of a clean message. Switch to `decode(errors="replace")`. Invalid bytes become the U+FFFD replacement character; all other formatting is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Change-Id: I3e3fd853218a6c4294b17e9529794320288124e5
1 parent 7fff439 commit 4ed855f

2 files changed

Lines changed: 14 additions & 1 deletion

File tree

mergify_cli/tests/test_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131
import pathlib
3232

3333

34+
def test_command_error_str_handles_non_utf8_stdout() -> None:
35+
# Some git invocations (e.g. legacy locales) can emit non-UTF-8 bytes;
36+
# str(CommandError) must not raise — error paths depend on it.
37+
error = utils.CommandError(("git", "show", "abc"), 1, b"\xff\xfe broken")
38+
assert "failed to run `git show abc`" in str(error)
39+
40+
3441
@pytest.mark.usefixtures("_git_repo")
3542
async def test_get_branch_name() -> None:
3643
assert await utils.git_get_branch_name() == "main"

mergify_cli/utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,13 @@ class CommandError(Exception):
9292
stdout: bytes
9393

9494
def __str__(self) -> str:
95-
return f"failed to run `{' '.join(self.command_args)}`: {self.stdout.decode()}"
95+
# ``errors="replace"`` so str(CommandError) never raises on
96+
# non-UTF-8 process output — callers in error paths (warnings,
97+
# CLI top-level handler) rely on this being safe.
98+
return (
99+
f"failed to run `{' '.join(self.command_args)}`: "
100+
f"{self.stdout.decode(errors='replace')}"
101+
)
96102

97103

98104
class MergifyError(click.ClickException):

0 commit comments

Comments
 (0)