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
179 changes: 179 additions & 0 deletions src/scm/providers/cursor_origin/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
CommitAuthorParam,
CommitComparison,
CommitFile,
CoPilotChatExtension,
CredentialsSet,
DeleteCommitAction,
DiffLine,
FileContent,
FileContentType,
FileStatus,
Expand All @@ -54,10 +56,15 @@
PullRequestBranch,
PullRequestCommit,
PullRequestFile,
PullRequestReviewState,
PullRequestState,
Repository,
RequestOptions,
ResourceId,
Review,
ReviewComment,
ReviewCommentInput,
ReviewEvent,
ReviewThread,
ReviewThreadComment,
TreeEntry,
Expand Down Expand Up @@ -119,6 +126,20 @@
"copied": "copied",
}

CURSOR_ORIGIN_REVIEW_EVENT_MAP: dict[ReviewEvent, str] = {
"approve": "approve",
"change_request": "request_changes",
"comment": "comment",
}
CURSOR_ORIGIN_REVIEW_STATE_MAP: dict[str, PullRequestReviewState] = {
"approve": "approved",
"request_changes": "changes_requested",
"comment": "commented",
}


type DiffSide = Literal["left", "right"]


class CursorOriginProvider:
def __init__(
Expand Down Expand Up @@ -248,6 +269,16 @@ def get_pull_request_comments(
general = [map_comment(comment) for comment in comments if _is_general_discussion(comment)]
return map_comments_page(response, comments, general)

def create_pull_request_comment(
self,
pull_request_id: str,
body: str,
extensions: list[CoPilotChatExtension] | None = None,
) -> ActionResult[Comment]:
"""Opens a general-discussion thread"""
response = self.post(f"/repos/{self.repository_path}/pulls/{pull_request_id}/comments", data={"body": body})
return map_action(response, map_comment)

def get_branch(
self,
branch: BranchName,
Expand Down Expand Up @@ -825,6 +856,106 @@ def get_pull_request_commits(
response, lambda raw: [map_pull_request_commit(commit) for commit in raw["commits"]]
)

def create_review_comment(
self,
pull_request_id: str,
commit_id: SHA,
body: str,
path: str,
line: DiffLine,
start_line: DiffLine | None = None,
) -> ActionResult[ReviewComment]:
response = self.post(
f"/repos/{self.repository_path}/pulls/{pull_request_id}/comments",
data={"body": body, "inline": inline_anchor(path, line, start_line)},
)
return map_action(response, map_review_comment)

def update_review_comment(self, pull_request_id: str, comment_id: str, body: str) -> ActionResult[ReviewComment]:
response = self.patch(f"/repos/{self.repository_path}/pulls/comments/{comment_id}", data={"body": body})
return map_action(response, map_review_comment)

def create_review(
self,
pull_request_id: str,
commit_sha: SHA,
event: ReviewEvent,
comments: list[ReviewCommentInput],
body: str | None = None,
) -> ActionResult[Review]:
"""Submit a review and its comments in one request.

Origin reviews the pull request's latest version, ignoring ``commit_sha``. If any
comment cannot be anchored, the entire request is rejected.
"""
data: dict[str, Any] = {
"verdict": CURSOR_ORIGIN_REVIEW_EVENT_MAP[event],
"comments": [self._review_comment(comment) for comment in comments],
}
if body is not None:
data["body"] = body
response = self.post(f"/repos/{self.repository_path}/pulls/{pull_request_id}/reviews", data=data)
return map_action(response, lambda raw: self._map_review(raw, pull_request_id))

def _review_comment(self, comment: ReviewCommentInput) -> dict[str, Any]:
"""A comment with no line anchors the whole file."""
line = comment.get("line")
if line is None:
return {"body": comment["body"], "file": {"path": comment["path"]}}
return {"body": comment["body"], "inline": inline_anchor(comment["path"], line, comment.get("start_line"))}

def list_pull_request_reviews(
self,
pull_request_id: str,
pagination: PaginationParams | None = None,
request_options: RequestOptions | None = None,
) -> PaginatedActionResult[list[Review]]:
"""Submitted reviews, oldest first."""
response = self.get(
f"/repos/{self.repository_path}/pulls/{pull_request_id}/reviews",
pagination=pagination,
request_options=request_options,
)
return map_paginated_action(
response,
lambda raw: [self._map_review(review, pull_request_id) for review in raw["reviews"]],
)

def _map_review(self, raw: dict[str, Any], pull_request_id: str) -> Review:
"""Origin has no per-review web link, so the pull request's stands in."""
return Review(
id=raw["id"],
html_url=f"{self._web_base_url}/{self.repository_path}/pull/{pull_request_id}",
state="dismissed" if raw.get("dismissal") else CURSOR_ORIGIN_REVIEW_STATE_MAP[raw["verdict"]],
author=map_author(raw["author"]),
body=raw["body"] or None,
submitted_at=raw["submittedAt"],
commit_id=raw["pullRequestVersion"]["headSha"],
)

def collapse_pull_request_comment(
self,
pull_request_id: str,
thread_id: str,
comment_node_id: str,
reason: str = "OUTDATED",
) -> None:
"""Origin cannot minimize a comment, so we resolve instead."""
self.patch(f"/repos/{self.repository_path}/pulls/threads/{thread_id}", data={"resolved": True})

def update_and_collapse_pull_request_comment(
self,
pull_request_id: str,
thread_id: str,
comment_id: str,
comment_node_id: str,
body: str,
reason: str = "OUTDATED",
) -> ActionResult[ReviewComment]:
result = self.update_review_comment(pull_request_id, comment_id, body)
self.collapse_pull_request_comment(pull_request_id, thread_id, comment_node_id, reason)
return result

def get_pull_request_review_threads(
self,
pull_request_id: str,
Expand Down Expand Up @@ -1011,6 +1142,54 @@ def _is_general_discussion(raw: dict[str, Any]) -> bool:
return not raw["thread"]["path"]


def map_diff_line(side: DiffSide, line: int) -> DiffLine:
return DiffLine(head=line) if side == "right" else DiffLine(base=line)


def map_review_comment(raw: dict[str, Any]) -> ReviewComment:
thread = raw["thread"]
line = start_line = None
start = thread.get("startLine", 0)
if start:
end = thread["endLine"]
Comment thread
sentry[bot] marked this conversation as resolved.
line = map_diff_line(thread["side"], end or start)
start_line = map_diff_line(thread["side"], start) if end else None
return ReviewComment(
id=raw["id"],
unique_id=raw["id"],
url=None,
file_path=thread.get("path") or None,
body=raw["body"],
author=map_author(raw["author"]),
created_at=raw["createdAt"],
diff_hunk=None,
line=line,
start_line=start_line,
review_id=None,
author_association=None,
commit_sha=thread["version"]["headSha"] if "version" in thread else None,
head=None,
thread_id=thread["id"],
)


def _diff_anchor(line: DiffLine) -> tuple[DiffSide, int]:
if "head" in line:
return "right", line["head"]
return "left", line["base"]


def inline_anchor(path: str, line: DiffLine, start_line: DiffLine | None) -> dict[str, Any]:
side, end = _diff_anchor(line)
anchor: dict[str, Any] = {"path": path, "side": side, "startLine": end}
if start_line is not None:
start = start_line.get("head") if side == "right" else start_line.get("base")
if start is not None and start < end:
anchor["startLine"] = start
anchor["endLine"] = end
return anchor


def map_review_thread_comment(raw: dict[str, Any]) -> ReviewThreadComment:
return ReviewThreadComment(
id=raw["id"],
Expand Down
Loading
Loading