gh

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package gh reads github.com without a token.

Every byte this package fetches is a byte a logged-out browser would get: public HTML, the JSON those pages ship inside themselves, the JSON their own front end asks for, Atom feeds, and the git smart protocol. There is no REST client here and there never will be one. The unauthenticated REST API allows sixty requests an hour, which is not enough to read one organization, and the pages are behind a CDN, which makes them faster than the API even when the API would work.

The consequence is a read-only tool. It cannot see a private repository and it cannot write anything. For that, use the official gh.

Index

Constants

View Source
const (
	StatusOK   = "ok"
	StatusWarn = "warn"
	StatusFail = "fail"
)

The three states a check can be in. Warn exists because most of what goes wrong here is survivable: a token in the environment, a cache that cannot be written, a page that parsed but looks thinner than it should.

View Source
const (
	BaseURL   = "https://github.com"
	RawURL    = "https://raw.githubusercontent.com"
	CodeLoad  = "https://codeload.github.com"
	GistURL   = "https://gist.github.com"
	GistRaw   = "https://gist.githubusercontent.com"
	AvatarURL = "https://avatars.githubusercontent.com"
	OpenGraph = "https://opengraph.githubassets.com"
)

The hosts. All five are public and none of them accept a credential from us.

View Source
const (
	// SrcID is derived from the id structure alone. No fetch, always correct.
	SrcID = "id"
	// SrcPayload is an explicit reference in a JSON payload or a Relay result.
	SrcPayload = "payload"
	// SrcFeed is an explicit reference in an Atom feed.
	SrcFeed = "feed"
	// SrcHTML was parsed out of rendered markup with a selector. Good, and it
	// degrades to a missing edge rather than a wrong one when a template moves.
	SrcHTML = "html"
	// SrcText is a pattern matched in free text: #42, a bare SHA. Heuristic, and
	// dropped by the default --min-trust.
	SrcText = "text"
)

The five extraction rules, in descending order of trust. Every edge carries the one that produced it, which is the field a consumer uses to decide how much to believe.

View Source
const (
	// Ownership and membership.
	PredOwnedBy          = "ownedBy"
	PredMemberOf         = "memberOf"
	PredPartOf           = "partOf"
	PredBelongsToPackage = "belongsToPackage"

	// Derivation. The edges that make a graph worth walking.
	PredForkOf     = "forkOf"
	PredTemplateOf = "templateOf"
	PredMirrorOf   = "mirrorOf"
	PredDependsOn  = "dependsOn"
	PredUsedBy     = "usedBy"

	// Authorship and activity.
	PredAuthoredBy          = "authoredBy"
	PredCommittedBy         = "committedBy"
	PredContributedTo       = "contributedTo"
	PredAssignedTo          = "assignedTo"
	PredReviewedBy          = "reviewedBy"
	PredReviewRequestedFrom = "reviewRequestedFrom"
	PredMergedBy            = "mergedBy"

	// Reference.
	PredReferences    = "references"
	PredCloses        = "closes"
	PredClosedBy      = "closedBy"
	PredDuplicateOf   = "duplicateOf"
	PredSubIssueOf    = "subIssueOf"
	PredLinkedTo      = "linkedTo"
	PredTargetsBranch = "targetsBranch"
	PredFromBranch    = "fromBranch"
	PredPointsAt      = "pointsAt"
	PredParentOf      = "parentOf"

	// Classification.
	PredHasTopic      = "hasTopic"
	PredHasLabel      = "hasLabel"
	PredInMilestone   = "inMilestone"
	PredWrittenIn     = "writtenIn"
	PredLicensedUnder = "licensedUnder"
	PredRelatedTopic  = "relatedTopic"

	// Social. Opt-in everywhere, because the star list of a popular repository
	// is thousands of pages and nobody wants that by accident.
	PredStarredBy   = "starredBy"
	PredFollows     = "follows"
	PredSponsors    = "sponsors"
	PredReactedWith = "reactedWith"
)

The predicate vocabulary. This is the complete set: an edge this tool emits has its predicate here, and adding a relation means adding a constant first.

View Source
const (
	FactName        = "name"
	FactDescription = "description"
	FactHomepage    = "homepage"
	FactCreated     = "created"
	FactUpdated     = "updated"
	FactStars       = "stars"
	FactForks       = "forks"
	FactWatchers    = "watchers"
	FactCommits     = "commits"
	FactURI         = "uri"
	FactAvatar      = "avatar"
	FactState       = "state"
	FactCount       = "count"
)

The literal predicates. These name Fact rows rather than edges.

View Source
const (
	NSSchema = "https://schema.org/"
	NSGH     = "https://github.com/ns#"
	NSGHR    = "https://github.com/"
	NSRdf    = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
	NSRdfs   = "http://www.w3.org/2000/01/rdf-schema#"
	NSXsd    = "http://www.w3.org/2001/XMLSchema#"
	NSDoap   = "http://usefulinc.com/ns/doap#"
	NSFoaf   = "http://xmlns.com/foaf/0.1/"
)

The namespaces.

View Source
const (
	TypeInteger  = "xsd:integer"
	TypeDecimal  = "xsd:decimal"
	TypeBoolean  = "xsd:boolean"
	TypeDateTime = "xsd:dateTime"
)

The datatypes a Fact can carry. They are CURIEs so a Fact reads the same in every serialisation.

View Source
const (
	FormatNT     = "nt"
	FormatNQuads = "nq"
	FormatTurtle = "ttl"
	FormatJSONLD = "jsonld"
)

The output formats.

View Source
const (
	SearchRepos       = "repositories"
	SearchIssues      = "issues"
	SearchPulls       = "pullrequests"
	SearchUsers       = "users"
	SearchCommits     = "commits"
	SearchDiscussions = "discussions"
	SearchTopics      = "topics"
	SearchPackages    = "registrypackages"
	SearchWikis       = "wikis"
	SearchMarket      = "marketplace"
	SearchCode        = "code"
)

The site's own names for the search types. They are not guessable (issues but pullrequests, registrypackages but wikis) which is exactly why they are constants.

View Source
const (
	KindRepo       = "repo"
	KindUser       = "user"
	KindOrg        = "org"
	KindIssue      = "issue"
	KindPR         = "pr"
	KindDiscussion = "discussion"
	KindCommit     = "commit"
	KindBranch     = "branch"
	KindTag        = "tag"
	KindRelease    = "release"
	KindFile       = "file"
	KindTree       = "tree"
	KindLabel      = "label"
	KindMilestone  = "milestone"
	KindTopic      = "topic"
	KindGist       = "gist"
	KindPackage    = "package"
	KindAction     = "action"
	KindWiki       = "wiki"
	KindAdvisory   = "advisory"
	KindCompare    = "compare"

	// These three name records GitHub derives rather than serves. There is no
	// page whose address is one contributor's statistics or one day of a
	// calendar, so they get a URI and no canonical URL, and Locate points at
	// the page they were read from instead of inventing one.
	KindContributor  = "contributor"
	KindContribution = "contribution"
	KindEvent        = "event"
)

The kinds. Twenty are addressable as github:// URIs; compare is a recognised route that names a range rather than a thing, and is here because people paste compare URLs.

View Source
const DefaultMinTrust = SrcHTML

DefaultMinTrust keeps everything except free-text guesses.

View Source
const Scheme = "github"

Scheme is the URI scheme this package mints and dereferences.

View Source
const UserAgentBase = "github-cli"

UserAgentBase is the honest half of the User-Agent. The version is appended at runtime. It is deliberately not configurable: making it configurable would be making impersonation a feature, and a tool that reads only public pages has no reason to hide.

Variables

DefaultFollow is the crawler's follow set. It deliberately excludes references, starredBy, follows, dependsOn, and usedBy: those five turn a bounded walk into an unbounded one, and each has to be asked for by name.

View Source
var Defaults = Config{
	Rate:    125 * time.Millisecond,
	Retries: 4,
	Workers: 4,
	Timeout: 30 * time.Second,
}

Defaults are the pacing numbers every command starts from. GitHub publishes no rate limit for the pages, so these are chosen to be quieter than a person browsing with a few tabs open: eight requests a second across four workers.

Kinds is the whole set, in the order above, for help text and for the error a bad kind produces. Listing them is the difference between an error a reader can act on and one that sends them to the source.

LiteralPredicates are the two whose object is a bare string rather than a URI, because a language and a licence are not github.com entities. RDF gives them synthetic IRIs in the gh: namespace; `github edges` prints them as they are written on the page.

RDFFormats is the accepted set, for help text and validation.

View Source
var Routes = []RouteInfo{
	{"/{owner}/{repo}", "embedded", "route-json", "sidebarAbout lives only in the HTML payload"},
	{"/{owner}/{repo}/tree/{ref}/{path}", "route-json", "embedded", ""},
	{"/{owner}/{repo}/blob/{ref}/{path}", "embedded", "raw", "the route JSON dropped the metadata block, so the page is the read; bytes from raw"},
	{"/{owner}/{repo}/branches", "route-json", "xhr", ""},
	{"/{owner}/{repo}/refs", "xhr", "git", "names only, 6 KB against 588 KB"},
	{"/{owner}/{repo}/commits/{ref}", "route-json", "feed", ""},
	{"/{owner}/{repo}/commit/{sha}", "route-json", "raw", "the diff comes from .patch"},
	{"/{owner}/{repo}/compare/{a}...{b}", "route-json", "raw", ""},
	{"/{owner}/{repo}/issues/{n}", "embedded", "ld-json", "Relay preloaded queries"},
	{"/{owner}/{repo}/pull/{n}", "embedded", "raw", "Relay, plus .diff and .patch"},
	{"/{owner}/{repo}/discussions/{n}", "embedded", "", "Relay"},
	{"/{owner}/{repo}/issues", "search", "", "type=issues with a repo: qualifier"},
	{"/{owner}/{repo}/pulls", "search", "", "type=pullrequests"},
	{"/{owner}/{repo}/releases", "feed", "html", "releases.atom, then a page each for assets"},
	{"/{owner}/{repo}/releases/tag/{tag}", "html", "feed", "download counts exist nowhere else"},
	{"/{owner}/{repo}/tags", "feed", "git", "the feed is recent, git is complete"},
	{"/{owner}/{repo}/graphs/contributors", "xhr", "", "answers 202 while it computes, so it polls"},
	{"/{owner}/{repo}/wiki", "feed", "html", ""},
	{"/{owner}/{repo}.git/info/refs", "git", "", "every ref and its SHA in one request"},
	{"/{login}", "html", "ld-json", "microdata and microformats, no payload at all"},
	{"/{org}", "html", "xhr", "two deferred fragments under --deep"},
	{"/{login}?tab=repositories", "search", "html", "user: qualifier"},
	{"/{login}?tab=stars", "html", "", ""},
	{"/{login}.atom", "feed", "", ""},
	{"/users/{login}/hovercard", "xhr", "html", ""},
	{"/users/{login}/contributions", "xhr", "", "returns an HTML fragment, not JSON"},
	{"/orgs/{org}/people", "html", "", ""},
	{"/search", "search", "", "ten types, code is the one that needs a token"},
	{"/trending", "html", "", "no JSON equivalent exists, tokened or not"},
	{"/topics/{slug}", "html", "search", ""},
	{"gist.github.com/{id}", "html", "raw", ""},
	{"raw.githubusercontent.com/...", "raw", "", ""},
	{"codeload.github.com/...", "raw", "", ""},
}

