Skip to content

fix(gitea): fetch PR-specific commits instead of all repo commits#2309

Open
lawrence3699 wants to merge 1 commit intoThe-PR-Agent:mainfrom
lawrence3699:fix/gitea-pr-commits-endpoint
Open

fix(gitea): fetch PR-specific commits instead of all repo commits#2309
lawrence3699 wants to merge 1 commit intoThe-PR-Agent:mainfrom
lawrence3699:fix/gitea-pr-commits-endpoint

Conversation

@lawrence3699
Copy link
Copy Markdown

PR Type

Bug fix

Description

GiteaProvider.__init__ calls list_all_commits(owner, repo) which hits GET /repos/{owner}/{repo}/commits — the repo-level commits endpoint. This returns commits from the default branch, not from the PR branch. As a result, self.last_commit points at the wrong commit.

The same file already has get_pr_commits(owner, repo, pr_number) which correctly calls GET /repos/{owner}/{repo}/pulls/{pr_number}/commits, and it's already used by get_commit_messages() (line 402). This fix switches __init__ to use that method too.

Before: last_commit is the oldest commit on the default branch → inline comments posted against wrong commit, persistent reviews never detect new changes, _get_file_content_from_latest_commit() fetches wrong content.

After: last_commit is the latest commit on the PR branch, consistent with the GitHub provider.

Since get_pr_commits returns raw JSON dicts (unlike the giteapy model objects from list_all_commits), the fix wraps the commit dict in a SimpleNamespace so existing .sha / .html_url attribute access continues to work.

Added a regression test that verifies the PR-specific endpoint is called and last_commit.sha matches the expected value.

Fixes #2206

Copilot AI review requested due to automatic review settings April 9, 2026 08:44
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Fix Gitea provider to fetch PR-specific commits instead of all repo commits

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Changed PR commits endpoint from repo-level to PR-specific endpoint
• Replaced list_all_commits() with get_pr_commits() API call
• Added proper handling for empty commits list with SimpleNamespace wrapper
• Added regression test verifying PR-specific commits are fetched
Diagram
flowchart LR
  A["GiteaProvider.__init__"] -->|"Previously called"| B["repo_api.list_all_commits<br/>all repo commits"]
  A -->|"Now calls"| C["repo_api.get_pr_commits<br/>PR-specific commits"]
  C --> D["SimpleNamespace wrapper<br/>for last_commit"]
  D --> E["Proper null handling<br/>if no commits"]
Loading

Grey Divider

File Changes

1. pr_agent/git_providers/gitea_provider.py 🐞 Bug fix +9/-3

Switch to PR-specific commits endpoint with null safety

• Changed from list_all_commits() to get_pr_commits() with pr_number parameter
• Added SimpleNamespace import for wrapping commit data
• Added null check and proper handling when PR commits list is empty
• Set last_commit to None if no commits exist instead of causing errors

pr_agent/git_providers/gitea_provider.py


2. tests/unittest/test_gitea_provider.py 🧪 Tests +68/-0

Add regression test for PR-specific commits endpoint

• Added json import for test data serialization
• Added comprehensive regression test test_init_uses_pr_commits_not_repo_commits
• Test verifies PR-specific commits endpoint is called, not repo-level endpoint
• Test validates last_commit object contains correct SHA and URL from PR commits

tests/unittest/test_gitea_provider.py


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects bot commented Apr 9, 2026

Code Review by Qodo

🐞 Bugs (1)   📘 Rule violations (0)   📎 Requirement gaps (2)   🎨 UX Issues (0)
🐞\ ☼ Reliability (1)
📎\ ≡ Correctness (1) ☼ Reliability (1)

Grey Divider


Action required

1. last_commit uses [-1] 📎
Description
get_pr_commits() returns commits newest-first per the Gitea PR commits endpoint behavior, but
__init__ selects self.pr_commits[-1], which points to the oldest commit. This can cause
commit-dependent behaviors to reference a non-head SHA and fail to detect new PR commits correctly.
Code

pr_agent/git_providers/gitea_provider.py[R95-99]

