fix(github): add GetFileContentAtRef and fix conformance test
- 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
|
||||
|
||||
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