Routes is the whole index. Surface names are the ones in doc 01: html, route-json, xhr, search, feed, raw, git, embedded, ld-json.

SearchTypes is every type in the order the commands present them.

SocialPredicates are the ones a command has to be asked for by name.

TrustLevels is the accepted set, for help text and for validation.

Functions

func Classify

func Classify(input string) (kind, id string, err error)

Classify turns anything a person might paste into a kind and an id. It does no I/O, and it never fails on a well-formed github.com URL.

Two of its answers are guesses and both are documented as such. A bare word is a user, because a pure function cannot tell a user from an organization without asking. A bare owner/name is a repository. `github get` reads the page and returns a record whose Kind is the truth; classification is a routing hint, not an answer.

func DefaultCacheDir

func DefaultCacheDir() string

DefaultCacheDir is $XDG_CACHE_HOME/github-cli, or the platform equivalent.

func DomainDefaults

func DomainDefaults(c *kit.Config)

DomainDefaults overlays this site's baseline onto the framework's. GitHub publishes no rate limit for the pages, so these are chosen to be quieter than a person browsing with a few tabs open: eight requests a second across four workers.

func Extract

func Extract(rec any) (Node, []Edge, []Fact)

Extract turns one record into its node, its edges, and its facts. A record kind it does not know produces an empty node, which every caller treats as nothing to say rather than as an error.

func IRI

func IRI(uri string) string

IRI maps a github:// URI to its dereferenceable https form. The github:// form stays on the record plane, where it is a stable key rather than a location.

The three derived kinds have no address of their own, so they map into the gh: namespace instead of pretending to be a page.

func Locate

func Locate(kind, id string) (string, error)

Locate turns a kind and id back into the canonical github.com URL. Locate(Classify(u)) is the canonical form of u, which is what makes -o url safe to pipe back into the tool.

func Page

func Page(parts ...string) string

Page builds a github.com URL from path segments, escaping each one.

It really does escape now. It used to copy the segments across untouched while the comment said otherwise, which is the kind of helper that works until the first branch with a space in its name.

func RepoOf

func RepoOf(kind, id string) (string, bool)

RepoOf returns the repository an id belongs to, for the kinds whose id carries one. This is what makes `github tree <a file URL>` work.

func ResolveRef

func ResolveRef(want, input string) (string, error)

ResolveRef resolves any accepted reference to the id for the kind a command names.

Classify has two guesses in it: one bare word is a user, and one bare owner/name is a repository. Those are guesses because a pure function cannot tell a user from an organization or a repository from anything else without asking, so a guess yields to the command that names its own kind while an explicit URL or URI does not. That is what makes `github org golang` work and `github org https://github.com/golang/go` fail with a message that says why.

func ResolveRepo

func ResolveRepo(input string) (string, error)

ResolveRepo resolves any reference to the repository it belongs to. Commands that work on a repository use it so that a pasted file URL, issue URL, or release URL all name the repository they are part of, which is what makes `github commits <any URL from the repo>` work.

func SortEdges

func SortEdges(edges []Edge)

SortEdges gives an export a stable order, which is what makes a diff of two runs readable.

func SplitPathID

func SplitPathID(id string) (repo, ref, path string, ok bool)

SplitPathID splits owner/name@ref/path/to/file into its three parts. The ref runs to the next slash, which means a branch with a slash in its name (feature/x) parses as ref "feature" and path "x/...". That is a real ambiguity in GitHub's own URLs and nothing here can resolve it; a caller who knows better passes --ref.

func SplitRepo

func SplitRepo(id string) (owner, name string, ok bool)

SplitRepo splits owner/name.

func SplitThreadID

func SplitThreadID(id string) (repo string, num string, ok bool)

SplitThreadID splits owner/name#123.

func Streams

func Streams(format string) bool

Streams reports whether a format can be written a triple at a time.

func TrustAtLeast

func TrustAtLeast(source, min string) bool

TrustAtLeast reports whether a source meets a floor. An unknown floor lets everything through rather than silently dropping the whole graph, and an unknown source is treated as the weakest thing there is.

func URI

func URI(kind, id string) string

URI renders the github:// form. It is a string join and not a url.URL, because ids contain `#` and `@` on purpose and url.URL would escape them.

func WriteRDF

func WriteRDF(w io.Writer, g *Graph, o RDFOptions) error

WriteRDF serialises a whole graph. N-Triples is the default because it streams: nt and nq write a line per triple as it is produced, ttl buffers one subject at a time, and jsonld buffers the lot.

Types

type Account

type Account struct {
	Base

	Login string `json:"login"          table:"login"`
	Name  string `json:"name,omitempty" table:"name"`
	// Type is User or Organization, decided by which blocks the page carries
	// rather than guessed from the login.
	Type string `json:"type" table:"type"`

	Bio         string   `json:"bio,omitempty"          table:"bio,truncate"`
	Company     string   `json:"company,omitempty"      table:"company"`
	Location    string   `json:"location,omitempty"     table:"location"`
	Website     string   `json:"website,omitempty"      table:"-"`
	Email       string   `json:"email,omitempty"        table:"-"`
	Pronouns    string   `json:"pronouns,omitempty"     table:"-"`
	SocialLinks []string `json:"social_links,omitempty" table:"-"`

	DatabaseID *int   `json:"database_id,omitempty" table:"-"`
	NodeID     string `json:"node_id,omitempty"     table:"-"`
	AvatarURL  string `json:"avatar_url,omitempty"  table:"-"`

	Followers        *int   `json:"followers,omitempty"         table:"followers"`
	FollowersDisplay string `json:"followers_display,omitempty" table:"-"`
	Following        *int   `json:"following,omitempty"         table:"following"`
	Starred          *int   `json:"starred,omitempty"           table:"-"`
	RepoCount        *int   `json:"repo_count,omitempty"        table:"repos"`
	// These three come from the navigation tabs, which is the only place
	// either template states them. A tab with nothing in it carries no counter
	// at all, so zero and absent are the same thing here and the field stays
	// nil rather than claiming a zero it did not read.
	PackageCount    *int `json:"package_count,omitempty"    table:"-"`
	ProjectCount    *int `json:"project_count,omitempty"    table:"-"`
	SponsoringCount *int `json:"sponsoring_count,omitempty" table:"-"`

	CreatedAt *time.Time `json:"created_at,omitempty" table:"joined,time"`

	Sponsorable bool `json:"sponsorable" table:"-"`
	IsVerified  bool `json:"is_verified" table:"-"`
	IsHireable  bool `json:"is_hireable" table:"-"`

	ReadmeHTML string `json:"readme_html,omitempty" table:"-"`
	ReadmeText string `json:"readme_text,omitempty" table:"-"`

	PinnedRepos   []string `json:"pinned_repos,omitempty"  table:"-"`
	Organizations []string `json:"organizations,omitempty" table:"-"`
	Achievements  []string `json:"achievements,omitempty"  table:"-"`

	SocialImageURL string `json:"social_image_url,omitempty" table:"-"`
}

Account is a user or an organization. Profiles carry no JSON payload at all, so every field here comes from microdata, a microformat class, a stable data attribute, or a counted link. That makes accounts the most selector-dependent records in the tool, and the reason every field has a golden pinning it.

type Action

type Action struct {
	Base

	Name        string `json:"name"                  table:"name"`
	Slug        string `json:"slug"                  table:"-"`
	Owner       string `json:"owner,omitempty"       table:"owner"`
	Description string `json:"description,omitempty" table:"description,truncate"`

	// ShortDescription is the one-line blurb on the listing card, which is a
	// different string from Description on an app and the same one on a
	// repository action. Both are kept rather than picked between.
	ShortDescription    string `json:"short_description,omitempty"    table:"-"`
	FullDescription     string `json:"full_description,omitempty"     table:"-"`
	ExtendedDescription string `json:"extended_description,omitempty" table:"-"`

	Type              string   `json:"type,omitempty"               table:"type"`
	PrimaryCategory   string   `json:"primary_category,omitempty"   table:"category"`
	SecondaryCategory string   `json:"secondary_category,omitempty" table:"-"`
	Highlights        []string `json:"highlights,omitempty"         table:"-"`

	// A repository action and a marketplace app are both listings and the
	// search results are interleaved, but only one of these two groups is ever
	// populated for a given record. Which group is filled in is itself the
	// answer to "what kind of thing is this".
	Path         string `json:"path,omitempty"          table:"-"`
	RepositoryID *int   `json:"repository_id,omitempty" table:"-"`
	IconName     string `json:"icon_name,omitempty"     table:"-"`
	IconColor    string `json:"icon_color,omitempty"    table:"-"`

	ListingID         *int   `json:"listing_id,omitempty"         table:"-"`
	LogoURL           string `json:"logo_url,omitempty"           table:"-"`
	InstallationCount *int   `json:"installation_count,omitempty" table:"installs"`
	State             string `json:"state,omitempty"              table:"state"`
	CompanyURL        string `json:"company_url,omitempty"        table:"-"`
	DocumentationURL  string `json:"documentation_url,omitempty"  table:"-"`
	SupportURL        string `json:"support_url,omitempty"        table:"-"`
	PrivacyPolicyURL  string `json:"privacy_policy_url,omitempty" table:"-"`
	TermsURL          string `json:"terms_url,omitempty"          table:"-"`
	PricingURL        string `json:"pricing_url,omitempty"        table:"-"`

	Stars          *int `json:"stars,omitempty"           table:"stars"`
	DependentCount *int `json:"dependent_count,omitempty" table:"used_by"`

	IsFree          bool `json:"is_free"           table:"-"`
	IsVerifiedOwner bool `json:"is_verified_owner" table:"verified"`
	IsFeatured      bool `json:"is_featured"       table:"-"`
	IsRecommended   bool `json:"is_recommended"    table:"-"`
	ByGitHub        bool `json:"by_github"         table:"-"`
}

Action is a marketplace listing. The type covers both actions and apps, and Type says which.

type Actor

type Actor struct {
	Login      string `json:"login"                 table:"login"`
	Name       string `json:"name,omitempty"        table:"name"`
	Type       string `json:"type,omitempty"        table:"-"`
	NodeID     string `json:"node_id,omitempty"     table:"-"`
	DatabaseID *int   `json:"database_id,omitempty" table:"-"`
	AvatarURL  string `json:"avatar_url,omitempty"  table:"-"`
	URL        string `json:"url,omitempty"         table:"-"`
	URI        string `json:"uri,omitempty"         table:"-"`
}

Actor is a person or an organization as it appears inside another record: on a commit, an issue, a release. It is deliberately small. The full account is a separate read, and inlining it would turn one request into hundreds. Both id forms are kept. The numeric one is what avatar URLs and the older links use, the base64 global one is what Relay results carry, and a joiner downstream will have one or the other and not both.

func (Actor) String

func (a Actor) String() string

String is the login, which is what an author column in a table is for. The renderer would otherwise fall back to JSON for a struct field, and a cell holding the whole actor is wide enough to push every other column off the screen. The full thing is still there in every other format.

type Asset

