Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions api/queries_pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ type PullRequest struct {
PotentialMergeCommit *Commit

Files struct {
Nodes []PullRequestFile
Nodes []PullRequestFile
PageInfo struct {
HasNextPage bool
EndCursor string
}
}

Author Author
Expand All @@ -79,6 +83,10 @@ type PullRequest struct {
Commits struct {
TotalCount int
Nodes []PullRequestCommit
PageInfo struct {
HasNextPage bool
EndCursor string
}
}
StatusCheckRollup struct {
Nodes []StatusCheckRollupNode
Expand Down Expand Up @@ -285,7 +293,7 @@ type PullRequestCommitCommit struct {
Email string
User GitHubUser
}
}
} `graphql:"authors(first: 100)"`
MessageHeadline string
MessageBody string
CommittedDate time.Time
Expand Down
6 changes: 4 additions & 2 deletions api/query_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ var prFiles = shortenQuery(`
deletions,
path,
changeType
}
},
pageInfo{hasNextPage,endCursor}
}
`)

Expand All @@ -171,7 +172,8 @@ var prCommits = shortenQuery(`
committedDate,
authoredDate
}
}
},
pageInfo{hasNextPage,endCursor}
}
`)

Expand Down
4 changes: 2 additions & 2 deletions api/query_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestPullRequestGraphQL(t *testing.T) {
{
name: "compressed query",
fields: []string{"files"},
want: "files(first: 100) {nodes {additions,deletions,path,changeType}}",
want: "files(first: 100) {nodes {additions,deletions,path,changeType},pageInfo{hasNextPage,endCursor}}",
},
{
name: "invalid fields",
Expand Down Expand Up @@ -72,7 +72,7 @@ func TestIssueGraphQL(t *testing.T) {
{
name: "compressed query",
fields: []string{"files"},
want: "files(first: 100) {nodes {additions,deletions,path,changeType}}",
want: "files(first: 100) {nodes {additions,deletions,path,changeType},pageInfo{hasNextPage,endCursor}}",
},
{
name: "projectItems",
Expand Down
101 changes: 101 additions & 0 deletions pkg/cmd/pr/shared/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,16 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
return preloadPrClosingIssuesReferences(httpClient, f.baseRefRepo, pr)
})
}
if fields.Contains("files") {
g.Go(func() error {
return preloadPrFiles(httpClient, f.baseRefRepo, pr)
})
}
if fields.Contains("commits") {
g.Go(func() error {
return preloadPrCommits(httpClient, f.baseRefRepo, pr)
})
}
if fields.Contains("statusCheckRollup") {
g.Go(func() error {
return preloadPrChecks(httpClient, f.baseRefRepo, pr)
Expand Down Expand Up @@ -560,6 +570,97 @@ func preloadPrClosingIssuesReferences(client *http.Client, repo ghrepo.Interface
return nil
}

func preloadPrFiles(client *http.Client, repo ghrepo.Interface, pr *api.PullRequest) error {
if !pr.Files.PageInfo.HasNextPage {
return nil
}

Comment on lines +573 to +577
type response struct {
Node struct {
PullRequest struct {
Files struct {
Nodes []api.PullRequestFile
PageInfo struct {
HasNextPage bool
EndCursor string
}
} `graphql:"files(first: 100, after: $endCursor)"`
} `graphql:"...on PullRequest"`
} `graphql:"node(id: $id)"`
}

variables := map[string]interface{}{
"id": githubv4.ID(pr.ID),
"endCursor": githubv4.String(pr.Files.PageInfo.EndCursor),
}

gql := api.NewClientFromHTTP(client)

for {
var query response
err := gql.Query(repo.RepoHost(), "FilesForPullRequest", &query, variables)
if err != nil {
return err
}

pr.Files.Nodes = append(pr.Files.Nodes, query.Node.PullRequest.Files.Nodes...)

if !query.Node.PullRequest.Files.PageInfo.HasNextPage {
break
}
variables["endCursor"] = githubv4.String(query.Node.PullRequest.Files.PageInfo.EndCursor)
}

pr.Files.PageInfo.HasNextPage = false
return nil
}

func preloadPrCommits(client *http.Client, repo ghrepo.Interface, pr *api.PullRequest) error {
if !pr.Commits.PageInfo.HasNextPage {
return nil
}

type response struct {
Node struct {
PullRequest struct {
Commits struct {
Nodes []api.PullRequestCommit
PageInfo struct {
HasNextPage bool
EndCursor string
Comment on lines +626 to +630
}
} `graphql:"commits(first: 100, after: $endCursor)"`
} `graphql:"...on PullRequest"`
} `graphql:"node(id: $id)"`
}

variables := map[string]interface{}{
"id": githubv4.ID(pr.ID),
"endCursor": githubv4.String(pr.Commits.PageInfo.EndCursor),
}

gql := api.NewClientFromHTTP(client)

for {
var query response
err := gql.Query(repo.RepoHost(), "CommitsForPullRequest", &query, variables)
if err != nil {
return err
}

pr.Commits.Nodes = append(pr.Commits.Nodes, query.Node.PullRequest.Commits.Nodes...)
pr.Commits.TotalCount = len(pr.Commits.Nodes)

if !query.Node.PullRequest.Commits.PageInfo.HasNextPage {
break
}
variables["endCursor"] = githubv4.String(query.Node.PullRequest.Commits.PageInfo.EndCursor)
}

pr.Commits.PageInfo.HasNextPage = false
return nil
}

func preloadPrChecks(client *http.Client, repo ghrepo.Interface, pr *api.PullRequest) error {
if len(pr.StatusCheckRollup.Nodes) == 0 {
return nil
Expand Down
147 changes: 147 additions & 0 deletions pkg/cmd/pr/shared/finder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/url"
"testing"

"github.com/cli/cli/v2/api"
ghContext "github.com/cli/cli/v2/context"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/ghrepo"
Expand Down Expand Up @@ -1008,3 +1009,149 @@ func (s *stubProgressIndicator) StartProgressIndicator() {
func (s *stubProgressIndicator) StopProgressIndicator() {
s.stopCalled = true
}

func TestPreloadPrFiles(t *testing.T) {
tests := []struct {
name string
setupPR func() *api.PullRequest
httpStub func(*httpmock.Registry)
wantPaths []string
}{
{
name: "no pagination needed",
setupPR: func() *api.PullRequest {
pr := &api.PullRequest{}
pr.Files.Nodes = []api.PullRequestFile{
{Path: "file1.go"},
}
return pr
},
wantPaths: []string{"file1.go"},
},
{
name: "paginates through additional pages",
setupPR: func() *api.PullRequest {
pr := &api.PullRequest{ID: "PR_123"}
pr.Files.Nodes = []api.PullRequestFile{
{Path: "file1.go"},
}
pr.Files.PageInfo.HasNextPage = true
pr.Files.PageInfo.EndCursor = "page1"
return pr
},
httpStub: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query FilesForPullRequest\b`),
httpmock.StringResponse(`{"data":{"node":{"files":{
"nodes":[{"path":"file2.go","additions":5,"deletions":2,"changeType":"ADDED"}],
"pageInfo":{"hasNextPage":true,"endCursor":"page2"}}}}}`),
)
r.Register(
httpmock.GraphQL(`query FilesForPullRequest\b`),
httpmock.StringResponse(`{"data":{"node":{"files":{
"nodes":[{"path":"file3.go","additions":1,"deletions":0,"changeType":"ADDED"}],
"pageInfo":{"hasNextPage":false,"endCursor":""}}}}}`),
)
},
wantPaths: []string{"file1.go", "file2.go", "file3.go"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
defer reg.Verify(t)
if tt.httpStub != nil {
tt.httpStub(reg)
}

client := &http.Client{Transport: reg}
repo := ghrepo.New("OWNER", "REPO")
pr := tt.setupPR()

err := preloadPrFiles(client, repo, pr)
require.NoError(t, err)

var gotPaths []string
for _, f := range pr.Files.Nodes {
gotPaths = append(gotPaths, f.Path)
}
assert.Equal(t, tt.wantPaths, gotPaths)
assert.False(t, pr.Files.PageInfo.HasNextPage)
})
}
}

func TestPreloadPrCommits(t *testing.T) {
tests := []struct {
name string
setupPR func() *api.PullRequest
httpStub func(*httpmock.Registry)
wantOIDs []string
}{
{
name: "no pagination needed",
setupPR: func() *api.PullRequest {
pr := &api.PullRequest{}
pr.Commits.TotalCount = 1
pr.Commits.Nodes = []api.PullRequestCommit{
{Commit: api.PullRequestCommitCommit{OID: "aaa"}},
}
return pr
},
wantOIDs: []string{"aaa"},
},
{
name: "paginates through additional pages",
setupPR: func() *api.PullRequest {
pr := &api.PullRequest{ID: "PR_123"}
pr.Commits.Nodes = []api.PullRequestCommit{
{Commit: api.PullRequestCommitCommit{OID: "aaa"}},
}
pr.Commits.PageInfo.HasNextPage = true
pr.Commits.PageInfo.EndCursor = "page1"
return pr
},
httpStub: func(r *httpmock.Registry) {
r.Register(
httpmock.GraphQL(`query CommitsForPullRequest\b`),
httpmock.StringResponse(`{"data":{"node":{"commits":{
"nodes":[{"commit":{"oid":"bbb","authors":{"nodes":[]},"messageHeadline":"second","messageBody":"","committedDate":"2025-01-01T00:00:00Z","authoredDate":"2025-01-01T00:00:00Z"}}],
"pageInfo":{"hasNextPage":true,"endCursor":"page2"}}}}}`),
)
r.Register(
httpmock.GraphQL(`query CommitsForPullRequest\b`),
httpmock.StringResponse(`{"data":{"node":{"commits":{
"nodes":[{"commit":{"oid":"ccc","authors":{"nodes":[]},"messageHeadline":"third","messageBody":"","committedDate":"2025-01-01T00:00:00Z","authoredDate":"2025-01-01T00:00:00Z"}}],
"pageInfo":{"hasNextPage":false,"endCursor":""}}}}}`),
)
},
wantOIDs: []string{"aaa", "bbb", "ccc"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
defer reg.Verify(t)
if tt.httpStub != nil {
tt.httpStub(reg)
}

client := &http.Client{Transport: reg}
repo := ghrepo.New("OWNER", "REPO")
pr := tt.setupPR()

err := preloadPrCommits(client, repo, pr)
require.NoError(t, err)

var gotOIDs []string
for _, c := range pr.Commits.Nodes {
gotOIDs = append(gotOIDs, c.Commit.OID)
}
assert.Equal(t, tt.wantOIDs, gotOIDs)
assert.Equal(t, len(tt.wantOIDs), pr.Commits.TotalCount)
assert.False(t, pr.Commits.PageInfo.HasNextPage)
})
}
}