Skip to content

Commit b4b61be

Browse files
fix(gitea): return repo settings as bytes
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) <noreply@anthropic.com>
1 parent 31d7dd0 commit b4b61be

2 files changed

Lines changed: 53 additions & 4 deletions

File tree

pr_agent/git_providers/gitea_provider.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -605,11 +605,11 @@ def get_pr_labels(self,update=False) -> List[str]:
605605

606606
return [label.name for label in labels]
607607

608-
def get_repo_settings(self) -> str:
608+
def get_repo_settings(self) -> bytes:
609609
"""Get repository settings"""
610610
if not self.repo_settings:
611611
self.logger.error("Repository settings not found")
612-
return ""
612+
return b""
613613

614614
response = self.repo_api.get_file_content(
615615
owner=self.owner,
@@ -619,9 +619,13 @@ def get_repo_settings(self) -> str:
619619
)
620620
if not response:
621621
self.logger.error("Failed to get repository settings")
622-
return ""
622+
return b""
623623

624-
return response
624+
# utils.apply_repo_settings() writes this via os.write() and later
625+
# calls .decode() on it, so it must be bytes to match the GitHub/
626+
# GitLab/Bitbucket contract. get_file_content() decodes the raw bytes
627+
# to str, so re-encode here (see issue #2347).
628+
return response.encode('utf-8')
625629

626630
def get_user_id(self) -> str:
627631
"""Get the ID of the authenticated user"""

tests/unittest/test_gitea_provider.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,48 @@ def call_api_side_effect(path, method, **kwargs):
103103
args, kwargs = mock_api_client.call_api.call_args
104104
assert args[0] == '/repos/owner/repo/pulls/123/commits'
105105
assert kwargs.get('auth_settings') == ['AuthorizationHeaderToken']
106+
107+
def test_get_repo_settings_returns_bytes(self):
108+
"""Regression for #2347: get_repo_settings must return bytes so that
109+
utils.apply_repo_settings can os.write() it and later .decode() it. The
110+
Gitea raw-file API yields str (unlike GitHub/GitLab/Bitbucket, which hand
111+
back bytes), so the provider must encode before returning."""
112+
from pr_agent.git_providers.gitea_provider import GiteaProvider
113+
114+
toml = '[pr_reviewer]\nnum_code_suggestions = 4\n'
115+
provider = GiteaProvider.__new__(GiteaProvider)
116+
provider.logger = MagicMock()
117+
provider.owner = 'owner'
118+
provider.repo = 'repo'
119+
provider.sha = 'sha1'
120+
provider.repo_settings = '.pr_agent.toml'
121+
provider.repo_api = MagicMock()
122+
provider.repo_api.get_file_content.return_value = toml # API decodes to str
123+
124+
result = provider.get_repo_settings()
125+
126+
assert isinstance(result, bytes)
127+
assert result == toml.encode('utf-8')
128+
# The bytes must survive the exact operations utils.py performs on them.
129+
assert result.decode() == toml
130+
131+
def test_get_repo_settings_empty_bytes_when_unset_or_missing(self):
132+
"""No settings path configured, or empty/absent file: return empty
133+
bytes, so every code path honours the -> bytes contract (not just the
134+
success path) and a caller can never receive a str."""
135+
from pr_agent.git_providers.gitea_provider import GiteaProvider
136+
137+
unset = GiteaProvider.__new__(GiteaProvider)
138+
unset.logger = MagicMock()
139+
unset.repo_settings = None
140+
assert unset.get_repo_settings() == b""
141+
142+
empty = GiteaProvider.__new__(GiteaProvider)
143+
empty.logger = MagicMock()
144+
empty.owner = 'owner'
145+
empty.repo = 'repo'
146+
empty.sha = 'sha1'
147+
empty.repo_settings = '.pr_agent.toml'
148+
empty.repo_api = MagicMock()
149+
empty.repo_api.get_file_content.return_value = ''
150+
assert empty.get_repo_settings() == b""

0 commit comments

Comments
 (0)