type Asset struct {
	Name string `json:"name" table:"name"`
	// Label is the text GitHub prints in place of the filename, "GitHub CLI
	// 2.63.2 checksums" for gh_2.63.2_checksums.txt. It is set per asset at
	// upload time and is usually the only human-readable thing in the row.
	Label       string `json:"label,omitempty"        table:"-"`
	Size        *int64 `json:"size,omitempty"         table:"-"`
	SizeDisplay string `json:"size_display,omitempty" table:"size"`
	// Digest is the sha256 the assets fragment publishes, prefixed "sha256:".
	// It is new: GitHub added it around the time it stopped showing download
	// counts to logged-out clients, so this record trades a popularity number
	// for something you can actually verify a download against.
	Digest string `json:"digest,omitempty" table:"-"`
	// DownloadCount is left absent on a keyless read. The release page used to
	// print it next to each asset and no longer does, and no other public
	// surface carries it. The field stays because the shape of the record
	// should not change when GitHub changes its mind again.
	DownloadCount *int       `json:"download_count,omitempty" table:"-"`
	URL           string     `json:"url"                      table:"url,url"`
	UpdatedAt     *time.Time `json:"updated_at,omitempty"     table:"-"`
	ContentType   string     `json:"content_type,omitempty"   table:"-"`
}

Asset is one release download.

type Base

type Base struct {
	Kind    string            `json:"kind"              table:"kind"`
	ID      string            `json:"id"                table:"id" kit:"id"`
	URI     string            `json:"uri,omitempty"     table:"-"`
	URL     string            `json:"url,omitempty"     table:"-,url"`
	Sources []string          `json:"sources,omitempty" table:"-"`
	Via     map[string]string `json:"via,omitempty"     table:"-"`
	Extra   json.RawMessage   `json:"extra,omitempty"   table:"-"`
}

Base is embedded in every record. Kind and ID are the identity, URI and URL are the two addresses, Sources records which pages were read, Via records which extraction tier produced a field, and Extra is the data-loss guard.

Via is deliberately not part of Extra. Extra means "GitHub sent this and nobody modelled it" and the suite asserts it is empty; provenance we wrote ourselves has no business making that assertion fail.

type BlobOptions

type BlobOptions struct {
	Ref string
	// Content fetches the bytes from raw.githubusercontent.com, one extra
	// request. Without it the record is metadata only.
	Content bool
	// Styled fetches the page HTML for rawLines and the syntax highlighting
	// spans that run parallel to them. It is expensive and only a consumer that
	// re-renders the file wants it.
	Styled bool
}

BlobOptions controls a file read.

type Check

type Check struct {
	Name   string `json:"name"   table:"check"`
	Status string `json:"status" table:"status"`
	Detail string `json:"detail" table:"detail"`
}

Check is one diagnostic.

type Client

type Client struct {
	HTTP      *http.Client
	UserAgent string

	Rate    time.Duration // the minimum gap between requests, shared by every worker
	Retries int
	Workers int

	CacheDir string
	NoCache  bool
	CacheTTL time.Duration

	// Deep makes a record read follow the extra requests that fill in fields
	// the primary surface omits. Off by default because it multiplies requests.
	Deep bool

	// Verbose writes one line per request to stderr: the URL, the surface, the
	// status, and the byte count. It is the fastest way to see what a command
	// actually costs.
	Verbose bool
	// contains filtered or unexported fields
}

Client reads github.com. It is safe for concurrent use: the pacer and the cache are synchronised, so a crawl running many workers still produces one polite stream of requests rather than one stream per worker.

There is no Token field. That is not an oversight, it is the design: see the package comment, and see TestNoAuth, which fails the build if a credential ever appears in this package.

func NewClient

func NewClient(cfg Config) *Client

NewClient returns a client configured from cfg, falling back to Defaults for anything unset.

func (*Client) Account

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

Account reads a user or organization profile. The concrete type is decided by the page, so a caller that does not know which it has can just call this.

func (*Client) Activity

func (c *Client) Activity(ctx context.Context, ref string, limit int, emit func(Event) error) error

Activity reads a public event stream. The same feed shape serves an account and a repository, so the argument is either a login or owner/name and the URL is the only thing that differs.

This replaces the REST events endpoint outright. The feed is public, cheap, and needs no credential, and the event class is encoded in each entry's id, so the type comes from a field rather than from matching on prose.

func (*Client) Archive

func (c *Client) Archive(ctx context.Context, repo, ref, format string) (io.ReadCloser, error)

Archive streams a repository tarball or zipball from codeload. The caller closes the reader. Nothing here is cached or buffered: an archive is measured in tens of megabytes and belongs on a disk, not in a map.

format is tar.gz or zip. ref may be a branch, a tag, or a SHA, and the three take different codeload paths, which is what refPath sorts out.

func (*Client) Blob

func (c *Client) Blob(ctx context.Context, repo, path string, opts BlobOptions) (*File, error)

Blob reads one file: its metadata, its rendered view, and its symbols.

func (*Client) Branches

func (c *Client) Branches(ctx context.Context, repo string, opts RefOptions, emit func(GitRef) error) error

Branches lists branches. Without Complete this is the branches route, which carries the last author and the last authored date and is capped by GitHub; with it, this is the git advertisement, which is complete and carries SHAs.

Neither is strictly better and the record says which one answered, so a consumer that finds Author empty knows why.

func (*Client) CommitInfo

func (c *Client) CommitInfo(ctx context.Context, repo, sha string, opts CommitInfoOptions) (*Commit, error)

CommitInfo reads one commit. sha may be a full SHA, an abbreviation, a branch, or a tag: the route resolves all four, and the record reports what it resolved to.

func (*Client) Commits

func (c *Client) Commits(ctx context.Context, repo string, opts CommitOptions, emit func(Commit) error) error

Commits streams a repository's history, newest first.

The route groups commits under calendar-day headings and this flattens them, keeping the heading on each record as DateGroup. That heading is the only place the surface says which timezone it grouped in, so throwing it away would make an off-by-one-day question unanswerable.

func (*Client) CompareRefs

func (c *Client) CompareRefs(ctx context.Context, repo, base, head string, opts CompareOptions) (*Compare, error)

CompareRefs reads the range between two refs.

It reads the plain-text patch mailbox, not the compare page. The page has no JSON payload of any kind, it is more than twice the size, and every field on it is a class name away from breaking. git-format-patch output is a format GitHub does not own and cannot restyle, and it carries every commit with its author, date, subject, and diff.

The trade is that the mailbox has no logins, only names and emails, so the authors on these commits have Name set and Login empty. That is honest: the patch really does not say who the GitHub user was.

func (*Client) Contributions

func (c *Client) Contributions(ctx context.Context, login string, year int, emit func(ContributionDay) error) error

Contributions reads a year of a profile's contribution graph, one record per day. This is the only representation of the numbers that exists without a token: the GraphQL field that carries them refuses anonymous callers.

A year is the largest window the fragment serves. Asking for a wider range gets the last year, so the range is stated rather than inferred.

func (*Client) Contributors

func (c *Client) Contributors(ctx context.Context, repo string, opts ContributorOptions, emit func(Contributor) error) error

Contributors reads the contributor graph's own data route.

The route answers 202 with an empty body while GitHub computes the numbers, which is normal rather than an error and is why this polls. A large repository takes a few seconds the first time and is instant afterwards.

func (*Client) Crawl

func (c *Client) Crawl(ctx context.Context, seed string, o CrawlOptions, sink CrawlSink) error

Crawl walks outward from a seed reference. Nodes, edges, and facts come out as they are discovered, and the walk stops cleanly at either bound and reports what it had rather than failing.

func (*Client) DefaultBranch

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

DefaultBranch asks the git advertisement which branch HEAD points at. It is the authoritative answer, where the route JSON's refInfo is whichever ref the URL happened to resolve to.

func (*Client) Dependencies

func (c *Client) Dependencies(ctx context.Context, repo string, limit int, emit func(Dependency) error) error

Dependencies lists what a repository declares in its manifests.

A repository with the dependency graph switched off answers with a page and no rows, which is an empty list rather than an error: the difference between "nothing to report" and "not enabled" is not on the page, so claiming to know which one it is would be making it up.

func (*Client) Dependents

func (c *Client) Dependents(ctx context.Context, repo string, limit int, emit func(Dependent) error) error

Dependents lists the repositories that depend on this one.

The list is ordered by stars and it is long: a popular library has tens of thousands of rows at thirty a page, so --limit is the flag that matters here and the walk stops the moment it is reached.

func (*Client) Diff

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

Diff returns the unified diff, which is the patch without the commit metadata. On a wide range it is a third of the size.

func (*Client) Discussion

func (c *Client) Discussion(ctx context.Context, repo string, number int) (*Discussion, error)

Discussion reads one discussion.

Discussions never migrated to React, so there is no payload to decode. What there is instead is a schema.org QAPage block, which carries the body, the upvote count, and the accepted answer, and is the most reliable thing on the page because GitHub publishes it for search engines and therefore keeps it working. Everything the block does not have comes from the markup.

repo may be owner/name or an organization login: organization-level discussions live at /orgs/{login}/discussions/{n} and the read follows the redirect either way, then takes the true repository off the sidebar.

func (*Client) Doctor

func (c *Client) Doctor(ctx context.Context, emit func(*Check) error) error

Doctor runs the checks in order and emits one record each. It stops for nothing: a failed reachability check makes the page check fail too, and seeing both is more useful than seeing the first one alone.

func (*Client) Download

func (c *Client) Download(ctx context.Context, repo, ref, path string, w io.Writer) (int64, error)

Download streams one file's bytes to w and reports how many it wrote.

It exists next to Raw because the two have different costs. Raw buffers, so a caller who wants to look at the bytes gets them in one piece and gets the cache; Download does not buffer and does not cache, so a caller piping a hundred-megabyte binary through to a disk pays for none of it.

func (*Client) Estimate

func (c *Client) Estimate(ctx context.Context, seed string, o CrawlOptions) (*CrawlPlan, error)

Estimate reads the seed and reports what one more level would cost. It is deliberately a lower bound and the note says so: the first level is countable because the seed's edges are in hand, and everything past it depends on a branching factor that cannot be seen from here without doing the walk.

func (*Client) Followers

func (c *Client) Followers(ctx context.Context, login string, limit int, emit func(Account) error) error

Followers lists the accounts following a login, newest first, which is the order the page uses and the only order it offers.

func (*Client) Following

func (c *Client) Following(ctx context.Context, login string, limit int, emit func(Account) error) error

Following lists the accounts a login follows.

func (*Client) Forks

func (c *Client) Forks(ctx context.Context, repo string, limit int, emit func(Repo) error) error

Forks lists the public forks of a repository. The page is the only keyless source: the network graph route needs a session and the search index does not model the parent link.

func (*Client) Get

func (c *Client) Get(ctx context.Context, rawURL string, s Surface) (*Response, error)

Get fetches a URL on a surface. Every request in the package goes through here, so pacing, caching, retry, and error classification each have exactly one home.

func (*Client) GetHTML

func (c *Client) GetHTML(ctx context.Context, rawURL string) (*Response, error)

GetHTML fetches a page.

func (*Client) GetJSON

func (c *Client) GetJSON(ctx context.Context, rawURL string, s Surface, v any) (*Response, error)

GetJSON fetches and decodes in one step.

func (*Client) Gist

func (c *Client) Gist(ctx context.Context, id string, withContent bool) (*Gist, error)

Gist reads one gist and its file list. Contents are a second request per file and are opt-in, because a gist can hold a megabyte of log paste.

func (*Client) Gists

func (c *Client) Gists(ctx context.Context, login string, limit int, emit func(Gist) error) error

Gists lists an account's public gists.

