From 697bfde215d41451d8ddcc49ebbd04f4c66efe85 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 09:42:41 +0000 Subject: [PATCH] feat(forge): add org label iterator Co-Authored-By: Virgil --- docs/architecture.md | 2 +- forge/labels.go | 34 ++++++++++++++++++++++++++++++++++ forge/labels_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index 0a72fce..509826b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -89,7 +89,7 @@ The `gitea/` package mirrors this using `GITEA_URL`/`GITEA_TOKEN` and `gitea.*` | `client.go` | `New`, `NewFromConfig`, `GetCurrentUser`, `ForkRepo`, `CreatePullRequest` | | `repos.go` | `ListOrgRepos`, `ListOrgReposIter`, `ListUserRepos`, `ListUserReposIter`, `GetRepo`, `CreateOrgRepo`, `DeleteRepo`, `MigrateRepo` | | `issues.go` | `ListIssues`, `ListIssuesIter`, `GetIssue`, `CreateIssue`, `EditIssue`, `AssignIssue`, `ListPullRequests`, `ListPullRequestsIter`, `GetPullRequest`, `CreateIssueComment`, `ListIssueComments`, `ListIssueCommentsIter`, `CloseIssue` | -| `labels.go` | `ListOrgLabels`, `ListRepoLabels`, `ListRepoLabelsIter`, `CreateRepoLabel`, `GetLabelByName`, `EnsureLabel`, `AddIssueLabels`, `RemoveIssueLabel` | +| `labels.go` | `ListOrgLabels`, `ListOrgLabelsIter`, `ListRepoLabels`, `ListRepoLabelsIter`, `CreateRepoLabel`, `GetLabelByName`, `EnsureLabel`, `AddIssueLabels`, `RemoveIssueLabel` | | `prs.go` | `MergePullRequest`, `SetPRDraft`, `ListPRReviews`, `GetCombinedStatus`, `DismissReview` | | `webhooks.go` | `CreateRepoWebhook`, `ListRepoWebhooks` | | `orgs.go` | `ListMyOrgs`, `GetOrg`, `CreateOrg` | diff --git a/forge/labels.go b/forge/labels.go index 3972279..f417821 100644 --- a/forge/labels.go +++ b/forge/labels.go @@ -48,6 +48,40 @@ func (c *Client) ListOrgLabels(org string) ([]*forgejo.Label, error) { return all, nil } +// ListOrgLabelsIter returns an iterator over unique labels across repos in the given organisation. +// Note: The Forgejo SDK does not have a dedicated org-level labels endpoint. +// Labels are yielded in first-seen order across repositories and deduplicated by name. +// Usage: ListOrgLabelsIter(...) +func (c *Client) ListOrgLabelsIter(org string) iter.Seq2[*forgejo.Label, error] { + return func(yield func(*forgejo.Label, error) bool) { + seen := make(map[string]struct{}) + + for repo, err := range c.ListOrgReposIter(org) { + if err != nil { + yield(nil, log.E("forge.ListOrgLabels", "failed to list org repos", err)) + return + } + + for label, err := range c.ListRepoLabelsIter(repo.Owner.UserName, repo.Name) { + if err != nil { + yield(nil, log.E("forge.ListOrgLabels", "failed to list repo labels", err)) + return + } + + key := strings.ToLower(label.Name) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + + if !yield(label, nil) { + return + } + } + } + } +} + // ListRepoLabels returns all labels for a repository. // Usage: ListRepoLabels(...) func (c *Client) ListRepoLabels(owner, repo string) ([]*forgejo.Label, error) { diff --git a/forge/labels_test.go b/forge/labels_test.go index 5d8c7b2..2d781b9 100644 --- a/forge/labels_test.go +++ b/forge/labels_test.go @@ -161,6 +161,31 @@ func TestClient_ListOrgLabels_Good(t *testing.T) { assert.Equal(t, "documentation", labels[2].Name) } +func TestClient_ListOrgLabelsIter_Good(t *testing.T) { + client, srv := newTestClient(t) + defer srv.Close() + + var names []string + for label, err := range client.ListOrgLabelsIter("test-org") { + require.NoError(t, err) + names = append(names, label.Name) + } + + require.Len(t, names, 3) + assert.Equal(t, []string{"bug", "feature", "documentation"}, names) +} + +func TestClient_ListOrgLabelsIter_Bad_ServerError_Good(t *testing.T) { + client, srv := newErrorServer(t) + defer srv.Close() + + for _, err := range client.ListOrgLabelsIter("test-org") { + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to list org repos") + break + } +} + func TestClient_ListOrgLabels_Bad_ServerError_Good(t *testing.T) { client, srv := newErrorServer(t) defer srv.Close()