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
176 lines
6.1 KiB
Go
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() != ""
|
|
}
|