func (*Client) GraphOf

func (c *Client) GraphOf(ctx context.Context, kind, id string) (*Graph, error)

GraphOf builds the node, edges, and facts for one entity. `github graph`, `github edges`, and `github rdf` all call it, so the three never disagree about what an entity's edges are.

func (*Client) GraphOfRef

func (c *Client) GraphOfRef(ctx context.Context, ref string) (string, string, *Graph, error)

GraphOfRef is GraphOf for a reference that has not been classified yet, and it reports the kind it turned out to be so a caller can say what it read.

func (*Client) Head

func (c *Client) Head(ctx context.Context, url string) (http.Header, error)

Head issues a HEAD and returns the response headers. It exists for one question, "how big is this file", which is worth asking without downloading the answer.

func (*Client) Issue

func (c *Client) Issue(ctx context.Context, repo string, number int) (*Issue, error)

Issue reads one issue. repo is owner/name.

A pull request number handed to this function does not 404: GitHub redirects /issues/{n} to /pull/{n}, and the read follows that redirect and hands off to PullRequest rather than returning a half-decoded record.

func (*Client) Languages

func (c *Client) Languages(ctx context.Context, repo string, emit func(LanguageShare) error) error

Languages reports the language histogram as one record per language. The numbers are on the repository record already; this exists because "what is this written in, in what proportion" is a question worth one command rather than a field selector on another one. Languages reports the language breakdown, largest first.

This reads the sidebar fragment rather than a whole repository page, because the fragment is where the numbers are and it is 3 KB where the page is 300. The numbers are percentages: GitHub computes byte counts and publishes only the proportions, so a byte count is not something this can report honestly.

func (*Client) Members

func (c *Client) Members(ctx context.Context, login string, limit int, emit func(Account) error) error

Members lists an organization's public members. The roster is at /orgs/{login}/people rather than on the profile, and the profile's avatar strip is a sample of it rather than a short version of it.

func (*Client) Org

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

Org reads an organization. It is an error to point this at a user, because a caller that asked for an organization and silently got a user back will not notice until something downstream is confusing.

func (*Client) Page

func (c *Client) Page(ctx context.Context, rawURL string) (*page.Page, error)

Page fetches a URL and hands back the whole extraction, nothing dropped. Every reader in the package works from this, and `github page` prints it, which is what makes the debugging tool show the same view the readers see rather than a second opinion about the page.

The URL is taken as given rather than resolved from an entity, because the pages worth inspecting most are the ones the model does not cover yet.

func (*Client) Patch

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

Patch returns the git patch for a commit, a pull request, or a range. url is any github.com URL naming one of the three; the .patch suffix is appended.

This is the cheapest complete view of a change on the whole site: no negotiation, no payload, no page, and no token.

func (*Client) Poll

func (c *Client) Poll(ctx context.Context, rawURL string, s Surface) (*Response, error)

Poll waits for a statistic GitHub computes in the background. Only the contributor graph needs it: the first request kicks off the job and answers 202 with an empty body, and the answer arrives some seconds later.

Observed twice in a row several seconds apart on a cold repository, so one retry is not enough. Eight attempts over a sixty second budget, and never a silent empty list: "no contributors" and "not computed yet" are different answers and conflating them would make an empty result look authoritative.

func (*Client) PullCommits

func (c *Client) PullCommits(ctx context.Context, repo string, number int, limit int, emit func(Commit) error) error

PullCommits emits the commits on a pull request. The route serves them all in one response grouped by push, which is why there is no pager here.

func (*Client) PullRequest

func (c *Client) PullRequest(ctx context.Context, repo string, number int) (*PullRequest, error)

PullRequest reads one pull request.

This one takes the HTML page rather than the route JSON, and the reason is worth writing down. Asking a React route for JSON returns the props for that route only, as a delta against the layout that is already mounted in a real browser. /pull/{n} with Accept: application/json is a kilobyte of websocket channel tokens: no title, no author, no merge state. The layout route that holds those lives in the page's embedded payload and nowhere else, so the page is what we fetch. It is about 165 KB on the wire and that is the price.

One fetch then covers everything a logged-out client can see:

pullRequestsLayoutRoute        title, author, refs, head sha, merge metadata
pullRequestsConversationsRoute node id and the lock flag
the sidebar markup             labels
og:description                 the body, truncated to 200 characters

The body really is a snippet. GitHub renders the conversation client-side and serves none of it to a logged-out client, so 200 characters of open graph text is the whole of what is reachable. _via records body: og for it, and the hovercard fragment is the fallback when the page has no open graph body.

func (*Client) Raw

func (c *Client) Raw(ctx context.Context, repo, ref, path string) ([]byte, error)

Raw returns the bytes of a file. One request to raw.githubusercontent.com, no page, no negotiation, and the same contract for a binary as for text.

func (*Client) Refs

func (c *Client) Refs(ctx context.Context, repo string, opts RefOptions, emit func(GitRef) error) error

Refs lists every ref of every kind in one request.

func (*Client) Release

func (c *Client) Release(ctx context.Context, repo, tag string, opts ReleaseOptions) (*Release, error)

Release reads one release by tag. tag may also be "latest", which github.com redirects to whatever that is today.

The per-tag page is not the list page with nine releases removed. It is a different template with a different shape, so it gets its own decoder rather than a selector that limps along on both. What it gains over a list entry is the commit the tag points at; what it lacks is nothing.

func (*Client) Releases

func (c *Client) Releases(ctx context.Context, repo string, opts ReleaseOptions, emit func(Release) error) error

Releases streams a repository's releases, newest first.

The list comes from the HTML pages rather than releases.atom, which is the opposite of what you would expect from a feed-shaped problem. The feed gives ten entries and does not page, so it cannot answer "every release"; the pages give ten at a time with a rel="next" and carry the Latest and Pre-release labels the feed has no room for.

func (*Client) Repo

func (c *Client) Repo(ctx context.Context, id string, opts RepoOptions) (*Repo, error)

Repo reads one repository. id is owner/name.

func (*Client) ReposAsShown

func (c *Client) ReposAsShown(ctx context.Context, login string, limit int, emit func(Repo) error) error

ReposAsShown lists an account's repositories in the order and with the filters the profile tab itself uses. `github repos --user x` runs a search instead, which sorts and pages better; this is what --as-shown selects when the exact page order is the point.

func (*Client) SearchAccounts

func (c *Client) SearchAccounts(ctx context.Context, query string, limit int, emit func(Account) error) error

SearchAccounts streams user records.

func (*Client) SearchCodeBy

func (c *Client) SearchCodeBy(context.Context, string, int, func(File) error) error

SearchCodeBy is the honest gap. The route answers 200 with zero results without a session, which is the worst possible failure mode: it looks like the query matched nothing.

func (*Client) SearchCommitsBy

func (c *Client) SearchCommitsBy(ctx context.Context, query string, limit int, emit func(Commit) error) error

SearchCommitsBy streams commit records. Commit search is the only source for signature and verification state on a keyless surface.

func (*Client) SearchDiscussionsBy

func (c *Client) SearchDiscussionsBy(ctx context.Context, query string, limit int, emit func(Discussion) error) error

SearchDiscussionsBy streams discussion records.

func (*Client) SearchIssuesAndPulls

func (c *Client) SearchIssuesAndPulls(ctx context.Context, query, typ string, limit int, emit func(Thread) error) error

SearchIssuesAndPulls streams thread records. typ is SearchIssues or SearchPulls: the result shape is identical and only the qualifier differs, which is why one decoder serves both.

func (*Client) SearchMarketplace

func (c *Client) SearchMarketplace(ctx context.Context, query string, limit int, emit func(Action) error) error

SearchMarketplace streams action and app listings.

func (*Client) SearchPackagesBy

func (c *Client) SearchPackagesBy(ctx context.Context, query string, limit int, emit func(Package) error) error

SearchPackagesBy streams package records. Search is the only source for packages, so these records are complete rather than thin.

func (*Client) SearchRepositories

func (c *Client) SearchRepositories(ctx context.Context, query string, limit int, emit func(Repo) error) error

SearchRepositories streams repository records for a query.

func (*Client) SearchTopicsBy

func (c *Client) SearchTopicsBy(ctx context.Context, query string, limit int, emit func(Topic) error) error

SearchTopicsBy streams topic records.

func (*Client) SearchWikisBy

func (c *Client) SearchWikisBy(ctx context.Context, query string, limit int, emit func(WikiPage) error) error

SearchWikisBy streams wiki page records.

func (*Client) Starred

func (c *Client) Starred(ctx context.Context, login string, limit int, emit func(Repo) error) error

Starred lists the repositories an account has starred. It is a different template from the repositories tab, so it gets its own row reader even though the two records are the same shape.

func (*Client) Stats

func (c *Client) Stats(ctx context.Context, repo string) (*RepoStats, error)

Stats is the counts in one record. Everything in it is already on the repository record; the point is a record with nothing else in it, so `github stats x -o json` is a thing you can diff week to week.

func (*Client) Stream

func (c *Client) Stream(ctx context.Context, rawURL string) (io.ReadCloser, http.Header, error)

Stream opens a body without buffering, retrying, or caching. Release assets and repository archives go through here: a tarball does not belong in memory and does not belong in the cache. The caller closes the reader.

func (*Client) Tags

func (c *Client) Tags(ctx context.Context, repo string, opts RefOptions, emit func(GitRef) error) error

Tags lists tags. The git advertisement is the default here rather than an opt-in, because the tags feed gives ten and the tags page gives ten at a time, and a repository with four hundred tags is the normal case.

func (*Client) Timeline

func (c *Client) Timeline(ctx context.Context, repo string, number int, limit int, emit func(TimelineItem) error) error

Timeline emits the events the issue page carries.

It is deliberately not a pager. The page preloads the first fifteen events and, on a long thread, the last few; walking past those needs the GraphQL endpoint, which needs a session. So this returns what the page had and Truncated says whether there is more, rather than pretending to a completeness it cannot deliver.

func (*Client) TopicPage

func (c *Client) TopicPage(ctx context.Context, slug string) (*Topic, error)

TopicPage reads one topic. The search result for a topic carries the name and a short blurb; the page carries the long description, the logo, who created the thing, when it was released, the Wikipedia link, and the related topics, which is most of what makes a topic worth having a record for.

func (*Client) Tree

func (c *Client) Tree(ctx context.Context, repo, path string, opts TreeOptions, emit func(TreeEntry) error) error

Tree emits the entries of one directory, or of a whole subtree under Recursive. path is repository-relative and empty means the root.

Entries are emitted as each directory arrives rather than collected, so a recursive walk of a large repository starts printing immediately.

func (*Client) Trending

func (c *Client) Trending(ctx context.Context, opts TrendingOptions, emit func(Trending) error) error

Trending lists the trending repositories. Rank is the position on the page, which is the only ordering the surface has and is worth keeping, since the list has no other stable key.

func (*Client) TrendingDevelopers

func (c *Client) TrendingDevelopers(ctx context.Context, opts TrendingOptions, emit func(Account) error) error

TrendingDevelopers lists the trending developers, each with the repository the page picked out for them.

func (*Client) VerifyCommits

func (c *Client) VerifyCommits(ctx context.Context, repo string, commits []*Commit) error

VerifyCommits fills in signature state, which no commit route carries.

