diff --git a/github/github-accessors.go b/github/github-accessors.go index 37801159bdc..bb0fe129c7e 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -17862,6 +17862,22 @@ func (i *Issue) GetID() int64 { return *i.ID } +// GetIssueDependenciesSummary returns the IssueDependenciesSummary field. +func (i *Issue) GetIssueDependenciesSummary() *IssueDependenciesSummary { + if i == nil { + return nil + } + return i.IssueDependenciesSummary +} + +// GetIssueFieldValues returns the IssueFieldValues slice if it's non-nil, nil otherwise. +func (i *Issue) GetIssueFieldValues() []*IssueFieldValue { + if i == nil || i.IssueFieldValues == nil { + return nil + } + return i.IssueFieldValues +} + // GetLabels returns the Labels slice if it's non-nil, nil otherwise. func (i *Issue) GetLabels() []*Label { if i == nil || i.Labels == nil { @@ -17918,6 +17934,22 @@ func (i *Issue) GetParentIssueURL() string { return *i.ParentIssueURL } +// GetPerformedViaGithubApp returns the PerformedViaGithubApp field. +func (i *Issue) GetPerformedViaGithubApp() *App { + if i == nil { + return nil + } + return i.PerformedViaGithubApp +} + +// GetPinnedComment returns the PinnedComment field. +func (i *Issue) GetPinnedComment() *IssueComment { + if i == nil { + return nil + } + return i.PinnedComment +} + // GetPullRequestLinks returns the PullRequestLinks field. func (i *Issue) GetPullRequestLinks() *PullRequestLinks { if i == nil { @@ -17966,6 +17998,14 @@ func (i *Issue) GetStateReason() string { return *i.StateReason } +// GetSubIssuesSummary returns the SubIssuesSummary field. +func (i *Issue) GetSubIssuesSummary() *SubIssuesSummary { + if i == nil { + return nil + } + return i.SubIssuesSummary +} + // GetTextMatches returns the TextMatches slice if it's non-nil, nil otherwise. func (i *Issue) GetTextMatches() []*TextMatch { if i == nil || i.TextMatches == nil { @@ -18166,6 +18206,46 @@ func (i *IssueCommentEvent) GetSender() *User { return i.Sender } +// GetBlockedBy returns the BlockedBy field if it's non-nil, zero value otherwise. +func (i *IssueDependenciesSummary) GetBlockedBy() int { + if i == nil || i.BlockedBy == nil { + return 0 + } + return *i.BlockedBy +} + +// GetBlocking returns the Blocking field if it's non-nil, zero value otherwise. +func (i *IssueDependenciesSummary) GetBlocking() int { + if i == nil || i.Blocking == nil { + return 0 + } + return *i.Blocking +} + +// GetTotalBlockedBy returns the TotalBlockedBy field if it's non-nil, zero value otherwise. +func (i *IssueDependenciesSummary) GetTotalBlockedBy() int { + if i == nil || i.TotalBlockedBy == nil { + return 0 + } + return *i.TotalBlockedBy +} + +// GetTotalBlocking returns the TotalBlocking field if it's non-nil, zero value otherwise. +func (i *IssueDependenciesSummary) GetTotalBlocking() int { + if i == nil || i.TotalBlocking == nil { + return 0 + } + return *i.TotalBlocking +} + +// GetIssueID returns the IssueID field. +func (i *IssueDependencyRequest) GetIssueID() int64 { + if i == nil { + return 0 + } + return i.IssueID +} + // GetAction returns the Action field. func (i *IssueEvent) GetAction() string { if i == nil { @@ -18326,6 +18406,70 @@ func (i *IssueEvent) GetURL() string { return *i.URL } +// GetColor returns the Color field if it's non-nil, zero value otherwise. +func (i *IssueFieldSelectOption) GetColor() string { + if i == nil || i.Color == nil { + return "" + } + return *i.Color +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (i *IssueFieldSelectOption) GetID() int64 { + if i == nil || i.ID == nil { + return 0 + } + return *i.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (i *IssueFieldSelectOption) GetName() string { + if i == nil || i.Name == nil { + return "" + } + return *i.Name +} + +// GetDataType returns the DataType field if it's non-nil, zero value otherwise. +func (i *IssueFieldValue) GetDataType() string { + if i == nil || i.DataType == nil { + return "" + } + return *i.DataType +} + +// GetIssueFieldID returns the IssueFieldID field if it's non-nil, zero value otherwise. +func (i *IssueFieldValue) GetIssueFieldID() int64 { + if i == nil || i.IssueFieldID == nil { + return 0 + } + return *i.IssueFieldID +} + +// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +func (i *IssueFieldValue) GetNodeID() string { + if i == nil || i.NodeID == nil { + return "" + } + return *i.NodeID +} + +// GetSingleSelectOption returns the SingleSelectOption field. +func (i *IssueFieldValue) GetSingleSelectOption() *IssueFieldSelectOption { + if i == nil { + return nil + } + return i.SingleSelectOption +} + +// GetValue returns the Value field. +func (i *IssueFieldValue) GetValue() any { + if i == nil { + return nil + } + return i.Value +} + // GetAssignee returns the Assignee field if it's non-nil, zero value otherwise. func (i *IssueImport) GetAssignee() string { if i == nil || i.Assignee == nil { @@ -37774,6 +37918,30 @@ func (s *SubIssueRequest) GetSubIssueID() int64 { return s.SubIssueID } +// GetCompleted returns the Completed field if it's non-nil, zero value otherwise. +func (s *SubIssuesSummary) GetCompleted() int { + if s == nil || s.Completed == nil { + return 0 + } + return *s.Completed +} + +// GetPercentCompleted returns the PercentCompleted field if it's non-nil, zero value otherwise. +func (s *SubIssuesSummary) GetPercentCompleted() int { + if s == nil || s.PercentCompleted == nil { + return 0 + } + return *s.PercentCompleted +} + +// GetTotal returns the Total field if it's non-nil, zero value otherwise. +func (s *SubIssuesSummary) GetTotal() int { + if s == nil || s.Total == nil { + return 0 + } + return *s.Total +} + // GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. func (s *Subscription) GetCreatedAt() Timestamp { if s == nil || s.CreatedAt == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index b1dc2e00138..968d2b8ed39 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -22609,6 +22609,25 @@ func TestIssue_GetID(tt *testing.T) { i.GetID() } +func TestIssue_GetIssueDependenciesSummary(tt *testing.T) { + tt.Parallel() + i := &Issue{} + i.GetIssueDependenciesSummary() + i = nil + i.GetIssueDependenciesSummary() +} + +func TestIssue_GetIssueFieldValues(tt *testing.T) { + tt.Parallel() + zeroValue := []*IssueFieldValue{} + i := &Issue{IssueFieldValues: zeroValue} + i.GetIssueFieldValues() + i = &Issue{} + i.GetIssueFieldValues() + i = nil + i.GetIssueFieldValues() +} + func TestIssue_GetLabels(tt *testing.T) { tt.Parallel() zeroValue := []*Label{} @@ -22683,6 +22702,22 @@ func TestIssue_GetParentIssueURL(tt *testing.T) { i.GetParentIssueURL() } +func TestIssue_GetPerformedViaGithubApp(tt *testing.T) { + tt.Parallel() + i := &Issue{} + i.GetPerformedViaGithubApp() + i = nil + i.GetPerformedViaGithubApp() +} + +func TestIssue_GetPinnedComment(tt *testing.T) { + tt.Parallel() + i := &Issue{} + i.GetPinnedComment() + i = nil + i.GetPinnedComment() +} + func TestIssue_GetPullRequestLinks(tt *testing.T) { tt.Parallel() i := &Issue{} @@ -22740,6 +22775,14 @@ func TestIssue_GetStateReason(tt *testing.T) { i.GetStateReason() } +func TestIssue_GetSubIssuesSummary(tt *testing.T) { + tt.Parallel() + i := &Issue{} + i.GetSubIssuesSummary() + i = nil + i.GetSubIssuesSummary() +} + func TestIssue_GetTextMatches(tt *testing.T) { tt.Parallel() zeroValue := []*TextMatch{} @@ -22982,6 +23025,58 @@ func TestIssueCommentEvent_GetSender(tt *testing.T) { i.GetSender() } +func TestIssueDependenciesSummary_GetBlockedBy(tt *testing.T) { + tt.Parallel() + var zeroValue int + i := &IssueDependenciesSummary{BlockedBy: &zeroValue} + i.GetBlockedBy() + i = &IssueDependenciesSummary{} + i.GetBlockedBy() + i = nil + i.GetBlockedBy() +} + +func TestIssueDependenciesSummary_GetBlocking(tt *testing.T) { + tt.Parallel() + var zeroValue int + i := &IssueDependenciesSummary{Blocking: &zeroValue} + i.GetBlocking() + i = &IssueDependenciesSummary{} + i.GetBlocking() + i = nil + i.GetBlocking() +} + +func TestIssueDependenciesSummary_GetTotalBlockedBy(tt *testing.T) { + tt.Parallel() + var zeroValue int + i := &IssueDependenciesSummary{TotalBlockedBy: &zeroValue} + i.GetTotalBlockedBy() + i = &IssueDependenciesSummary{} + i.GetTotalBlockedBy() + i = nil + i.GetTotalBlockedBy() +} + +func TestIssueDependenciesSummary_GetTotalBlocking(tt *testing.T) { + tt.Parallel() + var zeroValue int + i := &IssueDependenciesSummary{TotalBlocking: &zeroValue} + i.GetTotalBlocking() + i = &IssueDependenciesSummary{} + i.GetTotalBlocking() + i = nil + i.GetTotalBlocking() +} + +func TestIssueDependencyRequest_GetIssueID(tt *testing.T) { + tt.Parallel() + i := &IssueDependencyRequest{} + i.GetIssueID() + i = nil + i.GetIssueID() +} + func TestIssueEvent_GetAction(tt *testing.T) { tt.Parallel() i := &IssueEvent{} @@ -23160,6 +23255,88 @@ func TestIssueEvent_GetURL(tt *testing.T) { i.GetURL() } +func TestIssueFieldSelectOption_GetColor(tt *testing.T) { + tt.Parallel() + var zeroValue string + i := &IssueFieldSelectOption{Color: &zeroValue} + i.GetColor() + i = &IssueFieldSelectOption{} + i.GetColor() + i = nil + i.GetColor() +} + +func TestIssueFieldSelectOption_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + i := &IssueFieldSelectOption{ID: &zeroValue} + i.GetID() + i = &IssueFieldSelectOption{} + i.GetID() + i = nil + i.GetID() +} + +func TestIssueFieldSelectOption_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + i := &IssueFieldSelectOption{Name: &zeroValue} + i.GetName() + i = &IssueFieldSelectOption{} + i.GetName() + i = nil + i.GetName() +} + +func TestIssueFieldValue_GetDataType(tt *testing.T) { + tt.Parallel() + var zeroValue string + i := &IssueFieldValue{DataType: &zeroValue} + i.GetDataType() + i = &IssueFieldValue{} + i.GetDataType() + i = nil + i.GetDataType() +} + +func TestIssueFieldValue_GetIssueFieldID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + i := &IssueFieldValue{IssueFieldID: &zeroValue} + i.GetIssueFieldID() + i = &IssueFieldValue{} + i.GetIssueFieldID() + i = nil + i.GetIssueFieldID() +} + +func TestIssueFieldValue_GetNodeID(tt *testing.T) { + tt.Parallel() + var zeroValue string + i := &IssueFieldValue{NodeID: &zeroValue} + i.GetNodeID() + i = &IssueFieldValue{} + i.GetNodeID() + i = nil + i.GetNodeID() +} + +func TestIssueFieldValue_GetSingleSelectOption(tt *testing.T) { + tt.Parallel() + i := &IssueFieldValue{} + i.GetSingleSelectOption() + i = nil + i.GetSingleSelectOption() +} + +func TestIssueFieldValue_GetValue(tt *testing.T) { + tt.Parallel() + i := &IssueFieldValue{} + i.GetValue() + i = nil + i.GetValue() +} + func TestIssueImport_GetAssignee(tt *testing.T) { tt.Parallel() var zeroValue string @@ -47420,6 +47597,39 @@ func TestSubIssueRequest_GetSubIssueID(tt *testing.T) { s.GetSubIssueID() } +func TestSubIssuesSummary_GetCompleted(tt *testing.T) { + tt.Parallel() + var zeroValue int + s := &SubIssuesSummary{Completed: &zeroValue} + s.GetCompleted() + s = &SubIssuesSummary{} + s.GetCompleted() + s = nil + s.GetCompleted() +} + +func TestSubIssuesSummary_GetPercentCompleted(tt *testing.T) { + tt.Parallel() + var zeroValue int + s := &SubIssuesSummary{PercentCompleted: &zeroValue} + s.GetPercentCompleted() + s = &SubIssuesSummary{} + s.GetPercentCompleted() + s = nil + s.GetPercentCompleted() +} + +func TestSubIssuesSummary_GetTotal(tt *testing.T) { + tt.Parallel() + var zeroValue int + s := &SubIssuesSummary{Total: &zeroValue} + s.GetTotal() + s = &SubIssuesSummary{} + s.GetTotal() + s = nil + s.GetTotal() +} + func TestSubscription_GetCreatedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp diff --git a/github/github-iterators.go b/github/github-iterators.go index 5568ca79236..c6469aca716 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -3404,6 +3404,68 @@ func (s *IssuesService) ListAssigneesIter(ctx context.Context, owner string, rep } } +// ListBlockedByIter returns an iterator that paginates through all results of ListBlockedBy. +func (s *IssuesService) ListBlockedByIter(ctx context.Context, owner string, repo string, issueNumber int64, opts *ListOptions) iter.Seq2[*Issue, error] { + return func(yield func(*Issue, 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.ListBlockedBy(ctx, owner, repo, issueNumber, opts) + if err != nil { + yield(nil, err) + return + } + + for _, item := range results { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + } +} + +// ListBlockingIter returns an iterator that paginates through all results of ListBlocking. +func (s *IssuesService) ListBlockingIter(ctx context.Context, owner string, repo string, issueNumber int64, opts *ListOptions) iter.Seq2[*Issue, error] { + return func(yield func(*Issue, 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.ListBlocking(ctx, owner, repo, issueNumber, opts) + if err != nil { + yield(nil, err) + return + } + + for _, item := range results { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + } +} + // ListByOrgIter returns an iterator that paginates through all results of ListByOrg. func (s *IssuesService) ListByOrgIter(ctx context.Context, org string, opts *IssueListByOrgOptions) iter.Seq2[*Issue, error] { return func(yield func(*Issue, error) bool) { diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index f64e39c9fde..b109c3a7c6a 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -7359,6 +7359,150 @@ func TestIssuesService_ListAssigneesIter(t *testing.T) { } } +func TestIssuesService_ListBlockedByIter(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, `[{},{},{}]`) + case 2: + fmt.Fprint(w, `[{},{},{},{}]`) + case 3: + fmt.Fprint(w, `[{},{}]`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `[{},{}]`) + } + }) + + iter := client.Issues.ListBlockedByIter(t.Context(), "", "", 0, 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.Issues.ListBlockedByIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListOptions{} + iter = client.Issues.ListBlockedByIter(t.Context(), "", "", 0, 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.Issues.ListBlockedByIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Issues.ListBlockedByIter(t.Context(), "", "", 0, nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Issues.ListBlockedByIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Issues.ListBlockedByIter(t.Context(), "", "", 0, nil) + gotItems = 0 + iter(func(item *Issue, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Issues.ListBlockedByIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + +func TestIssuesService_ListBlockingIter(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, `[{},{},{}]`) + case 2: + fmt.Fprint(w, `[{},{},{},{}]`) + case 3: + fmt.Fprint(w, `[{},{}]`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `[{},{}]`) + } + }) + + iter := client.Issues.ListBlockingIter(t.Context(), "", "", 0, 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.Issues.ListBlockingIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListOptions{} + iter = client.Issues.ListBlockingIter(t.Context(), "", "", 0, 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.Issues.ListBlockingIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Issues.ListBlockingIter(t.Context(), "", "", 0, nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Issues.ListBlockingIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Issues.ListBlockingIter(t.Context(), "", "", 0, nil) + gotItems = 0 + iter(func(item *Issue, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Issues.ListBlockingIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestIssuesService_ListByOrgIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go index 98cdbbf9d0c..abd4c88fd5f 100644 --- a/github/github-stringify_test.go +++ b/github/github-stringify_test.go @@ -928,38 +928,42 @@ func TestInvitation_String(t *testing.T) { func TestIssue_String(t *testing.T) { t.Parallel() v := Issue{ - ID: Ptr(int64(0)), - Number: Ptr(0), - State: Ptr(""), - StateReason: Ptr(""), - Locked: Ptr(false), - Title: Ptr(""), - Body: Ptr(""), - AuthorAssociation: Ptr(""), - User: &User{}, - Assignee: &User{}, - Comments: Ptr(0), - ClosedAt: &Timestamp{}, - CreatedAt: &Timestamp{}, - UpdatedAt: &Timestamp{}, - ClosedBy: &User{}, - URL: Ptr(""), - HTMLURL: Ptr(""), - CommentsURL: Ptr(""), - EventsURL: Ptr(""), - LabelsURL: Ptr(""), - RepositoryURL: Ptr(""), - ParentIssueURL: Ptr(""), - Milestone: &Milestone{}, - PullRequestLinks: &PullRequestLinks{}, - Repository: &Repository{}, - Reactions: &Reactions{}, - NodeID: Ptr(""), - Draft: Ptr(false), - Type: &IssueType{}, - ActiveLockReason: Ptr(""), - } - want := `github.Issue{ID:0, Number:0, State:"", StateReason:"", Locked:false, Title:"", Body:"", AuthorAssociation:"", User:github.User{}, Assignee:github.User{}, Comments:0, ClosedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, ClosedBy:github.User{}, URL:"", HTMLURL:"", CommentsURL:"", EventsURL:"", LabelsURL:"", RepositoryURL:"", ParentIssueURL:"", Milestone:github.Milestone{}, PullRequestLinks:github.PullRequestLinks{}, Repository:github.Repository{}, Reactions:github.Reactions{}, NodeID:"", Draft:false, Type:github.IssueType{}, ActiveLockReason:""}` + ID: Ptr(int64(0)), + Number: Ptr(0), + State: Ptr(""), + StateReason: Ptr(""), + Locked: Ptr(false), + Title: Ptr(""), + Body: Ptr(""), + AuthorAssociation: Ptr(""), + User: &User{}, + Assignee: &User{}, + Comments: Ptr(0), + ClosedAt: &Timestamp{}, + CreatedAt: &Timestamp{}, + UpdatedAt: &Timestamp{}, + ClosedBy: &User{}, + URL: Ptr(""), + HTMLURL: Ptr(""), + CommentsURL: Ptr(""), + EventsURL: Ptr(""), + LabelsURL: Ptr(""), + RepositoryURL: Ptr(""), + ParentIssueURL: Ptr(""), + Milestone: &Milestone{}, + PullRequestLinks: &PullRequestLinks{}, + Repository: &Repository{}, + Reactions: &Reactions{}, + NodeID: Ptr(""), + Draft: Ptr(false), + Type: &IssueType{}, + PinnedComment: &IssueComment{}, + PerformedViaGithubApp: &App{}, + IssueDependenciesSummary: &IssueDependenciesSummary{}, + SubIssuesSummary: &SubIssuesSummary{}, + ActiveLockReason: Ptr(""), + } + want := `github.Issue{ID:0, Number:0, State:"", StateReason:"", Locked:false, Title:"", Body:"", AuthorAssociation:"", User:github.User{}, Assignee:github.User{}, Comments:0, ClosedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, ClosedBy:github.User{}, URL:"", HTMLURL:"", CommentsURL:"", EventsURL:"", LabelsURL:"", RepositoryURL:"", ParentIssueURL:"", Milestone:github.Milestone{}, PullRequestLinks:github.PullRequestLinks{}, Repository:github.Repository{}, Reactions:github.Reactions{}, NodeID:"", Draft:false, Type:github.IssueType{}, PinnedComment:github.IssueComment{}, PerformedViaGithubApp:github.App{}, IssueDependenciesSummary:github.IssueDependenciesSummary{}, SubIssuesSummary:github.SubIssuesSummary{}, ActiveLockReason:""}` if got := v.String(); got != want { t.Errorf("Issue.String = %v, want %v", got, want) } diff --git a/github/issues.go b/github/issues.go index f4c2c7b244e..035055ae4f1 100644 --- a/github/issues.go +++ b/github/issues.go @@ -17,6 +17,37 @@ import ( // GitHub API docs: https://docs.github.com/rest/issues/ type IssuesService service +// IssueDependenciesSummary represents a summary of issue dependency counts. +type IssueDependenciesSummary struct { + BlockedBy *int `json:"blocked_by,omitempty"` + Blocking *int `json:"blocking,omitempty"` + TotalBlockedBy *int `json:"total_blocked_by,omitempty"` + TotalBlocking *int `json:"total_blocking,omitempty"` +} + +// SubIssuesSummary represents a summary of sub-issue progress. +type SubIssuesSummary struct { + Total *int `json:"total,omitempty"` + Completed *int `json:"completed,omitempty"` + PercentCompleted *int `json:"percent_completed,omitempty"` +} + +// IssueFieldSelectOption represents a selected option for a single_select issue field. +type IssueFieldSelectOption struct { + ID *int64 `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Color *string `json:"color,omitempty"` +} + +// IssueFieldValue represents a value assigned to an issue field. +type IssueFieldValue struct { + IssueFieldID *int64 `json:"issue_field_id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + DataType *string `json:"data_type,omitempty"` + Value any `json:"value,omitempty"` + SingleSelectOption *IssueFieldSelectOption `json:"single_select_option,omitempty"` +} + // Issue represents a GitHub issue on a repository. // // Note: As far as the GitHub API is concerned, every pull request is an issue, @@ -39,30 +70,35 @@ type Issue struct { // Deprecated: GitHub will remove this field from Events API payloads on October 7, 2025. // Use the Issues REST API endpoint to retrieve this information. // See: https://docs.github.com/rest/issues/issues#get-an-issue - AuthorAssociation *string `json:"author_association,omitempty"` - User *User `json:"user,omitempty"` - Labels []*Label `json:"labels,omitempty"` - Assignee *User `json:"assignee,omitempty"` - Comments *int `json:"comments,omitempty"` - ClosedAt *Timestamp `json:"closed_at,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` - ClosedBy *User `json:"closed_by,omitempty"` - URL *string `json:"url,omitempty"` - HTMLURL *string `json:"html_url,omitempty"` - CommentsURL *string `json:"comments_url,omitempty"` - EventsURL *string `json:"events_url,omitempty"` - LabelsURL *string `json:"labels_url,omitempty"` - RepositoryURL *string `json:"repository_url,omitempty"` - ParentIssueURL *string `json:"parent_issue_url,omitempty"` - Milestone *Milestone `json:"milestone,omitempty"` - PullRequestLinks *PullRequestLinks `json:"pull_request,omitempty"` - Repository *Repository `json:"repository,omitempty"` - Reactions *Reactions `json:"reactions,omitempty"` - Assignees []*User `json:"assignees,omitempty"` - NodeID *string `json:"node_id,omitempty"` - Draft *bool `json:"draft,omitempty"` - Type *IssueType `json:"type,omitempty"` + AuthorAssociation *string `json:"author_association,omitempty"` + User *User `json:"user,omitempty"` + Labels []*Label `json:"labels,omitempty"` + Assignee *User `json:"assignee,omitempty"` + Comments *int `json:"comments,omitempty"` + ClosedAt *Timestamp `json:"closed_at,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + ClosedBy *User `json:"closed_by,omitempty"` + URL *string `json:"url,omitempty"` + HTMLURL *string `json:"html_url,omitempty"` + CommentsURL *string `json:"comments_url,omitempty"` + EventsURL *string `json:"events_url,omitempty"` + LabelsURL *string `json:"labels_url,omitempty"` + RepositoryURL *string `json:"repository_url,omitempty"` + ParentIssueURL *string `json:"parent_issue_url,omitempty"` + Milestone *Milestone `json:"milestone,omitempty"` + PullRequestLinks *PullRequestLinks `json:"pull_request,omitempty"` + Repository *Repository `json:"repository,omitempty"` + Reactions *Reactions `json:"reactions,omitempty"` + Assignees []*User `json:"assignees,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Draft *bool `json:"draft,omitempty"` + Type *IssueType `json:"type,omitempty"` + PinnedComment *IssueComment `json:"pinned_comment,omitempty"` + PerformedViaGithubApp *App `json:"performed_via_github_app,omitempty"` + IssueDependenciesSummary *IssueDependenciesSummary `json:"issue_dependencies_summary,omitempty"` + SubIssuesSummary *SubIssuesSummary `json:"sub_issues_summary,omitempty"` + IssueFieldValues []*IssueFieldValue `json:"issue_field_values,omitempty"` // TextMatches is only populated from search results that request text matches // See: search.go and https://docs.github.com/rest/search/#text-match-metadata diff --git a/github/issues_dependencies.go b/github/issues_dependencies.go new file mode 100644 index 00000000000..e2d067f400b --- /dev/null +++ b/github/issues_dependencies.go @@ -0,0 +1,110 @@ +// Copyright 2026 The go-github 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" + "fmt" +) + +// IssueDependencyRequest represents a request to add a dependency to an issue. +type IssueDependencyRequest struct { + IssueID int64 `json:"issue_id"` +} + +// ListBlockedBy lists the dependencies that block the specified issue. +// +// GitHub API docs: https://docs.github.com/rest/issues/issue-dependencies#list-dependencies-an-issue-is-blocked-by +// +//meta:operation GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by +func (s *IssuesService) ListBlockedBy(ctx context.Context, owner, repo string, issueNumber int64, opts *ListOptions) ([]*Issue, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/issues/%v/dependencies/blocked_by", owner, repo, issueNumber) + 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 issues []*Issue + resp, err := s.client.Do(ctx, req, &issues) + if err != nil { + return nil, resp, err + } + + return issues, resp, nil +} + +// AddBlockedBy adds a "blocked by" dependency to the specified issue. +// +// GitHub API docs: https://docs.github.com/rest/issues/issue-dependencies#add-a-dependency-an-issue-is-blocked-by +// +//meta:operation POST /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by +func (s *IssuesService) AddBlockedBy(ctx context.Context, owner, repo string, issueNumber int64, issueDepReq IssueDependencyRequest) (*Issue, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/issues/%v/dependencies/blocked_by", owner, repo, issueNumber) + req, err := s.client.NewRequest("POST", u, issueDepReq) + if err != nil { + return nil, nil, err + } + + var issue *Issue + resp, err := s.client.Do(ctx, req, &issue) + if err != nil { + return nil, resp, err + } + + return issue, resp, nil +} + +// RemoveBlockedBy removes a "blocked by" dependency from the specified issue. +// +// GitHub API docs: https://docs.github.com/rest/issues/issue-dependencies#remove-dependency-an-issue-is-blocked-by +// +//meta:operation DELETE /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by/{issue_id} +func (s *IssuesService) RemoveBlockedBy(ctx context.Context, owner, repo string, issueNumber int64, issueID int64) (*Issue, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/issues/%v/dependencies/blocked_by/%v", owner, repo, issueNumber, issueID) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, nil, err + } + + var issue *Issue + resp, err := s.client.Do(ctx, req, &issue) + if err != nil { + return nil, resp, err + } + + return issue, resp, nil +} + +// ListBlocking lists the issues that the specified issue is blocking. +// +// GitHub API docs: https://docs.github.com/rest/issues/issue-dependencies#list-dependencies-an-issue-is-blocking +// +//meta:operation GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocking +func (s *IssuesService) ListBlocking(ctx context.Context, owner, repo string, issueNumber int64, opts *ListOptions) ([]*Issue, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/issues/%v/dependencies/blocking", owner, repo, issueNumber) + 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 issues []*Issue + resp, err := s.client.Do(ctx, req, &issues) + if err != nil { + return nil, resp, err + } + + return issues, resp, nil +} diff --git a/github/issues_dependencies_test.go b/github/issues_dependencies_test.go new file mode 100644 index 00000000000..210bc8ef52c --- /dev/null +++ b/github/issues_dependencies_test.go @@ -0,0 +1,214 @@ +// Copyright 2026 The go-github 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 ( + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestIssuesService_ListBlockedBy(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/issues/1/dependencies/blocked_by", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"page": "2"}) + fmt.Fprint(w, `[{"number":1347,"title":"Found a bug"}]`) + }) + + opt := &ListOptions{Page: 2} + ctx := t.Context() + issues, _, err := client.Issues.ListBlockedBy(ctx, "o", "r", 1, opt) + if err != nil { + t.Errorf("Issues.ListBlockedBy returned error: %v", err) + } + + want := []*Issue{{Number: Ptr(1347), Title: Ptr("Found a bug")}} + if !cmp.Equal(issues, want) { + t.Errorf("Issues.ListBlockedBy returned %+v, want %+v", issues, want) + } + + const methodName = "ListBlockedBy" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Issues.ListBlockedBy(ctx, "\n", "\n", -1, opt) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Issues.ListBlockedBy(ctx, "o", "r", 1, opt) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestIssuesService_ListBlockedBy_invalidOwner(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + ctx := t.Context() + _, _, err := client.Issues.ListBlockedBy(ctx, "%", "%", 1, nil) + testURLParseError(t, err) +} + +func TestIssuesService_AddBlockedBy(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := IssueDependencyRequest{IssueID: int64(42)} + + mux.HandleFunc("/repos/o/r/issues/1/dependencies/blocked_by", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"issue_id":42}`+"\n") + + w.WriteHeader(http.StatusCreated) + fmt.Fprint(w, `{"number":42,"title":"Dependency issue"}`) + }) + + ctx := t.Context() + issue, _, err := client.Issues.AddBlockedBy(ctx, "o", "r", 1, input) + if err != nil { + t.Errorf("Issues.AddBlockedBy returned error: %v", err) + } + + want := &Issue{Number: Ptr(42), Title: Ptr("Dependency issue")} + if !cmp.Equal(issue, want) { + t.Errorf("Issues.AddBlockedBy returned %+v, want %+v", issue, want) + } + + const methodName = "AddBlockedBy" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Issues.AddBlockedBy(ctx, "\n", "\n", -1, input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Issues.AddBlockedBy(ctx, "o", "r", 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestIssuesService_AddBlockedBy_invalidOwner(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + ctx := t.Context() + _, _, err := client.Issues.AddBlockedBy(ctx, "%", "%", 1, IssueDependencyRequest{}) + testURLParseError(t, err) +} + +func TestIssuesService_RemoveBlockedBy(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/issues/1/dependencies/blocked_by/42", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + fmt.Fprint(w, `{"number":1,"title":"Original issue"}`) + }) + + ctx := t.Context() + issue, _, err := client.Issues.RemoveBlockedBy(ctx, "o", "r", 1, 42) + if err != nil { + t.Errorf("Issues.RemoveBlockedBy returned error: %v", err) + } + + want := &Issue{Number: Ptr(1), Title: Ptr("Original issue")} + if !cmp.Equal(issue, want) { + t.Errorf("Issues.RemoveBlockedBy returned %+v, want %+v", issue, want) + } + + const methodName = "RemoveBlockedBy" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Issues.RemoveBlockedBy(ctx, "\n", "\n", -1, 42) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Issues.RemoveBlockedBy(ctx, "o", "r", 1, 42) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestIssuesService_RemoveBlockedBy_invalidOwner(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + ctx := t.Context() + _, _, err := client.Issues.RemoveBlockedBy(ctx, "%", "%", 1, 42) + testURLParseError(t, err) +} + +func TestIssuesService_ListBlocking(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/issues/1/dependencies/blocking", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"page": "2"}) + fmt.Fprint(w, `[{"number":1348,"title":"Blocked issue"}]`) + }) + + opt := &ListOptions{Page: 2} + ctx := t.Context() + issues, _, err := client.Issues.ListBlocking(ctx, "o", "r", 1, opt) + if err != nil { + t.Errorf("Issues.ListBlocking returned error: %v", err) + } + + want := []*Issue{{Number: Ptr(1348), Title: Ptr("Blocked issue")}} + if !cmp.Equal(issues, want) { + t.Errorf("Issues.ListBlocking returned %+v, want %+v", issues, want) + } + + const methodName = "ListBlocking" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Issues.ListBlocking(ctx, "\n", "\n", -1, opt) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Issues.ListBlocking(ctx, "o", "r", 1, opt) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestIssuesService_ListBlocking_invalidOwner(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + ctx := t.Context() + _, _, err := client.Issues.ListBlocking(ctx, "%", "%", 1, nil) + testURLParseError(t, err) +} + +func TestIssueDependencyRequest_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &IssueDependencyRequest{}, `{"issue_id":0}`) + + u := &IssueDependencyRequest{ + IssueID: int64(1), + } + + want := `{ + "issue_id": 1 + }` + + testJSONMarshal(t, u, want) +}