Help us shape the future of Go! Take the 2026 Go Developer Survey

gh

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package gh talks to GitHub: the documented REST API for everything it covers; star lists go through the GraphQL API in listops.go.

Index

Constants

View Source
const MaxLists = 32

MaxLists is how many star lists GitHub allows per account. It is not in the documentation, so it is a default rather than a certainty: createUserList failing is the authoritative answer.

Variables

View Source
var (
	// ErrReadmeUnavailable means a README cannot be read and will not become
	// readable soon: it is over the size the API serves, blocked, or taken
	// down. A caller may remember that verdict; every other failure is
	// worth asking about again.
	ErrReadmeUnavailable = errors.New("readme unavailable")

	// ErrRateLimited means the primary rate limit is exhausted and its reset
	// is further away than the retry budget, so waiting was not an option.
	// Nothing else will succeed until the window resets, and a caller with
	// thousands of requests queued should stop rather than fail each one.
	ErrRateLimited = errors.New("rate limit exhausted")

	// ErrUnauthorized means GitHub rejected the token. A token that has
	// expired mid-run fails every request from then on in the same way.
	ErrUnauthorized = errors.New("token rejected")
)

Errors a caller can act on. Each wraps the error GitHub answered with, so errors.Is finds the sentinel and the message still says what happened.

View Source
var ErrNeedsUserScope = fmt.Errorf(
	"changing star lists needs a token with the 'user' scope; run `gh auth refresh -s user`, " +
		"or set GITHUB_TOKEN to a token that has it")

ErrNeedsUserScope means the token can read lists but not change them.

Functions

func DefaultCachePath

func DefaultCachePath() string

DefaultCachePath returns the per-user cache location.

func DefaultReadmeCachePath added in v0.2.0

func DefaultReadmeCachePath() string

DefaultReadmeCachePath returns the per-user README cache location.

func ResolveToken added in v0.2.0

func ResolveToken(ctx context.Context, explicit string) (string, error)

ResolveToken finds the GitHub token to use, once: the explicit value if there is one, then $GITHUB_TOKEN, then $GH_TOKEN, and finally whatever the gh CLI has stored. When every source comes up empty the error names all of them, so the reader knows which one to fill in.

A command should call this once at its start and hand the result to every client it builds. The constructors deliberately do no discovery of their own: each one that did repeated the gh fallback, so a run needing three clients could spend three timeouts on a stuck keyring and report the second failure as something other than a missing token.

func SaveCache

func SaveCache(path, user string, repos []Repo) error

SaveCache stores the stars of a full walk so that re-running with different clustering settings costs no API calls. Both timestamps are set to now. A top-up, which reads only the tail, goes through TopUpCache instead, so that a read that revisited nothing does not count as a full one.

README text is stripped: it lives in its own cache with its own expiry, and writing it here as well would both double the file and tie a megabyte of prose to the star list's one-day lifetime.

func SaveReadmes added in v0.2.0

func SaveReadmes(path string, c ReadmeCache) error

SaveReadmes stores the README openings.

func TopUpCache added in v0.2.0

func TopUpCache(path, user string, repos []Repo, fullFetchedAt time.Time) error

TopUpCache stores a topped-up star list. FetchedAt becomes now, while the full-fetch time is carried over from the cache that was topped up (fullFetchedAt, as LoadCache returned it), since only the pages after the cached tail were read. Stamping both, as SaveCache does, is what kept a cache topped up more often than --cache-full-ttl from ever being re-read in full.

Types

type Cache added in v0.2.0

type Cache struct {
	// Repos is nil on a miss.
	Repos []Repo
	// FetchedAt is when the cache was last written, whether by a full walk or
	// a top-up. --cache-ttl is measured against it.
	FetchedAt time.Time
	// FullFetchedAt is when the star list was last read in full. A top-up
	// never revisits the repositories already cached, so a description or
	// topic changed upstream reaches the model only once this passes
	// --cache-full-ttl and every page is read again.
	FullFetchedAt time.Time
}

Cache is a loaded star cache.

func LoadCache

func LoadCache(path, user string, maxAge time.Duration) (Cache, error)

LoadCache returns the cached stars for the user when they are younger than maxAge; a maxAge of zero or less accepts the cache at any age, which is what an incremental refresh reads it with. A miss — no file, a corrupt one, somebody else's stars — is reported as a Cache with no Repos rather than an error; a cache past maxAge keeps its timestamps, so the caller can say how old it was.

A file written before the full-fetch time was recorded has FullFetchedAt taken from FetchedAt. When that cache was last walked in full cannot be known, and treating it as never would force every upgrade into a full re-read; the next full walk records it properly.

