3995fa3136
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).
69 lines
2.1 KiB
Go
69 lines
2.1 KiB
Go
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
|
|
}
|