From 5d150c0c499616eda7ce88e6c0379ba65973257d Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Thu, 2 Apr 2026 18:17:56 +0530 Subject: [PATCH 1/5] add custom name and slice to be used for iteration support to gen-iterators.go --- github/gen-iterators.go | 42 +++++-- github/github-iterators.go | 105 ++++++++++++++++ github/github-iterators_test.go | 216 ++++++++++++++++++++++++++++++++ 3 files changed, 356 insertions(+), 7 deletions(-) diff --git a/github/gen-iterators.go b/github/gen-iterators.go index 5830f97ddae..410c1fb7294 100644 --- a/github/gen-iterators.go +++ b/github/gen-iterators.go @@ -162,6 +162,20 @@ var useCursorPagination = map[string]bool{ "RepositoriesService.ListHookDeliveries": true, } +// customNames provides custom names for iterator methods where the default methodName + "Iter" would be confusing. +var customNames = map[string]string{ + "RepositoriesService.GetCommit": "ListCommitFiles", + "RepositoriesService.CompareCommits": "ListCommitComparisonFiles", + "RepositoriesService.GetCombinedStatus": "ListCombinedStatus", +} + +// sliceToBeUsedForIteration identifies methods where the wrapper struct contains multiple []*T fields, +// and specifies which field should be used for iteration. +var sliceToBeUsedForIteration = map[string]string{ + "RepositoriesService.GetCommit": "Files", + "RepositoriesService.CompareCommits": "Files", +} + // customTestJSON maps method names to the JSON response they expect in tests. // This is needed for methods that internally unmarshal a wrapper struct // even though they return a slice. @@ -301,7 +315,9 @@ func (t *templateData) processMethods(f *ast.File) error { continue } - if !fd.Name.IsExported() || !strings.HasPrefix(fd.Name.Name, "List") { + methodKey := strings.TrimPrefix(typeToString(fd.Recv.List[0].Type), "*") + "." + fd.Name.Name + + if !fd.Name.IsExported() || (!strings.HasPrefix(fd.Name.Name, "List") && customNames[methodKey] == "") { continue } @@ -448,6 +464,13 @@ func (t *templateData) collectMethodInfo(fd *ast.FuncDecl) (*methodInfo, bool) { }, true } +func getIterName(methodInfo *methodInfo, methodName string) string { + if customName, ok := customNames[methodInfo.RecvType+"."+methodName]; ok { + return customName + "Iter" + } + return methodName + "Iter" +} + func (t *templateData) processReturnArrayType(fd *ast.FuncDecl, sliceRet *ast.ArrayType, methodInfo *methodInfo) { testJSON, emptyReturnValue := "[]", "{}" if val, ok := customTestJSON[fd.Name.Name]; ok { @@ -467,7 +490,7 @@ func (t *templateData) processReturnArrayType(fd *ast.FuncDecl, sliceRet *ast.Ar RecvVar: methodInfo.RecvVar, ClientField: methodInfo.ClientField, MethodName: fd.Name.Name, - IterMethod: fd.Name.Name + "Iter", + IterMethod: getIterName(methodInfo, fd.Name.Name), Args: methodInfo.Args, CallArgs: methodInfo.CallArgs, TestCallArgs: methodInfo.TestCallArgs, @@ -496,7 +519,7 @@ func (t *templateData) processReturnStarExpr(fd *ast.FuncDecl, starRet *ast.Star return } - itemsField, itemsType, ok := findSinglePointerSliceField(wrapperDef) + itemsField, itemsType, ok := findSinglePointerSliceField(wrapperDef, methodInfo.RecvType+"."+fd.Name.Name) if !ok { logf("Skipping %v.%v: wrapper %v does not contain exactly one []*T field", methodInfo.RecvTypeRaw, fd.Name.Name, wrapperType) return @@ -525,7 +548,7 @@ func (t *templateData) processReturnStarExpr(fd *ast.FuncDecl, starRet *ast.Star RecvVar: methodInfo.RecvVar, ClientField: methodInfo.ClientField, MethodName: fd.Name.Name, - IterMethod: fd.Name.Name + "Iter", + IterMethod: getIterName(methodInfo, fd.Name.Name), Args: methodInfo.Args, CallArgs: methodInfo.CallArgs, TestCallArgs: methodInfo.TestCallArgs, @@ -547,17 +570,22 @@ func (t *templateData) processReturnStarExpr(fd *ast.FuncDecl, starRet *ast.Star t.Methods = append(t.Methods, m) } -func findSinglePointerSliceField(sd *structDef) (fieldName, fieldType string, ok bool) { +func findSinglePointerSliceField(sd *structDef, methodKey string) (fieldName, fieldType string, ok bool) { matches := []string{} for name, typeStr := range sd.Fields { if strings.HasPrefix(typeStr, "[]*") { matches = append(matches, name) } } - if len(matches) != 1 { + if len(matches) != 1 && sliceToBeUsedForIteration[methodKey] == "" { return "", "", false } - fieldName = matches[0] + + if custom, ok := sliceToBeUsedForIteration[methodKey]; ok { + fieldName = custom + } else { + fieldName = matches[0] + } return fieldName, sd.Fields[fieldName], true } diff --git a/github/github-iterators.go b/github/github-iterators.go index f33c9edc47c..5568ca79236 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -5316,6 +5316,111 @@ func (s *ReactionsService) ListTeamDiscussionReactionsIter(ctx context.Context, } } +// ListCommitComparisonFilesIter returns an iterator that paginates through all results of CompareCommits. +func (s *RepositoriesService) ListCommitComparisonFilesIter(ctx context.Context, owner string, repo string, base string, head string, opts *ListOptions) iter.Seq2[*CommitFile, error] { + return func(yield func(*CommitFile, 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.CompareCommits(ctx, owner, repo, base, head, opts) + if err != nil { + yield(nil, err) + return + } + + var iterItems []*CommitFile + if results != nil { + iterItems = results.Files + } + for _, item := range iterItems { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + } +} + +// ListCombinedStatusIter returns an iterator that paginates through all results of GetCombinedStatus. +func (s *RepositoriesService) ListCombinedStatusIter(ctx context.Context, owner string, repo string, ref string, opts *ListOptions) iter.Seq2[*RepoStatus, error] { + return func(yield func(*RepoStatus, 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.GetCombinedStatus(ctx, owner, repo, ref, opts) + if err != nil { + yield(nil, err) + return + } + + var iterItems []*RepoStatus + if results != nil { + iterItems = results.Statuses + } + for _, item := range iterItems { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + } +} + +// ListCommitFilesIter returns an iterator that paginates through all results of GetCommit. +func (s *RepositoriesService) ListCommitFilesIter(ctx context.Context, owner string, repo string, sha string, opts *ListOptions) iter.Seq2[*CommitFile, error] { + return func(yield func(*CommitFile, 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.GetCommit(ctx, owner, repo, sha, opts) + if err != nil { + yield(nil, err) + return + } + + var iterItems []*CommitFile + if results != nil { + iterItems = results.Files + } + for _, item := range iterItems { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + } +} + // ListIter returns an iterator that paginates through all results of List. func (s *RepositoriesService) ListIter(ctx context.Context, user string, opts *RepositoryListOptions) iter.Seq2[*Repository, error] { return func(yield func(*Repository, error) bool) { diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index c48f02bcefc..f64e39c9fde 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -11751,6 +11751,222 @@ func TestReactionsService_ListTeamDiscussionReactionsIter(t *testing.T) { } } +func TestRepositoriesService_ListCommitComparisonFilesIter(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, `{"files": [{},{},{}]}`) + case 2: + fmt.Fprint(w, `{"files": [{},{},{},{}]}`) + case 3: + fmt.Fprint(w, `{"files": [{},{}]}`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `{"files": [{},{}]}`) + } + }) + + iter := client.Repositories.ListCommitComparisonFilesIter(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.Repositories.ListCommitComparisonFilesIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListOptions{} + iter = client.Repositories.ListCommitComparisonFilesIter(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.Repositories.ListCommitComparisonFilesIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Repositories.ListCommitComparisonFilesIter(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.Repositories.ListCommitComparisonFilesIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Repositories.ListCommitComparisonFilesIter(t.Context(), "", "", "", "", nil) + gotItems = 0 + iter(func(item *CommitFile, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Repositories.ListCommitComparisonFilesIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + +func TestRepositoriesService_ListCombinedStatusIter(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, `{"statuses": [{},{},{}]}`) + case 2: + fmt.Fprint(w, `{"statuses": [{},{},{},{}]}`) + case 3: + fmt.Fprint(w, `{"statuses": [{},{}]}`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `{"statuses": [{},{}]}`) + } + }) + + iter := client.Repositories.ListCombinedStatusIter(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.Repositories.ListCombinedStatusIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListOptions{} + iter = client.Repositories.ListCombinedStatusIter(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.Repositories.ListCombinedStatusIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Repositories.ListCombinedStatusIter(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.Repositories.ListCombinedStatusIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Repositories.ListCombinedStatusIter(t.Context(), "", "", "", nil) + gotItems = 0 + iter(func(item *RepoStatus, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Repositories.ListCombinedStatusIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + +func TestRepositoriesService_ListCommitFilesIter(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, `{"files": [{},{},{}]}`) + case 2: + fmt.Fprint(w, `{"files": [{},{},{},{}]}`) + case 3: + fmt.Fprint(w, `{"files": [{},{}]}`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `{"files": [{},{}]}`) + } + }) + + iter := client.Repositories.ListCommitFilesIter(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.Repositories.ListCommitFilesIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListOptions{} + iter = client.Repositories.ListCommitFilesIter(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.Repositories.ListCommitFilesIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Repositories.ListCommitFilesIter(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.Repositories.ListCommitFilesIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Repositories.ListCommitFilesIter(t.Context(), "", "", "", nil) + gotItems = 0 + iter(func(item *CommitFile, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Repositories.ListCommitFilesIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestRepositoriesService_ListIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) From 19e32243b844036e783c327bb9a84f8739716029 Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Thu, 2 Apr 2026 20:11:51 +0530 Subject: [PATCH 2/5] feedback --- github/gen-iterators.go | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/github/gen-iterators.go b/github/gen-iterators.go index 410c1fb7294..9c253ca0d10 100644 --- a/github/gen-iterators.go +++ b/github/gen-iterators.go @@ -519,10 +519,22 @@ func (t *templateData) processReturnStarExpr(fd *ast.FuncDecl, starRet *ast.Star return } - itemsField, itemsType, ok := findSinglePointerSliceField(wrapperDef, methodInfo.RecvType+"."+fd.Name.Name) - if !ok { - logf("Skipping %v.%v: wrapper %v does not contain exactly one []*T field", methodInfo.RecvTypeRaw, fd.Name.Name, wrapperType) - return + var itemsField string + var itemsType string + + if customNames, ok := sliceToBeUsedForIteration[methodInfo.RecvType+"."+fd.Name.Name]; ok { + itemsField = customNames + itemsType, ok = wrapperDef.Fields[itemsField] + if !ok || !strings.HasPrefix(itemsType, "[]*") { + logf("Skipping %v.%v: specified items field %v not found or not of type []*T in wrapper %v", methodInfo.RecvTypeRaw, fd.Name.Name, itemsField, wrapperType) + return + } + } else { + itemsField, itemsType, ok = findSinglePointerSliceField(wrapperDef) + if !ok { + logf("Skipping %v.%v: wrapper %v does not contain exactly one []*T field", methodInfo.RecvTypeRaw, fd.Name.Name, wrapperType) + return + } } testJSON, emptyReturnValue := "[]", "{}" @@ -570,22 +582,17 @@ func (t *templateData) processReturnStarExpr(fd *ast.FuncDecl, starRet *ast.Star t.Methods = append(t.Methods, m) } -func findSinglePointerSliceField(sd *structDef, methodKey string) (fieldName, fieldType string, ok bool) { +func findSinglePointerSliceField(sd *structDef) (fieldName, fieldType string, ok bool) { matches := []string{} for name, typeStr := range sd.Fields { if strings.HasPrefix(typeStr, "[]*") { matches = append(matches, name) } } - if len(matches) != 1 && sliceToBeUsedForIteration[methodKey] == "" { + if len(matches) != 1 { return "", "", false } - - if custom, ok := sliceToBeUsedForIteration[methodKey]; ok { - fieldName = custom - } else { - fieldName = matches[0] - } + fieldName = matches[0] return fieldName, sd.Fields[fieldName], true } From 9f4ff1f23a302d8808a9822391ab44d5f96ce25d Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Thu, 2 Apr 2026 20:16:03 +0530 Subject: [PATCH 3/5] fix typo --- github/gen-iterators.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/gen-iterators.go b/github/gen-iterators.go index 9c253ca0d10..0a7271d8b80 100644 --- a/github/gen-iterators.go +++ b/github/gen-iterators.go @@ -522,8 +522,8 @@ func (t *templateData) processReturnStarExpr(fd *ast.FuncDecl, starRet *ast.Star var itemsField string var itemsType string - if customNames, ok := sliceToBeUsedForIteration[methodInfo.RecvType+"."+fd.Name.Name]; ok { - itemsField = customNames + if field, ok := sliceToBeUsedForIteration[methodInfo.RecvType+"."+fd.Name.Name]; ok { + itemsField = field itemsType, ok = wrapperDef.Fields[itemsField] if !ok || !strings.HasPrefix(itemsType, "[]*") { logf("Skipping %v.%v: specified items field %v not found or not of type []*T in wrapper %v", methodInfo.RecvTypeRaw, fd.Name.Name, itemsField, wrapperType) From 913eaa114658b2df1e87f98e5fce767280aa9a7b Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Thu, 2 Apr 2026 20:31:26 +0530 Subject: [PATCH 4/5] feedback --- github/gen-iterators.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/github/gen-iterators.go b/github/gen-iterators.go index 0a7271d8b80..506c6dd6bc9 100644 --- a/github/gen-iterators.go +++ b/github/gen-iterators.go @@ -519,22 +519,17 @@ func (t *templateData) processReturnStarExpr(fd *ast.FuncDecl, starRet *ast.Star return } - var itemsField string - var itemsType string + var itemsField, itemsType string if field, ok := sliceToBeUsedForIteration[methodInfo.RecvType+"."+fd.Name.Name]; ok { itemsField = field - itemsType, ok = wrapperDef.Fields[itemsField] - if !ok || !strings.HasPrefix(itemsType, "[]*") { + if itemsType, ok = wrapperDef.Fields[itemsField]; !ok || !strings.HasPrefix(itemsType, "[]*") { logf("Skipping %v.%v: specified items field %v not found or not of type []*T in wrapper %v", methodInfo.RecvTypeRaw, fd.Name.Name, itemsField, wrapperType) return } - } else { - itemsField, itemsType, ok = findSinglePointerSliceField(wrapperDef) - if !ok { - logf("Skipping %v.%v: wrapper %v does not contain exactly one []*T field", methodInfo.RecvTypeRaw, fd.Name.Name, wrapperType) - return - } + } else if itemsField, itemsType, ok = findSinglePointerSliceField(wrapperDef); !ok { + logf("Skipping %v.%v: wrapper %v does not contain exactly one []*T field", methodInfo.RecvTypeRaw, fd.Name.Name, wrapperType) + return } testJSON, emptyReturnValue := "[]", "{}" From 1c55146163fd099b584ab33de87c2ac952a04dbf Mon Sep 17 00:00:00 2001 From: Dhananjay Mishra Date: Thu, 2 Apr 2026 20:48:06 +0530 Subject: [PATCH 5/5] feedback --- github/gen-iterators.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/github/gen-iterators.go b/github/gen-iterators.go index 506c6dd6bc9..a623cf1c151 100644 --- a/github/gen-iterators.go +++ b/github/gen-iterators.go @@ -316,7 +316,6 @@ func (t *templateData) processMethods(f *ast.File) error { } methodKey := strings.TrimPrefix(typeToString(fd.Recv.List[0].Type), "*") + "." + fd.Name.Name - if !fd.Name.IsExported() || (!strings.HasPrefix(fd.Name.Name, "List") && customNames[methodKey] == "") { continue } @@ -520,7 +519,6 @@ func (t *templateData) processReturnStarExpr(fd *ast.FuncDecl, starRet *ast.Star } var itemsField, itemsType string - if field, ok := sliceToBeUsedForIteration[methodInfo.RecvType+"."+fd.Name.Name]; ok { itemsField = field if itemsType, ok = wrapperDef.Fields[itemsField]; !ok || !strings.HasPrefix(itemsType, "[]*") {