diff --git a/github/copilot.go b/github/copilot.go index dc29d52dbc4..af36c29354b 100644 --- a/github/copilot.go +++ b/github/copilot.go @@ -356,6 +356,66 @@ func (s *CopilotService) ListCopilotEnterpriseSeats(ctx context.Context, enterpr return copilotSeats, resp, nil } +// ListOrganizationCopilotCodingAgentRepositoriesResponse represents the response from listing +// repositories enabled for the Copilot coding agent in an organization. +type ListOrganizationCopilotCodingAgentRepositoriesResponse struct { + TotalCount int `json:"total_count"` + Repositories []*Repository `json:"repositories"` +} + +// ListOrganizationCodingAgentRepositories lists repositories enabled for the Copilot coding agent in an organization. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-coding-agent-management?apiVersion=2022-11-28#list-repositories-enabled-for-copilot-coding-agent-in-an-organization +// +//meta:operation GET /orgs/{org}/copilot/coding-agent/permissions/repositories +func (s *CopilotService) ListOrganizationCodingAgentRepositories(ctx context.Context, org string, opts *ListOptions) (*ListOrganizationCopilotCodingAgentRepositoriesResponse, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/coding-agent/permissions/repositories", org) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var result *ListOrganizationCopilotCodingAgentRepositoriesResponse + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// CopilotOrganizationContentExclusionDetails lists all Copilot content exclusion +// rules for an organization, keyed by repository full name. Each value is the +// list of file paths excluded from Copilot for that repository. +type CopilotOrganizationContentExclusionDetails map[string][]string + +// GetOrganizationContentExclusionDetails gets the Copilot content exclusion rules for an organization. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-content-exclusion-management?apiVersion=2022-11-28#get-copilot-content-exclusion-rules-for-an-organization +// +//meta:operation GET /orgs/{org}/copilot/content_exclusion +func (s *CopilotService) GetOrganizationContentExclusionDetails(ctx context.Context, org string) (CopilotOrganizationContentExclusionDetails, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/content_exclusion", org) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + details := CopilotOrganizationContentExclusionDetails{} + resp, err := s.client.Do(ctx, req, &details) + if err != nil { + return nil, resp, err + } + + return details, resp, nil +} + // AddCopilotTeams adds teams to the Copilot for Business subscription for an organization. // // GitHub API docs: https://docs.github.com/rest/copilot/copilot-user-management?apiVersion=2022-11-28#add-teams-to-the-copilot-subscription-for-an-organization diff --git a/github/copilot_test.go b/github/copilot_test.go index 90e13a110c3..aa4e085363c 100644 --- a/github/copilot_test.go +++ b/github/copilot_test.go @@ -776,6 +776,130 @@ func TestCopilotService_ListCopilotEnterpriseSeats(t *testing.T) { }) } +func TestCopilotService_ListOrganizationCodingAgentRepositories(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/copilot/coding-agent/permissions/repositories", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "per_page": "100", + "page": "1", + }) + fmt.Fprint(w, `{ + "total_count": 2, + "repositories": [ + {"id": 1, "name": "Hello-World", "full_name": "octocat/Hello-World"}, + {"id": 2, "name": "Hello-World-2", "full_name": "octocat/Hello-World-2"} + ] + }`) + }) + + ctx := t.Context() + opts := &ListOptions{Page: 1, PerPage: 100} + got, _, err := client.Copilot.ListOrganizationCodingAgentRepositories(ctx, "o", opts) + if err != nil { + t.Errorf("Copilot.ListOrganizationCodingAgentRepositories returned error: %v", err) + } + + want := &ListOrganizationCopilotCodingAgentRepositoriesResponse{ + TotalCount: 2, + Repositories: []*Repository{ + {ID: Ptr(int64(1)), Name: Ptr("Hello-World"), FullName: Ptr("octocat/Hello-World")}, + {ID: Ptr(int64(2)), Name: Ptr("Hello-World-2"), FullName: Ptr("octocat/Hello-World-2")}, + }, + } + if !cmp.Equal(got, want) { + t.Errorf("Copilot.ListOrganizationCodingAgentRepositories returned %+v, want %+v", got, want) + } + + const methodName = "ListOrganizationCodingAgentRepositories" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.ListOrganizationCodingAgentRepositories(ctx, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.ListOrganizationCodingAgentRepositories(ctx, "o", opts) + if got != nil { + t.Errorf("Copilot.ListOrganizationCodingAgentRepositories returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_GetOrganizationContentExclusionDetails(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/copilot/content_exclusion", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "octo-repo": ["/src/some-dir/kernel.rs"], + "octo-repo-2": ["/docs/secret.md", "**/*.env"] + }`) + }) + + ctx := t.Context() + got, _, err := client.Copilot.GetOrganizationContentExclusionDetails(ctx, "o") + if err != nil { + t.Errorf("Copilot.GetOrganizationContentExclusionDetails returned error: %v", err) + } + + want := CopilotOrganizationContentExclusionDetails{ + "octo-repo": {"/src/some-dir/kernel.rs"}, + "octo-repo-2": {"/docs/secret.md", "**/*.env"}, + } + if !cmp.Equal(got, want) { + t.Errorf("Copilot.GetOrganizationContentExclusionDetails returned %+v, want %+v", got, want) + } + + const methodName = "GetOrganizationContentExclusionDetails" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.GetOrganizationContentExclusionDetails(ctx, "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.GetOrganizationContentExclusionDetails(ctx, "o") + if got != nil { + t.Errorf("Copilot.GetOrganizationContentExclusionDetails returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestListOrganizationCopilotCodingAgentRepositoriesResponse_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ListOrganizationCopilotCodingAgentRepositoriesResponse{}, `{"total_count":0,"repositories":null}`) + + r := &ListOrganizationCopilotCodingAgentRepositoriesResponse{ + TotalCount: 1, + Repositories: []*Repository{ + {ID: Ptr(int64(1)), Name: Ptr("Hello-World"), FullName: Ptr("octocat/Hello-World")}, + }, + } + want := `{ + "total_count": 1, + "repositories": [ + {"id": 1, "name": "Hello-World", "full_name": "octocat/Hello-World"} + ] + }` + testJSONMarshal(t, r, want) +} + +func TestCopilotOrganizationContentExclusionDetails_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, CopilotOrganizationContentExclusionDetails{}, `{}`) + + d := CopilotOrganizationContentExclusionDetails{ + "octo-repo": {"/src/some-dir/kernel.rs"}, + } + testJSONMarshal(t, d, `{"octo-repo":["/src/some-dir/kernel.rs"]}`) +} + func TestCopilotService_AddCopilotTeams(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/github-accessors.go b/github/github-accessors.go index 52a8b098559..01854b2e07c 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -20350,6 +20350,22 @@ func (l *ListOptions) GetPerPage() int { return l.PerPage } +// GetRepositories returns the Repositories slice if it's non-nil, nil otherwise. +func (l *ListOrganizationCopilotCodingAgentRepositoriesResponse) GetRepositories() []*Repository { + if l == nil || l.Repositories == nil { + return nil + } + return l.Repositories +} + +// GetTotalCount returns the TotalCount field. +func (l *ListOrganizationCopilotCodingAgentRepositoriesResponse) GetTotalCount() int { + if l == nil { + return 0 + } + return l.TotalCount +} + // GetOrganizations returns the Organizations slice if it's non-nil, nil otherwise. func (l *ListOrganizations) GetOrganizations() []*Organization { if l == nil || l.Organizations == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index df9a0703a54..d81598069f6 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -25709,6 +25709,25 @@ func TestListOptions_GetPerPage(tt *testing.T) { l.GetPerPage() } +func TestListOrganizationCopilotCodingAgentRepositoriesResponse_GetRepositories(tt *testing.T) { + tt.Parallel() + zeroValue := []*Repository{} + l := &ListOrganizationCopilotCodingAgentRepositoriesResponse{Repositories: zeroValue} + l.GetRepositories() + l = &ListOrganizationCopilotCodingAgentRepositoriesResponse{} + l.GetRepositories() + l = nil + l.GetRepositories() +} + +func TestListOrganizationCopilotCodingAgentRepositoriesResponse_GetTotalCount(tt *testing.T) { + tt.Parallel() + l := &ListOrganizationCopilotCodingAgentRepositoriesResponse{} + l.GetTotalCount() + l = nil + l.GetTotalCount() +} + func TestListOrganizations_GetOrganizations(tt *testing.T) { tt.Parallel() zeroValue := []*Organization{} diff --git a/github/github-iterators.go b/github/github-iterators.go index 5568ca79236..384d4718cd8 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -2467,6 +2467,41 @@ func (s *CopilotService) ListCopilotSeatsIter(ctx context.Context, org string, o } } +// ListOrganizationCodingAgentRepositoriesIter returns an iterator that paginates through all results of ListOrganizationCodingAgentRepositories. +func (s *CopilotService) ListOrganizationCodingAgentRepositoriesIter(ctx context.Context, org string, opts *ListOptions) iter.Seq2[*Repository, error] { + return func(yield func(*Repository, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &ListOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListOrganizationCodingAgentRepositories(ctx, org, opts) + if err != nil { + yield(nil, err) + return + } + + var iterItems []*Repository + if results != nil { + iterItems = results.Repositories + } + for _, item := range iterItems { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + } +} + // ListOrgAlertsIter returns an iterator that paginates through all results of ListOrgAlerts. func (s *DependabotService) ListOrgAlertsIter(ctx context.Context, org string, opts *ListAlertsOptions) iter.Seq2[*DependabotAlert, error] { return func(yield func(*DependabotAlert, error) bool) { diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index f64e39c9fde..ca1fb8d76c8 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -5271,6 +5271,78 @@ func TestCopilotService_ListCopilotSeatsIter(t *testing.T) { } } +func TestCopilotService_ListOrganizationCodingAgentRepositoriesIter(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + var callNum int + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + callNum++ + switch callNum { + case 1: + w.Header().Set("Link", `; rel="next"`) + fmt.Fprint(w, `{"repositories": [{},{},{}]}`) + case 2: + fmt.Fprint(w, `{"repositories": [{},{},{},{}]}`) + case 3: + fmt.Fprint(w, `{"repositories": [{},{}]}`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `{"repositories": [{},{}]}`) + } + }) + + iter := client.Copilot.ListOrganizationCodingAgentRepositoriesIter(t.Context(), "", nil) + var gotItems int + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 7; gotItems != want { + t.Errorf("client.Copilot.ListOrganizationCodingAgentRepositoriesIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListOptions{} + iter = client.Copilot.ListOrganizationCodingAgentRepositoriesIter(t.Context(), "", opts) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 2; gotItems != want { + t.Errorf("client.Copilot.ListOrganizationCodingAgentRepositoriesIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Copilot.ListOrganizationCodingAgentRepositoriesIter(t.Context(), "", nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Copilot.ListOrganizationCodingAgentRepositoriesIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Copilot.ListOrganizationCodingAgentRepositoriesIter(t.Context(), "", nil) + gotItems = 0 + iter(func(item *Repository, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Copilot.ListOrganizationCodingAgentRepositoriesIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestDependabotService_ListOrgAlertsIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t)