diff --git a/cmd/review-bot/main.go b/cmd/review-bot/main.go index d003585..e40c791 100644 --- a/cmd/review-bot/main.go +++ b/cmd/review-bot/main.go @@ -362,6 +362,27 @@ func main() { } else { slog.Info("marked old review as superseded", "old_state", existingReview.State, "new_review_id", posted.ID, "pr", prNumber) } + + // Resolve old review's inline comments (clears unresolved conversations in PR timeline) + oldComments, err := giteaClient.ListReviewComments(ctx, owner, repoName, prNumber, existingReview.ID) + if err != nil { + slog.Warn("could not list old review comments for resolution", "review_id", existingReview.ID, "error", err) + } else { + resolved := 0 + for _, c := range oldComments { + if c.ID == 0 { + continue + } + if err := giteaClient.ResolveComment(ctx, owner, repoName, c.ID); err != nil { + slog.Debug("could not resolve inline comment", "comment_id", c.ID, "error", err) + } else { + resolved++ + } + } + if resolved > 0 { + slog.Info("resolved old inline comments", "count", resolved, "pr", prNumber) + } + } } } diff --git a/gitea/client.go b/gitea/client.go index 1f5f44e..0461ca2 100644 --- a/gitea/client.go +++ b/gitea/client.go @@ -82,6 +82,7 @@ type ChangedFile struct { // ReviewComment represents an inline comment to attach to a review. type ReviewComment struct { + ID int64 `json:"id,omitempty"` Path string `json:"path"` NewPosition int64 `json:"new_position"` Body string `json:"body"` @@ -460,3 +461,49 @@ func (c *Client) EditComment(ctx context.Context, owner, repo string, commentID } return nil } + +// ListReviewComments returns the inline comments attached to a specific review. +func (c *Client) ListReviewComments(ctx context.Context, owner, repo string, prNumber int, reviewID int64) ([]ReviewComment, error) { + reqURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/reviews/%d/comments", + c.baseURL, + url.PathEscape(owner), + url.PathEscape(repo), + prNumber, + reviewID) + body, err := c.doGet(ctx, reqURL) + if err != nil { + return nil, fmt.Errorf("list review comments: %w", err) + } + var comments []ReviewComment + if err := json.Unmarshal(body, &comments); err != nil { + return nil, fmt.Errorf("parse review comments: %w", err) + } + return comments, nil +} + +// ResolveComment marks an inline review comment as resolved. +func (c *Client) ResolveComment(ctx context.Context, owner, repo string, commentID int64) error { + reqURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/comments/%d/resolve", + c.baseURL, + url.PathEscape(owner), + url.PathEscape(repo), + commentID) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, nil) + if err != nil { + return fmt.Errorf("create resolve request: %w", err) + } + req.Header.Set("Authorization", "token "+c.token) + + resp, err := c.http.Do(req) + if err != nil { + return fmt.Errorf("resolve comment: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("resolve comment failed (status %d): %s", resp.StatusCode, body) + } + return nil +}