5b43afc6d4
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 23s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 45s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m48s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m7s
- Fix Retry-After slice mutation: copy c.RetryBackoff before modifying to prevent permanent mutation of the shared slice (sonnet#1, security#1) - Cap Retry-After to 120s maximum to prevent excessive sleeps (security#2) - Guard auth header: only set Authorization when token is non-empty (gpt#2) - Fix GetFileContent doc comment to match actual behavior (sonnet#3, gpt#1) - Remove dead 'in_progress/queued' case in mapCheckRunStatus (sonnet#4) - Add testing.Short() guard to slow retry test (sonnet#5) - Reject dot-segments in escapePath to prevent path traversal (security#3) - Add regression tests for non-mutation and escapePath safety
74 lines
2.2 KiB
Go
74 lines
2.2 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"gitea.weiker.me/rodin/review-bot/vcs"
|
|
)
|
|
|
|
// GetFileContent fetches a file from a repo at the given ref.
|
|
// Delegates to GetFileContentAtRef with the provided ref.
|
|
func (c *Client) GetFileContent(ctx context.Context, owner, repo, path, ref string) (string, error) {
|
|
return c.GetFileContentAtRef(ctx, owner, repo, path, ref)
|
|
}
|
|
|
|
// ListContents lists files and directories at a given path in a repo.
|
|
// Returns the directory listing from the GitHub contents API.
|
|
func (c *Client) ListContents(ctx context.Context, owner, repo, path string) ([]vcs.ContentEntry, error) {
|
|
reqURL := fmt.Sprintf("%s/repos/%s/%s/contents/%s",
|
|
c.baseURL, url.PathEscape(owner), url.PathEscape(repo), escapePath(path))
|
|
body, err := c.doGet(ctx, reqURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list contents %s: %w", path, err)
|
|
}
|
|
var entries []struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Type string `json:"type"`
|
|
}
|
|
if err := json.Unmarshal(body, &entries); err != nil {
|
|
return nil, fmt.Errorf("parse contents JSON: %w", err)
|
|
}
|
|
result := make([]vcs.ContentEntry, len(entries))
|
|
for i, e := range entries {
|
|
result[i] = vcs.ContentEntry{
|
|
Name: e.Name,
|
|
Path: e.Path,
|
|
Type: e.Type,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// escapePath escapes each segment of a relative file path for use in URLs.
|
|
// Slashes are preserved as path separators; other special characters are escaped.
|
|
// Dot-segments ("." and "..") are removed 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 newlines for formatting, which we strip before decoding.
|
|
func decodeBase64Content(encoded string) (string, error) {
|
|
// GitHub inserts newlines in base64 content
|
|
cleaned := strings.ReplaceAll(encoded, "\n", "")
|
|
decoded, err := base64.StdEncoding.DecodeString(cleaned)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(decoded), nil
|
|
}
|