| Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 10h ago | 1 | package api |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "net/http" |
| 6 | "net/url" |
| 7 | "strings" |
| 8 | ) |
| 9 | |
| 10 | // seg escapes a single path segment. |
| 11 | func seg(s string) string { return url.PathEscape(s) } |
| 12 | |
| 13 | // pathSegs escapes a possibly-multi-segment path, preserving slashes. |
| 14 | func 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. |
| 29 | func (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. |
| 38 | func (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. |
| 45 | func (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. |
| 52 | func (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. |
| 59 | func (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. |
| 64 | func (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. |
| 71 | func (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. |
| 78 | func (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). |
| 91 | func (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. |
| 98 | func (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. |
| 109 | func (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. |
| 116 | func (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. |
| 127 | func (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. |
| 134 | func (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. |
| 141 | func (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. |
| 152 | func (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. |
| 159 | func (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. |
| 166 | func (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. |
| 173 | func (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. |
| 180 | func (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. |
| 187 | func (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). |
| 194 | func (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. |
| 201 | func (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. |
| 208 | func (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. |
| 215 | func (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. |
| 229 | func (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. |
| 245 | func (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). |
| 254 | func (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. |
| 265 | func (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. |
| 272 | func (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. |
| 279 | func (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. |
| 286 | func (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). |
| 293 | func (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. |
| 301 | func (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. |
| 306 | func (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. |
| 311 | func (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). |
| 318 | func (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). |
| 329 | func (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. |
| 336 | func (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. |
| 343 | func (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. |
| 350 | func (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. |
| 357 | func (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. |
| 364 | func (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. |
| 373 | func (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. |
| 380 | func (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. |
| 391 | func (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. |
| 397 | func (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. |
| 404 | func (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. |
| 411 | func (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 | } |