type CachedReadme added in v0.2.0

type CachedReadme struct {
	Text      string    `json:"text"`
	FetchedAt time.Time `json:"fetched_at"`
}

CachedReadme is one stored README opening. The timestamp is per entry because entries are written at different times: a run fetches only what is missing, and stamping the untouched ones again would mean nothing ever expired.

type Client

type Client struct {

	// Log receives progress lines; nil discards them.
	Log func(format string, args ...any)
	// contains filtered or unexported fields
}

Client reads from the GitHub REST API.

func NewClient

func NewClient(token string) (*Client, error)

NewClient builds a REST client around an already-resolved token; see ResolveToken for where one comes from. Every request this tool makes reads the caller's own account, so there is no unauthenticated mode.

func (*Client) Readme

func (c *Client) Readme(ctx context.Context, owner, repo string, limits ReadmeLimits) (string, error)

Readme fetches the repository's README and reduces it to the opening that describes the project — its title, tagline and first descriptive paragraphs, with badges, code, links and the sections every project has stripped out — within limits. A missing README is not an error; some repositories have none.

The whole file is read and reduced before anything is cut. A README that opens with a screen of badges and a table of contents has its prose further in than any byte budget reaches, so cutting first left nothing to read.

A README that exists but cannot be served — too large for the API, blocked, taken down — comes back wrapped in ErrReadmeUnavailable, which is the caller's cue that asking again later will not help. Any other error may be the connection, the token or the rate limit, and says nothing about the README itself.

func (*Client) Starred

func (c *Client) Starred(ctx context.Context) ([]Repo, error)

Starred fetches every repository the authenticated user has starred.

func (*Client) StarredIncremental added in v0.2.0

func (c *Client) StarredIncremental(ctx context.Context, cached []Repo) ([]Repo, bool, error)

StarredIncremental brings a cached star list up to date by re-reading only its last page onwards, and reports whether that was sound.

Stars come back oldest first, so everything already cached keeps its position and new ones land at the end: reading the final page again and walking on from there is enough to find them, at two requests instead of one per hundred stars.

What that assumes is that nothing was removed. Unstarring a repository shifts every later one down a slot, and an unstar early in the list would otherwise go unnoticed for as long as the cache lived — leaving the plan to file a repository the account no longer stars. So the overlap is checked rather than trusted: the page that should still hold the cached tail has to hold exactly it, in order. It does not, ok is false and the caller re-reads the list in full, which is the same work the old code did unconditionally.

func (*Client) Viewer

func (c *Client) Viewer(ctx context.Context) (string, error)

Viewer returns the login of the authenticated user.

type ItemLists

type ItemLists struct {
	RepoID  string
	ListIDs []string
	// Name is carried only so failures can be reported against something the
	// user recognises.
	Name string
}

ItemLists is one repository and the complete set of lists it should belong to afterwards.

type List

type List struct {
	ID          string
	Name        string
	Slug        string
	Description string
	Private     bool
	// Count is how many items the list holds, which is known without reading
	// them.
	Count int
	// Repos holds "owner/name" for every repository in the list. It is filled
	// in only by ListsWithRepos; a listing does not need it and paging every
	// list's contents is by far the slowest thing this client does.
	Repos []string
}

List is a star list and, when read through Lists, everything in it.

type ListsClient

type ListsClient struct {
	Log func(format string, args ...any)
	// contains filtered or unexported fields
}

ListsClient talks to the GraphQL API.

func NewListsClient

func NewListsClient(token string, opts ...ListsOption) (*ListsClient, error)

NewListsClient builds a GraphQL client around an already-resolved token; see ResolveToken for where one comes from. viewer.lists is the caller's own account, so there is no unauthenticated mode.

func (*ListsClient) CreateList

func (c *ListsClient) CreateList(ctx context.Context, name, description string, private bool) (List, error)

CreateList creates a list and returns it.

func (*ListsClient) DeleteList

func (c *ListsClient) DeleteList(ctx context.Context, id string) error

DeleteList removes a list. The repositories in it stay starred.

Deleting is retried when GitHub answers "Resource limits for this query exceeded": removing a list with a few hundred members is enough work for the request to be refused, and unlike a batched write there is nothing here to split, so the only remedy is to ask again.

func (*ListsClient) ListItems

func (c *ListsClient) ListItems(ctx context.Context, id string) ([]string, error)

ListItems pages one list's contents.

func (*ListsClient) Lists

func (c *ListsClient) Lists(ctx context.Context) ([]List, error)

Lists returns every star list on the account, without reading its contents.

