fix(github): add GetFileContentAtRef and fix conformance test
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 39s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 58s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m51s
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 39s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 58s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m51s
- Implement GetFileContentAtRef on *Client to satisfy vcs.PRReader interface - Add escapePath and decodeBase64Content helpers - Fix conformance_test.go to properly import and qualify github.Client (was using unqualified Client in package github_test) Fixes CI failure: the PRReader interface requires GetFileContentAtRef but it was missing from this PR (only present in the file-reader PR).
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
package github_test
|
package github_test
|
||||||
|
|
||||||
import "gitea.weiker.me/rodin/review-bot/vcs"
|
import (
|
||||||
|
"gitea.weiker.me/rodin/review-bot/github"
|
||||||
|
"gitea.weiker.me/rodin/review-bot/vcs"
|
||||||
|
)
|
||||||
|
|
||||||
var _ vcs.PRReader = (*Client)(nil)
|
// Compile-time interface conformance assertion.
|
||||||
|
// Verifies github.Client satisfies vcs.PRReader.
|
||||||
|
var _ vcs.PRReader = (*github.Client)(nil)
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package github
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetFileContentAtRef fetches a file at a specific ref from a repo.
|
||||||
|
// If ref is empty, the query parameter is omitted (uses default branch).
|
||||||
|
//
|
||||||
|
// Note: dot-segments ("." and "..") in the path are silently removed to
|
||||||
|
// prevent path traversal. This means a path like "foo/../bar" resolves
|
||||||
|
// to "foo/bar" rather than "bar".
|
||||||
|
func (c *Client) GetFileContentAtRef(ctx context.Context, owner, repo, path, ref string) (string, error) {
|
||||||
|
reqURL := fmt.Sprintf("%s/repos/%s/%s/contents/%s",
|
||||||
|
c.baseURL, url.PathEscape(owner), url.PathEscape(repo), escapePath(path))
|
||||||
|
if ref != "" {
|
||||||
|
reqURL += "?ref=" + url.QueryEscape(ref)
|
||||||
|
}
|
||||||
|
body, err := c.doGet(ctx, reqURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("fetch file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Encoding string `json:"encoding"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &resp); err != nil {
|
||||||
|
return "", fmt.Errorf("parse file content JSON: %w", err)
|
||||||
|
}
|
||||||
|
if resp.Encoding != "base64" {
|
||||||
|
return "", fmt.Errorf("unexpected encoding %q for file %s", resp.Encoding, path)
|
||||||
|
}
|
||||||
|
decoded, err := decodeBase64Content(resp.Content)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("decode base64 content for %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// escapePath encodes each segment of a slash-separated path, stripping
|
||||||
|
// dot-segments to prevent path traversal.
|
||||||
|
func escapePath(p string) string {
|
||||||
|
parts := strings.Split(p, "/")
|
||||||
|
var clean []string
|
||||||
|
for _, part := range parts {
|
||||||
|
if part == "." || part == ".." || part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clean = append(clean, url.PathEscape(part))
|
||||||
|
}
|
||||||
|
return strings.Join(clean, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeBase64Content decodes base64-encoded content from the GitHub contents API.
|
||||||
|
// GitHub returns base64 content with line breaks for formatting; we strip \r and \n before decoding.
|
||||||
|
func decodeBase64Content(encoded string) (string, error) {
|
||||||
|
cleaned := strings.NewReplacer("\n", "", "\r", "").Replace(encoded)
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(cleaned)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(decoded), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user