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
- Variables
- func DefaultCachePath() string
- func DefaultReadmeCachePath() string
- func ResolveToken(ctx context.Context, explicit string) (string, error)
- func SaveCache(path, user string, repos []Repo) error
- func SaveReadmes(path string, c ReadmeCache) error
- func TopUpCache(path, user string, repos []Repo, fullFetchedAt time.Time) error
- type Cache
- type CachedReadme
- type Client
- func (c *Client) Readme(ctx context.Context, owner, repo string, limits ReadmeLimits) (string, error)
- func (c *Client) Starred(ctx context.Context) ([]Repo, error)
- func (c *Client) StarredIncremental(ctx context.Context, cached []Repo) ([]Repo, bool, error)
- func (c *Client) Viewer(ctx context.Context) (string, error)
- type ItemLists
- type List
- type ListsClient
- func (c *ListsClient) CreateList(ctx context.Context, name, description string, private bool) (List, error)
- func (c *ListsClient) DeleteList(ctx context.Context, id string) error
- func (c *ListsClient) ListItems(ctx context.Context, id string) ([]string, error)
- func (c *ListsClient) Lists(ctx context.Context) ([]List, error)
- func (c *ListsClient) ListsWithRepos(ctx context.Context) ([]List, error)
- func (c *ListsClient) Login(ctx context.Context) (string, error)
- func (c *ListsClient) RepoIDs(ctx context.Context, fullNames []string) (map[string]string, error)
- func (c *ListsClient) SetItemLists(ctx context.Context, repoID string, listIDs []string) error
- func (c *ListsClient) SetItemListsBatch(ctx context.Context, items []ItemLists) (map[string]error, error)
- func (c *ListsClient) UpdateList(ctx context.Context, id, name, description string, private bool) error
- type ListsOption
- type ReadmeCache
- type ReadmeLimits
- type Repo
Constants ¶
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 ¶
var ( // 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") // 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.
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
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 ¶
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
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 ¶
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
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 ¶
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) StarredIncremental ¶ added in v0.2.0
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.
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) 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 ¶
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 ¶
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.