From 03aa64233f966ab21e4b139c33898f4286e4720e Mon Sep 17 00:00:00 2001 From: bircni Date: Mon, 12 Oct 2026 09:55:36 +0200 Subject: [PATCH] refactor(completion): complete assignees and tags through the SDK Completion fetched repository assignees and tags with raw GETs. List them through new Repositories.Assignees and Repositories.Tags service methods on the SDK, and remove the raw fetch helper. Signed-off-by: bircni --- internal/app/completion.go | 30 ++++---------------------- internal/services/repositories.go | 30 +++++++++++++++++++++++--- internal/services/repositories_test.go | 18 +++++++++++++--- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/internal/app/completion.go b/internal/app/completion.go index c72ce160..f596b3f6 100644 --- a/internal/app/completion.go +++ b/internal/app/completion.go @@ -5,10 +5,8 @@ package app import ( "context" - "encoding/json" "errors" "maps" - "net/http" "slices" "strconv" "strings" @@ -18,7 +16,6 @@ import ( "gitea.dev/tea/internal/services" "gitea.dev/tea/internal/settings" "gitea.dev/tea/internal/terminal" - "gitea.dev/tea/internal/transport" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -337,16 +334,6 @@ func (a *application) completionTarget(ctx context.Context, requireRepo bool) (t return t, nil } -// fetchJSON decodes the response to a GET of endpoint, for values that no service lists. -func fetchJSON(ctx context.Context, client *transport.Client, endpoint string, out any) error { - return services.API{Client: client}.Visit(ctx, services.APIOptions{Method: http.MethodGet, Endpoint: endpoint}, func(response *http.Response) error { - if response.StatusCode != http.StatusOK { - return nil // Visit reports the status. - } - return json.NewDecoder(response.Body).Decode(out) - }) -} - // completeIssues completes issue numbers in state (open, closed or all) with their titles. func (a *application) completeIssues(state string) cobra.CompletionFunc { return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) { @@ -390,13 +377,8 @@ func (a *application) completeMilestones() cobra.CompletionFunc { // completeUsers completes comma-separated usernames that can be assigned in the repository. func (a *application) completeUsers() cobra.CompletionFunc { return commaSeparated(a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) { - type user struct { - Login string `json:"login"` - FullName string `json:"full_name"` - } - var users []user - err := fetchJSON(ctx, t.client, "repos/"+t.Repo+"/assignees", &users) - return describe(users, func(item user) (string, string) { return item.Login, item.FullName }), err + users, err := services.Repositories{Client: t.client}.Assignees(ctx, t.Repo) + return describe(users, func(item services.Assignee) (string, string) { return item.Username, item.FullName }), err })) } @@ -416,12 +398,8 @@ func (a *application) completeBranches() cobra.CompletionFunc { // completeTags completes the repository's tags. func (a *application) completeTags() cobra.CompletionFunc { return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) { - type tag struct { - Name string `json:"name"` - } - var tags []tag - err := fetchJSON(ctx, t.client, "repos/"+t.Repo+"/tags?limit="+strconv.Itoa(completionLimit), &tags) - return describe(tags, func(item tag) (string, string) { return item.Name, "" }), err + tags, err := services.Repositories{Client: t.client}.Tags(ctx, t.Repo, completionLimit, 1) + return describe(tags, func(name string) (string, string) { return name, "" }), err }) } diff --git a/internal/services/repositories.go b/internal/services/repositories.go index 18596319..e033d57f 100644 --- a/internal/services/repositories.go +++ b/internal/services/repositories.go @@ -145,15 +145,39 @@ func (s Repositories) viewPermissions(ctx context.Context, repo string) (Reposit return RepositoryPermissions{Admin: item.Permissions.Admin, Push: item.Permissions.Push, Pull: item.Permissions.Pull}, nil } -// assignees lists usernames that can be assigned on repository issues. -func (s Repositories) assignees(ctx context.Context, repo string) ([]string, error) { +// Assignee is a user that can be assigned on repository issues. +type Assignee struct { + Username string `json:"username"` + FullName string `json:"full_name"` +} + +// Assignees lists the users that can be assigned on repository issues. +func (s Repositories) Assignees(ctx context.Context, repo string) ([]Assignee, error) { users, err := repoCall(ctx, s.Client, repo, func(ctx context.Context, c *gitea.Client, owner, name string) ([]*gitea.User, *gitea.Response, error) { return c.Repositories.GetAssignees(ctx, owner, name) }) + return convertAll(users, func(user *gitea.User) Assignee { return Assignee{Username: user.UserName, FullName: user.FullName} }), err +} + +// assignees lists usernames that can be assigned on repository issues. +func (s Repositories) assignees(ctx context.Context, repo string) ([]string, error) { + users, err := s.Assignees(ctx, repo) if err != nil { return nil, err } - return convertAll(users, login), nil + names := make([]string, len(users)) + for i, user := range users { + names[i] = user.Username + } + return names, nil +} + +// Tags lists a page of the repository's tag names. +func (s Repositories) Tags(ctx context.Context, repo string, limit, page int) ([]string, error) { + tags, err := repoCall(ctx, s.Client, repo, func(ctx context.Context, c *gitea.Client, owner, name string) ([]*gitea.Tag, *gitea.Response, error) { + return c.Repositories.ListRepoTags(ctx, owner, name, gitea.ListRepoTagsOptions{ListOptions: listOptions(limit, page)}) + }) + return convertAll(tags, func(tag *gitea.Tag) string { return tag.Name }), err } // Create creates a repository for the account or the named organization. diff --git a/internal/services/repositories_test.go b/internal/services/repositories_test.go index ad77d37d..8b4dd2c4 100644 --- a/internal/services/repositories_test.go +++ b/internal/services/repositories_test.go @@ -51,10 +51,16 @@ func TestRepositoryForkSendsOrganizationOnlyWhenSelected(t *testing.T) { } } -func TestRepositoryAssigneesAndMissingPermissions(t *testing.T) { +func TestRepositoryAssigneesTagsAndMissingPermissions(t *testing.T) { client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/api/v1/repos/alice/project/assignees" { - _, _ = w.Write([]byte(`[{"login":"alice"},{"login":"bob"}]`)) + switch r.URL.Path { + case "/api/v1/repos/alice/project/assignees": + _, _ = w.Write([]byte(`[{"login":"alice","full_name":"Alice A."},{"login":"bob"}]`)) + return + case "/api/v1/repos/alice/project/tags": + assert.Equal(t, "2", r.URL.Query().Get("page")) + assert.Equal(t, "5", r.URL.Query().Get("limit")) + _, _ = w.Write([]byte(`[{"name":"v1.0"},{"name":"v0.9"}]`)) return } _, _ = w.Write([]byte(`{"id":1}`)) @@ -63,6 +69,12 @@ func TestRepositoryAssigneesAndMissingPermissions(t *testing.T) { names, err := service.assignees(context.Background(), "alice/project") require.NoError(t, err) require.Equal(t, []string{"alice", "bob"}, names) + users, err := service.Assignees(context.Background(), "alice/project") + require.NoError(t, err) + require.Equal(t, []Assignee{{Username: "alice", FullName: "Alice A."}, {Username: "bob"}}, users) + tags, err := service.Tags(context.Background(), "alice/project", 5, 2) + require.NoError(t, err) + require.Equal(t, []string{"v1.0", "v0.9"}, tags) permissions, err := service.viewPermissions(context.Background(), "alice/project") require.NoError(t, err) require.Equal(t, RepositoryPermissions{}, permissions)