diff --git a/mergify_cli/stack/push.py b/mergify_cli/stack/push.py index 75219675..033d29e8 100644 --- a/mergify_cli/stack/push.py +++ b/mergify_cli/stack/push.py @@ -24,6 +24,8 @@ import sys import typing +import rich.markup + from mergify_cli import console from mergify_cli import console_error from mergify_cli import utils @@ -582,8 +584,16 @@ async def stack_push( with console.status("Fetching old PR heads for comparison..."): try: await fetch_old_pr_heads(remote, updated_pr_numbers) - except utils.CommandError: - pass # Non-fatal: change type will be "unknown" + except utils.CommandError as exc: + # Non-fatal: change type will be "unknown" — but surface + # the underlying error so the user can fix it. Escape the + # exception text since it can contain `[`/`]` that Rich + # would otherwise interpret as markup tags. + console.log( + f"[orange]Could not fetch old PR heads; revision-history " + f"change types will fall back to 'unknown': " + f"{rich.markup.escape(str(exc))}[/]", + ) # Detect change types before force-push overwrites refs change_types: dict[str, str] = {} diff --git a/mergify_cli/tests/test_utils.py b/mergify_cli/tests/test_utils.py index 2114ee5c..469ba475 100644 --- a/mergify_cli/tests/test_utils.py +++ b/mergify_cli/tests/test_utils.py @@ -31,6 +31,13 @@ import pathlib +def test_command_error_str_handles_non_utf8_stdout() -> None: + # Some git invocations (e.g. legacy locales) can emit non-UTF-8 bytes; + # str(CommandError) must not raise — error paths depend on it. + error = utils.CommandError(("git", "show", "abc"), 1, b"\xff\xfe broken") + assert "failed to run `git show abc`" in str(error) + + @pytest.mark.usefixtures("_git_repo") async def test_get_branch_name() -> None: assert await utils.git_get_branch_name() == "main" diff --git a/mergify_cli/utils.py b/mergify_cli/utils.py index 4f5addd1..3d875a1b 100644 --- a/mergify_cli/utils.py +++ b/mergify_cli/utils.py @@ -92,7 +92,13 @@ class CommandError(Exception): stdout: bytes def __str__(self) -> str: - return f"failed to run `{' '.join(self.command_args)}`: {self.stdout.decode()}" + # ``errors="replace"`` so str(CommandError) never raises on + # non-UTF-8 process output — callers in error paths (warnings, + # CLI top-level handler) rely on this being safe. + return ( + f"failed to run `{' '.join(self.command_args)}`: " + f"{self.stdout.decode(errors='replace')}" + ) class MergifyError(click.ClickException):