From 0193bd50ea300dab837137eab59b714f9be6e787 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 07:23:21 +0000 Subject: [PATCH] feat(scm): add issue iterators Co-Authored-By: Virgil --- forge/issues.go | 49 ++++++++++++++++++++++++++++++++++++++++++++ forge/issues_test.go | 14 +++++++++++++ gitea/issues.go | 48 +++++++++++++++++++++++++++++++++++++++++++ gitea/issues_test.go | 14 +++++++++++++ 4 files changed, 125 insertions(+) diff --git a/forge/issues.go b/forge/issues.go index c9d4620..2261c92 100644 --- a/forge/issues.go +++ b/forge/issues.go @@ -67,6 +67,55 @@ func (c *Client) ListIssues(owner, repo string, opts ListIssuesOpts) ([]*forgejo return all, nil } +// ListIssuesIter returns an iterator over issues for the given repository. +// Usage: ListIssuesIter(...) +func (c *Client) ListIssuesIter(owner, repo string, opts ListIssuesOpts) iter.Seq2[*forgejo.Issue, error] { + state := forgejo.StateOpen + switch opts.State { + case "closed": + state = forgejo.StateClosed + case "all": + state = forgejo.StateAll + } + + limit := opts.Limit + if limit == 0 { + limit = 50 + } + + page := opts.Page + if page == 0 { + page = 1 + } + + return func(yield func(*forgejo.Issue, error) bool) { + for { + issues, resp, err := c.api.ListRepoIssues(owner, repo, forgejo.ListIssueOption{ + ListOptions: forgejo.ListOptions{Page: page, PageSize: limit}, + State: state, + Type: forgejo.IssueTypeIssue, + Labels: opts.Labels, + }) + if err != nil { + yield(nil, log.E("forge.ListIssues", "failed to list issues", err)) + return + } + for _, issue := range issues { + if !yield(issue, nil) { + return + } + } + if len(issues) < limit || len(issues) == 0 { + break + } + if resp != nil && resp.LastPage > 0 && page >= resp.LastPage { + break + } + page++ + } + } +} + // GetIssue returns a single issue by number. // Usage: GetIssue(...) func (c *Client) GetIssue(owner, repo string, number int64) (*forgejo.Issue, error) { diff --git a/forge/issues_test.go b/forge/issues_test.go index 823f45a..f0fa8de 100644 --- a/forge/issues_test.go +++ b/forge/issues_test.go @@ -62,6 +62,20 @@ func TestClient_ListIssues_Good_Paginates_Good(t *testing.T) { assert.Equal(t, "Issue 2", issues[1].Title) } +func TestClient_ListIssuesIter_Good_Paginates_Good(t *testing.T) { + client, srv := newPaginatedIssuesClient(t) + defer srv.Close() + + var titles []string + for issue, err := range client.ListIssuesIter("test-org", "org-repo", ListIssuesOpts{Limit: 1}) { + require.NoError(t, err) + titles = append(titles, issue.Title) + } + + require.Len(t, titles, 2) + assert.Equal(t, []string{"Issue 1", "Issue 2"}, titles) +} + func TestClient_ListIssues_Good_StateMapping_Good(t *testing.T) { tests := []struct { name string diff --git a/gitea/issues.go b/gitea/issues.go index e6c033a..a60344b 100644 --- a/gitea/issues.go +++ b/gitea/issues.go @@ -63,6 +63,54 @@ func (c *Client) ListIssues(owner, repo string, opts ListIssuesOpts) ([]*gitea.I return all, nil } +// ListIssuesIter returns an iterator over issues for the given repository. +// Usage: ListIssuesIter(...) +func (c *Client) ListIssuesIter(owner, repo string, opts ListIssuesOpts) iter.Seq2[*gitea.Issue, error] { + state := gitea.StateOpen + switch opts.State { + case "closed": + state = gitea.StateClosed + case "all": + state = gitea.StateAll + } + + limit := opts.Limit + if limit == 0 { + limit = 50 + } + + page := opts.Page + if page == 0 { + page = 1 + } + + return func(yield func(*gitea.Issue, error) bool) { + for { + issues, resp, err := c.api.ListRepoIssues(owner, repo, gitea.ListIssueOption{ + ListOptions: gitea.ListOptions{Page: page, PageSize: limit}, + State: state, + Type: gitea.IssueTypeIssue, + }) + if err != nil { + yield(nil, log.E("gitea.ListIssues", "failed to list issues", err)) + return + } + for _, issue := range issues { + if !yield(issue, nil) { + return + } + } + if len(issues) < limit || len(issues) == 0 { + break + } + if resp != nil && resp.LastPage > 0 && page >= resp.LastPage { + break + } + page++ + } + } +} + // GetIssue returns a single issue by number. // Usage: GetIssue(...) func (c *Client) GetIssue(owner, repo string, number int64) (*gitea.Issue, error) { diff --git a/gitea/issues_test.go b/gitea/issues_test.go index 59e2b91..2725bf6 100644 --- a/gitea/issues_test.go +++ b/gitea/issues_test.go @@ -62,6 +62,20 @@ func TestClient_ListIssues_Good_Paginates_Good(t *testing.T) { assert.Equal(t, "Issue 2", issues[1].Title) } +func TestClient_ListIssuesIter_Good_Paginates_Good(t *testing.T) { + client, srv := newPaginatedIssuesClient(t) + defer srv.Close() + + var titles []string + for issue, err := range client.ListIssuesIter("test-org", "org-repo", ListIssuesOpts{Limit: 1}) { + require.NoError(t, err) + titles = append(titles, issue.Title) + } + + require.Len(t, titles, 2) + assert.Equal(t, []string{"Issue 1", "Issue 2"}, titles) +} + func TestClient_ListIssues_Good_StateMapping_Good(t *testing.T) { tests := []struct { name string