Skip to content
Open
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
3 changes: 2 additions & 1 deletion GUI/pullrequests.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,8 @@ def update_comments(self, comments):
for comment in comments:
time_str = comment.created_at.strftime("%Y-%m-%d %H:%M") if comment.created_at else "Unknown"
preview = comment.body[:50].replace("\n", " ") + "..." if len(comment.body) > 50 else comment.body.replace("\n", " ")
self.comments_list.Append(f"{comment.user.login} ({time_str}): {preview}")
source = "[Review] " if comment.kind == "review" else ""
self.comments_list.Append(f"{comment.user.login} ({time_str}): {source}{preview}")

def on_comment_select(self, event):
"""Show selected comment content."""
Expand Down
42 changes: 40 additions & 2 deletions github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,9 +737,47 @@ def close_pull_request(self, owner: str, repo: str, number: int) -> bool:
result = self.update_pull_request(owner, repo, number, state="closed")
return result is not None

def get_pr_review_comments(self, owner: str, repo: str, number: int, per_page: int = 100) -> list[Comment]:
"""Get review comments on a pull request."""
comments = []
page = 1

while True:
response = self._session.get(
f"{GITHUB_API_URL}/repos/{owner}/{repo}/pulls/{number}/comments",
params={
"per_page": per_page,
"page": page
}
)

if response.status_code != 200:
break

data = response.json()
if not data:
break

for item in data:
comments.append(Comment.from_github_api(item, kind="review"))

if len(data) < per_page:
break

page += 1

return comments

def get_pr_comments(self, owner: str, repo: str, number: int, per_page: int = 100) -> list[Comment]:
"""Get comments on a pull request (issue comments, not review comments)."""
return self.get_issue_comments(owner, repo, number, per_page)
"""Get all comments on a pull request, including review comments."""
issue_comments = self.get_issue_comments(owner, repo, number, per_page)
review_comments = self.get_pr_review_comments(owner, repo, number, per_page)
comments = issue_comments + review_comments

# Keep stable ordering by creation time so the dialog mirrors issue behavior.
comments.sort(key=lambda c: c.created_at.timestamp() if c.created_at else 0)

return comments

def create_pr_comment(self, owner: str, repo: str, number: int, body: str) -> Comment | None:
"""Create a comment on a pull request."""
Expand Down
6 changes: 4 additions & 2 deletions models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ class Comment:
created_at: Optional[datetime]
updated_at: Optional[datetime]
html_url: str = ""
kind: str = "issue"

@classmethod
def from_github_api(cls, data: dict) -> 'Comment':
def from_github_api(cls, data: dict, kind: str = "issue") -> 'Comment':
created_at = None
if data.get('created_at'):
try:
Expand All @@ -71,7 +72,8 @@ def from_github_api(cls, data: dict) -> 'Comment':
user=User.from_github_api(data.get('user')),
created_at=created_at,
updated_at=updated_at,
html_url=data.get('html_url', '')
html_url=data.get('html_url', ''),
kind=kind
)


Expand Down
64 changes: 64 additions & 0 deletions tests/test_pr_review_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from datetime import datetime, timezone
from models.issue import Comment


def _make_comment_data(comment_id: int = 1, body: str = "A comment") -> dict:
return {
"id": comment_id,
"body": body,
"user": {"id": 10, "login": "alice", "avatar_url": ""},
"created_at": "2026-01-01T10:00:00Z",
"updated_at": "2026-01-01T10:00:00Z",
"html_url": f"https://github.com/owner/repo/issues/1#issuecomment-{comment_id}",
}


def test_comment_kind_defaults_to_issue():
comment = Comment.from_github_api(_make_comment_data())
assert comment.kind == "issue"


def test_comment_kind_review_is_set():
comment = Comment.from_github_api(_make_comment_data(), kind="review")
assert comment.kind == "review"


def test_comment_kind_preserved_on_dataclass():
comment = Comment.from_github_api(_make_comment_data(comment_id=2), kind="review")
assert comment.id == 2
assert comment.body == "A comment"
assert comment.kind == "review"


def test_pr_comments_merge_sorted_by_created_at():
"""get_pr_comments returns issue + review comments sorted chronologically."""
issue_data = _make_comment_data(comment_id=1)
issue_data["created_at"] = "2026-01-01T10:00:00Z"

review_data = _make_comment_data(comment_id=2)
review_data["created_at"] = "2026-01-01T09:00:00Z" # earlier than issue comment

issue_comment = Comment.from_github_api(issue_data, kind="issue")
review_comment = Comment.from_github_api(review_data, kind="review")

merged = sorted(
[issue_comment, review_comment],
key=lambda c: c.created_at.timestamp() if c.created_at else 0,
)

assert merged[0].kind == "review"
assert merged[1].kind == "issue"


def test_pr_comments_sort_stable_for_no_created_at():
"""Comments with no created_at sort to front without crashing."""
data = _make_comment_data()
data["created_at"] = None
comment = Comment.from_github_api(data)
assert comment.created_at is None

merged = sorted(
[comment],
key=lambda c: c.created_at.timestamp() if c.created_at else 0,
)
assert len(merged) == 1