diff --git a/github/github-accessors.go b/github/github-accessors.go index ccefd992380..1811a37dd21 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -718,6 +718,14 @@ func (a *AddProjectV2FieldRequest) GetSingleSelectOptions() []*ProjectV2FieldSin return a.SingleSelectOptions } +// GetPullRequests returns the PullRequests slice if it's non-nil, nil otherwise. +func (a *AddPullRequestsToStackRequest) GetPullRequests() []int { + if a == nil || a.PullRequests == nil { + return nil + } + return a.PullRequests +} + // GetMessage returns the Message field if it's non-nil, zero value otherwise. func (a *AddResourcesToCostCenterResponse) GetMessage() string { if a == nil || a.Message == nil { @@ -11670,6 +11678,14 @@ func (c *CreatePullRequest) GetTitle() string { return *c.Title } +// GetPullRequests returns the PullRequests slice if it's non-nil, nil otherwise. +func (c *CreatePullRequestStackRequest) GetPullRequests() []int { + if c == nil || c.PullRequests == nil { + return nil + } + return c.PullRequests +} + // GetRef returns the Ref field. func (c *CreateRef) GetRef() string { if c == nil { @@ -31438,6 +31454,14 @@ func (p *PullRequestListOptions) GetState() string { return p.State } +// GetPullRequest returns the PullRequest field. +func (p *PullRequestListStacksOptions) GetPullRequest() int { + if p == nil { + return 0 + } + return p.PullRequest +} + // GetMerged returns the Merged field if it's non-nil, zero value otherwise. func (p *PullRequestMergeResult) GetMerged() bool { if p == nil || p.Merged == nil { @@ -32054,6 +32078,342 @@ func (p *PullRequestStackBase) GetSHA() string { return p.SHA } +// GetRef returns the Ref field. +func (p *PullRequestStackBranch) GetRef() string { + if p == nil { + return "" + } + return p.Ref +} + +// GetRepo returns the Repo field. +func (p *PullRequestStackBranch) GetRepo() *PullRequestStackRepository { + if p == nil { + return nil + } + return p.Repo +} + +// GetSHA returns the SHA field. +func (p *PullRequestStackBranch) GetSHA() string { + if p == nil { + return "" + } + return p.SHA +} + +// GetBase returns the Base field. +func (p *PullRequestStackDetails) GetBase() *PullRequestStackRef { + if p == nil { + return nil + } + return p.Base +} + +// GetCreatedAt returns the CreatedAt field. +func (p *PullRequestStackDetails) GetCreatedAt() Timestamp { + if p == nil { + return Timestamp{} + } + return p.CreatedAt +} + +// GetID returns the ID field. +func (p *PullRequestStackDetails) GetID() int64 { + if p == nil { + return 0 + } + return p.ID +} + +// GetNodeID returns the NodeID field. +func (p *PullRequestStackDetails) GetNodeID() string { + if p == nil { + return "" + } + return p.NodeID +} + +// GetNumber returns the Number field. +func (p *PullRequestStackDetails) GetNumber() int { + if p == nil { + return 0 + } + return p.Number +} + +// GetOpen returns the Open field. +func (p *PullRequestStackDetails) GetOpen() bool { + if p == nil { + return false + } + return p.Open +} + +// GetPullRequests returns the PullRequests slice if it's non-nil, nil otherwise. +func (p *PullRequestStackDetails) GetPullRequests() []*PullRequestStackEntry { + if p == nil || p.PullRequests == nil { + return nil + } + return p.PullRequests +} + +// GetURL returns the URL field. +func (p *PullRequestStackDetails) GetURL() string { + if p == nil { + return "" + } + return p.URL +} + +// GetBase returns the Base field. +func (p *PullRequestStackEntry) GetBase() *PullRequestStackBranch { + if p == nil { + return nil + } + return p.Base +} + +// GetDraft returns the Draft field. +func (p *PullRequestStackEntry) GetDraft() bool { + if p == nil { + return false + } + return p.Draft +} + +// GetHead returns the Head field. +func (p *PullRequestStackEntry) GetHead() *PullRequestStackBranch { + if p == nil { + return nil + } + return p.Head +} + +// GetHTMLURL returns the HTMLURL field. +func (p *PullRequestStackEntry) GetHTMLURL() string { + if p == nil { + return "" + } + return p.HTMLURL +} + +// GetID returns the ID field. +func (p *PullRequestStackEntry) GetID() int64 { + if p == nil { + return 0 + } + return p.ID +} + +// GetMergedAt returns the MergedAt field if it's non-nil, zero value otherwise. +func (p *PullRequestStackEntry) GetMergedAt() Timestamp { + if p == nil || p.MergedAt == nil { + return Timestamp{} + } + return *p.MergedAt +} + +// GetNodeID returns the NodeID field. +func (p *PullRequestStackEntry) GetNodeID() string { + if p == nil { + return "" + } + return p.NodeID +} + +// GetNumber returns the Number field. +func (p *PullRequestStackEntry) GetNumber() int { + if p == nil { + return 0 + } + return p.Number +} + +// GetState returns the State field. +func (p *PullRequestStackEntry) GetState() string { + if p == nil { + return "" + } + return p.State +} + +// GetTitle returns the Title field. +func (p *PullRequestStackEntry) GetTitle() string { + if p == nil { + return "" + } + return p.Title +} + +// GetURL returns the URL field. +func (p *PullRequestStackEntry) GetURL() string { + if p == nil { + return "" + } + return p.URL +} + +// GetUser returns the User field. +func (p *PullRequestStackEntry) GetUser() *User { + if p == nil { + return nil + } + return p.User +} + +// GetBase returns the Base field. +func (p *PullRequestStackMinimal) GetBase() *PullRequestStackRef { + if p == nil { + return nil + } + return p.Base +} + +// GetCreatedAt returns the CreatedAt field. +func (p *PullRequestStackMinimal) GetCreatedAt() Timestamp { + if p == nil { + return Timestamp{} + } + return p.CreatedAt +} + +// GetID returns the ID field. +func (p *PullRequestStackMinimal) GetID() int64 { + if p == nil { + return 0 + } + return p.ID +} + +// GetNodeID returns the NodeID field. +func (p *PullRequestStackMinimal) GetNodeID() string { + if p == nil { + return "" + } + return p.NodeID +} + +// GetNumber returns the Number field. +func (p *PullRequestStackMinimal) GetNumber() int { + if p == nil { + return 0 + } + return p.Number +} + +// GetOpen returns the Open field. +func (p *PullRequestStackMinimal) GetOpen() bool { + if p == nil { + return false + } + return p.Open +} + +// GetPullRequests returns the PullRequests slice if it's non-nil, nil otherwise. +func (p *PullRequestStackMinimal) GetPullRequests() []*PullRequestStackMinimalEntry { + if p == nil || p.PullRequests == nil { + return nil + } + return p.PullRequests +} + +// GetURL returns the URL field. +func (p *PullRequestStackMinimal) GetURL() string { + if p == nil { + return "" + } + return p.URL +} + +// GetDraft returns the Draft field. +func (p *PullRequestStackMinimalEntry) GetDraft() bool { + if p == nil { + return false + } + return p.Draft +} + +// GetHead returns the Head field. +func (p *PullRequestStackMinimalEntry) GetHead() *PullRequestStackMinimalHead { + if p == nil { + return nil + } + return p.Head +} + +// GetMergedAt returns the MergedAt field if it's non-nil, zero value otherwise. +func (p *PullRequestStackMinimalEntry) GetMergedAt() Timestamp { + if p == nil || p.MergedAt == nil { + return Timestamp{} + } + return *p.MergedAt +} + +// GetNumber returns the Number field. +func (p *PullRequestStackMinimalEntry) GetNumber() int { + if p == nil { + return 0 + } + return p.Number +} + +// GetState returns the State field. +func (p *PullRequestStackMinimalEntry) GetState() string { + if p == nil { + return "" + } + return p.State +} + +// GetRef returns the Ref field. +func (p *PullRequestStackMinimalHead) GetRef() string { + if p == nil { + return "" + } + return p.Ref +} + +// GetSHA returns the SHA field. +func (p *PullRequestStackMinimalHead) GetSHA() string { + if p == nil { + return "" + } + return p.SHA +} + +// GetRef returns the Ref field. +func (p *PullRequestStackRef) GetRef() string { + if p == nil { + return "" + } + return p.Ref +} + +// GetID returns the ID field. +func (p *PullRequestStackRepository) GetID() int64 { + if p == nil { + return 0 + } + return p.ID +} + +// GetName returns the Name field. +func (p *PullRequestStackRepository) GetName() string { + if p == nil { + return "" + } + return p.Name +} + +// GetURL returns the URL field. +func (p *PullRequestStackRepository) GetURL() string { + if p == nil { + return "" + } + return p.URL +} + // GetBody returns the Body field if it's non-nil, zero value otherwise. func (p *PullRequestSubmitReviewRequest) GetBody() string { if p == nil || p.Body == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 6ae6420e82b..3ad94da87bc 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -908,6 +908,17 @@ func TestAddProjectV2FieldRequest_GetSingleSelectOptions(tt *testing.T) { a.GetSingleSelectOptions() } +func TestAddPullRequestsToStackRequest_GetPullRequests(tt *testing.T) { + tt.Parallel() + zeroValue := []int{} + a := &AddPullRequestsToStackRequest{PullRequests: zeroValue} + a.GetPullRequests() + a = &AddPullRequestsToStackRequest{} + a.GetPullRequests() + a = nil + a.GetPullRequests() +} + func TestAddResourcesToCostCenterResponse_GetMessage(tt *testing.T) { tt.Parallel() var zeroValue string @@ -14794,6 +14805,17 @@ func TestCreatePullRequest_GetTitle(tt *testing.T) { c.GetTitle() } +func TestCreatePullRequestStackRequest_GetPullRequests(tt *testing.T) { + tt.Parallel() + zeroValue := []int{} + c := &CreatePullRequestStackRequest{PullRequests: zeroValue} + c.GetPullRequests() + c = &CreatePullRequestStackRequest{} + c.GetPullRequests() + c = nil + c.GetPullRequests() +} + func TestCreateRef_GetRef(tt *testing.T) { tt.Parallel() c := &CreateRef{} @@ -39497,6 +39519,14 @@ func TestPullRequestListOptions_GetState(tt *testing.T) { p.GetState() } +func TestPullRequestListStacksOptions_GetPullRequest(tt *testing.T) { + tt.Parallel() + p := &PullRequestListStacksOptions{} + p.GetPullRequest() + p = nil + p.GetPullRequest() +} + func TestPullRequestMergeResult_GetMerged(tt *testing.T) { tt.Parallel() var zeroValue bool @@ -40206,6 +40236,354 @@ func TestPullRequestStackBase_GetSHA(tt *testing.T) { p.GetSHA() } +func TestPullRequestStackBranch_GetRef(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackBranch{} + p.GetRef() + p = nil + p.GetRef() +} + +func TestPullRequestStackBranch_GetRepo(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackBranch{} + p.GetRepo() + p = nil + p.GetRepo() +} + +func TestPullRequestStackBranch_GetSHA(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackBranch{} + p.GetSHA() + p = nil + p.GetSHA() +} + +func TestPullRequestStackDetails_GetBase(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackDetails{} + p.GetBase() + p = nil + p.GetBase() +} + +func TestPullRequestStackDetails_GetCreatedAt(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackDetails{} + p.GetCreatedAt() + p = nil + p.GetCreatedAt() +} + +func TestPullRequestStackDetails_GetID(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackDetails{} + p.GetID() + p = nil + p.GetID() +} + +func TestPullRequestStackDetails_GetNodeID(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackDetails{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestPullRequestStackDetails_GetNumber(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackDetails{} + p.GetNumber() + p = nil + p.GetNumber() +} + +func TestPullRequestStackDetails_GetOpen(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackDetails{} + p.GetOpen() + p = nil + p.GetOpen() +} + +func TestPullRequestStackDetails_GetPullRequests(tt *testing.T) { + tt.Parallel() + zeroValue := []*PullRequestStackEntry{} + p := &PullRequestStackDetails{PullRequests: zeroValue} + p.GetPullRequests() + p = &PullRequestStackDetails{} + p.GetPullRequests() + p = nil + p.GetPullRequests() +} + +func TestPullRequestStackDetails_GetURL(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackDetails{} + p.GetURL() + p = nil + p.GetURL() +} + +func TestPullRequestStackEntry_GetBase(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetBase() + p = nil + p.GetBase() +} + +func TestPullRequestStackEntry_GetDraft(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetDraft() + p = nil + p.GetDraft() +} + +func TestPullRequestStackEntry_GetHead(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetHead() + p = nil + p.GetHead() +} + +func TestPullRequestStackEntry_GetHTMLURL(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetHTMLURL() + p = nil + p.GetHTMLURL() +} + +func TestPullRequestStackEntry_GetID(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetID() + p = nil + p.GetID() +} + +func TestPullRequestStackEntry_GetMergedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &PullRequestStackEntry{MergedAt: &zeroValue} + p.GetMergedAt() + p = &PullRequestStackEntry{} + p.GetMergedAt() + p = nil + p.GetMergedAt() +} + +func TestPullRequestStackEntry_GetNodeID(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestPullRequestStackEntry_GetNumber(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetNumber() + p = nil + p.GetNumber() +} + +func TestPullRequestStackEntry_GetState(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetState() + p = nil + p.GetState() +} + +func TestPullRequestStackEntry_GetTitle(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetTitle() + p = nil + p.GetTitle() +} + +func TestPullRequestStackEntry_GetURL(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetURL() + p = nil + p.GetURL() +} + +func TestPullRequestStackEntry_GetUser(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackEntry{} + p.GetUser() + p = nil + p.GetUser() +} + +func TestPullRequestStackMinimal_GetBase(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimal{} + p.GetBase() + p = nil + p.GetBase() +} + +func TestPullRequestStackMinimal_GetCreatedAt(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimal{} + p.GetCreatedAt() + p = nil + p.GetCreatedAt() +} + +func TestPullRequestStackMinimal_GetID(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimal{} + p.GetID() + p = nil + p.GetID() +} + +func TestPullRequestStackMinimal_GetNodeID(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimal{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestPullRequestStackMinimal_GetNumber(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimal{} + p.GetNumber() + p = nil + p.GetNumber() +} + +func TestPullRequestStackMinimal_GetOpen(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimal{} + p.GetOpen() + p = nil + p.GetOpen() +} + +func TestPullRequestStackMinimal_GetPullRequests(tt *testing.T) { + tt.Parallel() + zeroValue := []*PullRequestStackMinimalEntry{} + p := &PullRequestStackMinimal{PullRequests: zeroValue} + p.GetPullRequests() + p = &PullRequestStackMinimal{} + p.GetPullRequests() + p = nil + p.GetPullRequests() +} + +func TestPullRequestStackMinimal_GetURL(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimal{} + p.GetURL() + p = nil + p.GetURL() +} + +func TestPullRequestStackMinimalEntry_GetDraft(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimalEntry{} + p.GetDraft() + p = nil + p.GetDraft() +} + +func TestPullRequestStackMinimalEntry_GetHead(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimalEntry{} + p.GetHead() + p = nil + p.GetHead() +} + +func TestPullRequestStackMinimalEntry_GetMergedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &PullRequestStackMinimalEntry{MergedAt: &zeroValue} + p.GetMergedAt() + p = &PullRequestStackMinimalEntry{} + p.GetMergedAt() + p = nil + p.GetMergedAt() +} + +func TestPullRequestStackMinimalEntry_GetNumber(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimalEntry{} + p.GetNumber() + p = nil + p.GetNumber() +} + +func TestPullRequestStackMinimalEntry_GetState(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimalEntry{} + p.GetState() + p = nil + p.GetState() +} + +func TestPullRequestStackMinimalHead_GetRef(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimalHead{} + p.GetRef() + p = nil + p.GetRef() +} + +func TestPullRequestStackMinimalHead_GetSHA(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackMinimalHead{} + p.GetSHA() + p = nil + p.GetSHA() +} + +func TestPullRequestStackRef_GetRef(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackRef{} + p.GetRef() + p = nil + p.GetRef() +} + +func TestPullRequestStackRepository_GetID(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackRepository{} + p.GetID() + p = nil + p.GetID() +} + +func TestPullRequestStackRepository_GetName(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackRepository{} + p.GetName() + p = nil + p.GetName() +} + +func TestPullRequestStackRepository_GetURL(tt *testing.T) { + tt.Parallel() + p := &PullRequestStackRepository{} + p.GetURL() + p = nil + p.GetURL() +} + func TestPullRequestSubmitReviewRequest_GetBody(tt *testing.T) { tt.Parallel() var zeroValue string diff --git a/github/github-iterators.go b/github/github-iterators.go index 05120877ca5..fc421373bdd 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -5355,6 +5355,37 @@ func (s *PullRequestsService) ListReviewsIter(ctx context.Context, owner string, } } +// ListStacksIter returns an iterator that paginates through all results of ListStacks. +func (s *PullRequestsService) ListStacksIter(ctx context.Context, owner string, repo string, opts *PullRequestListStacksOptions) iter.Seq2[*PullRequestStackMinimal, error] { + return func(yield func(*PullRequestStackMinimal, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &PullRequestListStacksOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListStacks(ctx, owner, repo, opts) + if err != nil { + yield(nil, err) + return + } + + for _, item := range results { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.ListOptions.Page = resp.NextPage + } + } +} + // ListCommentReactionsIter returns an iterator that paginates through all results of ListCommentReactions. func (s *ReactionsService) ListCommentReactionsIter(ctx context.Context, owner string, repo string, id int64, opts *ListReactionOptions) iter.Seq2[*Reaction, error] { return func(yield func(*Reaction, error) bool) { diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index 75bc9f90c2b..9a85375cb09 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -11823,6 +11823,78 @@ func TestPullRequestsService_ListReviewsIter(t *testing.T) { } } +func TestPullRequestsService_ListStacksIter(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.PullRequests.ListStacksIter(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.PullRequests.ListStacksIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &PullRequestListStacksOptions{} + iter = client.PullRequests.ListStacksIter(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.PullRequests.ListStacksIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.PullRequests.ListStacksIter(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.PullRequests.ListStacksIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.PullRequests.ListStacksIter(t.Context(), "", "", nil) + gotItems = 0 + iter(func(item *PullRequestStackMinimal, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.PullRequests.ListStacksIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestReactionsService_ListCommentReactionsIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/pulls_stacks.go b/github/pulls_stacks.go new file mode 100644 index 00000000000..39377a711a3 --- /dev/null +++ b/github/pulls_stacks.go @@ -0,0 +1,270 @@ +// 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" +) + +// PullRequestListStacksOptions specifies the optional parameters to the +// PullRequestsService.ListStacks method. +type PullRequestListStacksOptions struct { + // PullRequest filters stacks to the stack containing this pull request number. + PullRequest int `url:"pull_request,omitempty"` + + ListOptions +} + +// CreatePullRequestStackRequest represents a request to create a pull request stack. +type CreatePullRequestStackRequest struct { + // PullRequests is an ordered list of pull request numbers from the bottom of the stack to the top. + PullRequests []int `json:"pull_requests"` +} + +// AddPullRequestsToStackRequest represents a request to append pull requests to a stack. +type AddPullRequestsToStackRequest struct { + // PullRequests is an ordered list of pull request numbers to append from the current top upward. + PullRequests []int `json:"pull_requests"` +} + +// PullRequestStackRef represents the branch a pull request stack ultimately +// targets. The stacked pull request endpoints return the ref alone, unlike +// PullRequestStackBase, which the pull request endpoints return with a SHA. +type PullRequestStackRef struct { + // Ref is the name of the branch the entire stack ultimately targets. + Ref string `json:"ref"` +} + +// PullRequestStackDetails represents a pull request stack returned by +// PullRequestsService.CreateStack, GetStack, AddToStack, and Unstack. +type PullRequestStackDetails struct { + // ID is the ID of the stack. + ID int64 `json:"id"` + // Number is the number of the stack. + Number int `json:"number"` + // NodeID is the global node ID of the stack. + NodeID string `json:"node_id"` + // URL is the API URL of the stack. + URL string `json:"url"` + // Base is the branch the entire stack ultimately targets. + Base *PullRequestStackRef `json:"base"` + // Open reports whether the stack contains any open pull requests. + Open bool `json:"open"` + // CreatedAt is the time the stack was created. + CreatedAt Timestamp `json:"created_at"` + // PullRequests contains the pull requests in the stack, from bottom to top. + PullRequests []*PullRequestStackEntry `json:"pull_requests"` +} + +// PullRequestStackEntry represents a pull request in a stack returned by +// PullRequestsService.CreateStack, GetStack, AddToStack, and Unstack. +type PullRequestStackEntry struct { + // ID is the ID of the pull request. + ID int64 `json:"id"` + // Number is the number of the pull request. + Number int `json:"number"` + // NodeID is the global node ID of the pull request. + NodeID string `json:"node_id"` + // URL is the API URL of the pull request. + URL string `json:"url"` + // HTMLURL is the web URL of the pull request. + HTMLURL string `json:"html_url"` + // Title is the title of the pull request. + Title string `json:"title"` + // State is the state of the pull request. Possible values are: "open" and "closed". + State string `json:"state"` + // Draft reports whether the pull request is a draft. + Draft bool `json:"draft"` + // MergedAt is the time the pull request was merged, or nil if it is unmerged. + MergedAt *Timestamp `json:"merged_at"` + // User is the author of the pull request. + User *User `json:"user"` + // Head is the branch the pull request merges from. + Head *PullRequestStackBranch `json:"head"` + // Base is the branch the pull request merges into, which is the pull + // request below it in the stack. + Base *PullRequestStackBranch `json:"base"` +} + +// PullRequestStackBranch represents the head or base branch of a pull request +// returned by the stacked pull request endpoints. +type PullRequestStackBranch struct { + // Ref is the name of the branch. + Ref string `json:"ref"` + // SHA is the SHA of the most recent commit on the branch. + SHA string `json:"sha"` + // Repo is the repository the branch belongs to. + Repo *PullRequestStackRepository `json:"repo"` +} + +// PullRequestStackRepository represents the repository a stacked pull +// request's branch belongs to. +type PullRequestStackRepository struct { + // ID is the ID of the repository. + ID int64 `json:"id"` + // URL is the API URL of the repository. + URL string `json:"url"` + // Name is the name of the repository. + Name string `json:"name"` +} + +// PullRequestStackMinimal represents a pull request stack returned by +// PullRequestsService.ListStacks. This endpoint returns less detail about each +// pull request in the stack than PullRequestStackDetails carries. +type PullRequestStackMinimal struct { + // ID is the ID of the stack. + ID int64 `json:"id"` + // Number is the number of the stack. + Number int `json:"number"` + // NodeID is the global node ID of the stack. + NodeID string `json:"node_id"` + // URL is the API URL of the stack. + URL string `json:"url"` + // Base is the branch the entire stack ultimately targets. + Base *PullRequestStackRef `json:"base"` + // Open reports whether the stack contains any open pull requests. + Open bool `json:"open"` + // CreatedAt is the time the stack was created. + CreatedAt Timestamp `json:"created_at"` + // PullRequests contains the pull requests in the stack, from bottom to top. + PullRequests []*PullRequestStackMinimalEntry `json:"pull_requests"` +} + +// PullRequestStackMinimalEntry represents a pull request in a stack +// returned by PullRequestsService.ListStacks. +type PullRequestStackMinimalEntry struct { + // Number is the number of the pull request. + Number int `json:"number"` + // State is the state of the pull request. Possible values are: "open" and "closed". + State string `json:"state"` + // Draft reports whether the pull request is a draft. + Draft bool `json:"draft"` + // MergedAt is the time the pull request was merged, or nil if it is unmerged. + MergedAt *Timestamp `json:"merged_at"` + // Head is the branch the pull request merges from. + Head *PullRequestStackMinimalHead `json:"head"` +} + +// PullRequestStackMinimalHead represents the head branch of a pull request +// returned by PullRequestsService.ListStacks. +type PullRequestStackMinimalHead struct { + // Ref is the name of the branch. + Ref string `json:"ref"` + // SHA is the SHA of the most recent commit on the branch. + SHA string `json:"sha"` +} + +// ListStacks lists pull request stacks in a repository. +// +// GitHub API docs: https://docs.github.com/rest/pulls/stacks?apiVersion=2022-11-28#list-pull-request-stacks +// +//meta:operation GET /repos/{owner}/{repo}/stacks +func (s *PullRequestsService) ListStacks(ctx context.Context, owner, repo string, opts *PullRequestListStacksOptions) ([]*PullRequestStackMinimal, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/stacks", owner, repo) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var stacks []*PullRequestStackMinimal + resp, err := s.client.Do(req, &stacks) + if err != nil { + return nil, resp, err + } + + return stacks, resp, nil +} + +// CreateStack creates a pull request stack from an ordered list of pull request numbers. +// +// GitHub API docs: https://docs.github.com/rest/pulls/stacks?apiVersion=2022-11-28#create-a-pull-request-stack +// +//meta:operation POST /repos/{owner}/{repo}/stacks +func (s *PullRequestsService) CreateStack(ctx context.Context, owner, repo string, body CreatePullRequestStackRequest) (*PullRequestStackDetails, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/stacks", owner, repo) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var stack *PullRequestStackDetails + resp, err := s.client.Do(req, &stack) + if err != nil { + return nil, resp, err + } + + return stack, resp, nil +} + +// GetStack gets a pull request stack by its stack number. +// +// GitHub API docs: https://docs.github.com/rest/pulls/stacks?apiVersion=2022-11-28#get-a-pull-request-stack +// +//meta:operation GET /repos/{owner}/{repo}/stacks/{stack_number} +func (s *PullRequestsService) GetStack(ctx context.Context, owner, repo string, stackNumber int) (*PullRequestStackDetails, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/stacks/%v", owner, repo, stackNumber) + req, err := s.client.NewRequest(ctx, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var stack *PullRequestStackDetails + resp, err := s.client.Do(req, &stack) + if err != nil { + return nil, resp, err + } + + return stack, resp, nil +} + +// AddToStack appends pull requests to a pull request stack. +// +// GitHub API docs: https://docs.github.com/rest/pulls/stacks?apiVersion=2022-11-28#add-pull-requests-to-a-pull-request-stack +// +//meta:operation POST /repos/{owner}/{repo}/stacks/{stack_number}/add +func (s *PullRequestsService) AddToStack(ctx context.Context, owner, repo string, stackNumber int, body AddPullRequestsToStackRequest) (*PullRequestStackDetails, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/stacks/%v/add", owner, repo, stackNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var stack *PullRequestStackDetails + resp, err := s.client.Do(req, &stack) + if err != nil { + return nil, resp, err + } + + return stack, resp, nil +} + +// Unstack removes the unmerged pull requests from a pull request stack. It +// returns nil when no pull requests remain and the stack is dissolved. +// +// GitHub API docs: https://docs.github.com/rest/pulls/stacks?apiVersion=2022-11-28#remove-pull-requests-from-a-pull-request-stack +// +//meta:operation POST /repos/{owner}/{repo}/stacks/{stack_number}/unstack +func (s *PullRequestsService) Unstack(ctx context.Context, owner, repo string, stackNumber int) (*PullRequestStackDetails, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/stacks/%v/unstack", owner, repo, stackNumber) + req, err := s.client.NewRequest(ctx, "POST", u, nil) + if err != nil { + return nil, nil, err + } + + var stack *PullRequestStackDetails + resp, err := s.client.Do(req, &stack) + if err != nil { + return nil, resp, err + } + + return stack, resp, nil +} diff --git a/github/pulls_stacks_test.go b/github/pulls_stacks_test.go new file mode 100644 index 00000000000..42d0c5b959c --- /dev/null +++ b/github/pulls_stacks_test.go @@ -0,0 +1,364 @@ +// 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 testPullRequestStackResponse() string { + return `{ + "id":1, + "number":42, + "node_id":"S_kwDOABCDEF4AAAAA", + "url":"https://api.github.com/repos/o/r/stacks/42", + "base":{"ref":"main"}, + "open":true, + "created_at":` + referenceTimeStr + `, + "pull_requests":[{ + "id":1001, + "number":101, + "node_id":"PR_kwDOABCDEF4AAAAA", + "url":"https://api.github.com/repos/o/r/pulls/101", + "html_url":"https://github.com/o/r/pull/101", + "title":"Add a feature", + "state":"open", + "draft":false, + "merged_at":null, + "user":{"login":"octocat"}, + "head":{"ref":"feature","sha":"abc123","repo":{"id":2001,"url":"https://api.github.com/repos/o/r","name":"r"}}, + "base":{"ref":"main","sha":"def456","repo":{"id":2001,"url":"https://api.github.com/repos/o/r","name":"r"}} + }] + }` +} + +func testPullRequestStackDetails() *PullRequestStackDetails { + repo := &PullRequestStackRepository{ + ID: 2001, + URL: "https://api.github.com/repos/o/r", + Name: "r", + } + + return &PullRequestStackDetails{ + ID: 1, + Number: 42, + NodeID: "S_kwDOABCDEF4AAAAA", + URL: "https://api.github.com/repos/o/r/stacks/42", + Base: &PullRequestStackRef{Ref: "main"}, + Open: true, + CreatedAt: referenceTimestamp, + PullRequests: []*PullRequestStackEntry{{ + ID: 1001, + Number: 101, + NodeID: "PR_kwDOABCDEF4AAAAA", + URL: "https://api.github.com/repos/o/r/pulls/101", + HTMLURL: "https://github.com/o/r/pull/101", + Title: "Add a feature", + State: "open", + Draft: false, + User: &User{Login: Ptr("octocat")}, + Head: &PullRequestStackBranch{ + Ref: "feature", + SHA: "abc123", + Repo: repo, + }, + Base: &PullRequestStackBranch{ + Ref: "main", + SHA: "def456", + Repo: repo, + }, + }}, + } +} + +func testPullRequestStackMinimalResponse() string { + return `{ + "id":1, + "number":42, + "node_id":"S_kwDOABCDEF4AAAAA", + "url":"https://api.github.com/repos/o/r/stacks/42", + "base":{"ref":"main"}, + "open":true, + "created_at":` + referenceTimeStr + `, + "pull_requests":[{ + "number":101, + "state":"open", + "draft":false, + "merged_at":null, + "head":{"ref":"feature","sha":"abc123"} + }] + }` +} + +func testPullRequestStackMinimal() *PullRequestStackMinimal { + return &PullRequestStackMinimal{ + ID: 1, + Number: 42, + NodeID: "S_kwDOABCDEF4AAAAA", + URL: "https://api.github.com/repos/o/r/stacks/42", + Base: &PullRequestStackRef{Ref: "main"}, + Open: true, + CreatedAt: referenceTimestamp, + PullRequests: []*PullRequestStackMinimalEntry{{ + Number: 101, + State: "open", + Draft: false, + Head: &PullRequestStackMinimalHead{ + Ref: "feature", + SHA: "abc123", + }, + }}, + } +} + +func TestPullRequestsService_ListStacks(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/stacks", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "pull_request": "101", + "page": "2", + "per_page": "50", + }) + fmt.Fprintf(w, "[%v]", testPullRequestStackMinimalResponse()) + }) + + opts := &PullRequestListStacksOptions{ + PullRequest: 101, + ListOptions: ListOptions{Page: 2, PerPage: 50}, + } + ctx := t.Context() + stacks, _, err := client.PullRequests.ListStacks(ctx, "o", "r", opts) + if err != nil { + t.Errorf("PullRequests.ListStacks returned error: %v", err) + } + + want := []*PullRequestStackMinimal{testPullRequestStackMinimal()} + if !cmp.Equal(stacks, want) { + t.Errorf("PullRequests.ListStacks returned %+v, want %+v", stacks, want) + } + + const methodName = "ListStacks" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.PullRequests.ListStacks(ctx, "\n", "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.PullRequests.ListStacks(ctx, "o", "r", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestPullRequestsService_ListStacks_invalidOwner(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + _, _, err := client.PullRequests.ListStacks(t.Context(), "%", "%", nil) + testURLParseError(t, err) +} + +func TestPullRequestsService_CreateStack(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + input := CreatePullRequestStackRequest{PullRequests: []int{101, 102}} + + mux.HandleFunc("/repos/o/r/stacks", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + w.WriteHeader(http.StatusCreated) + fmt.Fprint(w, testPullRequestStackResponse()) + }) + + ctx := t.Context() + stack, _, err := client.PullRequests.CreateStack(ctx, "o", "r", input) + if err != nil { + t.Errorf("PullRequests.CreateStack returned error: %v", err) + } + if want := testPullRequestStackDetails(); !cmp.Equal(stack, want) { + t.Errorf("PullRequests.CreateStack returned %+v, want %+v", stack, want) + } + + const methodName = "CreateStack" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.PullRequests.CreateStack(ctx, "\n", "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.PullRequests.CreateStack(ctx, "o", "r", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestPullRequestsService_CreateStack_invalidOwner(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + _, _, err := client.PullRequests.CreateStack(t.Context(), "%", "%", CreatePullRequestStackRequest{}) + testURLParseError(t, err) +} + +func TestPullRequestsService_GetStack(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/stacks/42", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, testPullRequestStackResponse()) + }) + + ctx := t.Context() + stack, _, err := client.PullRequests.GetStack(ctx, "o", "r", 42) + if err != nil { + t.Errorf("PullRequests.GetStack returned error: %v", err) + } + if want := testPullRequestStackDetails(); !cmp.Equal(stack, want) { + t.Errorf("PullRequests.GetStack returned %+v, want %+v", stack, want) + } + + const methodName = "GetStack" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.PullRequests.GetStack(ctx, "\n", "\n", 42) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.PullRequests.GetStack(ctx, "o", "r", 42) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestPullRequestsService_GetStack_invalidOwner(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + _, _, err := client.PullRequests.GetStack(t.Context(), "%", "%", 42) + testURLParseError(t, err) +} + +func TestPullRequestsService_AddToStack(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + input := AddPullRequestsToStackRequest{PullRequests: []int{103, 104}} + + mux.HandleFunc("/repos/o/r/stacks/42/add", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, testPullRequestStackResponse()) + }) + + ctx := t.Context() + stack, _, err := client.PullRequests.AddToStack(ctx, "o", "r", 42, input) + if err != nil { + t.Errorf("PullRequests.AddToStack returned error: %v", err) + } + if want := testPullRequestStackDetails(); !cmp.Equal(stack, want) { + t.Errorf("PullRequests.AddToStack returned %+v, want %+v", stack, want) + } + + const methodName = "AddToStack" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.PullRequests.AddToStack(ctx, "\n", "\n", 42, input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.PullRequests.AddToStack(ctx, "o", "r", 42, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestPullRequestsService_AddToStack_invalidOwner(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + _, _, err := client.PullRequests.AddToStack(t.Context(), "%", "%", 42, AddPullRequestsToStackRequest{}) + testURLParseError(t, err) +} + +func TestPullRequestsService_Unstack(t *testing.T) { + t.Parallel() + + t.Run("returns updated stack", func(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/repos/o/r/stacks/42/unstack", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, testPullRequestStackResponse()) + }) + + stack, _, err := client.PullRequests.Unstack(t.Context(), "o", "r", 42) + if err != nil { + t.Errorf("PullRequests.Unstack returned error: %v", err) + } + if want := testPullRequestStackDetails(); !cmp.Equal(stack, want) { + t.Errorf("PullRequests.Unstack returned %+v, want %+v", stack, want) + } + }) + + t.Run("returns nil when stack is dissolved", func(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/repos/o/r/stacks/42/unstack", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusNoContent) + }) + + stack, resp, err := client.PullRequests.Unstack(t.Context(), "o", "r", 42) + if err != nil { + t.Errorf("PullRequests.Unstack returned error: %v", err) + } + if stack != nil { + t.Errorf("PullRequests.Unstack returned %+v, want nil", stack) + } + if resp.StatusCode != http.StatusNoContent { + t.Errorf("PullRequests.Unstack returned status %v, want %v", resp.StatusCode, http.StatusNoContent) + } + }) + + client, _, _ := setup(t) + ctx := t.Context() + const methodName = "Unstack" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.PullRequests.Unstack(ctx, "\n", "\n", 42) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.PullRequests.Unstack(ctx, "o", "r", 42) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestPullRequestsService_Unstack_invalidOwner(t *testing.T) { + t.Parallel() + client, _, _ := setup(t) + + _, _, err := client.PullRequests.Unstack(t.Context(), "%", "%", 42) + testURLParseError(t, err) +}