Commit search is the only keyless surface that reports it, so this asks search for the exact SHAs and merges what comes back. It is a separate function rather than a flag because it is a different request against a different index, and a caller should see that in the code they wrote.

type Commit

type Commit struct {
	Base

	Repo   string `json:"repo"              table:"-"`
	SHA    string `json:"sha"               table:"sha"`
	NodeID string `json:"node_id,omitempty" table:"-"`

	Subject          string `json:"subject"                     table:"subject,truncate"`
	SubjectHighlight string `json:"subject_highlight,omitempty" table:"-"`
	Body             string `json:"body,omitempty"              table:"-"`
	BodyHTML         string `json:"body_html,omitempty"         table:"-"`

	Authors   []Actor `json:"authors,omitempty"   table:"authors"`
	Committer *Actor  `json:"committer,omitempty" table:"-"`
	Pusher    *Actor  `json:"pusher,omitempty"    table:"-"`

	AuthoredAt  *time.Time `json:"authored_at,omitempty"  table:"authored,time"`
	CommittedAt *time.Time `json:"committed_at,omitempty" table:"-"`
	PushedAt    *time.Time `json:"pushed_at,omitempty"    table:"-"`

	// DateGroup is the calendar-day heading the commit list grouped this commit
	// under. It is kept because it is the only place the surface tells you what
	// timezone it grouped in.
	DateGroup string `json:"date_group,omitempty" table:"-"`

	Verification string `json:"verification,omitempty" table:"-"`
	// VerificationReason is the why behind Verification: "unsigned",
	// "valid", "expired_key", and so on. Verification alone says a commit is
	// unverified without saying whether that is because nobody signed it or
	// because the signature failed, which are very different facts.
	VerificationReason string `json:"verification_reason,omitempty" table:"-"`
	SignedByGitHub     bool   `json:"signed_by_github"             table:"-"`
	HasSignature       bool   `json:"has_signature"                table:"-"`
	KeyID              string `json:"key_id,omitempty"             table:"-"`
	KeyExpired         bool   `json:"key_expired"                  table:"-"`

	StatusRollup  string `json:"status_rollup,omitempty"  table:"status"`
	StatusSummary string `json:"status_summary,omitempty" table:"-"`
	CommentCount  *int   `json:"comment_count,omitempty"  table:"-"`

	// IssueRefs are the issues and pull requests this commit's message closes
	// or mentions, already resolved by GitHub. This is the commit-to-thread
	// edge of the graph, handed over for free, and it is the reason commit
	// search is worth reading even when you already have the commit.
	IssueRefs []ThreadRef `json:"issue_refs,omitempty" table:"-"`

	Parents   []string     `json:"parents,omitempty"   table:"-"`
	Additions *int         `json:"additions,omitempty" table:"-"`
	Deletions *int         `json:"deletions,omitempty" table:"-"`
	Files     []FileChange `json:"files,omitempty"     table:"-"`
}

Commit is one commit. Authors is a list because co-authored commits are common and the payload already ships an array; Committer is separate and only differs from the author when the surface says it does.

type CommitInfoOptions

type CommitInfoOptions struct {
	// Files fills the per-file change list from the inline diff. The route
	// ships the whole thing whether we decode it or not, so this costs parsing
	// and memory rather than a request.
	Files bool
	// Patch fetches the .patch form as well, one extra request, for a caller
	// that wants to apply the change rather than describe it.
	Patch bool
}

CommitInfoOptions controls how much of a commit's diff comes back.

type CommitOptions

type CommitOptions struct {
	// Ref is a branch, a tag, or a SHA. Empty means the default branch.
	Ref string
	// Path limits history to one file or directory.
	Path string
	// Author is a login, not an email.
	Author string
	// Since and Until are dates, YYYY-MM-DD.
	Since string
	Until string
	// Limit stops the walk. Zero means every commit, which on a large
	// repository is thousands of requests, so callers should set it.
	Limit int
}

CommitOptions controls a commit walk. Every field maps to a query parameter the route already understands, so filtering happens on GitHub's side and not after a full download.

type Compare

type Compare struct {
	Base

	Repo     string `json:"repo"      table:"-"`
	BaseRef  string `json:"base_ref"  table:"base"`
	HeadRef  string `json:"head_ref"  table:"head"`
	PatchURL string `json:"patch_url" table:"-"`
	DiffURL  string `json:"diff_url"  table:"-"`

	Commits []Commit `json:"commits,omitempty" table:"-"`
	// CommitCount is len(Commits) and is here so a table row says something
	// useful without the caller reaching into the slice.
	CommitCount int `json:"commit_count" table:"commits"`

	Files     []FileChange `json:"files,omitempty" table:"-"`
	FileCount int          `json:"file_count"      table:"files"`
	Additions int          `json:"additions"       table:"+"`
	Deletions int          `json:"deletions"       table:"-"`

	// Patch is the raw stream, kept only when the caller asked for it. It is
	// megabytes on a wide range.
	Patch string `json:"patch,omitempty" table:"-"`
}

Compare is a range between two commits: what `github compare` returns.

It is built from the plain-text patch mailbox rather than the compare page. The page is 227 KB of HTML with no JSON payload at all, and the .patch suffix on the same range is a git-format-patch stream that carries every commit's author, date, subject, and diff with no markup to guess at. Parsing a format GitHub cannot restyle is the whole point.

type CompareOptions

type CompareOptions struct {
	// Files parses the per-file changes out of the patch. Free, since the
	// patch is already downloaded.
	Files bool
	// Patch keeps the raw stream on the record.
	Patch bool
}

CompareOptions controls a range read.

type Config

type Config struct {
	UserAgent string
	Rate      time.Duration
	Retries   int
	Workers   int
	Timeout   time.Duration
	CacheDir  string
	NoCache   bool
	CacheTTL  time.Duration
	Deep      bool
}

Config is the resolved per-run configuration. It carries no credential field because there is no credential.

type ContributionDay

type ContributionDay struct {
	Base

	Login string    `json:"login" table:"login"`
	Date  time.Time `json:"date"  table:"date,time"`
	Count int       `json:"count" table:"count"`
	Level int       `json:"level" table:"level"`
}

ContributionDay is one square of a profile contribution graph.

type Contributor

type Contributor struct {
	Base

	Repo  string `json:"repo"  table:"repo"`
	Login string `json:"login" table:"login"`

	Commits   *int `json:"commits,omitempty"   table:"commits"`
	Additions *int `json:"additions,omitempty" table:"+"`
	// Hidden by the tag-grammar collision, as everywhere else.
	Deletions *int `json:"deletions,omitempty" table:"-"`

	// FirstWeek and LastWeek are derived by trimming the leading and trailing
	// zero weeks, which turns a six-hundred-element array into two dates a
	// table can show.
	FirstWeek *time.Time `json:"first_week,omitempty" table:"first,time"`
	LastWeek  *time.Time `json:"last_week,omitempty"  table:"last,time"`

	Weeks []ContributorWeek `json:"weeks,omitempty" table:"-"`

	AvatarURL  string `json:"avatar_url,omitempty"  table:"-"`
	DatabaseID *int   `json:"database_id,omitempty" table:"-"`
}

Contributor is one person's contribution statistics for a repository. Weeks arrives with the response but is dropped unless asked for, because the route sends every week since the repository began for every contributor and that is megabytes of mostly zeroes. It is never a table column either way, because a hundred weeks is not a column.

type ContributorOptions

type ContributorOptions struct {
	// Weeks keeps the per-week breakdown. It is off by default because the
	// route sends every week since the repository began for every contributor,
	// which on an old project is a few hundred entries each and megabytes of
	// mostly zeroes for an answer whose question was "who wrote this".
	Weeks bool
	Limit int
}

ContributorOptions is what to do with the week array.

type ContributorWeek

type ContributorWeek struct {
	Week      time.Time `json:"week"      table:"week,time"`
	Additions int       `json:"additions" table:"+"`
	Deletions int       `json:"deletions" table:"-"`
	Commits   int       `json:"commits"   table:"commits"`
}

ContributorWeek is one week of one contributor's statistics.

type CrawlOptions

type CrawlOptions struct {
	Depth    int
	Follow   []string
	Kinds    []string
	MinTrust string
	Limit    int

	NodesOnly bool
	EdgesOnly bool
}

CrawlOptions bounds a walk. The budgets are the point of the struct. A tool that can accidentally send a million requests at somebody else's servers should be hard to point that way by accident.

type CrawlPlan

type CrawlPlan struct {
	Base

	Seed  string `json:"seed"  table:"seed"`
	Depth int    `json:"depth" table:"depth"`
	Nodes int    `json:"nodes" table:"nodes"`

	Note string `json:"note" table:"note"`
}

CrawlPlan is what --dry-run answers with. It is a record rather than a line on stderr so the answer goes through the same renderer, formats, and pipes as every other command, and so a script can size a walk without reading prose.

type CrawlSink

type CrawlSink struct {
	Node func(*Node) error
	Edge func(*Edge) error
	Fact func(*Fact) error
}

CrawlSink receives what the walk finds. Emission is streaming: a crawl of a large organization must never need the whole graph in memory, and a crawl that is interrupted has already emitted everything it found.

type Dependency

type Dependency struct {
	Base

	Repo    string `json:"repo"    table:"repo"`
	Package string `json:"package" table:"package"`

	SourceRepo   string `json:"source_repo,omitempty"  table:"source"`
	Version      string `json:"version,omitempty"      table:"version"`
	Relationship string `json:"relationship,omitempty" table:"rel"`
	Ecosystem    string `json:"ecosystem,omitempty"    table:"ecosystem"`
	Manifest     string `json:"manifest,omitempty"     table:"manifest"`
	License      string `json:"license,omitempty"      table:"-"`
}

Dependency is one row of /network/dependencies: a package this repository declares in one of its manifests.

The identity is the repository the package resolves to, because that is the only thing on the row with an address on github.com. A package GitHub cannot resolve to a repository has no Kind and no ID, and its name is still on the record, because a dependency list with the unresolvable rows silently dropped is a lie about what the manifest contains.

type Dependent

type Dependent struct {
	Base

	Repo      string `json:"repo"      table:"repo"`
	Dependent string `json:"dependent" table:"dependent"`
	Owner     string `json:"owner"     table:"-"`

	Stars *int `json:"stars,omitempty" table:"stars"`
	Forks *int `json:"forks,omitempty" table:"forks"`

	AvatarURL string `json:"avatar_url,omitempty" table:"-"`
}

Dependent is one row of /network/dependents: a repository that depends on this one. The two counts are on the row, so a caller sorting the dependents of a popular library by stars does not need a fetch per row.

type Discussion

type Discussion struct {
	Thread

	Category       string     `json:"category,omitempty"         table:"category"`
	IsAnswered     bool       `json:"is_answered"                table:"answered"`
	AnswerChosenAt *time.Time `json:"answer_chosen_at,omitempty" table:"-"`
	AnswerAuthor   *Actor     `json:"answer_author,omitempty"    table:"-"`
	Upvotes        *int       `json:"upvotes,omitempty"          table:"upvotes"`
}

Discussion adds the answer, which is the thing discussions have that issues do not.

type Domain

type Domain struct{}

Domain is the kit driver for github.com. A blank import of this package enables it in any multi-domain host, the way a database driver registers itself, and the same Domain builds the single github binary.

func (Domain) Classify

func (Domain) Classify(input string) (uriType, id string, err error)

Classify satisfies kit.Resolver, so a URI typed at a multi-domain host and one typed at github are read by the same parser.

