Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/scm/providers/cursor_origin/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
CommitAuthorParam,
CommitComparison,
CommitFile,
CommitWithChanges,
CoPilotChatExtension,
CredentialsSet,
DeleteCommitAction,
Expand Down Expand Up @@ -565,6 +566,46 @@ def _get_archive_location(self, ref: str, request_options: RequestOptions | None
allow_redirects=False,
)

def get_commit(
self,
sha: SHA,
request_options: RequestOptions | None = None,
) -> ActionResult[CommitWithChanges]:
"""Return a commit with its changed files."""
response = self.get(
f"/repos/{self.repository_path}/commits/{sha}",
request_options=request_options,
)
files = self.get_commit_changes(
sha,
pagination={"per_page": MAX_PAGE_SIZE},
request_options=request_options,
Comment on lines +581 to +582

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The code uses unsafe direct dictionary access (raw["stats"], raw["patch"]) on an API response, which will raise a KeyError if the keys are missing.
Severity: HIGH

Suggested Fix

Use the .get() method with a default value to safely access potentially missing keys from the API response. For example, access stats with stats = raw.get("stats") or {} and then additions=stats.get("additions"). Similarly, access the patch with patch=raw.get("patch").

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/scm/providers/cursor_origin/provider.py#L496-L497

Potential issue: The `get_commit` method and its helper `map_commit_file` in the Cursor
Origin provider use direct dictionary access to parse the API response. Specifically,
`raw["stats"]["additions"]`, `raw["stats"]["deletions"]`, and `raw["patch"]` are
accessed without checking if the keys exist. If the API returns a commit object without
a `stats` field (e.g., for an empty commit) or a file object without a `patch` field,
the application will raise a `KeyError` and crash while processing the commit. This
contrasts with the more defensive `.get()` approach used in other SCM providers like
GitHub.

Also affects:

  • src/scm/providers/cursor_origin/provider.py:523~523

Did we get this right? 👍 / 👎 to inform future reviews.

)["data"]
return map_action(
response,
lambda raw: CommitWithChanges(
id=raw["sha"],
message=raw["commit"]["message"],
author=map_commit_author(raw["commit"]["author"]),
additions=raw["stats"]["additions"],
deletions=raw["stats"]["deletions"],
files=files,
),
)

def get_commit_changes(
self,
sha: SHA,
pagination: PaginationParams | None = None,
request_options: RequestOptions | None = None,
) -> PaginatedActionResult[list[CommitFile]]:
response = self.get(
f"/repos/{self.repository_path}/commits/{sha}/files",
pagination=pagination,
request_options=request_options,
)
return map_paginated_action(response, lambda raw: [map_commit_file(file) for file in raw["files"]])

def create_check_run(
self,
name: str,
Expand Down
66 changes: 66 additions & 0 deletions tests/unit/provider/test_cursor_origin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,72 @@ def test_the_counts_and_the_changed_files_are_read(
assert result["meta"]["next_cursor"] == "t2"


class TestCommitDetail:
def test_a_commit_is_read_with_its_changed_files(
self, provider: CursorOriginProvider, client: unittest.mock.MagicMock
) -> None:
client.request.side_effect = [
_response({**_commit_raw("abc123"), "stats": {"additions": 6, "deletions": 3, "total": 9}}),
_response(
{
"files": [
{
"filename": "src/app.py",
"status": "modified",
"additions": 6,
"deletions": 3,
"changes": 9,
"patch": "@@ -1 +1 @@",
}
],
"nextPageToken": "",
}
),
]

result = provider.get_commit("abc123")

commit, files = client.request.call_args_list
assert commit.kwargs["path"] == f"/repos/{REPO}/commits/abc123"
assert files.kwargs["path"] == f"/repos/{REPO}/commits/abc123/files"
assert files.kwargs["params"] == {"pageSize": "100"}
assert result["data"]["id"] == "abc123"
assert (result["data"]["additions"], result["data"]["deletions"]) == (6, 3)
assert result["data"]["files"] == [
{
"filename": "src/app.py",
"status": "modified",
"patch": "@@ -1 +1 @@",
"additions": 6,
"deletions": 3,
"previous_filename": None,
}
]

def test_the_changed_files_are_paged(self, provider: CursorOriginProvider, client: unittest.mock.MagicMock) -> None:
client.request.return_value = _response(
{
"files": [
{
"filename": "logo.png",
"status": "added",
"additions": 0,
"deletions": 0,
"changes": 0,
"patch": "",
}
],
"nextPageToken": "t2",
}
)

result = provider.get_commit_changes("abc123", pagination={"cursor": "1", "per_page": 30})

assert client.request.call_args.kwargs["params"] == {"pageSize": "30"}
assert result["data"][0]["patch"] is None
assert result["meta"]["next_cursor"] == "t2"


class TestPullRequestDiff:
def test_the_changed_files_are_listed(
self, provider: CursorOriginProvider, client: unittest.mock.MagicMock
Expand Down
Loading