+            self.pr_commits = pr_commits_data if pr_commits_data else []
+            if self.pr_commits:
+                self.last_commit = SimpleNamespace(**self.pr_commits[-1])
+            else:
+                self.last_commit = None
Evidence
Compliance requires selecting the PR head commit from a newest-first PR commits list; the new code
wraps self.pr_commits[-1] into last_commit, which selects the wrong element under newest-first
ordering.

Gitea provider must correctly identify the latest PR commit given newest-first ordering
Commit-dependent features must operate on the PR head commit in Gitea provider
pr_agent/git_providers/gitea_provider.py[95-99]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GiteaProvider.__init__` sets `last_commit` from `self.pr_commits[-1]`, but the PR commits endpoint returns commits newest-first; this makes `last_commit` the oldest commit instead of the PR head.

## Issue Context
Downstream features use `last_commit`/`last_commit_id` as the PR head SHA for URLs, change detection, inline comments, and file content retrieval.

## Fix Focus Areas
- pr_agent/git_providers/gitea_provider.py[95-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Null last_commit crash 🐞
Description
GiteaProvider.__init__ can set self.last_commit/self.last_commit_id to None when
get_pr_commits() returns an empty list, but methods like get_latest_commit_url() and
_get_file_content_from_latest_commit() dereference .html_url/.sha unconditionally, causing
AttributeError at runtime. This is reachable because RepoApi.get_pr_commits() returns [] on
API errors/exceptions, so a transient Gitea/API issue will crash later tool flows (e.g., PR
description header generation).
Code

pr_agent/git_providers/gitea_provider.py[R95-101]

+            self.pr_commits = pr_commits_data if pr_commits_data else []
+            if self.pr_commits:
+                self.last_commit = SimpleNamespace(**self.pr_commits[-1])
+            else:
+                self.last_commit = None
            self.last_commit_id = self.last_commit
            self.base_sha = self.pr.base.sha if self.pr.base.sha else ""
Evidence
The new init path explicitly sets last_commit=None on empty commits, while other code paths assume
last_commit_id.sha and last_commit.html_url always exist. Since get_pr_commits() swallows
exceptions and returns an empty list, this None state is reachable during normal operation under API
failures/timeouts and will later crash in multiple places.

pr_agent/git_providers/gitea_provider.py[87-103]
pr_agent/git_providers/gitea_provider.py[232-234]
pr_agent/git_providers/gitea_provider.py[442-448]
pr_agent/git_providers/gitea_provider.py[1019-1050]
pr_agent/tools/pr_description.py[509-512]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`GiteaProvider.__init__` can leave `self.last_commit` as `None` when PR commits cannot be fetched (empty list), but later methods assume `last_commit`/`last_commit_id` always exist and unconditionally access `.sha`/`.html_url`, leading to `AttributeError` crashes.

### Issue Context
`RepoApi.get_pr_commits()` returns `[]` on exceptions, making the empty-commits path reachable during transient API/network failures.

### Fix Focus Areas
- pr_agent/git_providers/gitea_provider.py[87-103]
- pr_agent/git_providers/gitea_provider.py[232-234]
- pr_agent/git_providers/gitea_provider.py[442-448]
- pr_agent/git_providers/gitea_provider.py[1019-1050]

### Suggested fix
- In `__init__`, if `get_pr_commits()` returns empty:
 - Prefer a safe fallback: build `last_commit` from `self.pr.head.sha` (already available as `self.sha`) and (if needed) synthesize an `html_url`, or
 - Fail fast with a clear exception/log and stop initialization (instead of leaving `last_commit=None`).
- Add guards in `get_latest_commit_url()` and `_get_file_content_from_latest_commit()` to avoid dereferencing `None` (e.g., return "" / use `self.sha` fallback) so downstream tools don’t crash.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Test assumes oldest-first commits 📎
Description
The new regression test builds PR commits JSON in oldest→newest order, which will not catch the
required newest-first behavior from Gitea. This can allow an incorrect last_commit selection
strategy to pass while still violating head-commit selection in real usage.
Code

tests/unittest/test_gitea_provider.py[R132-135]

+        pr_commits_json = json.dumps([
+            {"sha": "older_commit_sha", "html_url": "https://gitea.example.com/owner/repo/commit/older_commit_sha"},
+            {"sha": "latest_commit_sha", "html_url": "https://gitea.example.com/owner/repo/commit/latest_commit_sha"},
+        ])
Evidence
Compliance requires correctly handling Gitea’s newest-first PR commits ordering; the test fixture
orders commits oldest-first, so it does not validate the required newest-first selection logic.

Gitea provider must correctly identify the latest PR commit given newest-first ordering
tests/unittest/test_gitea_provider.py[132-135]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The regression test constructs PR commits in oldest-to-newest order, but Gitea PR commits are returned newest-first; the test should reflect real ordering so it fails if the provider selects the wrong element.

## Issue Context
The goal is to ensure `last_commit` corresponds to the PR head commit under newest-first ordering.

## Fix Focus Areas
- tests/unittest/test_gitea_provider.py[132-135]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +95 to +99
self.pr_commits = pr_commits_data if pr_commits_data else []
if self.pr_commits:
self.last_commit = SimpleNamespace(**self.pr_commits[-1])
else:
self.last_commit = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. last_commit uses [-1] 📎 Requirement gap ≡ Correctness

get_pr_commits() returns commits newest-first per the Gitea PR commits endpoint behavior, but
__init__ selects self.pr_commits[-1], which points to the oldest commit. This can cause
commit-dependent behaviors to reference a non-head SHA and fail to detect new PR commits correctly.
Agent Prompt
## Issue description
`GiteaProvider.__init__` sets `last_commit` from `self.pr_commits[-1]`, but the PR commits endpoint returns commits newest-first; this makes `last_commit` the oldest commit instead of the PR head.

## Issue Context
Downstream features use `last_commit`/`last_commit_id` as the PR head SHA for URLs, change detection, inline comments, and file content retrieval.

## Fix Focus Areas
- pr_agent/git_providers/gitea_provider.py[95-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +95 to 101
self.pr_commits = pr_commits_data if pr_commits_data else []
if self.pr_commits:
self.last_commit = SimpleNamespace(**self.pr_commits[-1])
else:
self.last_commit = None
self.last_commit_id = self.last_commit
self.base_sha = self.pr.base.sha if self.pr.base.sha else ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Null last_commit crash 🐞 Bug ☼ Reliability

GiteaProvider.__init__ can set self.last_commit/self.last_commit_id to None when
get_pr_commits() returns an empty list, but methods like get_latest_commit_url() and
_get_file_content_from_latest_commit() dereference .html_url/.sha unconditionally, causing
AttributeError at runtime. This is reachable because RepoApi.get_pr_commits() returns [] on
API errors/exceptions, so a transient Gitea/API issue will crash later tool flows (e.g., PR
description header generation).
Agent Prompt
### Issue description
`GiteaProvider.__init__` can leave `self.last_commit` as `None` when PR commits cannot be fetched (empty list), but later methods assume `last_commit`/`last_commit_id` always exist and unconditionally access `.sha`/`.html_url`, leading to `AttributeError` crashes.

### Issue Context
`RepoApi.get_pr_commits()` returns `[]` on exceptions, making the empty-commits path reachable during transient API/network failures.

### Fix Focus Areas
- pr_agent/git_providers/gitea_provider.py[87-103]
- pr_agent/git_providers/gitea_provider.py[232-234]
- pr_agent/git_providers/gitea_provider.py[442-448]
- pr_agent/git_providers/gitea_provider.py[1019-1050]

### Suggested fix
- In `__init__`, if `get_pr_commits()` returns empty:
  - Prefer a safe fallback: build `last_commit` from `self.pr.head.sha` (already available as `self.sha`) and (if needed) synthesize an `html_url`, or
  - Fail fast with a clear exception/log and stop initialization (instead of leaving `last_commit=None`).
- Add guards in `get_latest_commit_url()` and `_get_file_content_from_latest_commit()` to avoid dereferencing `None` (e.g., return "" / use `self.sha` fallback) so downstream tools don’t crash.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes GiteaProvider initialization to fetch PR-specific commits (instead of repo default-branch commits) so last_commit/last_commit_id reflects the PR head and downstream behaviors (inline comments, persistent review updates, file-content reads) anchor to the correct commit.

Changes:

  • Switch GiteaProvider.__init__ from repo-level commits to get_pr_commits(owner, repo, pr_number).
  • Wrap the selected commit dict in SimpleNamespace to preserve .sha / .html_url attribute access.
  • Add a regression unit test asserting the PR commits endpoint is called and last_commit.sha matches expectations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
pr_agent/git_providers/gitea_provider.py Uses PR commits endpoint during provider init and adapts JSON commit shape for existing attribute-based code.
tests/unittest/test_gitea_provider.py Adds regression coverage ensuring init calls the PR commits endpoint and sets last_commit appropriately.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +90 to 100
pr_commits_data = self.repo_api.get_pr_commits(
owner=self.owner,
repo=self.repo
repo=self.repo,
pr_number=self.pr_number
)
self.last_commit = self.pr_commits[-1]
self.pr_commits = pr_commits_data if pr_commits_data else []
if self.pr_commits:
self.last_commit = SimpleNamespace(**self.pr_commits[-1])
else:
self.last_commit = None
self.last_commit_id = self.last_commit
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last_commit is derived from self.pr_commits[-1], which assumes the PR commits endpoint returns commits in oldest→newest order. If Gitea returns PR commits newest-first (as reported in #2206), this will still select the wrong commit. Prefer selecting the commit whose sha matches self.pr.head.sha (already stored in self.sha) rather than relying on list ordering (with a sensible fallback if not found).

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +173
pr_commits_json = json.dumps([
{"sha": "older_commit_sha", "html_url": "https://gitea.example.com/owner/repo/commit/older_commit_sha"},
{"sha": "latest_commit_sha", "html_url": "https://gitea.example.com/owner/repo/commit/latest_commit_sha"},
])

mock_pr = MagicMock()
mock_pr.head.sha = "latest_commit_sha"
mock_pr.base.sha = "base_sha"
mock_pr.base.ref = "main"

def call_api_side_effect(path, method, **kwargs):
called_paths.append(path)
mock_resp = MagicMock()
if 'pulls' in path and 'files' in path:
mock_resp.data = BytesIO(b'[]')
elif 'pulls' in path and 'commits' in path:
mock_resp.data = BytesIO(pr_commits_json.encode())
elif path.endswith('.diff'):
mock_resp.data = BytesIO(b'')
else:
mock_resp.data = BytesIO(b'{}')
return mock_resp

mock_api_client.call_api.side_effect = call_api_side_effect
mock_filter.side_effect = lambda files, **kw: files

# Mock repo_get_pull_request to return our PR object
with patch('pr_agent.git_providers.gitea_provider.giteapy.RepositoryApi.repo_get_pull_request', return_value=mock_pr):
from pr_agent.git_providers.gitea_provider import GiteaProvider
provider = GiteaProvider("https://gitea.example.com/owner/repo/pulls/42")

# Verify PR-specific commits endpoint was called (not repo-level commits)
pr_commits_calls = [p for p in called_paths if 'commits' in p]
assert any('/pulls/' in p and '/commits' in p for p in pr_commits_calls), \
f"Expected PR-specific commits endpoint, got: {pr_commits_calls}"
assert not any(p.endswith('/commits') and '/pulls/' not in p for p in pr_commits_calls), \
f"Should not call repo-level commits endpoint, got: {pr_commits_calls}"

# Verify last_commit has the latest PR commit's SHA
assert provider.last_commit is not None
assert provider.last_commit.sha == "latest_commit_sha"
assert provider.last_commit.html_url == "https://gitea.example.com/owner/repo/commit/latest_commit_sha"
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regression test encodes an ordering assumption by returning commits as [older, latest] and asserting last_commit.sha == "latest_commit_sha". If the real Gitea endpoint returns commits newest-first, this test will pass while production behavior is wrong (or vice versa). Make the test order-independent by asserting provider.last_commit.sha == mock_pr.head.sha and/or by varying the mocked commit order.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gitea provider: list_all_commits fetches repo commits instead of PR commits

2 participants