rickub/clipublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/rickub/cli.git
git clone ssh://git@rickub.com/rickub/cli.git
endpoints.go · 415 lines · 17.3 KBGo Blame HistoryRaw
Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 10h ago1package api
2
3import (
4 "context"
5 "net/http"
6 "net/url"
7 "strings"
8)
9
10// seg escapes a single path segment.
11func seg(s string) string { return url.PathEscape(s) }
12
13// pathSegs escapes a possibly-multi-segment path, preserving slashes.
14func pathSegs(p string) string {
15 p = strings.Trim(p, "/")
16 if p == "" {
17 return ""
18 }
19 parts := strings.Split(p, "/")
20 for i, part := range parts {
21 parts[i] = url.PathEscape(part)
22 }
23 return strings.Join(parts, "/")
24}
25
26// ---- identity ----
27
28// GetUser returns the identity that owns the presented PAT.
29func (c *Client) GetUser(ctx context.Context) (*User, error) {
30 var u User
31 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/user"}, &u)
32 return &u, err
33}
34
35// ---- repositories ----
36
37// CreateRepo creates a repository.
38func (c *Client) CreateRepo(ctx context.Context, in RepoCreate) (*Repo, error) {
39 var r Repo
40 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos", Body: in}, &r)
41 return &r, err
42}
43
44// GetRepo fetches a repository.
45func (c *Client) GetRepo(ctx context.Context, owner, repo string) (*Repo, error) {
46 var r Repo
47 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo)}, &r)
48 return &r, err
49}
50
51// UpdateRepo patches a repository.
52func (c *Client) UpdateRepo(ctx context.Context, owner, repo string, in RepoUpdate) (*Repo, error) {
53 var r Repo
54 err := c.do(ctx, Request{Method: http.MethodPatch, Path: "/repos/" + seg(owner) + "/" + seg(repo), Body: in}, &r)
55 return &r, err
56}
57
58// DeleteRepo deletes a repository.
59func (c *Client) DeleteRepo(ctx context.Context, owner, repo string) error {
60 return c.do(ctx, Request{Method: http.MethodDelete, Path: "/repos/" + seg(owner) + "/" + seg(repo)}, nil)
61}
62
63// ListUserRepos lists a user's repos visible to the caller.
64func (c *Client) ListUserRepos(ctx context.Context, handle string, page, perPage int) (*RepoPage, error) {
65 var p RepoPage
66 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/users/" + seg(handle) + "/repos", Query: pageQuery(page, perPage, nil)}, &p)
67 return &p, err
68}
69
70// ListOrgRepos lists an org's repos visible to the caller.
71func (c *Client) ListOrgRepos(ctx context.Context, handle string, page, perPage int) (*RepoPage, error) {
72 var p RepoPage
73 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/orgs/" + seg(handle) + "/repos", Query: pageQuery(page, perPage, nil)}, &p)
74 return &p, err
75}
76
77// SearchRepos searches repositories.
78func (c *Client) SearchRepos(ctx context.Context, q string, page, perPage int) (*RepoPage, error) {
79 extra := url.Values{}
80 if q != "" {
81 extra.Set("q", q)
82 }
83 var p RepoPage
84 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/search/repos", Query: pageQuery(page, perPage, extra)}, &p)
85 return &p, err
86}
87
88// ---- collaborators ----
89
90// ListCollaborators lists repo collaborators (admin only).
91func (c *Client) ListCollaborators(ctx context.Context, owner, repo string) ([]Collaborator, error) {
92 var cs []Collaborator
93 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/collaborators"}, &cs)
94 return cs, err
95}
96
97// PutCollaborator adds or updates a collaborator.
98func (c *Client) PutCollaborator(ctx context.Context, owner, repo, user, permission string) (*Collaborator, error) {
99 body := map[string]string{}
100 if permission != "" {
101 body["permission"] = permission
102 }
103 var col Collaborator
104 err := c.do(ctx, Request{Method: http.MethodPut, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/collaborators/" + seg(user), Body: body}, &col)
105 return &col, err
106}
107
108// DeleteCollaborator removes a collaborator.
109func (c *Client) DeleteCollaborator(ctx context.Context, owner, repo, user string) error {
110 return c.do(ctx, Request{Method: http.MethodDelete, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/collaborators/" + seg(user)}, nil)
111}
112
113// ---- merge requests ----
114
115// ListMergeRequests lists merge requests.
116func (c *Client) ListMergeRequests(ctx context.Context, owner, repo, state string, page, perPage int) (*MergeRequestPage, error) {
117 extra := url.Values{}
118 if state != "" {
119 extra.Set("state", state)
120 }
121 var p MergeRequestPage
122 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests", Query: pageQuery(page, perPage, extra)}, &p)
123 return &p, err
124}
125
126// CreateMergeRequest opens a merge request.
127func (c *Client) CreateMergeRequest(ctx context.Context, owner, repo string, in MergeRequestCreate) (*MergeRequest, error) {
128 var mr MergeRequest
129 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests", Body: in}, &mr)
130 return &mr, err
131}
132
133// GetMergeRequest fetches a merge request with its detail.
134func (c *Client) GetMergeRequest(ctx context.Context, owner, repo string, number int) (*MergeRequestDetail, error) {
135 var mr MergeRequestDetail
136 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests/" + itoa(number)}, &mr)
137 return &mr, err
138}
139
140// MergeMergeRequest merges a merge request.
141func (c *Client) MergeMergeRequest(ctx context.Context, owner, repo string, number int, method string) (*MergeRequest, error) {
142 body := map[string]string{}
143 if method != "" {
144 body["method"] = method
145 }
146 var mr MergeRequest
147 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests/" + itoa(number) + "/merge", Body: body}, &mr)
148 return &mr, err
149}
150
151// CloseMergeRequest closes a merge request.
152func (c *Client) CloseMergeRequest(ctx context.Context, owner, repo string, number int) (*MergeRequest, error) {
153 var mr MergeRequest
154 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests/" + itoa(number) + "/close"}, &mr)
155 return &mr, err
156}
157
158// ReopenMergeRequest reopens a closed merge request.
159func (c *Client) ReopenMergeRequest(ctx context.Context, owner, repo string, number int) (*MergeRequest, error) {
160 var mr MergeRequest
161 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests/" + itoa(number) + "/reopen"}, &mr)
162 return &mr, err
163}
164
165// CommentMergeRequest adds a comment to a merge request.
166func (c *Client) CommentMergeRequest(ctx context.Context, owner, repo string, number int, body string) (*Comment, error) {
167 var cm Comment
168 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests/" + itoa(number) + "/comments", Body: map[string]string{"body": body}}, &cm)
169 return &cm, err
170}
171
172// ReviewMergeRequest records a review verdict.
173func (c *Client) ReviewMergeRequest(ctx context.Context, owner, repo string, number int, verdict string) error {
174 return c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/merge-requests/" + itoa(number) + "/reviews", Body: map[string]string{"verdict": verdict}}, nil)
175}
176
177// ---- actions ----
178
179// ListRuns lists workflow runs.
180func (c *Client) ListRuns(ctx context.Context, owner, repo string, page, perPage int) (*RunPage, error) {
181 var p RunPage
182 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/actions/runs", Query: pageQuery(page, perPage, nil)}, &p)
183 return &p, err
184}
185
186// GetRun fetches a run with jobs + step statuses.
187func (c *Client) GetRun(ctx context.Context, owner, repo string, number int) (*Run, error) {
188 var r Run
189 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/actions/runs/" + itoa(number)}, &r)
190 return &r, err
191}
192
193// GetRunLogs fetches a run's accumulated logs (JSON form).
194func (c *Client) GetRunLogs(ctx context.Context, owner, repo string, number int) (*RunLogs, error) {
195 var l RunLogs
196 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/actions/runs/" + itoa(number) + "/logs"}, &l)
197 return &l, err
198}
199
200// RerunRun re-runs a finished run.
201func (c *Client) RerunRun(ctx context.Context, owner, repo string, number int) (*Run, error) {
202 var r Run
203 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/actions/runs/" + itoa(number) + "/rerun"}, &r)
204 return &r, err
205}
206
207// CancelRun cancels an in-flight run.
208func (c *Client) CancelRun(ctx context.Context, owner, repo string, number int) (*Run, error) {
209 var r Run
210 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/actions/runs/" + itoa(number) + "/cancel"}, &r)
211 return &r, err
212}
213
214// Dispatch triggers workflow_dispatch workflows.
215func (c *Client) Dispatch(ctx context.Context, owner, repo, ref string) (*DispatchResult, error) {
216 body := map[string]string{}
217 if ref != "" {
218 body["ref"] = ref
219 }
220 var d DispatchResult
221 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/actions/dispatch", Body: body}, &d)
222 return &d, err
223}
224
225// ---- device flow (pre-auth: the device code is the credential) ----
226
227// StartDeviceLogin requests a device code the user approves in their browser
228// (the URL is in the response). Works on a tokenless client.
229func (c *Client) StartDeviceLogin(ctx context.Context, scope, clientName string) (*DeviceCodeStart, error) {
230 body := map[string]string{}
231 if clientName != "" {
232 body["client_name"] = clientName
233 }
234 if scope != "" {
235 body["scope"] = scope
236 }
237 var d DeviceCodeStart
238 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/device/code", Body: body}, &d)
239 return &d, err
240}
241
242// PollDeviceToken exchanges an approved device code for a PAT. A pending
243// approval returns an *APIError with Code "authorization_pending" (keep
244// polling); "access_denied", "expired_token", and "invalid_grant" are terminal.
245func (c *Client) PollDeviceToken(ctx context.Context, deviceCode string) (*DeviceToken, error) {
246 var d DeviceToken
247 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/device/token", Body: map[string]string{"device_code": deviceCode}}, &d)
248 return &d, err
249}
250
251// ---- issues / labels / milestones ----
252
253// ListIssues lists a repo's issues (state: open default, closed, all).
254func (c *Client) ListIssues(ctx context.Context, owner, repo, state string, page, perPage int) (*IssuePage, error) {
255 extra := url.Values{}
256 if state != "" {
257 extra.Set("state", state)
258 }
259 var p IssuePage
260 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/issues", Query: pageQuery(page, perPage, extra)}, &p)
261 return &p, err
262}
263
264// GetIssue fetches an issue with body, comments, labels, milestone, assignees.
265func (c *Client) GetIssue(ctx context.Context, owner, repo string, number int) (*IssueDetail, error) {
266 var i IssueDetail
267 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/issues/" + itoa(number)}, &i)
268 return &i, err
269}
270
271// CreateIssue opens an issue.
272func (c *Client) CreateIssue(ctx context.Context, owner, repo, title, body string) (*IssueDetail, error) {
273 var i IssueDetail
274 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/issues", Body: map[string]string{"title": title, "body": body}}, &i)
275 return &i, err
276}
277
278// SetIssueState closes ("closed") or reopens ("open") an issue.
279func (c *Client) SetIssueState(ctx context.Context, owner, repo string, number int, state string) (*IssueDetail, error) {
280 var i IssueDetail
281 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/issues/" + itoa(number) + "/state", Body: map[string]string{"state": state}}, &i)
282 return &i, err
283}
284
285// CommentIssue adds a comment.
286func (c *Client) CommentIssue(ctx context.Context, owner, repo string, number int, body string) (*Comment, error) {
287 var cm Comment
288 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/issues/" + itoa(number) + "/comments", Body: map[string]string{"body": body}}, &cm)
289 return &cm, err
290}
291
292// SetIssueLabels replaces an issue's labels by name (empty clears all).
293func (c *Client) SetIssueLabels(ctx context.Context, owner, repo string, number int, labels []string) error {
294 if labels == nil {
295 labels = []string{}
296 }
297 return c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/issues/" + itoa(number) + "/labels", Body: map[string]any{"labels": labels}}, nil)
298}
299
300// SetIssueMilestone assigns (id or title) or clears ("") an issue's milestone.
301func (c *Client) SetIssueMilestone(ctx context.Context, owner, repo string, number int, milestone string) error {
302 return c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/issues/" + itoa(number) + "/milestone", Body: map[string]string{"milestone": milestone}}, nil)
303}
304
305// SetIssueAssignee adds (op "add") or removes (op "remove") an assignee.
306func (c *Client) SetIssueAssignee(ctx context.Context, owner, repo string, number int, op, user string) error {
307 return c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/issues/" + itoa(number) + "/assignees", Body: map[string]string{"op": op, "user": user}}, nil)
308}
309
310// ListLabels lists a repo's labels.
311func (c *Client) ListLabels(ctx context.Context, owner, repo string) ([]Label, error) {
312 var l []Label
313 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/labels"}, &l)
314 return l, err
315}
316
317// ListMilestones lists a repo's milestones (state: open default, closed, all).
318func (c *Client) ListMilestones(ctx context.Context, owner, repo, state string) ([]Milestone, error) {
319 extra := url.Values{}
320 if state != "" {
321 extra.Set("state", state)
322 }
323 var m []Milestone
324 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/milestones", Query: extra}, &m)
325 return m, err
326}
327
328// CreateMilestone creates a milestone (dueOn is YYYY-MM-DD or empty).
329func (c *Client) CreateMilestone(ctx context.Context, owner, repo, title, description, dueOn string) (*Milestone, error) {
330 var m Milestone
331 err := c.do(ctx, Request{Method: http.MethodPost, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/milestones", Body: map[string]string{"title": title, "description": description, "due_on": dueOn}}, &m)
332 return &m, err
333}
334
335// UpdateMilestone patches a milestone; nil fields keep their values.
336func (c *Client) UpdateMilestone(ctx context.Context, owner, repo, id string, in map[string]any) (*Milestone, error) {
337 var m Milestone
338 err := c.do(ctx, Request{Method: http.MethodPatch, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/milestones/" + seg(id), Body: in}, &m)
339 return &m, err
340}
341
342// DeleteMilestone removes a milestone.
343func (c *Client) DeleteMilestone(ctx context.Context, owner, repo, id string) error {
344 return c.do(ctx, Request{Method: http.MethodDelete, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/milestones/" + seg(id)}, nil)
345}
346
347// ---- organizations ----
348
349// GetOrg fetches basic org info.
350func (c *Client) GetOrg(ctx context.Context, handle string) (*Org, error) {
351 var o Org
352 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/orgs/" + seg(handle)}, &o)
353 return &o, err
354}
355
356// ListOrgMembers lists org members.
357func (c *Client) ListOrgMembers(ctx context.Context, handle string) ([]OrgMember, error) {
358 var m []OrgMember
359 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/orgs/" + seg(handle) + "/members"}, &m)
360 return m, err
361}
362
363// ListOrgTeams lists org teams.
364func (c *Client) ListOrgTeams(ctx context.Context, handle string) ([]Team, error) {
365 var t []Team
366 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/orgs/" + seg(handle) + "/teams"}, &t)
367 return t, err
368}
369
370// ---- code browsing ----
371
372// GetRefs returns branches, tags, and the default branch.
373func (c *Client) GetRefs(ctx context.Context, owner, repo string) (*Refs, error) {
374 var r Refs
375 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/refs"}, &r)
376 return &r, err
377}
378
379// GetContents returns a directory listing or file content.
380func (c *Client) GetContents(ctx context.Context, owner, repo, ref, path string) (*Contents, error) {
381 p := "/repos/" + seg(owner) + "/" + seg(repo) + "/contents/" + seg(ref)
382 if sp := pathSegs(path); sp != "" {
383 p += "/" + sp
384 }
385 var ct Contents
386 err := c.do(ctx, Request{Method: http.MethodGet, Path: p}, &ct)
387 return &ct, err
388}
389
390// GetRaw returns raw file bytes.
391func (c *Client) GetRaw(ctx context.Context, owner, repo, ref, path string) ([]byte, error) {
392 p := "/repos/" + seg(owner) + "/" + seg(repo) + "/raw/" + seg(ref) + "/" + pathSegs(path)
393 return c.RawText(ctx, p, nil)
394}
395
396// GetCommits returns commit history reachable from a ref.
397func (c *Client) GetCommits(ctx context.Context, owner, repo, ref string, page, perPage int) (*CommitPage, error) {
398 var p CommitPage
399 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/commits/" + seg(ref), Query: pageQuery(page, perPage, nil)}, &p)
400 return &p, err
401}
402
403// GetCommit returns a single commit's detail.
404func (c *Client) GetCommit(ctx context.Context, owner, repo, sha string) (*CommitDetail, error) {
405 var cd CommitDetail
406 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/commit/" + seg(sha)}, &cd)
407 return &cd, err
408}
409
410// Compare returns a base...head comparison.
411func (c *Client) Compare(ctx context.Context, owner, repo, spec string) (*Comparison, error) {
412 var cmp Comparison
413 err := c.do(ctx, Request{Method: http.MethodGet, Path: "/repos/" + seg(owner) + "/" + seg(repo) + "/compare/" + seg(spec)}, &cmp)
414 return &cmp, err
415}