1fcc0b738a
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 18s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 39s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m30s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m8s
- SetHTTPClient(nil): preserve CheckRedirect auth-stripping policy instead of restoring a plain http.Client that loses cross-host protection. - Authorization header: add comment documenting why Bearer scheme is correct (OAuth2 standard, works for both classic PATs and fine-grained tokens). - Retry-After parsing: support HTTP-date format (RFC 7231) in addition to integer seconds. GitHub only sends integers today, but the implementation is now spec-compliant. - escapePath dot-segment removal: document the behavior in public API doc comments for ListContents and GetFileContentAtRef so callers are aware without reading the internal helper.
95 lines
3.2 KiB
Go
95 lines
3.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.
|
|
// If the path points to a single file (not a directory), the API returns
|
|
// a JSON object instead of an array; this is handled by returning a
|
|
// single-element slice.
|
|
//
|
|
// 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) 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)
|
|
}
|
|
|
|
type entry struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// The GitHub contents API returns an array for directories and an object
|
|
// for single files. Try array first (common case), then fall back to object.
|
|
var entries []entry
|
|
if err := json.Unmarshal(body, &entries); err != nil {
|
|
var single entry
|
|
if err2 := json.Unmarshal(body, &single); err2 != nil {
|
|
return nil, fmt.Errorf("parse contents JSON: %w", err2)
|
|
}
|
|
entries = []entry{single}
|
|
}
|
|
|
|
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 silently removed to prevent path traversal.
|
|
// This is intentional: callers may receive a different path than requested without
|
|
// error. The function is package-private, and all callers (GetFileContentAtRef,
|
|
// ListContents) already handle missing-file errors from the API if the cleaned
|
|
// path doesn't match what the caller intended.
|
|
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) {
|
|
// GitHub inserts newlines in base64 content
|
|
cleaned := strings.NewReplacer("\n", "", "\r", "").Replace(encoded)
|
|
decoded, err := base64.StdEncoding.DecodeString(cleaned)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(decoded), nil
|
|
}
|