func (*ListsClient) ListsWithRepos

func (c *ListsClient) ListsWithRepos(ctx context.Context) ([]List, error)

ListsWithRepos returns every star list together with its full membership.

func (*ListsClient) Login

func (c *ListsClient) Login(ctx context.Context) (string, error)

Login reports the account the token belongs to.

func (*ListsClient) RepoIDs

func (c *ListsClient) RepoIDs(ctx context.Context, fullNames []string) (map[string]string, error)

RepoIDs resolves "owner/name" to the node ids the mutations take, in batches of one aliased query rather than one request each.

func (*ListsClient) SetItemLists

func (c *ListsClient) SetItemLists(ctx context.Context, repoID string, listIDs []string) error

SetItemLists sets the complete set of lists a repository belongs to.

The mutation replaces the whole set, exactly as the web UI's dialog did, so callers must pass the memberships they want to keep as well as the one they are adding — otherwise a hand-curated list silently loses the repository.

func (*ListsClient) SetItemListsBatch

func (c *ListsClient) SetItemListsBatch(ctx context.Context, items []ItemLists) (map[string]error, error)

SetItemListsBatch applies several membership changes in one request.

GraphQL executes top-level mutation fields serially and in order, so a batch behaves exactly like the same mutations sent one at a time — but it costs one request instead of one per repository. That matters because GitHub's limit on mutating requests is the binding constraint on how long an apply takes.

Failures are per repository: GitHub answers with the successes intact and an error naming the alias that failed, so a batch is not all-or-nothing. The returned map is keyed by repository name.

func (*ListsClient) UpdateList

func (c *ListsClient) UpdateList(ctx context.Context, id, name, description string, private bool) error

UpdateList sets a list's name, description and visibility.

type ListsOption

type ListsOption func(*ListsClient)

ListsOption adjusts a ListsClient. The endpoint override exists for GitHub Enterprise and for pointing tests at a local server, so there is no test-only constructor.

func WithEndpoint

func WithEndpoint(url string) ListsOption

WithEndpoint sends GraphQL somewhere other than api.github.com.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) ListsOption

WithHTTPClient supplies the HTTP client to use.

type ReadmeCache added in v0.2.0

type ReadmeCache map[string]CachedReadme

ReadmeCache maps a repository's full name to the opening of its README.

func LoadReadmes added in v0.2.0

func LoadReadmes(path string, maxAge time.Duration) (ReadmeCache, error)

LoadReadmes returns the cached README openings, dropping entries older than maxAge; maxAge of zero or less keeps every entry. A missing or unreadable cache is a miss rather than a failure, since the only cost is re-fetching.

func ReadmesFromStarCache added in v0.2.0

func ReadmesFromStarCache(path string) ReadmeCache

ReadmesFromStarCache harvests the READMEs an older version stored inside the star cache, so that upgrading does not re-fetch thousands of them. The star cache's own age and owner are ignored: a README belongs to a repository, not to a particular fetch of somebody's stars.

func (ReadmeCache) Prune added in v0.2.0

func (c ReadmeCache) Prune(maxAge time.Duration)

Prune drops entries older than maxAge; zero or less keeps every entry.

type ReadmeLimits added in v0.2.0

type ReadmeLimits struct {
	// Words is how many words of the opening to keep; zero or less keeps
	// the whole opening.
	Words int
	// Bytes caps the text kept, after the markup is stripped and the opening
	// chosen; zero or less is no cap. A safety net for the word cut, which
	// a language written without spaces can walk straight past.
	Bytes int
}

ReadmeLimits bounds what Readme keeps of a README.

type Repo

type Repo struct {
	ID          int64     `json:"id"`
	FullName    string    `json:"full_name"`
	Description string    `json:"description"`
	Language    string    `json:"language"`
	Topics      []string  `json:"topics"`
	Stars       int       `json:"stargazers_count"`
	HTMLURL     string    `json:"html_url"`
	Archived    bool      `json:"archived"`
	Fork        bool      `json:"fork"`
	PushedAt    time.Time `json:"pushed_at"`
	StarredAt   time.Time `json:"starred_at"`
	Readme      string    `json:"readme,omitempty"`
}

Repo is the subset of a starred repository that the categorizer reads. It is deliberately its own type rather than go-github's: it is what gets cached on disk, and the cache should not have to change when the API client does.

func (Repo) Name

func (r Repo) Name() string

Name returns the repository name without its owner.

func (Repo) Owner

func (r Repo) Owner() string

Owner returns the account or organization that owns the repository.

Jump to

Keyboard shortcuts

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