github

package
v0.1.0-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package github reads one pull request's metadata, commits, checks and conversation from GitHub's GraphQL API with the user's own token, and maps them onto the generation bundle the way every baum host does. It is the only part of baum that calls GitHub's API. Repositories and SSH aliases are parsed with go-gh; baum owns host recognition and credentials; the GraphQL requests are baum's own, over shurcooL-graphql.

Index

Constants

View Source
const (
	SourcePullRequest        = "pull_request"
	SourceRequestedReviewers = "requested_reviewers"
	SourceCommits            = "commits"
	SourceComments           = "comments"
	SourceReviews            = "reviews"
	SourceReviewThreads      = "review_threads"
	SourceChecks             = "checks"
	SourceRequiredChecks     = "required_checks"

	UnavailableReviewComments = "review_comments"
)

The bundle sources a fetch records, one per part read from GitHub. A part is true when GitHub returned all of it, even when that is nothing. Review comments are read with the review threads, so they have no source of their own: when the threads fail, Unavailable says so under UnavailableReviewComments.

View Source
const (
	MergeMethodMerge  = "MERGE"
	MergeMethodSquash = "SQUASH"
	MergeMethodRebase = "REBASE"
)

The merge methods GitHub names in its GraphQL schema.

View Source
const (
	MergeHeadMoved = "head_moved"
	MergeConflict  = "conflict"
	MergeBlocked   = "blocked"
	MergeForbidden = "forbidden"
	MergeNotOpen   = "not_open"
	MergeMethod    = "method_not_allowed"
	MergeFailed    = "failed"
)

Merge failure codes, stable for the app to branch on.

View Source
const (
	ReviewEventApprove        = "APPROVE"
	ReviewEventComment        = "COMMENT"
	ReviewEventRequestChanges = "REQUEST_CHANGES"
)

The events a review can be submitted with, as GitHub's REST API names them.

View Source
const (
	SideRight = "RIGHT"
	SideLeft  = "LEFT"
)

The sides of a diff a review comment can sit on.

View Source
const (
	ReviewForbidden   = "forbidden"
	ReviewNotFound    = "not_found"
	ReviewOwnPull     = "own_pull_request"
	ReviewInvalidLine = "invalid_line"
	ReviewFailed      = "failed"
)

Review failure codes, stable for the app to branch on.

View Source
const AdditionalViewerRepositories = 100
View Source
const InitialViewerRepositories = 8

InitialViewerRepositories matches the number of rows in the picker.

View Source
const MaxAuthoredPullRequests = 250

MaxAuthoredPullRequests caps the pull requests AuthoredPullRequests reads for one repository: five pages of the search.

View Source
const MaxSearchQualifiers = 300

MaxSearchQualifiers bounds the owner and repository qualifiers one search sends. GitHub's GraphQL search accepts at least 200 in one query.

View Source
const MaxThreadComments = 10

MaxThreadComments is how many comments of a thread a fix addresses: the first ones, which state what the reviewer asked.

View Source
const ReadThreadComments = 20

ReadThreadComments is how many comments of a thread the client reads, for the app to show a thread whole; a fix uses the first MaxThreadComments.

Variables

View Source
var (
	// ErrNoRemote reports a clone without an origin remote and no --repo.
	ErrNoRemote = errors.New("this clone has no origin remote; pass --repo owner/name")
	// ErrDetachedHead reports a detached HEAD and no --pr.
	ErrDetachedHead = errors.New("HEAD is detached, so there is no branch to find a pull request for; pass --pr")
	// ErrNoPullRequest reports a branch GitHub knows no pull request for.
	ErrNoPullRequest = errors.New("no pull request found for the current branch; pass --pr")
	// ErrAmbiguousPullRequest reports a branch with several open pull
	// requests.
	ErrAmbiguousPullRequest = errors.New("the current branch has several open pull requests; pass --pr")
)
View Source
var ErrHeadMoved = errors.New("the pull request's head changed while it was being read from GitHub")

ErrHeadMoved reports a pull request whose head changed while it was being read, every time it was read.

