feat: delete previous review before posting new one (#6)
CI / test (pull_request) Successful in 13s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 21s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 1m20s

Before posting a review, the bot now:
1. Calls GET /api/v1/user to identify its own login
2. Lists all reviews on the PR
3. Deletes any existing reviews from itself
4. Posts the fresh review

This keeps PR threads clean — one review per bot at any time.

New Gitea client methods:
- GetAuthenticatedUser() — token self-identification
- ListReviews() — fetch reviews on a PR
- DeleteReview() — delete a review by ID

Flag: --update-existing / UPDATE_EXISTING (default true)
Set to false to preserve old behavior (stack reviews).

All delete failures are non-fatal (logged as warnings).

Closes #6
This commit is contained in:
Rodin
2026-05-01 20:17:01 -07:00
parent aee903caa2
commit 0d417e068e
4 changed files with 208 additions and 0 deletions
+75
View File
@@ -266,3 +266,78 @@ func (c *Client) GetAllFilesInPath(ctx context.Context, owner, repo, path string
}
return results, nil
}
// Review represents a pull request review from the Gitea API.
type Review struct {
ID int64 `json:"id"`
User struct {
Login string `json:"login"`
} `json:"user"`
State string `json:"state"`
Stale bool `json:"stale"`
}
// GetAuthenticatedUser returns the login name of the token's owner.
func (c *Client) GetAuthenticatedUser(ctx context.Context) (string, error) {
reqURL := fmt.Sprintf("%s/api/v1/user", c.baseURL)
body, err := c.doGet(ctx, reqURL)
if err != nil {
return "", fmt.Errorf("get authenticated user: %w", err)
}
var user struct {
Login string `json:"login"`
}
if err := json.Unmarshal(body, &user); err != nil {
return "", fmt.Errorf("parse user response: %w", err)
}
if user.Login == "" {
return "", fmt.Errorf("empty login in user response")
}
return user.Login, nil
}
// ListReviews returns all reviews on a pull request.
func (c *Client) ListReviews(ctx context.Context, owner, repo string, number int) ([]Review, error) {
reqURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/reviews",
c.baseURL,
url.PathEscape(owner),
url.PathEscape(repo),
number)
body, err := c.doGet(ctx, reqURL)
if err != nil {
return nil, fmt.Errorf("list reviews: %w", err)
}
var reviews []Review
if err := json.Unmarshal(body, &reviews); err != nil {
return nil, fmt.Errorf("parse reviews: %w", err)
}
return reviews, nil
}
// DeleteReview deletes a review by ID. The token must belong to the review author.
func (c *Client) DeleteReview(ctx context.Context, owner, repo string, number int, reviewID int64) error {
reqURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/pulls/%d/reviews/%d",
c.baseURL,
url.PathEscape(owner),
url.PathEscape(repo),
number,
reviewID)
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, reqURL, nil)
if err != nil {
return fmt.Errorf("create delete request: %w", err)
}
req.Header.Set("Authorization", "token "+c.token)
resp, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("delete review: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("delete review failed (status %d): %s", resp.StatusCode, string(respBody))
}
return nil
}