From b4b61be042ac30f0f8c8df03386e3959fb8075ac Mon Sep 17 00:00:00 2001 From: Ismael Martinez Ramos Date: Mon, 8 Jun 2026 20:39:00 +0100 Subject: [PATCH] fix(gitea): return repo settings as bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GiteaProvider.get_repo_settings() returned a str, but utils.apply_repo_settings() writes the value with os.write() and later calls .decode() on it, both of which require bytes — the contract the GitHub, GitLab and Bitbucket providers already follow. On Gitea this raised "a bytes-like object is required, not 'str'" and broke repo-level .pr_agent.toml loading. Encode the content before returning, mirroring the Bitbucket provider, and add regression tests for the bytes contract and the empty cases. See #2347. Co-Authored-By: Claude Opus 4.8 (1M context) --- pr_agent/git_providers/gitea_provider.py | 12 ++++--- tests/unittest/test_gitea_provider.py | 45 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/pr_agent/git_providers/gitea_provider.py b/pr_agent/git_providers/gitea_provider.py index 89a6248e9b..a00be44caa 100644 --- a/pr_agent/git_providers/gitea_provider.py +++ b/pr_agent/git_providers/gitea_provider.py @@ -605,11 +605,11 @@ def get_pr_labels(self,update=False) -> List[str]: return [label.name for label in labels] - def get_repo_settings(self) -> str: + def get_repo_settings(self) -> bytes: """Get repository settings""" if not self.repo_settings: self.logger.error("Repository settings not found") - return "" + return b"" response = self.repo_api.get_file_content( owner=self.owner, @@ -619,9 +619,13 @@ def get_repo_settings(self) -> str: ) if not response: self.logger.error("Failed to get repository settings") - return "" + return b"" - return response + # utils.apply_repo_settings() writes this via os.write() and later + # calls .decode() on it, so it must be bytes to match the GitHub/ + # GitLab/Bitbucket contract. get_file_content() decodes the raw bytes + # to str, so re-encode here (see issue #2347). + return response.encode('utf-8') def get_user_id(self) -> str: """Get the ID of the authenticated user""" diff --git a/tests/unittest/test_gitea_provider.py b/tests/unittest/test_gitea_provider.py index 4174b398d0..f3d2bc0620 100644 --- a/tests/unittest/test_gitea_provider.py +++ b/tests/unittest/test_gitea_provider.py @@ -103,3 +103,48 @@ def call_api_side_effect(path, method, **kwargs): args, kwargs = mock_api_client.call_api.call_args assert args[0] == '/repos/owner/repo/pulls/123/commits' assert kwargs.get('auth_settings') == ['AuthorizationHeaderToken'] + + def test_get_repo_settings_returns_bytes(self): + """Regression for #2347: get_repo_settings must return bytes so that + utils.apply_repo_settings can os.write() it and later .decode() it. The + Gitea raw-file API yields str (unlike GitHub/GitLab/Bitbucket, which hand + back bytes), so the provider must encode before returning.""" + from pr_agent.git_providers.gitea_provider import GiteaProvider + + toml = '[pr_reviewer]\nnum_code_suggestions = 4\n' + provider = GiteaProvider.__new__(GiteaProvider) + provider.logger = MagicMock() + provider.owner = 'owner' + provider.repo = 'repo' + provider.sha = 'sha1' + provider.repo_settings = '.pr_agent.toml' + provider.repo_api = MagicMock() + provider.repo_api.get_file_content.return_value = toml # API decodes to str + + result = provider.get_repo_settings() + + assert isinstance(result, bytes) + assert result == toml.encode('utf-8') + # The bytes must survive the exact operations utils.py performs on them. + assert result.decode() == toml + + def test_get_repo_settings_empty_bytes_when_unset_or_missing(self): + """No settings path configured, or empty/absent file: return empty + bytes, so every code path honours the -> bytes contract (not just the + success path) and a caller can never receive a str.""" + from pr_agent.git_providers.gitea_provider import GiteaProvider + + unset = GiteaProvider.__new__(GiteaProvider) + unset.logger = MagicMock() + unset.repo_settings = None + assert unset.get_repo_settings() == b"" + + empty = GiteaProvider.__new__(GiteaProvider) + empty.logger = MagicMock() + empty.owner = 'owner' + empty.repo = 'repo' + empty.sha = 'sha1' + empty.repo_settings = '.pr_agent.toml' + empty.repo_api = MagicMock() + empty.repo_api.get_file_content.return_value = '' + assert empty.get_repo_settings() == b""