View Source
var ErrIdentityChanged = errors.New("GitHub identity changed; retry the operation")
View Source
var ErrInvalidRepository = errors.New("--repo must be owner/name, or host/owner/name for another GitHub host")

ErrInvalidRepository reports a --repo value that names no repository. It does not repeat the value, which may be a URL carrying credentials.

View Source
var ErrNotFound = errors.New("not found on GitHub")

ErrNotFound reports a repository or pull request GitHub does not show to this token.

View Source
var ErrNotGitHubRemote = errors.New("the remote does not name a repository on a GitHub host; pass --repo")

ErrNotGitHubRemote reports a remote URL that names no repository on a GitHub host. It does not repeat the URL, which may carry credentials.

View Source
var ErrResponseTooLarge = errors.New("GitHub discovery response unreadable or too large")

ErrResponseTooLarge is a response over Read's 8 MiB bound.

View Source
var ErrUnknownHost = errors.New("unrecognized GitHub host: use github.com, a *.ghe.com tenant, or explicitly set GH_HOST")

ErrUnknownHost reports a host baum does not recognize as GitHub. No token is looked up for it and no request is sent to it.

Functions

func IsGitHubHost

func IsGitHubHost(host string) bool

IsGitHubHost reports whether baum will talk to host: see gitHubHost.

func RemoteNames

func RemoteNames(remote string, repo Repository) bool

RemoteNames reports whether the git remote URL names repo, host included: a clone of github.com/o/n is not one of ghe.example.com/o/n. The remote is read as pull request resolution reads it (ssh.github.com is github.com, an SSH alias is resolved through `ssh -G`); a host that is not one gh treats as GitHub falls back to the URL's own host, or to an alias without a dot, whose owner/name must match.

func SetMaxPulls

func SetMaxPulls(n int) (restore func())

SetMaxPulls sets the listing bound and returns a function restoring it, for tests.

Types

type AuthoredPullRequest

type AuthoredPullRequest struct {
	Number    int
	Draft     bool
	State     string
	CreatedAt time.Time
	MergedAt  *time.Time
	// FirstReviewAt is the earliest submitted review by someone other than
	// the author, among the first twenty reviews; nil when there is none.
	FirstReviewAt *time.Time
	// Conflict is GitHub reporting an open pull request as conflicting now.
	Conflict bool
	// ChecksFailing is the head commit's combined check state being a failure
	// or an error now.
	ChecksFailing bool
}

AuthoredPullRequest is one pull request the signed-in account opened, with what delivery figures need of it.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client reads GitHub GraphQL evidence and read-only REST discovery metadata from any recognized GitHub host with that host's token. Its writes are merging a pull request the user confirmed (merge.go), through RESTReader.Write, updating its branch and re-running its workflow runs (internal/prupdate), and, after a confirmed fix's push, replying on and resolving the review threads the fix addressed (threads.go).

func New

func New(tokens TokenSource, opts Options) *Client

New returns a client that reads each host with the token tokens returns for it.

func NewFromConfig

func NewFromConfig(cfg config.Config, opts Options) *Client

NewFromConfig returns a client whose tokens cfg resolves per host (see config.Config.ResolveGitHubToken).

func (*Client) AuthoredPullRequests

func (c *Client) AuthoredPullRequests(ctx context.Context, repository Repository, since time.Time) ([]AuthoredPullRequest, error)

AuthoredPullRequests lists the pull requests the token's account opened in repository since the given day, newest first, with one search query a page, up to MaxAuthoredPullRequests. It reads only; the conflict and check states are what GitHub reports for the head now, not their history.

func (*Client) CurrentName

func (c *Client) CurrentName(ctx context.Context, repo Repository) (string, error)

CurrentName is the lowercase owner/name repo has now. GitHub answers a renamed or transferred repository's earlier name with the current one, as it forwards git remotes that still use it. It fails with ErrNotFound when the token cannot see the repository.

func (*Client) Fetch

