Documentation
¶
Overview ¶
Package gh is ghab's data layer: a Client wrapping go-gh's REST client with an in-memory TTL cache. Typed fetchers expose only the fields the UI shows.
Index ¶
- func AssetDestPath(cloneDir, assetName string) string
- func ExpandPath(path string) string
- func HumanBytes(n int64) string
- type Asset
- type BinaryError
- type Client
- func (c *Client) Dir(owner, repo, path string) ([]DirEntry, error)
- func (c *Client) DownloadAsset(url, destPath string, progress func(done, total int64)) error
- func (c *Client) FileRaw(owner, repo, path, ref string, size int64) ([]byte, error)
- func (c *Client) IssueDetail(owner, repo string, number int) (IssueDetail, error)
- func (c *Client) Issues(owner, repo string, perPage int) (issues []Issue, prs []Issue, err error)
- func (c *Client) MyPRs(queries []string, perPage int) (total int, items []SearchIssue, err error)
- func (c *Client) PullFiles(owner, repo string, number, perPage int) ([]PullFile, error)
- func (c *Client) PullReviewComments(owner, repo string, number int) ([]ReviewComment, error)
- func (c *Client) Readme(owner, repo string) (string, error)
- func (c *Client) Refresh(path string)
- func (c *Client) RefreshIssueDetail(owner, repo string, number int)
- func (c *Client) RefreshIssues(owner, repo string, perPage int)
- func (c *Client) RefreshMyPRs(queries []string, perPage int)
- func (c *Client) RefreshPullFiles(owner, repo string, number, perPage int)
- func (c *Client) RefreshPullReviewComments(owner, repo string, number int)
- func (c *Client) RefreshReadme(owner, repo string)
- func (c *Client) RefreshReleases(owner, repo string, perPage int)
- func (c *Client) RefreshTree(owner, repo, branch string)
- func (c *Client) RefreshUser(login string)
- func (c *Client) Releases(owner, repo string, perPage int) ([]Release, error)
- func (c *Client) RepoMeta(owner, repo string) (RepoMeta, error)
- func (c *Client) SearchRepos(query string, perPage int) (total int, items []UserRepo, err error)
- func (c *Client) Tree(owner, repo, branch string) (Tree, error)
- func (c *Client) User(login string) (User, error)
- func (c *Client) UserRepos(login string) ([]UserRepo, error)
- type Comment
- type DiffLine
- type DiffLineKind
- type DirEntry
- type FileThread
- type Issue
- type IssueDetail
- type LineThread
- type NoReadmeError
- type NotFoundError
- type PatchOmission
- type PullFile
- type Release
- type RepoMeta
- type ReviewComment
- type SearchIssue
- type TooLargeError
- type Tree
- type TreeEntry
- type User
- type UserRepo
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AssetDestPath ¶
AssetDestPath resolves the local path a downloaded release asset writes to: cloneDir (expanded) + "/downloads/" + assetName, per BUILD.md's M3 spec. The asset name is base-named so a hostile name with path separators can't write outside the downloads dir.
func ExpandPath ¶
ExpandPath expands a leading "~" (or "~/...") to the user's home directory, e.g. for [behavior].clone_dir ("~/Developer", "~/src"). Paths without a leading "~" pass through unchanged.
func HumanBytes ¶
HumanBytes formats a byte count the way ghab's UI shows it in placeholder lines ("binary file (243 KB)", "file too large (2.1 MB)"): whole KB below 1 MB, one decimal MB at or above it.
Types ¶
type Asset ¶
type Asset struct {
Name string `json:"name"`
Size int64 `json:"size"`
DownloadCount int `json:"download_count"`
BrowserDownloadURL string `json:"browser_download_url"`
URL string `json:"url"`
}
Asset is one release asset from repos/{o}/{r}/releases: name, size, and both download URLs -- BrowserDownloadURL for a human link, URL (the API endpoint) for DownloadAsset, which needs the API URL to stream through auth via the Accept: application/octet-stream trick.
type BinaryError ¶
BinaryError marks a file whose content sniffs as binary (a null byte in the first sniffWindow bytes).
func (*BinaryError) Error ¶
func (e *BinaryError) Error() string
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client fetches GitHub data through go-gh's REST client (auth resolved free: GH_TOKEN env, then gh's stored OAuth token) with a TTL cache in front of it. A second client (raw) is identical except it defaults to the "application/vnd.github.raw+json" Accept header, for the file/readme fetchers that want file bytes back instead of a JSON envelope. A third (assets) defaults to "application/octet-stream", for DownloadAsset.
func NewClient ¶
NewClient builds a Client. ttl is the cache lifetime for every endpoint, from [behavior].cache_ttl.
func (*Client) Dir ¶
Dir fetches repos/{owner}/{repo}/contents/{path}: one directory's listing. Used for lazy expansion when Tree's recursive fetch was truncated and the initial payload didn't include this directory's children.
func (*Client) DownloadAsset ¶
DownloadAsset streams a release asset from url (an Asset's URL field -- the API endpoint, not BrowserDownloadURL, so this works against private repos too) to destPath, creating destPath's parent directories as needed. progress is called after every chunk read with the bytes copied so far and the total reported via resp.ContentLength; total is 0 when the server doesn't report a length, in which case callers should fall back to the Asset.Size they already hold (from the Releases fetch) for display.
func (*Client) FileRaw ¶
FileRaw fetches a file's raw content from repos/{owner}/{repo}/contents/{path}?ref={ref}. size is the tree entry's reported size, checked BEFORE any request is made -- a file over 1 MB never touches the network. The response is also sniffed for binary content (a null byte in the first 8 KB); a binary hit returns a BinaryError carrying the fetched byte count instead of the bytes.
func (*Client) IssueDetail ¶
func (c *Client) IssueDetail(owner, repo string, number int) (IssueDetail, error)
IssueDetail fetches a single issue/PR (repos/{o}/{r}/issues/{n}) plus its comments (repos/{o}/{r}/issues/{n}/comments) -- two GETs combined into one typed result.
func (*Client) Issues ¶
Issues fetches repos/{owner}/{repo}/issues?state=all&per_page={n} once and splits the result client-side into issues (no pull_request key) and prs (has it). One network round trip feeds both the issues and PRs tabs -- the second caller (whichever tab asks second) is served from cache, since both request the identical URL. A 404 (or empty body) yields two empty slices rather than an error, same rationale as Releases.
func (*Client) MyPRs ¶
MyPRs runs GitHub's search/issues endpoint once per configured query and merges the results -- config's [behavior].my_prs_query, default the authored-or-review-requested pair "is:pr is:open author:@me" + "is:pr is:open review-requested:@me" (issue #11: browsing PRs across every repo without leaving the terminal).
It is two calls rather than one because GitHub's search syntax has no OR between qualifiers -- `author:@me review-requested:@me` in one query means BOTH, which matches nothing. The wider `involves:@me` does fit in one query but is a different scope: it also pulls in PRs you were merely mentioned on or commented on.
total is the count of distinct PRs returned, not the sum of GitHub's per-query total_count (which would double-count any overlap).
Same tight search rate bucket as SearchRepos (30 req/min) -- fired once per screen push, never per keystroke; the per-query cache entries mean a repeat push costs nothing.
func (*Client) PullFiles ¶
PullFiles fetches a PR's changed-files list, each with a per-file unified diff when GitHub provides one. A 404 (or empty body) yields an empty slice rather than an error, same rationale as Releases/Issues.
func (*Client) PullReviewComments ¶
func (c *Client) PullReviewComments(owner, repo string, number int) ([]ReviewComment, error)
PullReviewComments fetches a PR's line-anchored review comments. A 404 (or empty body) yields an empty slice rather than an error.
func (*Client) Readme ¶
Readme fetches repos/{owner}/{repo}/readme raw (glamour renders it client-side). A 404 becomes a typed NoReadmeError rather than the generic NotFoundError, since "no readme" is expected for plenty of repos.
func (*Client) Refresh ¶
Refresh busts the cache entry for path, forcing the next fetch to hit the network. Used by the "r" refresh binding.
func (*Client) RefreshIssueDetail ¶
RefreshIssueDetail busts the cache entries for one issue/PR's detail (the issue GET plus its comments list), forcing the next IssueDetail call to hit the network. Distinct from RefreshIssues, which busts the list fetch feeding the issues/prs tabs -- used by the "r" refresh binding when a PR's own detail/conversation view is open.
func (*Client) RefreshIssues ¶
RefreshIssues busts the issues cache entry, forcing the next Issues call to hit the network. Used by the "r" refresh binding when the issues or prs tab is active -- one bust covers both, since they share the one underlying fetch.
func (*Client) RefreshMyPRs ¶
RefreshMyPRs busts every my-PRs search cache entry for queries/perPage. Used by the "r" refresh binding on the my-PRs screen.
func (*Client) RefreshPullFiles ¶
RefreshPullFiles busts the cache entry for one PR's files-changed list. Used by the "r" refresh binding when the PR detail view is open.
func (*Client) RefreshPullReviewComments ¶
RefreshPullReviewComments busts the cache entry for one PR's review comments. Used by the "r" refresh binding when the PR detail view is open.
func (*Client) RefreshReadme ¶
RefreshReadme busts the readme cache entry, forcing the next Readme call to hit the network. Used by the "r" refresh binding when the readme tab is active.
func (*Client) RefreshReleases ¶
RefreshReleases busts the releases cache entry, forcing the next Releases call to hit the network. Used by the "r" refresh binding when the releases tab is active.
func (*Client) RefreshTree ¶
RefreshTree busts the tree cache entry, forcing the next Tree call to hit the network. Used by the "r" refresh binding when the code tab is active.
func (*Client) RefreshUser ¶
RefreshUser busts the profile screen's cache entries (user card + repo list) so its "r" binding refetches both.
func (*Client) Releases ¶
Releases fetches repos/{owner}/{repo}/releases?per_page={n}, serving from cache when fresh. A 404 (or an empty body) becomes an empty slice rather than an error -- a repo with no releases is a normal, expected state (same rationale as NoReadmeError, but releases don't need a typed error since "no releases" isn't worth a distinct render branch beyond an empty list).
func (*Client) SearchRepos ¶
SearchRepos queries search/repositories. The search rate bucket is tight (30 req/min) -- callers fire this only on an explicit enter, never per keystroke (BUILD.md's "HARD debounce").
func (*Client) Tree ¶
Tree fetches repos/{owner}/{repo}/git/trees/{branch}?recursive=1, serving from cache when fresh. GitHub caps recursive listings; if Truncated is true the response is incomplete and callers should fall back to Dir for lazy per-directory listing of whichever parts of the tree matter.
type Comment ¶
type Comment struct {
User issueUser `json:"user"`
CreatedAt time.Time `json:"created_at"`
Body string `json:"body"`
}
Comment is one entry from repos/{o}/{r}/issues/{n}/comments.
type DiffLine ¶
type DiffLine struct {
Kind DiffLineKind
Text string
OldLine int
NewLine int
}
DiffLine is one rendered line of a parsed patch. OldLine/NewLine are the 1-based line numbers on the removed/added side of the diff -- 0 when not applicable to this line's kind (an add line has no OldLine, a remove line has no NewLine, a hunk header has neither). They exist so the diff view can place review threads (anchored to a specific old/new-side line) right after the diff line they belong to.
func ParsePatch ¶
ParsePatch splits a GitHub "patch" string (a per-file unified diff body -- GitHub's /pulls/{n}/files response omits the "--- a/f" / "+++ b/f" file header lines other diff tools include, starting straight at the first "@@" hunk header) into typed, line-numbered lines. Empty or whitespace-only input (GitHub omits Patch for binary files, pure renames, and oversized text diffs -- see PullFile.Omission) yields nil.
func TrimContext ¶
TrimContext caps each run of consecutive DiffContext lines at contextLines per side, collapsing any excess into a single placeholder line -- the [behavior].diff_context_lines knob. GitHub's own patch already caps context at ~3 lines per hunk, so this only visibly bites when contextLines is set below GitHub's default (a config-first knob for narrower terminals / less scrolling); contextLines <= 0 disables trimming entirely (fail-soft: a bad config value floors at the compiled default in config.go, but this guard also protects a deliberate 0).
type DiffLineKind ¶
type DiffLineKind int
DiffLineKind classifies one parsed line of a unified diff patch, for the PR detail view's per-file diff coloring (BUILD.md extension: "add/remove line coloring").
const ( DiffContext DiffLineKind = iota DiffAdd DiffRemove DiffHunkHeader )
type DirEntry ¶
type DirEntry struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"` // "file" or "dir"
Size int64 `json:"size"`
SHA string `json:"sha"`
}
DirEntry is one entry from repos/{o}/{r}/contents/{path}: a single directory's non-recursive listing, used only when Tree reports Truncated.
type FileThread ¶
type FileThread struct {
Path string
Lines []LineThread
}
FileThread groups one file's review comments by anchor line, lines in ascending order (BUILD.md extension: "review threads grouped by file:line").
func GroupReviewThreads ¶
func GroupReviewThreads(comments []ReviewComment) []FileThread
GroupReviewThreads buckets comments by path, then by (side, line) within each path -- files sorted alphabetically, lines ascending within each file, comments oldest-first within each line. Pure and unit-testable without a network round trip (see pulls_test.go).
type Issue ¶
type Issue struct {
Number int `json:"number"`
Title string `json:"title"`
State string `json:"state"`
User issueUser `json:"user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Comments int `json:"comments"`
Body string `json:"body"`
PullRequest json.RawMessage `json:"pull_request,omitempty"`
}
Issue is one entry from repos/{o}/{r}/issues?state=all -- GitHub's issues endpoint returns pull requests too, distinguished only by the presence of a "pull_request" key (real issues never carry that key at all, not even as null). PullRequest is a json.RawMessage purely as a presence marker; its contents are never decoded further.
func (Issue) Author ¶
Author returns the issue/PR author's login, or "" if the API omitted the user object.
func (Issue) IsPullRequest ¶
IsPullRequest reports whether this issues-endpoint entry is actually a pull request.
type IssueDetail ¶
IssueDetail is the combined result of fetching one issue/PR plus its comments (two GETs -- see Client.IssueDetail).
type LineThread ¶
type LineThread struct {
Line int
Side string // "RIGHT" or "LEFT" -- see ReviewComment.Side
Thread []ReviewComment
}
LineThread is every comment anchored to one line within one file, ordered oldest-first (a conversation thread).
type NoReadmeError ¶
type NoReadmeError struct {
Owner, Repo string
}
NoReadmeError marks a repo with no readme (a 404 on the readme endpoint): the repo tab shows a muted "no readme" line for this, not an error banner -- it's a normal, expected repo state.
func (*NoReadmeError) Error ¶
func (e *NoReadmeError) Error() string
type NotFoundError ¶
type NotFoundError struct {
Path string
}
NotFoundError marks a 404 from the GitHub API so callers can render "not found or no access" instead of a raw HTTP error.
func (*NotFoundError) Error ¶
func (e *NotFoundError) Error() string
type PatchOmission ¶
type PatchOmission int
PatchOmission says WHY a PullFile carries no unified diff. The files endpoint has no "binary" flag -- it simply omits "patch", and it does that for several unrelated reasons, so "no patch" alone cannot mean binary. Distinguishing them is what keeps an oversized text diff from being labeled a binary file.
const ( // PatchPresent -- GitHub sent a patch; there is a diff to render. PatchPresent PatchOmission = iota // PatchOmittedRename -- a pure rename with no content change. PatchOmittedRename // PatchOmittedBinary -- no patch AND no line counts, so the file has // no text lines to diff at all: binary content. PatchOmittedBinary // PatchOmittedTooLarge -- the line counts say text changed, so a diff // exists; GitHub just declined to send it (its per-file diff size and // line caps). PatchOmittedTooLarge )
type PullFile ¶
type PullFile struct {
Filename string `json:"filename"`
PreviousFilename string `json:"previous_filename"`
Status string `json:"status"` // added, removed, modified, renamed, copied, changed, unchanged
Additions int `json:"additions"`
Deletions int `json:"deletions"`
Changes int `json:"changes"`
Patch string `json:"patch"`
}
PullFile is one entry from repos/{o}/{r}/pulls/{n}/files -- the files-changed list with +/- counts, plus GitHub's own per-file unified diff ("patch"). GitHub omits Patch for binary files, for files over its size threshold, and for pure renames with no content change -- see Omission, which the diff view uses to render the placeholder matching the actual reason instead of an empty pane.
func (PullFile) IsBinary ¶
IsBinary reports content GitHub cannot express as a text diff -- the diff view's binary-file placeholder case.
func (PullFile) IsDiffTooLarge ¶
IsDiffTooLarge reports a text diff GitHub withheld rather than one that does not exist -- the diff view labels this "diff not loaded", not "binary".
func (PullFile) IsPureRename ¶
IsPureRename reports a rename with no content change -- GitHub omits Patch for these (distinct from a binary file, which also omits Patch).
func (PullFile) IsRenamed ¶
IsRenamed reports whether this entry is a rename (with or without content changes).
func (PullFile) Omission ¶
func (f PullFile) Omission() PatchOmission
Omission classifies this entry per PatchOmission. The line counts carry the signal: GitHub reports 0/0/0 for a file it can't diff textually, and real +/- counts for a text diff it merely withheld.
type Release ¶
type Release struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
PublishedAt time.Time `json:"published_at"`
Prerelease bool `json:"prerelease"`
Draft bool `json:"draft"`
Assets []Asset `json:"assets"`
}
Release is the subset of repos/{o}/{r}/releases the UI shows.
type RepoMeta ¶
type RepoMeta struct {
FullName string `json:"full_name"`
Description string `json:"description"`
StargazersCount int `json:"stargazers_count"`
DefaultBranch string `json:"default_branch"`
Topics []string `json:"topics"`
OpenIssuesCount int `json:"open_issues_count"`
HTMLURL string `json:"html_url"`
Fork bool `json:"fork"`
License *struct {
SPDXID string `json:"spdx_id"`
} `json:"license"`
Parent *struct {
FullName string `json:"full_name"`
} `json:"parent"`
}
RepoMeta is the subset of GET repos/{owner}/{repo} the UI shows.
func (RepoMeta) LicenseSPDX ¶
LicenseSPDX returns the license's SPDX identifier, or "" if the repo has no license.
func (RepoMeta) ParentFullName ¶
ParentFullName returns the parent repo's "owner/name" if this repo is a fork, or "" otherwise.
type ReviewComment ¶
type ReviewComment struct {
ID int64 `json:"id"`
Path string `json:"path"`
Line int `json:"line"` // current-side line; 0 if the comment anchors to a since-removed/outdated line
OriginalLine int `json:"original_line"` // stable even after a force-push moves Line
Side string `json:"side"` // "RIGHT" (new file, the common case) or "LEFT" (old file); "" treated as RIGHT
Body string `json:"body"`
User issueUser `json:"user"`
CreatedAt time.Time `json:"created_at"`
}
ReviewComment is one entry from repos/{o}/{r}/pulls/{n}/comments -- a line-anchored review comment, distinct from the issue-style top-level comments IssueDetail fetches (BUILD.md extension: "review threads grouped by file:line").
func (ReviewComment) AnchorLine ¶
func (c ReviewComment) AnchorLine() int
AnchorLine returns the line this comment groups under: Line when present, else OriginalLine -- a comment on a line since edited or removed from the live diff still needs a stable grouping key.
func (ReviewComment) Author ¶
func (c ReviewComment) Author() string
Author returns the comment author's login, or "" if the API omitted the user object.
func (ReviewComment) IsLeftSide ¶
func (c ReviewComment) IsLeftSide() bool
IsLeftSide reports whether this comment anchors to the old (removed) side of the diff rather than the new (added/context) side.
type SearchIssue ¶
type SearchIssue struct {
Number int `json:"number"`
Title string `json:"title"`
State string `json:"state"`
User issueUser `json:"user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Comments int `json:"comments"`
Body string `json:"body"`
PullRequest json.RawMessage `json:"pull_request,omitempty"`
RepositoryURL string `json:"repository_url"`
}
SearchIssue is one entry from GitHub's search/issues endpoint -- the shape ghab's only cross-repo list ("my PRs", issue #11) fetches. It carries everything Issue does plus RepositoryURL, since search results span repos and the UI needs to know which repo each row belongs to.
func MergeSearchIssues ¶
func MergeSearchIssues(pages [][]SearchIssue) []SearchIssue
MergeSearchIssues concatenates several queries' result pages into one list: de-duplicated by repo+number (a PR matching two queries appears once) and ordered most-recently-updated first, so the merged list reads like a single ranked list rather than one query's page followed by another's. Pure and unit-testable without a network round trip.
func (SearchIssue) Author ¶
func (i SearchIssue) Author() string
Author returns the PR author's login, or "" if the API omitted the user object.
func (SearchIssue) RepoFullName ¶
func (i SearchIssue) RepoFullName() string
RepoFullName extracts "owner/repo" from RepositoryURL ("https://api.github.com/repos/{owner}/{repo}") -- "" if the field is missing or doesn't carry the expected "/repos/" marker.
type TooLargeError ¶
TooLargeError marks a file whose reported size exceeds maxFileSize; the blob is refused before any network request, per BUILD.md's M2 spec.
func (*TooLargeError) Error ¶
func (e *TooLargeError) Error() string
type TreeEntry ¶
type TreeEntry struct {
Path string `json:"path"`
Type string `json:"type"` // "blob" (file), "tree" (dir), or "commit" (submodule)
Size int64 `json:"size"`
SHA string `json:"sha"`
}
TreeEntry is one entry from repos/{o}/{r}/git/trees/{branch}?recursive=1.
type User ¶
type User struct {
Login string `json:"login"`
Name string `json:"name"`
Bio string `json:"bio"`
Location string `json:"location"`
Company string `json:"company"`
Blog string `json:"blog"`
Followers int `json:"followers"`
Following int `json:"following"`
PublicRepos int `json:"public_repos"`
HTMLURL string `json:"html_url"`
}
User is the subset of GET users/{login} the profile screen shows.
type UserRepo ¶
type UserRepo struct {
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
StargazersCount int `json:"stargazers_count"`
Language string `json:"language"`
Fork bool `json:"fork"`
Archived bool `json:"archived"`
UpdatedAt time.Time `json:"updated_at"`
HTMLURL string `json:"html_url"`
}
UserRepo is one repo row on the profile screen (and, since the search endpoint returns the same shape, one search result row).