Compare commits

..

1 Commits

Author SHA1 Message Date
claw e324f034b5 feat(github): implement PRReader + FileReader client (#80)
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 40s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m55s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 3m14s
Implement the GitHub API client with PRReader and FileReader interface
conformance for both github.com and GitHub Enterprise.

New files:
- github/client.go: Client struct, NewClient with configurable base URL,
  HTTP helpers with 429 retry and Retry-After support
- github/pr.go: GetPullRequest, GetPullRequestDiff (per-request Accept
  header), GetPullRequestFiles (paginated, populates Patch field),
  GetFileContentAtRef (base64 decode), GetCommitStatuses (merges commit
  statuses + check runs with conclusion mapping)
- github/files.go: GetFileContent (delegates to GetFileContentAtRef),
  ListContents, escapePath, decodeBase64Content helpers

Type changes:
- vcs/types.go: Add Patch field to ChangedFile struct

Tests cover: happy path, 404, 401, 429+retry, malformed response,
pagination, binary files, check run conclusion mapping, base64 decoding.

Compile-time checks:
  var _ vcs.PRReader = (*Client)(nil)
  var _ vcs.FileReader = (*Client)(nil)

Exit criteria met:
- go test ./github/... passes (all methods)
- NewClient with empty baseURL uses https://api.github.com
- NewClient with GHE URL targets correctly
- GetFileContent delegates to GetFileContentAtRef with empty ref
- GetPullRequestFiles paginates and populates Patch field
- GetCommitStatuses merges both commit statuses and check-runs
2026-05-12 15:17:56 -07:00
+2 -21
View File
@@ -5,6 +5,7 @@ package github
import (
"context"
"errors"
"fmt"
"io"
"net/http"
@@ -52,32 +53,12 @@ func asAPIError(err error) (*APIError, bool) {
return nil, false
}
var target *APIError
if ok := errorAs(err, &target); ok {
if errors.As(err, &target) {
return target, true
}
return nil, false
}
// errorAs is a type-safe wrapper for errors.As to avoid import cycle issues.
func errorAs(err error, target interface{}) bool {
switch t := target.(type) {
case **APIError:
for err != nil {
if e, ok := err.(*APIError); ok {
*t = e
return true
}
// Try unwrapping
if u, ok := err.(interface{ Unwrap() error }); ok {
err = u.Unwrap()
} else {
return false
}
}
}
return false
}
// Client interacts with the GitHub API.
// A Client is safe for concurrent use by multiple goroutines.
type Client struct {