func (c *Client) Fetch(ctx context.Context, repo Repository, number int, opts FetchOptions) (Evidence, error)

Fetch reads pull request number of repo: its header, commits, checks and statuses at the head with whether each is required, and its comments, reviews and review threads. A part GitHub refuses is left empty, recorded false in Sources and explained in Unavailable; a missing pull request, a rate limit the client does not wait out and cancellation fail the fetch. When the head moves while the parts are read, the fetch starts over, so every part describes the same head.

func (*Client) MergeOptions

func (c *Client) MergeOptions(ctx context.Context, repository Repository, number int) (MergeOptions, error)

MergeOptions reads the allowed merge methods and the pull request's head and mergeability.

func (*Client) MergePullRequest

func (c *Client) MergePullRequest(ctx context.Context, repository Repository, number int, method, expectedHead string) error

MergePullRequest merges one pull request with GitHub's own checks. GitHub refuses when the head is no longer expectedHead, so the user merges only the commit they confirmed.

func (*Client) REST

func (c *Client) REST(ctx context.Context, repository Repository) (*RESTReader, error)

func (*Client) ReplyToThread

func (c *Client) ReplyToThread(ctx context.Context, repository Repository, id, body string) error

ReplyToThread posts body as a reply in the review thread, as the signed-in account.

func (*Client) ResolvePR

func (c *Client) ResolvePR(ctx context.Context, clone Clone, opts ResolveOptions) (Target, error)

ResolvePR finds the pull request to work on. With --pr it is that pull request on --repo or on the repository the clone's origin remote names, and no request is made. Otherwise it follows the current branch:

  • When the branch's upstream is refs/pull/N/head (or /merge) of a GitHub remote, it is pull request N there.
  • When the upstream is a branch of a GitHub remote, it is the pull request whose head is that branch, as GitHub's associatedPullRequests lists it: the open one, or else the most recently updated. That pull request may target another repository, such as the upstream of a fork.
  • Failing those, it is the pull request whose head is the branch of the same name on --repo or origin.

With --repo, a pull request found through the upstream counts only when it is in that repository. Remote URLs are read with insteadOf applied.

func (*Client) ResolveThread

func (c *Client) ResolveThread(ctx context.Context, repository Repository, id string) error

ResolveThread marks the review thread resolved.

func (*Client) SearchRepositories

func (c *Client) SearchRepositories(ctx context.Context, host, query string, scope SearchScope, cursor *string) (ViewerRepositoryList, error)

SearchRepositories asks GitHub's repository search for names within scope, including forks and private repositories the token can see. GitHub combines the owner and repository qualifiers with OR. With nothing in scope it returns nothing, never all of GitHub. It never enumerates repositories. cursor continues an earlier page of the same search.

func (*Client) SubmitReview

func (c *Client) SubmitReview(ctx context.Context, repository Repository, number int, review ReviewSubmission) (ReviewResult, error)

SubmitReview submits one pull request review with its inline comments. It is sent once and never retried: a second attempt could post the review twice.

func (*Client) ThreadState

func (c *Client) ThreadState(ctx context.Context, repository Repository, id string) (ThreadState, error)

ThreadState reads one review thread by its node id.

func (*Client) UnresolvedThreads

func (c *Client) UnresolvedThreads(ctx context.Context, repository Repository, number int) (ReviewThreads, error)

UnresolvedThreads reads the pull request's unresolved review threads, with the first comments of each, from up to four pages of 50 threads. A pending comment (the signed-in account's unsubmitted review) is left out.

func (*Client) ViewerRepositories

func (c *Client) ViewerRepositories(ctx context.Context, host string, first int, cursor *string) (ViewerRepositoryList, error)

ViewerRepositories reads one topRepositories page, without following it.

type Clone

type Clone interface {
	// RemoteFetchURL is the URL git fetches the named remote from, with
	// insteadOf rewrites applied; "" when there is no such remote.
	RemoteFetchURL(ctx context.Context, remote string) (string, error)
	// CurrentBranch is the branch HEAD is on, "" when detached.
	CurrentBranch(ctx context.Context) (string, error)
	// BranchUpstream is the branch's branch.<name>.remote and
	// branch.<name>.merge, each "" when unset.
	BranchUpstream(ctx context.Context, branch string) (remote, merge string, err error)
}

