// Package wire is the protocol: the JSON shapes a Gitea instance sends and // takes, the identifiers that address them, and nothing else. // // It is a package because two layers need the same vocabulary and neither may // import the other. internal/gitea is the transport — HTTP verbs, pagination, // status codes, credentials — and internal/mapping is the bridge — md <-> JSON, // pure functions, no network. Both have to name a Gitea issue, and when each // named it with a struct of its own, every command written on top of the two // would have had to copy a payload field by field from one spelling into the // other. Two copies of a shape also drift: the first field only one of them // learns is a field the other silently drops. // // THIS PACKAGE IMPORTS THE STANDARD LIBRARY AND NOTHING ELSE — no HTTP, no // filesystem, no configuration, and above all not internal/issue. That is what // lets the transport and the bridge share it without either one landing inside // the other's layer, and layering_test.go fails the moment it stops being true. // // Structs and not map[string]any, because the two representations disagreeing // is the failure this vocabulary exists to make debuggable: a typo in a key is // a compile error here and a silently dropped field there. Anything Gitea sends // that is not named below is not read by anybody — decoding is lossy on // purpose, since the tracker is not the record for anything the domain owns. package wire // User is whoever wrote or was assigned something. // // Only the login crosses this boundary — it is the one field of a Gitea user // that means anything to a command, it is what `assignees:` holds, and a // display name is not an identity anything can be pushed against. A transport // that carries the rest invites somebody to use it. type User struct { Login string `json:"login"` } // Label as the tracker holds it. // // Color is hex. Gitea returns it without the leading `#` (`ee0701`) and accepts // it either way; both spellings are the same color, so a comparison has to // strip before it compares. type Label struct { ID int64 `json:"id"` Name string `json:"name"` Color string `json:"color"` Description string `json:"description"` Exclusive bool `json:"exclusive"` } // Milestone as the tracker holds it. The domain carries its title; the id // exists only long enough to be sent back. type Milestone struct { ID int64 `json:"id"` Title string `json:"title"` State string `json:"state"` Description string `json:"description"` } // RepoRef is the repository an issue payload says it belongs to. Present on a // dependency listing, where the answer may well be another repository. type RepoRef struct { Owner string `json:"owner"` Name string `json:"name"` FullName string `json:"full_name"` } // PullRequest is non-nil on a row that is a pull request rather than an issue. // Gitea's issue endpoints return both, and `type=issues` is a filter the server // has been known to ignore — which is why every listing re-checks it. type PullRequest struct { Merged bool `json:"merged"` HTMLURL string `json:"html_url"` } // Issue is a tracker row: a Gitea issue as the API reports it. // // Timestamps stay strings. They are written into an issue's metadata verbatim // and compared as opaque values; parsing them here would mean formatting them // back, and a round trip through a time package is a chance to hand the store a // different string than the tracker sent. type Issue struct { Number int `json:"number"` Title string `json:"title"` Body string `json:"body"` State string `json:"state"` // Ref is the branch the issue is pinned to. Ref string `json:"ref"` // HTMLURL and UpdatedAt are the tracker's own bookkeeping and land in the // domain's Extra untouched. HTMLURL string `json:"html_url"` // Comments is a count, not a thread: the thread is fetched separately and // parked beside the issue as a sidecar. Comments int `json:"comments"` Labels []Label `json:"labels"` Assignees []User `json:"assignees"` Milestone *Milestone `json:"milestone"` Repository *RepoRef `json:"repository"` PullRequest *PullRequest `json:"pull_request"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // IsPullRequest reports whether this row is a pull request. func (i *Issue) IsPullRequest() bool { return i.PullRequest != nil } // LabelNames are the label names, in the order the tracker listed them. func (i *Issue) LabelNames() []string { out := make([]string, 0, len(i.Labels)) for _, l := range i.Labels { out = append(out, l.Name) } return out } // AssigneeLogins are the assignees, as logins. func (i *Issue) AssigneeLogins() []string { out := make([]string, 0, len(i.Assignees)) for _, a := range i.Assignees { out = append(out, a.Login) } return out } // MilestoneTitle is the milestone's title, or "" when there is none. func (i *Issue) MilestoneTitle() string { if i.Milestone == nil { return "" } return i.Milestone.Title } // KeyIn is this issue's cross-repo handle. The payload's own repository wins // when it carries one — a dependency listing answers with issues from other // repositories — and fallback is the repository that was asked. func (i *Issue) KeyIn(fallback Repo) Key { repo := fallback if i.Repository != nil { if r, err := ParseRepo(i.Repository.FullName); err == nil { repo = r } } return Key{Repo: repo, Number: i.Number} } // Comment is one entry in an issue's thread. // // Read only, in practice: a thread is flattened to markdown for a reader and // nothing writes that markdown back, which is why the rendering may be as lossy // as a reader needs. type Comment struct { ID int64 `json:"id"` Body string `json:"body"` HTMLURL string `json:"html_url"` User User `json:"user"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` }