func (Domain) Info

func (Domain) Info() kit.DomainInfo

Info names the domain and every hostname that means it. The extra hosts are not decoration: a pasted raw.githubusercontent.com or gist.github.com link is a github address and has to resolve here rather than fall through as an unknown site.

func (Domain) Locate

func (Domain) Locate(uriType, id string) (string, error)

Locate satisfies kit.Resolver: the https location of one resource.

func (Domain) Register

func (d Domain) Register(app *kit.App)

Register installs the client factory, the domain globals, and every operation. It does no I/O and is deterministic, so a host can call it at startup.

type Edge

type Edge struct {
	Subject   string     `json:"subject"          table:"subject"`
	Predicate string     `json:"predicate"        table:"predicate"`
	Object    string     `json:"object"           table:"object"`
	Source    string     `json:"source"           table:"source"`
	Weight    *int       `json:"weight,omitempty" table:"weight"`
	At        *time.Time `json:"at,omitempty"     table:"-"`
}

Edge is one directed, typed relation between two entities.

There is no inverse flag. Every predicate has exactly one direction, and where the inverse is what you want, the edge is emitted with the other node as its subject rather than with a flag saying to read it backwards.

func FilterTrust

func FilterTrust(edges []Edge, min string) []Edge

FilterTrust drops the edges below a floor, in place.

type Event

type Event struct {
	Base

	Actor Actor  `json:"actor"          table:"actor"`
	Type  string `json:"type"           table:"type"`
	Repo  string `json:"repo,omitempty" table:"repo"`

	Title    string     `json:"title,omitempty"     table:"title,truncate"`
	BodyHTML string     `json:"body_html,omitempty" table:"-"`
	Target   string     `json:"target,omitempty"    table:"-"`
	At       *time.Time `json:"at,omitempty"        table:"at,time"`
}

Event is one entry of an activity feed. Type is derived from the entry id, which encodes the event class, rather than from the title text, which is prose and is localised.

type Fact

type Fact struct {
	Subject   string `json:"subject"            table:"subject"`
	Predicate string `json:"predicate"          table:"predicate"`
	Value     string `json:"value"              table:"value,truncate"`
	Datatype  string `json:"datatype,omitempty" table:"-"`
}

Fact is a literal statement about a node: a star count, a description, a timestamp.

It is a separate type from Edge on purpose. Edge.Object is a URI and every consumer of the graph is entitled to treat it as one, so putting "12000" in that field to carry a star count would break each of them for the sake of saving a struct. RDF emits both; `github edges` emits only edges, which is why its output reads as relations rather than as a flattened record.

type File

type File struct {
	Base

	Repo string `json:"repo" table:"-"`
	Ref  string `json:"ref"  table:"-"`
	Path string `json:"path" table:"path"`

	Size *int64 `json:"size,omitempty" table:"size"`
	// SizeDisplay is what the page shows, "13.3 KB". The page has no byte
	// count anywhere, so an exact Size costs a request to raw and is filled
	// only when the bytes were fetched anyway.
	SizeDisplay string `json:"size_display,omitempty" table:"-"`
	Lines       *int   `json:"lines,omitempty"        table:"lines"`
	Language    string `json:"language,omitempty"     table:"language"`
	IsBinary    bool   `json:"is_binary"              table:"-"`
	IsLFS       bool   `json:"is_lfs"                 table:"-"`
	IsGenerated bool   `json:"is_generated"           table:"-"`
	IsTruncated bool   `json:"is_truncated"           table:"-"`

	RawURL string `json:"raw_url" table:"-"`

	Content  string   `json:"content,omitempty"   table:"-"`
	RawLines []string `json:"raw_lines,omitempty" table:"-"`
	RichText string   `json:"rich_text,omitempty" table:"-"`

	TOC     []Heading `json:"toc,omitempty"     table:"-"`
	Symbols []Symbol  `json:"symbols,omitempty" table:"-"`
	// SymbolsStatus is ok, timed_out, not_analyzed, or unavailable. An empty
	// symbol list with not_analyzed means the language is unsupported, which is
	// a different fact from a file that genuinely has no symbols, and
	// unavailable means GitHub's analyser had not finished when we asked. The
	// caller should not have to guess which one it got.
	SymbolsStatus string `json:"symbols_status,omitempty" table:"-"`
}

File is a blob. The interesting part is Symbols: GitHub runs a symbol extractor over every blob it renders and ships the result in the route payload, and there is no unauthenticated REST equivalent anywhere.

type FileChange

type FileChange struct {
	Path      string `json:"path"                table:"path"`
	PrevPath  string `json:"prev_path,omitempty" table:"-"`
	Status    string `json:"status"              table:"status"`
	Additions *int   `json:"additions,omitempty" table:"+"`
	// Hidden by the same tag-grammar collision as PullRequest.Deletions.
	Deletions *int `json:"deletions,omitempty" table:"-"`
	IsBinary  bool `json:"is_binary"           table:"-"`
}

FileChange is one file in a commit or a diff.

type Gist

type Gist struct {
	Base

	Owner       string `json:"owner,omitempty"       table:"owner"`
	Description string `json:"description,omitempty" table:"description,truncate"`

	IsPublic  bool `json:"is_public"            table:"public"`
	FileCount *int `json:"file_count,omitempty" table:"files"`
	Forks     *int `json:"forks,omitempty"      table:"forks"`
	Stars     *int `json:"stars,omitempty"      table:"stars"`
	Revisions *int `json:"revisions,omitempty"  table:"-"`

	Files []GistFile `json:"files,omitempty" table:"-"`

	CreatedAt *time.Time `json:"created_at,omitempty" table:"created,time"`
	UpdatedAt *time.Time `json:"updated_at,omitempty" table:"-"`
}

Gist is a gist and its files.

type GistFile

type GistFile struct {
	Name     string `json:"name"               table:"name"`
	Language string `json:"language,omitempty" table:"language"`
	Size     *int64 `json:"size,omitempty"     table:"size"`
	RawURL   string `json:"raw_url"            table:"-"`
	Content  string `json:"content,omitempty"  table:"-"`
}

GistFile is one file in a gist.

type GitRef

type GitRef struct {
	Base

	Repo string `json:"repo"          table:"-"`
	Name string `json:"name"          table:"name"`
	Type string `json:"type"          table:"type"`
	SHA  string `json:"sha,omitempty" table:"sha"`

	// PeeledSHA is set for annotated tags, from the ^{} entry in the git
	// protocol advertisement.
	PeeledSHA string `json:"peeled_sha,omitempty" table:"-"`
	IsDefault bool   `json:"is_default"           table:"default"`
	Protected bool   `json:"protected"            table:"protected"`

	Author     *Actor     `json:"author,omitempty"      table:"author"`
	AuthoredAt *time.Time `json:"authored_at,omitempty" table:"authored,time"`
}

GitRef is a branch or a tag. It is not called Ref because Ident already owns the word "reference" in this package, and a git ref and a parsed URI are very different things to confuse in a stack trace.

Three surfaces carry refs and each is incomplete differently: the branches page has authors and dates but a truncated list, the refs XHR has every name and nothing else, and the git protocol has every name with its SHA. The commands pick per question, which is why `github refs --names-only` is 6 KB where `github refs` is 588 KB.

type Graph

type Graph struct {
	Nodes []Node `json:"nodes"`
	Edges []Edge `json:"edges"`
	Facts []Fact `json:"facts,omitempty"`
}

Graph is a set of nodes, edges, and facts held in memory. The streaming commands never build one; `github graph` for a single entity, `github rdf` for the buffered serialisations, and the tests all want the whole thing in hand.

func (*Graph) Add

func (g *Graph) Add(rec any)

Add folds a record into the graph, skipping a node already present so that a repeat visit does not duplicate it.

func (*Graph) AddNode

func (g *Graph) AddNode(n Node)

AddNode adds one node if its URI is new.

func (*Graph) Targets

func (g *Graph) Targets(allow map[string]bool) []string

Targets returns the object URIs reachable under an allowed predicate set, which is what the crawler walks. A bare-string object is never a target: there is no page for a language.

type Heading

type Heading struct {
	Level  int    `json:"level"  table:"level"`
	Text   string `json:"text"   table:"text"`
	Anchor string `json:"anchor" table:"anchor"`
}

Heading is one entry of a rendered markdown table of contents.

type Ident

type Ident struct {
	Kind   string `json:"kind"             table:"kind"`
	ID     string `json:"id"               table:"id"`
	Anchor string `json:"anchor,omitempty" table:"anchor"`
	URI    string `json:"uri"              table:"uri"`
	URL    string `json:"url"              table:"url,url"`
}

Ident is a parsed reference: what kind of thing, its canonical id, and the fragment the URL carried. The fragment never changes the kind. A link to #issuecomment-66046293 is still a link to the issue, and a link to #L10-L20 is still a link to the file, so the anchor is recorded and set aside.

func Parse

func Parse(input string) (Ident, error)

Parse is Classify with the fragment and the derived forms kept.

type Issue

type Issue struct {
	Thread

	IssueType     string   `json:"issue_type,omitempty"      table:"-"`
	SubIssueTotal *int     `json:"sub_issue_total,omitempty" table:"-"`
	SubIssueDone  *int     `json:"sub_issue_done,omitempty"  table:"-"`
	DuplicateOf   string   `json:"duplicate_of,omitempty"    table:"-"`
	LinkedPRs     []string `json:"linked_prs,omitempty"      table:"-"`
	ClosedByPRs   []string `json:"closed_by_prs,omitempty"   table:"-"`
	ProjectItems  []string `json:"project_items,omitempty"   table:"-"`
}

Issue adds the tracking relationships GitHub keeps between issues and the work that closes them.

type Label

type Label struct {
	Name        string `json:"name"                  table:"name"`
	Color       string `json:"color,omitempty"       table:"color"`
	Description string `json:"description,omitempty" table:"description,truncate"`
	URL         string `json:"url,omitempty"         table:"url,url"`
	NodeID      string `json:"node_id,omitempty"     table:"-"`
}

Label is a thread label.

type LanguageShare

type LanguageShare struct {
	Base

	Repo     string  `json:"repo"     table:"repo"`
	Language string  `json:"language" table:"language"`
	Percent  float64 `json:"percent"  table:"percent"`
	Color    string  `json:"color,omitempty" table:"-"`
}

LanguageShare is one language of one repository. The repository record carries the same numbers as a map, which is the right shape to keep and the wrong shape to print, so this is the row form of it.

type Milestone

type Milestone struct {
	Title    string     `json:"title"               table:"title"`
	Number   *int       `json:"number,omitempty"    table:"number"`
	Closed   bool       `json:"closed"              table:"closed"`
	DueOn    *time.Time `json:"due_on,omitempty"    table:"due,time"`
	ClosedAt *time.Time `json:"closed_at,omitempty" table:"-"`
	Progress *float64   `json:"progress,omitempty"  table:"progress"`
	URL      string     `json:"url,omitempty"       table:"url,url"`
}

Milestone is a thread milestone.

type Node

type Node struct {
	URI   string `json:"uri"             table:"uri"`
	Kind  string `json:"kind"            table:"kind"`
	ID    string `json:"id"              table:"id"`
	Label string `json:"label,omitempty" table:"label,truncate"`
	URL   string `json:"url,omitempty"   table:"url,url"`
}