Clone is the local clone a pull request is resolved from; *gitlocal.Repo implements it.

type Evidence

type Evidence struct {
	// Host is the GitHub host the evidence was read from.
	Host string
	// Repository is "owner/name" as GitHub spells it.
	Repository   string
	PullRequest  bundle.PullRequest
	Checks       []bundle.Check
	Conversation []bundle.ConversationItem
	Threads      []bundle.Thread
	// Images are the images GitHub rendered from the description, for the
	// attachment step to download. Their FetchURL is signed: it is never
	// stored or logged.
	Images  []bundle.RenderedImage
	Sources bundle.Sources
	// Unavailable says why each part recorded false in Sources could not be
	// read.
	Unavailable map[string]error
}

Evidence is the GitHub-sourced part of a generation bundle for one pull request at its current head.

func (Evidence) Apply

func (e Evidence) Apply(b *bundle.Bundle)

Apply copies the evidence into b, merging its sources into b's.

type FetchOptions

type FetchOptions struct {
	// MetadataOnly reads the PR header and current checks without fetching
	// conversations, review threads, commit lists or protection rules.
	MetadataOnly bool

	// ChecksHead reads checks for this reviewed commit, while PR metadata stays current.
	// Empty uses the current pull request head.
	ChecksHead string

	// BotReportMarkers are the bot-report markers of the base branch's
	// REVIEW.md marker registry. The registry is not built yet, so callers
	// pass none.
	BotReportMarkers []string
}

FetchOptions adjust what Fetch flags.

type ListedPull

type ListedPull struct {
	Number int
	Title  string
	Body   string
	// State is "open" or "closed".
	State string
	Draft bool
	// Author is the opener's login; Bot is set when GitHub reports the
	// account as a bot.
	Author           string
	Bot              bool
	HeadRef, HeadSHA string
	BaseRef, BaseSHA string
	// CrossRepository is set when the head is in another repository than
	// the base (a fork's, or one deleted since).
	CrossRepository bool
	// DefaultBranch is the base repository's default branch.
	DefaultBranch        string
	MergeCommitSHA       string
	CreatedAt, UpdatedAt time.Time
	MergedAt, ClosedAt   *time.Time
}

ListedPull is a pull request as the REST pull request list gives it.

type MergeError

type MergeError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

MergeError is a refusal from GitHub, worded for the person who clicked Merge.

func (*MergeError) Error

func (e *MergeError) Error() string

type MergeOptions

type MergeOptions struct {
	// Methods lists the allowed merge methods in a fixed order.
	Methods []string `json:"methods"`
	// Default is the method GitHub preselects for this user, or empty.
	Default          string `json:"default"`
	State            string `json:"state"`
	Draft            bool   `json:"draft"`
	HeadSHA          string `json:"head_sha"`
	Mergeable        string `json:"mergeable"`
	MergeStateStatus string `json:"merge_state_status"`
	// contains filtered or unexported fields
}

MergeOptions is what the repository allows and the pull request's current state, read just before the user confirms a merge.

type Options

type Options struct {
	// Transport carries the requests; nil uses http.DefaultTransport.
	Transport       http.RoundTripper
	AuthResult      func(host, token string, status int, headers http.Header)
	AccountIdentity func(host, token string) string
}

Options configure a Client. The zero value talks to GitHub.

type PullList

type PullList struct {
	// Pulls are the pull requests updated at or after the cursor, most
	// recently updated first.
	Pulls []ListedPull
	// ETag is the first page's, for the next call.
	ETag string
	// NotModified is set when the first page is unchanged since ETag.
	NotModified bool
	// Calls counts the requests made.
	Calls int
	// Truncated is set when the listing stopped at its bound with older pull
	// requests still updated at or after since unlisted.
	Truncated bool
}

PullList is what ListPullRequests read.

