Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

176 lines
6.1 KiB
Go

// Package forge is the single network surface for pull-request I/O. It
// wraps the official go-github SDK behind a small set of free functions
// — ListPRs, ViewPR, PRFiles, DiffPR, PostReviewComments, Available —
// every one taking (ctx, repoDir, …). Each constructs an internal
// ghClient that resolves the GitHub token (GH_TOKEN → GITHUB_TOKEN) and
// the owner/name slug from the indexed repo identity (falling back to
// `git remote get-url origin` through gitcmd).
//
// The consumed surface is the free functions, not a method set: callers
// never hold a client. The Client interface exists only as a test seam
// (a func-var indirection injects canned data) and as the future-backend
// seam — a deferred gh-CLI or GitLab backend slots in behind it without
// touching any caller.
//
// GitHub REST exposes no single reviewDecision or statusCheckRollup
// field; those are GraphQL aggregates. ReviewDecision and CIRollup are
// reconstructed here from ListReviews and check-runs/combined-status,
// opt-in per call (ListOpts.WithDecision / WithCI; ViewPR always fills
// them) so a cheap ListPRs skips the extra round-trips.
package forge
import (
"context"
"errors"
"time"
"github.com/zzet/gortex/internal/analysis"
)
// callTimeout bounds every network round-trip a forge call makes.
const callTimeout = 30 * time.Second
// ErrNotAuthenticated is returned when no GitHub token is resolvable
// from the environment.
var ErrNotAuthenticated = errors.New("no GitHub token: set GH_TOKEN (or GITHUB_TOKEN)")
// ErrRateLimited is returned when GitHub answers with a rate-limit /
// abuse-rate-limit error. The underlying *github.RateLimitError /
// *github.AbuseRateLimitError is mapped onto this sentinel (with a
// Retry-After hint preserved in the wrapped error's message) so callers
// can test errors.Is(err, ErrRateLimited).
var ErrRateLimited = errors.New("github rate limited")
// PR is the canonical pull-request value. It is built from a go-github
// *github.PullRequest; ReviewDecision and CIRollup are reconstructed (the
// REST API has no such aggregate fields). Files is EMPTY after ListPRs —
// only ViewPR / PRFiles hydrate it.
type PR struct {
Number int
Title string
Author string
BaseRef string // PullRequest.Base.Ref
HeadRef string // PullRequest.Head.Ref
IsDraft bool
ReviewDecision string // reconstructed from ListReviews (REST has no reviewDecision field)
CIRollup string // reconstructed from Checks + GetCombinedStatus; collapsed by RollupCI
UpdatedAt time.Time
Mergeable string
URL string
State string
Files []string // EMPTY after ListPRs; only ViewPR / PRFiles hydrate it
}
// PRDiff is the per-file diff of a pull request, each file's patch parsed
// into hunks via analysis.ParseDiffHunks.
type PRDiff struct {
Number int
BaseRef string
HeadRef string
Files []PRFile
Raw string
}
// PRFile is one changed file in a PR diff.
type PRFile struct {
Path string
OldPath string
Hunks []analysis.DiffHunk // analysis.ParseDiffHunks on the file's .GetPatch()
Status string
}
// ReviewComment is the one inline-comment type. Posting maps a finding
// to a ReviewComment; StartLine carries the multi-line range start and
// Side defaults to "RIGHT" (the new side).
type ReviewComment struct {
Path string
Line int
StartLine int
Side string
Body string
}
// ListOpts tunes ListPRs. Decision/CI reconstruction is opt-in per call
// so a cheap list skips the extra round-trips.
type ListOpts struct {
State string
Limit int
Author string
WithDecision bool
WithCI bool
}
// Client is the test seam / future-backend seam. The free functions are
// the consumed surface; this interface lets a test inject canned data and
// lets a deferred gh-CLI or GitLab backend slot in without touching any
// caller.
type Client interface {
ListPRs(ctx context.Context, opts ListOpts) ([]PR, error)
ViewPR(ctx context.Context, num int) (*PR, error)
PRFiles(ctx context.Context, num int) ([]string, error)
DiffPR(ctx context.Context, num int) (*PRDiff, error)
PostReviewComments(ctx context.Context, num int, comments []ReviewComment) error
}
// newClient resolves a *ghClient for repoDir. It is a package var so a
// test can swap it for one backed by an httptest server with a fixed
// owner/repo, bypassing token + git-remote resolution.
var newClient = newGHClient
// ListPRs lists pull requests for the repo at repoDir. PR.Files is EMPTY
// on every returned PR — triage and conflict detection must call
// PRFiles(num) explicitly per PR. Decision/CI aggregates are filled only
// when opts requests them.
func ListPRs(ctx context.Context, repoDir string, opts ListOpts) ([]PR, error) {
c, err := newClient(ctx, repoDir)
if err != nil {
return nil, err
}
return c.ListPRs(ctx, opts)
}
// ViewPR fetches a single pull request and hydrates its Files plus the
// reconstructed ReviewDecision / CIRollup aggregates.
func ViewPR(ctx context.Context, repoDir string, num int) (*PR, error) {
c, err := newClient(ctx, repoDir)
if err != nil {
return nil, err
}
return c.ViewPR(ctx, num)
}
// PRFiles returns the paths of files changed in a pull request.
func PRFiles(ctx context.Context, repoDir string, num int) ([]string, error) {
c, err := newClient(ctx, repoDir)
if err != nil {
return nil, err
}
return c.PRFiles(ctx, num)
}
// DiffPR returns the per-file diff of a pull request, each file's patch
// parsed into hunks.
func DiffPR(ctx context.Context, repoDir string, num int) (*PRDiff, error) {
c, err := newClient(ctx, repoDir)
if err != nil {
return nil, err
}
return c.DiffPR(ctx, num)
}
// PostReviewComments posts a batch of inline review comments on a pull
// request as a single review.
func PostReviewComments(ctx context.Context, repoDir string, num int, comments []ReviewComment) error {
c, err := newClient(ctx, repoDir)
if err != nil {
return err
}
return c.PostReviewComments(ctx, num, comments)
}
// Available reports whether a GitHub token (GH_TOKEN or GITHUB_TOKEN) is
// resolvable from the environment.
func Available(ctx context.Context) bool {
return resolveToken() != ""
}