From d606d0a202b00fc95000d5dba075e7e0bf8b11a4 Mon Sep 17 00:00:00 2001 From: claw Date: Wed, 13 May 2026 13:35:58 -0700 Subject: [PATCH 1/5] feat(vcs): add CommitID to ReviewRequest (#115) Add CommitID string field to vcs.ReviewRequest so both platform adapters can thread the commit anchor through the abstraction layer. Changes: - vcs/types.go: Add CommitID field with json tag commit_id,omitempty - gitea/client.go: Re-add commitID parameter to PostReview (was removed during PR #112 refactoring) - gitea/adapter.go: Forward req.CommitID to underlying client - github/review.go: Use req.CommitID as primary anchor, fall back to comment-derived CommitID when empty, reject on conflict - cmd/review-bot/main.go: Set ReviewRequest.CommitID = evaluatedSHA Fixes #115 --- cmd/review-bot/main.go | 2 +- gitea/adapter_test.go | 34 ++++++++++++++++++++++++++++++++++ gitea/client_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ vcs/types.go | 1 - 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/cmd/review-bot/main.go b/cmd/review-bot/main.go index fa7630e..2063a64 100644 --- a/cmd/review-bot/main.go +++ b/cmd/review-bot/main.go @@ -485,7 +485,7 @@ func main() { reviewReq := vcs.ReviewRequest{ Body: reviewBody, Event: event, - CommitID: pr.Head.SHA, + CommitID: evaluatedSHA, Comments: inlineComments, } posted, err := client.PostReview(ctx, owner, repoName, prNumber, reviewReq) diff --git a/gitea/adapter_test.go b/gitea/adapter_test.go index 1c59b22..6ab2a0d 100644 --- a/gitea/adapter_test.go +++ b/gitea/adapter_test.go @@ -214,6 +214,40 @@ func TestAdapter_PostReview_EventTranslation(t *testing.T) { } } + +func TestAdapter_PostReview_CommitID(t *testing.T) { + var gotCommitID string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var payload struct { + CommitID string `json:"commit_id"` + } + json.NewDecoder(r.Body).Decode(&payload) + gotCommitID = payload.CommitID + json.NewEncoder(w).Encode(map[string]any{ + "id": 1, + "body": "test", + "user": map[string]any{"login": "bot"}, + "commit_id": payload.CommitID, + }) + })) + defer server.Close() + + client := gitea.NewClient(server.URL, "token") + adapter := gitea.NewAdapter(client) + + _, err := adapter.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{ + Body: "test", + Event: vcs.ReviewEventApprove, + CommitID: "sha256abc", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotCommitID != "sha256abc" { + t.Errorf("expected commit_id %q forwarded to client, got %q", "sha256abc", gotCommitID) + } +} func TestAdapter_PostReview_WithComments_PositionTranslation(t *testing.T) { diff := `diff --git a/main.go b/main.go --- a/main.go diff --git a/gitea/client_test.go b/gitea/client_test.go index b06d204..5621e2f 100644 --- a/gitea/client_test.go +++ b/gitea/client_test.go @@ -147,6 +147,46 @@ func TestPostReview(t *testing.T) { } } +func TestPostReview_CommitID(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + + body, _ := io.ReadAll(r.Body) + var payload struct { + Body string `json:"body"` + Event string `json:"event"` + CommitID string `json:"commit_id"` + } + if err := json.Unmarshal(body, &payload); err != nil { + t.Fatalf("unmarshal payload: %v", err) + } + if payload.CommitID != "deadbeef123" { + t.Errorf("expected commit_id %q, got %q", "deadbeef123", payload.CommitID) + } + if payload.Event != "APPROVED" { + t.Errorf("expected event APPROVED, got %q", payload.Event) + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"id":101,"user":{"login":"review-bot"},"state":"APPROVED","stale":false,"commit_id":"deadbeef123"}`)) + })) + defer server.Close() + + client := NewClient(server.URL, "test-token") + review, err := client.PostReview(context.Background(), "owner", "repo", 3, "APPROVED", "LGTM", "deadbeef123", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if review.ID != 101 { + t.Errorf("expected review ID 101, got %d", review.ID) + } + if review.CommitID != "deadbeef123" { + t.Errorf("expected commit_id %q, got %q", "deadbeef123", review.CommitID) + } +} + func TestGetPullRequest_Non200(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) diff --git a/vcs/types.go b/vcs/types.go index f77df2c..cec0bf6 100644 --- a/vcs/types.go +++ b/vcs/types.go @@ -98,6 +98,5 @@ type ReviewRequest struct { // If empty, the platform defaults to the current PR head. // Adapters use this as the primary commit anchor for the review submission. CommitID string `json:"commit_id,omitempty"` - Comments []ReviewComment `json:"comments,omitempty"` } From 08b5d4051b5ad8ff55e575d4ae1deb4e66ce3877 Mon Sep 17 00:00:00 2001 From: claw Date: Wed, 13 May 2026 14:03:35 -0700 Subject: [PATCH 2/5] style: remove double blank lines in test files --- gitea/adapter_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gitea/adapter_test.go b/gitea/adapter_test.go index 6ab2a0d..adb2273 100644 --- a/gitea/adapter_test.go +++ b/gitea/adapter_test.go @@ -214,7 +214,6 @@ func TestAdapter_PostReview_EventTranslation(t *testing.T) { } } - func TestAdapter_PostReview_CommitID(t *testing.T) { var gotCommitID string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From 49db84fb82609e2978015d9312feb55f39b554b9 Mon Sep 17 00:00:00 2001 From: claw Date: Wed, 13 May 2026 14:14:11 -0700 Subject: [PATCH 3/5] fix(test): add missing blank line between test functions in adapter_test.go --- gitea/adapter_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gitea/adapter_test.go b/gitea/adapter_test.go index adb2273..46b4edc 100644 --- a/gitea/adapter_test.go +++ b/gitea/adapter_test.go @@ -247,6 +247,7 @@ func TestAdapter_PostReview_CommitID(t *testing.T) { t.Errorf("expected commit_id %q forwarded to client, got %q", "sha256abc", gotCommitID) } } + func TestAdapter_PostReview_WithComments_PositionTranslation(t *testing.T) { diff := `diff --git a/main.go b/main.go --- a/main.go From be68e518980d7c66637d0d1354ee3dff3bad66d7 Mon Sep 17 00:00:00 2001 From: claw Date: Wed, 13 May 2026 14:23:50 -0700 Subject: [PATCH 4/5] fix(vcs): address self-review NITs - gofmt alignment and comment clarity --- github/review.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/review.go b/github/review.go index 1d043e5..29e1639 100644 --- a/github/review.go +++ b/github/review.go @@ -103,7 +103,7 @@ func (c *Client) PostReview(ctx context.Context, owner, repo string, number int, // the resolved commit_id. for _, comment := range req.Comments { if comment.CommitID != "" { - if payload.CommitID == "" { + if payload.CommitID == "" { // only reachable when req.CommitID is empty payload.CommitID = comment.CommitID } else if payload.CommitID != comment.CommitID { return nil, ErrConflictingCommitIDs From 9a6298cc4feae2c6f4c795028d561370a7c59357 Mon Sep 17 00:00:00 2001 From: claw Date: Wed, 13 May 2026 16:31:11 -0700 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20address=20review=20NITs=20=E2=80=94?= =?UTF-8?q?=20readability,=20test=20dedup,=20consistent=20SHA=20var?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vcs/types.go: restore blank line between CommitID and Comments fields for visual grouping (scalar vs slice fields) - gitea/adapter_test.go: merge duplicate TestAdapter_PostReview_CommitID tests into one (Threading was a superset) - cmd/review-bot/main.go: use evaluatedSHA instead of pr.Head.SHA for inline comment CommitID for consistency with review-level usage --- cmd/review-bot/main.go | 2 +- gitea/adapter_test.go | 36 +----------------------------------- vcs/types.go | 1 + 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/cmd/review-bot/main.go b/cmd/review-bot/main.go index 2063a64..55a3c78 100644 --- a/cmd/review-bot/main.go +++ b/cmd/review-bot/main.go @@ -439,7 +439,7 @@ func main() { inlineComments = append(inlineComments, vcs.ReviewComment{ Path: f.File, Position: pos, - CommitID: pr.Head.SHA, + CommitID: evaluatedSHA, Body: fmt.Sprintf("**[%s]** %s", f.Severity, f.Finding), }) } diff --git a/gitea/adapter_test.go b/gitea/adapter_test.go index 46b4edc..edbf505 100644 --- a/gitea/adapter_test.go +++ b/gitea/adapter_test.go @@ -214,40 +214,6 @@ func TestAdapter_PostReview_EventTranslation(t *testing.T) { } } -func TestAdapter_PostReview_CommitID(t *testing.T) { - var gotCommitID string - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - var payload struct { - CommitID string `json:"commit_id"` - } - json.NewDecoder(r.Body).Decode(&payload) - gotCommitID = payload.CommitID - json.NewEncoder(w).Encode(map[string]any{ - "id": 1, - "body": "test", - "user": map[string]any{"login": "bot"}, - "commit_id": payload.CommitID, - }) - })) - defer server.Close() - - client := gitea.NewClient(server.URL, "token") - adapter := gitea.NewAdapter(client) - - _, err := adapter.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{ - Body: "test", - Event: vcs.ReviewEventApprove, - CommitID: "sha256abc", - }) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if gotCommitID != "sha256abc" { - t.Errorf("expected commit_id %q forwarded to client, got %q", "sha256abc", gotCommitID) - } -} - func TestAdapter_PostReview_WithComments_PositionTranslation(t *testing.T) { diff := `diff --git a/main.go b/main.go --- a/main.go @@ -443,7 +409,7 @@ func TestAdapter_RequestReviewerSelf(t *testing.T) { } } -func TestAdapter_PostReview_CommitID_Threading(t *testing.T) { +func TestAdapter_PostReview_CommitID(t *testing.T) { var gotPayload struct { Body string `json:"body"` Event string `json:"event"` diff --git a/vcs/types.go b/vcs/types.go index cec0bf6..f77df2c 100644 --- a/vcs/types.go +++ b/vcs/types.go @@ -98,5 +98,6 @@ type ReviewRequest struct { // If empty, the platform defaults to the current PR head. // Adapters use this as the primary commit anchor for the review submission. CommitID string `json:"commit_id,omitempty"` + Comments []ReviewComment `json:"comments,omitempty"` }