func ListPullRequests

func ListPullRequests(ctx context.Context, reader Reader, repository Repository, etag string, since time.Time) (PullList, error)

ListPullRequests lists the repository's pull requests of every state, most recently updated first, until one was last updated before since; one updated exactly at since is listed. The first page is read with etag; when GitHub answers that it is unchanged nothing else is read. The ETag is kept only for a first page read at the default size. A listing that reaches maxPulls with more still to list stops there and is Truncated.

type RESTReader

type RESTReader struct {
	// contains filtered or unexported fields
}

RESTReader reads, and for the confirmed writes of the app (Write) sends, only relative API paths on one validated GitHub host. Its identity partitions private caches without persisting a credential.

func (*RESTReader) CacheIdentity

func (r *RESTReader) CacheIdentity() string

func (*RESTReader) GraphQL

func (r *RESTReader) GraphQL(ctx context.Context, query string, variables map[string]any) (int, http.Header, []byte, error)

GraphQL reads one bounded operation without retries or waiting. Discovery owns scheduling and budgets; this uses the same authenticated, redirect-safe client.

func (*RESTReader) Read

func (r *RESTReader) Read(ctx context.Context, path, etag string) (int, http.Header, []byte, error)

Read returns bounded raw JSON and headers, including rate and conditional response headers. Only GET is available; redirects are refused.

func (*RESTReader) Tail

func (r *RESTReader) Tail(ctx context.Context, path string, limit int) (int, []byte, error)

Tail reads path, which GitHub may answer with a redirect to a signed download URL as it does for job logs, and returns the status and the last limit bytes of the body. One redirect is followed, only to an https URL, and the token is never sent there: it goes to the API host alone. At most 64 MiB are read.

func (*RESTReader) Write

func (r *RESTReader) Write(ctx context.Context, method, path string, payload any) (int, []byte, error)

Write sends one PUT or POST with a JSON body to a relative API path and returns the status and the bounded answer. It is never retried: the callers are the user's confirmed writes (update a branch, re-run workflow runs).

type RateLimitedError

type RateLimitedError struct {
	ResumeAt time.Time
	Err      error
}

RateLimitedError reports a GitHub rate limit the client did not wait out: its reset was further away than the client waits, or it persisted through every retry. ResumeAt is when GitHub said to try again, zero when unknown.

func (*RateLimitedError) Error

func (e *RateLimitedError) Error() string

func (*RateLimitedError) Unwrap

func (e *RateLimitedError) Unwrap() error

type Reader

type Reader interface {
	Read(ctx context.Context, path, etag string) (int, http.Header, []byte, error)
}

Reader reads one relative REST path with an optional ETag; *RESTReader is one.

type Repository

type Repository struct {
	Host, Owner, Name string
}

Repository names a repository on a GitHub host.

func ParseRepository

func ParseRepository(value string) (Repository, error)

ParseRepository reads --repo: "owner/name" on github.com, or "host/owner/name" on another GitHub host such as an Enterprise Server, as gh reads it. The host must be one gh treats as GitHub (see gitHubHost), so a mistyped or hostile host is refused before any token is looked up.

func RepositoryFromRemote

func RepositoryFromRemote(remote string) (Repository, error)

RepositoryFromRemote reads the repository a git remote URL names, in the https, ssh and scp-like forms, as gh reads it: an SSH host alias is resolved through `ssh -G`, and ssh.github.com is github.com. The host must be one gh treats as GitHub: github.com, a *.ghe.com tenant, a host gh is logged in to, or the one GH_HOST names. Any other host is another service, which must never be sent a GitHub token. An earlier github.com name GitHub forwards reads as the current one (gitlocal.RenamedTo).

func (Repository) String

func (r Repository) String() string

String is "owner/name" on github.com and "host/owner/name" elsewhere.

type ResolveOptions

type ResolveOptions struct {
	Repo string
	PR   int
}

ResolveOptions are the --repo and --pr flags; the zero value of each means it was not given.

type ReviewComment

