Skip to content

Commit

Permalink
internal/github: operations on repo labels
Browse files Browse the repository at this point in the history
Support creating, changing and listing a repo's labels.

For #64.

Change-Id: I9e3f5dd8b734e3d381a7886938d545744ed7f469
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/635455
Reviewed-by: Tatiana Bradley <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Hyang-Ah Hana Kim <[email protected]>
  • Loading branch information
jba committed Dec 12, 2024
1 parent 3e575fd commit d1c618a
Show file tree
Hide file tree
Showing 4 changed files with 377 additions and 1 deletion.
4 changes: 3 additions & 1 deletion internal/github/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ type User struct {

// A Label represents a project issue tracker label in GitHub JSON.
type Label struct {
Name string `json:"name"`
Name string `json:"name"`
Description string `json:"description"`
Color string `json:"color"` // hex code without '#'
}

// A Milestone represents a project issue milestone in GitHub JSON.
Expand Down
79 changes: 79 additions & 0 deletions internal/github/labels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package github

import (
"context"
"encoding/json"
"net/url"
)

// DownloadLabel downloads information about a label from GitHub.
func (c *Client) DownloadLabel(ctx context.Context, project, name string) (Label, error) {
var lab Label
_, err := c.get(ctx, labelURL(project, name), "", &lab)
if err != nil {
return Label{}, err
}
return lab, nil
}

// CreateLabel creates a new label.
func (c *Client) CreateLabel(ctx context.Context, project string, lab Label) error {
_, err := c.post(ctx, labelURL(project, ""), lab)
return err
}

// LabelChanges specifies changes to make to a label.
// Only non-empty fields will be changed.
type LabelChanges struct {
NewName string `json:"new_name,omitempty"`
Description string `json:"description,omitempty"`
Color string `json:"color,omitempty"`
}

// EditLabel changes a label.
func (c *Client) EditLabel(ctx context.Context, project, name string, changes LabelChanges) error {
_, err := c.patch(ctx, labelURL(project, name), changes)
return err
}

// ListLabels lists all the labels in a project.
func (c *Client) ListLabels(ctx context.Context, project string) ([]Label, error) {
values := url.Values{
"page": {"1"},
"per_page": {"100"},
}
var labels []Label
for p, err := range c.pages(ctx, labelURL(project, "")+"?"+values.Encode(), "") {
if err != nil {
return nil, err
}
for _, raw := range p.body {
var lab Label
if err := json.Unmarshal(raw, &lab); err != nil {
return nil, err
}
labels = append(labels, lab)
}
}
return labels, nil
}

// deleteLabel deletes a label.
// For testing only.
func (c *Client) deleteLabel(ctx context.Context, project, name string) error {
var x any
_, err := c.json(ctx, "DELETE", labelURL(project, name), &x)
return err
}

func labelURL(project, name string) string {
u := "https://api.github.com/repos/" + project + "/labels"
if name == "" {
return u
}
return u + "/" + name
}
69 changes: 69 additions & 0 deletions internal/github/labels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package github

import (
"maps"
"net/http"
"testing"

"golang.org/x/oscar/internal/httprr"
"golang.org/x/oscar/internal/secret"
"golang.org/x/oscar/internal/testutil"
)

func TestLabels(t *testing.T) {
const project = "jba/gabytest"
check := testutil.Checker(t)
lg := testutil.Slogger(t)

// Initial load.
rr, err := httprr.Open("testdata/labels.httprr", http.DefaultTransport)
check(err)
rr.ScrubReq(Scrub)
sdb := secret.Empty()
if rr.Recording() {
sdb = secret.Netrc()
}
c := New(lg, nil, sdb, rr.Client())
labels, err := c.ListLabels(ctx, project)
check(err)
want := map[string]bool{"bug": true, "enhancement": true, "question": true}
got := map[string]bool{}
for _, lab := range labels {
if want[lab.Name] {
got[lab.Name] = true
}
}
if !maps.Equal(got, want) {
t.Errorf("got %v, want %v", got, want)
}

lab := Label{Name: "gabytest", Description: "for testing gaby", Color: "888888"}
const (
// For EditLabel. The httprr package does not support two identical requests
// in the same replay file, so we can't (1) get a label by name, (2) change
// something other than the name, and (3) get it by name again to confirm the
// change. We have to change the name.
newName = "gabytest2"
newColor = "555555"
)
// Clean up from a possible earlier failed test. Ignore error; we don't care if
// the labels don't exist.
_ = c.deleteLabel(ctx, project, lab.Name)
_ = c.deleteLabel(ctx, project, newName)
check(c.CreateLabel(ctx, project, lab))
gotlab, err := c.DownloadLabel(ctx, project, lab.Name)
check(err)
if gotlab != lab {
t.Fatalf("got %+v, want %+v", gotlab, lab)
}
check(c.EditLabel(ctx, project, lab.Name, LabelChanges{NewName: newName, Color: newColor}))
gotlab, err = c.DownloadLabel(ctx, project, newName)
check(err)
if gotlab != (Label{newName, lab.Description, newColor}) {
t.Fatalf("got %+v, want %+v", gotlab, lab)
}
}
226 changes: 226 additions & 0 deletions internal/github/testdata/labels.httprr
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
httprr trace v1
139 3565
GET https://api.github.com/repos/jba/gabytest/labels?page=1&per_page=100 HTTP/1.1
Host: api.github.com
User-Agent: Go-http-client/1.1

HTTP/2.0 200 OK
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Cache-Control: private, max-age=60, s-maxage=60
Content-Security-Policy: default-src 'none'
Content-Type: application/json; charset=utf-8
Date: Wed, 11 Dec 2024 21:53:06 GMT
Etag: W/"0fa80d49e81bb03e0a5bcc350a2b4c7f0d53735a9fbdc76e11d16471b5e5d454"
Github-Authentication-Token-Expiration: 2025-11-05 00:00:00 -0500
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Server: github.com
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
X-Accepted-Github-Permissions: issues=read; pull_requests=read
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Github-Api-Version-Selected: 2022-11-28
X-Github-Media-Type: github.v3; format=json
X-Github-Request-Id: E802:B3947:E56B13:1C52C8F:675A09C2
X-Ratelimit-Limit: 5000
X-Ratelimit-Remaining: 4956
X-Ratelimit-Reset: 1733954911
X-Ratelimit-Resource: core
X-Ratelimit-Used: 44
X-Xss-Protection: 0

[{"id":7871271184,"node_id":"LA_kwDONcSULs8AAAAB1SoREA","url":"https://api.github.com/repos/jba/gabytest/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"},{"id":7871271186,"node_id":"LA_kwDONcSULs8AAAAB1SoREg","url":"https://api.github.com/repos/jba/gabytest/labels/documentation","name":"documentation","color":"0075ca","default":true,"description":"Improvements or additions to documentation"},{"id":7871271187,"node_id":"LA_kwDONcSULs8AAAAB1SoREw","url":"https://api.github.com/repos/jba/gabytest/labels/duplicate","name":"duplicate","color":"cfd3d7","default":true,"description":"This issue or pull request already exists"},{"id":7871271189,"node_id":"LA_kwDONcSULs8AAAAB1SoRFQ","url":"https://api.github.com/repos/jba/gabytest/labels/enhancement","name":"enhancement","color":"a2eeef","default":true,"description":"New feature or request"},{"id":7871345406,"node_id":"LA_kwDONcSULs8AAAAB1Ssy_g","url":"https://api.github.com/repos/jba/gabytest/labels/gabytest2","name":"gabytest2","color":"555555","default":false,"description":"for testing gaby"},{"id":7871271194,"node_id":"LA_kwDONcSULs8AAAAB1SoRGg","url":"https://api.github.com/repos/jba/gabytest/labels/good%20first%20issue","name":"good first issue","color":"7057ff","default":true,"description":"Good for newcomers"},{"id":7871271192,"node_id":"LA_kwDONcSULs8AAAAB1SoRGA","url":"https://api.github.com/repos/jba/gabytest/labels/help%20wanted","name":"help wanted","color":"008672","default":true,"description":"Extra attention is needed"},{"id":7871271199,"node_id":"LA_kwDONcSULs8AAAAB1SoRHw","url":"https://api.github.com/repos/jba/gabytest/labels/invalid","name":"invalid","color":"e4e669","default":true,"description":"This doesn't seem right"},{"id":7871271203,"node_id":"LA_kwDONcSULs8AAAAB1SoRIw","url":"https://api.github.com/repos/jba/gabytest/labels/question","name":"question","color":"d876e3","default":true,"description":"Further information is requested"},{"id":7871271207,"node_id":"LA_kwDONcSULs8AAAAB1SoRJw","url":"https://api.github.com/repos/jba/gabytest/labels/wontfix","name":"wontfix","color":"ffffff","default":true,"description":"This will not be worked on"}]201 1329
DELETE https://api.github.com/repos/jba/gabytest/labels/gabytest HTTP/1.1
Host: api.github.com
User-Agent: Go-http-client/1.1
Content-Length: 4
Content-Type: application/json; charset=utf-8

nullHTTP/2.0 404 Not Found
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Content-Security-Policy: default-src 'none'
Content-Type: application/json; charset=utf-8
Date: Wed, 11 Dec 2024 21:53:06 GMT
Github-Authentication-Token-Expiration: 2025-11-05 00:00:00 -0500
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Server: github.com
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Vary: Accept-Encoding, Accept, X-Requested-With
X-Accepted-Github-Permissions: issues=write; pull_requests=write
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Github-Api-Version-Selected: 2022-11-28
X-Github-Media-Type: github.v3; format=json
X-Github-Request-Id: E802:B3947:E56B6E:1C52D59:675A09C2
X-Ratelimit-Limit: 5000
X-Ratelimit-Remaining: 4955
X-Ratelimit-Reset: 1733954911
X-Ratelimit-Resource: core
X-Ratelimit-Used: 45
X-Xss-Protection: 0

{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/labels#delete-a-label","status":"404"}202 1165
DELETE https://api.github.com/repos/jba/gabytest/labels/gabytest2 HTTP/1.1
Host: api.github.com
User-Agent: Go-http-client/1.1
Content-Length: 4
Content-Type: application/json; charset=utf-8

nullHTTP/2.0 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Content-Security-Policy: default-src 'none'
Date: Wed, 11 Dec 2024 21:53:06 GMT
Github-Authentication-Token-Expiration: 2025-11-05 00:00:00 -0500
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Server: github.com
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Vary: Accept-Encoding, Accept, X-Requested-With
X-Accepted-Github-Permissions: issues=write; pull_requests=write
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Github-Api-Version-Selected: 2022-11-28
X-Github-Media-Type: github.v3; format=json
X-Github-Request-Id: E802:B3947:E56BF9:1C52E4F:675A09C2
X-Ratelimit-Limit: 5000
X-Ratelimit-Remaining: 4954
X-Ratelimit-Reset: 1733954911
X-Ratelimit-Resource: core
X-Ratelimit-Used: 46
X-Xss-Protection: 0

256 1671
POST https://api.github.com/repos/jba/gabytest/labels HTTP/1.1
Host: api.github.com
User-Agent: Go-http-client/1.1
Content-Length: 69
Content-Type: application/json; charset=utf-8

{"name":"gabytest","description":"for testing gaby","color":"888888"}HTTP/2.0 201 Created
Content-Length: 205
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Cache-Control: private, max-age=60, s-maxage=60
Content-Security-Policy: default-src 'none'
Content-Type: application/json; charset=utf-8
Date: Wed, 11 Dec 2024 21:53:07 GMT
Etag: "db3e2dec9e5b6979a17a57fab93c1faf8c3fc16e62b9a6eb0c4ad7c0589e52b3"
Github-Authentication-Token-Expiration: 2025-11-05 00:00:00 -0500
Location: https://api.github.com/repos/jba/gabytest/labels/gabytest
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Server: github.com
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
X-Accepted-Github-Permissions: issues=write; pull_requests=write
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Github-Api-Version-Selected: 2022-11-28
X-Github-Media-Type: github.v3; format=json
X-Github-Request-Id: E802:B3947:E56C9E:1C52FBD:675A09C2
X-Ratelimit-Limit: 5000
X-Ratelimit-Remaining: 4953
X-Ratelimit-Reset: 1733954911
X-Ratelimit-Resource: core
X-Ratelimit-Used: 47
X-Xss-Protection: 0

{"id":7871345485,"node_id":"LA_kwDONcSULs8AAAAB1SszTQ","url":"https://api.github.com/repos/jba/gabytest/labels/gabytest","name":"gabytest","color":"888888","default":false,"description":"for testing gaby"}128 1622
GET https://api.github.com/repos/jba/gabytest/labels/gabytest HTTP/1.1
Host: api.github.com
User-Agent: Go-http-client/1.1

HTTP/2.0 200 OK
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Cache-Control: private, max-age=60, s-maxage=60
Content-Security-Policy: default-src 'none'
Content-Type: application/json; charset=utf-8
Date: Wed, 11 Dec 2024 21:53:07 GMT
Etag: W/"1d770fa2a4c482e95a47476ca768bb2b05cf6f10df5ed9e1b15f8c2518a4cb94"
Github-Authentication-Token-Expiration: 2025-11-05 00:00:00 -0500
Last-Modified: Wed, 11 Dec 2024 21:53:07 GMT
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Server: github.com
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
X-Accepted-Github-Permissions: issues=read; pull_requests=read
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Github-Api-Version-Selected: 2022-11-28
X-Github-Media-Type: github.v3; format=json
X-Github-Request-Id: E802:B3947:E56D3B:1C530F6:675A09C3
X-Ratelimit-Limit: 5000
X-Ratelimit-Remaining: 4952
X-Ratelimit-Reset: 1733954911
X-Ratelimit-Resource: core
X-Ratelimit-Used: 48
X-Xss-Protection: 0

{"id":7871345485,"node_id":"LA_kwDONcSULs8AAAAB1SszTQ","url":"https://api.github.com/repos/jba/gabytest/labels/gabytest","name":"gabytest","color":"888888","default":false,"description":"for testing gaby"}238 1580
PATCH https://api.github.com/repos/jba/gabytest/labels/gabytest HTTP/1.1
Host: api.github.com
User-Agent: Go-http-client/1.1
Content-Length: 41
Content-Type: application/json; charset=utf-8

{"new_name":"gabytest2","color":"555555"}HTTP/2.0 200 OK
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Cache-Control: private, max-age=60, s-maxage=60
Content-Security-Policy: default-src 'none'
Content-Type: application/json; charset=utf-8
Date: Wed, 11 Dec 2024 21:53:07 GMT
Etag: W/"e51dba2ffd4d1bc5216695bf82f2482bd167c8e6d01c9b59a50dfe27a7878999"
Github-Authentication-Token-Expiration: 2025-11-05 00:00:00 -0500
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Server: github.com
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
X-Accepted-Github-Permissions: issues=write; pull_requests=write
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Github-Api-Version-Selected: 2022-11-28
X-Github-Media-Type: github.v3; format=json
X-Github-Request-Id: E802:B3947:E56DA3:1C531E0:675A09C3
X-Ratelimit-Limit: 5000
X-Ratelimit-Remaining: 4951
X-Ratelimit-Reset: 1733954911
X-Ratelimit-Resource: core
X-Ratelimit-Used: 49
X-Xss-Protection: 0

{"id":7871345485,"node_id":"LA_kwDONcSULs8AAAAB1SszTQ","url":"https://api.github.com/repos/jba/gabytest/labels/gabytest2","name":"gabytest2","color":"555555","default":false,"description":"for testing gaby"}129 1624
GET https://api.github.com/repos/jba/gabytest/labels/gabytest2 HTTP/1.1
Host: api.github.com
User-Agent: Go-http-client/1.1

HTTP/2.0 200 OK
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Cache-Control: private, max-age=60, s-maxage=60
Content-Security-Policy: default-src 'none'
Content-Type: application/json; charset=utf-8
Date: Wed, 11 Dec 2024 21:53:07 GMT
Etag: W/"d5381293305f4c64e71089a457e59908dcceb7f208c4dcbe6d390e84ee781f45"
Github-Authentication-Token-Expiration: 2025-11-05 00:00:00 -0500
Last-Modified: Wed, 11 Dec 2024 21:53:07 GMT
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Server: github.com
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
X-Accepted-Github-Permissions: issues=read; pull_requests=read
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Github-Api-Version-Selected: 2022-11-28
X-Github-Media-Type: github.v3; format=json
X-Github-Request-Id: E802:B3947:E56E7E:1C53366:675A09C3
X-Ratelimit-Limit: 5000
X-Ratelimit-Remaining: 4950
X-Ratelimit-Reset: 1733954911
X-Ratelimit-Resource: core
X-Ratelimit-Used: 50
X-Xss-Protection: 0

{"id":7871345485,"node_id":"LA_kwDONcSULs8AAAAB1SszTQ","url":"https://api.github.com/repos/jba/gabytest/labels/gabytest2","name":"gabytest2","color":"555555","default":false,"description":"for testing gaby"}

0 comments on commit d1c618a

Please sign in to comment.