Node is one entity. It is deliberately thin: the label and the two addresses and nothing else, because the full record is one `github get` away by URI and duplicating it here would make a crawl of ten thousand nodes unprintable.

type Org

type Org struct {
	Account

	VerifiedDomains []string `json:"verified_domains,omitempty" table:"-"`
	MemberCount     *int     `json:"member_count,omitempty"     table:"members"`
	// Members is the avatar strip on the front page, which is a sample and not
	// the roster. It never sets MemberCount for that reason: `github members`
	// walks /orgs/{login}/people for the real list.
	Members      []string       `json:"members,omitempty"          table:"-"`
	TopLanguages map[string]int `json:"top_languages,omitempty"    table:"-"`
	TopTopics    []string       `json:"top_topics,omitempty"       table:"-"`
	IsEnterprise bool           `json:"is_enterprise"              table:"-"`
}

Org is an Account plus the five things only an organization page has. TopLanguages and TopTopics are deferred fragments and arrive only with --deep.

type Package

type Package struct {
	Base

	Repo string `json:"repo,omitempty" table:"repo"`
	Name string `json:"name"           table:"name"`
	Type string `json:"type"           table:"type"`

	Summary   string   `json:"summary,omitempty"   table:"summary,truncate"`
	Downloads *int     `json:"downloads,omitempty" table:"downloads"`
	Topics    []string `json:"topics,omitempty"    table:"-"`
	Source    string   `json:"source,omitempty"    table:"-"`

	UpdatedAt *time.Time `json:"updated_at,omitempty" table:"updated,time"`
}

Package is a published package. Search is the only source, which means the record is complete the moment it is read.

type PullRequest

type PullRequest struct {
	Thread

	BaseRef string `json:"base_ref,omitempty" table:"base"`
	HeadRef string `json:"head_ref,omitempty" table:"head"`
	BaseOID string `json:"base_oid,omitempty" table:"-"`
	HeadOID string `json:"head_oid,omitempty" table:"-"`

	Merged    bool       `json:"merged"              table:"-"`
	MergedAt  *time.Time `json:"merged_at,omitempty" table:"merged,time"`
	MergedBy  *Actor     `json:"merged_by,omitempty" table:"-"`
	Mergeable string     `json:"mergeable,omitempty" table:"-"`
	IsDraft   bool       `json:"is_draft"            table:"-"`

	Additions *int `json:"additions,omitempty" table:"+"`
	// The render tag grammar uses "-" to mean "skip this column", and "-" is
	// also the natural header for deletions. The grammar wins: deletions is
	// hidden by default and shown with --fields deletions. This is deliberate,
	// please do not "fix" it.
	Deletions    *int `json:"deletions,omitempty"     table:"-"`
	ChangedFiles *int `json:"changed_files,omitempty" table:"files"`
	CommitCount  *int `json:"commit_count,omitempty"  table:"-"`

	ReviewDecision string   `json:"review_decision,omitempty" table:"review"`
	ReviewRequests []Actor  `json:"review_requests,omitempty" table:"-"`
	ClosesIssues   []string `json:"closes_issues,omitempty"   table:"-"`
}

PullRequest adds the diff and the merge state.

type RDFOptions

type RDFOptions struct {
	Format string
	// Graph is the fourth position for N-Quads. Putting the source URL there
	// means the provenance survives into the RDF and a quad store can answer
	// which page told us this.
	Graph string
}

RDFOptions controls a serialisation.

type RDFWriter

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

RDFWriter is the streaming form. A crawl hands it nodes, edges, and facts as it finds them and it writes lines, so `github export --depth 3 --format nt` over a large organization never holds the graph in memory. The buffered formats are handled by collecting into a Graph and calling WriteRDF, and this type reports which is which through Streams.

func NewRDFWriter

func NewRDFWriter(w io.Writer, o RDFOptions) *RDFWriter

NewRDFWriter returns a streaming writer for nt or nq, and nil for the formats that cannot stream.

func (*RDFWriter) Edge

func (r *RDFWriter) Edge(e *Edge) error

func (*RDFWriter) Fact

func (r *RDFWriter) Fact(f *Fact) error

func (*RDFWriter) Node

func (r *RDFWriter) Node(n *Node) error

type Reaction

type Reaction struct {
	Content string `json:"content" table:"content"`
	Count   int    `json:"count"   table:"count"`
}

Reaction is one emoji group. Content is the GraphQL enum: THUMBS_UP, THUMBS_DOWN, LAUGH, HOORAY, CONFUSED, HEART, ROCKET, EYES. All eight always arrive, most with a zero count, and the decoder drops the zeroes so an unreacted thread has an empty list rather than eight noisy nothings.

type RefOptions

type RefOptions struct {
	// Complete reads the git advertisement instead of the branches page: every
	// ref in one request, with SHAs, and no cap. It costs the author and date
	// the page carries, because the protocol does not have them.
	Complete bool
	// Pulls includes refs/pull/*, which github.com advertises for every pull
	// request ever opened. On a busy repository that is most of the response.
	Pulls bool
	Limit int
}

RefOptions controls a ref listing.

type Release

type Release struct {
	Base

	Repo string `json:"repo" table:"-"`
	Tag  string `json:"tag"  table:"tag"`

	Title    string `json:"title,omitempty"     table:"title,truncate"`
	Body     string `json:"body,omitempty"      table:"-"`
	BodyHTML string `json:"body_html,omitempty" table:"-"`

	Author *Actor `json:"author,omitempty" table:"author"`

	PublishedAt *time.Time `json:"published_at,omitempty" table:"published,time"`
	UpdatedAt   *time.Time `json:"updated_at,omitempty"   table:"-"`

	IsPrerelease bool `json:"is_prerelease" table:"pre"`
	IsLatest     bool `json:"is_latest"     table:"latest"`
	IsDraft      bool `json:"is_draft"      table:"-"`

	CommitSHA string `json:"commit_sha,omitempty" table:"-"`

	Assets     []Asset `json:"assets,omitempty"      table:"-"`
	TarballURL string  `json:"tarball_url,omitempty" table:"-"`
	ZipballURL string  `json:"zipball_url,omitempty" table:"-"`

	// RepoDatabaseID comes free from the Atom <id>, which is
	// tag:github.com,2008:Repository/11180687/v0.164.0. That is how a release
	// read from a feed joins to a repository record without a second fetch.
	RepoDatabaseID *int `json:"repo_database_id,omitempty" table:"-"`
}

Release is one published release. Assets and download counts exist only on the per-release HTML page, so the feed-driven listing leaves Assets nil and `--assets` opts into one request per release.

type ReleaseOptions

type ReleaseOptions struct {
	// Assets fetches the lazy asset fragment for each release, one extra
	// request each. Without it a release record has no downloads.
	Assets bool
	// Body keeps the rendered release notes, which are most of the bytes on a
	// project that writes a changelog.
	Body  bool
	Limit int
}

ReleaseOptions controls a release listing.

type Repo

type Repo struct {
	Base

	Owner string `json:"owner" table:"owner"`
	Name  string `json:"name"  table:"name"`

	Description          string   `json:"description,omitempty"           table:"description,truncate"`
	DescriptionHighlight string   `json:"description_highlight,omitempty" table:"-"`
	Homepage             string   `json:"homepage,omitempty"              table:"-"`
	Topics               []string `json:"topics,omitempty"                table:"topics"`

	DatabaseID *int   `json:"database_id,omitempty" table:"-"`
	NodeID     string `json:"node_id,omitempty"     table:"-"`

	DefaultBranch string `json:"default_branch,omitempty" table:"branch"`
	HeadSHA       string `json:"head_sha,omitempty"       table:"-"`

	Language      string           `json:"language,omitempty"       table:"language"`
	LanguageColor string           `json:"language_color,omitempty" table:"-"`
	Languages     map[string]int64 `json:"languages,omitempty"      table:"-"`

	Stars        *int   `json:"stars,omitempty"         table:"stars"`
	StarsDisplay string `json:"stars_display,omitempty" table:"-"`
	Forks        *int   `json:"forks,omitempty"         table:"forks"`
	Watchers     *int   `json:"watchers,omitempty"      table:"watchers"`

	OpenIssues       *int `json:"open_issues,omitempty"        table:"issues"`
	GoodFirstIssues  *int `json:"good_first_issues,omitempty"  table:"-"`
	HelpWantedIssues *int `json:"help_wanted_issues,omitempty" table:"-"`

	CommitCount        *int   `json:"commit_count,omitempty"         table:"-"`
	CommitCountDisplay string `json:"commit_count_display,omitempty" table:"-"`
	ReleaseCount       *int   `json:"release_count,omitempty"        table:"-"`
	TagCount           *int   `json:"tag_count,omitempty"            table:"-"`
	FileCount          *int   `json:"file_count,omitempty"           table:"-"`
	DependentCount     *int   `json:"dependent_count,omitempty"      table:"-"`
	ContributorCount   *int   `json:"contributor_count,omitempty"    table:"-"`

	// License comes from one sidebar anchor and from nowhere else on any
	// keyless surface. See page.LicenseLink.
	License string `json:"license,omitempty" table:"license"`

	IsFork     bool   `json:"is_fork"              table:"-"`
	ForkOf     string `json:"fork_of,omitempty"    table:"-"`
	IsArchived bool   `json:"is_archived"          table:"-"`
	IsMirror   bool   `json:"is_mirror"            table:"-"`
	IsTemplate bool   `json:"is_template"          table:"-"`
	IsEmpty    bool   `json:"is_empty"             table:"-"`
	IsPrivate  bool   `json:"is_private"           table:"-"`
	IsOrgOwned bool   `json:"is_org_owned"         table:"-"`
	Visibility string `json:"visibility,omitempty" table:"-"`

	Sponsorable    bool `json:"sponsorable"     table:"-"`
	HasFunding     bool `json:"has_funding"     table:"-"`
	HasCitation    bool `json:"has_citation"    table:"-"`
	HasDiscussions bool `json:"has_discussions" table:"-"`
	HasWiki        bool `json:"has_wiki"        table:"-"`
	HasPages       bool `json:"has_pages"       table:"-"`

	CreatedAt *time.Time `json:"created_at,omitempty" table:"created,time"`
	PushedAt  *time.Time `json:"pushed_at,omitempty"  table:"pushed,time"`
	UpdatedAt *time.Time `json:"updated_at,omitempty" table:"updated,time"`

	OwnerAvatarURL string `json:"owner_avatar_url,omitempty" table:"-"`
	SocialImageURL string `json:"social_image_url,omitempty" table:"-"`

	ReadmePath string `json:"readme_path,omitempty" table:"-"`
	ReadmeHTML string `json:"readme_html,omitempty" table:"-"`
	ReadmeText string `json:"readme_text,omitempty" table:"-"`

	Tree []TreeEntry `json:"tree,omitempty" table:"-"`

	StargazersPath  string `json:"stargazers_path,omitempty"   table:"-"`
	ForkNetworkPath string `json:"fork_network_path,omitempty" table:"-"`
	ActivityPath    string `json:"activity_path,omitempty"     table:"-"`
}

Repo is the centre of the model. A read from a page fills most of it, a read from a search result fills a thinner but honest subset, and Sources says which happened.

type RepoOptions

type RepoOptions struct {
	// Deep runs the fragment fetches: the language histogram and the dependent
	// count. Four to six requests instead of one.
	Deep bool
	// Readme keeps the rendered README, which is most of the response body on a
	// well-documented repository.
	Readme bool
}