type ReviewComment struct {
	Path      string `json:"path"`
	Line      int    `json:"line"`
	Side      string `json:"side"`
	StartLine int    `json:"start_line,omitempty"`
	StartSide string `json:"start_side,omitempty"`
	Body      string `json:"body"`
}

ReviewComment is one inline comment on a diff line. StartLine and StartSide are set for a comment on a range of lines and omitted otherwise.

type ReviewError

type ReviewError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

ReviewError is a refusal from GitHub, worded for the person who clicked Submit.

func (*ReviewError) Error

func (e *ReviewError) Error() string

type ReviewResult

type ReviewResult struct {
	URL string `json:"url"`
}

ReviewResult is the submitted review.

type ReviewSubmission

type ReviewSubmission struct {
	CommitID string          `json:"commit_id"`
	Body     string          `json:"body,omitempty"`
	Event    string          `json:"event"`
	Comments []ReviewComment `json:"comments,omitempty"`
}

ReviewSubmission is a review the user wrote. CommitID is the head commit the reviewer read, so comments land on the lines they saw.

type ReviewThread

type ReviewThread struct {
	// ID is the thread's GraphQL node id, which baum alone uses.
	ID   string
	Path string
	// Line is the thread's line, or its original line once outdated; 0 for
	// a comment on the whole file.
	Line     int
	Outdated bool
	// CanReply and CanResolve are the signed-in account's.
	CanReply, CanResolve bool
	// URL is the thread's first submitted comment on GitHub.
	URL string
	// Comments are the first ReadThreadComments submitted comments; Total
	// counts them all.
	Comments []ThreadComment
	Total    int
}

ReviewThread is an unresolved review thread as a fix reads it.

type ReviewThreads

type ReviewThreads struct {
	HeadSHA string
	// ViewerDidAuthor: the signed-in account opened the pull request.
	ViewerDidAuthor bool
	// Unresolved are the unresolved threads in GitHub's order.
	Unresolved []ReviewThread
	// Capped: the pull request has more threads than baum read.
	Capped bool
}

ReviewThreads is what a fix reads of a pull request's review threads.

type SearchScope

type SearchScope struct {
	Owners       []string
	Repositories []string
}

SearchScope is what a repository search may return: every repository of Owners (the viewer's login and its organizations) and the listed Repositories (lowercase owner/name), the viewer's collaborations elsewhere. An owner of one of those repositories is not searched as a whole, since the viewer has no permission on its other repositories.

type Target

type Target struct {
	Repository Repository
	Number     int
}

Target is the pull request a command works on.

type ThreadComment

type ThreadComment struct {
	Author string
	// Association is GitHub's author association (OWNER, MEMBER,
	// COLLABORATOR, CONTRIBUTOR, NONE…); Bot marks an app's account.
	Association string
	Bot         bool
	Body        string
	// URL is the comment on GitHub; it is not part of what a fix digests.
	URL string `json:"-"`
}

ThreadComment is one submitted comment of a review thread. Body is the reviewer's text, untrusted.

type ThreadState

type ThreadState struct {
	// Repository is "owner/name" and Number the pull request the thread is on.
	Repository string
	Number     int
	Resolved   bool
	// CanReply and CanResolve are the signed-in account's.
	CanReply, CanResolve bool
}

ThreadState is a review thread as GitHub reports it now.

type TokenSource

type TokenSource func(ctx context.Context, host string) (string, error)

TokenSource returns the token for one GitHub host. The client asks it only for the host of the repository it is about to read, and sends the token only there.

type ViewerRepository

type ViewerRepository struct {
	Repository  string
	Private     bool
	Fork        bool
	PushedAt    time.Time
	Description string
}

ViewerRepository is a GitHub repository available to the signed-in account.

type ViewerRepositoryList

type ViewerRepositoryList struct {
	Repositories []ViewerRepository
	TotalCount   int
	NextCursor   *string
	// Scope is what the viewer may search, read with the top repositories.
	Scope SearchScope
}

ViewerRepositoryList contains one bounded GitHub response.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL