diff --git a/internal/github/data.go b/internal/github/data.go index ac53cfc..0748a3b 100644 --- a/internal/github/data.go +++ b/internal/github/data.go @@ -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. diff --git a/internal/github/labels.go b/internal/github/labels.go new file mode 100644 index 0000000..c46cfe2 --- /dev/null +++ b/internal/github/labels.go @@ -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 +} diff --git a/internal/github/labels_test.go b/internal/github/labels_test.go new file mode 100644 index 0000000..7755126 --- /dev/null +++ b/internal/github/labels_test.go @@ -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) + } +} diff --git a/internal/github/testdata/labels.httprr b/internal/github/testdata/labels.httprr new file mode 100644 index 0000000..a6853b8 --- /dev/null +++ b/internal/github/testdata/labels.httprr @@ -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"} \ No newline at end of file