RepoOptions controls how much a repository read costs.

type RepoStats

type RepoStats struct {
	Base

	Repo string `json:"repo" table:"repo"`

	Stars        *int `json:"stars,omitempty"        table:"stars"`
	Forks        *int `json:"forks,omitempty"        table:"forks"`
	Watchers     *int `json:"watchers,omitempty"     table:"watching"`
	OpenIssues   *int `json:"open_issues,omitempty"  table:"issues"`
	Commits      *int `json:"commits,omitempty"      table:"commits"`
	Releases     *int `json:"releases,omitempty"     table:"releases"`
	Tags         *int `json:"tags,omitempty"         table:"tags"`
	Contributors *int `json:"contributors,omitempty" table:"people"`
	Dependents   *int `json:"dependents,omitempty"   table:"used_by"`

	PushedAt *time.Time `json:"pushed_at,omitempty" table:"pushed,time"`
}

RepoStats is the counts and nothing else.

Every field is already on Repo. The reason to have it separately is that a record with eight numbers in it is something you can store once a day and diff; a record with a readme in it is not.

type Response

type Response struct {
	Body     []byte
	Status   int
	Header   http.Header
	URL      string
	FinalURL string
	Surface  Surface
}

Response is one completed exchange. FinalURL differs from URL when GitHub redirected, which is how a renamed repository is detected without a second request.

type RouteInfo

type RouteInfo struct {
	Route    string `json:"route"              table:"route"`
	Primary  string `json:"primary"            table:"primary"`
	Fallback string `json:"fallback,omitempty" table:"fallback"`
	Note     string `json:"note,omitempty"     table:"note,truncate"`
}

RouteInfo is one row: what the route is, which surface answers it best, and what to fall back to when that surface declines.

type Surface

type Surface int

Surface names one of the eight ways this tool reads github.com. It decides the request headers and how a non-200 is read, which is why it travels with every request instead of being guessed from the URL.

const (
	// SurfaceHTML is a plain page fetch. Every route serves one.
	SurfaceHTML Surface = iota
	// SurfaceRouteJSON asks a React route for its props with Accept: json.
	SurfaceRouteJSON
	// SurfaceXHR sets X-Requested-With, which unlocks the fragments the front
	// end fetches for itself: refs, contributor statistics, hovercards.
	SurfaceXHR
	// SurfaceSearch is /search?q=&type=, which answers JSON for the asking.
	SurfaceSearch
	// SurfaceFeed is an .atom endpoint.
	SurfaceFeed
	// SurfaceRaw is raw.githubusercontent.com and codeload: bytes, no page.
	SurfaceRaw
	// SurfaceGit is the git smart protocol at /{owner}/{repo}.git/info/refs.
	SurfaceGit
)

func (Surface) String

func (s Surface) String() string

type Symbol

type Symbol struct {
	Name               string `json:"name"          table:"name"`
	Kind               string `json:"kind"          table:"kind"`
	FullyQualifiedName string `json:"fqn,omitempty" table:"-"`
	IdentStart         int    `json:"ident_start"   table:"-"`
	IdentEnd           int    `json:"ident_end"     table:"-"`
	ExtentStart        int    `json:"extent_start"  table:"-"`
	ExtentEnd          int    `json:"extent_end"    table:"-"`
}

Symbol is one extracted definition, with byte offsets into the blob.

type Thread

type Thread struct {
	Base

	Repo   string `json:"repo"   table:"repo"`
	Number int    `json:"number" table:"number"`

	Title          string `json:"title"                     table:"title,truncate"`
	TitleHighlight string `json:"title_highlight,omitempty" table:"-"`
	TitleHTML      string `json:"title_html,omitempty"      table:"-"`

	State       string `json:"state"                  table:"state"`
	StateReason string `json:"state_reason,omitempty" table:"-"`

	Body     string `json:"body,omitempty"      table:"-"`
	BodyHTML string `json:"body_html,omitempty" table:"-"`

	Author Actor `json:"author" table:"author"`

	Labels    []Label    `json:"labels,omitempty"    table:"labels"`
	Milestone *Milestone `json:"milestone,omitempty" table:"-"`
	Assignees []Actor    `json:"assignees,omitempty" table:"-"`
	Reactions []Reaction `json:"reactions,omitempty" table:"-"`

	CommentCount *int `json:"comment_count,omitempty" table:"comments"`

	Locked   bool `json:"locked"    table:"-"`
	IsPinned bool `json:"is_pinned" table:"-"`

	CreatedAt *time.Time `json:"created_at,omitempty" table:"created,time"`
	UpdatedAt *time.Time `json:"updated_at,omitempty" table:"updated,time"`
	ClosedAt  *time.Time `json:"closed_at,omitempty"  table:"-"`

	AuthorAssociation string `json:"author_association,omitempty" table:"-"`

	NodeID     string `json:"node_id,omitempty"     table:"-"`
	DatabaseID *int   `json:"database_id,omitempty" table:"-"`
}

Thread is what issues, pull requests, and discussions have in common, which is most of it: their pages share a Relay payload shape.

type ThreadRef

type ThreadRef struct {
	DatabaseID    *int   `json:"database_id,omitempty" table:"id"`
	Title         string `json:"title,omitempty"       table:"title,truncate"`
	State         string `json:"state,omitempty"       table:"state"`
	IsPullRequest bool   `json:"is_pull_request"       table:"-"`
	Merged        bool   `json:"merged"                table:"-"`
	URL           string `json:"url,omitempty"         table:"url"`
}

ThreadRef is a pointer to an issue or a pull request from somewhere else. It is not a Thread: it carries only what the referring surface knew, and the caller resolves it with `github get` when it wants the rest.

type TimelineItem

type TimelineItem struct {
	Base

	Thread string `json:"thread"           table:"-"`
	Type   string `json:"type"             table:"type"`
	Cursor string `json:"cursor,omitempty" table:"-"`

	Actor     *Actor     `json:"actor,omitempty"      table:"actor"`
	CreatedAt *time.Time `json:"created_at,omitempty" table:"created,time"`

	Body     string `json:"body,omitempty"      table:"body,truncate"`
	BodyHTML string `json:"body_html,omitempty" table:"-"`

	Label     *Label     `json:"label,omitempty"      table:"-"`
	Milestone *Milestone `json:"milestone,omitempty"  table:"-"`
	Assignee  *Actor     `json:"assignee,omitempty"   table:"-"`
	FromTitle string     `json:"from_title,omitempty" table:"-"`
	ToTitle   string     `json:"to_title,omitempty"   table:"-"`
	Commit    string     `json:"commit,omitempty"     table:"-"`
	Source    string     `json:"source,omitempty"     table:"-"`
	Reactions []Reaction `json:"reactions,omitempty"  table:"-"`

	Minimized       bool       `json:"minimized"                  table:"-"`
	MinimizedReason string     `json:"minimized_reason,omitempty" table:"-"`
	CreatedViaEmail bool       `json:"created_via_email"          table:"-"`
	LastEditedAt    *time.Time `json:"last_edited_at,omitempty"   table:"-"`
}

TimelineItem is one event on a thread. Type is the GraphQL __typename, lower-snake-cased.

An unrecognised typename does not get dropped: Type is set, the common fields are filled, the whole node goes into Extra, and the fixture suite fails and names it. That is the entire strategy for union drift.

type Topic

type Topic struct {
	Base

	Name        string `json:"name"                   table:"name"`
	DisplayName string `json:"display_name,omitempty" table:"display"`

	ShortDescription string `json:"short_description,omitempty" table:"description,truncate"`
	Description      string `json:"description,omitempty"       table:"-"`
	DescriptionHTML  string `json:"description_html,omitempty"  table:"-"`

	LogoURL      string   `json:"logo_url,omitempty"      table:"-"`
	WikipediaURL string   `json:"wikipedia_url,omitempty" table:"-"`
	GitHubURL    string   `json:"github_url,omitempty"    table:"-"`
	CreatedBy    string   `json:"created_by,omitempty"    table:"-"`
	Released     string   `json:"released,omitempty"      table:"released"`
	Aliases      []string `json:"aliases,omitempty"       table:"-"`
	Related      []string `json:"related,omitempty"       table:"-"`

	StargazerCount *int `json:"stargazer_count,omitempty" table:"stars"`
	AppliedCount   *int `json:"applied_count,omitempty"   table:"repos"`

	Featured bool `json:"featured" table:"-"`
	Curated  bool `json:"curated"  table:"-"`
}

Topic is a curated or uncurated topic. The search result carries most of it; the long description, the logo, the creator, the release year, the Wikipedia link, and the aliases need the topic page.

type TreeEntry

type TreeEntry struct {
	Base

	Repo string `json:"repo" table:"-"`
	Ref  string `json:"ref"  table:"-"`

	Name string `json:"name" table:"name"`
	Path string `json:"path" table:"path"`
	// Type is contentType verbatim: file, directory, symlink_file,
	// symlink_directory, submodule.
	Type string `json:"type" table:"type"`

	Size *int64 `json:"size,omitempty" table:"size"`
	SHA  string `json:"sha,omitempty"  table:"-"`
}

TreeEntry is one row of a directory listing. Size and SHA are absent from the tree route and cost one request each, which is what `--sizes` opts into.

type TreeOptions

type TreeOptions struct {
	// Ref is a branch, a tag, or a SHA. Empty means the default branch, which
	// GitHub resolves itself when the ref in the URL is HEAD, so an empty ref
	// costs no extra request.
	Ref string
	// Recursive walks subdirectories breadth-first. There is no ?recursive=1 on
	// this route, so this is one request per directory. For a whole large tree,
	// Archive is one request instead of hundreds.
	Recursive bool
	// Sizes fills Size on file entries, at one HEAD request each.
	Sizes bool
	// Limit stops the walk after this many entries. Zero means no limit.
	Limit int
}

TreeOptions controls a directory listing.

type Trending struct {
	Repo

	StarsInPeriod *int    `json:"stars_in_period,omitempty" table:"period_stars"`
	Period        string  `json:"period"                    table:"period"`
	BuiltBy       []Actor `json:"built_by,omitempty"        table:"-"`
	Rank          int     `json:"rank"                      table:"rank"`
}

Trending embeds Repo because a trending entry is a repository with three extra facts. Embedding is what makes `github trending -o url | xargs -n1 github get` work with no special case anywhere.

type TrendingOptions

type TrendingOptions struct {
	// Since is daily, weekly, or monthly. Empty means daily, which is what the
	// page defaults to.
	Since string
	// Language filters by the language slug in the URL, not by a query.
	Language string
	// SpokenLanguage is the natural-language filter, a two-letter code.
	SpokenLanguage string
	Limit          int
}

TrendingOptions are the three knobs the trending page has.

type WikiPage

type WikiPage struct {
	Base

	Repo   string `json:"repo"             table:"repo"`
	Title  string `json:"title"            table:"title"`
	Path   string `json:"path"             table:"path"`
	Format string `json:"format,omitempty" table:"-"`

	Body     string `json:"body,omitempty"      table:"-"`
	BodyHTML string `json:"body_html,omitempty" table:"-"`

	UpdatedAt *time.Time `json:"updated_at,omitempty" table:"updated,time"`
	Author    *Actor     `json:"author,omitempty"     table:"author"`
}

WikiPage is one page of a repository wiki.

Jump to

Keyboard shortcuts

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