Compare commits

..

1 commit
dev ... main

Author SHA1 Message Date
1895ee9bbd Merge pull request 'dev' (#16) from dev into main
All checks were successful
Security Scan / security (push) Successful in 8s
Test / test (push) Successful in 41s
Reviewed-on: #16
2026-03-23 20:39:49 +00:00
115 changed files with 1750 additions and 19185 deletions

View file

@ -33,9 +33,9 @@ The library is a flat package (`package forge`) with a layered design:
5. **`config.go`** — Config resolution: flags > env (`FORGE_URL`, `FORGE_TOKEN`) > defaults (`http://localhost:3000`). 5. **`config.go`** — Config resolution: flags > env (`FORGE_URL`, `FORGE_TOKEN`) > defaults (`http://localhost:3000`).
6. **`forge.go`** — Top-level `Forge` struct aggregating all 20 service fields. Created via `NewForge(url, token)` or `NewForgeFromConfig(flagURL, flagToken)`. 6. **`forge.go`** — Top-level `Forge` struct aggregating all 18 service fields. Created via `NewForge(url, token)` or `NewForgeFromConfig(flagURL, flagToken)`.
7. **Service files** (`repos.go`, `issues.go`, etc.) — Each service struct embeds `Resource[T,C,U]` for standard CRUD, then adds hand-written action methods (e.g. `Fork`, `Pin`, `MirrorSync`). 20 services total: repos, issues, pulls, orgs, users, teams, admin, branches, releases, labels, webhooks, notifications, packages, actions, contents, wiki, commits, milestones, misc, activitypub. 7. **Service files** (`repos.go`, `issues.go`, etc.) — Each service struct embeds `Resource[T,C,U]` for standard CRUD, then adds hand-written action methods (e.g. `Fork`, `Pin`, `MirrorSync`). 18 services total: repos, issues, pulls, orgs, users, teams, admin, branches, releases, labels, webhooks, notifications, packages, actions, contents, wiki, commits, misc.
8. **`types/`** — Generated Go types from `swagger.v1.json` (229 types). The `//go:generate` directive lives in `types/generate.go`. **Do not hand-edit generated type files** — modify `cmd/forgegen/` instead. 8. **`types/`** — Generated Go types from `swagger.v1.json` (229 types). The `//go:generate` directive lives in `types/generate.go`. **Do not hand-edit generated type files** — modify `cmd/forgegen/` instead.

View file

@ -2,22 +2,15 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"net/url"
"strconv"
core "dappco.re/go/core"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// ActionsService handles CI/CD actions operations across repositories and // ActionsService handles CI/CD actions operations across repositories and
// organisations — secrets, variables, workflow dispatches, and tasks. // organisations — secrets, variables, and workflow dispatches.
// No Resource embedding — heterogeneous endpoints across repo and org levels. // No Resource embedding — heterogeneous endpoints across repo and org levels.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Actions.ListRepoSecrets(ctx, "core", "go-forge")
type ActionsService struct { type ActionsService struct {
client *Client client *Client
} }
@ -28,239 +21,82 @@ func newActionsService(c *Client) *ActionsService {
// ListRepoSecrets returns all secrets for a repository. // ListRepoSecrets returns all secrets for a repository.
func (s *ActionsService) ListRepoSecrets(ctx context.Context, owner, repo string) ([]types.Secret, error) { func (s *ActionsService) ListRepoSecrets(ctx context.Context, owner, repo string) ([]types.Secret, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets", owner, repo)
return ListAll[types.Secret](ctx, s.client, path, nil) return ListAll[types.Secret](ctx, s.client, path, nil)
} }
// IterRepoSecrets returns an iterator over all secrets for a repository. // IterRepoSecrets returns an iterator over all secrets for a repository.
func (s *ActionsService) IterRepoSecrets(ctx context.Context, owner, repo string) iter.Seq2[types.Secret, error] { func (s *ActionsService) IterRepoSecrets(ctx context.Context, owner, repo string) iter.Seq2[types.Secret, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets", owner, repo)
return ListIter[types.Secret](ctx, s.client, path, nil) return ListIter[types.Secret](ctx, s.client, path, nil)
} }
// CreateRepoSecret creates or updates a secret in a repository. // CreateRepoSecret creates or updates a secret in a repository.
// Forgejo expects a PUT with {"data": "secret-value"} body. // Forgejo expects a PUT with {"data": "secret-value"} body.
func (s *ActionsService) CreateRepoSecret(ctx context.Context, owner, repo, name string, data string) error { func (s *ActionsService) CreateRepoSecret(ctx context.Context, owner, repo, name string, data string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets/{secretname}", pathParams("owner", owner, "repo", repo, "secretname", name)) path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets/%s", owner, repo, name)
body := map[string]string{"data": data} body := map[string]string{"data": data}
return s.client.Put(ctx, path, body, nil) return s.client.Put(ctx, path, body, nil)
} }
// DeleteRepoSecret removes a secret from a repository. // DeleteRepoSecret removes a secret from a repository.
func (s *ActionsService) DeleteRepoSecret(ctx context.Context, owner, repo, name string) error { func (s *ActionsService) DeleteRepoSecret(ctx context.Context, owner, repo, name string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/secrets/{secretname}", pathParams("owner", owner, "repo", repo, "secretname", name)) path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets/%s", owner, repo, name)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// ListRepoVariables returns all action variables for a repository. // ListRepoVariables returns all action variables for a repository.
func (s *ActionsService) ListRepoVariables(ctx context.Context, owner, repo string) ([]types.ActionVariable, error) { func (s *ActionsService) ListRepoVariables(ctx context.Context, owner, repo string) ([]types.ActionVariable, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables", owner, repo)
return ListAll[types.ActionVariable](ctx, s.client, path, nil) return ListAll[types.ActionVariable](ctx, s.client, path, nil)
} }
// IterRepoVariables returns an iterator over all action variables for a repository. // IterRepoVariables returns an iterator over all action variables for a repository.
func (s *ActionsService) IterRepoVariables(ctx context.Context, owner, repo string) iter.Seq2[types.ActionVariable, error] { func (s *ActionsService) IterRepoVariables(ctx context.Context, owner, repo string) iter.Seq2[types.ActionVariable, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables", owner, repo)
return ListIter[types.ActionVariable](ctx, s.client, path, nil) return ListIter[types.ActionVariable](ctx, s.client, path, nil)
} }
// CreateRepoVariable creates a new action variable in a repository. // CreateRepoVariable creates a new action variable in a repository.
// Forgejo expects a POST with {"value": "var-value"} body. // Forgejo expects a POST with {"value": "var-value"} body.
func (s *ActionsService) CreateRepoVariable(ctx context.Context, owner, repo, name, value string) error { func (s *ActionsService) CreateRepoVariable(ctx context.Context, owner, repo, name, value string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{variablename}", pathParams("owner", owner, "repo", repo, "variablename", name)) path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/%s", owner, repo, name)
body := types.CreateVariableOption{Value: value} body := types.CreateVariableOption{Value: value}
return s.client.Post(ctx, path, body, nil) return s.client.Post(ctx, path, body, nil)
} }
// UpdateRepoVariable updates an existing action variable in a repository.
func (s *ActionsService) UpdateRepoVariable(ctx context.Context, owner, repo, name string, opts *types.UpdateVariableOption) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{variablename}", pathParams("owner", owner, "repo", repo, "variablename", name))
return s.client.Put(ctx, path, opts, nil)
}
// DeleteRepoVariable removes an action variable from a repository. // DeleteRepoVariable removes an action variable from a repository.
func (s *ActionsService) DeleteRepoVariable(ctx context.Context, owner, repo, name string) error { func (s *ActionsService) DeleteRepoVariable(ctx context.Context, owner, repo, name string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/variables/{variablename}", pathParams("owner", owner, "repo", repo, "variablename", name)) path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/%s", owner, repo, name)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// ListOrgSecrets returns all secrets for an organisation. // ListOrgSecrets returns all secrets for an organisation.
func (s *ActionsService) ListOrgSecrets(ctx context.Context, org string) ([]types.Secret, error) { func (s *ActionsService) ListOrgSecrets(ctx context.Context, org string) ([]types.Secret, error) {
path := ResolvePath("/api/v1/orgs/{org}/actions/secrets", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/actions/secrets", org)
return ListAll[types.Secret](ctx, s.client, path, nil) return ListAll[types.Secret](ctx, s.client, path, nil)
} }
// IterOrgSecrets returns an iterator over all secrets for an organisation. // IterOrgSecrets returns an iterator over all secrets for an organisation.
func (s *ActionsService) IterOrgSecrets(ctx context.Context, org string) iter.Seq2[types.Secret, error] { func (s *ActionsService) IterOrgSecrets(ctx context.Context, org string) iter.Seq2[types.Secret, error] {
path := ResolvePath("/api/v1/orgs/{org}/actions/secrets", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/actions/secrets", org)
return ListIter[types.Secret](ctx, s.client, path, nil) return ListIter[types.Secret](ctx, s.client, path, nil)
} }
// ListOrgVariables returns all action variables for an organisation. // ListOrgVariables returns all action variables for an organisation.
func (s *ActionsService) ListOrgVariables(ctx context.Context, org string) ([]types.ActionVariable, error) { func (s *ActionsService) ListOrgVariables(ctx context.Context, org string) ([]types.ActionVariable, error) {
path := ResolvePath("/api/v1/orgs/{org}/actions/variables", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/actions/variables", org)
return ListAll[types.ActionVariable](ctx, s.client, path, nil) return ListAll[types.ActionVariable](ctx, s.client, path, nil)
} }
// IterOrgVariables returns an iterator over all action variables for an organisation. // IterOrgVariables returns an iterator over all action variables for an organisation.
func (s *ActionsService) IterOrgVariables(ctx context.Context, org string) iter.Seq2[types.ActionVariable, error] { func (s *ActionsService) IterOrgVariables(ctx context.Context, org string) iter.Seq2[types.ActionVariable, error] {
path := ResolvePath("/api/v1/orgs/{org}/actions/variables", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/actions/variables", org)
return ListIter[types.ActionVariable](ctx, s.client, path, nil) return ListIter[types.ActionVariable](ctx, s.client, path, nil)
} }
// GetOrgVariable returns a single action variable for an organisation.
func (s *ActionsService) GetOrgVariable(ctx context.Context, org, name string) (*types.ActionVariable, error) {
path := ResolvePath("/api/v1/orgs/{org}/actions/variables/{variablename}", pathParams("org", org, "variablename", name))
var out types.ActionVariable
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// CreateOrgVariable creates a new action variable in an organisation.
func (s *ActionsService) CreateOrgVariable(ctx context.Context, org, name, value string) error {
path := ResolvePath("/api/v1/orgs/{org}/actions/variables/{variablename}", pathParams("org", org, "variablename", name))
body := types.CreateVariableOption{Value: value}
return s.client.Post(ctx, path, body, nil)
}
// UpdateOrgVariable updates an existing action variable in an organisation.
func (s *ActionsService) UpdateOrgVariable(ctx context.Context, org, name string, opts *types.UpdateVariableOption) error {
path := ResolvePath("/api/v1/orgs/{org}/actions/variables/{variablename}", pathParams("org", org, "variablename", name))
return s.client.Put(ctx, path, opts, nil)
}
// DeleteOrgVariable removes an action variable from an organisation.
func (s *ActionsService) DeleteOrgVariable(ctx context.Context, org, name string) error {
path := ResolvePath("/api/v1/orgs/{org}/actions/variables/{variablename}", pathParams("org", org, "variablename", name))
return s.client.Delete(ctx, path)
}
// CreateOrgSecret creates or updates a secret in an organisation.
func (s *ActionsService) CreateOrgSecret(ctx context.Context, org, name, data string) error {
path := ResolvePath("/api/v1/orgs/{org}/actions/secrets/{secretname}", pathParams("org", org, "secretname", name))
body := map[string]string{"data": data}
return s.client.Put(ctx, path, body, nil)
}
// DeleteOrgSecret removes a secret from an organisation.
func (s *ActionsService) DeleteOrgSecret(ctx context.Context, org, name string) error {
path := ResolvePath("/api/v1/orgs/{org}/actions/secrets/{secretname}", pathParams("org", org, "secretname", name))
return s.client.Delete(ctx, path)
}
// ListUserVariables returns all action variables for the authenticated user.
func (s *ActionsService) ListUserVariables(ctx context.Context) ([]types.ActionVariable, error) {
return ListAll[types.ActionVariable](ctx, s.client, "/api/v1/user/actions/variables", nil)
}
// IterUserVariables returns an iterator over all action variables for the authenticated user.
func (s *ActionsService) IterUserVariables(ctx context.Context) iter.Seq2[types.ActionVariable, error] {
return ListIter[types.ActionVariable](ctx, s.client, "/api/v1/user/actions/variables", nil)
}
// GetUserVariable returns a single action variable for the authenticated user.
func (s *ActionsService) GetUserVariable(ctx context.Context, name string) (*types.ActionVariable, error) {
path := ResolvePath("/api/v1/user/actions/variables/{variablename}", pathParams("variablename", name))
var out types.ActionVariable
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// CreateUserVariable creates a new action variable for the authenticated user.
func (s *ActionsService) CreateUserVariable(ctx context.Context, name, value string) error {
path := ResolvePath("/api/v1/user/actions/variables/{variablename}", pathParams("variablename", name))
body := types.CreateVariableOption{Value: value}
return s.client.Post(ctx, path, body, nil)
}
// UpdateUserVariable updates an existing action variable for the authenticated user.
func (s *ActionsService) UpdateUserVariable(ctx context.Context, name string, opts *types.UpdateVariableOption) error {
path := ResolvePath("/api/v1/user/actions/variables/{variablename}", pathParams("variablename", name))
return s.client.Put(ctx, path, opts, nil)
}
// DeleteUserVariable removes an action variable for the authenticated user.
func (s *ActionsService) DeleteUserVariable(ctx context.Context, name string) error {
path := ResolvePath("/api/v1/user/actions/variables/{variablename}", pathParams("variablename", name))
return s.client.Delete(ctx, path)
}
// CreateUserSecret creates or updates a secret for the authenticated user.
func (s *ActionsService) CreateUserSecret(ctx context.Context, name, data string) error {
path := ResolvePath("/api/v1/user/actions/secrets/{secretname}", pathParams("secretname", name))
body := map[string]string{"data": data}
return s.client.Put(ctx, path, body, nil)
}
// DeleteUserSecret removes a secret for the authenticated user.
func (s *ActionsService) DeleteUserSecret(ctx context.Context, name string) error {
path := ResolvePath("/api/v1/user/actions/secrets/{secretname}", pathParams("secretname", name))
return s.client.Delete(ctx, path)
}
// DispatchWorkflow triggers a workflow run. // DispatchWorkflow triggers a workflow run.
func (s *ActionsService) DispatchWorkflow(ctx context.Context, owner, repo, workflow string, opts map[string]any) error { func (s *ActionsService) DispatchWorkflow(ctx context.Context, owner, repo, workflow string, opts map[string]any) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/workflows/{workflowname}/dispatches", pathParams("owner", owner, "repo", repo, "workflowname", workflow)) path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/dispatches", owner, repo, workflow)
return s.client.Post(ctx, path, opts, nil) return s.client.Post(ctx, path, opts, nil)
} }
// ListRepoTasks returns a single page of action tasks for a repository.
func (s *ActionsService) ListRepoTasks(ctx context.Context, owner, repo string, opts ListOptions) (*types.ActionTaskResponse, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/actions/tasks", pathParams("owner", owner, "repo", repo))
if opts.Page > 0 || opts.Limit > 0 {
u, err := url.Parse(path)
if err != nil {
return nil, core.E("ActionsService.ListRepoTasks", "forge: parse path", err)
}
q := u.Query()
if opts.Page > 0 {
q.Set("page", strconv.Itoa(opts.Page))
}
if opts.Limit > 0 {
q.Set("limit", strconv.Itoa(opts.Limit))
}
u.RawQuery = q.Encode()
path = u.String()
}
var out types.ActionTaskResponse
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// IterRepoTasks returns an iterator over all action tasks for a repository.
func (s *ActionsService) IterRepoTasks(ctx context.Context, owner, repo string) iter.Seq2[types.ActionTask, error] {
return func(yield func(types.ActionTask, error) bool) {
const limit = 50
var seen int64
for page := 1; ; page++ {
resp, err := s.ListRepoTasks(ctx, owner, repo, ListOptions{Page: page, Limit: limit})
if err != nil {
yield(*new(types.ActionTask), err)
return
}
for _, item := range resp.Entries {
if !yield(*item, nil) {
return
}
seen++
}
if resp.TotalCount > 0 {
if seen >= resp.TotalCount {
return
}
continue
}
if len(resp.Entries) < limit {
return
}
}
}
}

View file

@ -2,7 +2,7 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -10,7 +10,7 @@ import (
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestActionsService_ListRepoSecrets_Good(t *testing.T) { func TestActionsService_Good_ListRepoSecrets(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -42,7 +42,7 @@ func TestActionsService_ListRepoSecrets_Good(t *testing.T) {
} }
} }
func TestActionsService_CreateRepoSecret_Good(t *testing.T) { func TestActionsService_Good_CreateRepoSecret(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut { if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method) t.Errorf("expected PUT, got %s", r.Method)
@ -68,7 +68,7 @@ func TestActionsService_CreateRepoSecret_Good(t *testing.T) {
} }
} }
func TestActionsService_DeleteRepoSecret_Good(t *testing.T) { func TestActionsService_Good_DeleteRepoSecret(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method) t.Errorf("expected DELETE, got %s", r.Method)
@ -87,7 +87,7 @@ func TestActionsService_DeleteRepoSecret_Good(t *testing.T) {
} }
} }
func TestActionsService_ListRepoVariables_Good(t *testing.T) { func TestActionsService_Good_ListRepoVariables(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -115,7 +115,7 @@ func TestActionsService_ListRepoVariables_Good(t *testing.T) {
} }
} }
func TestActionsService_CreateRepoVariable_Good(t *testing.T) { func TestActionsService_Good_CreateRepoVariable(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -141,39 +141,7 @@ func TestActionsService_CreateRepoVariable_Good(t *testing.T) {
} }
} }
func TestActionsService_UpdateRepoVariable_Good(t *testing.T) { func TestActionsService_Good_DeleteRepoVariable(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/actions/variables/CI_ENV" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var body types.UpdateVariableOption
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.Name != "CI_ENV_NEW" {
t.Errorf("got name=%q, want %q", body.Name, "CI_ENV_NEW")
}
if body.Value != "production" {
t.Errorf("got value=%q, want %q", body.Value, "production")
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
err := f.Actions.UpdateRepoVariable(context.Background(), "core", "go-forge", "CI_ENV", &types.UpdateVariableOption{
Name: "CI_ENV_NEW",
Value: "production",
})
if err != nil {
t.Fatal(err)
}
}
func TestActionsService_DeleteRepoVariable_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method) t.Errorf("expected DELETE, got %s", r.Method)
@ -192,7 +160,7 @@ func TestActionsService_DeleteRepoVariable_Good(t *testing.T) {
} }
} }
func TestActionsService_ListOrgSecrets_Good(t *testing.T) { func TestActionsService_Good_ListOrgSecrets(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -220,7 +188,7 @@ func TestActionsService_ListOrgSecrets_Good(t *testing.T) {
} }
} }
func TestActionsService_ListOrgVariables_Good(t *testing.T) { func TestActionsService_Good_ListOrgVariables(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -248,188 +216,7 @@ func TestActionsService_ListOrgVariables_Good(t *testing.T) {
} }
} }
func TestActionsService_GetOrgVariable_Good(t *testing.T) { func TestActionsService_Good_DispatchWorkflow(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/lethean/actions/variables/ORG_VAR" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.ActionVariable{Name: "ORG_VAR", Data: "org-value"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
variable, err := f.Actions.GetOrgVariable(context.Background(), "lethean", "ORG_VAR")
if err != nil {
t.Fatal(err)
}
if variable.Name != "ORG_VAR" || variable.Data != "org-value" {
t.Fatalf("unexpected variable: %#v", variable)
}
}
func TestActionsService_CreateUserVariable_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/user/actions/variables/CI_ENV" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var body types.CreateVariableOption
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.Value != "production" {
t.Errorf("got value=%q, want %q", body.Value, "production")
}
w.WriteHeader(http.StatusCreated)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Actions.CreateUserVariable(context.Background(), "CI_ENV", "production"); err != nil {
t.Fatal(err)
}
}
func TestActionsService_ListUserVariables_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/user/actions/variables" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.ActionVariable{{Name: "CI_ENV", Data: "production"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
vars, err := f.Actions.ListUserVariables(context.Background())
if err != nil {
t.Fatal(err)
}
if len(vars) != 1 || vars[0].Name != "CI_ENV" {
t.Fatalf("unexpected variables: %#v", vars)
}
}
func TestActionsService_GetUserVariable_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/user/actions/variables/CI_ENV" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.ActionVariable{Name: "CI_ENV", Data: "production"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
variable, err := f.Actions.GetUserVariable(context.Background(), "CI_ENV")
if err != nil {
t.Fatal(err)
}
if variable.Name != "CI_ENV" || variable.Data != "production" {
t.Fatalf("unexpected variable: %#v", variable)
}
}
func TestActionsService_UpdateUserVariable_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method)
}
if r.URL.Path != "/api/v1/user/actions/variables/CI_ENV" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var body types.UpdateVariableOption
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.Name != "CI_ENV_NEW" || body.Value != "staging" {
t.Fatalf("unexpected body: %#v", body)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Actions.UpdateUserVariable(context.Background(), "CI_ENV", &types.UpdateVariableOption{
Name: "CI_ENV_NEW",
Value: "staging",
}); err != nil {
t.Fatal(err)
}
}
func TestActionsService_DeleteUserVariable_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/user/actions/variables/OLD_VAR" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Actions.DeleteUserVariable(context.Background(), "OLD_VAR"); err != nil {
t.Fatal(err)
}
}
func TestActionsService_CreateUserSecret_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method)
}
if r.URL.Path != "/api/v1/user/actions/secrets/DEPLOY_KEY" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var body map[string]string
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body["data"] != "secret-value" {
t.Errorf("got data=%q, want %q", body["data"], "secret-value")
}
w.WriteHeader(http.StatusCreated)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Actions.CreateUserSecret(context.Background(), "DEPLOY_KEY", "secret-value"); err != nil {
t.Fatal(err)
}
}
func TestActionsService_DeleteUserSecret_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/user/actions/secrets/OLD_KEY" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Actions.DeleteUserSecret(context.Background(), "OLD_KEY"); err != nil {
t.Fatal(err)
}
}
func TestActionsService_DispatchWorkflow_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -457,88 +244,7 @@ func TestActionsService_DispatchWorkflow_Good(t *testing.T) {
} }
} }
func TestActionsService_ListRepoTasks_Good(t *testing.T) { func TestActionsService_Bad_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/actions/tasks" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("page"); got != "2" {
t.Errorf("got page=%q, want %q", got, "2")
}
if got := r.URL.Query().Get("limit"); got != "25" {
t.Errorf("got limit=%q, want %q", got, "25")
}
json.NewEncoder(w).Encode(types.ActionTaskResponse{
Entries: []*types.ActionTask{
{ID: 101, Name: "build"},
{ID: 102, Name: "test"},
},
TotalCount: 2,
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
resp, err := f.Actions.ListRepoTasks(context.Background(), "core", "go-forge", ListOptions{Page: 2, Limit: 25})
if err != nil {
t.Fatal(err)
}
if resp.TotalCount != 2 {
t.Fatalf("got total_count=%d, want 2", resp.TotalCount)
}
if len(resp.Entries) != 2 {
t.Fatalf("got %d tasks, want 2", len(resp.Entries))
}
if resp.Entries[0].ID != 101 || resp.Entries[1].Name != "test" {
t.Fatalf("unexpected tasks: %#v", resp.Entries)
}
}
func TestActionsService_IterRepoTasks_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/actions/tasks" {
t.Errorf("wrong path: %s", r.URL.Path)
}
switch r.URL.Query().Get("page") {
case "1":
json.NewEncoder(w).Encode(types.ActionTaskResponse{
Entries: []*types.ActionTask{{ID: 1, Name: "build"}},
TotalCount: 2,
})
case "2":
json.NewEncoder(w).Encode(types.ActionTaskResponse{
Entries: []*types.ActionTask{{ID: 2, Name: "test"}},
TotalCount: 2,
})
default:
t.Fatalf("unexpected page %q", r.URL.Query().Get("page"))
}
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var got []types.ActionTask
for task, err := range f.Actions.IterRepoTasks(context.Background(), "core", "go-forge") {
if err != nil {
t.Fatal(err)
}
got = append(got, task)
}
if len(got) != 2 {
t.Fatalf("got %d tasks, want 2", len(got))
}
if got[0].ID != 1 || got[1].Name != "test" {
t.Fatalf("unexpected tasks: %#v", got)
}
}
func TestActionsService_NotFound_Bad(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "not found"})

View file

@ -1,67 +0,0 @@
package forge
import (
"context"
"dappco.re/go/core/forge/types"
)
// ActivityPubService handles ActivityPub actor and inbox endpoints.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.ActivityPub.GetInstanceActor(ctx)
type ActivityPubService struct {
client *Client
}
func newActivityPubService(c *Client) *ActivityPubService {
return &ActivityPubService{client: c}
}
// GetInstanceActor returns the instance's ActivityPub actor.
func (s *ActivityPubService) GetInstanceActor(ctx context.Context) (*types.ActivityPub, error) {
var out types.ActivityPub
if err := s.client.Get(ctx, "/activitypub/actor", &out); err != nil {
return nil, err
}
return &out, nil
}
// SendInstanceActorInbox sends an ActivityPub object to the instance inbox.
func (s *ActivityPubService) SendInstanceActorInbox(ctx context.Context, body *types.ForgeLike) error {
return s.client.Post(ctx, "/activitypub/actor/inbox", body, nil)
}
// GetRepositoryActor returns the ActivityPub actor for a repository.
func (s *ActivityPubService) GetRepositoryActor(ctx context.Context, repositoryID int64) (*types.ActivityPub, error) {
path := ResolvePath("/activitypub/repository-id/{repository-id}", Params{"repository-id": int64String(repositoryID)})
var out types.ActivityPub
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// SendRepositoryInbox sends an ActivityPub object to a repository inbox.
func (s *ActivityPubService) SendRepositoryInbox(ctx context.Context, repositoryID int64, body *types.ForgeLike) error {
path := ResolvePath("/activitypub/repository-id/{repository-id}/inbox", Params{"repository-id": int64String(repositoryID)})
return s.client.Post(ctx, path, body, nil)
}
// GetPersonActor returns the Person actor for a user.
func (s *ActivityPubService) GetPersonActor(ctx context.Context, userID int64) (*types.ActivityPub, error) {
path := ResolvePath("/activitypub/user-id/{user-id}", Params{"user-id": int64String(userID)})
var out types.ActivityPub
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// SendPersonInbox sends an ActivityPub object to a user's inbox.
func (s *ActivityPubService) SendPersonInbox(ctx context.Context, userID int64, body *types.ForgeLike) error {
path := ResolvePath("/activitypub/user-id/{user-id}/inbox", Params{"user-id": int64String(userID)})
return s.client.Post(ctx, path, body, nil)
}

View file

@ -1,59 +0,0 @@
package forge
import (
"context"
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"testing"
"dappco.re/go/core/forge/types"
)
func TestActivityPubService_GetInstanceActor_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/activitypub/actor" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
json.NewEncoder(w).Encode(types.ActivityPub{Context: "https://www.w3.org/ns/activitystreams"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
actor, err := f.ActivityPub.GetInstanceActor(context.Background())
if err != nil {
t.Fatal(err)
}
if actor.Context != "https://www.w3.org/ns/activitystreams" {
t.Fatalf("got context=%q", actor.Context)
}
}
func TestActivityPubService_SendRepositoryInbox_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/activitypub/repository-id/42/inbox" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
var body types.ForgeLike
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode body: %v", err)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.ActivityPub.SendRepositoryInbox(context.Background(), 42, &types.ForgeLike{}); err != nil {
t.Fatal(err)
}
}

439
admin.go
View file

@ -3,100 +3,17 @@ package forge
import ( import (
"context" "context"
"iter" "iter"
"net/http"
"net/url"
"strconv"
core "dappco.re/go/core"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// AdminService handles site administration operations. // AdminService handles site administration operations.
// Unlike other services, AdminService does not embed Resource[T,C,U] // Unlike other services, AdminService does not embed Resource[T,C,U]
// because admin endpoints are heterogeneous. // because admin endpoints are heterogeneous.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Admin.ListUsers(ctx)
type AdminService struct { type AdminService struct {
client *Client client *Client
} }
// AdminActionsRunListOptions controls filtering for admin Actions run listings.
//
// Usage:
//
// opts := forge.AdminActionsRunListOptions{Event: "push", Status: "success"}
type AdminActionsRunListOptions struct {
Event string
Branch string
Status string
Actor string
HeadSHA string
}
// String returns a safe summary of the admin Actions run filters.
func (o AdminActionsRunListOptions) String() string {
return optionString("forge.AdminActionsRunListOptions",
"event", o.Event,
"branch", o.Branch,
"status", o.Status,
"actor", o.Actor,
"head_sha", o.HeadSHA,
)
}
// GoString returns a safe Go-syntax summary of the admin Actions run filters.
func (o AdminActionsRunListOptions) GoString() string { return o.String() }
func (o AdminActionsRunListOptions) queryParams() map[string]string {
query := make(map[string]string, 5)
if o.Event != "" {
query["event"] = o.Event
}
if o.Branch != "" {
query["branch"] = o.Branch
}
if o.Status != "" {
query["status"] = o.Status
}
if o.Actor != "" {
query["actor"] = o.Actor
}
if o.HeadSHA != "" {
query["head_sha"] = o.HeadSHA
}
if len(query) == 0 {
return nil
}
return query
}
// AdminUnadoptedListOptions controls filtering for unadopted repository listings.
//
// Usage:
//
// opts := forge.AdminUnadoptedListOptions{Pattern: "core/*"}
type AdminUnadoptedListOptions struct {
Pattern string
}
// String returns a safe summary of the unadopted repository filters.
func (o AdminUnadoptedListOptions) String() string {
return optionString("forge.AdminUnadoptedListOptions", "pattern", o.Pattern)
}
// GoString returns a safe Go-syntax summary of the unadopted repository filters.
func (o AdminUnadoptedListOptions) GoString() string { return o.String() }
func (o AdminUnadoptedListOptions) queryParams() map[string]string {
if o.Pattern == "" {
return nil
}
return map[string]string{"pattern": o.Pattern}
}
func newAdminService(c *Client) *AdminService { func newAdminService(c *Client) *AdminService {
return &AdminService{client: c} return &AdminService{client: c}
} }
@ -120,58 +37,6 @@ func (s *AdminService) CreateUser(ctx context.Context, opts *types.CreateUserOpt
return &out, nil return &out, nil
} }
// CreateUserKey adds a public key on behalf of a user.
func (s *AdminService) CreateUserKey(ctx context.Context, username string, opts *types.CreateKeyOption) (*types.PublicKey, error) {
path := ResolvePath("/api/v1/admin/users/{username}/keys", Params{"username": username})
var out types.PublicKey
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteUserKey deletes a user's public key.
func (s *AdminService) DeleteUserKey(ctx context.Context, username string, id int64) error {
path := ResolvePath("/api/v1/admin/users/{username}/keys/{id}", Params{"username": username, "id": int64String(id)})
return s.client.Delete(ctx, path)
}
// CreateUserOrg creates an organisation on behalf of a user.
func (s *AdminService) CreateUserOrg(ctx context.Context, username string, opts *types.CreateOrgOption) (*types.Organization, error) {
path := ResolvePath("/api/v1/admin/users/{username}/orgs", Params{"username": username})
var out types.Organization
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// GetUserQuota returns a user's quota information.
func (s *AdminService) GetUserQuota(ctx context.Context, username string) (*types.QuotaInfo, error) {
path := ResolvePath("/api/v1/admin/users/{username}/quota", Params{"username": username})
var out types.QuotaInfo
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// SetUserQuotaGroups sets the user's quota groups to a given list.
func (s *AdminService) SetUserQuotaGroups(ctx context.Context, username string, opts *types.SetUserQuotaGroupsOptions) error {
path := ResolvePath("/api/v1/admin/users/{username}/quota/groups", Params{"username": username})
return s.client.Post(ctx, path, opts, nil)
}
// CreateUserRepo creates a repository on behalf of a user.
func (s *AdminService) CreateUserRepo(ctx context.Context, username string, opts *types.CreateRepoOption) (*types.Repository, error) {
path := ResolvePath("/api/v1/admin/users/{username}/repos", Params{"username": username})
var out types.Repository
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// EditUser edits an existing user (admin only). // EditUser edits an existing user (admin only).
func (s *AdminService) EditUser(ctx context.Context, username string, opts map[string]any) error { func (s *AdminService) EditUser(ctx context.Context, username string, opts map[string]any) error {
path := ResolvePath("/api/v1/admin/users/{username}", Params{"username": username}) path := ResolvePath("/api/v1/admin/users/{username}", Params{"username": username})
@ -200,219 +65,6 @@ func (s *AdminService) IterOrgs(ctx context.Context) iter.Seq2[types.Organizatio
return ListIter[types.Organization](ctx, s.client, "/api/v1/admin/orgs", nil) return ListIter[types.Organization](ctx, s.client, "/api/v1/admin/orgs", nil)
} }
// ListEmails returns all email addresses (admin only).
func (s *AdminService) ListEmails(ctx context.Context) ([]types.Email, error) {
return ListAll[types.Email](ctx, s.client, "/api/v1/admin/emails", nil)
}
// IterEmails returns an iterator over all email addresses (admin only).
func (s *AdminService) IterEmails(ctx context.Context) iter.Seq2[types.Email, error] {
return ListIter[types.Email](ctx, s.client, "/api/v1/admin/emails", nil)
}
// ListHooks returns all global hooks (admin only).
func (s *AdminService) ListHooks(ctx context.Context) ([]types.Hook, error) {
return ListAll[types.Hook](ctx, s.client, "/api/v1/admin/hooks", nil)
}
// IterHooks returns an iterator over all global hooks (admin only).
func (s *AdminService) IterHooks(ctx context.Context) iter.Seq2[types.Hook, error] {
return ListIter[types.Hook](ctx, s.client, "/api/v1/admin/hooks", nil)
}
// GetHook returns a single global hook by ID (admin only).
func (s *AdminService) GetHook(ctx context.Context, id int64) (*types.Hook, error) {
path := ResolvePath("/api/v1/admin/hooks/{id}", Params{"id": int64String(id)})
var out types.Hook
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// CreateHook creates a new global hook (admin only).
func (s *AdminService) CreateHook(ctx context.Context, opts *types.CreateHookOption) (*types.Hook, error) {
var out types.Hook
if err := s.client.Post(ctx, "/api/v1/admin/hooks", opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// EditHook updates an existing global hook (admin only).
func (s *AdminService) EditHook(ctx context.Context, id int64, opts *types.EditHookOption) (*types.Hook, error) {
path := ResolvePath("/api/v1/admin/hooks/{id}", Params{"id": int64String(id)})
var out types.Hook
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteHook deletes a global hook (admin only).
func (s *AdminService) DeleteHook(ctx context.Context, id int64) error {
path := ResolvePath("/api/v1/admin/hooks/{id}", Params{"id": int64String(id)})
return s.client.Delete(ctx, path)
}
// ListQuotaGroups returns all available quota groups.
func (s *AdminService) ListQuotaGroups(ctx context.Context) ([]types.QuotaGroup, error) {
return ListAll[types.QuotaGroup](ctx, s.client, "/api/v1/admin/quota/groups", nil)
}
// IterQuotaGroups returns an iterator over all available quota groups.
func (s *AdminService) IterQuotaGroups(ctx context.Context) iter.Seq2[types.QuotaGroup, error] {
return func(yield func(types.QuotaGroup, error) bool) {
groups, err := s.ListQuotaGroups(ctx)
if err != nil {
yield(*new(types.QuotaGroup), err)
return
}
for _, group := range groups {
if !yield(group, nil) {
return
}
}
}
}
// CreateQuotaGroup creates a new quota group.
func (s *AdminService) CreateQuotaGroup(ctx context.Context, opts *types.CreateQuotaGroupOptions) (*types.QuotaGroup, error) {
var out types.QuotaGroup
if err := s.client.Post(ctx, "/api/v1/admin/quota/groups", opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// GetQuotaGroup returns information about a quota group.
func (s *AdminService) GetQuotaGroup(ctx context.Context, quotagroup string) (*types.QuotaGroup, error) {
path := ResolvePath("/api/v1/admin/quota/groups/{quotagroup}", Params{"quotagroup": quotagroup})
var out types.QuotaGroup
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteQuotaGroup deletes a quota group.
func (s *AdminService) DeleteQuotaGroup(ctx context.Context, quotagroup string) error {
path := ResolvePath("/api/v1/admin/quota/groups/{quotagroup}", Params{"quotagroup": quotagroup})
return s.client.Delete(ctx, path)
}
// AddQuotaGroupRule adds a quota rule to a quota group.
func (s *AdminService) AddQuotaGroupRule(ctx context.Context, quotagroup, quotarule string) error {
path := ResolvePath("/api/v1/admin/quota/groups/{quotagroup}/rules/{quotarule}", Params{"quotagroup": quotagroup, "quotarule": quotarule})
return s.client.Put(ctx, path, nil, nil)
}
// RemoveQuotaGroupRule removes a quota rule from a quota group.
func (s *AdminService) RemoveQuotaGroupRule(ctx context.Context, quotagroup, quotarule string) error {
path := ResolvePath("/api/v1/admin/quota/groups/{quotagroup}/rules/{quotarule}", Params{"quotagroup": quotagroup, "quotarule": quotarule})
return s.client.Delete(ctx, path)
}
// ListQuotaGroupUsers returns all users in a quota group.
func (s *AdminService) ListQuotaGroupUsers(ctx context.Context, quotagroup string) ([]types.User, error) {
path := ResolvePath("/api/v1/admin/quota/groups/{quotagroup}/users", Params{"quotagroup": quotagroup})
return ListAll[types.User](ctx, s.client, path, nil)
}
// IterQuotaGroupUsers returns an iterator over all users in a quota group.
func (s *AdminService) IterQuotaGroupUsers(ctx context.Context, quotagroup string) iter.Seq2[types.User, error] {
path := ResolvePath("/api/v1/admin/quota/groups/{quotagroup}/users", Params{"quotagroup": quotagroup})
return ListIter[types.User](ctx, s.client, path, nil)
}
// AddQuotaGroupUser adds a user to a quota group.
func (s *AdminService) AddQuotaGroupUser(ctx context.Context, quotagroup, username string) error {
path := ResolvePath("/api/v1/admin/quota/groups/{quotagroup}/users/{username}", Params{"quotagroup": quotagroup, "username": username})
return s.client.Put(ctx, path, nil, nil)
}
// RemoveQuotaGroupUser removes a user from a quota group.
func (s *AdminService) RemoveQuotaGroupUser(ctx context.Context, quotagroup, username string) error {
path := ResolvePath("/api/v1/admin/quota/groups/{quotagroup}/users/{username}", Params{"quotagroup": quotagroup, "username": username})
return s.client.Delete(ctx, path)
}
// ListQuotaRules returns all available quota rules.
func (s *AdminService) ListQuotaRules(ctx context.Context) ([]types.QuotaRuleInfo, error) {
return ListAll[types.QuotaRuleInfo](ctx, s.client, "/api/v1/admin/quota/rules", nil)
}
// IterQuotaRules returns an iterator over all available quota rules.
func (s *AdminService) IterQuotaRules(ctx context.Context) iter.Seq2[types.QuotaRuleInfo, error] {
return func(yield func(types.QuotaRuleInfo, error) bool) {
rules, err := s.ListQuotaRules(ctx)
if err != nil {
yield(*new(types.QuotaRuleInfo), err)
return
}
for _, rule := range rules {
if !yield(rule, nil) {
return
}
}
}
}
// CreateQuotaRule creates a new quota rule.
func (s *AdminService) CreateQuotaRule(ctx context.Context, opts *types.CreateQuotaRuleOptions) (*types.QuotaRuleInfo, error) {
var out types.QuotaRuleInfo
if err := s.client.Post(ctx, "/api/v1/admin/quota/rules", opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// GetQuotaRule returns information about a quota rule.
func (s *AdminService) GetQuotaRule(ctx context.Context, quotarule string) (*types.QuotaRuleInfo, error) {
path := ResolvePath("/api/v1/admin/quota/rules/{quotarule}", Params{"quotarule": quotarule})
var out types.QuotaRuleInfo
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// EditQuotaRule updates an existing quota rule.
func (s *AdminService) EditQuotaRule(ctx context.Context, quotarule string, opts *types.EditQuotaRuleOptions) (*types.QuotaRuleInfo, error) {
path := ResolvePath("/api/v1/admin/quota/rules/{quotarule}", Params{"quotarule": quotarule})
var out types.QuotaRuleInfo
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteQuotaRule deletes a quota rule.
func (s *AdminService) DeleteQuotaRule(ctx context.Context, quotarule string) error {
path := ResolvePath("/api/v1/admin/quota/rules/{quotarule}", Params{"quotarule": quotarule})
return s.client.Delete(ctx, path)
}
// ListUnadoptedRepos returns all unadopted repositories on the instance.
func (s *AdminService) ListUnadoptedRepos(ctx context.Context, filters ...AdminUnadoptedListOptions) ([]string, error) {
return ListAll[string](ctx, s.client, "/api/v1/admin/unadopted", adminUnadoptedQuery(filters...))
}
// IterUnadoptedRepos returns an iterator over all unadopted repositories on the instance.
func (s *AdminService) IterUnadoptedRepos(ctx context.Context, filters ...AdminUnadoptedListOptions) iter.Seq2[string, error] {
return ListIter[string](ctx, s.client, "/api/v1/admin/unadopted", adminUnadoptedQuery(filters...))
}
// SearchEmails searches all email addresses by keyword (admin only).
func (s *AdminService) SearchEmails(ctx context.Context, q string) ([]types.Email, error) {
return ListAll[types.Email](ctx, s.client, "/api/v1/admin/emails/search", map[string]string{"q": q})
}
// IterSearchEmails returns an iterator over all email addresses matching a keyword (admin only).
func (s *AdminService) IterSearchEmails(ctx context.Context, q string) iter.Seq2[types.Email, error] {
return ListIter[types.Email](ctx, s.client, "/api/v1/admin/emails/search", map[string]string{"q": q})
}
// RunCron runs a cron task by name (admin only). // RunCron runs a cron task by name (admin only).
func (s *AdminService) RunCron(ctx context.Context, task string) error { func (s *AdminService) RunCron(ctx context.Context, task string) error {
path := ResolvePath("/api/v1/admin/cron/{task}", Params{"task": task}) path := ResolvePath("/api/v1/admin/cron/{task}", Params{"task": task})
@ -429,103 +81,12 @@ func (s *AdminService) IterCron(ctx context.Context) iter.Seq2[types.Cron, error
return ListIter[types.Cron](ctx, s.client, "/api/v1/admin/cron", nil) return ListIter[types.Cron](ctx, s.client, "/api/v1/admin/cron", nil)
} }
// ListActionsRuns returns a single page of Actions workflow runs across the instance.
func (s *AdminService) ListActionsRuns(ctx context.Context, filters AdminActionsRunListOptions, opts ListOptions) (*PagedResult[types.ActionTask], error) {
if opts.Page < 1 {
opts.Page = 1
}
if opts.Limit < 1 {
opts.Limit = 50
}
u, err := url.Parse("/api/v1/admin/actions/runs")
if err != nil {
return nil, core.E("AdminService.ListActionsRuns", "forge: parse path", err)
}
q := u.Query()
for key, value := range filters.queryParams() {
q.Set(key, value)
}
q.Set("page", strconv.Itoa(opts.Page))
q.Set("limit", strconv.Itoa(opts.Limit))
u.RawQuery = q.Encode()
var out types.ActionTaskResponse
resp, err := s.client.doJSON(ctx, http.MethodGet, u.String(), nil, &out)
if err != nil {
return nil, err
}
totalCount, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
items := make([]types.ActionTask, 0, len(out.Entries))
for _, run := range out.Entries {
if run != nil {
items = append(items, *run)
}
}
return &PagedResult[types.ActionTask]{
Items: items,
TotalCount: totalCount,
Page: opts.Page,
HasMore: (totalCount > 0 && (opts.Page-1)*opts.Limit+len(items) < totalCount) ||
(totalCount == 0 && len(items) >= opts.Limit),
}, nil
}
// IterActionsRuns returns an iterator over all Actions workflow runs across the instance.
func (s *AdminService) IterActionsRuns(ctx context.Context, filters AdminActionsRunListOptions) iter.Seq2[types.ActionTask, error] {
return func(yield func(types.ActionTask, error) bool) {
page := 1
for {
result, err := s.ListActionsRuns(ctx, filters, ListOptions{Page: page, Limit: 50})
if err != nil {
yield(*new(types.ActionTask), err)
return
}
for _, item := range result.Items {
if !yield(item, nil) {
return
}
}
if !result.HasMore {
break
}
page++
}
}
}
// AdoptRepo adopts an unadopted repository (admin only). // AdoptRepo adopts an unadopted repository (admin only).
func (s *AdminService) AdoptRepo(ctx context.Context, owner, repo string) error { func (s *AdminService) AdoptRepo(ctx context.Context, owner, repo string) error {
path := ResolvePath("/api/v1/admin/unadopted/{owner}/{repo}", Params{"owner": owner, "repo": repo}) path := ResolvePath("/api/v1/admin/unadopted/{owner}/{repo}", Params{"owner": owner, "repo": repo})
return s.client.Post(ctx, path, nil, nil) return s.client.Post(ctx, path, nil, nil)
} }
// DeleteUnadoptedRepo deletes an unadopted repository's files.
func (s *AdminService) DeleteUnadoptedRepo(ctx context.Context, owner, repo string) error {
path := ResolvePath("/api/v1/admin/unadopted/{owner}/{repo}", Params{"owner": owner, "repo": repo})
return s.client.Delete(ctx, path)
}
func adminUnadoptedQuery(filters ...AdminUnadoptedListOptions) map[string]string {
if len(filters) == 0 {
return nil
}
query := make(map[string]string, 1)
for _, filter := range filters {
if filter.Pattern != "" {
query["pattern"] = filter.Pattern
}
}
if len(query) == 0 {
return nil
}
return query
}
// GenerateRunnerToken generates an actions runner registration token. // GenerateRunnerToken generates an actions runner registration token.
func (s *AdminService) GenerateRunnerToken(ctx context.Context) (string, error) { func (s *AdminService) GenerateRunnerToken(ctx context.Context) (string, error) {
var out struct { var out struct {

View file

@ -2,7 +2,7 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -10,7 +10,7 @@ import (
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestAdminService_ListUsers_Good(t *testing.T) { func TestAdminService_Good_ListUsers(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -39,7 +39,7 @@ func TestAdminService_ListUsers_Good(t *testing.T) {
} }
} }
func TestAdminService_CreateUser_Good(t *testing.T) { func TestAdminService_Good_CreateUser(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -78,7 +78,7 @@ func TestAdminService_CreateUser_Good(t *testing.T) {
} }
} }
func TestAdminService_DeleteUser_Good(t *testing.T) { func TestAdminService_Good_DeleteUser(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method) t.Errorf("expected DELETE, got %s", r.Method)
@ -96,7 +96,7 @@ func TestAdminService_DeleteUser_Good(t *testing.T) {
} }
} }
func TestAdminService_RunCron_Good(t *testing.T) { func TestAdminService_Good_RunCron(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -114,7 +114,7 @@ func TestAdminService_RunCron_Good(t *testing.T) {
} }
} }
func TestAdminService_EditUser_Good(t *testing.T) { func TestAdminService_Good_EditUser(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch { if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method) t.Errorf("expected PATCH, got %s", r.Method)
@ -142,7 +142,7 @@ func TestAdminService_EditUser_Good(t *testing.T) {
} }
} }
func TestAdminService_RenameUser_Good(t *testing.T) { func TestAdminService_Good_RenameUser(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -167,7 +167,7 @@ func TestAdminService_RenameUser_Good(t *testing.T) {
} }
} }
func TestAdminService_ListOrgs_Good(t *testing.T) { func TestAdminService_Good_ListOrgs(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -195,633 +195,7 @@ func TestAdminService_ListOrgs_Good(t *testing.T) {
} }
} }
func TestAdminService_ListEmails_Good(t *testing.T) { func TestAdminService_Good_ListCron(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/emails" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.Email{
{Email: "alice@example.com", Primary: true},
{Email: "bob@example.com", Verified: true},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
emails, err := f.Admin.ListEmails(context.Background())
if err != nil {
t.Fatal(err)
}
if len(emails) != 2 {
t.Errorf("got %d emails, want 2", len(emails))
}
if emails[0].Email != "alice@example.com" || !emails[0].Primary {
t.Errorf("got first email=%+v, want primary alice@example.com", emails[0])
}
}
func TestAdminService_ListHooks_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/hooks" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Hook{
{ID: 7, Type: "forgejo", URL: "https://example.com/admin-hook", Active: true},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
hooks, err := f.Admin.ListHooks(context.Background())
if err != nil {
t.Fatal(err)
}
if len(hooks) != 1 {
t.Fatalf("got %d hooks, want 1", len(hooks))
}
if hooks[0].ID != 7 || hooks[0].URL != "https://example.com/admin-hook" {
t.Errorf("unexpected hook: %+v", hooks[0])
}
}
func TestAdminService_CreateHook_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/hooks" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.CreateHookOption
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Type != "forgejo" {
t.Errorf("got type=%q, want %q", opts.Type, "forgejo")
}
json.NewEncoder(w).Encode(types.Hook{
ID: 12,
Type: opts.Type,
Active: opts.Active,
Events: opts.Events,
URL: "https://example.com/admin-hook",
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
hook, err := f.Admin.CreateHook(context.Background(), &types.CreateHookOption{
Type: "forgejo",
Active: true,
Events: []string{"push"},
})
if err != nil {
t.Fatal(err)
}
if hook.ID != 12 {
t.Errorf("got id=%d, want 12", hook.ID)
}
if hook.Type != "forgejo" {
t.Errorf("got type=%q, want %q", hook.Type, "forgejo")
}
}
func TestAdminService_GetHook_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/hooks/7" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.Hook{
ID: 7,
Type: "forgejo",
Active: true,
URL: "https://example.com/admin-hook",
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
hook, err := f.Admin.GetHook(context.Background(), 7)
if err != nil {
t.Fatal(err)
}
if hook.ID != 7 {
t.Errorf("got id=%d, want 7", hook.ID)
}
}
func TestAdminService_EditHook_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/hooks/7" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.EditHookOption
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if !opts.Active {
t.Error("expected active=true")
}
json.NewEncoder(w).Encode(types.Hook{
ID: 7,
Type: "forgejo",
Active: opts.Active,
URL: "https://example.com/admin-hook",
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
hook, err := f.Admin.EditHook(context.Background(), 7, &types.EditHookOption{Active: true})
if err != nil {
t.Fatal(err)
}
if hook.ID != 7 || !hook.Active {
t.Errorf("unexpected hook: %+v", hook)
}
}
func TestAdminService_DeleteHook_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/hooks/7" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Admin.DeleteHook(context.Background(), 7); err != nil {
t.Fatal(err)
}
}
func TestAdminService_ListQuotaGroups_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/groups" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode([]types.QuotaGroup{
{
Name: "default",
Rules: []*types.QuotaRuleInfo{
{Name: "git", Limit: 200000000, Subjects: []string{"size:repos:all"}},
},
},
{
Name: "premium",
},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
groups, err := f.Admin.ListQuotaGroups(context.Background())
if err != nil {
t.Fatal(err)
}
if len(groups) != 2 {
t.Fatalf("got %d groups, want 2", len(groups))
}
if groups[0].Name != "default" {
t.Errorf("got name=%q, want %q", groups[0].Name, "default")
}
if len(groups[0].Rules) != 1 || groups[0].Rules[0].Name != "git" {
t.Errorf("unexpected rules: %+v", groups[0].Rules)
}
}
func TestAdminService_IterQuotaGroups_Good(t *testing.T) {
var requests int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/groups" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode([]types.QuotaGroup{
{Name: "default"},
{Name: "premium"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var got []string
for group, err := range f.Admin.IterQuotaGroups(context.Background()) {
if err != nil {
t.Fatal(err)
}
got = append(got, group.Name)
}
if requests != 1 {
t.Fatalf("expected 1 request, got %d", requests)
}
if len(got) != 2 || got[0] != "default" || got[1] != "premium" {
t.Fatalf("got %#v", got)
}
}
func TestAdminService_CreateQuotaGroup_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/groups" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.CreateQuotaGroupOptions
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Name != "newgroup" {
t.Errorf("got name=%q, want %q", opts.Name, "newgroup")
}
if len(opts.Rules) != 1 || opts.Rules[0].Name != "git" {
t.Fatalf("unexpected rules: %+v", opts.Rules)
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(types.QuotaGroup{
Name: opts.Name,
Rules: []*types.QuotaRuleInfo{
{
Name: opts.Rules[0].Name,
Limit: opts.Rules[0].Limit,
Subjects: opts.Rules[0].Subjects,
},
},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
group, err := f.Admin.CreateQuotaGroup(context.Background(), &types.CreateQuotaGroupOptions{
Name: "newgroup",
Rules: []*types.CreateQuotaRuleOptions{
{
Name: "git",
Limit: 200000000,
Subjects: []string{"size:repos:all"},
},
},
})
if err != nil {
t.Fatal(err)
}
if group.Name != "newgroup" {
t.Errorf("got name=%q, want %q", group.Name, "newgroup")
}
if len(group.Rules) != 1 || group.Rules[0].Limit != 200000000 {
t.Errorf("unexpected rules: %+v", group.Rules)
}
}
func TestAdminService_GetQuotaGroup_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/groups/default" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.QuotaGroup{
Name: "default",
Rules: []*types.QuotaRuleInfo{
{Name: "git", Limit: 200000000, Subjects: []string{"size:repos:all"}},
},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
group, err := f.Admin.GetQuotaGroup(context.Background(), "default")
if err != nil {
t.Fatal(err)
}
if group.Name != "default" {
t.Errorf("got name=%q, want %q", group.Name, "default")
}
if len(group.Rules) != 1 || group.Rules[0].Name != "git" {
t.Fatalf("unexpected rules: %+v", group.Rules)
}
}
func TestAdminService_DeleteQuotaGroup_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/groups/default" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Admin.DeleteQuotaGroup(context.Background(), "default"); err != nil {
t.Fatal(err)
}
}
func TestAdminService_ListQuotaGroupUsers_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/groups/default/users" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode([]types.User{
{ID: 1, UserName: "alice"},
{ID: 2, UserName: "bob"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
users, err := f.Admin.ListQuotaGroupUsers(context.Background(), "default")
if err != nil {
t.Fatal(err)
}
if len(users) != 2 {
t.Fatalf("got %d users, want 2", len(users))
}
if users[0].UserName != "alice" {
t.Errorf("got username=%q, want %q", users[0].UserName, "alice")
}
}
func TestAdminService_AddQuotaGroupUser_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/groups/default/users/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Admin.AddQuotaGroupUser(context.Background(), "default", "alice"); err != nil {
t.Fatal(err)
}
}
func TestAdminService_RemoveQuotaGroupUser_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/groups/default/users/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Admin.RemoveQuotaGroupUser(context.Background(), "default", "alice"); err != nil {
t.Fatal(err)
}
}
func TestAdminService_ListQuotaRules_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/rules" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.QuotaRuleInfo{
{Name: "git", Limit: 200000000, Subjects: []string{"size:repos:all"}},
{Name: "artifacts", Limit: 50000000, Subjects: []string{"size:assets:artifacts"}},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
rules, err := f.Admin.ListQuotaRules(context.Background())
if err != nil {
t.Fatal(err)
}
if len(rules) != 2 {
t.Fatalf("got %d rules, want 2", len(rules))
}
if rules[0].Name != "git" {
t.Errorf("got name=%q, want %q", rules[0].Name, "git")
}
}
func TestAdminService_IterQuotaRules_Good(t *testing.T) {
var requests int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/rules" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode([]types.QuotaRuleInfo{
{Name: "git"},
{Name: "artifacts"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var got []string
for rule, err := range f.Admin.IterQuotaRules(context.Background()) {
if err != nil {
t.Fatal(err)
}
got = append(got, rule.Name)
}
if requests != 1 {
t.Fatalf("expected 1 request, got %d", requests)
}
if len(got) != 2 || got[0] != "git" || got[1] != "artifacts" {
t.Fatalf("got %#v", got)
}
}
func TestAdminService_CreateQuotaRule_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/rules" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.CreateQuotaRuleOptions
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Name != "git" || opts.Limit != 200000000 {
t.Fatalf("unexpected options: %+v", opts)
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(types.QuotaRuleInfo{
Name: opts.Name,
Limit: opts.Limit,
Subjects: opts.Subjects,
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
rule, err := f.Admin.CreateQuotaRule(context.Background(), &types.CreateQuotaRuleOptions{
Name: "git",
Limit: 200000000,
Subjects: []string{"size:repos:all"},
})
if err != nil {
t.Fatal(err)
}
if rule.Name != "git" || rule.Limit != 200000000 {
t.Errorf("unexpected rule: %+v", rule)
}
}
func TestAdminService_GetQuotaRule_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/rules/git" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.QuotaRuleInfo{
Name: "git",
Limit: 200000000,
Subjects: []string{"size:repos:all"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
rule, err := f.Admin.GetQuotaRule(context.Background(), "git")
if err != nil {
t.Fatal(err)
}
if rule.Name != "git" {
t.Errorf("got name=%q, want %q", rule.Name, "git")
}
}
func TestAdminService_EditQuotaRule_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/rules/git" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.EditQuotaRuleOptions
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Limit != 500000000 {
t.Fatalf("unexpected options: %+v", opts)
}
json.NewEncoder(w).Encode(types.QuotaRuleInfo{
Name: "git",
Limit: opts.Limit,
Subjects: opts.Subjects,
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
rule, err := f.Admin.EditQuotaRule(context.Background(), "git", &types.EditQuotaRuleOptions{
Limit: 500000000,
Subjects: []string{"size:repos:all", "size:assets:packages"},
})
if err != nil {
t.Fatal(err)
}
if rule.Limit != 500000000 {
t.Errorf("got limit=%d, want 500000000", rule.Limit)
}
}
func TestAdminService_DeleteQuotaRule_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/quota/rules/git" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Admin.DeleteQuotaRule(context.Background(), "git"); err != nil {
t.Fatal(err)
}
}
func TestAdminService_SearchEmails_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/emails/search" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("q"); got != "alice" {
t.Errorf("got q=%q, want %q", got, "alice")
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Email{
{Email: "alice@example.com", Primary: true},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
emails, err := f.Admin.SearchEmails(context.Background(), "alice")
if err != nil {
t.Fatal(err)
}
if len(emails) != 1 {
t.Errorf("got %d emails, want 1", len(emails))
}
if emails[0].Email != "alice@example.com" {
t.Errorf("got email=%q, want %q", emails[0].Email, "alice@example.com")
}
}
func TestAdminService_ListCron_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -849,7 +223,7 @@ func TestAdminService_ListCron_Good(t *testing.T) {
} }
} }
func TestAdminService_AdoptRepo_Good(t *testing.T) { func TestAdminService_Good_AdoptRepo(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -867,153 +241,7 @@ func TestAdminService_AdoptRepo_Good(t *testing.T) {
} }
} }
func TestAdminService_ListUnadoptedRepos_Good(t *testing.T) { func TestAdminService_Good_GenerateRunnerToken(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/unadopted" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
if got := r.URL.Query().Get("pattern"); got != "core/*" {
t.Errorf("got pattern=%q, want %q", got, "core/*")
}
if got := r.URL.Query().Get("page"); got != "1" {
t.Errorf("got page=%q, want %q", got, "1")
}
if got := r.URL.Query().Get("limit"); got != "50" {
t.Errorf("got limit=%q, want %q", got, "50")
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]string{"core/myrepo"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
repos, err := f.Admin.ListUnadoptedRepos(context.Background(), AdminUnadoptedListOptions{Pattern: "core/*"})
if err != nil {
t.Fatal(err)
}
if len(repos) != 1 || repos[0] != "core/myrepo" {
t.Fatalf("unexpected result: %#v", repos)
}
}
func TestAdminService_DeleteUnadoptedRepo_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/unadopted/alice/myrepo" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Admin.DeleteUnadoptedRepo(context.Background(), "alice", "myrepo"); err != nil {
t.Fatal(err)
}
}
func TestAdminService_ListActionsRuns_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/admin/actions/runs" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("status"); got != "in_progress" {
t.Errorf("got status=%q, want %q", got, "in_progress")
}
if got := r.URL.Query().Get("branch"); got != "main" {
t.Errorf("got branch=%q, want %q", got, "main")
}
if got := r.URL.Query().Get("actor"); got != "alice" {
t.Errorf("got actor=%q, want %q", got, "alice")
}
if got := r.URL.Query().Get("page"); got != "2" {
t.Errorf("got page=%q, want %q", got, "2")
}
if got := r.URL.Query().Get("limit"); got != "25" {
t.Errorf("got limit=%q, want %q", got, "25")
}
w.Header().Set("X-Total-Count", "3")
json.NewEncoder(w).Encode(types.ActionTaskResponse{
Entries: []*types.ActionTask{
{ID: 101, Name: "build", Status: "in_progress", Event: "push"},
{ID: 102, Name: "test", Status: "queued", Event: "push"},
},
TotalCount: 3,
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
result, err := f.Admin.ListActionsRuns(context.Background(), AdminActionsRunListOptions{
Status: "in_progress",
Branch: "main",
Actor: "alice",
}, ListOptions{Page: 2, Limit: 25})
if err != nil {
t.Fatal(err)
}
if result.TotalCount != 3 {
t.Fatalf("got total count=%d, want 3", result.TotalCount)
}
if len(result.Items) != 2 {
t.Fatalf("got %d runs, want 2", len(result.Items))
}
if result.Items[0].ID != 101 || result.Items[0].Name != "build" {
t.Errorf("unexpected first run: %+v", result.Items[0])
}
}
func TestAdminService_IterActionsRuns_Good(t *testing.T) {
calls := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if r.URL.Path != "/api/v1/admin/actions/runs" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "2")
switch calls {
case 1:
json.NewEncoder(w).Encode(types.ActionTaskResponse{
Entries: []*types.ActionTask{
{ID: 201, Name: "build"},
},
TotalCount: 2,
})
default:
json.NewEncoder(w).Encode(types.ActionTaskResponse{
Entries: []*types.ActionTask{
{ID: 202, Name: "test"},
},
TotalCount: 2,
})
}
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var ids []int64
for run, err := range f.Admin.IterActionsRuns(context.Background(), AdminActionsRunListOptions{}) {
if err != nil {
t.Fatal(err)
}
ids = append(ids, run.ID)
}
if len(ids) != 2 || ids[0] != 201 || ids[1] != 202 {
t.Fatalf("unexpected run ids: %v", ids)
}
}
func TestAdminService_GenerateRunnerToken_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -1035,7 +263,7 @@ func TestAdminService_GenerateRunnerToken_Good(t *testing.T) {
} }
} }
func TestAdminService_DeleteUser_NotFound_Bad(t *testing.T) { func TestAdminService_Bad_DeleteUser_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "user not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "user not found"})
@ -1049,7 +277,7 @@ func TestAdminService_DeleteUser_NotFound_Bad(t *testing.T) {
} }
} }
func TestAdminService_CreateUser_Forbidden_Bad(t *testing.T) { func TestAdminService_Bad_CreateUser_Forbidden(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
json.NewEncoder(w).Encode(map[string]string{"message": "only admins can create users"}) json.NewEncoder(w).Encode(map[string]string{"message": "only admins can create users"})

View file

@ -1,266 +0,0 @@
package forge
import (
"fmt"
"testing"
"time"
)
func TestParams_String_Good(t *testing.T) {
params := Params{"repo": "go-forge", "owner": "core"}
want := `forge.Params{owner="core", repo="go-forge"}`
if got := params.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(params); got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", params); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestParams_String_NilSafe(t *testing.T) {
var params Params
want := "forge.Params{<nil>}"
if got := params.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(params); got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", params); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestListOptions_String_Good(t *testing.T) {
opts := ListOptions{Page: 2, Limit: 25}
want := "forge.ListOptions{page=2, limit=25}"
if got := opts.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(opts); got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", opts); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestRateLimit_String_Good(t *testing.T) {
rl := RateLimit{Limit: 80, Remaining: 79, Reset: 1700000003}
want := "forge.RateLimit{limit=80, remaining=79, reset=1700000003}"
if got := rl.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(rl); got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", rl); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestPagedResult_String_Good(t *testing.T) {
page := PagedResult[int]{
Items: []int{1, 2, 3},
TotalCount: 10,
Page: 2,
HasMore: true,
}
want := "forge.PagedResult{items=3, totalCount=10, page=2, hasMore=true}"
if got := page.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(page); got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", page); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestOption_Stringers_Good(t *testing.T) {
when := time.Date(2026, time.April, 2, 8, 3, 4, 0, time.UTC)
cases := []struct {
name string
got fmt.Stringer
want string
}{
{
name: "AdminActionsRunListOptions",
got: AdminActionsRunListOptions{Event: "push", Status: "success"},
want: `forge.AdminActionsRunListOptions{event="push", status="success"}`,
},
{
name: "AttachmentUploadOptions",
got: AttachmentUploadOptions{Name: "screenshot.png", UpdatedAt: &when},
want: `forge.AttachmentUploadOptions{name="screenshot.png", updated_at="2026-04-02T08:03:04Z"}`,
},
{
name: "NotificationListOptions",
got: NotificationListOptions{All: true, StatusTypes: []string{"unread"}, SubjectTypes: []string{"issue"}},
want: `forge.NotificationListOptions{all=true, status_types=[]string{"unread"}, subject_types=[]string{"issue"}}`,
},
{
name: "SearchIssuesOptions",
got: SearchIssuesOptions{State: "open", PriorityRepoID: 99, Assigned: true, Query: "build"},
want: `forge.SearchIssuesOptions{state="open", q="build", priority_repo_id=99, assigned=true}`,
},
{
name: "IssueListOptions",
got: IssueListOptions{State: "open", Labels: "bug", Query: "panic", CreatedBy: "alice"},
want: `forge.IssueListOptions{state="open", labels="bug", q="panic", created_by="alice"}`,
},
{
name: "PullListOptions",
got: PullListOptions{State: "open", Sort: "priority", Milestone: 7, Labels: []int64{1, 2}, Poster: "alice"},
want: `forge.PullListOptions{state="open", sort="priority", milestone=7, labels=[]int64{1, 2}, poster="alice"}`,
},
{
name: "ReleaseListOptions",
got: ReleaseListOptions{Draft: true, PreRelease: true, Query: "1.0"},
want: `forge.ReleaseListOptions{draft=true, pre-release=true, q="1.0"}`,
},
{
name: "CommitListOptions",
got: func() CommitListOptions {
stat := false
verification := false
files := false
return CommitListOptions{
Sha: "main",
Path: "docs",
Stat: &stat,
Verification: &verification,
Files: &files,
Not: "deadbeef",
}
}(),
want: `forge.CommitListOptions{sha="main", path="docs", stat=false, verification=false, files=false, not="deadbeef"}`,
},
{
name: "ReleaseAttachmentUploadOptions",
got: ReleaseAttachmentUploadOptions{Name: "release.zip"},
want: `forge.ReleaseAttachmentUploadOptions{name="release.zip"}`,
},
{
name: "UserSearchOptions",
got: UserSearchOptions{UID: 1001},
want: `forge.UserSearchOptions{uid=1001}`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.got.String(); got != tc.want {
t.Fatalf("got String()=%q, want %q", got, tc.want)
}
if got := fmt.Sprint(tc.got); got != tc.want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, tc.want)
}
if got := fmt.Sprintf("%#v", tc.got); got != tc.want {
t.Fatalf("got GoString=%q, want %q", got, tc.want)
}
})
}
}
func TestOption_Stringers_Empty(t *testing.T) {
cases := []struct {
name string
got fmt.Stringer
want string
}{
{
name: "AdminUnadoptedListOptions",
got: AdminUnadoptedListOptions{},
want: `forge.AdminUnadoptedListOptions{}`,
},
{
name: "MilestoneListOptions",
got: MilestoneListOptions{},
want: `forge.MilestoneListOptions{}`,
},
{
name: "UserKeyListOptions",
got: UserKeyListOptions{},
want: `forge.UserKeyListOptions{}`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.got.String(); got != tc.want {
t.Fatalf("got String()=%q, want %q", got, tc.want)
}
if got := fmt.Sprint(tc.got); got != tc.want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, tc.want)
}
if got := fmt.Sprintf("%#v", tc.got); got != tc.want {
t.Fatalf("got GoString=%q, want %q", got, tc.want)
}
})
}
}
func TestService_Stringers_Good(t *testing.T) {
client := NewClient("https://forge.example", "token")
cases := []struct {
name string
got fmt.Stringer
want string
}{
{
name: "RepoService",
got: newRepoService(client),
want: `forge.RepoService{resource=forge.Resource{path="/api/v1/repos/{owner}/{repo}", collection="/api/v1/repos/{owner}"}}`,
},
{
name: "AdminService",
got: newAdminService(client),
want: `forge.AdminService{client=forge.Client{baseURL="https://forge.example", token=set, userAgent="go-forge/0.1"}}`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.got.String(); got != tc.want {
t.Fatalf("got String()=%q, want %q", got, tc.want)
}
if got := fmt.Sprint(tc.got); got != tc.want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, tc.want)
}
if got := fmt.Sprintf("%#v", tc.got); got != tc.want {
t.Fatalf("got GoString=%q, want %q", got, tc.want)
}
})
}
}
func TestService_Stringers_NilSafe(t *testing.T) {
var repo *RepoService
if got, want := repo.String(), "forge.RepoService{<nil>}"; got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got, want := fmt.Sprint(repo), "forge.RepoService{<nil>}"; got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got, want := fmt.Sprintf("%#v", repo), "forge.RepoService{<nil>}"; got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
var admin *AdminService
if got, want := admin.String(), "forge.AdminService{<nil>}"; got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got, want := fmt.Sprint(admin), "forge.AdminService{<nil>}"; got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got, want := fmt.Sprintf("%#v", admin), "forge.AdminService{<nil>}"; got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}

View file

@ -2,87 +2,40 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// BranchService handles branch operations within a repository. // BranchService handles branch operations within a repository.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Branches.ListBranchProtections(ctx, "core", "go-forge")
type BranchService struct { type BranchService struct {
Resource[types.Branch, types.CreateBranchRepoOption, types.UpdateBranchRepoOption] Resource[types.Branch, types.CreateBranchRepoOption, struct{}]
} }
func newBranchService(c *Client) *BranchService { func newBranchService(c *Client) *BranchService {
return &BranchService{ return &BranchService{
Resource: *NewResource[types.Branch, types.CreateBranchRepoOption, types.UpdateBranchRepoOption]( Resource: *NewResource[types.Branch, types.CreateBranchRepoOption, struct{}](
c, "/api/v1/repos/{owner}/{repo}/branches/{branch}", c, "/api/v1/repos/{owner}/{repo}/branches/{branch}",
), ),
} }
} }
// ListBranches returns all branches for a repository.
func (s *BranchService) ListBranches(ctx context.Context, owner, repo string) ([]types.Branch, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branches", pathParams("owner", owner, "repo", repo))
return ListAll[types.Branch](ctx, s.client, path, nil)
}
// IterBranches returns an iterator over all branches for a repository.
func (s *BranchService) IterBranches(ctx context.Context, owner, repo string) iter.Seq2[types.Branch, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branches", pathParams("owner", owner, "repo", repo))
return ListIter[types.Branch](ctx, s.client, path, nil)
}
// CreateBranch creates a new branch in a repository.
func (s *BranchService) CreateBranch(ctx context.Context, owner, repo string, opts *types.CreateBranchRepoOption) (*types.Branch, error) {
var out types.Branch
if err := s.client.Post(ctx, ResolvePath("/api/v1/repos/{owner}/{repo}/branches", pathParams("owner", owner, "repo", repo)), opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// GetBranch returns a single branch by name.
func (s *BranchService) GetBranch(ctx context.Context, owner, repo, branch string) (*types.Branch, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branches/{branch}", pathParams("owner", owner, "repo", repo, "branch", branch))
var out types.Branch
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// UpdateBranch renames a branch in a repository.
func (s *BranchService) UpdateBranch(ctx context.Context, owner, repo, branch string, opts *types.UpdateBranchRepoOption) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branches/{branch}", pathParams("owner", owner, "repo", repo, "branch", branch))
return s.client.Patch(ctx, path, opts, nil)
}
// DeleteBranch removes a branch from a repository.
func (s *BranchService) DeleteBranch(ctx context.Context, owner, repo, branch string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branches/{branch}", pathParams("owner", owner, "repo", repo, "branch", branch))
return s.client.Delete(ctx, path)
}
// ListBranchProtections returns all branch protections for a repository. // ListBranchProtections returns all branch protections for a repository.
func (s *BranchService) ListBranchProtections(ctx context.Context, owner, repo string) ([]types.BranchProtection, error) { func (s *BranchService) ListBranchProtections(ctx context.Context, owner, repo string) ([]types.BranchProtection, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
return ListAll[types.BranchProtection](ctx, s.client, path, nil) return ListAll[types.BranchProtection](ctx, s.client, path, nil)
} }
// IterBranchProtections returns an iterator over all branch protections for a repository. // IterBranchProtections returns an iterator over all branch protections for a repository.
func (s *BranchService) IterBranchProtections(ctx context.Context, owner, repo string) iter.Seq2[types.BranchProtection, error] { func (s *BranchService) IterBranchProtections(ctx context.Context, owner, repo string) iter.Seq2[types.BranchProtection, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
return ListIter[types.BranchProtection](ctx, s.client, path, nil) return ListIter[types.BranchProtection](ctx, s.client, path, nil)
} }
// GetBranchProtection returns a single branch protection by name. // GetBranchProtection returns a single branch protection by name.
func (s *BranchService) GetBranchProtection(ctx context.Context, owner, repo, name string) (*types.BranchProtection, error) { func (s *BranchService) GetBranchProtection(ctx context.Context, owner, repo, name string) (*types.BranchProtection, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections/{name}", pathParams("owner", owner, "repo", repo, "name", name)) path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
var out types.BranchProtection var out types.BranchProtection
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -92,7 +45,7 @@ func (s *BranchService) GetBranchProtection(ctx context.Context, owner, repo, na
// CreateBranchProtection creates a new branch protection rule. // CreateBranchProtection creates a new branch protection rule.
func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo string, opts *types.CreateBranchProtectionOption) (*types.BranchProtection, error) { func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo string, opts *types.CreateBranchProtectionOption) (*types.BranchProtection, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
var out types.BranchProtection var out types.BranchProtection
if err := s.client.Post(ctx, path, opts, &out); err != nil { if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err return nil, err
@ -102,7 +55,7 @@ func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo
// EditBranchProtection updates an existing branch protection rule. // EditBranchProtection updates an existing branch protection rule.
func (s *BranchService) EditBranchProtection(ctx context.Context, owner, repo, name string, opts *types.EditBranchProtectionOption) (*types.BranchProtection, error) { func (s *BranchService) EditBranchProtection(ctx context.Context, owner, repo, name string, opts *types.EditBranchProtectionOption) (*types.BranchProtection, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections/{name}", pathParams("owner", owner, "repo", repo, "name", name)) path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
var out types.BranchProtection var out types.BranchProtection
if err := s.client.Patch(ctx, path, opts, &out); err != nil { if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err return nil, err
@ -112,6 +65,6 @@ func (s *BranchService) EditBranchProtection(ctx context.Context, owner, repo, n
// DeleteBranchProtection deletes a branch protection rule. // DeleteBranchProtection deletes a branch protection rule.
func (s *BranchService) DeleteBranchProtection(ctx context.Context, owner, repo, name string) error { func (s *BranchService) DeleteBranchProtection(ctx context.Context, owner, repo, name string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/branch_protections/{name}", pathParams("owner", owner, "repo", repo, "name", name)) path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }

View file

@ -1,66 +0,0 @@
package forge
import (
"context"
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"testing"
"dappco.re/go/core/forge/types"
)
func TestBranchService_ListBranches_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/branches" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Branch{{Name: "main"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
branches, err := f.Branches.ListBranches(context.Background(), "core", "go-forge")
if err != nil {
t.Fatal(err)
}
if len(branches) != 1 || branches[0].Name != "main" {
t.Fatalf("got %#v", branches)
}
}
func TestBranchService_CreateBranch_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/branches" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var body types.CreateBranchRepoOption
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.BranchName != "release/v1" {
t.Fatalf("unexpected body: %+v", body)
}
json.NewEncoder(w).Encode(types.Branch{Name: body.BranchName})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
branch, err := f.Branches.CreateBranch(context.Background(), "core", "go-forge", &types.CreateBranchRepoOption{
BranchName: "release/v1",
OldRefName: "main",
})
if err != nil {
t.Fatal(err)
}
if branch.Name != "release/v1" {
t.Fatalf("got name=%q", branch.Name)
}
}

View file

@ -2,7 +2,7 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -10,7 +10,7 @@ import (
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestBranchService_List_Good(t *testing.T) { func TestBranchService_Good_List(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -36,7 +36,7 @@ func TestBranchService_List_Good(t *testing.T) {
} }
} }
func TestBranchService_Get_Good(t *testing.T) { func TestBranchService_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -61,34 +61,7 @@ func TestBranchService_Get_Good(t *testing.T) {
} }
} }
func TestBranchService_UpdateBranch_Good(t *testing.T) { func TestBranchService_Good_CreateProtection(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/branches/main" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.UpdateBranchRepoOption
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Name != "develop" {
t.Errorf("got name=%q, want %q", opts.Name, "develop")
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Branches.UpdateBranch(context.Background(), "core", "go-forge", "main", &types.UpdateBranchRepoOption{
Name: "develop",
}); err != nil {
t.Fatal(err)
}
}
func TestBranchService_CreateProtection_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)

376
client.go
View file

@ -3,160 +3,67 @@ package forge
import ( import (
"bytes" "bytes"
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"mime/multipart" "errors"
"fmt"
"io"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings"
goio "io" coreerr "dappco.re/go/core/log"
core "dappco.re/go/core"
) )
// APIError represents an error response from the Forgejo API. // APIError represents an error response from the Forgejo API.
//
// Usage:
//
// if apiErr, ok := err.(*forge.APIError); ok {
// _ = apiErr.StatusCode
// }
type APIError struct { type APIError struct {
StatusCode int StatusCode int
Message string Message string
URL string URL string
} }
// Error returns the formatted Forge API error string.
//
// Usage:
//
// err := (&forge.APIError{StatusCode: 404, Message: "not found", URL: "/api/v1/repos/x/y"}).Error()
func (e *APIError) Error() string { func (e *APIError) Error() string {
if e == nil { return fmt.Sprintf("forge: %s %d: %s", e.URL, e.StatusCode, e.Message)
return "forge.APIError{<nil>}"
}
return core.Concat("forge: ", e.URL, " ", strconv.Itoa(e.StatusCode), ": ", e.Message)
} }
// String returns a safe summary of the API error.
//
// Usage:
//
// s := err.String()
func (e *APIError) String() string { return e.Error() }
// GoString returns a safe Go-syntax summary of the API error.
//
// Usage:
//
// s := fmt.Sprintf("%#v", err)
func (e *APIError) GoString() string { return e.Error() }
// IsNotFound returns true if the error is a 404 response. // IsNotFound returns true if the error is a 404 response.
//
// Usage:
//
// if forge.IsNotFound(err) {
// return nil
// }
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
var apiErr *APIError var apiErr *APIError
return core.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound
} }
// IsForbidden returns true if the error is a 403 response. // IsForbidden returns true if the error is a 403 response.
//
// Usage:
//
// if forge.IsForbidden(err) {
// return nil
// }
func IsForbidden(err error) bool { func IsForbidden(err error) bool {
var apiErr *APIError var apiErr *APIError
return core.As(err, &apiErr) && apiErr.StatusCode == http.StatusForbidden return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusForbidden
} }
// IsConflict returns true if the error is a 409 response. // IsConflict returns true if the error is a 409 response.
//
// Usage:
//
// if forge.IsConflict(err) {
// return nil
// }
func IsConflict(err error) bool { func IsConflict(err error) bool {
var apiErr *APIError var apiErr *APIError
return core.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict
} }
// Option configures the Client. // Option configures the Client.
//
// Usage:
//
// opts := []forge.Option{forge.WithUserAgent("go-forge/1.0")}
type Option func(*Client) type Option func(*Client)
// WithHTTPClient sets a custom http.Client. // WithHTTPClient sets a custom http.Client.
//
// Usage:
//
// c := forge.NewClient(url, token, forge.WithHTTPClient(http.DefaultClient))
func WithHTTPClient(hc *http.Client) Option { func WithHTTPClient(hc *http.Client) Option {
return func(c *Client) { c.httpClient = hc } return func(c *Client) { c.httpClient = hc }
} }
// WithUserAgent sets the User-Agent header. // WithUserAgent sets the User-Agent header.
//
// Usage:
//
// c := forge.NewClient(url, token, forge.WithUserAgent("go-forge/1.0"))
func WithUserAgent(ua string) Option { func WithUserAgent(ua string) Option {
return func(c *Client) { c.userAgent = ua } return func(c *Client) { c.userAgent = ua }
} }
// RateLimit represents the rate limit information from the Forgejo API. // RateLimit represents the rate limit information from the Forgejo API.
//
// Usage:
//
// rl := client.RateLimit()
// _ = rl.Remaining
type RateLimit struct { type RateLimit struct {
Limit int Limit int
Remaining int Remaining int
Reset int64 Reset int64
} }
// String returns a safe summary of the rate limit state.
//
// Usage:
//
// rl := client.RateLimit()
// _ = rl.String()
func (r RateLimit) String() string {
return core.Concat(
"forge.RateLimit{limit=",
strconv.Itoa(r.Limit),
", remaining=",
strconv.Itoa(r.Remaining),
", reset=",
strconv.FormatInt(r.Reset, 10),
"}",
)
}
// GoString returns a safe Go-syntax summary of the rate limit state.
//
// Usage:
//
// _ = fmt.Sprintf("%#v", client.RateLimit())
func (r RateLimit) GoString() string { return r.String() }
// Client is a low-level HTTP client for the Forgejo API. // Client is a low-level HTTP client for the Forgejo API.
//
// Usage:
//
// c := forge.NewClient("https://forge.lthn.ai", "token")
// _ = c
type Client struct { type Client struct {
baseURL string baseURL string
token string token string
@ -165,100 +72,15 @@ type Client struct {
rateLimit RateLimit rateLimit RateLimit
} }
// BaseURL returns the configured Forgejo base URL.
//
// Usage:
//
// baseURL := client.BaseURL()
func (c *Client) BaseURL() string {
if c == nil {
return ""
}
return c.baseURL
}
// RateLimit returns the last known rate limit information. // RateLimit returns the last known rate limit information.
//
// Usage:
//
// rl := client.RateLimit()
func (c *Client) RateLimit() RateLimit { func (c *Client) RateLimit() RateLimit {
if c == nil {
return RateLimit{}
}
return c.rateLimit return c.rateLimit
} }
// UserAgent returns the configured User-Agent header value.
//
// Usage:
//
// ua := client.UserAgent()
func (c *Client) UserAgent() string {
if c == nil {
return ""
}
return c.userAgent
}
// HTTPClient returns the configured underlying HTTP client.
//
// Usage:
//
// hc := client.HTTPClient()
func (c *Client) HTTPClient() *http.Client {
if c == nil {
return nil
}
return c.httpClient
}
// String returns a safe summary of the client configuration.
//
// Usage:
//
// s := client.String()
func (c *Client) String() string {
if c == nil {
return "forge.Client{<nil>}"
}
tokenState := "unset"
if c.HasToken() {
tokenState = "set"
}
return core.Concat("forge.Client{baseURL=", strconv.Quote(c.baseURL), ", token=", tokenState, ", userAgent=", strconv.Quote(c.userAgent), "}")
}
// GoString returns a safe Go-syntax summary of the client configuration.
//
// Usage:
//
// s := fmt.Sprintf("%#v", client)
func (c *Client) GoString() string { return c.String() }
// HasToken reports whether the client was configured with an API token.
//
// Usage:
//
// if c.HasToken() {
// _ = "authenticated"
// }
func (c *Client) HasToken() bool {
if c == nil {
return false
}
return c.token != ""
}
// NewClient creates a new Forgejo API client. // NewClient creates a new Forgejo API client.
//
// Usage:
//
// c := forge.NewClient("https://forge.lthn.ai", "token")
// _ = c
func NewClient(url, token string, opts ...Option) *Client { func NewClient(url, token string, opts ...Option) *Client {
c := &Client{ c := &Client{
baseURL: trimTrailingSlashes(url), baseURL: strings.TrimRight(url, "/"),
token: token, token: token,
httpClient: &http.Client{ httpClient: &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
@ -274,64 +96,36 @@ func NewClient(url, token string, opts ...Option) *Client {
} }
// Get performs a GET request. // Get performs a GET request.
//
// Usage:
//
// var out map[string]string
// err := client.Get(ctx, "/api/v1/user", &out)
func (c *Client) Get(ctx context.Context, path string, out any) error { func (c *Client) Get(ctx context.Context, path string, out any) error {
_, err := c.doJSON(ctx, http.MethodGet, path, nil, out) _, err := c.doJSON(ctx, http.MethodGet, path, nil, out)
return err return err
} }
// Post performs a POST request. // Post performs a POST request.
//
// Usage:
//
// var out map[string]any
// err := client.Post(ctx, "/api/v1/orgs/core/repos", body, &out)
func (c *Client) Post(ctx context.Context, path string, body, out any) error { func (c *Client) Post(ctx context.Context, path string, body, out any) error {
_, err := c.doJSON(ctx, http.MethodPost, path, body, out) _, err := c.doJSON(ctx, http.MethodPost, path, body, out)
return err return err
} }
// Patch performs a PATCH request. // Patch performs a PATCH request.
//
// Usage:
//
// var out map[string]any
// err := client.Patch(ctx, "/api/v1/repos/core/go-forge", body, &out)
func (c *Client) Patch(ctx context.Context, path string, body, out any) error { func (c *Client) Patch(ctx context.Context, path string, body, out any) error {
_, err := c.doJSON(ctx, http.MethodPatch, path, body, out) _, err := c.doJSON(ctx, http.MethodPatch, path, body, out)
return err return err
} }
// Put performs a PUT request. // Put performs a PUT request.
//
// Usage:
//
// var out map[string]any
// err := client.Put(ctx, "/api/v1/repos/core/go-forge", body, &out)
func (c *Client) Put(ctx context.Context, path string, body, out any) error { func (c *Client) Put(ctx context.Context, path string, body, out any) error {
_, err := c.doJSON(ctx, http.MethodPut, path, body, out) _, err := c.doJSON(ctx, http.MethodPut, path, body, out)
return err return err
} }
// Delete performs a DELETE request. // Delete performs a DELETE request.
//
// Usage:
//
// err := client.Delete(ctx, "/api/v1/repos/core/go-forge")
func (c *Client) Delete(ctx context.Context, path string) error { func (c *Client) Delete(ctx context.Context, path string) error {
_, err := c.doJSON(ctx, http.MethodDelete, path, nil, nil) _, err := c.doJSON(ctx, http.MethodDelete, path, nil, nil)
return err return err
} }
// DeleteWithBody performs a DELETE request with a JSON body. // DeleteWithBody performs a DELETE request with a JSON body.
//
// Usage:
//
// err := client.DeleteWithBody(ctx, "/api/v1/repos/core/go-forge/labels", body)
func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) error { func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) error {
_, err := c.doJSON(ctx, http.MethodDelete, path, body, nil) _, err := c.doJSON(ctx, http.MethodDelete, path, body, nil)
return err return err
@ -340,29 +134,21 @@ func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) erro
// PostRaw performs a POST request with a JSON body and returns the raw // PostRaw performs a POST request with a JSON body and returns the raw
// response body as bytes instead of JSON-decoding. Useful for endpoints // response body as bytes instead of JSON-decoding. Useful for endpoints
// such as /markdown that return raw HTML text. // such as /markdown that return raw HTML text.
//
// Usage:
//
// body, err := client.PostRaw(ctx, "/api/v1/markdown", payload)
func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, error) { func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, error) {
return c.postRawJSON(ctx, path, body)
}
func (c *Client) postRawJSON(ctx context.Context, path string, body any) ([]byte, error) {
url := c.baseURL + path url := c.baseURL + path
var bodyReader goio.Reader var bodyReader io.Reader
if body != nil { if body != nil {
data, err := json.Marshal(body) data, err := json.Marshal(body)
if err != nil { if err != nil {
return nil, core.E("Client.PostRaw", "forge: marshal body", err) return nil, coreerr.E("Client.PostRaw", "forge: marshal body", err)
} }
bodyReader = bytes.NewReader(data) bodyReader = bytes.NewReader(data)
} }
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader)
if err != nil { if err != nil {
return nil, core.E("Client.PostRaw", "forge: create request", err) return nil, coreerr.E("Client.PostRaw", "forge: create request", err)
} }
req.Header.Set("Authorization", "token "+c.token) req.Header.Set("Authorization", "token "+c.token)
@ -373,136 +159,30 @@ func (c *Client) postRawJSON(ctx context.Context, path string, body any) ([]byte
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
return nil, core.E("Client.PostRaw", "forge: request POST "+path, err) return nil, coreerr.E("Client.PostRaw", "forge: request POST "+path, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
c.updateRateLimit(resp)
if resp.StatusCode >= 400 { if resp.StatusCode >= 400 {
return nil, c.parseError(resp, path) return nil, c.parseError(resp, path)
} }
data, err := goio.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, core.E("Client.PostRaw", "forge: read response body", err) return nil, coreerr.E("Client.PostRaw", "forge: read response body", err)
} }
return data, nil return data, nil
} }
func (c *Client) postRawText(ctx context.Context, path, body string) ([]byte, error) {
url := c.baseURL + path
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBufferString(body))
if err != nil {
return nil, core.E("Client.PostText", "forge: create request", err)
}
req.Header.Set("Authorization", "token "+c.token)
req.Header.Set("Accept", "text/html")
req.Header.Set("Content-Type", "text/plain")
if c.userAgent != "" {
req.Header.Set("User-Agent", c.userAgent)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, core.E("Client.PostText", "forge: request POST "+path, err)
}
defer resp.Body.Close()
c.updateRateLimit(resp)
if resp.StatusCode >= 400 {
return nil, c.parseError(resp, path)
}
data, err := goio.ReadAll(resp.Body)
if err != nil {
return nil, core.E("Client.PostText", "forge: read response body", err)
}
return data, nil
}
func (c *Client) postMultipartJSON(ctx context.Context, path string, query map[string]string, fields map[string]string, fieldName, fileName string, content goio.Reader, out any) error {
target, err := url.Parse(c.baseURL + path)
if err != nil {
return core.E("Client.PostMultipart", "forge: parse url", err)
}
if len(query) > 0 {
values := target.Query()
for key, value := range query {
values.Set(key, value)
}
target.RawQuery = values.Encode()
}
var body bytes.Buffer
writer := multipart.NewWriter(&body)
for key, value := range fields {
if err := writer.WriteField(key, value); err != nil {
return core.E("Client.PostMultipart", "forge: create multipart form field", err)
}
}
if fieldName != "" {
part, err := writer.CreateFormFile(fieldName, fileName)
if err != nil {
return core.E("Client.PostMultipart", "forge: create multipart form file", err)
}
if content != nil {
if _, err := goio.Copy(part, content); err != nil {
return core.E("Client.PostMultipart", "forge: write multipart form file", err)
}
}
}
if err := writer.Close(); err != nil {
return core.E("Client.PostMultipart", "forge: close multipart writer", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), &body)
if err != nil {
return core.E("Client.PostMultipart", "forge: create request", err)
}
req.Header.Set("Authorization", "token "+c.token)
req.Header.Set("Content-Type", writer.FormDataContentType())
if c.userAgent != "" {
req.Header.Set("User-Agent", c.userAgent)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return core.E("Client.PostMultipart", "forge: request POST "+path, err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return c.parseError(resp, path)
}
if out == nil {
return nil
}
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return core.E("Client.PostMultipart", "forge: decode response body", err)
}
return nil
}
// GetRaw performs a GET request and returns the raw response body as bytes // GetRaw performs a GET request and returns the raw response body as bytes
// instead of JSON-decoding. Useful for endpoints that return raw file content. // instead of JSON-decoding. Useful for endpoints that return raw file content.
//
// Usage:
//
// body, err := client.GetRaw(ctx, "/api/v1/signing-key.gpg")
func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) { func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) {
url := c.baseURL + path url := c.baseURL + path
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, core.E("Client.GetRaw", "forge: create request", err) return nil, coreerr.E("Client.GetRaw", "forge: create request", err)
} }
req.Header.Set("Authorization", "token "+c.token) req.Header.Set("Authorization", "token "+c.token)
@ -512,19 +192,17 @@ func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) {
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
return nil, core.E("Client.GetRaw", "forge: request GET "+path, err) return nil, coreerr.E("Client.GetRaw", "forge: request GET "+path, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
c.updateRateLimit(resp)
if resp.StatusCode >= 400 { if resp.StatusCode >= 400 {
return nil, c.parseError(resp, path) return nil, c.parseError(resp, path)
} }
data, err := goio.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, core.E("Client.GetRaw", "forge: read response body", err) return nil, coreerr.E("Client.GetRaw", "forge: read response body", err)
} }
return data, nil return data, nil
@ -538,18 +216,18 @@ func (c *Client) do(ctx context.Context, method, path string, body, out any) err
func (c *Client) doJSON(ctx context.Context, method, path string, body, out any) (*http.Response, error) { func (c *Client) doJSON(ctx context.Context, method, path string, body, out any) (*http.Response, error) {
url := c.baseURL + path url := c.baseURL + path
var bodyReader goio.Reader var bodyReader io.Reader
if body != nil { if body != nil {
data, err := json.Marshal(body) data, err := json.Marshal(body)
if err != nil { if err != nil {
return nil, core.E("Client.doJSON", "forge: marshal body", err) return nil, coreerr.E("Client.doJSON", "forge: marshal body", err)
} }
bodyReader = bytes.NewReader(data) bodyReader = bytes.NewReader(data)
} }
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
if err != nil { if err != nil {
return nil, core.E("Client.doJSON", "forge: create request", err) return nil, coreerr.E("Client.doJSON", "forge: create request", err)
} }
req.Header.Set("Authorization", "token "+c.token) req.Header.Set("Authorization", "token "+c.token)
@ -563,7 +241,7 @@ func (c *Client) doJSON(ctx context.Context, method, path string, body, out any)
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
return nil, core.E("Client.doJSON", "forge: request "+method+" "+path, err) return nil, coreerr.E("Client.doJSON", "forge: request "+method+" "+path, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -575,7 +253,7 @@ func (c *Client) doJSON(ctx context.Context, method, path string, body, out any)
if out != nil && resp.StatusCode != http.StatusNoContent { if out != nil && resp.StatusCode != http.StatusNoContent {
if err := json.NewDecoder(resp.Body).Decode(out); err != nil { if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return nil, core.E("Client.doJSON", "forge: decode response", err) return nil, coreerr.E("Client.doJSON", "forge: decode response", err)
} }
} }
@ -588,7 +266,7 @@ func (c *Client) parseError(resp *http.Response, path string) error {
} }
// Read a bit of the body to see if we can get a message // Read a bit of the body to see if we can get a message
data, _ := goio.ReadAll(goio.LimitReader(resp.Body, 1024)) data, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
_ = json.Unmarshal(data, &errBody) _ = json.Unmarshal(data, &errBody)
msg := errBody.Message msg := errBody.Message

View file

@ -2,16 +2,14 @@ package forge
import ( import (
"context" "context"
"fmt" "encoding/json"
json "github.com/goccy/go-json" "errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
core "dappco.re/go/core"
) )
func TestClient_Get_Good(t *testing.T) { func TestClient_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -37,7 +35,7 @@ func TestClient_Get_Good(t *testing.T) {
} }
} }
func TestClient_Post_Good(t *testing.T) { func TestClient_Good_Post(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -64,37 +62,7 @@ func TestClient_Post_Good(t *testing.T) {
} }
} }
func TestClient_PostRaw_Good(t *testing.T) { func TestClient_Good_Delete(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if got := r.URL.Path; got != "/api/v1/markdown" {
t.Errorf("wrong path: %s", got)
}
w.Header().Set("X-RateLimit-Limit", "100")
w.Header().Set("X-RateLimit-Remaining", "98")
w.Header().Set("X-RateLimit-Reset", "1700000001")
w.Write([]byte("<p>Hello</p>"))
}))
defer srv.Close()
c := NewClient(srv.URL, "test-token")
body := map[string]string{"text": "Hello"}
got, err := c.PostRaw(context.Background(), "/api/v1/markdown", body)
if err != nil {
t.Fatal(err)
}
if string(got) != "<p>Hello</p>" {
t.Errorf("got body=%q", string(got))
}
rl := c.RateLimit()
if rl.Limit != 100 || rl.Remaining != 98 || rl.Reset != 1700000001 {
t.Fatalf("unexpected rate limit: %+v", rl)
}
}
func TestClient_Delete_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method) t.Errorf("expected DELETE, got %s", r.Method)
@ -110,36 +78,7 @@ func TestClient_Delete_Good(t *testing.T) {
} }
} }
func TestClient_GetRaw_Good(t *testing.T) { func TestClient_Bad_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if got := r.URL.Path; got != "/api/v1/signing-key.gpg" {
t.Errorf("wrong path: %s", got)
}
w.Header().Set("X-RateLimit-Limit", "60")
w.Header().Set("X-RateLimit-Remaining", "59")
w.Header().Set("X-RateLimit-Reset", "1700000002")
w.Write([]byte("key-data"))
}))
defer srv.Close()
c := NewClient(srv.URL, "test-token")
got, err := c.GetRaw(context.Background(), "/api/v1/signing-key.gpg")
if err != nil {
t.Fatal(err)
}
if string(got) != "key-data" {
t.Errorf("got body=%q", string(got))
}
rl := c.RateLimit()
if rl.Limit != 60 || rl.Remaining != 59 || rl.Reset != 1700000002 {
t.Fatalf("unexpected rate limit: %+v", rl)
}
}
func TestClient_ServerError_Bad(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": "internal error"}) json.NewEncoder(w).Encode(map[string]string{"message": "internal error"})
@ -152,7 +91,7 @@ func TestClient_ServerError_Bad(t *testing.T) {
t.Fatal("expected error") t.Fatal("expected error")
} }
var apiErr *APIError var apiErr *APIError
if !core.As(err, &apiErr) { if !errors.As(err, &apiErr) {
t.Fatalf("expected APIError, got %T", err) t.Fatalf("expected APIError, got %T", err)
} }
if apiErr.StatusCode != 500 { if apiErr.StatusCode != 500 {
@ -160,7 +99,7 @@ func TestClient_ServerError_Bad(t *testing.T) {
} }
} }
func TestClient_NotFound_Bad(t *testing.T) { func TestClient_Bad_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
@ -174,7 +113,7 @@ func TestClient_NotFound_Bad(t *testing.T) {
} }
} }
func TestClient_ContextCancellation_Good(t *testing.T) { func TestClient_Good_ContextCancellation(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-r.Context().Done() <-r.Context().Done()
})) }))
@ -189,117 +128,54 @@ func TestClient_ContextCancellation_Good(t *testing.T) {
} }
} }
func TestClient_Options_Good(t *testing.T) { func TestClient_Good_Options(t *testing.T) {
c := NewClient("https://forge.lthn.ai", "tok", c := NewClient("https://forge.lthn.ai", "tok",
WithUserAgent("go-forge/1.0"), WithUserAgent("go-forge/1.0"),
) )
if c.userAgent != "go-forge/1.0" { if c.userAgent != "go-forge/1.0" {
t.Errorf("got user agent=%q", c.userAgent) t.Errorf("got user agent=%q", c.userAgent)
} }
if got := c.UserAgent(); got != "go-forge/1.0" {
t.Errorf("got UserAgent()=%q", got)
}
} }
func TestClient_HasToken_Good(t *testing.T) { func TestClient_Good_WithHTTPClient(t *testing.T) {
c := NewClient("https://forge.lthn.ai", "tok")
if !c.HasToken() {
t.Fatal("expected HasToken to report configured token")
}
}
func TestClient_HasToken_Bad(t *testing.T) {
c := NewClient("https://forge.lthn.ai", "")
if c.HasToken() {
t.Fatal("expected HasToken to report missing token")
}
}
func TestClient_NilSafeAccessors(t *testing.T) {
var c *Client
if got := c.BaseURL(); got != "" {
t.Fatalf("got BaseURL()=%q, want empty string", got)
}
if got := c.RateLimit(); got != (RateLimit{}) {
t.Fatalf("got RateLimit()=%#v, want zero value", got)
}
if got := c.UserAgent(); got != "" {
t.Fatalf("got UserAgent()=%q, want empty string", got)
}
if got := c.HTTPClient(); got != nil {
t.Fatal("expected HTTPClient() to return nil")
}
if got := c.HasToken(); got {
t.Fatal("expected HasToken() to report false")
}
}
func TestClient_WithHTTPClient_Good(t *testing.T) {
custom := &http.Client{} custom := &http.Client{}
c := NewClient("https://forge.lthn.ai", "tok", WithHTTPClient(custom)) c := NewClient("https://forge.lthn.ai", "tok", WithHTTPClient(custom))
if c.httpClient != custom { if c.httpClient != custom {
t.Error("expected custom HTTP client to be set") t.Error("expected custom HTTP client to be set")
} }
if got := c.HTTPClient(); got != custom {
t.Error("expected HTTPClient() to return the configured HTTP client")
}
} }
func TestClient_String_Good(t *testing.T) { func TestAPIError_Good_Error(t *testing.T) {
c := NewClient("https://forge.lthn.ai", "tok", WithUserAgent("go-forge/1.0"))
got := fmt.Sprint(c)
want := `forge.Client{baseURL="https://forge.lthn.ai", token=set, userAgent="go-forge/1.0"}`
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
if got := c.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", c); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestAPIError_Error_Good(t *testing.T) {
e := &APIError{StatusCode: 404, Message: "not found", URL: "/api/v1/repos/x/y"} e := &APIError{StatusCode: 404, Message: "not found", URL: "/api/v1/repos/x/y"}
got := e.Error() got := e.Error()
want := "forge: /api/v1/repos/x/y 404: not found" want := "forge: /api/v1/repos/x/y 404: not found"
if got != want { if got != want {
t.Errorf("got %q, want %q", got, want) t.Errorf("got %q, want %q", got, want)
} }
if got := e.String(); got != want {
t.Errorf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(e); got != want {
t.Errorf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", e); got != want {
t.Errorf("got GoString=%q, want %q", got, want)
}
} }
func TestIsConflict_Match_Good(t *testing.T) { func TestIsConflict_Good(t *testing.T) {
err := &APIError{StatusCode: http.StatusConflict, Message: "conflict", URL: "/test"} err := &APIError{StatusCode: http.StatusConflict, Message: "conflict", URL: "/test"}
if !IsConflict(err) { if !IsConflict(err) {
t.Error("expected IsConflict to return true for 409") t.Error("expected IsConflict to return true for 409")
} }
} }
func TestIsConflict_NotConflict_Bad(t *testing.T) { func TestIsConflict_Bad_NotConflict(t *testing.T) {
err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"} err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"}
if IsConflict(err) { if IsConflict(err) {
t.Error("expected IsConflict to return false for 404") t.Error("expected IsConflict to return false for 404")
} }
} }
func TestIsForbidden_NotForbidden_Bad(t *testing.T) { func TestIsForbidden_Bad_NotForbidden(t *testing.T) {
err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"} err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"}
if IsForbidden(err) { if IsForbidden(err) {
t.Error("expected IsForbidden to return false for 404") t.Error("expected IsForbidden to return false for 404")
} }
} }
func TestClient_RateLimit_Good(t *testing.T) { func TestClient_Good_RateLimit(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-RateLimit-Limit", "100") w.Header().Set("X-RateLimit-Limit", "100")
w.Header().Set("X-RateLimit-Remaining", "99") w.Header().Set("X-RateLimit-Remaining", "99")
@ -326,7 +202,7 @@ func TestClient_RateLimit_Good(t *testing.T) {
} }
} }
func TestClient_Forbidden_Bad(t *testing.T) { func TestClient_Bad_Forbidden(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
json.NewEncoder(w).Encode(map[string]string{"message": "forbidden"}) json.NewEncoder(w).Encode(map[string]string{"message": "forbidden"})
@ -340,7 +216,7 @@ func TestClient_Forbidden_Bad(t *testing.T) {
} }
} }
func TestClient_Conflict_Bad(t *testing.T) { func TestClient_Bad_Conflict(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusConflict) w.WriteHeader(http.StatusConflict)
json.NewEncoder(w).Encode(map[string]string{"message": "already exists"}) json.NewEncoder(w).Encode(map[string]string{"message": "already exists"})

View file

@ -2,15 +2,14 @@ package main
import ( import (
"bytes" "bytes"
"cmp"
"maps" "maps"
"path/filepath"
"slices" "slices"
"strconv"
"strings" "strings"
"text/template" "text/template"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io" coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
) )
// typeGrouping maps type name prefixes to output file names. // typeGrouping maps type name prefixes to output file names.
@ -111,7 +110,7 @@ func classifyType(name string) string {
bestKey := "" bestKey := ""
bestGroup := "" bestGroup := ""
for key, group := range typeGrouping { for key, group := range typeGrouping {
if core.HasPrefix(name, key) && len(key) > len(bestKey) { if strings.HasPrefix(name, key) && len(key) > len(bestKey) {
bestKey = key bestKey = key
bestGroup = group bestGroup = group
} }
@ -123,10 +122,10 @@ func classifyType(name string) string {
// Strip CRUD prefixes and Option suffix, then retry. // Strip CRUD prefixes and Option suffix, then retry.
base := name base := name
for _, prefix := range []string{"Create", "Edit", "Delete", "Update", "Add", "Submit", "Replace", "Set", "Transfer"} { for _, prefix := range []string{"Create", "Edit", "Delete", "Update", "Add", "Submit", "Replace", "Set", "Transfer"} {
base = core.TrimPrefix(base, prefix) base = strings.TrimPrefix(base, prefix)
} }
base = core.TrimSuffix(base, "Option") base = strings.TrimSuffix(base, "Option")
base = core.TrimSuffix(base, "Options") base = strings.TrimSuffix(base, "Options")
if base != name && base != "" { if base != name && base != "" {
if group, ok := typeGrouping[base]; ok { if group, ok := typeGrouping[base]; ok {
@ -136,7 +135,7 @@ func classifyType(name string) string {
bestKey = "" bestKey = ""
bestGroup = "" bestGroup = ""
for key, group := range typeGrouping { for key, group := range typeGrouping {
if core.HasPrefix(base, key) && len(key) > len(bestKey) { if strings.HasPrefix(base, key) && len(key) > len(bestKey) {
bestKey = key bestKey = key
bestGroup = group bestGroup = group
} }
@ -152,7 +151,7 @@ func classifyType(name string) string {
// sanitiseLine collapses a multi-line string into a single line, // sanitiseLine collapses a multi-line string into a single line,
// replacing newlines and consecutive whitespace with a single space. // replacing newlines and consecutive whitespace with a single space.
func sanitiseLine(s string) string { func sanitiseLine(s string) string {
return core.Join(" ", splitFields(s)...) return strings.Join(strings.Fields(s), " ")
} }
// enumConstName generates a Go constant name for an enum value. // enumConstName generates a Go constant name for an enum value.
@ -177,12 +176,6 @@ import "time"
{{- if .Description}} {{- if .Description}}
// {{.Name}} — {{sanitise .Description}} // {{.Name}} — {{sanitise .Description}}
{{- end}} {{- end}}
{{- if .Usage}}
//
// Usage:
//
// opts := {{.Usage}}
{{- end}}
{{- if .IsEnum}} {{- if .IsEnum}}
type {{.Name}} string type {{.Name}} string
@ -191,8 +184,6 @@ const (
{{enumConstName $t.Name .}} {{$t.Name}} = "{{.}}" {{enumConstName $t.Name .}} {{$t.Name}} = "{{.}}"
{{- end}} {{- end}}
) )
{{- else if .IsAlias}}
type {{.Name}} {{.AliasType}}
{{- else if (eq (len .Fields) 0)}} {{- else if (eq (len .Fields) 0)}}
// {{.Name}} has no fields in the swagger spec. // {{.Name}} has no fields in the swagger spec.
type {{.Name}} struct{} type {{.Name}} struct{}
@ -213,18 +204,11 @@ type templateData struct {
} }
// Generate writes Go source files for the extracted types, grouped by logical domain. // Generate writes Go source files for the extracted types, grouped by logical domain.
//
// Usage:
//
// err := Generate(types, pairs, "types")
// _ = err
func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error { func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error {
if err := coreio.Local.EnsureDir(outDir); err != nil { if err := coreio.Local.EnsureDir(outDir); err != nil {
return core.E("Generate", "create output directory", err) return coreerr.E("Generate", "create output directory", err)
} }
populateUsageExamples(types)
// Group types by output file. // Group types by output file.
groups := make(map[string][]*GoType) groups := make(map[string][]*GoType)
for _, gt := range types { for _, gt := range types {
@ -235,7 +219,7 @@ func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error {
// Sort types within each group for deterministic output. // Sort types within each group for deterministic output.
for file := range groups { for file := range groups {
slices.SortFunc(groups[file], func(a, b *GoType) int { slices.SortFunc(groups[file], func(a, b *GoType) int {
return cmp.Compare(a.Name, b.Name) return strings.Compare(a.Name, b.Name)
}) })
} }
@ -244,158 +228,20 @@ func Generate(types map[string]*GoType, pairs []CRUDPair, outDir string) error {
slices.Sort(fileNames) slices.Sort(fileNames)
for _, file := range fileNames { for _, file := range fileNames {
outPath := core.JoinPath(outDir, file+".go") outPath := filepath.Join(outDir, file+".go")
if err := writeFile(outPath, groups[file]); err != nil { if err := writeFile(outPath, groups[file]); err != nil {
return core.E("Generate", "write "+outPath, err) return coreerr.E("Generate", "write "+outPath, err)
} }
} }
return nil return nil
} }
func populateUsageExamples(types map[string]*GoType) {
for _, gt := range types {
gt.Usage = usageExample(gt)
}
}
func usageExample(gt *GoType) string {
switch {
case gt.IsEnum && len(gt.EnumValues) > 0:
return enumConstName(gt.Name, gt.EnumValues[0])
case gt.IsAlias:
return gt.Name + "(" + exampleTypeExpression(gt.AliasType) + ")"
default:
example := exampleTypeLiteral(gt)
if example == "" {
example = gt.Name + "{}"
}
return example
}
}
func exampleTypeLiteral(gt *GoType) string {
if len(gt.Fields) == 0 {
return gt.Name + "{}"
}
field := chooseUsageField(gt.Fields)
if field.GoName == "" {
return gt.Name + "{}"
}
return gt.Name + "{" + field.GoName + ": " + exampleValue(field) + "}"
}
func exampleTypeExpression(typeName string) string {
switch {
case typeName == "string":
return strconv.Quote("example")
case typeName == "bool":
return "true"
case typeName == "int", typeName == "int32", typeName == "int64", typeName == "uint", typeName == "uint32", typeName == "uint64":
return "1"
case typeName == "float32", typeName == "float64":
return "1.0"
case typeName == "time.Time":
return "time.Now()"
case core.HasPrefix(typeName, "[]string"):
return "[]string{\"example\"}"
case core.HasPrefix(typeName, "[]int64"):
return "[]int64{1}"
case core.HasPrefix(typeName, "[]int"):
return "[]int{1}"
case core.HasPrefix(typeName, "map["):
return typeName + "{\"key\": \"value\"}"
default:
return typeName + "{}"
}
}
func chooseUsageField(fields []GoField) GoField {
best := fields[0]
bestScore := usageFieldScore(best)
for _, field := range fields[1:] {
score := usageFieldScore(field)
if score < bestScore || (score == bestScore && field.GoName < best.GoName) {
best = field
bestScore = score
}
}
return best
}
func usageFieldScore(field GoField) int {
score := 100
if field.Required {
score -= 50
}
switch {
case core.HasSuffix(field.GoType, "string"):
score -= 30
case core.Contains(field.GoType, "time.Time"):
score -= 25
case core.HasSuffix(field.GoType, "bool"):
score -= 20
case core.Contains(field.GoType, "int"):
score -= 15
case core.HasPrefix(field.GoType, "[]"):
score -= 10
}
if core.Contains(field.GoName, "Name") || core.Contains(field.GoName, "Title") || core.Contains(field.GoName, "Body") || core.Contains(field.GoName, "Description") {
score -= 10
}
return score
}
func exampleValue(field GoField) string {
switch {
case core.HasPrefix(field.GoType, "*"):
return "&" + core.TrimPrefix(field.GoType, "*") + "{}"
case field.GoType == "string":
return exampleStringValue(field.GoName)
case field.GoType == "time.Time":
return "time.Now()"
case field.GoType == "bool":
return "true"
case core.HasSuffix(field.GoType, "int64"), core.HasSuffix(field.GoType, "int"), core.HasSuffix(field.GoType, "uint64"), core.HasSuffix(field.GoType, "uint"):
return "1"
case core.HasPrefix(field.GoType, "[]string"):
return "[]string{\"example\"}"
case core.HasPrefix(field.GoType, "[]int64"):
return "[]int64{1}"
case core.HasPrefix(field.GoType, "[]int"):
return "[]int{1}"
case core.HasPrefix(field.GoType, "map["):
return field.GoType + "{\"key\": \"value\"}"
default:
return "{}"
}
}
func exampleStringValue(fieldName string) string {
switch {
case core.Contains(fieldName, "URL"):
return "\"https://example.com\""
case core.Contains(fieldName, "Email"):
return "\"alice@example.com\""
case core.Contains(fieldName, "Tag"):
return "\"v1.0.0\""
case core.Contains(fieldName, "Branch"), core.Contains(fieldName, "Ref"):
return "\"main\""
default:
return "\"example\""
}
}
// writeFile renders and writes a single Go source file for the given types. // writeFile renders and writes a single Go source file for the given types.
func writeFile(path string, types []*GoType) error { func writeFile(path string, types []*GoType) error {
needTime := slices.ContainsFunc(types, func(gt *GoType) bool { needTime := slices.ContainsFunc(types, func(gt *GoType) bool {
if core.Contains(gt.AliasType, "time.Time") {
return true
}
return slices.ContainsFunc(gt.Fields, func(f GoField) bool { return slices.ContainsFunc(gt.Fields, func(f GoField) bool {
return core.Contains(f.GoType, "time.Time") return strings.Contains(f.GoType, "time.Time")
}) })
}) })
@ -406,12 +252,11 @@ func writeFile(path string, types []*GoType) error {
var buf bytes.Buffer var buf bytes.Buffer
if err := fileHeader.Execute(&buf, data); err != nil { if err := fileHeader.Execute(&buf, data); err != nil {
return core.E("writeFile", "execute template", err) return coreerr.E("writeFile", "execute template", err)
} }
content := strings.TrimRight(buf.String(), "\n") + "\n" if err := coreio.Local.Write(path, buf.String()); err != nil {
if err := coreio.Local.Write(path, content); err != nil { return coreerr.E("writeFile", "write file", err)
return core.E("writeFile", "write file", err)
} }
return nil return nil

View file

@ -1,13 +1,15 @@
package main package main
import ( import (
"os"
"path/filepath"
"strings"
"testing" "testing"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io" coreio "dappco.re/go/core/io"
) )
func TestGenerate_CreatesFiles_Good(t *testing.T) { func TestGenerate_Good_CreatesFiles(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json") spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -21,10 +23,10 @@ func TestGenerate_CreatesFiles_Good(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
entries, _ := coreio.Local.List(outDir) entries, _ := os.ReadDir(outDir)
goFiles := 0 goFiles := 0
for _, e := range entries { for _, e := range entries {
if core.HasSuffix(e.Name(), ".go") { if strings.HasSuffix(e.Name(), ".go") {
goFiles++ goFiles++
} }
} }
@ -33,7 +35,7 @@ func TestGenerate_CreatesFiles_Good(t *testing.T) {
} }
} }
func TestGenerate_ValidGoSyntax_Good(t *testing.T) { func TestGenerate_Good_ValidGoSyntax(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json") spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -47,11 +49,11 @@ func TestGenerate_ValidGoSyntax_Good(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
entries, _ := coreio.Local.List(outDir) entries, _ := os.ReadDir(outDir)
var content string var content string
for _, e := range entries { for _, e := range entries {
if core.HasSuffix(e.Name(), ".go") { if strings.HasSuffix(e.Name(), ".go") {
content, err = coreio.Local.Read(core.JoinPath(outDir, e.Name())) content, err = coreio.Local.Read(filepath.Join(outDir, e.Name()))
if err == nil { if err == nil {
break break
} }
@ -60,15 +62,15 @@ func TestGenerate_ValidGoSyntax_Good(t *testing.T) {
if err != nil || content == "" { if err != nil || content == "" {
t.Fatal("could not read any generated file") t.Fatal("could not read any generated file")
} }
if !core.Contains(content, "package types") { if !strings.Contains(content, "package types") {
t.Error("missing package declaration") t.Error("missing package declaration")
} }
if !core.Contains(content, "// Code generated") { if !strings.Contains(content, "// Code generated") {
t.Error("missing generated comment") t.Error("missing generated comment")
} }
} }
func TestGenerate_RepositoryType_Good(t *testing.T) { func TestGenerate_Good_RepositoryType(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json") spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -83,10 +85,10 @@ func TestGenerate_RepositoryType_Good(t *testing.T) {
} }
var content string var content string
entries, _ := coreio.Local.List(outDir) entries, _ := os.ReadDir(outDir)
for _, e := range entries { for _, e := range entries {
data, _ := coreio.Local.Read(core.JoinPath(outDir, e.Name())) data, _ := coreio.Local.Read(filepath.Join(outDir, e.Name()))
if core.Contains(data, "type Repository struct") { if strings.Contains(data, "type Repository struct") {
content = data content = data
break break
} }
@ -105,13 +107,13 @@ func TestGenerate_RepositoryType_Good(t *testing.T) {
"`json:\"private,omitempty\"`", "`json:\"private,omitempty\"`",
} }
for _, check := range checks { for _, check := range checks {
if !core.Contains(content, check) { if !strings.Contains(content, check) {
t.Errorf("missing field with tag %s", check) t.Errorf("missing field with tag %s", check)
} }
} }
} }
func TestGenerate_TimeImport_Good(t *testing.T) { func TestGenerate_Good_TimeImport(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json") spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -125,131 +127,11 @@ func TestGenerate_TimeImport_Good(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
entries, _ := coreio.Local.List(outDir) entries, _ := os.ReadDir(outDir)
for _, e := range entries { for _, e := range entries {
content, _ := coreio.Local.Read(core.JoinPath(outDir, e.Name())) content, _ := coreio.Local.Read(filepath.Join(outDir, e.Name()))
if core.Contains(content, "time.Time") && !core.Contains(content, "\"time\"") { if strings.Contains(content, "time.Time") && !strings.Contains(content, "\"time\"") {
t.Errorf("file %s uses time.Time but doesn't import time", e.Name()) t.Errorf("file %s uses time.Time but doesn't import time", e.Name())
} }
} }
} }
func TestGenerate_AdditionalProperties_Good(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil {
t.Fatal(err)
}
types := ExtractTypes(spec)
pairs := DetectCRUDPairs(spec)
outDir := t.TempDir()
if err := Generate(types, pairs, outDir); err != nil {
t.Fatal(err)
}
entries, _ := coreio.Local.List(outDir)
var hookContent string
var teamContent string
for _, e := range entries {
data, _ := coreio.Local.Read(core.JoinPath(outDir, e.Name()))
if core.Contains(data, "type CreateHookOptionConfig") {
hookContent = data
}
if core.Contains(data, "UnitsMap map[string]string `json:\"units_map,omitempty\"`") {
teamContent = data
}
}
if hookContent == "" {
t.Fatal("CreateHookOptionConfig type not found in any generated file")
}
if !core.Contains(hookContent, "type CreateHookOptionConfig map[string]any") {
t.Fatalf("generated alias not found in file:\n%s", hookContent)
}
if teamContent == "" {
t.Fatal("typed units_map field not found in any generated file")
}
}
func TestGenerate_UsageExamples_Good(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil {
t.Fatal(err)
}
types := ExtractTypes(spec)
pairs := DetectCRUDPairs(spec)
outDir := t.TempDir()
if err := Generate(types, pairs, outDir); err != nil {
t.Fatal(err)
}
entries, _ := coreio.Local.List(outDir)
var content string
for _, e := range entries {
data, _ := coreio.Local.Read(core.JoinPath(outDir, e.Name()))
if core.Contains(data, "type CreateIssueOption struct") {
content = data
break
}
}
if content == "" {
t.Fatal("CreateIssueOption type not found in any generated file")
}
if !core.Contains(content, "// Usage:") {
t.Fatalf("generated option type is missing usage documentation:\n%s", content)
}
if !core.Contains(content, "opts :=") {
t.Fatalf("generated usage example is missing assignment syntax:\n%s", content)
}
}
func TestGenerate_UsageExamples_AllKinds_Good(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil {
t.Fatal(err)
}
types := ExtractTypes(spec)
pairs := DetectCRUDPairs(spec)
outDir := t.TempDir()
if err := Generate(types, pairs, outDir); err != nil {
t.Fatal(err)
}
entries, _ := coreio.Local.List(outDir)
var content string
for _, e := range entries {
data, _ := coreio.Local.Read(core.JoinPath(outDir, e.Name()))
if core.Contains(data, "type CommitStatusState string") {
content = data
break
}
}
if content == "" {
t.Fatal("CommitStatusState type not found in any generated file")
}
if !core.Contains(content, "type CommitStatusState string") {
t.Fatalf("CommitStatusState type not generated:\n%s", content)
}
if !core.Contains(content, "// Usage:") {
t.Fatalf("generated enum type is missing usage documentation:\n%s", content)
}
content = ""
for _, e := range entries {
data, _ := coreio.Local.Read(core.JoinPath(outDir, e.Name()))
if core.Contains(data, "type CreateHookOptionConfig map[string]any") {
content = data
break
}
}
if content == "" {
t.Fatal("CreateHookOptionConfig type not found in any generated file")
}
if !core.Contains(content, "CreateHookOptionConfig(map[string]any{\"key\": \"value\"})") {
t.Fatalf("generated alias type is missing a valid usage example:\n%s", content)
}
}

View file

@ -1,41 +0,0 @@
package main
import (
"unicode"
core "dappco.re/go/core"
)
func splitFields(s string) []string {
return splitFunc(s, unicode.IsSpace)
}
func splitSnakeKebab(s string) []string {
return splitFunc(s, func(r rune) bool {
return r == '_' || r == '-'
})
}
func splitFunc(s string, isDelimiter func(rune) bool) []string {
var parts []string
buf := core.NewBuilder()
flush := func() {
if buf.Len() == 0 {
return
}
parts = append(parts, buf.String())
buf.Reset()
}
for _, r := range s {
if isDelimiter(r) {
flush()
continue
}
buf.WriteRune(r)
}
flush()
return parts
}

View file

@ -2,9 +2,8 @@ package main
import ( import (
"flag" "flag"
"fmt"
"os" "os"
core "dappco.re/go/core"
) )
func main() { func main() {
@ -12,26 +11,20 @@ func main() {
outDir := flag.String("out", "types", "output directory for generated types") outDir := flag.String("out", "types", "output directory for generated types")
flag.Parse() flag.Parse()
if err := run(*specPath, *outDir); err != nil { spec, err := LoadSpec(*specPath)
core.Print(os.Stderr, "forgegen: %v", err)
os.Exit(1)
}
}
func run(specPath, outDir string) error {
spec, err := LoadSpec(specPath)
if err != nil { if err != nil {
return core.E("forgegen.main", "load spec", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
} }
types := ExtractTypes(spec) types := ExtractTypes(spec)
pairs := DetectCRUDPairs(spec) pairs := DetectCRUDPairs(spec)
core.Print(nil, "Loaded %d types, %d CRUD pairs", len(types), len(pairs)) fmt.Printf("Loaded %d types, %d CRUD pairs\n", len(types), len(pairs))
core.Print(nil, "Output dir: %s", outDir) fmt.Printf("Output dir: %s\n", *outDir)
if err := Generate(types, pairs, outDir); err != nil { if err := Generate(types, pairs, *outDir); err != nil {
return core.E("forgegen.main", "generate types", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
} }
return nil
} }

View file

@ -1,40 +0,0 @@
package main
import (
"testing"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
)
func TestMain_Run_Good(t *testing.T) {
outDir := t.TempDir()
if err := run("../../testdata/swagger.v1.json", outDir); err != nil {
t.Fatal(err)
}
entries, err := coreio.Local.List(outDir)
if err != nil {
t.Fatal(err)
}
goFiles := 0
for _, e := range entries {
if core.HasSuffix(e.Name(), ".go") {
goFiles++
}
}
if goFiles == 0 {
t.Fatal("no .go files generated by run")
}
}
func TestMain_Run_Bad(t *testing.T) {
err := run("/does/not/exist/swagger.v1.json", t.TempDir())
if err == nil {
t.Fatal("expected error for invalid spec path")
}
if !core.Contains(err.Error(), "load spec") {
t.Fatalf("got error %q, expected load spec context", err.Error())
}
}

View file

@ -1,20 +1,16 @@
package main package main
import ( import (
"cmp" "encoding/json"
json "github.com/goccy/go-json" "fmt"
"slices" "slices"
"strings"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io" coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
) )
// Spec represents a Swagger 2.0 specification document. // Spec represents a Swagger 2.0 specification document.
//
// Usage:
//
// spec, err := LoadSpec("testdata/swagger.v1.json")
// _ = spec
type Spec struct { type Spec struct {
Swagger string `json:"swagger"` Swagger string `json:"swagger"`
Info SpecInfo `json:"info"` Info SpecInfo `json:"info"`
@ -23,70 +19,42 @@ type Spec struct {
} }
// SpecInfo holds metadata about the API specification. // SpecInfo holds metadata about the API specification.
//
// Usage:
//
// _ = SpecInfo{Title: "Forgejo API", Version: "1.0"}
type SpecInfo struct { type SpecInfo struct {
Title string `json:"title"` Title string `json:"title"`
Version string `json:"version"` Version string `json:"version"`
} }
// SchemaDefinition represents a single type definition in the swagger spec. // SchemaDefinition represents a single type definition in the swagger spec.
//
// Usage:
//
// _ = SchemaDefinition{Type: "object"}
type SchemaDefinition struct { type SchemaDefinition struct {
Description string `json:"description"` Description string `json:"description"`
Format string `json:"format"` Type string `json:"type"`
Ref string `json:"$ref"` Properties map[string]SchemaProperty `json:"properties"`
Items *SchemaProperty `json:"items"` Required []string `json:"required"`
Type string `json:"type"` Enum []any `json:"enum"`
Properties map[string]SchemaProperty `json:"properties"` XGoName string `json:"x-go-name"`
Required []string `json:"required"`
Enum []any `json:"enum"`
AdditionalProperties *SchemaProperty `json:"additionalProperties"`
XGoName string `json:"x-go-name"`
} }
// SchemaProperty represents a single property within a schema definition. // SchemaProperty represents a single property within a schema definition.
//
// Usage:
//
// _ = SchemaProperty{Type: "string"}
type SchemaProperty struct { type SchemaProperty struct {
Type string `json:"type"` Type string `json:"type"`
Format string `json:"format"` Format string `json:"format"`
Description string `json:"description"` Description string `json:"description"`
Ref string `json:"$ref"` Ref string `json:"$ref"`
Items *SchemaProperty `json:"items"` Items *SchemaProperty `json:"items"`
Enum []any `json:"enum"` Enum []any `json:"enum"`
AdditionalProperties *SchemaProperty `json:"additionalProperties"` XGoName string `json:"x-go-name"`
XGoName string `json:"x-go-name"`
} }
// GoType is the intermediate representation for a Go type to be generated. // GoType is the intermediate representation for a Go type to be generated.
//
// Usage:
//
// _ = GoType{Name: "Repository"}
type GoType struct { type GoType struct {
Name string Name string
Description string Description string
Usage string
Fields []GoField Fields []GoField
IsEnum bool IsEnum bool
EnumValues []string EnumValues []string
IsAlias bool
AliasType string
} }
// GoField is the intermediate representation for a single struct field. // GoField is the intermediate representation for a single struct field.
//
// Usage:
//
// _ = GoField{GoName: "ID", GoType: "int64"}
type GoField struct { type GoField struct {
GoName string GoName string
GoType string GoType string
@ -96,10 +64,6 @@ type GoField struct {
} }
// CRUDPair groups a base type with its corresponding Create and Edit option types. // CRUDPair groups a base type with its corresponding Create and Edit option types.
//
// Usage:
//
// _ = CRUDPair{Base: "Repository", Create: "CreateRepoOption", Edit: "EditRepoOption"}
type CRUDPair struct { type CRUDPair struct {
Base string Base string
Create string Create string
@ -107,29 +71,19 @@ type CRUDPair struct {
} }
// LoadSpec reads and parses a Swagger 2.0 JSON file from the given path. // LoadSpec reads and parses a Swagger 2.0 JSON file from the given path.
//
// Usage:
//
// spec, err := LoadSpec("testdata/swagger.v1.json")
// _ = spec
func LoadSpec(path string) (*Spec, error) { func LoadSpec(path string) (*Spec, error) {
content, err := coreio.Local.Read(path) content, err := coreio.Local.Read(path)
if err != nil { if err != nil {
return nil, core.E("LoadSpec", "read spec", err) return nil, coreerr.E("LoadSpec", "read spec", err)
} }
var spec Spec var spec Spec
if err := json.Unmarshal([]byte(content), &spec); err != nil { if err := json.Unmarshal([]byte(content), &spec); err != nil {
return nil, core.E("LoadSpec", "parse spec", err) return nil, coreerr.E("LoadSpec", "parse spec", err)
} }
return &spec, nil return &spec, nil
} }
// ExtractTypes converts all swagger definitions into Go type intermediate representations. // ExtractTypes converts all swagger definitions into Go type intermediate representations.
//
// Usage:
//
// types := ExtractTypes(spec)
// _ = types["Repository"]
func ExtractTypes(spec *Spec) map[string]*GoType { func ExtractTypes(spec *Spec) map[string]*GoType {
result := make(map[string]*GoType) result := make(map[string]*GoType)
for name, def := range spec.Definitions { for name, def := range spec.Definitions {
@ -137,19 +91,12 @@ func ExtractTypes(spec *Spec) map[string]*GoType {
if len(def.Enum) > 0 { if len(def.Enum) > 0 {
gt.IsEnum = true gt.IsEnum = true
for _, v := range def.Enum { for _, v := range def.Enum {
gt.EnumValues = append(gt.EnumValues, core.Sprint(v)) gt.EnumValues = append(gt.EnumValues, fmt.Sprintf("%v", v))
} }
slices.Sort(gt.EnumValues) slices.Sort(gt.EnumValues)
result[name] = gt result[name] = gt
continue continue
} }
if aliasType, ok := definitionAliasType(def, spec.Definitions); ok {
gt.IsAlias = true
gt.AliasType = aliasType
result[name] = gt
continue
}
required := make(map[string]bool) required := make(map[string]bool)
for _, r := range def.Required { for _, r := range def.Required {
required[r] = true required[r] = true
@ -161,7 +108,7 @@ func ExtractTypes(spec *Spec) map[string]*GoType {
} }
gf := GoField{ gf := GoField{
GoName: goName, GoName: goName,
GoType: resolveGoType(prop, spec.Definitions), GoType: resolveGoType(prop),
JSONName: fieldName, JSONName: fieldName,
Comment: prop.Description, Comment: prop.Description,
Required: required[fieldName], Required: required[fieldName],
@ -169,69 +116,24 @@ func ExtractTypes(spec *Spec) map[string]*GoType {
gt.Fields = append(gt.Fields, gf) gt.Fields = append(gt.Fields, gf)
} }
slices.SortFunc(gt.Fields, func(a, b GoField) int { slices.SortFunc(gt.Fields, func(a, b GoField) int {
return cmp.Compare(a.GoName, b.GoName) return strings.Compare(a.GoName, b.GoName)
}) })
result[name] = gt result[name] = gt
} }
return result return result
} }
func definitionAliasType(def SchemaDefinition, defs map[string]SchemaDefinition) (string, bool) {
if def.Ref != "" {
return refName(def.Ref), true
}
switch def.Type {
case "string":
return "string", true
case "integer":
switch def.Format {
case "int64":
return "int64", true
case "int32":
return "int32", true
default:
return "int", true
}
case "number":
switch def.Format {
case "float":
return "float32", true
default:
return "float64", true
}
case "boolean":
return "bool", true
case "array":
if def.Items != nil {
return "[]" + resolveGoType(*def.Items, defs), true
}
return "[]any", true
case "object":
if def.AdditionalProperties != nil {
return resolveMapType(*def.AdditionalProperties, defs), true
}
}
return "", false
}
// DetectCRUDPairs finds Create*Option / Edit*Option pairs in the swagger definitions // DetectCRUDPairs finds Create*Option / Edit*Option pairs in the swagger definitions
// and maps them back to the base type name. // and maps them back to the base type name.
//
// Usage:
//
// pairs := DetectCRUDPairs(spec)
// _ = pairs
func DetectCRUDPairs(spec *Spec) []CRUDPair { func DetectCRUDPairs(spec *Spec) []CRUDPair {
var pairs []CRUDPair var pairs []CRUDPair
for name := range spec.Definitions { for name := range spec.Definitions {
if !core.HasPrefix(name, "Create") || !core.HasSuffix(name, "Option") { if !strings.HasPrefix(name, "Create") || !strings.HasSuffix(name, "Option") {
continue continue
} }
inner := core.TrimPrefix(name, "Create") inner := strings.TrimPrefix(name, "Create")
inner = core.TrimSuffix(inner, "Option") inner = strings.TrimSuffix(inner, "Option")
editName := core.Concat("Edit", inner, "Option") editName := "Edit" + inner + "Option"
pair := CRUDPair{Base: inner, Create: name} pair := CRUDPair{Base: inner, Create: name}
if _, ok := spec.Definitions[editName]; ok { if _, ok := spec.Definitions[editName]; ok {
pair.Edit = editName pair.Edit = editName
@ -239,15 +141,16 @@ func DetectCRUDPairs(spec *Spec) []CRUDPair {
pairs = append(pairs, pair) pairs = append(pairs, pair)
} }
slices.SortFunc(pairs, func(a, b CRUDPair) int { slices.SortFunc(pairs, func(a, b CRUDPair) int {
return cmp.Compare(a.Base, b.Base) return strings.Compare(a.Base, b.Base)
}) })
return pairs return pairs
} }
// resolveGoType maps a swagger schema property to a Go type string. // resolveGoType maps a swagger schema property to a Go type string.
func resolveGoType(prop SchemaProperty, defs map[string]SchemaDefinition) string { func resolveGoType(prop SchemaProperty) string {
if prop.Ref != "" { if prop.Ref != "" {
return refGoType(prop.Ref, defs) parts := strings.Split(prop.Ref, "/")
return "*" + parts[len(parts)-1]
} }
switch prop.Type { switch prop.Type {
case "string": case "string":
@ -279,74 +182,33 @@ func resolveGoType(prop SchemaProperty, defs map[string]SchemaDefinition) string
return "bool" return "bool"
case "array": case "array":
if prop.Items != nil { if prop.Items != nil {
return "[]" + resolveGoType(*prop.Items, defs) return "[]" + resolveGoType(*prop.Items)
} }
return "[]any" return "[]any"
case "object": case "object":
return resolveMapType(prop, defs) return "map[string]any"
default: default:
return "any" return "any"
} }
} }
// resolveMapType maps a swagger object with additionalProperties to a Go map type.
func resolveMapType(prop SchemaProperty, defs map[string]SchemaDefinition) string {
valueType := "any"
if prop.AdditionalProperties != nil {
valueType = resolveGoType(*prop.AdditionalProperties, defs)
}
return "map[string]" + valueType
}
func refName(ref string) string {
parts := core.Split(ref, "/")
return parts[len(parts)-1]
}
func refGoType(ref string, defs map[string]SchemaDefinition) string {
name := refName(ref)
def, ok := defs[name]
if !ok {
return "*" + name
}
if definitionNeedsPointer(def) {
return "*" + name
}
return name
}
func definitionNeedsPointer(def SchemaDefinition) bool {
if len(def.Enum) > 0 {
return false
}
if def.Ref != "" {
return false
}
switch def.Type {
case "string", "integer", "number", "boolean", "array":
return false
case "object":
return true
default:
return false
}
}
// pascalCase converts a snake_case or kebab-case string to PascalCase, // pascalCase converts a snake_case or kebab-case string to PascalCase,
// with common acronyms kept uppercase. // with common acronyms kept uppercase.
func pascalCase(s string) string { func pascalCase(s string) string {
var parts []string var parts []string
for _, p := range splitSnakeKebab(s) { for p := range strings.FieldsFuncSeq(s, func(r rune) bool {
return r == '_' || r == '-'
}) {
if len(p) == 0 { if len(p) == 0 {
continue continue
} }
upper := core.Upper(p) upper := strings.ToUpper(p)
switch upper { switch upper {
case "ID", "URL", "HTML", "SSH", "HTTP", "HTTPS", "API", "URI", "GPG", "IP", "CSS", "JS": case "ID", "URL", "HTML", "SSH", "HTTP", "HTTPS", "API", "URI", "GPG", "IP", "CSS", "JS":
parts = append(parts, upper) parts = append(parts, upper)
default: default:
parts = append(parts, core.Concat(core.Upper(p[:1]), p[1:])) parts = append(parts, strings.ToUpper(p[:1])+p[1:])
} }
} }
return core.Concat(parts...) return strings.Join(parts, "")
} }

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
) )
func TestParser_LoadSpec_Good(t *testing.T) { func TestParser_Good_LoadSpec(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json") spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -17,7 +17,7 @@ func TestParser_LoadSpec_Good(t *testing.T) {
} }
} }
func TestParser_ExtractTypes_Good(t *testing.T) { func TestParser_Good_ExtractTypes(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json") spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -38,7 +38,7 @@ func TestParser_ExtractTypes_Good(t *testing.T) {
} }
} }
func TestParser_FieldTypes_Good(t *testing.T) { func TestParser_Good_FieldTypes(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json") spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -70,15 +70,11 @@ func TestParser_FieldTypes_Good(t *testing.T) {
if f.GoType != "*User" { if f.GoType != "*User" {
t.Errorf("owner: got %q, want *User", f.GoType) t.Errorf("owner: got %q, want *User", f.GoType)
} }
case "units_map":
if f.GoType != "map[string]string" {
t.Errorf("units_map: got %q, want map[string]string", f.GoType)
}
} }
} }
} }
func TestParser_DetectCreateEditPairs_Good(t *testing.T) { func TestParser_Good_DetectCreateEditPairs(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json") spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -105,65 +101,3 @@ func TestParser_DetectCreateEditPairs_Good(t *testing.T) {
t.Fatal("Repo pair not found") t.Fatal("Repo pair not found")
} }
} }
func TestParser_AdditionalPropertiesAlias_Good(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil {
t.Fatal(err)
}
types := ExtractTypes(spec)
alias, ok := types["CreateHookOptionConfig"]
if !ok {
t.Fatal("CreateHookOptionConfig type not found")
}
if !alias.IsAlias {
t.Fatal("expected CreateHookOptionConfig to be emitted as an alias")
}
if alias.AliasType != "map[string]any" {
t.Fatalf("got alias type %q, want map[string]any", alias.AliasType)
}
}
func TestParser_PrimitiveAndCollectionAliases_Good(t *testing.T) {
spec, err := LoadSpec("../../testdata/swagger.v1.json")
if err != nil {
t.Fatal(err)
}
types := ExtractTypes(spec)
cases := []struct {
name string
wantType string
}{
{name: "CommitStatusState", wantType: "string"},
{name: "IssueFormFieldType", wantType: "string"},
{name: "IssueFormFieldVisible", wantType: "string"},
{name: "NotifySubjectType", wantType: "string"},
{name: "ReviewStateType", wantType: "string"},
{name: "StateType", wantType: "string"},
{name: "TimeStamp", wantType: "int64"},
{name: "IssueTemplateLabels", wantType: "[]string"},
{name: "QuotaGroupList", wantType: "[]*QuotaGroup"},
{name: "QuotaUsedArtifactList", wantType: "[]*QuotaUsedArtifact"},
{name: "QuotaUsedAttachmentList", wantType: "[]*QuotaUsedAttachment"},
{name: "QuotaUsedPackageList", wantType: "[]*QuotaUsedPackage"},
{name: "CreatePullReviewCommentOptions", wantType: "CreatePullReviewComment"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gt, ok := types[tc.name]
if !ok {
t.Fatalf("type %q not found", tc.name)
}
if !gt.IsAlias {
t.Fatalf("type %q should be emitted as an alias", tc.name)
}
if gt.AliasType != tc.wantType {
t.Fatalf("type %q: got alias %q, want %q", tc.name, gt.AliasType, tc.wantType)
}
})
}
}

View file

@ -2,8 +2,8 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"strconv"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
@ -12,71 +12,10 @@ import (
// and git notes. // and git notes.
// No Resource embedding — collection and item commit paths differ, and the // No Resource embedding — collection and item commit paths differ, and the
// remaining endpoints are heterogeneous across status and note paths. // remaining endpoints are heterogeneous across status and note paths.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Commits.GetCombinedStatus(ctx, "core", "go-forge", "main")
type CommitService struct { type CommitService struct {
client *Client client *Client
} }
// CommitListOptions controls filtering for repository commit listings.
//
// Usage:
//
// stat := false
// opts := forge.CommitListOptions{Sha: "main", Stat: &stat}
type CommitListOptions struct {
Sha string
Path string
Stat *bool
Verification *bool
Files *bool
Not string
}
// String returns a safe summary of the commit list filters.
func (o CommitListOptions) String() string {
return optionString("forge.CommitListOptions",
"sha", o.Sha,
"path", o.Path,
"stat", o.Stat,
"verification", o.Verification,
"files", o.Files,
"not", o.Not,
)
}
// GoString returns a safe Go-syntax summary of the commit list filters.
func (o CommitListOptions) GoString() string { return o.String() }
func (o CommitListOptions) queryParams() map[string]string {
query := make(map[string]string, 6)
if o.Sha != "" {
query["sha"] = o.Sha
}
if o.Path != "" {
query["path"] = o.Path
}
if o.Stat != nil {
query["stat"] = strconv.FormatBool(*o.Stat)
}
if o.Verification != nil {
query["verification"] = strconv.FormatBool(*o.Verification)
}
if o.Files != nil {
query["files"] = strconv.FormatBool(*o.Files)
}
if o.Not != "" {
query["not"] = o.Not
}
if len(query) == 0 {
return nil
}
return query
}
const ( const (
commitCollectionPath = "/api/v1/repos/{owner}/{repo}/commits" commitCollectionPath = "/api/v1/repos/{owner}/{repo}/commits"
commitItemPath = "/api/v1/repos/{owner}/{repo}/git/commits/{sha}" commitItemPath = "/api/v1/repos/{owner}/{repo}/git/commits/{sha}"
@ -87,18 +26,18 @@ func newCommitService(c *Client) *CommitService {
} }
// List returns a single page of commits for a repository. // List returns a single page of commits for a repository.
func (s *CommitService) List(ctx context.Context, params Params, opts ListOptions, filters ...CommitListOptions) (*PagedResult[types.Commit], error) { func (s *CommitService) List(ctx context.Context, params Params, opts ListOptions) (*PagedResult[types.Commit], error) {
return ListPage[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), commitListQuery(filters...), opts) return ListPage[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), nil, opts)
} }
// ListAll returns all commits for a repository. // ListAll returns all commits for a repository.
func (s *CommitService) ListAll(ctx context.Context, params Params, filters ...CommitListOptions) ([]types.Commit, error) { func (s *CommitService) ListAll(ctx context.Context, params Params) ([]types.Commit, error) {
return ListAll[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), commitListQuery(filters...)) return ListAll[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), nil)
} }
// Iter returns an iterator over all commits for a repository. // Iter returns an iterator over all commits for a repository.
func (s *CommitService) Iter(ctx context.Context, params Params, filters ...CommitListOptions) iter.Seq2[types.Commit, error] { func (s *CommitService) Iter(ctx context.Context, params Params) iter.Seq2[types.Commit, error] {
return ListIter[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), commitListQuery(filters...)) return ListIter[types.Commit](ctx, s.client, ResolvePath(commitCollectionPath, params), nil)
} }
// Get returns a single commit by SHA or ref. // Get returns a single commit by SHA or ref.
@ -110,40 +49,9 @@ func (s *CommitService) Get(ctx context.Context, params Params) (*types.Commit,
return &out, nil return &out, nil
} }
// GetDiffOrPatch returns a commit diff or patch as raw bytes.
func (s *CommitService) GetDiffOrPatch(ctx context.Context, owner, repo, sha, diffType string) ([]byte, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/git/commits/{sha}.{diffType}", pathParams("owner", owner, "repo", repo, "sha", sha, "diffType", diffType))
return s.client.GetRaw(ctx, path)
}
// GetPullRequest returns the pull request associated with a commit SHA.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Commits.GetPullRequest(ctx, "core", "go-forge", "abc123")
func (s *CommitService) GetPullRequest(ctx context.Context, owner, repo, sha string) (*types.PullRequest, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/commits/{sha}/pull", pathParams("owner", owner, "repo", repo, "sha", sha))
var out types.PullRequest
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// GetCombinedStatus returns the combined status for a given ref (branch, tag, or SHA). // GetCombinedStatus returns the combined status for a given ref (branch, tag, or SHA).
func (s *CommitService) GetCombinedStatus(ctx context.Context, owner, repo, ref string) (*types.CombinedStatus, error) { func (s *CommitService) GetCombinedStatus(ctx context.Context, owner, repo, ref string) (*types.CombinedStatus, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/statuses/{ref}", pathParams("owner", owner, "repo", repo, "ref", ref)) path := fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, ref)
var out types.CombinedStatus
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// GetCombinedStatusByRef returns the combined status for a given commit reference.
func (s *CommitService) GetCombinedStatusByRef(ctx context.Context, owner, repo, ref string) (*types.CombinedStatus, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/commits/{ref}/status", pathParams("owner", owner, "repo", repo, "ref", ref))
var out types.CombinedStatus var out types.CombinedStatus
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -153,7 +61,7 @@ func (s *CommitService) GetCombinedStatusByRef(ctx context.Context, owner, repo,
// ListStatuses returns all commit statuses for a given ref. // ListStatuses returns all commit statuses for a given ref.
func (s *CommitService) ListStatuses(ctx context.Context, owner, repo, ref string) ([]types.CommitStatus, error) { func (s *CommitService) ListStatuses(ctx context.Context, owner, repo, ref string) ([]types.CommitStatus, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/commits/{ref}/statuses", pathParams("owner", owner, "repo", repo, "ref", ref)) path := fmt.Sprintf("/api/v1/repos/%s/%s/commits/%s/statuses", owner, repo, ref)
var out []types.CommitStatus var out []types.CommitStatus
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -161,25 +69,9 @@ func (s *CommitService) ListStatuses(ctx context.Context, owner, repo, ref strin
return out, nil return out, nil
} }
// IterStatuses returns an iterator over all commit statuses for a given ref.
func (s *CommitService) IterStatuses(ctx context.Context, owner, repo, ref string) iter.Seq2[types.CommitStatus, error] {
return func(yield func(types.CommitStatus, error) bool) {
statuses, err := s.ListStatuses(ctx, owner, repo, ref)
if err != nil {
yield(*new(types.CommitStatus), err)
return
}
for _, status := range statuses {
if !yield(status, nil) {
return
}
}
}
}
// CreateStatus creates a new commit status for the given SHA. // CreateStatus creates a new commit status for the given SHA.
func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha string, opts *types.CreateStatusOption) (*types.CommitStatus, error) { func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha string, opts *types.CreateStatusOption) (*types.CommitStatus, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/statuses/{sha}", pathParams("owner", owner, "repo", repo, "sha", sha)) path := fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, sha)
var out types.CommitStatus var out types.CommitStatus
if err := s.client.Post(ctx, path, opts, &out); err != nil { if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err return nil, err
@ -189,39 +81,10 @@ func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha strin
// GetNote returns the git note for a given commit SHA. // GetNote returns the git note for a given commit SHA.
func (s *CommitService) GetNote(ctx context.Context, owner, repo, sha string) (*types.Note, error) { func (s *CommitService) GetNote(ctx context.Context, owner, repo, sha string) (*types.Note, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/git/notes/{sha}", pathParams("owner", owner, "repo", repo, "sha", sha)) path := fmt.Sprintf("/api/v1/repos/%s/%s/git/notes/%s", owner, repo, sha)
var out types.Note var out types.Note
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
} }
return &out, nil return &out, nil
} }
// SetNote creates or updates the git note for a given commit SHA.
func (s *CommitService) SetNote(ctx context.Context, owner, repo, sha, message string) (*types.Note, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/git/notes/{sha}", pathParams("owner", owner, "repo", repo, "sha", sha))
var out types.Note
if err := s.client.Post(ctx, path, types.NoteOptions{Message: message}, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteNote removes the git note for a given commit SHA.
func (s *CommitService) DeleteNote(ctx context.Context, owner, repo, sha string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/git/notes/{sha}", pathParams("owner", owner, "repo", repo, "sha", sha))
return s.client.Delete(ctx, path)
}
func commitListQuery(filters ...CommitListOptions) map[string]string {
query := make(map[string]string, len(filters))
for _, filter := range filters {
for key, value := range filter.queryParams() {
query[key] = value
}
}
if len(query) == 0 {
return nil
}
return query
}

View file

@ -1,36 +0,0 @@
package forge
import (
"context"
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"testing"
"dappco.re/go/core/forge/types"
)
func TestCommitService_GetCombinedStatusByRef_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/commits/main/status" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.CombinedStatus{
SHA: "main",
TotalCount: 3,
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
status, err := f.Commits.GetCombinedStatusByRef(context.Background(), "core", "go-forge", "main")
if err != nil {
t.Fatal(err)
}
if status.SHA != "main" || status.TotalCount != 3 {
t.Fatalf("got %#v", status)
}
}

View file

@ -2,8 +2,7 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -11,7 +10,7 @@ import (
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestCommitService_List_Good(t *testing.T) { func TestCommitService_Good_List(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -62,56 +61,7 @@ func TestCommitService_List_Good(t *testing.T) {
} }
} }
func TestCommitService_ListFiltered_Good(t *testing.T) { func TestCommitService_Good_Get(t *testing.T) {
stat := false
verification := false
files := false
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/commits" {
t.Errorf("wrong path: %s", r.URL.Path)
}
want := map[string]string{
"sha": "main",
"path": "docs",
"stat": "false",
"verification": "false",
"files": "false",
"not": "deadbeef",
"page": "1",
"limit": "50",
}
for key, wantValue := range want {
if got := r.URL.Query().Get(key); got != wantValue {
t.Errorf("got %s=%q, want %q", key, got, wantValue)
}
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Commit{{SHA: "abc123"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
commits, err := f.Commits.ListAll(context.Background(), Params{"owner": "core", "repo": "go-forge"}, CommitListOptions{
Sha: "main",
Path: "docs",
Stat: &stat,
Verification: &verification,
Files: &files,
Not: "deadbeef",
})
if err != nil {
t.Fatal(err)
}
if len(commits) != 1 || commits[0].SHA != "abc123" {
t.Fatalf("got %#v", commits)
}
}
func TestCommitService_Get_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -145,65 +95,7 @@ func TestCommitService_Get_Good(t *testing.T) {
} }
} }
func TestCommitService_GetDiffOrPatch_Good(t *testing.T) { func TestCommitService_Good_ListStatuses(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/git/commits/abc123.diff" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("Content-Type", "text/plain")
io.WriteString(w, "diff --git a/README.md b/README.md")
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
data, err := f.Commits.GetDiffOrPatch(context.Background(), "core", "go-forge", "abc123", "diff")
if err != nil {
t.Fatal(err)
}
if string(data) != "diff --git a/README.md b/README.md" {
t.Fatalf("got body=%q", string(data))
}
}
func TestCommitService_GetPullRequest_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/commits/abc123/pull" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.PullRequest{
ID: 17,
Index: 9,
Title: "Add commit-linked pull request",
Head: &types.PRBranchInfo{
Ref: "feature/commit-link",
},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
pr, err := f.Commits.GetPullRequest(context.Background(), "core", "go-forge", "abc123")
if err != nil {
t.Fatal(err)
}
if pr.ID != 17 {
t.Errorf("got id=%d, want 17", pr.ID)
}
if pr.Index != 9 {
t.Errorf("got index=%d, want 9", pr.Index)
}
if pr.Head == nil || pr.Head.Ref != "feature/commit-link" {
t.Fatalf("unexpected head branch info: %+v", pr.Head)
}
}
func TestCommitService_ListStatuses_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -234,40 +126,7 @@ func TestCommitService_ListStatuses_Good(t *testing.T) {
} }
} }
func TestCommitService_IterStatuses_Good(t *testing.T) { func TestCommitService_Good_CreateStatus(t *testing.T) {
var requests int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/commits/abc123/statuses" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode([]types.CommitStatus{
{ID: 1, Context: "ci/build"},
{ID: 2, Context: "ci/test"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var got []string
for status, err := range f.Commits.IterStatuses(context.Background(), "core", "go-forge", "abc123") {
if err != nil {
t.Fatal(err)
}
got = append(got, status.Context)
}
if requests != 1 {
t.Fatalf("expected 1 request, got %d", requests)
}
if len(got) != 2 || got[0] != "ci/build" || got[1] != "ci/test" {
t.Fatalf("got %#v", got)
}
}
func TestCommitService_CreateStatus_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -310,7 +169,7 @@ func TestCommitService_CreateStatus_Good(t *testing.T) {
} }
} }
func TestCommitService_GetNote_Good(t *testing.T) { func TestCommitService_Good_GetNote(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -340,62 +199,7 @@ func TestCommitService_GetNote_Good(t *testing.T) {
} }
} }
func TestCommitService_SetNote_Good(t *testing.T) { func TestCommitService_Good_GetCombinedStatus(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/git/notes/abc123" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.NoteOptions
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Message != "reviewed and approved" {
t.Errorf("got message=%q, want %q", opts.Message, "reviewed and approved")
}
json.NewEncoder(w).Encode(types.Note{
Message: "reviewed and approved",
Commit: &types.Commit{
SHA: "abc123",
},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
note, err := f.Commits.SetNote(context.Background(), "core", "go-forge", "abc123", "reviewed and approved")
if err != nil {
t.Fatal(err)
}
if note.Message != "reviewed and approved" {
t.Errorf("got message=%q, want %q", note.Message, "reviewed and approved")
}
if note.Commit.SHA != "abc123" {
t.Errorf("got commit sha=%q, want %q", note.Commit.SHA, "abc123")
}
}
func TestCommitService_DeleteNote_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/git/notes/abc123" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Commits.DeleteNote(context.Background(), "core", "go-forge", "abc123"); err != nil {
t.Fatal(err)
}
}
func TestCommitService_GetCombinedStatus_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -430,7 +234,7 @@ func TestCommitService_GetCombinedStatus_Good(t *testing.T) {
} }
} }
func TestCommitService_NotFound_Bad(t *testing.T) { func TestCommitService_Bad_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "not found"})

122
config.go
View file

@ -1,108 +1,26 @@
package forge package forge
import ( import (
"encoding/json"
"os" "os"
"path/filepath"
core "dappco.re/go/core" coreerr "dappco.re/go/core/log"
coreio "dappco.re/go/core/io"
) )
const ( const (
// DefaultURL is the fallback Forgejo instance URL when neither flag nor // DefaultURL is the fallback Forgejo instance URL when neither flag nor
// environment variable is set. // environment variable is set.
//
// Usage:
// cfgURL, _, _ := forge.ResolveConfig("", "")
// _ = cfgURL == forge.DefaultURL
DefaultURL = "http://localhost:3000" DefaultURL = "http://localhost:3000"
) )
const defaultConfigPath = ".config/forge/config.json"
type configFile struct {
URL string `json:"url"`
Token string `json:"token"`
}
// ConfigPath returns the default config file path used by SaveConfig and
// ResolveConfig.
//
// Usage:
//
// path, err := forge.ConfigPath()
// _ = path
func ConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", core.E("ConfigPath", "forge: resolve home directory", err)
}
return filepath.Join(home, defaultConfigPath), nil
}
func readConfigFile() (url, token string, err error) {
path, err := ConfigPath()
if err != nil {
return "", "", err
}
data, err := coreio.Local.Read(path)
if err != nil {
if os.IsNotExist(err) {
return "", "", nil
}
return "", "", core.E("ResolveConfig", "forge: read config file", err)
}
var cfg configFile
if err := json.Unmarshal([]byte(data), &cfg); err != nil {
return "", "", core.E("ResolveConfig", "forge: decode config file", err)
}
return cfg.URL, cfg.Token, nil
}
// SaveConfig persists the Forgejo URL and API token to the default config file.
// It creates the parent directory if it does not already exist.
//
// Usage:
//
// _ = forge.SaveConfig("https://forge.example.com", "token")
func SaveConfig(url, token string) error {
path, err := ConfigPath()
if err != nil {
return err
}
if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil {
return core.E("SaveConfig", "forge: create config directory", err)
}
payload, err := json.MarshalIndent(configFile{URL: url, Token: token}, "", " ")
if err != nil {
return core.E("SaveConfig", "forge: encode config file", err)
}
return coreio.Local.WriteMode(path, string(payload), 0600)
}
// ResolveConfig resolves the Forgejo URL and API token from flags, environment // ResolveConfig resolves the Forgejo URL and API token from flags, environment
// variables, config file, and built-in defaults. Priority order: // variables, and built-in defaults. Priority order: flags > env > defaults.
// flags > env > config file > defaults.
// //
// Environment variables: // Environment variables:
// - FORGE_URL — base URL of the Forgejo instance // - FORGE_URL — base URL of the Forgejo instance
// - FORGE_TOKEN — API token for authentication // - FORGE_TOKEN — API token for authentication
//
// Usage:
//
// url, token, err := forge.ResolveConfig("", "")
// _ = url
// _ = token
func ResolveConfig(flagURL, flagToken string) (url, token string, err error) { func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
if envURL, ok := os.LookupEnv("FORGE_URL"); ok && envURL != "" { url = os.Getenv("FORGE_URL")
url = envURL token = os.Getenv("FORGE_TOKEN")
}
if envToken, ok := os.LookupEnv("FORGE_TOKEN"); ok && envToken != "" {
token = envToken
}
if flagURL != "" { if flagURL != "" {
url = flagURL url = flagURL
@ -110,49 +28,21 @@ func ResolveConfig(flagURL, flagToken string) (url, token string, err error) {
if flagToken != "" { if flagToken != "" {
token = flagToken token = flagToken
} }
if url == "" || token == "" {
fileURL, fileToken, fileErr := readConfigFile()
if fileErr != nil {
return "", "", fileErr
}
if url == "" {
url = fileURL
}
if token == "" {
token = fileToken
}
}
if url == "" { if url == "" {
url = DefaultURL url = DefaultURL
} }
return url, token, nil return url, token, nil
} }
// NewFromConfig creates a new Forge client using resolved configuration.
//
// Usage:
//
// f, err := forge.NewFromConfig("", "")
// _ = f
func NewFromConfig(flagURL, flagToken string, opts ...Option) (*Forge, error) {
return NewForgeFromConfig(flagURL, flagToken, opts...)
}
// NewForgeFromConfig creates a new Forge client using resolved configuration. // NewForgeFromConfig creates a new Forge client using resolved configuration.
// It returns an error if no API token is available from flags, environment, // It returns an error if no API token is available from flags or environment.
// or the saved config file.
//
// Usage:
//
// f, err := forge.NewForgeFromConfig("", "")
// _ = f
func NewForgeFromConfig(flagURL, flagToken string, opts ...Option) (*Forge, error) { func NewForgeFromConfig(flagURL, flagToken string, opts ...Option) (*Forge, error) {
url, token, err := ResolveConfig(flagURL, flagToken) url, token, err := ResolveConfig(flagURL, flagToken)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if token == "" { if token == "" {
return nil, core.E("NewForgeFromConfig", "forge: no API token configured (set FORGE_TOKEN or pass --token)", nil) return nil, coreerr.E("NewForgeFromConfig", "forge: no API token configured (set FORGE_TOKEN or pass --token)", nil)
} }
return NewForge(url, token, opts...), nil return NewForge(url, token, opts...), nil
} }

View file

@ -1,15 +1,11 @@
package forge package forge
import ( import (
"encoding/json" "os"
"path/filepath"
"testing" "testing"
coreio "dappco.re/go/core/io"
) )
func TestResolveConfig_EnvOverrides_Good(t *testing.T) { func TestResolveConfig_Good_EnvOverrides(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("FORGE_URL", "https://forge.example.com") t.Setenv("FORGE_URL", "https://forge.example.com")
t.Setenv("FORGE_TOKEN", "env-token") t.Setenv("FORGE_TOKEN", "env-token")
@ -25,8 +21,7 @@ func TestResolveConfig_EnvOverrides_Good(t *testing.T) {
} }
} }
func TestResolveConfig_FlagOverridesEnv_Good(t *testing.T) { func TestResolveConfig_Good_FlagOverridesEnv(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("FORGE_URL", "https://env.example.com") t.Setenv("FORGE_URL", "https://env.example.com")
t.Setenv("FORGE_TOKEN", "env-token") t.Setenv("FORGE_TOKEN", "env-token")
@ -42,10 +37,9 @@ func TestResolveConfig_FlagOverridesEnv_Good(t *testing.T) {
} }
} }
func TestResolveConfig_DefaultURL_Good(t *testing.T) { func TestResolveConfig_Good_DefaultURL(t *testing.T) {
t.Setenv("HOME", t.TempDir()) os.Unsetenv("FORGE_URL")
t.Setenv("FORGE_URL", "") os.Unsetenv("FORGE_TOKEN")
t.Setenv("FORGE_TOKEN", "")
url, _, err := ResolveConfig("", "") url, _, err := ResolveConfig("", "")
if err != nil { if err != nil {
@ -56,150 +50,12 @@ func TestResolveConfig_DefaultURL_Good(t *testing.T) {
} }
} }
func TestResolveConfig_ConfigFile_Good(t *testing.T) { func TestNewForgeFromConfig_Bad_NoToken(t *testing.T) {
home := t.TempDir() os.Unsetenv("FORGE_URL")
t.Setenv("HOME", home) os.Unsetenv("FORGE_TOKEN")
t.Setenv("FORGE_URL", "")
t.Setenv("FORGE_TOKEN", "")
cfgPath := filepath.Join(home, ".config", "forge", "config.json")
if err := coreio.Local.EnsureDir(filepath.Dir(cfgPath)); err != nil {
t.Fatal(err)
}
data, err := json.Marshal(map[string]string{
"url": "https://file.example.com",
"token": "file-token",
})
if err != nil {
t.Fatal(err)
}
if err := coreio.Local.WriteMode(cfgPath, string(data), 0600); err != nil {
t.Fatal(err)
}
url, token, err := ResolveConfig("", "")
if err != nil {
t.Fatal(err)
}
if url != "https://file.example.com" {
t.Errorf("got url=%q", url)
}
if token != "file-token" {
t.Errorf("got token=%q", token)
}
}
func TestResolveConfig_EnvOverridesConfig_Good(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("FORGE_URL", "https://env.example.com")
t.Setenv("FORGE_TOKEN", "env-token")
if err := SaveConfig("https://file.example.com", "file-token"); err != nil {
t.Fatal(err)
}
url, token, err := ResolveConfig("", "")
if err != nil {
t.Fatal(err)
}
if url != "https://env.example.com" {
t.Errorf("got url=%q", url)
}
if token != "env-token" {
t.Errorf("got token=%q", token)
}
}
func TestResolveConfig_FlagOverridesBrokenConfig_Good(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("FORGE_URL", "")
t.Setenv("FORGE_TOKEN", "")
cfgPath := filepath.Join(home, ".config", "forge", "config.json")
if err := coreio.Local.EnsureDir(filepath.Dir(cfgPath)); err != nil {
t.Fatal(err)
}
if err := coreio.Local.WriteMode(cfgPath, "{not-json", 0600); err != nil {
t.Fatal(err)
}
url, token, err := ResolveConfig("https://flag.example.com", "flag-token")
if err != nil {
t.Fatal(err)
}
if url != "https://flag.example.com" {
t.Errorf("got url=%q", url)
}
if token != "flag-token" {
t.Errorf("got token=%q", token)
}
}
func TestNewForgeFromConfig_NoToken_Bad(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("FORGE_URL", "")
t.Setenv("FORGE_TOKEN", "")
_, err := NewForgeFromConfig("", "") _, err := NewForgeFromConfig("", "")
if err == nil { if err == nil {
t.Fatal("expected error for missing token") t.Fatal("expected error for missing token")
} }
} }
func TestNewFromConfig_Good(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("FORGE_URL", "https://forge.example.com")
t.Setenv("FORGE_TOKEN", "env-token")
f, err := NewFromConfig("", "")
if err != nil {
t.Fatal(err)
}
if f == nil {
t.Fatal("expected forge client")
}
if got := f.BaseURL(); got != "https://forge.example.com" {
t.Errorf("got baseURL=%q", got)
}
}
func TestSaveConfig_Good(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
if err := SaveConfig("https://file.example.com", "file-token"); err != nil {
t.Fatal(err)
}
cfgPath := filepath.Join(home, ".config", "forge", "config.json")
data, err := coreio.Local.Read(cfgPath)
if err != nil {
t.Fatal(err)
}
var cfg map[string]string
if err := json.Unmarshal([]byte(data), &cfg); err != nil {
t.Fatal(err)
}
if cfg["url"] != "https://file.example.com" {
t.Errorf("got url=%q", cfg["url"])
}
if cfg["token"] != "file-token" {
t.Errorf("got token=%q", cfg["token"])
}
}
func TestConfigPath_Good(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
got, err := ConfigPath()
if err != nil {
t.Fatal(err)
}
want := filepath.Join(home, ".config", "forge", "config.json")
if got != want {
t.Fatalf("got path=%q, want %q", got, want)
}
}

View file

@ -2,20 +2,13 @@ package forge
import ( import (
"context" "context"
"iter" "fmt"
"net/url"
core "dappco.re/go/core"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// ContentService handles file read/write operations via the Forgejo API. // ContentService handles file read/write operations via the Forgejo API.
// No Resource embedding — paths vary by operation. // No Resource embedding — paths vary by operation.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Contents.GetFile(ctx, "core", "go-forge", "README.md")
type ContentService struct { type ContentService struct {
client *Client client *Client
} }
@ -24,48 +17,9 @@ func newContentService(c *Client) *ContentService {
return &ContentService{client: c} return &ContentService{client: c}
} }
// ListContents returns the entries in a repository directory.
// If ref is non-empty, the listing is resolved against that branch, tag, or commit.
func (s *ContentService) ListContents(ctx context.Context, owner, repo, ref string) ([]types.ContentsResponse, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/contents", pathParams("owner", owner, "repo", repo))
if ref != "" {
u, err := url.Parse(path)
if err != nil {
return nil, core.E("ContentService.ListContents", "forge: parse path", err)
}
q := u.Query()
q.Set("ref", ref)
u.RawQuery = q.Encode()
path = u.String()
}
var out []types.ContentsResponse
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return out, nil
}
// IterContents returns an iterator over the entries in a repository directory.
// If ref is non-empty, the listing is resolved against that branch, tag, or commit.
func (s *ContentService) IterContents(ctx context.Context, owner, repo, ref string) iter.Seq2[types.ContentsResponse, error] {
return func(yield func(types.ContentsResponse, error) bool) {
items, err := s.ListContents(ctx, owner, repo, ref)
if err != nil {
yield(*new(types.ContentsResponse), err)
return
}
for _, item := range items {
if !yield(item, nil) {
return
}
}
}
}
// GetFile returns metadata and content for a file in a repository. // GetFile returns metadata and content for a file in a repository.
func (s *ContentService) GetFile(ctx context.Context, owner, repo, filepath string) (*types.ContentsResponse, error) { func (s *ContentService) GetFile(ctx context.Context, owner, repo, filepath string) (*types.ContentsResponse, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/contents/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath)) path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
var out types.ContentsResponse var out types.ContentsResponse
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -75,7 +29,7 @@ func (s *ContentService) GetFile(ctx context.Context, owner, repo, filepath stri
// CreateFile creates a new file in a repository. // CreateFile creates a new file in a repository.
func (s *ContentService) CreateFile(ctx context.Context, owner, repo, filepath string, opts *types.CreateFileOptions) (*types.FileResponse, error) { func (s *ContentService) CreateFile(ctx context.Context, owner, repo, filepath string, opts *types.CreateFileOptions) (*types.FileResponse, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/contents/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath)) path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
var out types.FileResponse var out types.FileResponse
if err := s.client.Post(ctx, path, opts, &out); err != nil { if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err return nil, err
@ -85,7 +39,7 @@ func (s *ContentService) CreateFile(ctx context.Context, owner, repo, filepath s
// UpdateFile updates an existing file in a repository. // UpdateFile updates an existing file in a repository.
func (s *ContentService) UpdateFile(ctx context.Context, owner, repo, filepath string, opts *types.UpdateFileOptions) (*types.FileResponse, error) { func (s *ContentService) UpdateFile(ctx context.Context, owner, repo, filepath string, opts *types.UpdateFileOptions) (*types.FileResponse, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/contents/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath)) path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
var out types.FileResponse var out types.FileResponse
if err := s.client.Put(ctx, path, opts, &out); err != nil { if err := s.client.Put(ctx, path, opts, &out); err != nil {
return nil, err return nil, err
@ -95,12 +49,12 @@ func (s *ContentService) UpdateFile(ctx context.Context, owner, repo, filepath s
// DeleteFile deletes a file from a repository. Uses DELETE with a JSON body. // DeleteFile deletes a file from a repository. Uses DELETE with a JSON body.
func (s *ContentService) DeleteFile(ctx context.Context, owner, repo, filepath string, opts *types.DeleteFileOptions) error { func (s *ContentService) DeleteFile(ctx context.Context, owner, repo, filepath string, opts *types.DeleteFileOptions) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/contents/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath)) path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
return s.client.DeleteWithBody(ctx, path, opts) return s.client.DeleteWithBody(ctx, path, opts)
} }
// GetRawFile returns the raw file content as bytes. // GetRawFile returns the raw file content as bytes.
func (s *ContentService) GetRawFile(ctx context.Context, owner, repo, filepath string) ([]byte, error) { func (s *ContentService) GetRawFile(ctx context.Context, owner, repo, filepath string) ([]byte, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/raw/{filepath}", pathParams("owner", owner, "repo", repo, "filepath", filepath)) path := fmt.Sprintf("/api/v1/repos/%s/%s/raw/%s", owner, repo, filepath)
return s.client.GetRaw(ctx, path) return s.client.GetRaw(ctx, path)
} }

View file

@ -2,7 +2,7 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -10,70 +10,7 @@ import (
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestContentService_ListContents_Good(t *testing.T) { func TestContentService_Good_GetFile(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/contents" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("ref"); got != "main" {
t.Errorf("got ref=%q, want %q", got, "main")
}
json.NewEncoder(w).Encode([]types.ContentsResponse{
{Name: "README.md", Path: "README.md", Type: "file"},
{Name: "docs", Path: "docs", Type: "dir"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
items, err := f.Contents.ListContents(context.Background(), "core", "go-forge", "main")
if err != nil {
t.Fatal(err)
}
if len(items) != 2 {
t.Fatalf("got %d items, want 2", len(items))
}
if items[0].Name != "README.md" || items[1].Type != "dir" {
t.Fatalf("unexpected results: %+v", items)
}
}
func TestContentService_IterContents_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/contents" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.ContentsResponse{
{Name: "README.md", Path: "README.md", Type: "file"},
{Name: "docs", Path: "docs", Type: "dir"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var got []string
for item, err := range f.Contents.IterContents(context.Background(), "core", "go-forge", "") {
if err != nil {
t.Fatal(err)
}
got = append(got, item.Name)
}
if len(got) != 2 {
t.Fatalf("got %d items, want 2", len(got))
}
if got[0] != "README.md" || got[1] != "docs" {
t.Fatalf("unexpected items: %+v", got)
}
}
func TestContentService_GetFile_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -112,7 +49,7 @@ func TestContentService_GetFile_Good(t *testing.T) {
} }
} }
func TestContentService_CreateFile_Good(t *testing.T) { func TestContentService_Good_CreateFile(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -161,7 +98,7 @@ func TestContentService_CreateFile_Good(t *testing.T) {
} }
} }
func TestContentService_UpdateFile_Good(t *testing.T) { func TestContentService_Good_UpdateFile(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut { if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method) t.Errorf("expected PUT, got %s", r.Method)
@ -201,7 +138,7 @@ func TestContentService_UpdateFile_Good(t *testing.T) {
} }
} }
func TestContentService_DeleteFile_Good(t *testing.T) { func TestContentService_Good_DeleteFile(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method) t.Errorf("expected DELETE, got %s", r.Method)
@ -231,7 +168,7 @@ func TestContentService_DeleteFile_Good(t *testing.T) {
} }
} }
func TestContentService_GetRawFile_Good(t *testing.T) { func TestContentService_Good_GetRawFile(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -255,7 +192,7 @@ func TestContentService_GetRawFile_Good(t *testing.T) {
} }
} }
func TestContentService_NotFound_Bad(t *testing.T) { func TestContentService_Bad_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "file not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "file not found"})
@ -272,7 +209,7 @@ func TestContentService_NotFound_Bad(t *testing.T) {
} }
} }
func TestContentService_GetRawNotFound_Bad(t *testing.T) { func TestContentService_Bad_GetRawNotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "file not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "file not found"})

3
doc.go
View file

@ -2,9 +2,8 @@
// //
// Usage: // Usage:
// //
// ctx := context.Background()
// f := forge.NewForge("https://forge.lthn.ai", "your-token") // f := forge.NewForge("https://forge.lthn.ai", "your-token")
// repos, err := f.Repos.ListOrgRepos(ctx, "core") // repos, err := f.Repos.List(ctx, forge.Params{"org": "core"}, forge.DefaultList)
// //
// Types are generated from Forgejo's swagger.v1.json spec via cmd/forgegen/. // Types are generated from Forgejo's swagger.v1.json spec via cmd/forgegen/.
// Run `go generate ./types/...` to regenerate after a Forgejo upgrade. // Run `go generate ./types/...` to regenerate after a Forgejo upgrade.

View file

@ -9,7 +9,7 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| Kind | Name | Signature | Description | Test Coverage | | Kind | Name | Signature | Description | Test Coverage |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| type | APIError | `type APIError struct` | APIError represents an error response from the Forgejo API. | `TestAPIError_Good_Error`, `TestClient_Bad_ServerError`, `TestIsConflict_Bad_NotConflict` (+2 more) | | type | APIError | `type APIError struct` | APIError represents an error response from the Forgejo API. | `TestAPIError_Good_Error`, `TestClient_Bad_ServerError`, `TestIsConflict_Bad_NotConflict` (+2 more) |
| type | ActionsService | `type ActionsService struct` | ActionsService handles CI/CD actions operations across repositories and organisations — secrets, variables, workflow dispatches, and tasks. No Resource embedding — heterogeneous endpoints across repo and org levels. | `TestActionsService_Bad_NotFound`, `TestActionsService_Good_CreateRepoSecret`, `TestActionsService_Good_CreateRepoVariable` (+9 more) | | type | ActionsService | `type ActionsService struct` | ActionsService handles CI/CD actions operations across repositories and organisations — secrets, variables, and workflow dispatches. No Resource embedding — heterogeneous endpoints across repo and org levels. | `TestActionsService_Bad_NotFound`, `TestActionsService_Good_CreateRepoSecret`, `TestActionsService_Good_CreateRepoVariable` (+7 more) |
| type | AdminService | `type AdminService struct` | AdminService handles site administration operations. Unlike other services, AdminService does not embed Resource[T,C,U] because admin endpoints are heterogeneous. | `TestAdminService_Bad_CreateUser_Forbidden`, `TestAdminService_Bad_DeleteUser_NotFound`, `TestAdminService_Good_AdoptRepo` (+9 more) | | type | AdminService | `type AdminService struct` | AdminService handles site administration operations. Unlike other services, AdminService does not embed Resource[T,C,U] because admin endpoints are heterogeneous. | `TestAdminService_Bad_CreateUser_Forbidden`, `TestAdminService_Bad_DeleteUser_NotFound`, `TestAdminService_Good_AdoptRepo` (+9 more) |
| type | BranchService | `type BranchService struct` | BranchService handles branch operations within a repository. | `TestBranchService_Good_CreateProtection`, `TestBranchService_Good_Get`, `TestBranchService_Good_List` | | type | BranchService | `type BranchService struct` | BranchService handles branch operations within a repository. | `TestBranchService_Good_CreateProtection`, `TestBranchService_Good_Get`, `TestBranchService_Good_List` |
| type | Client | `type Client struct` | Client is a low-level HTTP client for the Forgejo API. | `TestClient_Bad_Conflict`, `TestClient_Bad_Forbidden`, `TestClient_Bad_NotFound` (+9 more) | | type | Client | `type Client struct` | Client is a low-level HTTP client for the Forgejo API. | `TestClient_Bad_Conflict`, `TestClient_Bad_Forbidden`, `TestClient_Bad_NotFound` (+9 more) |
@ -34,7 +34,7 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| type | Resource | `type Resource[T any, C any, U any] struct` | Resource provides generic CRUD operations for a Forgejo API resource. T is the resource type, C is the create options type, U is the update options type. | `TestResource_Bad_IterError`, `TestResource_Good_Create`, `TestResource_Good_Delete` (+6 more) | | type | Resource | `type Resource[T any, C any, U any] struct` | Resource provides generic CRUD operations for a Forgejo API resource. T is the resource type, C is the create options type, U is the update options type. | `TestResource_Bad_IterError`, `TestResource_Good_Create`, `TestResource_Good_Delete` (+6 more) |
| type | TeamService | `type TeamService struct` | TeamService handles team operations. | `TestTeamService_Good_AddMember`, `TestTeamService_Good_Get`, `TestTeamService_Good_ListMembers` | | type | TeamService | `type TeamService struct` | TeamService handles team operations. | `TestTeamService_Good_AddMember`, `TestTeamService_Good_Get`, `TestTeamService_Good_ListMembers` |
| type | UserService | `type UserService struct` | UserService handles user operations. | `TestUserService_Good_Get`, `TestUserService_Good_GetCurrent`, `TestUserService_Good_ListFollowers` | | type | UserService | `type UserService struct` | UserService handles user operations. | `TestUserService_Good_Get`, `TestUserService_Good_GetCurrent`, `TestUserService_Good_ListFollowers` |
| type | WebhookService | `type WebhookService struct` | WebhookService handles webhook (hook) operations for repositories, organisations, and the authenticated user. Embeds Resource for standard CRUD on /api/v1/repos/{owner}/{repo}/hooks/{id}. | `TestWebhookService_Bad_NotFound`, `TestWebhookService_Good_Create`, `TestWebhookService_Good_Get` (+8 more) | | type | WebhookService | `type WebhookService struct` | WebhookService handles webhook (hook) operations within a repository. Embeds Resource for standard CRUD on /api/v1/repos/{owner}/{repo}/hooks/{id}. | `TestWebhookService_Bad_NotFound`, `TestWebhookService_Good_Create`, `TestWebhookService_Good_Get` (+3 more) |
| type | WikiService | `type WikiService struct` | WikiService handles wiki page operations for a repository. No Resource embedding — custom endpoints for wiki CRUD. | `TestWikiService_Bad_NotFound`, `TestWikiService_Good_CreatePage`, `TestWikiService_Good_DeletePage` (+3 more) | | type | WikiService | `type WikiService struct` | WikiService handles wiki page operations for a repository. No Resource embedding — custom endpoints for wiki CRUD. | `TestWikiService_Bad_NotFound`, `TestWikiService_Good_CreatePage`, `TestWikiService_Good_DeletePage` (+3 more) |
| function | IsConflict | `func IsConflict(err error) bool` | IsConflict returns true if the error is a 409 response. | `TestClient_Bad_Conflict`, `TestIsConflict_Bad_NotConflict`, `TestIsConflict_Good` (+1 more) | | function | IsConflict | `func IsConflict(err error) bool` | IsConflict returns true if the error is a 409 response. | `TestClient_Bad_Conflict`, `TestIsConflict_Bad_NotConflict`, `TestIsConflict_Good` (+1 more) |
| function | IsForbidden | `func IsForbidden(err error) bool` | IsForbidden returns true if the error is a 403 response. | `TestAdminService_Bad_CreateUser_Forbidden`, `TestClient_Bad_Forbidden`, `TestIsForbidden_Bad_NotForbidden` | | function | IsForbidden | `func IsForbidden(err error) bool` | IsForbidden returns true if the error is a 403 response. | `TestAdminService_Bad_CreateUser_Forbidden`, `TestClient_Bad_Forbidden`, `TestIsForbidden_Bad_NotForbidden` |
@ -56,7 +56,6 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| method | ActionsService.DeleteRepoSecret | `func (s *ActionsService) DeleteRepoSecret(ctx context.Context, owner, repo, name string) error` | DeleteRepoSecret removes a secret from a repository. | `TestActionsService_Good_DeleteRepoSecret` | | method | ActionsService.DeleteRepoSecret | `func (s *ActionsService) DeleteRepoSecret(ctx context.Context, owner, repo, name string) error` | DeleteRepoSecret removes a secret from a repository. | `TestActionsService_Good_DeleteRepoSecret` |
| method | ActionsService.DeleteRepoVariable | `func (s *ActionsService) DeleteRepoVariable(ctx context.Context, owner, repo, name string) error` | DeleteRepoVariable removes an action variable from a repository. | `TestActionsService_Good_DeleteRepoVariable` | | method | ActionsService.DeleteRepoVariable | `func (s *ActionsService) DeleteRepoVariable(ctx context.Context, owner, repo, name string) error` | DeleteRepoVariable removes an action variable from a repository. | `TestActionsService_Good_DeleteRepoVariable` |
| method | ActionsService.DispatchWorkflow | `func (s *ActionsService) DispatchWorkflow(ctx context.Context, owner, repo, workflow string, opts map[string]any) error` | DispatchWorkflow triggers a workflow run. | `TestActionsService_Good_DispatchWorkflow` | | method | ActionsService.DispatchWorkflow | `func (s *ActionsService) DispatchWorkflow(ctx context.Context, owner, repo, workflow string, opts map[string]any) error` | DispatchWorkflow triggers a workflow run. | `TestActionsService_Good_DispatchWorkflow` |
| method | ActionsService.IterRepoTasks | `func (s *ActionsService) IterRepoTasks(ctx context.Context, owner, repo string) iter.Seq2[types.ActionTask, error]` | IterRepoTasks returns an iterator over all action tasks for a repository. | `TestActionsService_Good_IterRepoTasks` |
| method | ActionsService.IterOrgSecrets | `func (s *ActionsService) IterOrgSecrets(ctx context.Context, org string) iter.Seq2[types.Secret, error]` | IterOrgSecrets returns an iterator over all secrets for an organisation. | No direct tests. | | method | ActionsService.IterOrgSecrets | `func (s *ActionsService) IterOrgSecrets(ctx context.Context, org string) iter.Seq2[types.Secret, error]` | IterOrgSecrets returns an iterator over all secrets for an organisation. | No direct tests. |
| method | ActionsService.IterOrgVariables | `func (s *ActionsService) IterOrgVariables(ctx context.Context, org string) iter.Seq2[types.ActionVariable, error]` | IterOrgVariables returns an iterator over all action variables for an organisation. | No direct tests. | | method | ActionsService.IterOrgVariables | `func (s *ActionsService) IterOrgVariables(ctx context.Context, org string) iter.Seq2[types.ActionVariable, error]` | IterOrgVariables returns an iterator over all action variables for an organisation. | No direct tests. |
| method | ActionsService.IterRepoSecrets | `func (s *ActionsService) IterRepoSecrets(ctx context.Context, owner, repo string) iter.Seq2[types.Secret, error]` | IterRepoSecrets returns an iterator over all secrets for a repository. | No direct tests. | | method | ActionsService.IterRepoSecrets | `func (s *ActionsService) IterRepoSecrets(ctx context.Context, owner, repo string) iter.Seq2[types.Secret, error]` | IterRepoSecrets returns an iterator over all secrets for a repository. | No direct tests. |
@ -64,7 +63,6 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| method | ActionsService.ListOrgSecrets | `func (s *ActionsService) ListOrgSecrets(ctx context.Context, org string) ([]types.Secret, error)` | ListOrgSecrets returns all secrets for an organisation. | `TestActionsService_Good_ListOrgSecrets` | | method | ActionsService.ListOrgSecrets | `func (s *ActionsService) ListOrgSecrets(ctx context.Context, org string) ([]types.Secret, error)` | ListOrgSecrets returns all secrets for an organisation. | `TestActionsService_Good_ListOrgSecrets` |
| method | ActionsService.ListOrgVariables | `func (s *ActionsService) ListOrgVariables(ctx context.Context, org string) ([]types.ActionVariable, error)` | ListOrgVariables returns all action variables for an organisation. | `TestActionsService_Good_ListOrgVariables` | | method | ActionsService.ListOrgVariables | `func (s *ActionsService) ListOrgVariables(ctx context.Context, org string) ([]types.ActionVariable, error)` | ListOrgVariables returns all action variables for an organisation. | `TestActionsService_Good_ListOrgVariables` |
| method | ActionsService.ListRepoSecrets | `func (s *ActionsService) ListRepoSecrets(ctx context.Context, owner, repo string) ([]types.Secret, error)` | ListRepoSecrets returns all secrets for a repository. | `TestActionsService_Bad_NotFound`, `TestActionsService_Good_ListRepoSecrets` | | method | ActionsService.ListRepoSecrets | `func (s *ActionsService) ListRepoSecrets(ctx context.Context, owner, repo string) ([]types.Secret, error)` | ListRepoSecrets returns all secrets for a repository. | `TestActionsService_Bad_NotFound`, `TestActionsService_Good_ListRepoSecrets` |
| method | ActionsService.ListRepoTasks | `func (s *ActionsService) ListRepoTasks(ctx context.Context, owner, repo string, opts ListOptions) (*types.ActionTaskResponse, error)` | ListRepoTasks returns a single page of action tasks for a repository. | `TestActionsService_Good_ListRepoTasks` |
| method | ActionsService.ListRepoVariables | `func (s *ActionsService) ListRepoVariables(ctx context.Context, owner, repo string) ([]types.ActionVariable, error)` | ListRepoVariables returns all action variables for a repository. | `TestActionsService_Good_ListRepoVariables` | | method | ActionsService.ListRepoVariables | `func (s *ActionsService) ListRepoVariables(ctx context.Context, owner, repo string) ([]types.ActionVariable, error)` | ListRepoVariables returns all action variables for a repository. | `TestActionsService_Good_ListRepoVariables` |
| method | AdminService.AdoptRepo | `func (s *AdminService) AdoptRepo(ctx context.Context, owner, repo string) error` | AdoptRepo adopts an unadopted repository (admin only). | `TestAdminService_Good_AdoptRepo` | | method | AdminService.AdoptRepo | `func (s *AdminService) AdoptRepo(ctx context.Context, owner, repo string) error` | AdoptRepo adopts an unadopted repository (admin only). | `TestAdminService_Good_AdoptRepo` |
| method | AdminService.CreateUser | `func (s *AdminService) CreateUser(ctx context.Context, opts *types.CreateUserOption) (*types.User, error)` | CreateUser creates a new user (admin only). | `TestAdminService_Bad_CreateUser_Forbidden`, `TestAdminService_Good_CreateUser` | | method | AdminService.CreateUser | `func (s *AdminService) CreateUser(ctx context.Context, opts *types.CreateUserOption) (*types.User, error)` | CreateUser creates a new user (admin only). | `TestAdminService_Bad_CreateUser_Forbidden`, `TestAdminService_Good_CreateUser` |
@ -72,15 +70,11 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| method | AdminService.EditUser | `func (s *AdminService) EditUser(ctx context.Context, username string, opts map[string]any) error` | EditUser edits an existing user (admin only). | `TestAdminService_Good_EditUser` | | method | AdminService.EditUser | `func (s *AdminService) EditUser(ctx context.Context, username string, opts map[string]any) error` | EditUser edits an existing user (admin only). | `TestAdminService_Good_EditUser` |
| method | AdminService.GenerateRunnerToken | `func (s *AdminService) GenerateRunnerToken(ctx context.Context) (string, error)` | GenerateRunnerToken generates an actions runner registration token. | `TestAdminService_Good_GenerateRunnerToken` | | method | AdminService.GenerateRunnerToken | `func (s *AdminService) GenerateRunnerToken(ctx context.Context) (string, error)` | GenerateRunnerToken generates an actions runner registration token. | `TestAdminService_Good_GenerateRunnerToken` |
| method | AdminService.IterCron | `func (s *AdminService) IterCron(ctx context.Context) iter.Seq2[types.Cron, error]` | IterCron returns an iterator over all cron tasks (admin only). | No direct tests. | | method | AdminService.IterCron | `func (s *AdminService) IterCron(ctx context.Context) iter.Seq2[types.Cron, error]` | IterCron returns an iterator over all cron tasks (admin only). | No direct tests. |
| method | AdminService.IterEmails | `func (s *AdminService) IterEmails(ctx context.Context) iter.Seq2[types.Email, error]` | IterEmails returns an iterator over all email addresses (admin only). | No direct tests. |
| method | AdminService.IterOrgs | `func (s *AdminService) IterOrgs(ctx context.Context) iter.Seq2[types.Organization, error]` | IterOrgs returns an iterator over all organisations (admin only). | No direct tests. | | method | AdminService.IterOrgs | `func (s *AdminService) IterOrgs(ctx context.Context) iter.Seq2[types.Organization, error]` | IterOrgs returns an iterator over all organisations (admin only). | No direct tests. |
| method | AdminService.IterSearchEmails | `func (s *AdminService) IterSearchEmails(ctx context.Context, q string) iter.Seq2[types.Email, error]` | IterSearchEmails returns an iterator over all email addresses matching a keyword (admin only). | No direct tests. |
| method | AdminService.IterUsers | `func (s *AdminService) IterUsers(ctx context.Context) iter.Seq2[types.User, error]` | IterUsers returns an iterator over all users (admin only). | No direct tests. | | method | AdminService.IterUsers | `func (s *AdminService) IterUsers(ctx context.Context) iter.Seq2[types.User, error]` | IterUsers returns an iterator over all users (admin only). | No direct tests. |
| method | AdminService.ListCron | `func (s *AdminService) ListCron(ctx context.Context) ([]types.Cron, error)` | ListCron returns all cron tasks (admin only). | `TestAdminService_Good_ListCron` | | method | AdminService.ListCron | `func (s *AdminService) ListCron(ctx context.Context) ([]types.Cron, error)` | ListCron returns all cron tasks (admin only). | `TestAdminService_Good_ListCron` |
| method | AdminService.ListEmails | `func (s *AdminService) ListEmails(ctx context.Context) ([]types.Email, error)` | ListEmails returns all email addresses (admin only). | `TestAdminService_ListEmails_Good` |
| method | AdminService.ListOrgs | `func (s *AdminService) ListOrgs(ctx context.Context) ([]types.Organization, error)` | ListOrgs returns all organisations (admin only). | `TestAdminService_Good_ListOrgs` | | method | AdminService.ListOrgs | `func (s *AdminService) ListOrgs(ctx context.Context) ([]types.Organization, error)` | ListOrgs returns all organisations (admin only). | `TestAdminService_Good_ListOrgs` |
| method | AdminService.ListUsers | `func (s *AdminService) ListUsers(ctx context.Context) ([]types.User, error)` | ListUsers returns all users (admin only). | `TestAdminService_Good_ListUsers` | | method | AdminService.ListUsers | `func (s *AdminService) ListUsers(ctx context.Context) ([]types.User, error)` | ListUsers returns all users (admin only). | `TestAdminService_Good_ListUsers` |
| method | AdminService.SearchEmails | `func (s *AdminService) SearchEmails(ctx context.Context, q string) ([]types.Email, error)` | SearchEmails searches all email addresses by keyword (admin only). | `TestAdminService_SearchEmails_Good` |
| method | AdminService.RenameUser | `func (s *AdminService) RenameUser(ctx context.Context, username, newName string) error` | RenameUser renames a user (admin only). | `TestAdminService_Good_RenameUser` | | method | AdminService.RenameUser | `func (s *AdminService) RenameUser(ctx context.Context, username, newName string) error` | RenameUser renames a user (admin only). | `TestAdminService_Good_RenameUser` |
| method | AdminService.RunCron | `func (s *AdminService) RunCron(ctx context.Context, task string) error` | RunCron runs a cron task by name (admin only). | `TestAdminService_Good_RunCron` | | method | AdminService.RunCron | `func (s *AdminService) RunCron(ctx context.Context, task string) error` | RunCron runs a cron task by name (admin only). | `TestAdminService_Good_RunCron` |
| method | BranchService.CreateBranchProtection | `func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo string, opts *types.CreateBranchProtectionOption) (*types.BranchProtection, error)` | CreateBranchProtection creates a new branch protection rule. | `TestBranchService_Good_CreateProtection` | | method | BranchService.CreateBranchProtection | `func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo string, opts *types.CreateBranchProtectionOption) (*types.BranchProtection, error)` | CreateBranchProtection creates a new branch protection rule. | `TestBranchService_Good_CreateProtection` |
@ -93,14 +87,11 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| method | Client.DeleteWithBody | `func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) error` | DeleteWithBody performs a DELETE request with a JSON body. | No direct tests. | | method | Client.DeleteWithBody | `func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) error` | DeleteWithBody performs a DELETE request with a JSON body. | No direct tests. |
| method | Client.Get | `func (c *Client) Get(ctx context.Context, path string, out any) error` | Get performs a GET request. | `TestClient_Bad_Forbidden`, `TestClient_Bad_NotFound`, `TestClient_Bad_ServerError` (+3 more) | | method | Client.Get | `func (c *Client) Get(ctx context.Context, path string, out any) error` | Get performs a GET request. | `TestClient_Bad_Forbidden`, `TestClient_Bad_NotFound`, `TestClient_Bad_ServerError` (+3 more) |
| method | Client.GetRaw | `func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error)` | GetRaw performs a GET request and returns the raw response body as bytes instead of JSON-decoding. Useful for endpoints that return raw file content. | No direct tests. | | method | Client.GetRaw | `func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error)` | GetRaw performs a GET request and returns the raw response body as bytes instead of JSON-decoding. Useful for endpoints that return raw file content. | No direct tests. |
| method | Client.HasToken | `func (c *Client) HasToken() bool` | HasToken reports whether the client was configured with an API token. | `TestClient_HasToken_Bad`, `TestClient_HasToken_Good` |
| method | Client.HTTPClient | `func (c *Client) HTTPClient() *http.Client` | HTTPClient returns the configured underlying HTTP client. | `TestClient_Good_WithHTTPClient` |
| method | Client.Patch | `func (c *Client) Patch(ctx context.Context, path string, body, out any) error` | Patch performs a PATCH request. | No direct tests. | | method | Client.Patch | `func (c *Client) Patch(ctx context.Context, path string, body, out any) error` | Patch performs a PATCH request. | No direct tests. |
| method | Client.Post | `func (c *Client) Post(ctx context.Context, path string, body, out any) error` | Post performs a POST request. | `TestClient_Bad_Conflict`, `TestClient_Good_Post` | | method | Client.Post | `func (c *Client) Post(ctx context.Context, path string, body, out any) error` | Post performs a POST request. | `TestClient_Bad_Conflict`, `TestClient_Good_Post` |
| method | Client.PostRaw | `func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, error)` | PostRaw performs a POST request with a JSON body and returns the raw response body as bytes instead of JSON-decoding. Useful for endpoints such as /markdown that return raw HTML text. | No direct tests. | | method | Client.PostRaw | `func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, error)` | PostRaw performs a POST request with a JSON body and returns the raw response body as bytes instead of JSON-decoding. Useful for endpoints such as /markdown that return raw HTML text. | No direct tests. |
| method | Client.Put | `func (c *Client) Put(ctx context.Context, path string, body, out any) error` | Put performs a PUT request. | No direct tests. | | method | Client.Put | `func (c *Client) Put(ctx context.Context, path string, body, out any) error` | Put performs a PUT request. | No direct tests. |
| method | Client.RateLimit | `func (c *Client) RateLimit() RateLimit` | RateLimit returns the last known rate limit information. | `TestClient_Good_RateLimit` | | method | Client.RateLimit | `func (c *Client) RateLimit() RateLimit` | RateLimit returns the last known rate limit information. | `TestClient_Good_RateLimit` |
| method | Client.UserAgent | `func (c *Client) UserAgent() string` | UserAgent returns the configured User-Agent header value. | `TestClient_Good_Options` |
| method | CommitService.CreateStatus | `func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha string, opts *types.CreateStatusOption) (*types.CommitStatus, error)` | CreateStatus creates a new commit status for the given SHA. | `TestCommitService_Good_CreateStatus` | | method | CommitService.CreateStatus | `func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha string, opts *types.CreateStatusOption) (*types.CommitStatus, error)` | CreateStatus creates a new commit status for the given SHA. | `TestCommitService_Good_CreateStatus` |
| method | CommitService.Get | `func (s *CommitService) Get(ctx context.Context, params Params) (*types.Commit, error)` | Get returns a single commit by SHA or ref. | `TestCommitService_Good_Get`, `TestCommitService_Good_List` | | method | CommitService.Get | `func (s *CommitService) Get(ctx context.Context, params Params) (*types.Commit, error)` | Get returns a single commit by SHA or ref. | `TestCommitService_Good_Get`, `TestCommitService_Good_List` |
| method | CommitService.GetCombinedStatus | `func (s *CommitService) GetCombinedStatus(ctx context.Context, owner, repo, ref string) (*types.CombinedStatus, error)` | GetCombinedStatus returns the combined status for a given ref (branch, tag, or SHA). | `TestCommitService_Good_GetCombinedStatus` | | method | CommitService.GetCombinedStatus | `func (s *CommitService) GetCombinedStatus(ctx context.Context, owner, repo, ref string) (*types.CombinedStatus, error)` | GetCombinedStatus returns the combined status for a given ref (branch, tag, or SHA). | `TestCommitService_Good_GetCombinedStatus` |
@ -115,28 +106,17 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| method | ContentService.GetRawFile | `func (s *ContentService) GetRawFile(ctx context.Context, owner, repo, filepath string) ([]byte, error)` | GetRawFile returns the raw file content as bytes. | `TestContentService_Bad_GetRawNotFound`, `TestContentService_Good_GetRawFile` | | method | ContentService.GetRawFile | `func (s *ContentService) GetRawFile(ctx context.Context, owner, repo, filepath string) ([]byte, error)` | GetRawFile returns the raw file content as bytes. | `TestContentService_Bad_GetRawNotFound`, `TestContentService_Good_GetRawFile` |
| method | ContentService.UpdateFile | `func (s *ContentService) UpdateFile(ctx context.Context, owner, repo, filepath string, opts *types.UpdateFileOptions) (*types.FileResponse, error)` | UpdateFile updates an existing file in a repository. | `TestContentService_Good_UpdateFile` | | method | ContentService.UpdateFile | `func (s *ContentService) UpdateFile(ctx context.Context, owner, repo, filepath string, opts *types.UpdateFileOptions) (*types.FileResponse, error)` | UpdateFile updates an existing file in a repository. | `TestContentService_Good_UpdateFile` |
| method | Forge.Client | `func (f *Forge) Client() *Client` | Client returns the underlying HTTP client. | `TestForge_Good_Client` | | method | Forge.Client | `func (f *Forge) Client() *Client` | Client returns the underlying HTTP client. | `TestForge_Good_Client` |
| method | Forge.HasToken | `func (f *Forge) HasToken() bool` | HasToken reports whether the Forge client was configured with an API token. | `TestForge_HasToken_Bad`, `TestForge_HasToken_Good` |
| method | Forge.HTTPClient | `func (f *Forge) HTTPClient() *http.Client` | HTTPClient returns the configured underlying HTTP client. | `TestForge_HTTPClient_Good` |
| method | Forge.RateLimit | `func (f *Forge) RateLimit() RateLimit` | RateLimit returns the last known rate limit information. | `TestForge_Good_RateLimit` |
| method | Forge.UserAgent | `func (f *Forge) UserAgent() string` | UserAgent returns the configured User-Agent header value. | `TestForge_Good_UserAgent` |
| method | IssueService.AddLabels | `func (s *IssueService) AddLabels(ctx context.Context, owner, repo string, index int64, labelIDs []int64) error` | AddLabels adds labels to an issue. | No direct tests. | | method | IssueService.AddLabels | `func (s *IssueService) AddLabels(ctx context.Context, owner, repo string, index int64, labelIDs []int64) error` | AddLabels adds labels to an issue. | No direct tests. |
| method | IssueService.AddReaction | `func (s *IssueService) AddReaction(ctx context.Context, owner, repo string, index int64, reaction string) error` | AddReaction adds a reaction to an issue. | No direct tests. | | method | IssueService.AddReaction | `func (s *IssueService) AddReaction(ctx context.Context, owner, repo string, index int64, reaction string) error` | AddReaction adds a reaction to an issue. | No direct tests. |
| method | IssueService.CreateComment | `func (s *IssueService) CreateComment(ctx context.Context, owner, repo string, index int64, body string) (*types.Comment, error)` | CreateComment creates a comment on an issue. | `TestIssueService_Good_CreateComment` | | method | IssueService.CreateComment | `func (s *IssueService) CreateComment(ctx context.Context, owner, repo string, index int64, body string) (*types.Comment, error)` | CreateComment creates a comment on an issue. | `TestIssueService_Good_CreateComment` |
| method | IssueService.AddTime | `func (s *IssueService) AddTime(ctx context.Context, owner, repo string, index int64, opts *types.AddTimeOption) (*types.TrackedTime, error)` | AddTime adds tracked time to an issue. | `TestIssueService_AddTime_Good` |
| method | IssueService.DeleteStopwatch | `func (s *IssueService) DeleteStopwatch(ctx context.Context, owner, repo string, index int64) error` | DeleteStopwatch deletes an issue's existing stopwatch. | `TestIssueService_DeleteStopwatch_Good` |
| method | IssueService.DeleteReaction | `func (s *IssueService) DeleteReaction(ctx context.Context, owner, repo string, index int64, reaction string) error` | DeleteReaction removes a reaction from an issue. | No direct tests. | | method | IssueService.DeleteReaction | `func (s *IssueService) DeleteReaction(ctx context.Context, owner, repo string, index int64, reaction string) error` | DeleteReaction removes a reaction from an issue. | No direct tests. |
| method | IssueService.IterComments | `func (s *IssueService) IterComments(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Comment, error]` | IterComments returns an iterator over all comments on an issue. | No direct tests. | | method | IssueService.IterComments | `func (s *IssueService) IterComments(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Comment, error]` | IterComments returns an iterator over all comments on an issue. | No direct tests. |
| method | IssueService.ListComments | `func (s *IssueService) ListComments(ctx context.Context, owner, repo string, index int64) ([]types.Comment, error)` | ListComments returns all comments on an issue. | No direct tests. | | method | IssueService.ListComments | `func (s *IssueService) ListComments(ctx context.Context, owner, repo string, index int64) ([]types.Comment, error)` | ListComments returns all comments on an issue. | No direct tests. |
| method | IssueService.IterTimeline | `func (s *IssueService) IterTimeline(ctx context.Context, owner, repo string, index int64, since, before *time.Time) iter.Seq2[types.TimelineComment, error]` | IterTimeline returns an iterator over all comments and events on an issue. | `TestIssueService_IterTimeline_Good` |
| method | IssueService.ListTimeline | `func (s *IssueService) ListTimeline(ctx context.Context, owner, repo string, index int64, since, before *time.Time) ([]types.TimelineComment, error)` | ListTimeline returns all comments and events on an issue. | `TestIssueService_ListTimeline_Good` |
| method | IssueService.ListTimes | `func (s *IssueService) ListTimes(ctx context.Context, owner, repo string, index int64, user string, since, before *time.Time) ([]types.TrackedTime, error)` | ListTimes returns all tracked times on an issue. | `TestIssueService_ListTimes_Good` |
| method | IssueService.IterTimes | `func (s *IssueService) IterTimes(ctx context.Context, owner, repo string, index int64, user string, since, before *time.Time) iter.Seq2[types.TrackedTime, error]` | IterTimes returns an iterator over all tracked times on an issue. | `TestIssueService_IterTimes_Good` |
| method | IssueService.Pin | `func (s *IssueService) Pin(ctx context.Context, owner, repo string, index int64) error` | Pin pins an issue. | `TestIssueService_Good_Pin` | | method | IssueService.Pin | `func (s *IssueService) Pin(ctx context.Context, owner, repo string, index int64) error` | Pin pins an issue. | `TestIssueService_Good_Pin` |
| method | IssueService.RemoveLabel | `func (s *IssueService) RemoveLabel(ctx context.Context, owner, repo string, index int64, labelID int64) error` | RemoveLabel removes a single label from an issue. | No direct tests. | | method | IssueService.RemoveLabel | `func (s *IssueService) RemoveLabel(ctx context.Context, owner, repo string, index int64, labelID int64) error` | RemoveLabel removes a single label from an issue. | No direct tests. |
| method | IssueService.SetDeadline | `func (s *IssueService) SetDeadline(ctx context.Context, owner, repo string, index int64, deadline string) error` | SetDeadline sets or updates the deadline on an issue. | No direct tests. | | method | IssueService.SetDeadline | `func (s *IssueService) SetDeadline(ctx context.Context, owner, repo string, index int64, deadline string) error` | SetDeadline sets or updates the deadline on an issue. | No direct tests. |
| method | IssueService.StartStopwatch | `func (s *IssueService) StartStopwatch(ctx context.Context, owner, repo string, index int64) error` | StartStopwatch starts the stopwatch on an issue. | No direct tests. | | method | IssueService.StartStopwatch | `func (s *IssueService) StartStopwatch(ctx context.Context, owner, repo string, index int64) error` | StartStopwatch starts the stopwatch on an issue. | No direct tests. |
| method | IssueService.StopStopwatch | `func (s *IssueService) StopStopwatch(ctx context.Context, owner, repo string, index int64) error` | StopStopwatch stops the stopwatch on an issue. | No direct tests. | | method | IssueService.StopStopwatch | `func (s *IssueService) StopStopwatch(ctx context.Context, owner, repo string, index int64) error` | StopStopwatch stops the stopwatch on an issue. | No direct tests. |
| method | IssueService.ResetTime | `func (s *IssueService) ResetTime(ctx context.Context, owner, repo string, index int64) error` | ResetTime removes all tracked time from an issue. | `TestIssueService_ResetTime_Good` |
| method | IssueService.Unpin | `func (s *IssueService) Unpin(ctx context.Context, owner, repo string, index int64) error` | Unpin unpins an issue. | No direct tests. | | method | IssueService.Unpin | `func (s *IssueService) Unpin(ctx context.Context, owner, repo string, index int64) error` | Unpin unpins an issue. | No direct tests. |
| method | LabelService.CreateOrgLabel | `func (s *LabelService) CreateOrgLabel(ctx context.Context, org string, opts *types.CreateLabelOption) (*types.Label, error)` | CreateOrgLabel creates a new label in an organisation. | `TestLabelService_Good_CreateOrgLabel` | | method | LabelService.CreateOrgLabel | `func (s *LabelService) CreateOrgLabel(ctx context.Context, org string, opts *types.CreateLabelOption) (*types.Label, error)` | CreateOrgLabel creates a new label in an organisation. | `TestLabelService_Good_CreateOrgLabel` |
| method | LabelService.CreateRepoLabel | `func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string, opts *types.CreateLabelOption) (*types.Label, error)` | CreateRepoLabel creates a new label in a repository. | `TestLabelService_Good_CreateRepoLabel` | | method | LabelService.CreateRepoLabel | `func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string, opts *types.CreateLabelOption) (*types.Label, error)` | CreateRepoLabel creates a new label in a repository. | `TestLabelService_Good_CreateRepoLabel` |
@ -149,27 +129,19 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| method | LabelService.ListRepoLabels | `func (s *LabelService) ListRepoLabels(ctx context.Context, owner, repo string) ([]types.Label, error)` | ListRepoLabels returns all labels for a repository. | `TestLabelService_Good_ListRepoLabels` | | method | LabelService.ListRepoLabels | `func (s *LabelService) ListRepoLabels(ctx context.Context, owner, repo string) ([]types.Label, error)` | ListRepoLabels returns all labels for a repository. | `TestLabelService_Good_ListRepoLabels` |
| method | MilestoneService.Create | `func (s *MilestoneService) Create(ctx context.Context, owner, repo string, opts *types.CreateMilestoneOption) (*types.Milestone, error)` | Create creates a new milestone. | No direct tests. | | method | MilestoneService.Create | `func (s *MilestoneService) Create(ctx context.Context, owner, repo string, opts *types.CreateMilestoneOption) (*types.Milestone, error)` | Create creates a new milestone. | No direct tests. |
| method | MilestoneService.Get | `func (s *MilestoneService) Get(ctx context.Context, owner, repo string, id int64) (*types.Milestone, error)` | Get returns a single milestone by ID. | No direct tests. | | method | MilestoneService.Get | `func (s *MilestoneService) Get(ctx context.Context, owner, repo string, id int64) (*types.Milestone, error)` | Get returns a single milestone by ID. | No direct tests. |
| method | MilestoneService.ListAll | `func (s *MilestoneService) ListAll(ctx context.Context, params Params, filters ...MilestoneListOptions) ([]types.Milestone, error)` | ListAll returns all milestones for a repository. | No direct tests. | | method | MilestoneService.ListAll | `func (s *MilestoneService) ListAll(ctx context.Context, params Params) ([]types.Milestone, error)` | ListAll returns all milestones for a repository. | No direct tests. |
| method | MiscService.GetGitignoreTemplate | `func (s *MiscService) GetGitignoreTemplate(ctx context.Context, name string) (*types.GitignoreTemplateInfo, error)` | GetGitignoreTemplate returns a single gitignore template by name. | `TestMiscService_Good_GetGitignoreTemplate` | | method | MiscService.GetGitignoreTemplate | `func (s *MiscService) GetGitignoreTemplate(ctx context.Context, name string) (*types.GitignoreTemplateInfo, error)` | GetGitignoreTemplate returns a single gitignore template by name. | `TestMiscService_Good_GetGitignoreTemplate` |
| method | MiscService.GetAPISettings | `func (s *MiscService) GetAPISettings(ctx context.Context) (*types.GeneralAPISettings, error)` | GetAPISettings returns the instance's global API settings. | `TestMiscService_GetAPISettings_Good` |
| method | MiscService.GetAttachmentSettings | `func (s *MiscService) GetAttachmentSettings(ctx context.Context) (*types.GeneralAttachmentSettings, error)` | GetAttachmentSettings returns the instance's global attachment settings. | `TestMiscService_GetAttachmentSettings_Good` |
| method | MiscService.GetLicense | `func (s *MiscService) GetLicense(ctx context.Context, name string) (*types.LicenseTemplateInfo, error)` | GetLicense returns a single licence template by name. | `TestMiscService_Bad_NotFound`, `TestMiscService_Good_GetLicense` | | method | MiscService.GetLicense | `func (s *MiscService) GetLicense(ctx context.Context, name string) (*types.LicenseTemplateInfo, error)` | GetLicense returns a single licence template by name. | `TestMiscService_Bad_NotFound`, `TestMiscService_Good_GetLicense` |
| method | MiscService.GetNodeInfo | `func (s *MiscService) GetNodeInfo(ctx context.Context) (*types.NodeInfo, error)` | GetNodeInfo returns the NodeInfo metadata for the Forgejo instance. | `TestMiscService_Good_GetNodeInfo` | | method | MiscService.GetNodeInfo | `func (s *MiscService) GetNodeInfo(ctx context.Context) (*types.NodeInfo, error)` | GetNodeInfo returns the NodeInfo metadata for the Forgejo instance. | `TestMiscService_Good_GetNodeInfo` |
| method | MiscService.GetRepositorySettings | `func (s *MiscService) GetRepositorySettings(ctx context.Context) (*types.GeneralRepoSettings, error)` | GetRepositorySettings returns the instance's global repository settings. | `TestMiscService_GetRepositorySettings_Good` |
| method | MiscService.GetVersion | `func (s *MiscService) GetVersion(ctx context.Context) (*types.ServerVersion, error)` | GetVersion returns the server version. | `TestMiscService_Good_GetVersion` | | method | MiscService.GetVersion | `func (s *MiscService) GetVersion(ctx context.Context) (*types.ServerVersion, error)` | GetVersion returns the server version. | `TestMiscService_Good_GetVersion` |
| method | MiscService.GetUISettings | `func (s *MiscService) GetUISettings(ctx context.Context) (*types.GeneralUISettings, error)` | GetUISettings returns the instance's global UI settings. | `TestMiscService_GetUISettings_Good` |
| method | MiscService.ListGitignoreTemplates | `func (s *MiscService) ListGitignoreTemplates(ctx context.Context) ([]string, error)` | ListGitignoreTemplates returns all available gitignore template names. | `TestMiscService_Good_ListGitignoreTemplates` | | method | MiscService.ListGitignoreTemplates | `func (s *MiscService) ListGitignoreTemplates(ctx context.Context) ([]string, error)` | ListGitignoreTemplates returns all available gitignore template names. | `TestMiscService_Good_ListGitignoreTemplates` |
| method | MiscService.ListLicenses | `func (s *MiscService) ListLicenses(ctx context.Context) ([]types.LicensesTemplateListEntry, error)` | ListLicenses returns all available licence templates. | `TestMiscService_Good_ListLicenses` | | method | MiscService.ListLicenses | `func (s *MiscService) ListLicenses(ctx context.Context) ([]types.LicensesTemplateListEntry, error)` | ListLicenses returns all available licence templates. | `TestMiscService_Good_ListLicenses` |
| method | MiscService.RenderMarkdown | `func (s *MiscService) RenderMarkdown(ctx context.Context, text, mode string) (string, error)` | RenderMarkdown renders markdown text to HTML. The response is raw HTML text, not JSON. | `TestMiscService_Good_RenderMarkdown` | | method | MiscService.RenderMarkdown | `func (s *MiscService) RenderMarkdown(ctx context.Context, text, mode string) (string, error)` | RenderMarkdown renders markdown text to HTML. The response is raw HTML text, not JSON. | `TestMiscService_Good_RenderMarkdown` |
| method | MiscService.RenderMarkup | `func (s *MiscService) RenderMarkup(ctx context.Context, text, mode string) (string, error)` | RenderMarkup renders markup text to HTML. The response is raw HTML text, not JSON. | `TestMiscService_Good_RenderMarkup` |
| method | MiscService.RenderMarkdownRaw | `func (s *MiscService) RenderMarkdownRaw(ctx context.Context, text string) (string, error)` | RenderMarkdownRaw renders raw markdown text to HTML. The request body is sent as text/plain and the response is raw HTML text, not JSON. | `TestMiscService_RenderMarkdownRaw_Good` |
| method | NotificationService.GetThread | `func (s *NotificationService) GetThread(ctx context.Context, id int64) (*types.NotificationThread, error)` | GetThread returns a single notification thread by ID. | `TestNotificationService_Bad_NotFound`, `TestNotificationService_Good_GetThread` | | method | NotificationService.GetThread | `func (s *NotificationService) GetThread(ctx context.Context, id int64) (*types.NotificationThread, error)` | GetThread returns a single notification thread by ID. | `TestNotificationService_Bad_NotFound`, `TestNotificationService_Good_GetThread` |
| method | NotificationService.Iter | `func (s *NotificationService) Iter(ctx context.Context) iter.Seq2[types.NotificationThread, error]` | Iter returns an iterator over all notifications for the authenticated user. | No direct tests. | | method | NotificationService.Iter | `func (s *NotificationService) Iter(ctx context.Context) iter.Seq2[types.NotificationThread, error]` | Iter returns an iterator over all notifications for the authenticated user. | No direct tests. |
| method | NotificationService.IterRepo | `func (s *NotificationService) IterRepo(ctx context.Context, owner, repo string) iter.Seq2[types.NotificationThread, error]` | IterRepo returns an iterator over all notifications for a specific repository. | No direct tests. | | method | NotificationService.IterRepo | `func (s *NotificationService) IterRepo(ctx context.Context, owner, repo string) iter.Seq2[types.NotificationThread, error]` | IterRepo returns an iterator over all notifications for a specific repository. | No direct tests. |
| method | NotificationService.List | `func (s *NotificationService) List(ctx context.Context) ([]types.NotificationThread, error)` | List returns all notifications for the authenticated user. | `TestNotificationService_Good_List` | | method | NotificationService.List | `func (s *NotificationService) List(ctx context.Context) ([]types.NotificationThread, error)` | List returns all notifications for the authenticated user. | `TestNotificationService_Good_List` |
| method | NotificationService.ListRepo | `func (s *NotificationService) ListRepo(ctx context.Context, owner, repo string) ([]types.NotificationThread, error)` | ListRepo returns all notifications for a specific repository. | `TestNotificationService_Good_ListRepo` | | method | NotificationService.ListRepo | `func (s *NotificationService) ListRepo(ctx context.Context, owner, repo string) ([]types.NotificationThread, error)` | ListRepo returns all notifications for a specific repository. | `TestNotificationService_Good_ListRepo` |
| method | NotificationService.MarkNotifications | `func (s *NotificationService) MarkNotifications(ctx context.Context, opts *NotificationMarkOptions) ([]types.NotificationThread, error)` | MarkNotifications marks authenticated-user notification threads as read, pinned, or unread. | `TestNotificationService_MarkNotifications_Good` |
| method | NotificationService.MarkRepoNotifications | `func (s *NotificationService) MarkRepoNotifications(ctx context.Context, owner, repo string, opts *NotificationRepoMarkOptions) ([]types.NotificationThread, error)` | MarkRepoNotifications marks repository notification threads as read, unread, or pinned. | `TestNotificationService_MarkRepoNotifications_Good` |
| method | NotificationService.MarkRead | `func (s *NotificationService) MarkRead(ctx context.Context) error` | MarkRead marks all notifications as read. | `TestNotificationService_Good_MarkRead` | | method | NotificationService.MarkRead | `func (s *NotificationService) MarkRead(ctx context.Context) error` | MarkRead marks all notifications as read. | `TestNotificationService_Good_MarkRead` |
| method | NotificationService.MarkThreadRead | `func (s *NotificationService) MarkThreadRead(ctx context.Context, id int64) error` | MarkThreadRead marks a single notification thread as read. | `TestNotificationService_Good_MarkThreadRead` | | method | NotificationService.MarkThreadRead | `func (s *NotificationService) MarkThreadRead(ctx context.Context, id int64) error` | MarkThreadRead marks a single notification thread as read. | `TestNotificationService_Good_MarkThreadRead` |
| method | OrgService.AddMember | `func (s *OrgService) AddMember(ctx context.Context, org, username string) error` | AddMember adds a user to an organisation. | No direct tests. | | method | OrgService.AddMember | `func (s *OrgService) AddMember(ctx context.Context, org, username string) error` | AddMember adds a user to an organisation. | No direct tests. |
@ -187,35 +159,26 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| method | PackageService.List | `func (s *PackageService) List(ctx context.Context, owner string) ([]types.Package, error)` | List returns all packages for a given owner. | `TestPackageService_Good_List` | | method | PackageService.List | `func (s *PackageService) List(ctx context.Context, owner string) ([]types.Package, error)` | List returns all packages for a given owner. | `TestPackageService_Good_List` |
| method | PackageService.ListFiles | `func (s *PackageService) ListFiles(ctx context.Context, owner, pkgType, name, version string) ([]types.PackageFile, error)` | ListFiles returns all files for a specific package version. | `TestPackageService_Good_ListFiles` | | method | PackageService.ListFiles | `func (s *PackageService) ListFiles(ctx context.Context, owner, pkgType, name, version string) ([]types.PackageFile, error)` | ListFiles returns all files for a specific package version. | `TestPackageService_Good_ListFiles` |
| method | PullService.DismissReview | `func (s *PullService) DismissReview(ctx context.Context, owner, repo string, index, reviewID int64, msg string) error` | DismissReview dismisses a pull request review. | No direct tests. | | method | PullService.DismissReview | `func (s *PullService) DismissReview(ctx context.Context, owner, repo string, index, reviewID int64, msg string) error` | DismissReview dismisses a pull request review. | No direct tests. |
| method | PullService.CancelReviewRequests | `func (s *PullService) CancelReviewRequests(ctx context.Context, owner, repo string, index int64, opts *types.PullReviewRequestOptions) error` | CancelReviewRequests cancels review requests for a pull request. | `TestPullService_CancelReviewRequests_Good` |
| method | PullService.IterReviews | `func (s *PullService) IterReviews(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.PullReview, error]` | IterReviews returns an iterator over all reviews on a pull request. | No direct tests. | | method | PullService.IterReviews | `func (s *PullService) IterReviews(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.PullReview, error]` | IterReviews returns an iterator over all reviews on a pull request. | No direct tests. |
| method | PullService.ListReviews | `func (s *PullService) ListReviews(ctx context.Context, owner, repo string, index int64) ([]types.PullReview, error)` | ListReviews returns all reviews on a pull request. | No direct tests. | | method | PullService.ListReviews | `func (s *PullService) ListReviews(ctx context.Context, owner, repo string, index int64) ([]types.PullReview, error)` | ListReviews returns all reviews on a pull request. | No direct tests. |
| method | PullService.Merge | `func (s *PullService) Merge(ctx context.Context, owner, repo string, index int64, method string) error` | Merge merges a pull request. Method is one of "merge", "rebase", "rebase-merge", "squash", "fast-forward-only", "manually-merged". | `TestPullService_Bad_Merge`, `TestPullService_Good_Merge` | | method | PullService.Merge | `func (s *PullService) Merge(ctx context.Context, owner, repo string, index int64, method string) error` | Merge merges a pull request. Method is one of "merge", "rebase", "rebase-merge", "squash", "fast-forward-only", "manually-merged". | `TestPullService_Bad_Merge`, `TestPullService_Good_Merge` |
| method | PullService.RequestReviewers | `func (s *PullService) RequestReviewers(ctx context.Context, owner, repo string, index int64, opts *types.PullReviewRequestOptions) ([]types.PullReview, error)` | RequestReviewers creates review requests for a pull request. | `TestPullService_RequestReviewers_Good` |
| method | PullService.SubmitReview | `func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, index int64, review map[string]any) (*types.PullReview, error)` | SubmitReview creates a new review on a pull request. | No direct tests. | | method | PullService.SubmitReview | `func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, index int64, review map[string]any) (*types.PullReview, error)` | SubmitReview creates a new review on a pull request. | No direct tests. |
| method | PullService.UndismissReview | `func (s *PullService) UndismissReview(ctx context.Context, owner, repo string, index, reviewID int64) error` | UndismissReview undismisses a pull request review. | No direct tests. | | method | PullService.UndismissReview | `func (s *PullService) UndismissReview(ctx context.Context, owner, repo string, index, reviewID int64) error` | UndismissReview undismisses a pull request review. | No direct tests. |
| method | PullService.Update | `func (s *PullService) Update(ctx context.Context, owner, repo string, index int64) error` | Update updates a pull request branch with the base branch. | No direct tests. | | method | PullService.Update | `func (s *PullService) Update(ctx context.Context, owner, repo string, index int64) error` | Update updates a pull request branch with the base branch. | No direct tests. |
| method | ReleaseService.DeleteAsset | `func (s *ReleaseService) DeleteAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) error` | DeleteAsset deletes a single asset from a release. | No direct tests. | | method | ReleaseService.DeleteAsset | `func (s *ReleaseService) DeleteAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) error` | DeleteAsset deletes a single asset from a release. | No direct tests. |
| method | ReleaseService.EditAsset | `func (s *ReleaseService) EditAsset(ctx context.Context, owner, repo string, releaseID, attachmentID int64, opts *types.EditAttachmentOptions) (*types.Attachment, error)` | EditAsset updates a release asset. | `TestReleaseService_EditAttachment_Good` |
| method | ReleaseService.DeleteByTag | `func (s *ReleaseService) DeleteByTag(ctx context.Context, owner, repo, tag string) error` | DeleteByTag deletes a release by its tag name. | No direct tests. | | method | ReleaseService.DeleteByTag | `func (s *ReleaseService) DeleteByTag(ctx context.Context, owner, repo, tag string) error` | DeleteByTag deletes a release by its tag name. | No direct tests. |
| method | ReleaseService.GetAsset | `func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) (*types.Attachment, error)` | GetAsset returns a single asset for a release. | No direct tests. | | method | ReleaseService.GetAsset | `func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) (*types.Attachment, error)` | GetAsset returns a single asset for a release. | No direct tests. |
| method | ReleaseService.GetByTag | `func (s *ReleaseService) GetByTag(ctx context.Context, owner, repo, tag string) (*types.Release, error)` | GetByTag returns a release by its tag name. | `TestReleaseService_Good_GetByTag` | | method | ReleaseService.GetByTag | `func (s *ReleaseService) GetByTag(ctx context.Context, owner, repo, tag string) (*types.Release, error)` | GetByTag returns a release by its tag name. | `TestReleaseService_Good_GetByTag` |
| method | ReleaseService.GetLatest | `func (s *ReleaseService) GetLatest(ctx context.Context, owner, repo string) (*types.Release, error)` | GetLatest returns the most recent non-prerelease, non-draft release. | `TestReleaseService_GetLatest_Good` |
| method | ReleaseService.IterAssets | `func (s *ReleaseService) IterAssets(ctx context.Context, owner, repo string, releaseID int64) iter.Seq2[types.Attachment, error]` | IterAssets returns an iterator over all assets for a release. | No direct tests. | | method | ReleaseService.IterAssets | `func (s *ReleaseService) IterAssets(ctx context.Context, owner, repo string, releaseID int64) iter.Seq2[types.Attachment, error]` | IterAssets returns an iterator over all assets for a release. | No direct tests. |
| method | ReleaseService.ListAssets | `func (s *ReleaseService) ListAssets(ctx context.Context, owner, repo string, releaseID int64) ([]types.Attachment, error)` | ListAssets returns all assets for a release. | No direct tests. | | method | ReleaseService.ListAssets | `func (s *ReleaseService) ListAssets(ctx context.Context, owner, repo string, releaseID int64) ([]types.Attachment, error)` | ListAssets returns all assets for a release. | No direct tests. |
| method | RepoService.AcceptTransfer | `func (s *RepoService) AcceptTransfer(ctx context.Context, owner, repo string) error` | AcceptTransfer accepts a pending repository transfer. | No direct tests. | | method | RepoService.AcceptTransfer | `func (s *RepoService) AcceptTransfer(ctx context.Context, owner, repo string) error` | AcceptTransfer accepts a pending repository transfer. | No direct tests. |
| method | RepoService.Fork | `func (s *RepoService) Fork(ctx context.Context, owner, repo, org string) (*types.Repository, error)` | Fork forks a repository. If org is non-empty, forks into that organisation. | `TestRepoService_Good_Fork` | | method | RepoService.Fork | `func (s *RepoService) Fork(ctx context.Context, owner, repo, org string) (*types.Repository, error)` | Fork forks a repository. If org is non-empty, forks into that organisation. | `TestRepoService_Good_Fork` |
| method | RepoService.GetArchive | `func (s *RepoService) GetArchive(ctx context.Context, owner, repo, archive string) ([]byte, error)` | GetArchive returns a repository archive as raw bytes. | `TestRepoService_GetArchive_Good` |
| method | RepoService.GetSigningKey | `func (s *RepoService) GetSigningKey(ctx context.Context, owner, repo string) (string, error)` | GetSigningKey returns the repository signing key as ASCII-armoured text. | `TestRepoService_GetSigningKey_Good` |
| method | RepoService.GetNewPinAllowed | `func (s *RepoService) GetNewPinAllowed(ctx context.Context, owner, repo string) (*types.NewIssuePinsAllowed, error)` | GetNewPinAllowed returns whether new issue pins are allowed for a repository. | `TestRepoService_GetNewPinAllowed_Good` |
| method | RepoService.IterOrgRepos | `func (s *RepoService) IterOrgRepos(ctx context.Context, org string) iter.Seq2[types.Repository, error]` | IterOrgRepos returns an iterator over all repositories for an organisation. | No direct tests. | | method | RepoService.IterOrgRepos | `func (s *RepoService) IterOrgRepos(ctx context.Context, org string) iter.Seq2[types.Repository, error]` | IterOrgRepos returns an iterator over all repositories for an organisation. | No direct tests. |
| method | RepoService.IterUserRepos | `func (s *RepoService) IterUserRepos(ctx context.Context) iter.Seq2[types.Repository, error]` | IterUserRepos returns an iterator over all repositories for the authenticated user. | No direct tests. | | method | RepoService.IterUserRepos | `func (s *RepoService) IterUserRepos(ctx context.Context) iter.Seq2[types.Repository, error]` | IterUserRepos returns an iterator over all repositories for the authenticated user. | No direct tests. |
| method | RepoService.ListOrgRepos | `func (s *RepoService) ListOrgRepos(ctx context.Context, org string) ([]types.Repository, error)` | ListOrgRepos returns all repositories for an organisation. | `TestRepoService_Good_ListOrgRepos` | | method | RepoService.ListOrgRepos | `func (s *RepoService) ListOrgRepos(ctx context.Context, org string) ([]types.Repository, error)` | ListOrgRepos returns all repositories for an organisation. | `TestRepoService_Good_ListOrgRepos` |
| method | RepoService.ListUserRepos | `func (s *RepoService) ListUserRepos(ctx context.Context) ([]types.Repository, error)` | ListUserRepos returns all repositories for the authenticated user. | No direct tests. | | method | RepoService.ListUserRepos | `func (s *RepoService) ListUserRepos(ctx context.Context) ([]types.Repository, error)` | ListUserRepos returns all repositories for the authenticated user. | No direct tests. |
| method | RepoService.MirrorSync | `func (s *RepoService) MirrorSync(ctx context.Context, owner, repo string) error` | MirrorSync triggers a mirror sync. | No direct tests. | | method | RepoService.MirrorSync | `func (s *RepoService) MirrorSync(ctx context.Context, owner, repo string) error` | MirrorSync triggers a mirror sync. | No direct tests. |
| method | RepoService.RejectTransfer | `func (s *RepoService) RejectTransfer(ctx context.Context, owner, repo string) error` | RejectTransfer rejects a pending repository transfer. | No direct tests. | | method | RepoService.RejectTransfer | `func (s *RepoService) RejectTransfer(ctx context.Context, owner, repo string) error` | RejectTransfer rejects a pending repository transfer. | No direct tests. |
| method | RepoService.DeleteAvatar | `func (s *RepoService) DeleteAvatar(ctx context.Context, owner, repo string) error` | DeleteAvatar deletes a repository avatar. | `TestRepoService_DeleteAvatar_Good` |
| method | RepoService.UpdateAvatar | `func (s *RepoService) UpdateAvatar(ctx context.Context, owner, repo string, opts *types.UpdateRepoAvatarOption) error` | UpdateAvatar updates a repository avatar. | `TestRepoService_UpdateAvatar_Good` |
| method | RepoService.Transfer | `func (s *RepoService) Transfer(ctx context.Context, owner, repo string, opts map[string]any) error` | Transfer initiates a repository transfer. | No direct tests. | | method | RepoService.Transfer | `func (s *RepoService) Transfer(ctx context.Context, owner, repo string, opts map[string]any) error` | Transfer initiates a repository transfer. | No direct tests. |
| method | Resource.Create | `func (r *Resource[T, C, U]) Create(ctx context.Context, params Params, body *C) (*T, error)` | Create creates a new resource. | `TestResource_Good_Create` | | method | Resource.Create | `func (r *Resource[T, C, U]) Create(ctx context.Context, params Params, body *C) (*T, error)` | Create creates a new resource. | `TestResource_Good_Create` |
| method | Resource.Delete | `func (r *Resource[T, C, U]) Delete(ctx context.Context, params Params) error` | Delete removes a resource. | `TestResource_Good_Delete` | | method | Resource.Delete | `func (r *Resource[T, C, U]) Delete(ctx context.Context, params Params) error` | Delete removes a resource. | `TestResource_Good_Delete` |
@ -235,17 +198,7 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| method | TeamService.RemoveMember | `func (s *TeamService) RemoveMember(ctx context.Context, teamID int64, username string) error` | RemoveMember removes a user from a team. | No direct tests. | | method | TeamService.RemoveMember | `func (s *TeamService) RemoveMember(ctx context.Context, teamID int64, username string) error` | RemoveMember removes a user from a team. | No direct tests. |
| method | TeamService.RemoveRepo | `func (s *TeamService) RemoveRepo(ctx context.Context, teamID int64, org, repo string) error` | RemoveRepo removes a repository from a team. | No direct tests. | | method | TeamService.RemoveRepo | `func (s *TeamService) RemoveRepo(ctx context.Context, teamID int64, org, repo string) error` | RemoveRepo removes a repository from a team. | No direct tests. |
| method | UserService.Follow | `func (s *UserService) Follow(ctx context.Context, username string) error` | Follow follows a user as the authenticated user. | No direct tests. | | method | UserService.Follow | `func (s *UserService) Follow(ctx context.Context, username string) error` | Follow follows a user as the authenticated user. | No direct tests. |
| method | UserService.CheckFollowing | `func (s *UserService) CheckFollowing(ctx context.Context, username, target string) (bool, error)` | CheckFollowing reports whether one user is following another user. | `TestUserService_CheckFollowing_Good`, `TestUserService_CheckFollowing_Bad_NotFound` |
| method | UserService.GetCurrent | `func (s *UserService) GetCurrent(ctx context.Context) (*types.User, error)` | GetCurrent returns the authenticated user. | `TestUserService_Good_GetCurrent` | | method | UserService.GetCurrent | `func (s *UserService) GetCurrent(ctx context.Context) (*types.User, error)` | GetCurrent returns the authenticated user. | `TestUserService_Good_GetCurrent` |
| method | UserService.GetQuota | `func (s *UserService) GetQuota(ctx context.Context) (*types.QuotaInfo, error)` | GetQuota returns the authenticated user's quota information. | `TestUserService_GetQuota_Good` |
| method | UserService.ListQuotaArtifacts | `func (s *UserService) ListQuotaArtifacts(ctx context.Context) ([]types.QuotaUsedArtifact, error)` | ListQuotaArtifacts returns all artifacts affecting the authenticated user's quota. | `TestUserService_ListQuotaArtifacts_Good` |
| method | UserService.IterQuotaArtifacts | `func (s *UserService) IterQuotaArtifacts(ctx context.Context) iter.Seq2[types.QuotaUsedArtifact, error]` | IterQuotaArtifacts returns an iterator over all artifacts affecting the authenticated user's quota. | `TestUserService_IterQuotaArtifacts_Good` |
| method | UserService.ListQuotaAttachments | `func (s *UserService) ListQuotaAttachments(ctx context.Context) ([]types.QuotaUsedAttachment, error)` | ListQuotaAttachments returns all attachments affecting the authenticated user's quota. | `TestUserService_ListQuotaAttachments_Good` |
| method | UserService.IterQuotaAttachments | `func (s *UserService) IterQuotaAttachments(ctx context.Context) iter.Seq2[types.QuotaUsedAttachment, error]` | IterQuotaAttachments returns an iterator over all attachments affecting the authenticated user's quota. | `TestUserService_IterQuotaAttachments_Good` |
| method | UserService.ListQuotaPackages | `func (s *UserService) ListQuotaPackages(ctx context.Context) ([]types.QuotaUsedPackage, error)` | ListQuotaPackages returns all packages affecting the authenticated user's quota. | `TestUserService_ListQuotaPackages_Good` |
| method | UserService.IterQuotaPackages | `func (s *UserService) IterQuotaPackages(ctx context.Context) iter.Seq2[types.QuotaUsedPackage, error]` | IterQuotaPackages returns an iterator over all packages affecting the authenticated user's quota. | `TestUserService_IterQuotaPackages_Good` |
| method | UserService.ListStopwatches | `func (s *UserService) ListStopwatches(ctx context.Context) ([]types.StopWatch, error)` | ListStopwatches returns all existing stopwatches for the authenticated user. | `TestUserService_ListStopwatches_Good` |
| method | UserService.IterStopwatches | `func (s *UserService) IterStopwatches(ctx context.Context) iter.Seq2[types.StopWatch, error]` | IterStopwatches returns an iterator over all existing stopwatches for the authenticated user. | `TestUserService_IterStopwatches_Good` |
| method | UserService.IterFollowers | `func (s *UserService) IterFollowers(ctx context.Context, username string) iter.Seq2[types.User, error]` | IterFollowers returns an iterator over all followers of a user. | No direct tests. | | method | UserService.IterFollowers | `func (s *UserService) IterFollowers(ctx context.Context, username string) iter.Seq2[types.User, error]` | IterFollowers returns an iterator over all followers of a user. | No direct tests. |
| method | UserService.IterFollowing | `func (s *UserService) IterFollowing(ctx context.Context, username string) iter.Seq2[types.User, error]` | IterFollowing returns an iterator over all users that a user is following. | No direct tests. | | method | UserService.IterFollowing | `func (s *UserService) IterFollowing(ctx context.Context, username string) iter.Seq2[types.User, error]` | IterFollowing returns an iterator over all users that a user is following. | No direct tests. |
| method | UserService.IterStarred | `func (s *UserService) IterStarred(ctx context.Context, username string) iter.Seq2[types.Repository, error]` | IterStarred returns an iterator over all repositories starred by a user. | No direct tests. | | method | UserService.IterStarred | `func (s *UserService) IterStarred(ctx context.Context, username string) iter.Seq2[types.Repository, error]` | IterStarred returns an iterator over all repositories starred by a user. | No direct tests. |
@ -257,16 +210,6 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r
| method | UserService.Unstar | `func (s *UserService) Unstar(ctx context.Context, owner, repo string) error` | Unstar unstars a repository as the authenticated user. | No direct tests. | | method | UserService.Unstar | `func (s *UserService) Unstar(ctx context.Context, owner, repo string) error` | Unstar unstars a repository as the authenticated user. | No direct tests. |
| method | WebhookService.IterOrgHooks | `func (s *WebhookService) IterOrgHooks(ctx context.Context, org string) iter.Seq2[types.Hook, error]` | IterOrgHooks returns an iterator over all webhooks for an organisation. | No direct tests. | | method | WebhookService.IterOrgHooks | `func (s *WebhookService) IterOrgHooks(ctx context.Context, org string) iter.Seq2[types.Hook, error]` | IterOrgHooks returns an iterator over all webhooks for an organisation. | No direct tests. |
| method | WebhookService.ListOrgHooks | `func (s *WebhookService) ListOrgHooks(ctx context.Context, org string) ([]types.Hook, error)` | ListOrgHooks returns all webhooks for an organisation. | `TestWebhookService_Good_ListOrgHooks` | | method | WebhookService.ListOrgHooks | `func (s *WebhookService) ListOrgHooks(ctx context.Context, org string) ([]types.Hook, error)` | ListOrgHooks returns all webhooks for an organisation. | `TestWebhookService_Good_ListOrgHooks` |
| method | WebhookService.ListGitHooks | `func (s *WebhookService) ListGitHooks(ctx context.Context, owner, repo string) ([]types.GitHook, error)` | ListGitHooks returns all Git hooks for a repository. | `TestWebhookService_Good_ListGitHooks` |
| method | WebhookService.GetGitHook | `func (s *WebhookService) GetGitHook(ctx context.Context, owner, repo, id string) (*types.GitHook, error)` | GetGitHook returns a single Git hook for a repository. | `TestWebhookService_Good_GetGitHook` |
| method | WebhookService.EditGitHook | `func (s *WebhookService) EditGitHook(ctx context.Context, owner, repo, id string, opts *types.EditGitHookOption) (*types.GitHook, error)` | EditGitHook updates an existing Git hook in a repository. | `TestWebhookService_Good_EditGitHook` |
| method | WebhookService.DeleteGitHook | `func (s *WebhookService) DeleteGitHook(ctx context.Context, owner, repo, id string) error` | DeleteGitHook deletes a Git hook from a repository. | `TestWebhookService_Good_DeleteGitHook` |
| method | WebhookService.IterUserHooks | `func (s *WebhookService) IterUserHooks(ctx context.Context) iter.Seq2[types.Hook, error]` | IterUserHooks returns an iterator over all webhooks for the authenticated user. | No direct tests. |
| method | WebhookService.ListUserHooks | `func (s *WebhookService) ListUserHooks(ctx context.Context) ([]types.Hook, error)` | ListUserHooks returns all webhooks for the authenticated user. | `TestWebhookService_Good_ListUserHooks` |
| method | WebhookService.GetUserHook | `func (s *WebhookService) GetUserHook(ctx context.Context, id int64) (*types.Hook, error)` | GetUserHook returns a single webhook for the authenticated user. | `TestWebhookService_Good_GetUserHook` |
| method | WebhookService.CreateUserHook | `func (s *WebhookService) CreateUserHook(ctx context.Context, opts *types.CreateHookOption) (*types.Hook, error)` | CreateUserHook creates a webhook for the authenticated user. | `TestWebhookService_Good_CreateUserHook` |
| method | WebhookService.EditUserHook | `func (s *WebhookService) EditUserHook(ctx context.Context, id int64, opts *types.EditHookOption) (*types.Hook, error)` | EditUserHook updates an existing authenticated-user webhook. | `TestWebhookService_Good_EditUserHook` |
| method | WebhookService.DeleteUserHook | `func (s *WebhookService) DeleteUserHook(ctx context.Context, id int64) error` | DeleteUserHook deletes an authenticated-user webhook. | `TestWebhookService_Good_DeleteUserHook` |
| method | WebhookService.TestHook | `func (s *WebhookService) TestHook(ctx context.Context, owner, repo string, id int64) error` | TestHook triggers a test delivery for a webhook. | `TestWebhookService_Good_TestHook` | | method | WebhookService.TestHook | `func (s *WebhookService) TestHook(ctx context.Context, owner, repo string, id int64) error` | TestHook triggers a test delivery for a webhook. | `TestWebhookService_Good_TestHook` |
| method | WikiService.CreatePage | `func (s *WikiService) CreatePage(ctx context.Context, owner, repo string, opts *types.CreateWikiPageOptions) (*types.WikiPage, error)` | CreatePage creates a new wiki page. | `TestWikiService_Good_CreatePage` | | method | WikiService.CreatePage | `func (s *WikiService) CreatePage(ctx context.Context, owner, repo string, opts *types.CreateWikiPageOptions) (*types.WikiPage, error)` | CreatePage creates a new wiki page. | `TestWikiService_Good_CreatePage` |
| method | WikiService.DeletePage | `func (s *WikiService) DeletePage(ctx context.Context, owner, repo, pageName string) error` | DeletePage removes a wiki page. | `TestWikiService_Good_DeletePage` | | method | WikiService.DeletePage | `func (s *WikiService) DeletePage(ctx context.Context, owner, repo, pageName string) error` | DeletePage removes a wiki page. | `TestWikiService_Good_DeletePage` |

View file

@ -15,7 +15,7 @@ go-forge is organised in three layers, each building on the one below:
``` ```
┌─────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ Forge (top-level client) │ │ Forge (top-level client) │
│ Aggregates 20 service structs │ │ Aggregates 18 service structs │
├─────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────┤
│ Service layer │ │ Service layer │
│ RepoService, IssueService, PullService, ... │ │ RepoService, IssueService, PullService, ... │
@ -49,7 +49,6 @@ func (c *Client) Delete(ctx context.Context, path string) error
func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) error func (c *Client) DeleteWithBody(ctx context.Context, path string, body any) error
func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error)
func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, error) func (c *Client) PostRaw(ctx context.Context, path string, body any) ([]byte, error)
func (c *Client) HTTPClient() *http.Client
``` ```
The `Raw` variants return the response body as `[]byte` instead of decoding JSON. This is used by endpoints that return non-JSON content (e.g. the markdown rendering endpoint returns raw HTML). The `Raw` variants return the response body as `[]byte` instead of decoding JSON. This is used by endpoints that return non-JSON content (e.g. the markdown rendering endpoint returns raw HTML).

View file

@ -39,7 +39,7 @@ All tests use the standard `testing` package with `net/http/httptest` for HTTP s
go test ./... go test ./...
# Run a specific test by name # Run a specific test by name
go test -v -run TestClient_Get_Good ./... go test -v -run TestClient_Good_Get ./...
# Run tests with race detection # Run tests with race detection
go test -race ./... go test -race ./...
@ -59,7 +59,7 @@ core go cov --open # Open coverage report in browser
### Test naming convention ### Test naming convention
Tests follow `Test<TypeOrArea>_<MethodOrCase>_<Good|Bad|Ugly>`: Tests follow the `_Good`, `_Bad`, `_Ugly` suffix pattern:
- **`_Good`** — Happy-path tests confirming correct behaviour. - **`_Good`** — Happy-path tests confirming correct behaviour.
- **`_Bad`** — Expected error conditions (e.g. 404, 500 responses). - **`_Bad`** — Expected error conditions (e.g. 404, 500 responses).
@ -67,11 +67,11 @@ Tests follow `Test<TypeOrArea>_<MethodOrCase>_<Good|Bad|Ugly>`:
Examples: Examples:
``` ```
TestClient_Get_Good TestClient_Good_Get
TestClient_ServerError_Bad TestClient_Bad_ServerError
TestClient_NotFound_Bad TestClient_Bad_NotFound
TestClient_ContextCancellation_Good TestClient_Good_ContextCancellation
TestResource_ListAll_Good TestResource_Good_ListAll
``` ```
@ -173,7 +173,7 @@ To add coverage for a new Forgejo API domain:
```go ```go
func (s *TopicService) ListRepoTopics(ctx context.Context, owner, repo string) ([]types.Topic, error) { func (s *TopicService) ListRepoTopics(ctx context.Context, owner, repo string) ([]types.Topic, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/topics", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/topics", owner, repo)
return ListAll[types.Topic](ctx, s.client, path, nil) return ListAll[types.Topic](ctx, s.client, path, nil)
} }
``` ```

View file

@ -5,7 +5,7 @@ description: Full-coverage Go client for the Forgejo API with generics-based CRU
# go-forge # go-forge
`dappco.re/go/core/forge` is a Go client library for the [Forgejo](https://forgejo.org) REST API. It provides typed access to 20 API domains (repositories, issues, pull requests, organisations, milestones, ActivityPub, and more) through a single top-level `Forge` client. Types are generated directly from Forgejo's `swagger.v1.json` specification, keeping the library in lockstep with the server. `dappco.re/go/core/forge` is a Go client library for the [Forgejo](https://forgejo.org) REST API. It provides typed access to 18 API domains (repositories, issues, pull requests, organisations, and more) through a single top-level `Forge` client. Types are generated directly from Forgejo's `swagger.v1.json` specification, keeping the library in lockstep with the server.
**Module path:** `dappco.re/go/core/forge` **Module path:** `dappco.re/go/core/forge`
**Go version:** 1.26+ **Go version:** 1.26+
@ -75,7 +75,7 @@ Environment variables:
go-forge/ go-forge/
├── client.go HTTP client, auth, error handling, rate limits ├── client.go HTTP client, auth, error handling, rate limits
├── config.go Config resolution: flags > env > defaults ├── config.go Config resolution: flags > env > defaults
├── forge.go Top-level Forge struct aggregating all 20 services ├── forge.go Top-level Forge struct aggregating all 18 services
├── resource.go Generic Resource[T, C, U] for CRUD operations ├── resource.go Generic Resource[T, C, U] for CRUD operations
├── pagination.go ListPage, ListAll, ListIter — paginated requests ├── pagination.go ListPage, ListAll, ListIter — paginated requests
├── params.go Path variable resolution ({owner}/{repo} -> values) ├── params.go Path variable resolution ({owner}/{repo} -> values)
@ -92,13 +92,11 @@ go-forge/
├── webhooks.go WebhookService — repo and org webhooks ├── webhooks.go WebhookService — repo and org webhooks
├── notifications.go NotificationService — notifications, threads ├── notifications.go NotificationService — notifications, threads
├── packages.go PackageService — package registry ├── packages.go PackageService — package registry
├── actions.go ActionsService — CI/CD secrets, variables, dispatches, tasks ├── actions.go ActionsService — CI/CD secrets, variables, dispatches
├── contents.go ContentService — file read/write/delete ├── contents.go ContentService — file read/write/delete
├── wiki.go WikiService — wiki pages ├── wiki.go WikiService — wiki pages
├── misc.go MiscService — markdown, licences, gitignore, version
├── commits.go CommitService — statuses, notes ├── commits.go CommitService — statuses, notes
├── milestones.go MilestoneService — repository milestones ├── misc.go MiscService — markdown, licences, gitignore, version
├── activitypub.go ActivityPubService — ActivityPub actors and inboxes
├── types/ 229 generated Go types from swagger.v1.json ├── types/ 229 generated Go types from swagger.v1.json
│ ├── generate.go go:generate directive │ ├── generate.go go:generate directive
│ ├── repo.go Repository, CreateRepoOption, EditRepoOption, ... │ ├── repo.go Repository, CreateRepoOption, EditRepoOption, ...
@ -116,7 +114,7 @@ go-forge/
## Services ## Services
The `Forge` struct exposes 20 service fields, each handling a different API domain: The `Forge` struct exposes 18 service fields, each handling a different API domain:
| Service | Struct | Embedding | Domain | | Service | Struct | Embedding | Domain |
|-----------------|---------------------|----------------------------------|--------------------------------------| |-----------------|---------------------|----------------------------------|--------------------------------------|
@ -133,20 +131,18 @@ The `Forge` struct exposes 20 service fields, each handling a different API doma
| `Webhooks` | `WebhookService` | `Resource[Hook, ...]` | Repo and org webhooks | | `Webhooks` | `WebhookService` | `Resource[Hook, ...]` | Repo and org webhooks |
| `Notifications` | `NotificationService` | (standalone) | Notifications, threads | | `Notifications` | `NotificationService` | (standalone) | Notifications, threads |
| `Packages` | `PackageService` | (standalone) | Package registry | | `Packages` | `PackageService` | (standalone) | Package registry |
| `Actions` | `ActionsService` | (standalone) | CI/CD secrets, variables, dispatches, tasks | | `Actions` | `ActionsService` | (standalone) | CI/CD secrets, variables, dispatches |
| `Contents` | `ContentService` | (standalone) | File read/write/delete | | `Contents` | `ContentService` | (standalone) | File read/write/delete |
| `Wiki` | `WikiService` | (standalone) | Wiki pages | | `Wiki` | `WikiService` | (standalone) | Wiki pages |
| `Misc` | `MiscService` | (standalone) | Markdown, licences, gitignore, version |
| `Commits` | `CommitService` | (standalone) | Commit statuses, git notes | | `Commits` | `CommitService` | (standalone) | Commit statuses, git notes |
| `Milestones` | `MilestoneService` | (standalone) | Repository milestones | | `Misc` | `MiscService` | (standalone) | Markdown, licences, gitignore, version |
| `ActivityPub` | `ActivityPubService` | (standalone) | ActivityPub actors and inboxes |
Services that embed `Resource[T, C, U]` inherit `List`, `ListAll`, `Iter`, `Get`, `Create`, `Update`, and `Delete` methods automatically. Standalone services have hand-written methods because their API endpoints are heterogeneous and do not fit a uniform CRUD pattern. Services that embed `Resource[T, C, U]` inherit `List`, `ListAll`, `Iter`, `Get`, `Create`, `Update`, and `Delete` methods automatically. Standalone services have hand-written methods because their API endpoints are heterogeneous and do not fit a uniform CRUD pattern.
## Dependencies ## Dependencies
This module has a small dependency set: `dappco.re/go/core` and `github.com/goccy/go-json`, plus the Go standard library (`net/http`, `context`, `iter`, etc.) where appropriate. This module has **zero external dependencies**. It relies solely on the Go standard library (`net/http`, `encoding/json`, `context`, `iter`, etc.) and requires Go 1.26 or later.
``` ```
module dappco.re/go/core/forge module dappco.re/go/core/forge

113
forge.go
View file

@ -1,14 +1,6 @@
package forge package forge
import "net/http"
// Forge is the top-level client for the Forgejo API. // Forge is the top-level client for the Forgejo API.
//
// Usage:
//
// ctx := context.Background()
// f := forge.NewForge("https://forge.lthn.ai", "token")
// repo, err := f.Repos.Get(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
type Forge struct { type Forge struct {
client *Client client *Client
@ -31,16 +23,9 @@ type Forge struct {
Misc *MiscService Misc *MiscService
Commits *CommitService Commits *CommitService
Milestones *MilestoneService Milestones *MilestoneService
ActivityPub *ActivityPubService
} }
// NewForge creates a new Forge client. // NewForge creates a new Forge client.
//
// Usage:
//
// ctx := context.Background()
// f := forge.NewForge("https://forge.lthn.ai", "token")
// repos, err := f.Repos.ListOrgRepos(ctx, "core")
func NewForge(url, token string, opts ...Option) *Forge { func NewForge(url, token string, opts ...Option) *Forge {
c := NewClient(url, token, opts...) c := NewClient(url, token, opts...)
f := &Forge{client: c} f := &Forge{client: c}
@ -63,102 +48,8 @@ func NewForge(url, token string, opts ...Option) *Forge {
f.Misc = newMiscService(c) f.Misc = newMiscService(c)
f.Commits = newCommitService(c) f.Commits = newCommitService(c)
f.Milestones = newMilestoneService(c) f.Milestones = newMilestoneService(c)
f.ActivityPub = newActivityPubService(c)
return f return f
} }
// Client returns the underlying Forge client. // Client returns the underlying HTTP client.
// func (f *Forge) Client() *Client { return f.client }
// Usage:
//
// client := f.Client()
func (f *Forge) Client() *Client {
if f == nil {
return nil
}
return f.client
}
// BaseURL returns the configured Forgejo base URL.
//
// Usage:
//
// baseURL := f.BaseURL()
func (f *Forge) BaseURL() string {
if f == nil || f.client == nil {
return ""
}
return f.client.BaseURL()
}
// RateLimit returns the last known rate limit information.
//
// Usage:
//
// rl := f.RateLimit()
func (f *Forge) RateLimit() RateLimit {
if f == nil || f.client == nil {
return RateLimit{}
}
return f.client.RateLimit()
}
// UserAgent returns the configured User-Agent header value.
//
// Usage:
//
// ua := f.UserAgent()
func (f *Forge) UserAgent() string {
if f == nil || f.client == nil {
return ""
}
return f.client.UserAgent()
}
// HTTPClient returns the configured underlying HTTP client.
//
// Usage:
//
// hc := f.HTTPClient()
func (f *Forge) HTTPClient() *http.Client {
if f == nil || f.client == nil {
return nil
}
return f.client.HTTPClient()
}
// HasToken reports whether the Forge client was configured with an API token.
//
// Usage:
//
// if f.HasToken() {
// _ = "authenticated"
// }
func (f *Forge) HasToken() bool {
if f == nil || f.client == nil {
return false
}
return f.client.HasToken()
}
// String returns a safe summary of the Forge client.
//
// Usage:
//
// s := f.String()
func (f *Forge) String() string {
if f == nil {
return "forge.Forge{<nil>}"
}
if f.client == nil {
return "forge.Forge{client=<nil>}"
}
return "forge.Forge{client=" + f.client.String() + "}"
}
// GoString returns a safe Go-syntax summary of the Forge client.
//
// Usage:
//
// s := fmt.Sprintf("%#v", f)
func (f *Forge) GoString() string { return f.String() }

View file

@ -1,10 +1,8 @@
package forge package forge
import ( import (
"bytes"
"context" "context"
"fmt" "encoding/json"
json "github.com/goccy/go-json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -12,7 +10,7 @@ import (
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestForge_NewForge_Good(t *testing.T) { func TestForge_Good_NewForge(t *testing.T) {
f := NewForge("https://forge.lthn.ai", "tok") f := NewForge("https://forge.lthn.ai", "tok")
if f.Repos == nil { if f.Repos == nil {
t.Fatal("Repos service is nil") t.Fatal("Repos service is nil")
@ -20,12 +18,9 @@ func TestForge_NewForge_Good(t *testing.T) {
if f.Issues == nil { if f.Issues == nil {
t.Fatal("Issues service is nil") t.Fatal("Issues service is nil")
} }
if f.ActivityPub == nil {
t.Fatal("ActivityPub service is nil")
}
} }
func TestForge_Client_Good(t *testing.T) { func TestForge_Good_Client(t *testing.T) {
f := NewForge("https://forge.lthn.ai", "tok") f := NewForge("https://forge.lthn.ai", "tok")
c := f.Client() c := f.Client()
if c == nil { if c == nil {
@ -34,92 +29,9 @@ func TestForge_Client_Good(t *testing.T) {
if c.baseURL != "https://forge.lthn.ai" { if c.baseURL != "https://forge.lthn.ai" {
t.Errorf("got baseURL=%q", c.baseURL) t.Errorf("got baseURL=%q", c.baseURL)
} }
if got := c.BaseURL(); got != "https://forge.lthn.ai" {
t.Errorf("got BaseURL()=%q", got)
}
} }
func TestForge_BaseURL_Good(t *testing.T) { func TestRepoService_Good_ListOrgRepos(t *testing.T) {
f := NewForge("https://forge.lthn.ai", "tok")
if got := f.BaseURL(); got != "https://forge.lthn.ai" {
t.Fatalf("got base URL %q", got)
}
}
func TestForge_RateLimit_Good(t *testing.T) {
f := NewForge("https://forge.lthn.ai", "tok")
if got := f.RateLimit(); got != (RateLimit{}) {
t.Fatalf("got rate limit %#v", got)
}
}
func TestForge_UserAgent_Good(t *testing.T) {
f := NewForge("https://forge.lthn.ai", "tok", WithUserAgent("go-forge/1.0"))
if got := f.UserAgent(); got != "go-forge/1.0" {
t.Fatalf("got user agent %q", got)
}
}
func TestForge_HTTPClient_Good(t *testing.T) {
custom := &http.Client{}
f := NewForge("https://forge.lthn.ai", "tok", WithHTTPClient(custom))
if got := f.HTTPClient(); got != custom {
t.Fatal("expected HTTPClient() to return the configured HTTP client")
}
}
func TestForge_HasToken_Good(t *testing.T) {
f := NewForge("https://forge.lthn.ai", "tok")
if !f.HasToken() {
t.Fatal("expected HasToken to report configured token")
}
}
func TestForge_HasToken_Bad(t *testing.T) {
f := NewForge("https://forge.lthn.ai", "")
if f.HasToken() {
t.Fatal("expected HasToken to report missing token")
}
}
func TestForge_NilSafeAccessors(t *testing.T) {
var f *Forge
if got := f.Client(); got != nil {
t.Fatal("expected Client() to return nil")
}
if got := f.BaseURL(); got != "" {
t.Fatalf("got BaseURL()=%q, want empty string", got)
}
if got := f.RateLimit(); got != (RateLimit{}) {
t.Fatalf("got RateLimit()=%#v, want zero value", got)
}
if got := f.UserAgent(); got != "" {
t.Fatalf("got UserAgent()=%q, want empty string", got)
}
if got := f.HTTPClient(); got != nil {
t.Fatal("expected HTTPClient() to return nil")
}
if got := f.HasToken(); got {
t.Fatal("expected HasToken() to report false")
}
}
func TestForge_String_Good(t *testing.T) {
f := NewForge("https://forge.lthn.ai", "tok", WithUserAgent("go-forge/1.0"))
got := fmt.Sprint(f)
want := `forge.Forge{client=forge.Client{baseURL="https://forge.lthn.ai", token=set, userAgent="go-forge/1.0"}}`
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
if got := f.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", f); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestRepoService_ListOrgRepos_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -144,7 +56,7 @@ func TestRepoService_ListOrgRepos_Good(t *testing.T) {
} }
} }
func TestRepoService_Get_Good(t *testing.T) { func TestRepoService_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/core/go-forge" { if r.URL.Path != "/api/v1/repos/core/go-forge" {
t.Errorf("wrong path: %s", r.URL.Path) t.Errorf("wrong path: %s", r.URL.Path)
@ -165,7 +77,7 @@ func TestRepoService_Get_Good(t *testing.T) {
} }
} }
func TestRepoService_Update_Good(t *testing.T) { func TestRepoService_Good_Update(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch { if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method) t.Errorf("expected PATCH, got %s", r.Method)
@ -193,7 +105,7 @@ func TestRepoService_Update_Good(t *testing.T) {
} }
} }
func TestRepoService_Delete_Good(t *testing.T) { func TestRepoService_Good_Delete(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method) t.Errorf("expected DELETE, got %s", r.Method)
@ -213,7 +125,7 @@ func TestRepoService_Delete_Good(t *testing.T) {
} }
} }
func TestRepoService_Get_Bad(t *testing.T) { func TestRepoService_Bad_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
@ -226,7 +138,7 @@ func TestRepoService_Get_Bad(t *testing.T) {
} }
} }
func TestRepoService_Fork_Good(t *testing.T) { func TestRepoService_Good_Fork(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -245,80 +157,3 @@ func TestRepoService_Fork_Good(t *testing.T) {
t.Error("expected fork=true") t.Error("expected fork=true")
} }
} }
func TestRepoService_GetArchive_Good(t *testing.T) {
want := []byte("zip-bytes")
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/archive/master.zip" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(want)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
got, err := f.Repos.GetArchive(context.Background(), "core", "go-forge", "master.zip")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("got %q, want %q", got, want)
}
}
func TestRepoService_GetRawFile_Good(t *testing.T) {
want := []byte("# go-forge\n\nA Go client for Forgejo.")
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/raw/README.md" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(want)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
got, err := f.Repos.GetRawFile(context.Background(), "core", "go-forge", "README.md")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Fatalf("got %q, want %q", got, want)
}
}
func TestRepoService_ListTags_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/tags" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Tag{{Name: "v1.0.0"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
tags, err := f.Repos.ListTags(context.Background(), "core", "go-forge")
if err != nil {
t.Fatal(err)
}
if len(tags) != 1 || tags[0].Name != "v1.0.0" {
t.Fatalf("unexpected result: %+v", tags)
}
}

5
go.mod
View file

@ -3,9 +3,8 @@ module dappco.re/go/core/forge
go 1.26.0 go 1.26.0
require ( require (
dappco.re/go/core v0.4.7
dappco.re/go/core/io v0.2.0 dappco.re/go/core/io v0.2.0
github.com/goccy/go-json v0.10.6 dappco.re/go/core/log v0.1.0
) )
require dappco.re/go/core/log v0.0.4 // indirect require forge.lthn.ai/core/go-log v0.0.4 // indirect

6
go.sum
View file

@ -1,13 +1,11 @@
dappco.re/go/core v0.4.7 h1:KmIA/2lo6rl1NMtLrKqCWfMlUqpDZYH3q0/d10dTtGA=
dappco.re/go/core v0.4.7/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4= dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E= dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=

View file

@ -1,121 +0,0 @@
package forge
import (
"fmt"
"strconv"
"strings"
"time"
core "dappco.re/go/core"
)
func trimTrailingSlashes(s string) string {
for core.HasSuffix(s, "/") {
s = core.TrimSuffix(s, "/")
}
return s
}
func int64String(v int64) string {
return strconv.FormatInt(v, 10)
}
func pathParams(values ...string) Params {
params := make(Params, len(values)/2)
for i := 0; i+1 < len(values); i += 2 {
params[values[i]] = values[i+1]
}
return params
}
func optionString(typeName string, fields ...any) string {
var b strings.Builder
b.WriteString(typeName)
b.WriteString("{")
wroteField := false
for i := 0; i+1 < len(fields); i += 2 {
name, _ := fields[i].(string)
value := fields[i+1]
if isZeroOptionValue(value) {
continue
}
if wroteField {
b.WriteString(", ")
}
wroteField = true
b.WriteString(name)
b.WriteString("=")
b.WriteString(formatOptionValue(value))
}
b.WriteString("}")
return b.String()
}
func isZeroOptionValue(v any) bool {
switch x := v.(type) {
case nil:
return true
case string:
return x == ""
case bool:
return !x
case int:
return x == 0
case int64:
return x == 0
case []string:
return len(x) == 0
case *time.Time:
return x == nil
case *bool:
return x == nil
case time.Time:
return x.IsZero()
default:
return false
}
}
func formatOptionValue(v any) string {
switch x := v.(type) {
case string:
return strconv.Quote(x)
case bool:
return strconv.FormatBool(x)
case int:
return strconv.Itoa(x)
case int64:
return strconv.FormatInt(x, 10)
case []string:
return fmt.Sprintf("%#v", x)
case *time.Time:
if x == nil {
return "<nil>"
}
return strconv.Quote(x.Format(time.RFC3339))
case *bool:
if x == nil {
return "<nil>"
}
return strconv.FormatBool(*x)
case time.Time:
return strconv.Quote(x.Format(time.RFC3339))
default:
return fmt.Sprintf("%#v", v)
}
}
func serviceString(typeName, fieldName string, value any) string {
return typeName + "{" + fieldName + "=" + fmt.Sprint(value) + "}"
}
func lastIndexByte(s string, b byte) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == b {
return i
}
}
return -1
}

731
issues.go
View file

@ -2,156 +2,17 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"strconv"
"time"
goio "io"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// IssueService handles issue operations within a repository. // IssueService handles issue operations within a repository.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Issues.ListAll(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
type IssueService struct { type IssueService struct {
Resource[types.Issue, types.CreateIssueOption, types.EditIssueOption] Resource[types.Issue, types.CreateIssueOption, types.EditIssueOption]
} }
// IssueListOptions controls filtering for repository issue listings.
//
// Usage:
//
// opts := forge.IssueListOptions{State: "open", Labels: "bug"}
type IssueListOptions struct {
State string
Labels string
Query string
Type string
Milestones string
Since *time.Time
Before *time.Time
CreatedBy string
AssignedBy string
MentionedBy string
}
// String returns a safe summary of the issue list filters.
func (o IssueListOptions) String() string {
return optionString("forge.IssueListOptions",
"state", o.State,
"labels", o.Labels,
"q", o.Query,
"type", o.Type,
"milestones", o.Milestones,
"since", o.Since,
"before", o.Before,
"created_by", o.CreatedBy,
"assigned_by", o.AssignedBy,
"mentioned_by", o.MentionedBy,
)
}
// GoString returns a safe Go-syntax summary of the issue list filters.
func (o IssueListOptions) GoString() string { return o.String() }
func (o IssueListOptions) queryParams() map[string]string {
query := make(map[string]string, 10)
if o.State != "" {
query["state"] = o.State
}
if o.Labels != "" {
query["labels"] = o.Labels
}
if o.Query != "" {
query["q"] = o.Query
}
if o.Type != "" {
query["type"] = o.Type
}
if o.Milestones != "" {
query["milestones"] = o.Milestones
}
if o.Since != nil {
query["since"] = o.Since.Format(time.RFC3339)
}
if o.Before != nil {
query["before"] = o.Before.Format(time.RFC3339)
}
if o.CreatedBy != "" {
query["created_by"] = o.CreatedBy
}
if o.AssignedBy != "" {
query["assigned_by"] = o.AssignedBy
}
if o.MentionedBy != "" {
query["mentioned_by"] = o.MentionedBy
}
if len(query) == 0 {
return nil
}
return query
}
// AttachmentUploadOptions controls metadata sent when uploading an attachment.
//
// Usage:
//
// opts := forge.AttachmentUploadOptions{Name: "screenshot.png"}
type AttachmentUploadOptions struct {
Name string
UpdatedAt *time.Time
}
// String returns a safe summary of the attachment upload metadata.
func (o AttachmentUploadOptions) String() string {
return optionString("forge.AttachmentUploadOptions",
"name", o.Name,
"updated_at", o.UpdatedAt,
)
}
// GoString returns a safe Go-syntax summary of the attachment upload metadata.
func (o AttachmentUploadOptions) GoString() string { return o.String() }
// RepoCommentListOptions controls filtering for repository-wide issue comment listings.
//
// Usage:
//
// opts := forge.RepoCommentListOptions{Page: 1, Limit: 50}
type RepoCommentListOptions struct {
Since *time.Time
Before *time.Time
}
// String returns a safe summary of the repository comment filters.
func (o RepoCommentListOptions) String() string {
return optionString("forge.RepoCommentListOptions",
"since", o.Since,
"before", o.Before,
)
}
// GoString returns a safe Go-syntax summary of the repository comment filters.
func (o RepoCommentListOptions) GoString() string { return o.String() }
func (o RepoCommentListOptions) queryParams() map[string]string {
query := make(map[string]string, 2)
if o.Since != nil {
query["since"] = o.Since.Format(time.RFC3339)
}
if o.Before != nil {
query["before"] = o.Before.Format(time.RFC3339)
}
if len(query) == 0 {
return nil
}
return query
}
func newIssueService(c *Client) *IssueService { func newIssueService(c *Client) *IssueService {
return &IssueService{ return &IssueService{
Resource: *NewResource[types.Issue, types.CreateIssueOption, types.EditIssueOption]( Resource: *NewResource[types.Issue, types.CreateIssueOption, types.EditIssueOption](
@ -160,285 +21,79 @@ func newIssueService(c *Client) *IssueService {
} }
} }
// SearchIssuesOptions controls filtering for the global issue search endpoint.
//
// Usage:
//
// opts := forge.SearchIssuesOptions{State: "open"}
type SearchIssuesOptions struct {
State string
Labels string
Milestones string
Query string
PriorityRepoID int64
Type string
Since *time.Time
Before *time.Time
Assigned bool
Created bool
Mentioned bool
ReviewRequested bool
Reviewed bool
Owner string
Team string
}
// String returns a safe summary of the issue search filters.
func (o SearchIssuesOptions) String() string {
return optionString("forge.SearchIssuesOptions",
"state", o.State,
"labels", o.Labels,
"milestones", o.Milestones,
"q", o.Query,
"priority_repo_id", o.PriorityRepoID,
"type", o.Type,
"since", o.Since,
"before", o.Before,
"assigned", o.Assigned,
"created", o.Created,
"mentioned", o.Mentioned,
"review_requested", o.ReviewRequested,
"reviewed", o.Reviewed,
"owner", o.Owner,
"team", o.Team,
)
}
// GoString returns a safe Go-syntax summary of the issue search filters.
func (o SearchIssuesOptions) GoString() string { return o.String() }
func (o SearchIssuesOptions) queryParams() map[string]string {
query := make(map[string]string, 12)
if o.State != "" {
query["state"] = o.State
}
if o.Labels != "" {
query["labels"] = o.Labels
}
if o.Milestones != "" {
query["milestones"] = o.Milestones
}
if o.Query != "" {
query["q"] = o.Query
}
if o.PriorityRepoID != 0 {
query["priority_repo_id"] = strconv.FormatInt(o.PriorityRepoID, 10)
}
if o.Type != "" {
query["type"] = o.Type
}
if o.Since != nil {
query["since"] = o.Since.Format(time.RFC3339)
}
if o.Before != nil {
query["before"] = o.Before.Format(time.RFC3339)
}
if o.Assigned {
query["assigned"] = strconv.FormatBool(true)
}
if o.Created {
query["created"] = strconv.FormatBool(true)
}
if o.Mentioned {
query["mentioned"] = strconv.FormatBool(true)
}
if o.ReviewRequested {
query["review_requested"] = strconv.FormatBool(true)
}
if o.Reviewed {
query["reviewed"] = strconv.FormatBool(true)
}
if o.Owner != "" {
query["owner"] = o.Owner
}
if o.Team != "" {
query["team"] = o.Team
}
if len(query) == 0 {
return nil
}
return query
}
// SearchIssuesPage returns a single page of issues matching the search filters.
func (s *IssueService) SearchIssuesPage(ctx context.Context, opts SearchIssuesOptions, pageOpts ListOptions) (*PagedResult[types.Issue], error) {
return ListPage[types.Issue](ctx, s.client, "/api/v1/repos/issues/search", opts.queryParams(), pageOpts)
}
// SearchIssues returns all issues matching the search filters.
func (s *IssueService) SearchIssues(ctx context.Context, opts SearchIssuesOptions) ([]types.Issue, error) {
return ListAll[types.Issue](ctx, s.client, "/api/v1/repos/issues/search", opts.queryParams())
}
// IterSearchIssues returns an iterator over issues matching the search filters.
func (s *IssueService) IterSearchIssues(ctx context.Context, opts SearchIssuesOptions) iter.Seq2[types.Issue, error] {
return ListIter[types.Issue](ctx, s.client, "/api/v1/repos/issues/search", opts.queryParams())
}
// ListIssues returns all issues in a repository.
func (s *IssueService) ListIssues(ctx context.Context, owner, repo string, filters ...IssueListOptions) ([]types.Issue, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues", pathParams("owner", owner, "repo", repo))
return ListAll[types.Issue](ctx, s.client, path, issueListQuery(filters...))
}
// IterIssues returns an iterator over all issues in a repository.
func (s *IssueService) IterIssues(ctx context.Context, owner, repo string, filters ...IssueListOptions) iter.Seq2[types.Issue, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues", pathParams("owner", owner, "repo", repo))
return ListIter[types.Issue](ctx, s.client, path, issueListQuery(filters...))
}
// CreateIssue creates a new issue in a repository.
func (s *IssueService) CreateIssue(ctx context.Context, owner, repo string, opts *types.CreateIssueOption) (*types.Issue, error) {
var out types.Issue
if err := s.client.Post(ctx, ResolvePath("/api/v1/repos/{owner}/{repo}/issues", pathParams("owner", owner, "repo", repo)), opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// Pin pins an issue. // Pin pins an issue.
func (s *IssueService) Pin(ctx context.Context, owner, repo string, index int64) error { func (s *IssueService) Pin(ctx context.Context, owner, repo string, index int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/pin", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", owner, repo, index)
return s.client.Post(ctx, path, nil, nil) return s.client.Post(ctx, path, nil, nil)
} }
// MovePin moves a pinned issue to a new position.
func (s *IssueService) MovePin(ctx context.Context, owner, repo string, index, position int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/pin/{position}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "position", int64String(position)))
return s.client.Patch(ctx, path, nil, nil)
}
// ListPinnedIssues returns all pinned issues in a repository.
func (s *IssueService) ListPinnedIssues(ctx context.Context, owner, repo string) ([]types.Issue, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/pinned", pathParams("owner", owner, "repo", repo))
return ListAll[types.Issue](ctx, s.client, path, nil)
}
// IterPinnedIssues returns an iterator over all pinned issues in a repository.
func (s *IssueService) IterPinnedIssues(ctx context.Context, owner, repo string) iter.Seq2[types.Issue, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/pinned", pathParams("owner", owner, "repo", repo))
return ListIter[types.Issue](ctx, s.client, path, nil)
}
// Unpin unpins an issue. // Unpin unpins an issue.
func (s *IssueService) Unpin(ctx context.Context, owner, repo string, index int64) error { func (s *IssueService) Unpin(ctx context.Context, owner, repo string, index int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/pin", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", owner, repo, index)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// SetDeadline sets or updates the deadline on an issue. // SetDeadline sets or updates the deadline on an issue.
func (s *IssueService) SetDeadline(ctx context.Context, owner, repo string, index int64, deadline string) error { func (s *IssueService) SetDeadline(ctx context.Context, owner, repo string, index int64, deadline string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/deadline", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/deadline", owner, repo, index)
body := map[string]string{"due_date": deadline} body := map[string]string{"due_date": deadline}
return s.client.Post(ctx, path, body, nil) return s.client.Post(ctx, path, body, nil)
} }
// AddReaction adds a reaction to an issue. // AddReaction adds a reaction to an issue.
func (s *IssueService) AddReaction(ctx context.Context, owner, repo string, index int64, reaction string) error { func (s *IssueService) AddReaction(ctx context.Context, owner, repo string, index int64, reaction string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/reactions", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", owner, repo, index)
body := types.EditReactionOption{Reaction: reaction} body := map[string]string{"content": reaction}
return s.client.Post(ctx, path, body, nil) return s.client.Post(ctx, path, body, nil)
} }
// ListReactions returns all reactions on an issue.
func (s *IssueService) ListReactions(ctx context.Context, owner, repo string, index int64) ([]types.Reaction, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/reactions", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListAll[types.Reaction](ctx, s.client, path, nil)
}
// IterReactions returns an iterator over all reactions on an issue.
func (s *IssueService) IterReactions(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Reaction, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/reactions", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListIter[types.Reaction](ctx, s.client, path, nil)
}
// DeleteReaction removes a reaction from an issue. // DeleteReaction removes a reaction from an issue.
func (s *IssueService) DeleteReaction(ctx context.Context, owner, repo string, index int64, reaction string) error { func (s *IssueService) DeleteReaction(ctx context.Context, owner, repo string, index int64, reaction string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/reactions", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", owner, repo, index)
body := types.EditReactionOption{Reaction: reaction} body := map[string]string{"content": reaction}
return s.client.DeleteWithBody(ctx, path, body) return s.client.DeleteWithBody(ctx, path, body)
} }
// StartStopwatch starts the stopwatch on an issue. // StartStopwatch starts the stopwatch on an issue.
func (s *IssueService) StartStopwatch(ctx context.Context, owner, repo string, index int64) error { func (s *IssueService) StartStopwatch(ctx context.Context, owner, repo string, index int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/stopwatch/start", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/stopwatch/start", owner, repo, index)
return s.client.Post(ctx, path, nil, nil) return s.client.Post(ctx, path, nil, nil)
} }
// StopStopwatch stops the stopwatch on an issue. // StopStopwatch stops the stopwatch on an issue.
func (s *IssueService) StopStopwatch(ctx context.Context, owner, repo string, index int64) error { func (s *IssueService) StopStopwatch(ctx context.Context, owner, repo string, index int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/stopwatch/stop", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/stopwatch/stop", owner, repo, index)
return s.client.Post(ctx, path, nil, nil) return s.client.Post(ctx, path, nil, nil)
} }
// DeleteStopwatch deletes an issue's existing stopwatch.
func (s *IssueService) DeleteStopwatch(ctx context.Context, owner, repo string, index int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/stopwatch/delete", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return s.client.Delete(ctx, path)
}
// ListTimes returns all tracked times on an issue.
func (s *IssueService) ListTimes(ctx context.Context, owner, repo string, index int64, user string, since, before *time.Time) ([]types.TrackedTime, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/times", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListAll[types.TrackedTime](ctx, s.client, path, issueTimeQuery(user, since, before))
}
// IterTimes returns an iterator over all tracked times on an issue.
func (s *IssueService) IterTimes(ctx context.Context, owner, repo string, index int64, user string, since, before *time.Time) iter.Seq2[types.TrackedTime, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/times", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListIter[types.TrackedTime](ctx, s.client, path, issueTimeQuery(user, since, before))
}
// AddTime adds tracked time to an issue.
func (s *IssueService) AddTime(ctx context.Context, owner, repo string, index int64, opts *types.AddTimeOption) (*types.TrackedTime, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/times", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
var out types.TrackedTime
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// ResetTime removes all tracked time from an issue.
func (s *IssueService) ResetTime(ctx context.Context, owner, repo string, index int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/times", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return s.client.Delete(ctx, path)
}
// DeleteTime removes a specific tracked time entry from an issue.
func (s *IssueService) DeleteTime(ctx context.Context, owner, repo string, index, timeID int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/times/{id}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(timeID)))
return s.client.Delete(ctx, path)
}
// AddLabels adds labels to an issue. // AddLabels adds labels to an issue.
func (s *IssueService) AddLabels(ctx context.Context, owner, repo string, index int64, labelIDs []int64) error { func (s *IssueService) AddLabels(ctx context.Context, owner, repo string, index int64, labelIDs []int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/labels", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner, repo, index)
body := types.IssueLabelsOption{Labels: toAnySlice(labelIDs)} body := types.IssueLabelsOption{Labels: toAnySlice(labelIDs)}
return s.client.Post(ctx, path, body, nil) return s.client.Post(ctx, path, body, nil)
} }
// RemoveLabel removes a single label from an issue. // RemoveLabel removes a single label from an issue.
func (s *IssueService) RemoveLabel(ctx context.Context, owner, repo string, index int64, labelID int64) error { func (s *IssueService) RemoveLabel(ctx context.Context, owner, repo string, index int64, labelID int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/labels/{id}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(labelID))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels/%d", owner, repo, index, labelID)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// ListComments returns all comments on an issue. // ListComments returns all comments on an issue.
func (s *IssueService) ListComments(ctx context.Context, owner, repo string, index int64) ([]types.Comment, error) { func (s *IssueService) ListComments(ctx context.Context, owner, repo string, index int64) ([]types.Comment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
return ListAll[types.Comment](ctx, s.client, path, nil) return ListAll[types.Comment](ctx, s.client, path, nil)
} }
// IterComments returns an iterator over all comments on an issue. // IterComments returns an iterator over all comments on an issue.
func (s *IssueService) IterComments(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Comment, error] { func (s *IssueService) IterComments(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Comment, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
return ListIter[types.Comment](ctx, s.client, path, nil) return ListIter[types.Comment](ctx, s.client, path, nil)
} }
// CreateComment creates a comment on an issue. // CreateComment creates a comment on an issue.
func (s *IssueService) CreateComment(ctx context.Context, owner, repo string, index int64, body string) (*types.Comment, error) { func (s *IssueService) CreateComment(ctx context.Context, owner, repo string, index int64, body string) (*types.Comment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
opts := types.CreateIssueCommentOption{Body: body} opts := types.CreateIssueCommentOption{Body: body}
var out types.Comment var out types.Comment
if err := s.client.Post(ctx, path, opts, &out); err != nil { if err := s.client.Post(ctx, path, opts, &out); err != nil {
@ -447,328 +102,6 @@ func (s *IssueService) CreateComment(ctx context.Context, owner, repo string, in
return &out, nil return &out, nil
} }
// EditComment updates an issue comment.
func (s *IssueService) EditComment(ctx context.Context, owner, repo string, index, id int64, opts *types.EditIssueCommentOption) (*types.Comment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/comments/{id}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(id)))
var out types.Comment
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteComment deletes an issue comment.
func (s *IssueService) DeleteComment(ctx context.Context, owner, repo string, index, id int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/comments/{id}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(id)))
return s.client.Delete(ctx, path)
}
// ListRepoComments returns all comments in a repository.
func (s *IssueService) ListRepoComments(ctx context.Context, owner, repo string, filters ...RepoCommentListOptions) ([]types.Comment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments", pathParams("owner", owner, "repo", repo))
return ListAll[types.Comment](ctx, s.client, path, repoCommentQuery(filters...))
}
// IterRepoComments returns an iterator over all comments in a repository.
func (s *IssueService) IterRepoComments(ctx context.Context, owner, repo string, filters ...RepoCommentListOptions) iter.Seq2[types.Comment, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments", pathParams("owner", owner, "repo", repo))
return ListIter[types.Comment](ctx, s.client, path, repoCommentQuery(filters...))
}
// GetRepoComment returns a single comment in a repository.
func (s *IssueService) GetRepoComment(ctx context.Context, owner, repo string, id int64) (*types.Comment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
var out types.Comment
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// EditRepoComment updates a repository comment.
func (s *IssueService) EditRepoComment(ctx context.Context, owner, repo string, id int64, opts *types.EditIssueCommentOption) (*types.Comment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
var out types.Comment
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteRepoComment deletes a repository comment.
func (s *IssueService) DeleteRepoComment(ctx context.Context, owner, repo string, id int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
return s.client.Delete(ctx, path)
}
// ListCommentReactions returns all reactions on an issue comment.
func (s *IssueService) ListCommentReactions(ctx context.Context, owner, repo string, id int64) ([]types.Reaction, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/reactions", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
return ListAll[types.Reaction](ctx, s.client, path, nil)
}
// IterCommentReactions returns an iterator over all reactions on an issue comment.
func (s *IssueService) IterCommentReactions(ctx context.Context, owner, repo string, id int64) iter.Seq2[types.Reaction, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/reactions", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
return ListIter[types.Reaction](ctx, s.client, path, nil)
}
// AddCommentReaction adds a reaction to an issue comment.
func (s *IssueService) AddCommentReaction(ctx context.Context, owner, repo string, id int64, reaction string) (*types.Reaction, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/reactions", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
var out types.Reaction
if err := s.client.Post(ctx, path, types.EditReactionOption{Reaction: reaction}, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteCommentReaction removes a reaction from an issue comment.
func (s *IssueService) DeleteCommentReaction(ctx context.Context, owner, repo string, id int64, reaction string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/reactions", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
return s.client.DeleteWithBody(ctx, path, types.EditReactionOption{Reaction: reaction})
}
func issueListQuery(filters ...IssueListOptions) map[string]string {
query := make(map[string]string, len(filters))
for _, filter := range filters {
for key, value := range filter.queryParams() {
query[key] = value
}
}
if len(query) == 0 {
return nil
}
return query
}
func attachmentUploadQuery(opts *AttachmentUploadOptions) map[string]string {
if opts == nil {
return nil
}
query := make(map[string]string, 2)
if opts.Name != "" {
query["name"] = opts.Name
}
if opts.UpdatedAt != nil {
query["updated_at"] = opts.UpdatedAt.Format(time.RFC3339)
}
if len(query) == 0 {
return nil
}
return query
}
func (s *IssueService) createAttachment(ctx context.Context, path string, opts *AttachmentUploadOptions, filename string, content goio.Reader) (*types.Attachment, error) {
var out types.Attachment
if err := s.client.postMultipartJSON(ctx, path, attachmentUploadQuery(opts), nil, "attachment", filename, content, &out); err != nil {
return nil, err
}
return &out, nil
}
// CreateAttachment uploads a new attachment to an issue.
func (s *IssueService) CreateAttachment(ctx context.Context, owner, repo string, index int64, opts *AttachmentUploadOptions, filename string, content goio.Reader) (*types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/assets", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return s.createAttachment(ctx, path, opts, filename, content)
}
// ListAttachments returns all attachments on an issue.
func (s *IssueService) ListAttachments(ctx context.Context, owner, repo string, index int64) ([]types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/assets", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListAll[types.Attachment](ctx, s.client, path, nil)
}
// IterAttachments returns an iterator over all attachments on an issue.
func (s *IssueService) IterAttachments(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Attachment, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/assets", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListIter[types.Attachment](ctx, s.client, path, nil)
}
// GetAttachment returns a single attachment on an issue.
func (s *IssueService) GetAttachment(ctx context.Context, owner, repo string, index, attachmentID int64) (*types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "attachment_id", int64String(attachmentID)))
var out types.Attachment
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// EditAttachment updates an issue attachment.
func (s *IssueService) EditAttachment(ctx context.Context, owner, repo string, index, attachmentID int64, opts *types.EditAttachmentOptions) (*types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "attachment_id", int64String(attachmentID)))
var out types.Attachment
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteAttachment removes an issue attachment.
func (s *IssueService) DeleteAttachment(ctx context.Context, owner, repo string, index, attachmentID int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/assets/{attachment_id}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "attachment_id", int64String(attachmentID)))
return s.client.Delete(ctx, path)
}
// ListCommentAttachments returns all attachments on an issue comment.
func (s *IssueService) ListCommentAttachments(ctx context.Context, owner, repo string, id int64) ([]types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/assets", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
return ListAll[types.Attachment](ctx, s.client, path, nil)
}
// IterCommentAttachments returns an iterator over all attachments on an issue comment.
func (s *IssueService) IterCommentAttachments(ctx context.Context, owner, repo string, id int64) iter.Seq2[types.Attachment, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/assets", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
return ListIter[types.Attachment](ctx, s.client, path, nil)
}
// GetCommentAttachment returns a single attachment on an issue comment.
func (s *IssueService) GetCommentAttachment(ctx context.Context, owner, repo string, id, attachmentID int64) (*types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id}", pathParams("owner", owner, "repo", repo, "id", int64String(id), "attachment_id", int64String(attachmentID)))
var out types.Attachment
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// CreateCommentAttachment uploads a new attachment to an issue comment.
func (s *IssueService) CreateCommentAttachment(ctx context.Context, owner, repo string, id int64, opts *AttachmentUploadOptions, filename string, content goio.Reader) (*types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/assets", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
return s.createAttachment(ctx, path, opts, filename, content)
}
// EditCommentAttachment updates an issue comment attachment.
func (s *IssueService) EditCommentAttachment(ctx context.Context, owner, repo string, id, attachmentID int64, opts *types.EditAttachmentOptions) (*types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id}", pathParams("owner", owner, "repo", repo, "id", int64String(id), "attachment_id", int64String(attachmentID)))
var out types.Attachment
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteCommentAttachment removes an issue comment attachment.
func (s *IssueService) DeleteCommentAttachment(ctx context.Context, owner, repo string, id, attachmentID int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id}", pathParams("owner", owner, "repo", repo, "id", int64String(id), "attachment_id", int64String(attachmentID)))
return s.client.Delete(ctx, path)
}
// ListTimeline returns all comments and events on an issue.
func (s *IssueService) ListTimeline(ctx context.Context, owner, repo string, index int64, since, before *time.Time) ([]types.TimelineComment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/timeline", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
query := make(map[string]string, 2)
if since != nil {
query["since"] = since.Format(time.RFC3339)
}
if before != nil {
query["before"] = before.Format(time.RFC3339)
}
if len(query) == 0 {
query = nil
}
return ListAll[types.TimelineComment](ctx, s.client, path, query)
}
// IterTimeline returns an iterator over all comments and events on an issue.
func (s *IssueService) IterTimeline(ctx context.Context, owner, repo string, index int64, since, before *time.Time) iter.Seq2[types.TimelineComment, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/timeline", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
query := make(map[string]string, 2)
if since != nil {
query["since"] = since.Format(time.RFC3339)
}
if before != nil {
query["before"] = before.Format(time.RFC3339)
}
if len(query) == 0 {
query = nil
}
return ListIter[types.TimelineComment](ctx, s.client, path, query)
}
// ListSubscriptions returns all users subscribed to an issue.
func (s *IssueService) ListSubscriptions(ctx context.Context, owner, repo string, index int64) ([]types.User, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/subscriptions", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListAll[types.User](ctx, s.client, path, nil)
}
// IterSubscriptions returns an iterator over all users subscribed to an issue.
func (s *IssueService) IterSubscriptions(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.User, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/subscriptions", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListIter[types.User](ctx, s.client, path, nil)
}
// CheckSubscription returns the authenticated user's subscription state for an issue.
func (s *IssueService) CheckSubscription(ctx context.Context, owner, repo string, index int64) (*types.WatchInfo, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/subscriptions/check", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
var out types.WatchInfo
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// SubscribeUser subscribes a user to an issue.
func (s *IssueService) SubscribeUser(ctx context.Context, owner, repo string, index int64, user string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/subscriptions/{user}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "user", user))
return s.client.Put(ctx, path, nil, nil)
}
// UnsubscribeUser unsubscribes a user from an issue.
func (s *IssueService) UnsubscribeUser(ctx context.Context, owner, repo string, index int64, user string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/subscriptions/{user}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "user", user))
return s.client.Delete(ctx, path)
}
// ListDependencies returns all issues that block the given issue.
func (s *IssueService) ListDependencies(ctx context.Context, owner, repo string, index int64) ([]types.Issue, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/dependencies", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListAll[types.Issue](ctx, s.client, path, nil)
}
// IterDependencies returns an iterator over all issues that block the given issue.
func (s *IssueService) IterDependencies(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Issue, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/dependencies", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListIter[types.Issue](ctx, s.client, path, nil)
}
// AddDependency makes another issue block the issue at the given path.
func (s *IssueService) AddDependency(ctx context.Context, owner, repo string, index int64, dependency types.IssueMeta) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/dependencies", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return s.client.Post(ctx, path, dependency, nil)
}
// RemoveDependency removes an issue dependency from the issue at the given path.
func (s *IssueService) RemoveDependency(ctx context.Context, owner, repo string, index int64, dependency types.IssueMeta) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/dependencies", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return s.client.DeleteWithBody(ctx, path, dependency)
}
// ListBlocks returns all issues blocked by the given issue.
func (s *IssueService) ListBlocks(ctx context.Context, owner, repo string, index int64) ([]types.Issue, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/blocks", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListAll[types.Issue](ctx, s.client, path, nil)
}
// IterBlocks returns an iterator over all issues blocked by the given issue.
func (s *IssueService) IterBlocks(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Issue, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/blocks", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListIter[types.Issue](ctx, s.client, path, nil)
}
// AddBlock makes the issue at the given path block another issue.
func (s *IssueService) AddBlock(ctx context.Context, owner, repo string, index int64, blockedIssue types.IssueMeta) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/blocks", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return s.client.Post(ctx, path, blockedIssue, nil)
}
// RemoveBlock removes an issue block from the issue at the given path.
func (s *IssueService) RemoveBlock(ctx context.Context, owner, repo string, index int64, blockedIssue types.IssueMeta) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}/blocks", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return s.client.DeleteWithBody(ctx, path, blockedIssue)
}
// toAnySlice converts a slice of int64 to a slice of any for IssueLabelsOption. // toAnySlice converts a slice of int64 to a slice of any for IssueLabelsOption.
func toAnySlice(ids []int64) []any { func toAnySlice(ids []int64) []any {
out := make([]any, len(ids)) out := make([]any, len(ids))
@ -777,37 +110,3 @@ func toAnySlice(ids []int64) []any {
} }
return out return out
} }
func repoCommentQuery(filters ...RepoCommentListOptions) map[string]string {
if len(filters) == 0 {
return nil
}
query := make(map[string]string, 2)
for _, filter := range filters {
for key, value := range filter.queryParams() {
query[key] = value
}
}
if len(query) == 0 {
return nil
}
return query
}
func issueTimeQuery(user string, since, before *time.Time) map[string]string {
query := make(map[string]string, 3)
if user != "" {
query["user"] = user
}
if since != nil {
query["since"] = since.Format(time.RFC3339)
}
if before != nil {
query["before"] = before.Format(time.RFC3339)
}
if len(query) == 0 {
return nil
}
return query
}

View file

@ -1,63 +0,0 @@
package forge
import (
"context"
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"testing"
"dappco.re/go/core/forge/types"
)
func TestIssueService_ListIssues_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/issues" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Issue{{ID: 1, Title: "bug"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
issues, err := f.Issues.ListIssues(context.Background(), "core", "go-forge")
if err != nil {
t.Fatal(err)
}
if len(issues) != 1 || issues[0].Title != "bug" {
t.Fatalf("got %#v", issues)
}
}
func TestIssueService_CreateIssue_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/issues" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var body types.CreateIssueOption
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.Title != "new issue" {
t.Fatalf("unexpected body: %+v", body)
}
json.NewEncoder(w).Encode(types.Issue{ID: 1, Index: 1, Title: body.Title})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
issue, err := f.Issues.CreateIssue(context.Background(), "core", "go-forge", &types.CreateIssueOption{Title: "new issue"})
if err != nil {
t.Fatal(err)
}
if issue.Title != "new issue" {
t.Fatalf("got title=%q", issue.Title)
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
@ -9,11 +10,6 @@ import (
// LabelService handles repository labels, organisation labels, and issue labels. // LabelService handles repository labels, organisation labels, and issue labels.
// No Resource embedding — paths are heterogeneous. // No Resource embedding — paths are heterogeneous.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Labels.ListRepoLabels(ctx, "core", "go-forge")
type LabelService struct { type LabelService struct {
client *Client client *Client
} }
@ -24,19 +20,19 @@ func newLabelService(c *Client) *LabelService {
// ListRepoLabels returns all labels for a repository. // ListRepoLabels returns all labels for a repository.
func (s *LabelService) ListRepoLabels(ctx context.Context, owner, repo string) ([]types.Label, error) { func (s *LabelService) ListRepoLabels(ctx context.Context, owner, repo string) ([]types.Label, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
return ListAll[types.Label](ctx, s.client, path, nil) return ListAll[types.Label](ctx, s.client, path, nil)
} }
// IterRepoLabels returns an iterator over all labels for a repository. // IterRepoLabels returns an iterator over all labels for a repository.
func (s *LabelService) IterRepoLabels(ctx context.Context, owner, repo string) iter.Seq2[types.Label, error] { func (s *LabelService) IterRepoLabels(ctx context.Context, owner, repo string) iter.Seq2[types.Label, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
return ListIter[types.Label](ctx, s.client, path, nil) return ListIter[types.Label](ctx, s.client, path, nil)
} }
// GetRepoLabel returns a single label by ID. // GetRepoLabel returns a single label by ID.
func (s *LabelService) GetRepoLabel(ctx context.Context, owner, repo string, id int64) (*types.Label, error) { func (s *LabelService) GetRepoLabel(ctx context.Context, owner, repo string, id int64) (*types.Label, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id))) path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
var out types.Label var out types.Label
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -46,7 +42,7 @@ func (s *LabelService) GetRepoLabel(ctx context.Context, owner, repo string, id
// CreateRepoLabel creates a new label in a repository. // CreateRepoLabel creates a new label in a repository.
func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string, opts *types.CreateLabelOption) (*types.Label, error) { func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string, opts *types.CreateLabelOption) (*types.Label, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
var out types.Label var out types.Label
if err := s.client.Post(ctx, path, opts, &out); err != nil { if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err return nil, err
@ -56,7 +52,7 @@ func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string,
// EditRepoLabel updates an existing label in a repository. // EditRepoLabel updates an existing label in a repository.
func (s *LabelService) EditRepoLabel(ctx context.Context, owner, repo string, id int64, opts *types.EditLabelOption) (*types.Label, error) { func (s *LabelService) EditRepoLabel(ctx context.Context, owner, repo string, id int64, opts *types.EditLabelOption) (*types.Label, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id))) path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
var out types.Label var out types.Label
if err := s.client.Patch(ctx, path, opts, &out); err != nil { if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err return nil, err
@ -66,89 +62,28 @@ func (s *LabelService) EditRepoLabel(ctx context.Context, owner, repo string, id
// DeleteRepoLabel deletes a label from a repository. // DeleteRepoLabel deletes a label from a repository.
func (s *LabelService) DeleteRepoLabel(ctx context.Context, owner, repo string, id int64) error { func (s *LabelService) DeleteRepoLabel(ctx context.Context, owner, repo string, id int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/labels/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id))) path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// ListOrgLabels returns all labels for an organisation. // ListOrgLabels returns all labels for an organisation.
func (s *LabelService) ListOrgLabels(ctx context.Context, org string) ([]types.Label, error) { func (s *LabelService) ListOrgLabels(ctx context.Context, org string) ([]types.Label, error) {
path := ResolvePath("/api/v1/orgs/{org}/labels", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
return ListAll[types.Label](ctx, s.client, path, nil) return ListAll[types.Label](ctx, s.client, path, nil)
} }
// IterOrgLabels returns an iterator over all labels for an organisation. // IterOrgLabels returns an iterator over all labels for an organisation.
func (s *LabelService) IterOrgLabels(ctx context.Context, org string) iter.Seq2[types.Label, error] { func (s *LabelService) IterOrgLabels(ctx context.Context, org string) iter.Seq2[types.Label, error] {
path := ResolvePath("/api/v1/orgs/{org}/labels", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
return ListIter[types.Label](ctx, s.client, path, nil) return ListIter[types.Label](ctx, s.client, path, nil)
} }
// CreateOrgLabel creates a new label in an organisation. // CreateOrgLabel creates a new label in an organisation.
func (s *LabelService) CreateOrgLabel(ctx context.Context, org string, opts *types.CreateLabelOption) (*types.Label, error) { func (s *LabelService) CreateOrgLabel(ctx context.Context, org string, opts *types.CreateLabelOption) (*types.Label, error) {
path := ResolvePath("/api/v1/orgs/{org}/labels", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
var out types.Label var out types.Label
if err := s.client.Post(ctx, path, opts, &out); err != nil { if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err return nil, err
} }
return &out, nil return &out, nil
} }
// GetOrgLabel returns a single label for an organisation.
func (s *LabelService) GetOrgLabel(ctx context.Context, org string, id int64) (*types.Label, error) {
path := ResolvePath("/api/v1/orgs/{org}/labels/{id}", pathParams("org", org, "id", int64String(id)))
var out types.Label
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// EditOrgLabel updates an existing label in an organisation.
func (s *LabelService) EditOrgLabel(ctx context.Context, org string, id int64, opts *types.EditLabelOption) (*types.Label, error) {
path := ResolvePath("/api/v1/orgs/{org}/labels/{id}", pathParams("org", org, "id", int64String(id)))
var out types.Label
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteOrgLabel deletes a label from an organisation.
func (s *LabelService) DeleteOrgLabel(ctx context.Context, org string, id int64) error {
path := ResolvePath("/api/v1/orgs/{org}/labels/{id}", pathParams("org", org, "id", int64String(id)))
return s.client.Delete(ctx, path)
}
// ListLabelTemplates returns all available label template names.
func (s *LabelService) ListLabelTemplates(ctx context.Context) ([]string, error) {
var out []string
if err := s.client.Get(ctx, "/api/v1/label/templates", &out); err != nil {
return nil, err
}
return out, nil
}
// IterLabelTemplates returns an iterator over all available label template names.
func (s *LabelService) IterLabelTemplates(ctx context.Context) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
items, err := s.ListLabelTemplates(ctx)
if err != nil {
yield("", err)
return
}
for _, item := range items {
if !yield(item, nil) {
return
}
}
}
}
// GetLabelTemplate returns all labels for a label template.
func (s *LabelService) GetLabelTemplate(ctx context.Context, name string) ([]types.LabelTemplate, error) {
path := ResolvePath("/api/v1/label/templates/{name}", pathParams("name", name))
var out []types.LabelTemplate
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return out, nil
}

View file

@ -2,7 +2,7 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -10,7 +10,7 @@ import (
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestLabelService_ListRepoLabels_Good(t *testing.T) { func TestLabelService_Good_ListRepoLabels(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -42,7 +42,7 @@ func TestLabelService_ListRepoLabels_Good(t *testing.T) {
} }
} }
func TestLabelService_CreateRepoLabel_Good(t *testing.T) { func TestLabelService_Good_CreateRepoLabel(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -84,7 +84,7 @@ func TestLabelService_CreateRepoLabel_Good(t *testing.T) {
} }
} }
func TestLabelService_GetRepoLabel_Good(t *testing.T) { func TestLabelService_Good_GetRepoLabel(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -106,7 +106,7 @@ func TestLabelService_GetRepoLabel_Good(t *testing.T) {
} }
} }
func TestLabelService_EditRepoLabel_Good(t *testing.T) { func TestLabelService_Good_EditRepoLabel(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch { if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method) t.Errorf("expected PATCH, got %s", r.Method)
@ -135,7 +135,7 @@ func TestLabelService_EditRepoLabel_Good(t *testing.T) {
} }
} }
func TestLabelService_DeleteRepoLabel_Good(t *testing.T) { func TestLabelService_Good_DeleteRepoLabel(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method) t.Errorf("expected DELETE, got %s", r.Method)
@ -154,7 +154,7 @@ func TestLabelService_DeleteRepoLabel_Good(t *testing.T) {
} }
} }
func TestLabelService_ListOrgLabels_Good(t *testing.T) { func TestLabelService_Good_ListOrgLabels(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -182,7 +182,7 @@ func TestLabelService_ListOrgLabels_Good(t *testing.T) {
} }
} }
func TestLabelService_CreateOrgLabel_Good(t *testing.T) { func TestLabelService_Good_CreateOrgLabel(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -214,7 +214,7 @@ func TestLabelService_CreateOrgLabel_Good(t *testing.T) {
} }
} }
func TestLabelService_NotFound_Bad(t *testing.T) { func TestLabelService_Bad_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "label not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "label not found"})

View file

@ -2,49 +2,12 @@ package forge
import ( import (
"context" "context"
"iter" "fmt"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// MilestoneListOptions controls filtering for repository milestone listings.
//
// Usage:
//
// opts := forge.MilestoneListOptions{State: "open"}
type MilestoneListOptions struct {
State string
Name string
}
// String returns a safe summary of the milestone filters.
func (o MilestoneListOptions) String() string {
return optionString("forge.MilestoneListOptions", "state", o.State, "name", o.Name)
}
// GoString returns a safe Go-syntax summary of the milestone filters.
func (o MilestoneListOptions) GoString() string { return o.String() }
func (o MilestoneListOptions) queryParams() map[string]string {
query := make(map[string]string, 2)
if o.State != "" {
query["state"] = o.State
}
if o.Name != "" {
query["name"] = o.Name
}
if len(query) == 0 {
return nil
}
return query
}
// MilestoneService handles repository milestones. // MilestoneService handles repository milestones.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Milestones.ListAll(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
type MilestoneService struct { type MilestoneService struct {
client *Client client *Client
} }
@ -53,27 +16,15 @@ func newMilestoneService(c *Client) *MilestoneService {
return &MilestoneService{client: c} return &MilestoneService{client: c}
} }
// List returns a single page of milestones for a repository.
func (s *MilestoneService) List(ctx context.Context, params Params, opts ListOptions, filters ...MilestoneListOptions) (*PagedResult[types.Milestone], error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones", params)
return ListPage[types.Milestone](ctx, s.client, path, milestoneQuery(filters...), opts)
}
// Iter returns an iterator over all milestones for a repository.
func (s *MilestoneService) Iter(ctx context.Context, params Params, filters ...MilestoneListOptions) iter.Seq2[types.Milestone, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones", params)
return ListIter[types.Milestone](ctx, s.client, path, milestoneQuery(filters...))
}
// ListAll returns all milestones for a repository. // ListAll returns all milestones for a repository.
func (s *MilestoneService) ListAll(ctx context.Context, params Params, filters ...MilestoneListOptions) ([]types.Milestone, error) { func (s *MilestoneService) ListAll(ctx context.Context, params Params) ([]types.Milestone, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones", params) path := fmt.Sprintf("/api/v1/repos/%s/%s/milestones", params["owner"], params["repo"])
return ListAll[types.Milestone](ctx, s.client, path, milestoneQuery(filters...)) return ListAll[types.Milestone](ctx, s.client, path, nil)
} }
// Get returns a single milestone by ID. // Get returns a single milestone by ID.
func (s *MilestoneService) Get(ctx context.Context, owner, repo string, id int64) (*types.Milestone, error) { func (s *MilestoneService) Get(ctx context.Context, owner, repo string, id int64) (*types.Milestone, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id))) path := fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%d", owner, repo, id)
var out types.Milestone var out types.Milestone
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -83,43 +34,10 @@ func (s *MilestoneService) Get(ctx context.Context, owner, repo string, id int64
// Create creates a new milestone. // Create creates a new milestone.
func (s *MilestoneService) Create(ctx context.Context, owner, repo string, opts *types.CreateMilestoneOption) (*types.Milestone, error) { func (s *MilestoneService) Create(ctx context.Context, owner, repo string, opts *types.CreateMilestoneOption) (*types.Milestone, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/milestones", owner, repo)
var out types.Milestone var out types.Milestone
if err := s.client.Post(ctx, path, opts, &out); err != nil { if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err return nil, err
} }
return &out, nil return &out, nil
} }
// Edit updates an existing milestone.
func (s *MilestoneService) Edit(ctx context.Context, owner, repo string, id int64, opts *types.EditMilestoneOption) (*types.Milestone, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
var out types.Milestone
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// Delete removes a milestone.
func (s *MilestoneService) Delete(ctx context.Context, owner, repo string, id int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/milestones/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id)))
return s.client.Delete(ctx, path)
}
func milestoneQuery(filters ...MilestoneListOptions) map[string]string {
if len(filters) == 0 {
return nil
}
query := make(map[string]string, 2)
for _, filter := range filters {
for key, value := range filter.queryParams() {
query[key] = value
}
}
if len(query) == 0 {
return nil
}
return query
}

View file

@ -1,273 +0,0 @@
package forge
import (
"context"
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"dappco.re/go/core/forge/types"
)
func TestMilestoneService_List_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/milestones" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("page"); got != "1" {
t.Errorf("got page=%q, want %q", got, "1")
}
if got := r.URL.Query().Get("limit"); got != "1" {
t.Errorf("got limit=%q, want %q", got, "1")
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.Milestone{{ID: 2, Title: "v2.0"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
page, err := f.Milestones.List(context.Background(), Params{"owner": "core", "repo": "go-forge"}, ListOptions{Page: 1, Limit: 1})
if err != nil {
t.Fatal(err)
}
if page.Page != 1 {
t.Errorf("got page=%d, want 1", page.Page)
}
if page.TotalCount != 2 {
t.Errorf("got total=%d, want 2", page.TotalCount)
}
if !page.HasMore {
t.Error("expected HasMore=true")
}
if len(page.Items) != 1 || page.Items[0].Title != "v2.0" {
t.Fatalf("unexpected items: %+v", page.Items)
}
}
func TestMilestoneService_ListWithFilters_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/milestones" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("state"); got != "all" {
t.Errorf("got state=%q, want %q", got, "all")
}
if got := r.URL.Query().Get("name"); got != "v1.0" {
t.Errorf("got name=%q, want %q", got, "v1.0")
}
if got := r.URL.Query().Get("page"); got != "1" {
t.Errorf("got page=%q, want %q", got, "1")
}
if got := r.URL.Query().Get("limit"); got != "1" {
t.Errorf("got limit=%q, want %q", got, "1")
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Milestone{{ID: 1, Title: "v1.0"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
page, err := f.Milestones.List(
context.Background(),
Params{"owner": "core", "repo": "go-forge"},
ListOptions{Page: 1, Limit: 1},
MilestoneListOptions{State: "all", Name: "v1.0"},
)
if err != nil {
t.Fatal(err)
}
if len(page.Items) != 1 || page.Items[0].Title != "v1.0" {
t.Fatalf("unexpected items: %+v", page.Items)
}
}
func TestMilestoneService_Iter_Good(t *testing.T) {
requests := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/milestones" {
t.Errorf("wrong path: %s", r.URL.Path)
}
switch requests {
case 1:
if got := r.URL.Query().Get("page"); got != "1" {
t.Errorf("got page=%q, want %q", got, "1")
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.Milestone{{ID: 1, Title: "v1.0"}})
case 2:
if got := r.URL.Query().Get("page"); got != "2" {
t.Errorf("got page=%q, want %q", got, "2")
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.Milestone{{ID: 2, Title: "v2.0"}})
default:
t.Fatalf("unexpected request %d", requests)
}
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var got []string
for milestone, err := range f.Milestones.Iter(context.Background(), Params{"owner": "core", "repo": "go-forge"}) {
if err != nil {
t.Fatal(err)
}
got = append(got, milestone.Title)
}
if !reflect.DeepEqual(got, []string{"v1.0", "v2.0"}) {
t.Fatalf("got %v", got)
}
}
func TestMilestoneService_ListAll_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/milestones" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode([]types.Milestone{
{ID: 1, Title: "v1.0"},
{ID: 2, Title: "v2.0"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
milestones, err := f.Milestones.ListAll(context.Background(), Params{"owner": "core", "repo": "go-forge"})
if err != nil {
t.Fatal(err)
}
if len(milestones) != 2 {
t.Errorf("got %d milestones, want 2", len(milestones))
}
if milestones[0].Title != "v1.0" {
t.Errorf("got title=%q, want %q", milestones[0].Title, "v1.0")
}
}
func TestMilestoneService_Get_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/milestones/7" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.Milestone{ID: 7, Title: "v1.0"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
milestone, err := f.Milestones.Get(context.Background(), "core", "go-forge", 7)
if err != nil {
t.Fatal(err)
}
if milestone.ID != 7 {
t.Errorf("got id=%d, want 7", milestone.ID)
}
if milestone.Title != "v1.0" {
t.Errorf("got title=%q, want %q", milestone.Title, "v1.0")
}
}
func TestMilestoneService_Create_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/milestones" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.CreateMilestoneOption
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Title != "v1.0" {
t.Errorf("got title=%q, want %q", opts.Title, "v1.0")
}
json.NewEncoder(w).Encode(types.Milestone{ID: 3, Title: opts.Title})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
milestone, err := f.Milestones.Create(context.Background(), "core", "go-forge", &types.CreateMilestoneOption{
Title: "v1.0",
})
if err != nil {
t.Fatal(err)
}
if milestone.ID != 3 {
t.Errorf("got id=%d, want 3", milestone.ID)
}
if milestone.Title != "v1.0" {
t.Errorf("got title=%q, want %q", milestone.Title, "v1.0")
}
}
func TestMilestoneService_Edit_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/milestones/3" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.EditMilestoneOption
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Title != "v1.1" {
t.Errorf("got title=%q, want %q", opts.Title, "v1.1")
}
json.NewEncoder(w).Encode(types.Milestone{ID: 3, Title: opts.Title})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
milestone, err := f.Milestones.Edit(context.Background(), "core", "go-forge", 3, &types.EditMilestoneOption{
Title: "v1.1",
})
if err != nil {
t.Fatal(err)
}
if milestone.ID != 3 {
t.Errorf("got id=%d, want 3", milestone.ID)
}
if milestone.Title != "v1.1" {
t.Errorf("got title=%q, want %q", milestone.Title, "v1.1")
}
}
func TestMilestoneService_Delete_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/milestones/3" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Milestones.Delete(context.Background(), "core", "go-forge", 3); err != nil {
t.Fatal(err)
}
}

109
misc.go
View file

@ -2,7 +2,7 @@ package forge
import ( import (
"context" "context"
"iter" "fmt"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
@ -11,11 +11,6 @@ import (
// markdown rendering, licence templates, gitignore templates, and // markdown rendering, licence templates, gitignore templates, and
// server metadata. // server metadata.
// No Resource embedding — heterogeneous read-only endpoints. // No Resource embedding — heterogeneous read-only endpoints.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Misc.GetVersion(ctx)
type MiscService struct { type MiscService struct {
client *Client client *Client
} }
@ -35,27 +30,6 @@ func (s *MiscService) RenderMarkdown(ctx context.Context, text, mode string) (st
return string(data), nil return string(data), nil
} }
// RenderMarkup renders markup text to HTML. The response is raw HTML text,
// not JSON.
func (s *MiscService) RenderMarkup(ctx context.Context, text, mode string) (string, error) {
body := types.MarkupOption{Text: text, Mode: mode}
data, err := s.client.PostRaw(ctx, "/api/v1/markup", body)
if err != nil {
return "", err
}
return string(data), nil
}
// RenderMarkdownRaw renders raw markdown text to HTML. The request body is
// sent as text/plain and the response is raw HTML text, not JSON.
func (s *MiscService) RenderMarkdownRaw(ctx context.Context, text string) (string, error) {
data, err := s.client.postRawText(ctx, "/api/v1/markdown/raw", text)
if err != nil {
return "", err
}
return string(data), nil
}
// ListLicenses returns all available licence templates. // ListLicenses returns all available licence templates.
func (s *MiscService) ListLicenses(ctx context.Context) ([]types.LicensesTemplateListEntry, error) { func (s *MiscService) ListLicenses(ctx context.Context) ([]types.LicensesTemplateListEntry, error) {
var out []types.LicensesTemplateListEntry var out []types.LicensesTemplateListEntry
@ -65,25 +39,9 @@ func (s *MiscService) ListLicenses(ctx context.Context) ([]types.LicensesTemplat
return out, nil return out, nil
} }
// IterLicenses returns an iterator over all available licence templates.
func (s *MiscService) IterLicenses(ctx context.Context) iter.Seq2[types.LicensesTemplateListEntry, error] {
return func(yield func(types.LicensesTemplateListEntry, error) bool) {
items, err := s.ListLicenses(ctx)
if err != nil {
yield(*new(types.LicensesTemplateListEntry), err)
return
}
for _, item := range items {
if !yield(item, nil) {
return
}
}
}
}
// GetLicense returns a single licence template by name. // GetLicense returns a single licence template by name.
func (s *MiscService) GetLicense(ctx context.Context, name string) (*types.LicenseTemplateInfo, error) { func (s *MiscService) GetLicense(ctx context.Context, name string) (*types.LicenseTemplateInfo, error) {
path := ResolvePath("/api/v1/licenses/{name}", pathParams("name", name)) path := fmt.Sprintf("/api/v1/licenses/%s", name)
var out types.LicenseTemplateInfo var out types.LicenseTemplateInfo
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -100,25 +58,9 @@ func (s *MiscService) ListGitignoreTemplates(ctx context.Context) ([]string, err
return out, nil return out, nil
} }
// IterGitignoreTemplates returns an iterator over all available gitignore template names.
func (s *MiscService) IterGitignoreTemplates(ctx context.Context) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
items, err := s.ListGitignoreTemplates(ctx)
if err != nil {
yield("", err)
return
}
for _, item := range items {
if !yield(item, nil) {
return
}
}
}
}
// GetGitignoreTemplate returns a single gitignore template by name. // GetGitignoreTemplate returns a single gitignore template by name.
func (s *MiscService) GetGitignoreTemplate(ctx context.Context, name string) (*types.GitignoreTemplateInfo, error) { func (s *MiscService) GetGitignoreTemplate(ctx context.Context, name string) (*types.GitignoreTemplateInfo, error) {
path := ResolvePath("/api/v1/gitignore/templates/{name}", pathParams("name", name)) path := fmt.Sprintf("/api/v1/gitignore/templates/%s", name)
var out types.GitignoreTemplateInfo var out types.GitignoreTemplateInfo
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -135,51 +77,6 @@ func (s *MiscService) GetNodeInfo(ctx context.Context) (*types.NodeInfo, error)
return &out, nil return &out, nil
} }
// GetSigningKey returns the instance's default signing key.
func (s *MiscService) GetSigningKey(ctx context.Context) (string, error) {
data, err := s.client.GetRaw(ctx, "/api/v1/signing-key.gpg")
if err != nil {
return "", err
}
return string(data), nil
}
// GetAPISettings returns the instance's global API settings.
func (s *MiscService) GetAPISettings(ctx context.Context) (*types.GeneralAPISettings, error) {
var out types.GeneralAPISettings
if err := s.client.Get(ctx, "/api/v1/settings/api", &out); err != nil {
return nil, err
}
return &out, nil
}
// GetAttachmentSettings returns the instance's global attachment settings.
func (s *MiscService) GetAttachmentSettings(ctx context.Context) (*types.GeneralAttachmentSettings, error) {
var out types.GeneralAttachmentSettings
if err := s.client.Get(ctx, "/api/v1/settings/attachment", &out); err != nil {
return nil, err
}
return &out, nil
}
// GetRepositorySettings returns the instance's global repository settings.
func (s *MiscService) GetRepositorySettings(ctx context.Context) (*types.GeneralRepoSettings, error) {
var out types.GeneralRepoSettings
if err := s.client.Get(ctx, "/api/v1/settings/repository", &out); err != nil {
return nil, err
}
return &out, nil
}
// GetUISettings returns the instance's global UI settings.
func (s *MiscService) GetUISettings(ctx context.Context) (*types.GeneralUISettings, error) {
var out types.GeneralUISettings
if err := s.client.Get(ctx, "/api/v1/settings/ui", &out); err != nil {
return nil, err
}
return &out, nil
}
// GetVersion returns the server version. // GetVersion returns the server version.
func (s *MiscService) GetVersion(ctx context.Context) (*types.ServerVersion, error) { func (s *MiscService) GetVersion(ctx context.Context) (*types.ServerVersion, error) {
var out types.ServerVersion var out types.ServerVersion

View file

@ -2,17 +2,15 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestMiscService_RenderMarkdown_Good(t *testing.T) { func TestMiscService_Good_RenderMarkdown(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -46,85 +44,7 @@ func TestMiscService_RenderMarkdown_Good(t *testing.T) {
} }
} }
func TestMiscService_RenderMarkup_Good(t *testing.T) { func TestMiscService_Good_GetVersion(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/markup" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.MarkupOption
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Text != "**Hello**" {
t.Errorf("got text=%q, want %q", opts.Text, "**Hello**")
}
if opts.Mode != "gfm" {
t.Errorf("got mode=%q, want %q", opts.Mode, "gfm")
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("<p><strong>Hello</strong></p>\n"))
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
html, err := f.Misc.RenderMarkup(context.Background(), "**Hello**", "gfm")
if err != nil {
t.Fatal(err)
}
want := "<p><strong>Hello</strong></p>\n"
if html != want {
t.Errorf("got %q, want %q", html, want)
}
}
func TestMiscService_RenderMarkdownRaw_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/markdown/raw" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.Header.Get("Content-Type"); !strings.HasPrefix(got, "text/plain") {
t.Errorf("got content-type=%q, want text/plain", got)
}
if got := r.Header.Get("Accept"); got != "text/html" {
t.Errorf("got accept=%q, want text/html", got)
}
data, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
if string(data) != "# Hello" {
t.Errorf("got body=%q, want %q", string(data), "# Hello")
}
w.Header().Set("X-RateLimit-Limit", "80")
w.Header().Set("X-RateLimit-Remaining", "79")
w.Header().Set("X-RateLimit-Reset", "1700000003")
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("<h1>Hello</h1>\n"))
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
html, err := f.Misc.RenderMarkdownRaw(context.Background(), "# Hello")
if err != nil {
t.Fatal(err)
}
want := "<h1>Hello</h1>\n"
if html != want {
t.Errorf("got %q, want %q", html, want)
}
rl := f.Client().RateLimit()
if rl.Limit != 80 || rl.Remaining != 79 || rl.Reset != 1700000003 {
t.Fatalf("unexpected rate limit: %+v", rl)
}
}
func TestMiscService_GetVersion_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -148,117 +68,7 @@ func TestMiscService_GetVersion_Good(t *testing.T) {
} }
} }
func TestMiscService_GetAPISettings_Good(t *testing.T) { func TestMiscService_Good_ListLicenses(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/settings/api" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.GeneralAPISettings{
DefaultGitTreesPerPage: 25,
DefaultMaxBlobSize: 4096,
DefaultPagingNum: 1,
MaxResponseItems: 500,
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
settings, err := f.Misc.GetAPISettings(context.Background())
if err != nil {
t.Fatal(err)
}
if settings.DefaultPagingNum != 1 || settings.MaxResponseItems != 500 {
t.Fatalf("unexpected api settings: %+v", settings)
}
}
func TestMiscService_GetAttachmentSettings_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/settings/attachment" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.GeneralAttachmentSettings{
AllowedTypes: "image/*",
Enabled: true,
MaxFiles: 10,
MaxSize: 1048576,
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
settings, err := f.Misc.GetAttachmentSettings(context.Background())
if err != nil {
t.Fatal(err)
}
if !settings.Enabled || settings.MaxFiles != 10 {
t.Fatalf("unexpected attachment settings: %+v", settings)
}
}
func TestMiscService_GetRepositorySettings_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/settings/repository" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.GeneralRepoSettings{
ForksDisabled: true,
HTTPGitDisabled: true,
LFSDisabled: true,
MigrationsDisabled: true,
MirrorsDisabled: false,
StarsDisabled: true,
TimeTrackingDisabled: false,
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
settings, err := f.Misc.GetRepositorySettings(context.Background())
if err != nil {
t.Fatal(err)
}
if !settings.ForksDisabled || !settings.HTTPGitDisabled {
t.Fatalf("unexpected repository settings: %+v", settings)
}
}
func TestMiscService_GetUISettings_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/settings/ui" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.GeneralUISettings{
AllowedReactions: []string{"+1", "-1"},
CustomEmojis: []string{":forgejo:"},
DefaultTheme: "forgejo-auto",
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
settings, err := f.Misc.GetUISettings(context.Background())
if err != nil {
t.Fatal(err)
}
if settings.DefaultTheme != "forgejo-auto" || len(settings.AllowedReactions) != 2 {
t.Fatalf("unexpected ui settings: %+v", settings)
}
}
func TestMiscService_ListLicenses_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -289,38 +99,7 @@ func TestMiscService_ListLicenses_Good(t *testing.T) {
} }
} }
func TestMiscService_IterLicenses_Good(t *testing.T) { func TestMiscService_Good_GetLicense(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/licenses" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode([]types.LicensesTemplateListEntry{
{Key: "mit", Name: "MIT License"},
{Key: "gpl-3.0", Name: "GNU General Public License v3.0"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var names []string
for item, err := range f.Misc.IterLicenses(context.Background()) {
if err != nil {
t.Fatal(err)
}
names = append(names, item.Name)
}
if len(names) != 2 {
t.Fatalf("got %d licences, want 2", len(names))
}
if names[0] != "MIT License" || names[1] != "GNU General Public License v3.0" {
t.Fatalf("unexpected licences: %+v", names)
}
}
func TestMiscService_GetLicense_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -349,7 +128,7 @@ func TestMiscService_GetLicense_Good(t *testing.T) {
} }
} }
func TestMiscService_ListGitignoreTemplates_Good(t *testing.T) { func TestMiscService_Good_ListGitignoreTemplates(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -374,35 +153,7 @@ func TestMiscService_ListGitignoreTemplates_Good(t *testing.T) {
} }
} }
func TestMiscService_IterGitignoreTemplates_Good(t *testing.T) { func TestMiscService_Good_GetGitignoreTemplate(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/gitignore/templates" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode([]string{"Go", "Python", "Node"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var names []string
for item, err := range f.Misc.IterGitignoreTemplates(context.Background()) {
if err != nil {
t.Fatal(err)
}
names = append(names, item)
}
if len(names) != 3 {
t.Fatalf("got %d templates, want 3", len(names))
}
if names[0] != "Go" {
t.Errorf("got [0]=%q, want %q", names[0], "Go")
}
}
func TestMiscService_GetGitignoreTemplate_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -427,7 +178,7 @@ func TestMiscService_GetGitignoreTemplate_Good(t *testing.T) {
} }
} }
func TestMiscService_GetNodeInfo_Good(t *testing.T) { func TestMiscService_Good_GetNodeInfo(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -458,30 +209,7 @@ func TestMiscService_GetNodeInfo_Good(t *testing.T) {
} }
} }
func TestMiscService_GetSigningKey_Good(t *testing.T) { func TestMiscService_Bad_NotFound(t *testing.T) {
want := "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..."
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/signing-key.gpg" {
t.Errorf("wrong path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(want))
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
key, err := f.Misc.GetSigningKey(context.Background())
if err != nil {
t.Fatal(err)
}
if key != want {
t.Fatalf("got %q, want %q", key, want)
}
}
func TestMiscService_NotFound_Bad(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "not found"})

View file

@ -2,216 +2,42 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"net/http"
"net/url"
"strconv"
"time"
core "dappco.re/go/core"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// NotificationListOptions controls filtering for notification listings.
//
// Usage:
//
// opts := forge.NotificationListOptions{All: true, StatusTypes: []string{"unread"}}
type NotificationListOptions struct {
All bool
StatusTypes []string
SubjectTypes []string
Since *time.Time
Before *time.Time
}
// String returns a safe summary of the notification filters.
func (o NotificationListOptions) String() string {
return optionString("forge.NotificationListOptions",
"all", o.All,
"status_types", o.StatusTypes,
"subject_types", o.SubjectTypes,
"since", o.Since,
"before", o.Before,
)
}
// GoString returns a safe Go-syntax summary of the notification filters.
func (o NotificationListOptions) GoString() string { return o.String() }
func (o NotificationListOptions) addQuery(values url.Values) {
if o.All {
values.Set("all", "true")
}
for _, status := range o.StatusTypes {
if status != "" {
values.Add("status-types", status)
}
}
for _, subjectType := range o.SubjectTypes {
if subjectType != "" {
values.Add("subject-type", subjectType)
}
}
if o.Since != nil {
values.Set("since", o.Since.Format(time.RFC3339))
}
if o.Before != nil {
values.Set("before", o.Before.Format(time.RFC3339))
}
}
// NotificationService handles notification operations via the Forgejo API. // NotificationService handles notification operations via the Forgejo API.
// No Resource embedding — varied endpoint shapes. // No Resource embedding — varied endpoint shapes.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Notifications.List(ctx)
type NotificationService struct { type NotificationService struct {
client *Client client *Client
} }
// NotificationRepoMarkOptions controls how repository notifications are marked.
//
// Usage:
//
// opts := forge.NotificationRepoMarkOptions{All: true, ToStatus: "read"}
type NotificationRepoMarkOptions struct {
All bool
StatusTypes []string
ToStatus string
LastReadAt *time.Time
}
// String returns a safe summary of the repository notification mark options.
func (o NotificationRepoMarkOptions) String() string {
return optionString("forge.NotificationRepoMarkOptions",
"all", o.All,
"status_types", o.StatusTypes,
"to_status", o.ToStatus,
"last_read_at", o.LastReadAt,
)
}
// GoString returns a safe Go-syntax summary of the repository notification mark options.
func (o NotificationRepoMarkOptions) GoString() string { return o.String() }
// NotificationMarkOptions controls how authenticated-user notifications are marked.
//
// Usage:
//
// opts := forge.NotificationMarkOptions{All: true, ToStatus: "read"}
type NotificationMarkOptions struct {
All bool
StatusTypes []string
ToStatus string
LastReadAt *time.Time
}
// String returns a safe summary of the authenticated-user notification mark options.
func (o NotificationMarkOptions) String() string {
return optionString("forge.NotificationMarkOptions",
"all", o.All,
"status_types", o.StatusTypes,
"to_status", o.ToStatus,
"last_read_at", o.LastReadAt,
)
}
// GoString returns a safe Go-syntax summary of the authenticated-user notification mark options.
func (o NotificationMarkOptions) GoString() string { return o.String() }
func newNotificationService(c *Client) *NotificationService { func newNotificationService(c *Client) *NotificationService {
return &NotificationService{client: c} return &NotificationService{client: c}
} }
func notificationMarkQueryString(all bool, statusTypes []string, toStatus string, lastReadAt *time.Time) string {
values := url.Values{}
if all {
values.Set("all", "true")
}
for _, status := range statusTypes {
if status != "" {
values.Add("status-types", status)
}
}
if toStatus != "" {
values.Set("to-status", toStatus)
}
if lastReadAt != nil {
values.Set("last_read_at", lastReadAt.Format(time.RFC3339))
}
return values.Encode()
}
func (o NotificationRepoMarkOptions) queryString() string {
return notificationMarkQueryString(o.All, o.StatusTypes, o.ToStatus, o.LastReadAt)
}
func (o NotificationMarkOptions) queryString() string {
return notificationMarkQueryString(o.All, o.StatusTypes, o.ToStatus, o.LastReadAt)
}
// List returns all notifications for the authenticated user. // List returns all notifications for the authenticated user.
func (s *NotificationService) List(ctx context.Context, filters ...NotificationListOptions) ([]types.NotificationThread, error) { func (s *NotificationService) List(ctx context.Context) ([]types.NotificationThread, error) {
return s.listAll(ctx, "/api/v1/notifications", filters...) return ListAll[types.NotificationThread](ctx, s.client, "/api/v1/notifications", nil)
} }
// Iter returns an iterator over all notifications for the authenticated user. // Iter returns an iterator over all notifications for the authenticated user.
func (s *NotificationService) Iter(ctx context.Context, filters ...NotificationListOptions) iter.Seq2[types.NotificationThread, error] { func (s *NotificationService) Iter(ctx context.Context) iter.Seq2[types.NotificationThread, error] {
return s.listIter(ctx, "/api/v1/notifications", filters...) return ListIter[types.NotificationThread](ctx, s.client, "/api/v1/notifications", nil)
}
// NewAvailable returns the count of unread notifications for the authenticated user.
func (s *NotificationService) NewAvailable(ctx context.Context) (*types.NotificationCount, error) {
var out types.NotificationCount
if err := s.client.Get(ctx, "/api/v1/notifications/new", &out); err != nil {
return nil, err
}
return &out, nil
} }
// ListRepo returns all notifications for a specific repository. // ListRepo returns all notifications for a specific repository.
func (s *NotificationService) ListRepo(ctx context.Context, owner, repo string, filters ...NotificationListOptions) ([]types.NotificationThread, error) { func (s *NotificationService) ListRepo(ctx context.Context, owner, repo string) ([]types.NotificationThread, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/notifications", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/notifications", owner, repo)
return s.listAll(ctx, path, filters...) return ListAll[types.NotificationThread](ctx, s.client, path, nil)
} }
// IterRepo returns an iterator over all notifications for a specific repository. // IterRepo returns an iterator over all notifications for a specific repository.
func (s *NotificationService) IterRepo(ctx context.Context, owner, repo string, filters ...NotificationListOptions) iter.Seq2[types.NotificationThread, error] { func (s *NotificationService) IterRepo(ctx context.Context, owner, repo string) iter.Seq2[types.NotificationThread, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/notifications", pathParams("owner", owner, "repo", repo)) path := fmt.Sprintf("/api/v1/repos/%s/%s/notifications", owner, repo)
return s.listIter(ctx, path, filters...) return ListIter[types.NotificationThread](ctx, s.client, path, nil)
}
// MarkNotifications marks authenticated-user notification threads as read, pinned, or unread.
func (s *NotificationService) MarkNotifications(ctx context.Context, opts *NotificationMarkOptions) ([]types.NotificationThread, error) {
path := "/api/v1/notifications"
if opts != nil {
if query := opts.queryString(); query != "" {
path += "?" + query
}
}
var out []types.NotificationThread
if err := s.client.Put(ctx, path, nil, &out); err != nil {
return nil, err
}
return out, nil
}
// MarkRepoNotifications marks repository notification threads as read, unread, or pinned.
func (s *NotificationService) MarkRepoNotifications(ctx context.Context, owner, repo string, opts *NotificationRepoMarkOptions) ([]types.NotificationThread, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/notifications", pathParams("owner", owner, "repo", repo))
if opts != nil {
if query := opts.queryString(); query != "" {
path += "?" + query
}
}
var out []types.NotificationThread
if err := s.client.Put(ctx, path, nil, &out); err != nil {
return nil, err
}
return out, nil
} }
// MarkRead marks all notifications as read. // MarkRead marks all notifications as read.
@ -221,7 +47,7 @@ func (s *NotificationService) MarkRead(ctx context.Context) error {
// GetThread returns a single notification thread by ID. // GetThread returns a single notification thread by ID.
func (s *NotificationService) GetThread(ctx context.Context, id int64) (*types.NotificationThread, error) { func (s *NotificationService) GetThread(ctx context.Context, id int64) (*types.NotificationThread, error) {
path := ResolvePath("/api/v1/notifications/threads/{id}", pathParams("id", int64String(id))) path := fmt.Sprintf("/api/v1/notifications/threads/%d", id)
var out types.NotificationThread var out types.NotificationThread
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -231,84 +57,6 @@ func (s *NotificationService) GetThread(ctx context.Context, id int64) (*types.N
// MarkThreadRead marks a single notification thread as read. // MarkThreadRead marks a single notification thread as read.
func (s *NotificationService) MarkThreadRead(ctx context.Context, id int64) error { func (s *NotificationService) MarkThreadRead(ctx context.Context, id int64) error {
path := ResolvePath("/api/v1/notifications/threads/{id}", pathParams("id", int64String(id))) path := fmt.Sprintf("/api/v1/notifications/threads/%d", id)
return s.client.Patch(ctx, path, nil, nil) return s.client.Patch(ctx, path, nil, nil)
} }
func (s *NotificationService) listAll(ctx context.Context, path string, filters ...NotificationListOptions) ([]types.NotificationThread, error) {
var all []types.NotificationThread
page := 1
for {
result, err := s.listPage(ctx, path, ListOptions{Page: page, Limit: defaultPageLimit}, filters...)
if err != nil {
return nil, err
}
all = append(all, result.Items...)
if !result.HasMore {
break
}
page++
}
return all, nil
}
func (s *NotificationService) listIter(ctx context.Context, path string, filters ...NotificationListOptions) iter.Seq2[types.NotificationThread, error] {
return func(yield func(types.NotificationThread, error) bool) {
page := 1
for {
result, err := s.listPage(ctx, path, ListOptions{Page: page, Limit: defaultPageLimit}, filters...)
if err != nil {
yield(*new(types.NotificationThread), err)
return
}
for _, item := range result.Items {
if !yield(item, nil) {
return
}
}
if !result.HasMore {
break
}
page++
}
}
}
func (s *NotificationService) listPage(ctx context.Context, path string, opts ListOptions, filters ...NotificationListOptions) (*PagedResult[types.NotificationThread], error) {
if opts.Page < 1 {
opts.Page = 1
}
if opts.Limit < 1 {
opts.Limit = defaultPageLimit
}
u, err := url.Parse(path)
if err != nil {
return nil, core.E("NotificationService.listPage", "forge: parse path", err)
}
values := u.Query()
values.Set("page", strconv.Itoa(opts.Page))
values.Set("limit", strconv.Itoa(opts.Limit))
for _, filter := range filters {
filter.addQuery(values)
}
u.RawQuery = values.Encode()
var items []types.NotificationThread
resp, err := s.client.doJSON(ctx, http.MethodGet, u.String(), nil, &items)
if err != nil {
return nil, err
}
totalCount, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
return &PagedResult[types.NotificationThread]{
Items: items,
TotalCount: totalCount,
Page: opts.Page,
HasMore: (totalCount > 0 && (opts.Page-1)*opts.Limit+len(items) < totalCount) ||
(totalCount == 0 && len(items) >= opts.Limit),
}, nil
}

View file

@ -2,16 +2,15 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"time"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestNotificationService_List_Good(t *testing.T) { func TestNotificationService_Good_List(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -46,55 +45,7 @@ func TestNotificationService_List_Good(t *testing.T) {
} }
} }
func TestNotificationService_List_Filters(t *testing.T) { func TestNotificationService_Good_ListRepo(t *testing.T) {
since := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC)
before := time.Date(2026, time.April, 2, 12, 0, 0, 0, time.UTC)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/notifications" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("all"); got != "true" {
t.Errorf("got all=%q, want true", got)
}
if got := r.URL.Query()["status-types"]; len(got) != 2 || got[0] != "unread" || got[1] != "pinned" {
t.Errorf("got status-types=%v, want [unread pinned]", got)
}
if got := r.URL.Query()["subject-type"]; len(got) != 2 || got[0] != "issue" || got[1] != "pull" {
t.Errorf("got subject-type=%v, want [issue pull]", got)
}
if got := r.URL.Query().Get("since"); got != since.Format(time.RFC3339) {
t.Errorf("got since=%q, want %q", got, since.Format(time.RFC3339))
}
if got := r.URL.Query().Get("before"); got != before.Format(time.RFC3339) {
t.Errorf("got before=%q, want %q", got, before.Format(time.RFC3339))
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.NotificationThread{
{ID: 11, Unread: true, Subject: &types.NotificationSubject{Title: "Filtered"}},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
threads, err := f.Notifications.List(context.Background(), NotificationListOptions{
All: true,
StatusTypes: []string{"unread", "pinned"},
SubjectTypes: []string{"issue", "pull"},
Since: &since,
Before: &before,
})
if err != nil {
t.Fatal(err)
}
if len(threads) != 1 || threads[0].ID != 11 {
t.Fatalf("got threads=%+v", threads)
}
}
func TestNotificationService_ListRepo_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -122,68 +73,7 @@ func TestNotificationService_ListRepo_Good(t *testing.T) {
} }
} }
func TestNotificationService_ListRepo_Filters(t *testing.T) { func TestNotificationService_Good_GetThread(t *testing.T) {
since := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/notifications" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query()["status-types"]; len(got) != 1 || got[0] != "read" {
t.Errorf("got status-types=%v, want [read]", got)
}
if got := r.URL.Query()["subject-type"]; len(got) != 1 || got[0] != "repository" {
t.Errorf("got subject-type=%v, want [repository]", got)
}
if got := r.URL.Query().Get("since"); got != since.Format(time.RFC3339) {
t.Errorf("got since=%q, want %q", got, since.Format(time.RFC3339))
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.NotificationThread{
{ID: 12, Unread: false, Subject: &types.NotificationSubject{Title: "Repo filtered"}},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
threads, err := f.Notifications.ListRepo(context.Background(), "core", "go-forge", NotificationListOptions{
StatusTypes: []string{"read"},
SubjectTypes: []string{"repository"},
Since: &since,
})
if err != nil {
t.Fatal(err)
}
if len(threads) != 1 || threads[0].ID != 12 {
t.Fatalf("got threads=%+v", threads)
}
}
func TestNotificationService_NewAvailable_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/notifications/new" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.NotificationCount{New: 3})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
count, err := f.Notifications.NewAvailable(context.Background())
if err != nil {
t.Fatal(err)
}
if count.New != 3 {
t.Fatalf("got new=%d, want 3", count.New)
}
}
func TestNotificationService_GetThread_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -217,57 +107,7 @@ func TestNotificationService_GetThread_Good(t *testing.T) {
} }
} }
func TestNotificationService_MarkNotifications_Good(t *testing.T) { func TestNotificationService_Good_MarkRead(t *testing.T) {
lastReadAt := time.Date(2026, time.April, 2, 15, 4, 5, 0, time.UTC)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method)
}
if r.URL.Path != "/api/v1/notifications" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("all"); got != "true" {
t.Errorf("got all=%q, want true", got)
}
if got := r.URL.Query()["status-types"]; len(got) != 2 || got[0] != "unread" || got[1] != "pinned" {
t.Errorf("got status-types=%v, want [unread pinned]", got)
}
if got := r.URL.Query().Get("to-status"); got != "read" {
t.Errorf("got to-status=%q, want read", got)
}
if got := r.URL.Query().Get("last_read_at"); got != lastReadAt.Format(time.RFC3339) {
t.Errorf("got last_read_at=%q, want %q", got, lastReadAt.Format(time.RFC3339))
}
w.WriteHeader(http.StatusResetContent)
json.NewEncoder(w).Encode([]types.NotificationThread{
{ID: 21, Unread: false, Subject: &types.NotificationSubject{Title: "Release notes"}},
{ID: 22, Unread: false, Subject: &types.NotificationSubject{Title: "Issue triaged"}},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
threads, err := f.Notifications.MarkNotifications(context.Background(), &NotificationMarkOptions{
All: true,
StatusTypes: []string{"unread", "pinned"},
ToStatus: "read",
LastReadAt: &lastReadAt,
})
if err != nil {
t.Fatal(err)
}
if len(threads) != 2 {
t.Fatalf("got %d threads, want 2", len(threads))
}
if threads[0].ID != 21 || threads[1].ID != 22 {
t.Fatalf("got ids=%d,%d want 21,22", threads[0].ID, threads[1].ID)
}
if threads[0].Subject.Title != "Release notes" {
t.Errorf("got title=%q, want %q", threads[0].Subject.Title, "Release notes")
}
}
func TestNotificationService_MarkRead_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut { if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method) t.Errorf("expected PUT, got %s", r.Method)
@ -286,7 +126,7 @@ func TestNotificationService_MarkRead_Good(t *testing.T) {
} }
} }
func TestNotificationService_MarkThreadRead_Good(t *testing.T) { func TestNotificationService_Good_MarkThreadRead(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch { if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method) t.Errorf("expected PATCH, got %s", r.Method)
@ -305,57 +145,7 @@ func TestNotificationService_MarkThreadRead_Good(t *testing.T) {
} }
} }
func TestNotificationService_MarkRepoNotifications_Good(t *testing.T) { func TestNotificationService_Bad_NotFound(t *testing.T) {
lastReadAt := time.Date(2026, time.April, 2, 15, 4, 5, 0, time.UTC)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/notifications" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query()["status-types"]; len(got) != 2 || got[0] != "unread" || got[1] != "pinned" {
t.Errorf("got status-types=%v, want [unread pinned]", got)
}
if got := r.URL.Query().Get("all"); got != "true" {
t.Errorf("got all=%q, want true", got)
}
if got := r.URL.Query().Get("to-status"); got != "read" {
t.Errorf("got to-status=%q, want read", got)
}
if got := r.URL.Query().Get("last_read_at"); got != lastReadAt.Format(time.RFC3339) {
t.Errorf("got last_read_at=%q, want %q", got, lastReadAt.Format(time.RFC3339))
}
w.WriteHeader(http.StatusResetContent)
json.NewEncoder(w).Encode([]types.NotificationThread{
{ID: 7, Unread: false, Subject: &types.NotificationSubject{Title: "Pinned release"}},
{ID: 8, Unread: false, Subject: &types.NotificationSubject{Title: "New docs"}},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
threads, err := f.Notifications.MarkRepoNotifications(context.Background(), "core", "go-forge", &NotificationRepoMarkOptions{
All: true,
StatusTypes: []string{"unread", "pinned"},
ToStatus: "read",
LastReadAt: &lastReadAt,
})
if err != nil {
t.Fatal(err)
}
if len(threads) != 2 {
t.Fatalf("got %d threads, want 2", len(threads))
}
if threads[0].ID != 7 || threads[1].ID != 8 {
t.Fatalf("got ids=%d,%d want 7,8", threads[0].ID, threads[1].ID)
}
if threads[0].Subject.Title != "Pinned release" {
t.Errorf("got title=%q, want %q", threads[0].Subject.Title, "Pinned release")
}
}
func TestNotificationService_NotFound_Bad(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "thread not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "thread not found"})

281
orgs.go
View file

@ -2,49 +2,17 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"net/http"
"time"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// OrgService handles organisation operations. // OrgService handles organisation operations.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Orgs.ListMembers(ctx, "core")
type OrgService struct { type OrgService struct {
Resource[types.Organization, types.CreateOrgOption, types.EditOrgOption] Resource[types.Organization, types.CreateOrgOption, types.EditOrgOption]
} }
// OrgActivityFeedListOptions controls filtering for organisation activity feeds.
//
// Usage:
//
// opts := forge.OrgActivityFeedListOptions{Date: &day}
type OrgActivityFeedListOptions struct {
Date *time.Time
}
// String returns a safe summary of the organisation activity feed filters.
func (o OrgActivityFeedListOptions) String() string {
return optionString("forge.OrgActivityFeedListOptions", "date", o.Date)
}
// GoString returns a safe Go-syntax summary of the organisation activity feed filters.
func (o OrgActivityFeedListOptions) GoString() string { return o.String() }
func (o OrgActivityFeedListOptions) queryParams() map[string]string {
if o.Date == nil {
return nil
}
return map[string]string{
"date": o.Date.Format("2006-01-02"),
}
}
func newOrgService(c *Client) *OrgService { func newOrgService(c *Client) *OrgService {
return &OrgService{ return &OrgService{
Resource: *NewResource[types.Organization, types.CreateOrgOption, types.EditOrgOption]( Resource: *NewResource[types.Organization, types.CreateOrgOption, types.EditOrgOption](
@ -53,257 +21,39 @@ func newOrgService(c *Client) *OrgService {
} }
} }
// ListOrgs returns all organisations.
func (s *OrgService) ListOrgs(ctx context.Context) ([]types.Organization, error) {
return ListAll[types.Organization](ctx, s.client, "/api/v1/orgs", nil)
}
// IterOrgs returns an iterator over all organisations.
func (s *OrgService) IterOrgs(ctx context.Context) iter.Seq2[types.Organization, error] {
return ListIter[types.Organization](ctx, s.client, "/api/v1/orgs", nil)
}
// CreateOrg creates a new organisation.
func (s *OrgService) CreateOrg(ctx context.Context, opts *types.CreateOrgOption) (*types.Organization, error) {
var out types.Organization
if err := s.client.Post(ctx, "/api/v1/orgs", opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// ListMembers returns all members of an organisation. // ListMembers returns all members of an organisation.
func (s *OrgService) ListMembers(ctx context.Context, org string) ([]types.User, error) { func (s *OrgService) ListMembers(ctx context.Context, org string) ([]types.User, error) {
path := ResolvePath("/api/v1/orgs/{org}/members", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/members", org)
return ListAll[types.User](ctx, s.client, path, nil) return ListAll[types.User](ctx, s.client, path, nil)
} }
// IterMembers returns an iterator over all members of an organisation. // IterMembers returns an iterator over all members of an organisation.
func (s *OrgService) IterMembers(ctx context.Context, org string) iter.Seq2[types.User, error] { func (s *OrgService) IterMembers(ctx context.Context, org string) iter.Seq2[types.User, error] {
path := ResolvePath("/api/v1/orgs/{org}/members", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/members", org)
return ListIter[types.User](ctx, s.client, path, nil) return ListIter[types.User](ctx, s.client, path, nil)
} }
// AddMember adds a user to an organisation. // AddMember adds a user to an organisation.
func (s *OrgService) AddMember(ctx context.Context, org, username string) error { func (s *OrgService) AddMember(ctx context.Context, org, username string) error {
path := ResolvePath("/api/v1/orgs/{org}/members/{username}", pathParams("org", org, "username", username)) path := fmt.Sprintf("/api/v1/orgs/%s/members/%s", org, username)
return s.client.Put(ctx, path, nil, nil) return s.client.Put(ctx, path, nil, nil)
} }
// RemoveMember removes a user from an organisation. // RemoveMember removes a user from an organisation.
func (s *OrgService) RemoveMember(ctx context.Context, org, username string) error { func (s *OrgService) RemoveMember(ctx context.Context, org, username string) error {
path := ResolvePath("/api/v1/orgs/{org}/members/{username}", pathParams("org", org, "username", username)) path := fmt.Sprintf("/api/v1/orgs/%s/members/%s", org, username)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// IsMember reports whether a user is a member of an organisation.
func (s *OrgService) IsMember(ctx context.Context, org, username string) (bool, error) {
path := ResolvePath("/api/v1/orgs/{org}/members/{username}", pathParams("org", org, "username", username))
resp, err := s.client.doJSON(ctx, http.MethodGet, path, nil, nil)
if err != nil {
if IsNotFound(err) {
return false, nil
}
return false, err
}
return resp.StatusCode == http.StatusNoContent, nil
}
// ListBlockedUsers returns all users blocked by an organisation.
func (s *OrgService) ListBlockedUsers(ctx context.Context, org string) ([]types.BlockedUser, error) {
path := ResolvePath("/api/v1/orgs/{org}/list_blocked", pathParams("org", org))
return ListAll[types.BlockedUser](ctx, s.client, path, nil)
}
// IterBlockedUsers returns an iterator over all users blocked by an organisation.
func (s *OrgService) IterBlockedUsers(ctx context.Context, org string) iter.Seq2[types.BlockedUser, error] {
path := ResolvePath("/api/v1/orgs/{org}/list_blocked", pathParams("org", org))
return ListIter[types.BlockedUser](ctx, s.client, path, nil)
}
// IsBlocked reports whether a user is blocked by an organisation.
func (s *OrgService) IsBlocked(ctx context.Context, org, username string) (bool, error) {
path := ResolvePath("/api/v1/orgs/{org}/block/{username}", pathParams("org", org, "username", username))
resp, err := s.client.doJSON(ctx, "GET", path, nil, nil)
if err != nil {
if IsNotFound(err) {
return false, nil
}
return false, err
}
return resp.StatusCode == http.StatusNoContent, nil
}
// ListPublicMembers returns all public members of an organisation.
func (s *OrgService) ListPublicMembers(ctx context.Context, org string) ([]types.User, error) {
path := ResolvePath("/api/v1/orgs/{org}/public_members", pathParams("org", org))
return ListAll[types.User](ctx, s.client, path, nil)
}
// IterPublicMembers returns an iterator over all public members of an organisation.
func (s *OrgService) IterPublicMembers(ctx context.Context, org string) iter.Seq2[types.User, error] {
path := ResolvePath("/api/v1/orgs/{org}/public_members", pathParams("org", org))
return ListIter[types.User](ctx, s.client, path, nil)
}
// IsPublicMember reports whether a user is a public member of an organisation.
func (s *OrgService) IsPublicMember(ctx context.Context, org, username string) (bool, error) {
path := ResolvePath("/api/v1/orgs/{org}/public_members/{username}", pathParams("org", org, "username", username))
resp, err := s.client.doJSON(ctx, "GET", path, nil, nil)
if err != nil {
if IsNotFound(err) {
return false, nil
}
return false, err
}
return resp.StatusCode == http.StatusNoContent, nil
}
// PublicizeMember makes a user's membership public within an organisation.
func (s *OrgService) PublicizeMember(ctx context.Context, org, username string) error {
path := ResolvePath("/api/v1/orgs/{org}/public_members/{username}", pathParams("org", org, "username", username))
return s.client.Put(ctx, path, nil, nil)
}
// ConcealMember hides a user's public membership within an organisation.
func (s *OrgService) ConcealMember(ctx context.Context, org, username string) error {
path := ResolvePath("/api/v1/orgs/{org}/public_members/{username}", pathParams("org", org, "username", username))
return s.client.Delete(ctx, path)
}
// Block blocks a user within an organisation.
func (s *OrgService) Block(ctx context.Context, org, username string) error {
path := ResolvePath("/api/v1/orgs/{org}/block/{username}", pathParams("org", org, "username", username))
return s.client.Put(ctx, path, nil, nil)
}
// Unblock unblocks a user within an organisation.
func (s *OrgService) Unblock(ctx context.Context, org, username string) error {
path := ResolvePath("/api/v1/orgs/{org}/unblock/{username}", pathParams("org", org, "username", username))
return s.client.Delete(ctx, path)
}
// GetQuota returns the quota information for an organisation.
func (s *OrgService) GetQuota(ctx context.Context, org string) (*types.QuotaInfo, error) {
path := ResolvePath("/api/v1/orgs/{org}/quota", pathParams("org", org))
var out types.QuotaInfo
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// CheckQuota reports whether an organisation is over quota for the current subject.
func (s *OrgService) CheckQuota(ctx context.Context, org string) (bool, error) {
path := ResolvePath("/api/v1/orgs/{org}/quota/check", pathParams("org", org))
var out bool
if err := s.client.Get(ctx, path, &out); err != nil {
return false, err
}
return out, nil
}
// ListQuotaArtifacts returns all artefacts counting towards an organisation's quota.
func (s *OrgService) ListQuotaArtifacts(ctx context.Context, org string) ([]types.QuotaUsedArtifact, error) {
path := ResolvePath("/api/v1/orgs/{org}/quota/artifacts", pathParams("org", org))
return ListAll[types.QuotaUsedArtifact](ctx, s.client, path, nil)
}
// IterQuotaArtifacts returns an iterator over all artefacts counting towards an organisation's quota.
func (s *OrgService) IterQuotaArtifacts(ctx context.Context, org string) iter.Seq2[types.QuotaUsedArtifact, error] {
path := ResolvePath("/api/v1/orgs/{org}/quota/artifacts", pathParams("org", org))
return ListIter[types.QuotaUsedArtifact](ctx, s.client, path, nil)
}
// ListQuotaAttachments returns all attachments counting towards an organisation's quota.
func (s *OrgService) ListQuotaAttachments(ctx context.Context, org string) ([]types.QuotaUsedAttachment, error) {
path := ResolvePath("/api/v1/orgs/{org}/quota/attachments", pathParams("org", org))
return ListAll[types.QuotaUsedAttachment](ctx, s.client, path, nil)
}
// IterQuotaAttachments returns an iterator over all attachments counting towards an organisation's quota.
func (s *OrgService) IterQuotaAttachments(ctx context.Context, org string) iter.Seq2[types.QuotaUsedAttachment, error] {
path := ResolvePath("/api/v1/orgs/{org}/quota/attachments", pathParams("org", org))
return ListIter[types.QuotaUsedAttachment](ctx, s.client, path, nil)
}
// ListQuotaPackages returns all packages counting towards an organisation's quota.
func (s *OrgService) ListQuotaPackages(ctx context.Context, org string) ([]types.QuotaUsedPackage, error) {
path := ResolvePath("/api/v1/orgs/{org}/quota/packages", pathParams("org", org))
return ListAll[types.QuotaUsedPackage](ctx, s.client, path, nil)
}
// IterQuotaPackages returns an iterator over all packages counting towards an organisation's quota.
func (s *OrgService) IterQuotaPackages(ctx context.Context, org string) iter.Seq2[types.QuotaUsedPackage, error] {
path := ResolvePath("/api/v1/orgs/{org}/quota/packages", pathParams("org", org))
return ListIter[types.QuotaUsedPackage](ctx, s.client, path, nil)
}
// GetRunnerRegistrationToken returns an organisation actions runner registration token.
func (s *OrgService) GetRunnerRegistrationToken(ctx context.Context, org string) (string, error) {
path := ResolvePath("/api/v1/orgs/{org}/actions/runners/registration-token", pathParams("org", org))
resp, err := s.client.doJSON(ctx, http.MethodGet, path, nil, nil)
if err != nil {
return "", err
}
return resp.Header.Get("token"), nil
}
// UpdateAvatar updates an organisation avatar.
func (s *OrgService) UpdateAvatar(ctx context.Context, org string, opts *types.UpdateUserAvatarOption) error {
path := ResolvePath("/api/v1/orgs/{org}/avatar", pathParams("org", org))
return s.client.Post(ctx, path, opts, nil)
}
// DeleteAvatar deletes an organisation avatar.
func (s *OrgService) DeleteAvatar(ctx context.Context, org string) error {
path := ResolvePath("/api/v1/orgs/{org}/avatar", pathParams("org", org))
return s.client.Delete(ctx, path)
}
// SearchTeams searches for teams within an organisation.
func (s *OrgService) SearchTeams(ctx context.Context, org, q string) ([]types.Team, error) {
path := ResolvePath("/api/v1/orgs/{org}/teams/search", pathParams("org", org))
return ListAll[types.Team](ctx, s.client, path, map[string]string{"q": q})
}
// IterSearchTeams returns an iterator over teams within an organisation.
func (s *OrgService) IterSearchTeams(ctx context.Context, org, q string) iter.Seq2[types.Team, error] {
path := ResolvePath("/api/v1/orgs/{org}/teams/search", pathParams("org", org))
return ListIter[types.Team](ctx, s.client, path, map[string]string{"q": q})
}
// GetUserPermissions returns a user's permissions in an organisation.
func (s *OrgService) GetUserPermissions(ctx context.Context, username, org string) (*types.OrganizationPermissions, error) {
path := ResolvePath("/api/v1/users/{username}/orgs/{org}/permissions", pathParams("username", username, "org", org))
var out types.OrganizationPermissions
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// ListActivityFeeds returns the organisation's activity feed entries.
func (s *OrgService) ListActivityFeeds(ctx context.Context, org string, filters ...OrgActivityFeedListOptions) ([]types.Activity, error) {
path := ResolvePath("/api/v1/orgs/{org}/activities/feeds", pathParams("org", org))
return ListAll[types.Activity](ctx, s.client, path, orgActivityFeedQuery(filters...))
}
// IterActivityFeeds returns an iterator over the organisation's activity feed entries.
func (s *OrgService) IterActivityFeeds(ctx context.Context, org string, filters ...OrgActivityFeedListOptions) iter.Seq2[types.Activity, error] {
path := ResolvePath("/api/v1/orgs/{org}/activities/feeds", pathParams("org", org))
return ListIter[types.Activity](ctx, s.client, path, orgActivityFeedQuery(filters...))
}
// ListUserOrgs returns all organisations for a user. // ListUserOrgs returns all organisations for a user.
func (s *OrgService) ListUserOrgs(ctx context.Context, username string) ([]types.Organization, error) { func (s *OrgService) ListUserOrgs(ctx context.Context, username string) ([]types.Organization, error) {
path := ResolvePath("/api/v1/users/{username}/orgs", pathParams("username", username)) path := fmt.Sprintf("/api/v1/users/%s/orgs", username)
return ListAll[types.Organization](ctx, s.client, path, nil) return ListAll[types.Organization](ctx, s.client, path, nil)
} }
// IterUserOrgs returns an iterator over all organisations for a user. // IterUserOrgs returns an iterator over all organisations for a user.
func (s *OrgService) IterUserOrgs(ctx context.Context, username string) iter.Seq2[types.Organization, error] { func (s *OrgService) IterUserOrgs(ctx context.Context, username string) iter.Seq2[types.Organization, error] {
path := ResolvePath("/api/v1/users/{username}/orgs", pathParams("username", username)) path := fmt.Sprintf("/api/v1/users/%s/orgs", username)
return ListIter[types.Organization](ctx, s.client, path, nil) return ListIter[types.Organization](ctx, s.client, path, nil)
} }
@ -316,20 +66,3 @@ func (s *OrgService) ListMyOrgs(ctx context.Context) ([]types.Organization, erro
func (s *OrgService) IterMyOrgs(ctx context.Context) iter.Seq2[types.Organization, error] { func (s *OrgService) IterMyOrgs(ctx context.Context) iter.Seq2[types.Organization, error] {
return ListIter[types.Organization](ctx, s.client, "/api/v1/user/orgs", nil) return ListIter[types.Organization](ctx, s.client, "/api/v1/user/orgs", nil)
} }
func orgActivityFeedQuery(filters ...OrgActivityFeedListOptions) map[string]string {
if len(filters) == 0 {
return nil
}
query := make(map[string]string, 1)
for _, filter := range filters {
if filter.Date != nil {
query["date"] = filter.Date.Format("2006-01-02")
}
}
if len(query) == 0 {
return nil
}
return query
}

View file

@ -1,63 +0,0 @@
package forge
import (
"context"
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"testing"
"dappco.re/go/core/forge/types"
)
func TestOrgService_ListOrgs_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Organization{{ID: 1, Name: "core"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
orgs, err := f.Orgs.ListOrgs(context.Background())
if err != nil {
t.Fatal(err)
}
if len(orgs) != 1 || orgs[0].Name != "core" {
t.Fatalf("got %#v", orgs)
}
}
func TestOrgService_CreateOrg_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var body types.CreateOrgOption
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.UserName != "core" {
t.Fatalf("unexpected body: %+v", body)
}
json.NewEncoder(w).Encode(types.Organization{ID: 1, Name: body.UserName})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
org, err := f.Orgs.CreateOrg(context.Background(), &types.CreateOrgOption{UserName: "core"})
if err != nil {
t.Fatal(err)
}
if org.Name != "core" {
t.Fatalf("got name=%q", org.Name)
}
}

View file

@ -2,16 +2,15 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"time"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestOrgService_List_Good(t *testing.T) { func TestOrgService_Good_List(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -37,7 +36,7 @@ func TestOrgService_List_Good(t *testing.T) {
} }
} }
func TestOrgService_Get_Good(t *testing.T) { func TestOrgService_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -59,7 +58,7 @@ func TestOrgService_Get_Good(t *testing.T) {
} }
} }
func TestOrgService_ListMembers_Good(t *testing.T) { func TestOrgService_Good_ListMembers(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -87,265 +86,3 @@ func TestOrgService_ListMembers_Good(t *testing.T) {
t.Errorf("got username=%q, want %q", members[0].UserName, "alice") t.Errorf("got username=%q, want %q", members[0].UserName, "alice")
} }
} }
func TestOrgService_IsMember_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/members/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
member, err := f.Orgs.IsMember(context.Background(), "core", "alice")
if err != nil {
t.Fatal(err)
}
if !member {
t.Fatal("got member=false, want true")
}
}
func TestOrgService_ListPublicMembers_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/public_members" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.User{
{ID: 1, UserName: "alice"},
{ID: 2, UserName: "bob"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
members, err := f.Orgs.ListPublicMembers(context.Background(), "core")
if err != nil {
t.Fatal(err)
}
if len(members) != 2 {
t.Errorf("got %d members, want 2", len(members))
}
if members[0].UserName != "alice" {
t.Errorf("got username=%q, want %q", members[0].UserName, "alice")
}
}
func TestOrgService_ListBlockedUsers_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/list_blocked" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.BlockedUser{
{BlockID: 1},
{BlockID: 2},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
blocked, err := f.Orgs.ListBlockedUsers(context.Background(), "core")
if err != nil {
t.Fatal(err)
}
if len(blocked) != 2 {
t.Fatalf("got %d blocked users, want 2", len(blocked))
}
if blocked[0].BlockID != 1 {
t.Errorf("got block_id=%d, want %d", blocked[0].BlockID, 1)
}
}
func TestOrgService_PublicizeMember_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/public_members/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Orgs.PublicizeMember(context.Background(), "core", "alice"); err != nil {
t.Fatal(err)
}
}
func TestOrgService_ConcealMember_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/public_members/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Orgs.ConcealMember(context.Background(), "core", "alice"); err != nil {
t.Fatal(err)
}
}
func TestOrgService_Block_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/block/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Orgs.Block(context.Background(), "core", "alice"); err != nil {
t.Fatal(err)
}
}
func TestOrgService_Unblock_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/unblock/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Orgs.Unblock(context.Background(), "core", "alice"); err != nil {
t.Fatal(err)
}
}
func TestOrgService_ListActivityFeeds_Good(t *testing.T) {
date := time.Date(2026, time.April, 2, 15, 4, 5, 0, time.UTC)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/activities/feeds" {
t.Errorf("wrong path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("date"); got != "2026-04-02" {
t.Errorf("wrong date: %s", got)
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Activity{{
ID: 9,
OpType: "create_org",
Content: "created organisation",
}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
activities, err := f.Orgs.ListActivityFeeds(context.Background(), "core", OrgActivityFeedListOptions{Date: &date})
if err != nil {
t.Fatal(err)
}
if len(activities) != 1 || activities[0].ID != 9 || activities[0].OpType != "create_org" {
t.Fatalf("got %#v", activities)
}
}
func TestOrgService_IterActivityFeeds_Good(t *testing.T) {
var requests int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/activities/feeds" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Activity{{
ID: 11,
OpType: "update_org",
Content: "updated organisation",
}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var got []int64
for activity, err := range f.Orgs.IterActivityFeeds(context.Background(), "core") {
if err != nil {
t.Fatal(err)
}
got = append(got, activity.ID)
}
if requests != 1 {
t.Fatalf("expected 1 request, got %d", requests)
}
if len(got) != 1 || got[0] != 11 {
t.Fatalf("got %#v", got)
}
}
func TestOrgService_IsBlocked_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/block/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
blocked, err := f.Orgs.IsBlocked(context.Background(), "core", "alice")
if err != nil {
t.Fatal(err)
}
if !blocked {
t.Fatal("got blocked=false, want true")
}
}
func TestOrgService_IsPublicMember_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/public_members/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
public, err := f.Orgs.IsPublicMember(context.Background(), "core", "alice")
if err != nil {
t.Fatal(err)
}
if !public {
t.Fatal("got public=false, want true")
}
}

View file

@ -2,6 +2,7 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
@ -9,11 +10,6 @@ import (
// PackageService handles package registry operations via the Forgejo API. // PackageService handles package registry operations via the Forgejo API.
// No Resource embedding — paths vary by operation. // No Resource embedding — paths vary by operation.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Packages.List(ctx, "core")
type PackageService struct { type PackageService struct {
client *Client client *Client
} }
@ -24,19 +20,19 @@ func newPackageService(c *Client) *PackageService {
// List returns all packages for a given owner. // List returns all packages for a given owner.
func (s *PackageService) List(ctx context.Context, owner string) ([]types.Package, error) { func (s *PackageService) List(ctx context.Context, owner string) ([]types.Package, error) {
path := ResolvePath("/api/v1/packages/{owner}", pathParams("owner", owner)) path := fmt.Sprintf("/api/v1/packages/%s", owner)
return ListAll[types.Package](ctx, s.client, path, nil) return ListAll[types.Package](ctx, s.client, path, nil)
} }
// Iter returns an iterator over all packages for a given owner. // Iter returns an iterator over all packages for a given owner.
func (s *PackageService) Iter(ctx context.Context, owner string) iter.Seq2[types.Package, error] { func (s *PackageService) Iter(ctx context.Context, owner string) iter.Seq2[types.Package, error] {
path := ResolvePath("/api/v1/packages/{owner}", pathParams("owner", owner)) path := fmt.Sprintf("/api/v1/packages/%s", owner)
return ListIter[types.Package](ctx, s.client, path, nil) return ListIter[types.Package](ctx, s.client, path, nil)
} }
// Get returns a single package by owner, type, name, and version. // Get returns a single package by owner, type, name, and version.
func (s *PackageService) Get(ctx context.Context, owner, pkgType, name, version string) (*types.Package, error) { func (s *PackageService) Get(ctx context.Context, owner, pkgType, name, version string) (*types.Package, error) {
path := ResolvePath("/api/v1/packages/{owner}/{type}/{name}/{version}", pathParams("owner", owner, "type", pkgType, "name", name, "version", version)) path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s", owner, pkgType, name, version)
var out types.Package var out types.Package
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -46,18 +42,18 @@ func (s *PackageService) Get(ctx context.Context, owner, pkgType, name, version
// Delete removes a package by owner, type, name, and version. // Delete removes a package by owner, type, name, and version.
func (s *PackageService) Delete(ctx context.Context, owner, pkgType, name, version string) error { func (s *PackageService) Delete(ctx context.Context, owner, pkgType, name, version string) error {
path := ResolvePath("/api/v1/packages/{owner}/{type}/{name}/{version}", pathParams("owner", owner, "type", pkgType, "name", name, "version", version)) path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s", owner, pkgType, name, version)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// ListFiles returns all files for a specific package version. // ListFiles returns all files for a specific package version.
func (s *PackageService) ListFiles(ctx context.Context, owner, pkgType, name, version string) ([]types.PackageFile, error) { func (s *PackageService) ListFiles(ctx context.Context, owner, pkgType, name, version string) ([]types.PackageFile, error) {
path := ResolvePath("/api/v1/packages/{owner}/{type}/{name}/{version}/files", pathParams("owner", owner, "type", pkgType, "name", name, "version", version)) path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s/files", owner, pkgType, name, version)
return ListAll[types.PackageFile](ctx, s.client, path, nil) return ListAll[types.PackageFile](ctx, s.client, path, nil)
} }
// IterFiles returns an iterator over all files for a specific package version. // IterFiles returns an iterator over all files for a specific package version.
func (s *PackageService) IterFiles(ctx context.Context, owner, pkgType, name, version string) iter.Seq2[types.PackageFile, error] { func (s *PackageService) IterFiles(ctx context.Context, owner, pkgType, name, version string) iter.Seq2[types.PackageFile, error] {
path := ResolvePath("/api/v1/packages/{owner}/{type}/{name}/{version}/files", pathParams("owner", owner, "type", pkgType, "name", name, "version", version)) path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s/files", owner, pkgType, name, version)
return ListIter[types.PackageFile](ctx, s.client, path, nil) return ListIter[types.PackageFile](ctx, s.client, path, nil)
} }

View file

@ -2,7 +2,7 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -10,7 +10,7 @@ import (
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestPackageService_List_Good(t *testing.T) { func TestPackageService_Good_List(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -42,7 +42,7 @@ func TestPackageService_List_Good(t *testing.T) {
} }
} }
func TestPackageService_Get_Good(t *testing.T) { func TestPackageService_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -75,7 +75,7 @@ func TestPackageService_Get_Good(t *testing.T) {
} }
} }
func TestPackageService_Delete_Good(t *testing.T) { func TestPackageService_Good_Delete(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method) t.Errorf("expected DELETE, got %s", r.Method)
@ -94,7 +94,7 @@ func TestPackageService_Delete_Good(t *testing.T) {
} }
} }
func TestPackageService_ListFiles_Good(t *testing.T) { func TestPackageService_Good_ListFiles(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -125,7 +125,7 @@ func TestPackageService_ListFiles_Good(t *testing.T) {
} }
} }
func TestPackageService_NotFound_Bad(t *testing.T) { func TestPackageService_Bad_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "package not found"}) json.NewEncoder(w).Encode(map[string]string{"message": "package not found"})

View file

@ -7,58 +7,19 @@ import (
"net/url" "net/url"
"strconv" "strconv"
core "dappco.re/go/core" coreerr "dappco.re/go/core/log"
) )
const defaultPageLimit = 50
// ListOptions controls pagination. // ListOptions controls pagination.
//
// Usage:
//
// opts := forge.ListOptions{Page: 1, Limit: 50}
// _ = opts
type ListOptions struct { type ListOptions struct {
Page int // 1-based page number Page int // 1-based page number
Limit int // items per page (default 50) Limit int // items per page (default 50)
} }
// String returns a safe summary of the pagination options. // DefaultList returns sensible default pagination.
// var DefaultList = ListOptions{Page: 1, Limit: 50}
// Usage:
//
// _ = forge.DefaultList.String()
func (o ListOptions) String() string {
return core.Concat(
"forge.ListOptions{page=",
strconv.Itoa(o.Page),
", limit=",
strconv.Itoa(o.Limit),
"}",
)
}
// GoString returns a safe Go-syntax summary of the pagination options.
//
// Usage:
//
// _ = fmt.Sprintf("%#v", forge.DefaultList)
func (o ListOptions) GoString() string { return o.String() }
// DefaultList provides sensible default pagination.
//
// Usage:
//
// page, err := forge.ListPage[types.Repository](ctx, client, path, nil, forge.DefaultList)
// _ = page
var DefaultList = ListOptions{Page: 1, Limit: defaultPageLimit}
// PagedResult holds a single page of results with metadata. // PagedResult holds a single page of results with metadata.
//
// Usage:
//
// page, err := forge.ListPage[types.Repository](ctx, client, path, nil, forge.DefaultList)
// _ = page
type PagedResult[T any] struct { type PagedResult[T any] struct {
Items []T Items []T
TotalCount int TotalCount int
@ -66,55 +27,19 @@ type PagedResult[T any] struct {
HasMore bool HasMore bool
} }
// String returns a safe summary of a page of results.
//
// Usage:
//
// page, _ := forge.ListPage[types.Repository](...)
// _ = page.String()
func (r PagedResult[T]) String() string {
items := 0
if r.Items != nil {
items = len(r.Items)
}
return core.Concat(
"forge.PagedResult{items=",
strconv.Itoa(items),
", totalCount=",
strconv.Itoa(r.TotalCount),
", page=",
strconv.Itoa(r.Page),
", hasMore=",
strconv.FormatBool(r.HasMore),
"}",
)
}
// GoString returns a safe Go-syntax summary of a page of results.
//
// Usage:
//
// _ = fmt.Sprintf("%#v", page)
func (r PagedResult[T]) GoString() string { return r.String() }
// ListPage fetches a single page of results. // ListPage fetches a single page of results.
// Extra query params can be passed via the query map. // Extra query params can be passed via the query map.
//
// Usage:
//
// page, err := forge.ListPage[types.Repository](ctx, client, "/api/v1/user/repos", nil, forge.DefaultList)
// _ = page
func ListPage[T any](ctx context.Context, c *Client, path string, query map[string]string, opts ListOptions) (*PagedResult[T], error) { func ListPage[T any](ctx context.Context, c *Client, path string, query map[string]string, opts ListOptions) (*PagedResult[T], error) {
if opts.Page < 1 { if opts.Page < 1 {
opts.Page = 1 opts.Page = 1
} }
if opts.Limit < 1 { if opts.Limit < 1 {
opts.Limit = defaultPageLimit opts.Limit = 50
} }
u, err := url.Parse(path) u, err := url.Parse(path)
if err != nil { if err != nil {
return nil, core.E("ListPage", "forge: parse path", err) return nil, coreerr.E("ListPage", "forge: parse path", err)
} }
q := u.Query() q := u.Query()
@ -145,17 +70,12 @@ func ListPage[T any](ctx context.Context, c *Client, path string, query map[stri
} }
// ListAll fetches all pages of results. // ListAll fetches all pages of results.
//
// Usage:
//
// items, err := forge.ListAll[types.Repository](ctx, client, "/api/v1/user/repos", nil)
// _ = items
func ListAll[T any](ctx context.Context, c *Client, path string, query map[string]string) ([]T, error) { func ListAll[T any](ctx context.Context, c *Client, path string, query map[string]string) ([]T, error) {
var all []T var all []T
page := 1 page := 1
for { for {
result, err := ListPage[T](ctx, c, path, query, ListOptions{Page: page, Limit: defaultPageLimit}) result, err := ListPage[T](ctx, c, path, query, ListOptions{Page: page, Limit: 50})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -170,17 +90,11 @@ func ListAll[T any](ctx context.Context, c *Client, path string, query map[strin
} }
// ListIter returns an iterator over all resources across all pages. // ListIter returns an iterator over all resources across all pages.
//
// Usage:
//
// for item, err := range forge.ListIter[types.Repository](ctx, client, "/api/v1/user/repos", nil) {
// _, _ = item, err
// }
func ListIter[T any](ctx context.Context, c *Client, path string, query map[string]string) iter.Seq2[T, error] { func ListIter[T any](ctx context.Context, c *Client, path string, query map[string]string) iter.Seq2[T, error] {
return func(yield func(T, error) bool) { return func(yield func(T, error) bool) {
page := 1 page := 1
for { for {
result, err := ListPage[T](ctx, c, path, query, ListOptions{Page: page, Limit: defaultPageLimit}) result, err := ListPage[T](ctx, c, path, query, ListOptions{Page: page, Limit: 50})
if err != nil { if err != nil {
yield(*new(T), err) yield(*new(T), err)
return return

View file

@ -2,13 +2,13 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
) )
func TestPagination_SinglePage_Good(t *testing.T) { func TestPagination_Good_SinglePage(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Total-Count", "2") w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]map[string]int{{"id": 1}, {"id": 2}}) json.NewEncoder(w).Encode([]map[string]int{{"id": 1}, {"id": 2}})
@ -25,7 +25,7 @@ func TestPagination_SinglePage_Good(t *testing.T) {
} }
} }
func TestPagination_MultiPage_Good(t *testing.T) { func TestPagination_Good_MultiPage(t *testing.T) {
page := 0 page := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
page++ page++
@ -48,7 +48,7 @@ func TestPagination_MultiPage_Good(t *testing.T) {
} }
} }
func TestPagination_EmptyResult_Good(t *testing.T) { func TestPagination_Good_EmptyResult(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Total-Count", "0") w.Header().Set("X-Total-Count", "0")
json.NewEncoder(w).Encode([]map[string]int{}) json.NewEncoder(w).Encode([]map[string]int{})
@ -65,7 +65,7 @@ func TestPagination_EmptyResult_Good(t *testing.T) {
} }
} }
func TestPagination_Iter_Good(t *testing.T) { func TestPagination_Good_Iter(t *testing.T) {
page := 0 page := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
page++ page++
@ -95,7 +95,7 @@ func TestPagination_Iter_Good(t *testing.T) {
} }
} }
func TestListPage_QueryParams_Good(t *testing.T) { func TestListPage_Good_QueryParams(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Query().Get("page") p := r.URL.Query().Get("page")
l := r.URL.Query().Get("limit") l := r.URL.Query().Get("limit")
@ -116,7 +116,7 @@ func TestListPage_QueryParams_Good(t *testing.T) {
} }
} }
func TestPagination_ServerError_Bad(t *testing.T) { func TestPagination_Bad_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500) w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{"message": "fail"}) json.NewEncoder(w).Encode(map[string]string{"message": "fail"})

View file

@ -2,68 +2,17 @@ package forge
import ( import (
"net/url" "net/url"
"sort"
"strconv"
"strings" "strings"
core "dappco.re/go/core"
) )
// Params maps path variable names to values. // Params maps path variable names to values.
// Example: Params{"owner": "core", "repo": "go-forge"} // Example: Params{"owner": "core", "repo": "go-forge"}
//
// Usage:
//
// params := forge.Params{"owner": "core", "repo": "go-forge"}
// _ = params
type Params map[string]string type Params map[string]string
// String returns a safe summary of the path parameters.
//
// Usage:
//
// _ = forge.Params{"owner": "core"}.String()
func (p Params) String() string {
if p == nil {
return "forge.Params{<nil>}"
}
keys := make([]string, 0, len(p))
for k := range p {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
b.WriteString("forge.Params{")
for i, k := range keys {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(k)
b.WriteString("=")
b.WriteString(strconv.Quote(p[k]))
}
b.WriteString("}")
return b.String()
}
// GoString returns a safe Go-syntax summary of the path parameters.
//
// Usage:
//
// _ = fmt.Sprintf("%#v", forge.Params{"owner": "core"})
func (p Params) GoString() string { return p.String() }
// ResolvePath substitutes {placeholders} in path with values from params. // ResolvePath substitutes {placeholders} in path with values from params.
//
// Usage:
//
// path := forge.ResolvePath("/api/v1/repos/{owner}/{repo}", forge.Params{"owner": "core", "repo": "go-forge"})
// _ = path
func ResolvePath(path string, params Params) string { func ResolvePath(path string, params Params) string {
for k, v := range params { for k, v := range params {
path = core.Replace(path, "{"+k+"}", url.PathEscape(v)) path = strings.ReplaceAll(path, "{"+k+"}", url.PathEscape(v))
} }
return path return path
} }

View file

@ -2,7 +2,7 @@ package forge
import "testing" import "testing"
func TestResolvePath_Simple_Good(t *testing.T) { func TestResolvePath_Good_Simple(t *testing.T) {
got := ResolvePath("/api/v1/repos/{owner}/{repo}", Params{"owner": "core", "repo": "go-forge"}) got := ResolvePath("/api/v1/repos/{owner}/{repo}", Params{"owner": "core", "repo": "go-forge"})
want := "/api/v1/repos/core/go-forge" want := "/api/v1/repos/core/go-forge"
if got != want { if got != want {
@ -10,14 +10,14 @@ func TestResolvePath_Simple_Good(t *testing.T) {
} }
} }
func TestResolvePath_NoParams_Good(t *testing.T) { func TestResolvePath_Good_NoParams(t *testing.T) {
got := ResolvePath("/api/v1/user", nil) got := ResolvePath("/api/v1/user", nil)
if got != "/api/v1/user" { if got != "/api/v1/user" {
t.Errorf("got %q", got) t.Errorf("got %q", got)
} }
} }
func TestResolvePath_WithID_Good(t *testing.T) { func TestResolvePath_Good_WithID(t *testing.T) {
got := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}", Params{ got := ResolvePath("/api/v1/repos/{owner}/{repo}/issues/{index}", Params{
"owner": "core", "repo": "go-forge", "index": "42", "owner": "core", "repo": "go-forge", "index": "42",
}) })
@ -27,7 +27,7 @@ func TestResolvePath_WithID_Good(t *testing.T) {
} }
} }
func TestResolvePath_URLEncoding_Good(t *testing.T) { func TestResolvePath_Good_URLEncoding(t *testing.T) {
got := ResolvePath("/api/v1/repos/{owner}/{repo}", Params{"owner": "my org", "repo": "my repo"}) got := ResolvePath("/api/v1/repos/{owner}/{repo}", Params{"owner": "my org", "repo": "my repo"})
want := "/api/v1/repos/my%20org/my%20repo" want := "/api/v1/repos/my%20org/my%20repo"
if got != want { if got != want {

303
pulls.go
View file

@ -2,71 +2,17 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"net/url"
"strconv"
core "dappco.re/go/core"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// PullService handles pull request operations within a repository. // PullService handles pull request operations within a repository.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Pulls.ListAll(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
type PullService struct { type PullService struct {
Resource[types.PullRequest, types.CreatePullRequestOption, types.EditPullRequestOption] Resource[types.PullRequest, types.CreatePullRequestOption, types.EditPullRequestOption]
} }
// PullListOptions controls filtering for repository pull request listings.
//
// Usage:
//
// opts := forge.PullListOptions{State: "open", Labels: []int64{1, 2}}
type PullListOptions struct {
State string
Sort string
Milestone int64
Labels []int64
Poster string
}
// String returns a safe summary of the pull request list filters.
func (o PullListOptions) String() string {
return optionString("forge.PullListOptions",
"state", o.State,
"sort", o.Sort,
"milestone", o.Milestone,
"labels", o.Labels,
"poster", o.Poster,
)
}
// GoString returns a safe Go-syntax summary of the pull request list filters.
func (o PullListOptions) GoString() string { return o.String() }
func (o PullListOptions) addQuery(values url.Values) {
if o.State != "" {
values.Set("state", o.State)
}
if o.Sort != "" {
values.Set("sort", o.Sort)
}
if o.Milestone != 0 {
values.Set("milestone", strconv.FormatInt(o.Milestone, 10))
}
for _, label := range o.Labels {
if label != 0 {
values.Add("labels", strconv.FormatInt(label, 10))
}
}
if o.Poster != "" {
values.Set("poster", o.Poster)
}
}
func newPullService(c *Client) *PullService { func newPullService(c *Client) *PullService {
return &PullService{ return &PullService{
Resource: *NewResource[types.PullRequest, types.CreatePullRequestOption, types.EditPullRequestOption]( Resource: *NewResource[types.PullRequest, types.CreatePullRequestOption, types.EditPullRequestOption](
@ -75,132 +21,34 @@ func newPullService(c *Client) *PullService {
} }
} }
// ListPullRequests returns all pull requests in a repository.
func (s *PullService) ListPullRequests(ctx context.Context, owner, repo string, filters ...PullListOptions) ([]types.PullRequest, error) {
return s.listAll(ctx, owner, repo, filters...)
}
// IterPullRequests returns an iterator over all pull requests in a repository.
func (s *PullService) IterPullRequests(ctx context.Context, owner, repo string, filters ...PullListOptions) iter.Seq2[types.PullRequest, error] {
return s.listIter(ctx, owner, repo, filters...)
}
// CreatePullRequest creates a pull request in a repository.
func (s *PullService) CreatePullRequest(ctx context.Context, owner, repo string, opts *types.CreatePullRequestOption) (*types.PullRequest, error) {
var out types.PullRequest
if err := s.client.Post(ctx, ResolvePath("/api/v1/repos/{owner}/{repo}/pulls", pathParams("owner", owner, "repo", repo)), opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// Merge merges a pull request. Method is one of "merge", "rebase", "rebase-merge", "squash", "fast-forward-only", "manually-merged". // Merge merges a pull request. Method is one of "merge", "rebase", "rebase-merge", "squash", "fast-forward-only", "manually-merged".
func (s *PullService) Merge(ctx context.Context, owner, repo string, index int64, method string) error { func (s *PullService) Merge(ctx context.Context, owner, repo string, index int64, method string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/merge", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
body := map[string]string{"Do": method} body := map[string]string{"Do": method}
return s.client.Post(ctx, path, body, nil) return s.client.Post(ctx, path, body, nil)
} }
// CancelScheduledAutoMerge cancels the scheduled auto merge for a pull request.
func (s *PullService) CancelScheduledAutoMerge(ctx context.Context, owner, repo string, index int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/merge", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return s.client.Delete(ctx, path)
}
// Update updates a pull request branch with the base branch. // Update updates a pull request branch with the base branch.
func (s *PullService) Update(ctx context.Context, owner, repo string, index int64) error { func (s *PullService) Update(ctx context.Context, owner, repo string, index int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/update", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/update", owner, repo, index)
return s.client.Post(ctx, path, nil, nil) return s.client.Post(ctx, path, nil, nil)
} }
// GetDiffOrPatch returns a pull request diff or patch as raw bytes.
func (s *PullService) GetDiffOrPatch(ctx context.Context, owner, repo string, index int64, diffType string) ([]byte, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}.{diffType}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "diffType", diffType))
return s.client.GetRaw(ctx, path)
}
// ListCommits returns all commits for a pull request.
func (s *PullService) ListCommits(ctx context.Context, owner, repo string, index int64) ([]types.Commit, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/commits", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListAll[types.Commit](ctx, s.client, path, nil)
}
// IterCommits returns an iterator over all commits for a pull request.
func (s *PullService) IterCommits(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.Commit, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/commits", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListIter[types.Commit](ctx, s.client, path, nil)
}
// ListReviews returns all reviews on a pull request. // ListReviews returns all reviews on a pull request.
func (s *PullService) ListReviews(ctx context.Context, owner, repo string, index int64) ([]types.PullReview, error) { func (s *PullService) ListReviews(ctx context.Context, owner, repo string, index int64) ([]types.PullReview, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
return ListAll[types.PullReview](ctx, s.client, path, nil) return ListAll[types.PullReview](ctx, s.client, path, nil)
} }
// IterReviews returns an iterator over all reviews on a pull request. // IterReviews returns an iterator over all reviews on a pull request.
func (s *PullService) IterReviews(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.PullReview, error] { func (s *PullService) IterReviews(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.PullReview, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
return ListIter[types.PullReview](ctx, s.client, path, nil) return ListIter[types.PullReview](ctx, s.client, path, nil)
} }
// ListFiles returns all changed files on a pull request.
func (s *PullService) ListFiles(ctx context.Context, owner, repo string, index int64) ([]types.ChangedFile, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/files", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListAll[types.ChangedFile](ctx, s.client, path, nil)
}
// IterFiles returns an iterator over all changed files on a pull request.
func (s *PullService) IterFiles(ctx context.Context, owner, repo string, index int64) iter.Seq2[types.ChangedFile, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/files", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return ListIter[types.ChangedFile](ctx, s.client, path, nil)
}
// GetByBaseHead returns a pull request for a given base and head branch pair.
func (s *PullService) GetByBaseHead(ctx context.Context, owner, repo, base, head string) (*types.PullRequest, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{base}/{head}", pathParams(
"owner", owner,
"repo", repo,
"base", base,
"head", head,
))
var out types.PullRequest
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// ListReviewers returns all users who can be requested to review a pull request.
func (s *PullService) ListReviewers(ctx context.Context, owner, repo string) ([]types.User, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/reviewers", pathParams("owner", owner, "repo", repo))
return ListAll[types.User](ctx, s.client, path, nil)
}
// IterReviewers returns an iterator over all users who can be requested to review a pull request.
func (s *PullService) IterReviewers(ctx context.Context, owner, repo string) iter.Seq2[types.User, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/reviewers", pathParams("owner", owner, "repo", repo))
return ListIter[types.User](ctx, s.client, path, nil)
}
// RequestReviewers creates review requests for a pull request.
func (s *PullService) RequestReviewers(ctx context.Context, owner, repo string, index int64, opts *types.PullReviewRequestOptions) ([]types.PullReview, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/requested_reviewers", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
var out []types.PullReview
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
}
return out, nil
}
// CancelReviewRequests cancels review requests for a pull request.
func (s *PullService) CancelReviewRequests(ctx context.Context, owner, repo string, index int64, opts *types.PullReviewRequestOptions) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/requested_reviewers", pathParams("owner", owner, "repo", repo, "index", int64String(index)))
return s.client.DeleteWithBody(ctx, path, opts)
}
// SubmitReview creates a new review on a pull request. // SubmitReview creates a new review on a pull request.
func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, index int64, review *types.SubmitPullReviewOptions) (*types.PullReview, error) { func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, index int64, review map[string]any) (*types.PullReview, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews", pathParams("owner", owner, "repo", repo, "index", int64String(index))) path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
var out types.PullReview var out types.PullReview
if err := s.client.Post(ctx, path, review, &out); err != nil { if err := s.client.Post(ctx, path, review, &out); err != nil {
return nil, err return nil, err
@ -208,148 +56,15 @@ func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, inde
return &out, nil return &out, nil
} }
// GetReview returns a single pull request review.
func (s *PullService) GetReview(ctx context.Context, owner, repo string, index, reviewID int64) (*types.PullReview, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID)))
var out types.PullReview
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteReview deletes a pull request review.
func (s *PullService) DeleteReview(ctx context.Context, owner, repo string, index, reviewID int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID)))
return s.client.Delete(ctx, path)
}
func (s *PullService) listPage(ctx context.Context, owner, repo string, opts ListOptions, filters ...PullListOptions) (*PagedResult[types.PullRequest], error) {
if opts.Page < 1 {
opts.Page = 1
}
if opts.Limit < 1 {
opts.Limit = defaultPageLimit
}
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls", pathParams("owner", owner, "repo", repo))
u, err := url.Parse(path)
if err != nil {
return nil, core.E("PullService.listPage", "forge: parse path", err)
}
values := u.Query()
values.Set("page", strconv.Itoa(opts.Page))
values.Set("limit", strconv.Itoa(opts.Limit))
for _, filter := range filters {
filter.addQuery(values)
}
u.RawQuery = values.Encode()
var items []types.PullRequest
resp, err := s.client.doJSON(ctx, "GET", u.String(), nil, &items)
if err != nil {
return nil, err
}
totalCount, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
return &PagedResult[types.PullRequest]{
Items: items,
TotalCount: totalCount,
Page: opts.Page,
HasMore: (totalCount > 0 && (opts.Page-1)*opts.Limit+len(items) < totalCount) ||
(totalCount == 0 && len(items) >= opts.Limit),
}, nil
}
func (s *PullService) listAll(ctx context.Context, owner, repo string, filters ...PullListOptions) ([]types.PullRequest, error) {
var all []types.PullRequest
page := 1
for {
result, err := s.listPage(ctx, owner, repo, ListOptions{Page: page, Limit: defaultPageLimit}, filters...)
if err != nil {
return nil, err
}
all = append(all, result.Items...)
if !result.HasMore {
break
}
page++
}
return all, nil
}
func (s *PullService) listIter(ctx context.Context, owner, repo string, filters ...PullListOptions) iter.Seq2[types.PullRequest, error] {
return func(yield func(types.PullRequest, error) bool) {
page := 1
for {
result, err := s.listPage(ctx, owner, repo, ListOptions{Page: page, Limit: defaultPageLimit}, filters...)
if err != nil {
yield(*new(types.PullRequest), err)
return
}
for _, item := range result.Items {
if !yield(item, nil) {
return
}
}
if !result.HasMore {
break
}
page++
}
}
}
// ListReviewComments returns all comments on a pull request review.
func (s *PullService) ListReviewComments(ctx context.Context, owner, repo string, index, reviewID int64) ([]types.PullReviewComment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID)))
return ListAll[types.PullReviewComment](ctx, s.client, path, nil)
}
// IterReviewComments returns an iterator over all comments on a pull request review.
func (s *PullService) IterReviewComments(ctx context.Context, owner, repo string, index, reviewID int64) iter.Seq2[types.PullReviewComment, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID)))
return ListIter[types.PullReviewComment](ctx, s.client, path, nil)
}
// GetReviewComment returns a single comment on a pull request review.
func (s *PullService) GetReviewComment(ctx context.Context, owner, repo string, index, reviewID, commentID int64) (*types.PullReviewComment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID), "comment", int64String(commentID)))
var out types.PullReviewComment
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// CreateReviewComment creates a new comment on a pull request review.
func (s *PullService) CreateReviewComment(ctx context.Context, owner, repo string, index, reviewID int64, opts *types.CreatePullReviewCommentOptions) (*types.PullReviewComment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID)))
var out types.PullReviewComment
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// DeleteReviewComment deletes a comment on a pull request review.
func (s *PullService) DeleteReviewComment(ctx context.Context, owner, repo string, index, reviewID, commentID int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment}", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID), "comment", int64String(commentID)))
return s.client.Delete(ctx, path)
}
// DismissReview dismisses a pull request review. // DismissReview dismisses a pull request review.
func (s *PullService) DismissReview(ctx context.Context, owner, repo string, index, reviewID int64, msg string) error { func (s *PullService) DismissReview(ctx context.Context, owner, repo string, index, reviewID int64, msg string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/dismissals", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID))) path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals", owner, repo, index, reviewID)
body := map[string]string{"message": msg} body := map[string]string{"message": msg}
return s.client.Post(ctx, path, body, nil) return s.client.Post(ctx, path, body, nil)
} }
// UndismissReview undismisses a pull request review. // UndismissReview undismisses a pull request review.
func (s *PullService) UndismissReview(ctx context.Context, owner, repo string, index, reviewID int64) error { func (s *PullService) UndismissReview(ctx context.Context, owner, repo string, index, reviewID int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/undismissals", pathParams("owner", owner, "repo", repo, "index", int64String(index), "id", int64String(reviewID))) path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals", owner, repo, index, reviewID)
return s.client.Post(ctx, path, nil, nil) return s.client.Post(ctx, path, nil, nil)
} }

View file

@ -1,67 +0,0 @@
package forge
import (
"context"
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"testing"
"dappco.re/go/core/forge/types"
)
func TestPullService_ListPullRequests_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/pulls" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.PullRequest{{ID: 1, Title: "add feature"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
prs, err := f.Pulls.ListPullRequests(context.Background(), "core", "go-forge")
if err != nil {
t.Fatal(err)
}
if len(prs) != 1 || prs[0].Title != "add feature" {
t.Fatalf("got %#v", prs)
}
}
func TestPullService_CreatePullRequest_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/pulls" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var body types.CreatePullRequestOption
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.Title != "add feature" {
t.Fatalf("unexpected body: %+v", body)
}
json.NewEncoder(w).Encode(types.PullRequest{ID: 1, Title: body.Title, Index: 1})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
pr, err := f.Pulls.CreatePullRequest(context.Background(), "core", "go-forge", &types.CreatePullRequestOption{
Title: "add feature",
Base: "main",
Head: "feature",
})
if err != nil {
t.Fatal(err)
}
if pr.Title != "add feature" {
t.Fatalf("got title=%q", pr.Title)
}
}

View file

@ -2,16 +2,15 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"testing" "testing"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestPullService_List_Good(t *testing.T) { func TestPullService_Good_List(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -42,54 +41,7 @@ func TestPullService_List_Good(t *testing.T) {
} }
} }
func TestPullService_ListFiltered_Good(t *testing.T) { func TestPullService_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/pulls" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
want := map[string]string{
"state": "open",
"sort": "priority",
"milestone": "7",
"poster": "alice",
"page": "1",
"limit": "50",
}
for key, wantValue := range want {
if got := r.URL.Query().Get(key); got != wantValue {
t.Errorf("got %s=%q, want %q", key, got, wantValue)
}
}
if got := r.URL.Query()["labels"]; !reflect.DeepEqual(got, []string{"1", "2"}) {
t.Errorf("got labels=%v, want %v", got, []string{"1", "2"})
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.PullRequest{{ID: 1, Title: "add feature"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
prs, err := f.Pulls.ListPullRequests(context.Background(), "core", "go-forge", PullListOptions{
State: "open",
Sort: "priority",
Milestone: 7,
Labels: []int64{1, 2},
Poster: "alice",
})
if err != nil {
t.Fatal(err)
}
if len(prs) != 1 || prs[0].Title != "add feature" {
t.Fatalf("got %#v", prs)
}
}
func TestPullService_Get_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -111,7 +63,7 @@ func TestPullService_Get_Good(t *testing.T) {
} }
} }
func TestPullService_Create_Good(t *testing.T) { func TestPullService_Good_Create(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -142,248 +94,7 @@ func TestPullService_Create_Good(t *testing.T) {
} }
} }
func TestPullService_ListReviewers_Good(t *testing.T) { func TestPullService_Good_Merge(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/reviewers" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
json.NewEncoder(w).Encode([]types.User{
{UserName: "alice"},
{UserName: "bob"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
reviewers, err := f.Pulls.ListReviewers(context.Background(), "core", "go-forge")
if err != nil {
t.Fatal(err)
}
if len(reviewers) != 2 || reviewers[0].UserName != "alice" || reviewers[1].UserName != "bob" {
t.Fatalf("got %#v", reviewers)
}
}
func TestPullService_ListFiles_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/pulls/7/files" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.ChangedFile{
{Filename: "README.md", Status: "modified", Additions: 2, Deletions: 1},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
files, err := f.Pulls.ListFiles(context.Background(), "core", "go-forge", 7)
if err != nil {
t.Fatal(err)
}
if len(files) != 1 {
t.Fatalf("got %d files, want 1", len(files))
}
if files[0].Filename != "README.md" || files[0].Status != "modified" {
t.Fatalf("got %#v", files[0])
}
}
func TestPullService_GetByBaseHead_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/pulls/main/feature" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
json.NewEncoder(w).Encode(types.PullRequest{Index: 7, Title: "Add feature"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
pr, err := f.Pulls.GetByBaseHead(context.Background(), "core", "go-forge", "main", "feature")
if err != nil {
t.Fatal(err)
}
if pr.Index != 7 || pr.Title != "Add feature" {
t.Fatalf("got %+v", pr)
}
}
func TestPullService_IterFiles_Good(t *testing.T) {
requests := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/pulls/7/files" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
switch requests {
case 1:
if got := r.URL.Query().Get("page"); got != "1" {
t.Errorf("got page=%q, want %q", got, "1")
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.ChangedFile{{Filename: "README.md", Status: "modified"}})
case 2:
if got := r.URL.Query().Get("page"); got != "2" {
t.Errorf("got page=%q, want %q", got, "2")
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.ChangedFile{{Filename: "docs/guide.md", Status: "added"}})
default:
t.Fatalf("unexpected request %d", requests)
}
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var got []string
for file, err := range f.Pulls.IterFiles(context.Background(), "core", "go-forge", 7) {
if err != nil {
t.Fatal(err)
}
got = append(got, file.Filename)
}
if len(got) != 2 || got[0] != "README.md" || got[1] != "docs/guide.md" {
t.Fatalf("got %#v", got)
}
}
func TestPullService_IterReviewers_Good(t *testing.T) {
requests := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/reviewers" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
switch requests {
case 1:
if got := r.URL.Query().Get("page"); got != "1" {
t.Errorf("got page=%q, want %q", got, "1")
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.User{{UserName: "alice"}})
case 2:
if got := r.URL.Query().Get("page"); got != "2" {
t.Errorf("got page=%q, want %q", got, "2")
}
w.Header().Set("X-Total-Count", "2")
json.NewEncoder(w).Encode([]types.User{{UserName: "bob"}})
default:
t.Fatalf("unexpected request %d", requests)
}
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
var got []string
for reviewer, err := range f.Pulls.IterReviewers(context.Background(), "core", "go-forge") {
if err != nil {
t.Fatal(err)
}
got = append(got, reviewer.UserName)
}
if len(got) != 2 || got[0] != "alice" || got[1] != "bob" {
t.Fatalf("got %#v", got)
}
}
func TestPullService_RequestReviewers_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/pulls/7/requested_reviewers" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
var body types.PullReviewRequestOptions
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode body: %v", err)
}
if len(body.Reviewers) != 2 || body.Reviewers[0] != "alice" || body.Reviewers[1] != "bob" {
t.Fatalf("got reviewers %#v", body.Reviewers)
}
if len(body.TeamReviewers) != 1 || body.TeamReviewers[0] != "platform" {
t.Fatalf("got team reviewers %#v", body.TeamReviewers)
}
json.NewEncoder(w).Encode([]types.PullReview{
{ID: 101, Body: "requested"},
})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
reviews, err := f.Pulls.RequestReviewers(context.Background(), "core", "go-forge", 7, &types.PullReviewRequestOptions{
Reviewers: []string{"alice", "bob"},
TeamReviewers: []string{"platform"},
})
if err != nil {
t.Fatal(err)
}
if len(reviews) != 1 || reviews[0].ID != 101 || reviews[0].Body != "requested" {
t.Fatalf("got %#v", reviews)
}
}
func TestPullService_CancelReviewRequests_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/pulls/7/requested_reviewers" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
var body types.PullReviewRequestOptions
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode body: %v", err)
}
if len(body.Reviewers) != 1 || body.Reviewers[0] != "alice" {
t.Fatalf("got reviewers %#v", body.Reviewers)
}
if len(body.TeamReviewers) != 1 || body.TeamReviewers[0] != "platform" {
t.Fatalf("got team reviewers %#v", body.TeamReviewers)
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Pulls.CancelReviewRequests(context.Background(), "core", "go-forge", 7, &types.PullReviewRequestOptions{
Reviewers: []string{"alice"},
TeamReviewers: []string{"platform"},
}); err != nil {
t.Fatal(err)
}
}
func TestPullService_Merge_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -407,7 +118,7 @@ func TestPullService_Merge_Good(t *testing.T) {
} }
} }
func TestPullService_Merge_Bad(t *testing.T) { func TestPullService_Bad_Merge(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusConflict) w.WriteHeader(http.StatusConflict)
json.NewEncoder(w).Encode(map[string]string{"message": "already merged"}) json.NewEncoder(w).Encode(map[string]string{"message": "already merged"})
@ -419,23 +130,3 @@ func TestPullService_Merge_Bad(t *testing.T) {
t.Fatalf("expected conflict, got %v", err) t.Fatalf("expected conflict, got %v", err)
} }
} }
func TestPullService_CancelScheduledAutoMerge_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/pulls/7/merge" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := f.Pulls.CancelScheduledAutoMerge(context.Background(), "core", "go-forge", 7); err != nil {
t.Fatal(err)
}
}

View file

@ -2,96 +2,17 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"strconv"
goio "io"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// ReleaseService handles release operations within a repository. // ReleaseService handles release operations within a repository.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Releases.ListAll(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
type ReleaseService struct { type ReleaseService struct {
Resource[types.Release, types.CreateReleaseOption, types.EditReleaseOption] Resource[types.Release, types.CreateReleaseOption, types.EditReleaseOption]
} }
// ReleaseListOptions controls filtering for repository release listings.
//
// Usage:
//
// opts := forge.ReleaseListOptions{Draft: true, Query: "1.0"}
type ReleaseListOptions struct {
Draft bool
PreRelease bool
Query string
}
// String returns a safe summary of the release list filters.
func (o ReleaseListOptions) String() string {
return optionString("forge.ReleaseListOptions",
"draft", o.Draft,
"pre-release", o.PreRelease,
"q", o.Query,
)
}
// GoString returns a safe Go-syntax summary of the release list filters.
func (o ReleaseListOptions) GoString() string { return o.String() }
func (o ReleaseListOptions) queryParams() map[string]string {
query := make(map[string]string, 3)
if o.Draft {
query["draft"] = strconv.FormatBool(true)
}
if o.PreRelease {
query["pre-release"] = strconv.FormatBool(true)
}
if o.Query != "" {
query["q"] = o.Query
}
if len(query) == 0 {
return nil
}
return query
}
// ReleaseAttachmentUploadOptions controls metadata sent when uploading a release attachment.
//
// Usage:
//
// opts := forge.ReleaseAttachmentUploadOptions{Name: "release.zip"}
type ReleaseAttachmentUploadOptions struct {
Name string
ExternalURL string
}
// String returns a safe summary of the release attachment upload metadata.
func (o ReleaseAttachmentUploadOptions) String() string {
return optionString("forge.ReleaseAttachmentUploadOptions",
"name", o.Name,
"external_url", o.ExternalURL,
)
}
// GoString returns a safe Go-syntax summary of the release attachment upload metadata.
func (o ReleaseAttachmentUploadOptions) GoString() string { return o.String() }
func releaseAttachmentUploadQuery(opts *ReleaseAttachmentUploadOptions) map[string]string {
if opts == nil || opts.Name == "" {
return nil
}
query := make(map[string]string, 1)
if opts.Name != "" {
query["name"] = opts.Name
}
return query
}
func newReleaseService(c *Client) *ReleaseService { func newReleaseService(c *Client) *ReleaseService {
return &ReleaseService{ return &ReleaseService{
Resource: *NewResource[types.Release, types.CreateReleaseOption, types.EditReleaseOption]( Resource: *NewResource[types.Release, types.CreateReleaseOption, types.EditReleaseOption](
@ -100,40 +21,9 @@ func newReleaseService(c *Client) *ReleaseService {
} }
} }
// ListReleases returns all releases in a repository.
func (s *ReleaseService) ListReleases(ctx context.Context, owner, repo string, filters ...ReleaseListOptions) ([]types.Release, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases", pathParams("owner", owner, "repo", repo))
return ListAll[types.Release](ctx, s.client, path, releaseListQuery(filters...))
}
// IterReleases returns an iterator over all releases in a repository.
func (s *ReleaseService) IterReleases(ctx context.Context, owner, repo string, filters ...ReleaseListOptions) iter.Seq2[types.Release, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases", pathParams("owner", owner, "repo", repo))
return ListIter[types.Release](ctx, s.client, path, releaseListQuery(filters...))
}
// CreateRelease creates a release in a repository.
func (s *ReleaseService) CreateRelease(ctx context.Context, owner, repo string, opts *types.CreateReleaseOption) (*types.Release, error) {
var out types.Release
if err := s.client.Post(ctx, ResolvePath("/api/v1/repos/{owner}/{repo}/releases", pathParams("owner", owner, "repo", repo)), opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// GetByTag returns a release by its tag name. // GetByTag returns a release by its tag name.
func (s *ReleaseService) GetByTag(ctx context.Context, owner, repo, tag string) (*types.Release, error) { func (s *ReleaseService) GetByTag(ctx context.Context, owner, repo, tag string) (*types.Release, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/tags/{tag}", pathParams("owner", owner, "repo", repo, "tag", tag)) path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner, repo, tag)
var out types.Release
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// GetLatest returns the most recent non-prerelease, non-draft release.
func (s *ReleaseService) GetLatest(ctx context.Context, owner, repo string) (*types.Release, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/latest", pathParams("owner", owner, "repo", repo))
var out types.Release var out types.Release
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -143,66 +33,25 @@ func (s *ReleaseService) GetLatest(ctx context.Context, owner, repo string) (*ty
// DeleteByTag deletes a release by its tag name. // DeleteByTag deletes a release by its tag name.
func (s *ReleaseService) DeleteByTag(ctx context.Context, owner, repo, tag string) error { func (s *ReleaseService) DeleteByTag(ctx context.Context, owner, repo, tag string) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/tags/{tag}", pathParams("owner", owner, "repo", repo, "tag", tag)) path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner, repo, tag)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// ListAssets returns all assets for a release. // ListAssets returns all assets for a release.
func (s *ReleaseService) ListAssets(ctx context.Context, owner, repo string, releaseID int64) ([]types.Attachment, error) { func (s *ReleaseService) ListAssets(ctx context.Context, owner, repo string, releaseID int64) ([]types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{id}/assets", pathParams("owner", owner, "repo", repo, "id", int64String(releaseID))) path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner, repo, releaseID)
return ListAll[types.Attachment](ctx, s.client, path, nil) return ListAll[types.Attachment](ctx, s.client, path, nil)
} }
// CreateAttachment uploads a new attachment to a release.
//
// If opts.ExternalURL is set, the upload uses the external_url form field and
// ignores filename/content.
func (s *ReleaseService) CreateAttachment(ctx context.Context, owner, repo string, releaseID int64, opts *ReleaseAttachmentUploadOptions, filename string, content goio.Reader) (*types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{id}/assets", pathParams("owner", owner, "repo", repo, "id", int64String(releaseID)))
fields := make(map[string]string, 1)
fieldName := "attachment"
if opts != nil && opts.ExternalURL != "" {
fields["external_url"] = opts.ExternalURL
fieldName = ""
filename = ""
content = nil
}
var out types.Attachment
if err := s.client.postMultipartJSON(ctx, path, releaseAttachmentUploadQuery(opts), fields, fieldName, filename, content, &out); err != nil {
return nil, err
}
return &out, nil
}
// EditAttachment updates a release attachment.
func (s *ReleaseService) EditAttachment(ctx context.Context, owner, repo string, releaseID, attachmentID int64, opts *types.EditAttachmentOptions) (*types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{id}/assets/{attachment_id}", pathParams("owner", owner, "repo", repo, "id", int64String(releaseID), "attachment_id", int64String(attachmentID)))
var out types.Attachment
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// CreateAsset uploads a new asset to a release.
func (s *ReleaseService) CreateAsset(ctx context.Context, owner, repo string, releaseID int64, opts *ReleaseAttachmentUploadOptions, filename string, content goio.Reader) (*types.Attachment, error) {
return s.CreateAttachment(ctx, owner, repo, releaseID, opts, filename, content)
}
// EditAsset updates a release asset.
func (s *ReleaseService) EditAsset(ctx context.Context, owner, repo string, releaseID, attachmentID int64, opts *types.EditAttachmentOptions) (*types.Attachment, error) {
return s.EditAttachment(ctx, owner, repo, releaseID, attachmentID, opts)
}
// IterAssets returns an iterator over all assets for a release. // IterAssets returns an iterator over all assets for a release.
func (s *ReleaseService) IterAssets(ctx context.Context, owner, repo string, releaseID int64) iter.Seq2[types.Attachment, error] { func (s *ReleaseService) IterAssets(ctx context.Context, owner, repo string, releaseID int64) iter.Seq2[types.Attachment, error] {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{id}/assets", pathParams("owner", owner, "repo", repo, "id", int64String(releaseID))) path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner, repo, releaseID)
return ListIter[types.Attachment](ctx, s.client, path, nil) return ListIter[types.Attachment](ctx, s.client, path, nil)
} }
// GetAsset returns a single asset for a release. // GetAsset returns a single asset for a release.
func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) (*types.Attachment, error) { func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) (*types.Attachment, error) {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{releaseID}/assets/{assetID}", pathParams("owner", owner, "repo", repo, "releaseID", int64String(releaseID), "assetID", int64String(assetID))) path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", owner, repo, releaseID, assetID)
var out types.Attachment var out types.Attachment
if err := s.client.Get(ctx, path, &out); err != nil { if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err return nil, err
@ -212,19 +61,6 @@ func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, relea
// DeleteAsset deletes a single asset from a release. // DeleteAsset deletes a single asset from a release.
func (s *ReleaseService) DeleteAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) error { func (s *ReleaseService) DeleteAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) error {
path := ResolvePath("/api/v1/repos/{owner}/{repo}/releases/{releaseID}/assets/{assetID}", pathParams("owner", owner, "repo", repo, "releaseID", int64String(releaseID), "assetID", int64String(assetID))) path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", owner, repo, releaseID, assetID)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
func releaseListQuery(filters ...ReleaseListOptions) map[string]string {
query := make(map[string]string, len(filters))
for _, filter := range filters {
for key, value := range filter.queryParams() {
query[key] = value
}
}
if len(query) == 0 {
return nil
}
return query
}

View file

@ -1,66 +0,0 @@
package forge
import (
"context"
json "github.com/goccy/go-json"
"net/http"
"net/http/httptest"
"testing"
"dappco.re/go/core/forge/types"
)
func TestReleaseService_ListReleases_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/releases" {
t.Errorf("wrong path: %s", r.URL.Path)
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Release{{ID: 1, TagName: "v1.0.0"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
releases, err := f.Releases.ListReleases(context.Background(), "core", "go-forge")
if err != nil {
t.Fatal(err)
}
if len(releases) != 1 || releases[0].TagName != "v1.0.0" {
t.Fatalf("got %#v", releases)
}
}
func TestReleaseService_CreateRelease_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/releases" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var body types.CreateReleaseOption
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body.TagName != "v1.0.0" {
t.Fatalf("unexpected body: %+v", body)
}
json.NewEncoder(w).Encode(types.Release{ID: 1, TagName: body.TagName})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
release, err := f.Releases.CreateRelease(context.Background(), "core", "go-forge", &types.CreateReleaseOption{
TagName: "v1.0.0",
Title: "Release 1.0",
})
if err != nil {
t.Fatal(err)
}
if release.TagName != "v1.0.0" {
t.Fatalf("got tag=%q", release.TagName)
}
}

View file

@ -1,64 +1,16 @@
package forge package forge
import ( import (
"bytes"
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"io"
"mime"
"mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"testing" "testing"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func readMultipartReleaseAttachment(t *testing.T, r *http.Request) (map[string]string, string, string) { func TestReleaseService_Good_List(t *testing.T) {
t.Helper()
mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
t.Fatal(err)
}
if mediaType != "multipart/form-data" {
t.Fatalf("got content-type=%q", mediaType)
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
fields := make(map[string]string)
reader := multipart.NewReader(bytes.NewReader(body), params["boundary"])
var fileName string
var fileContent string
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
data, err := io.ReadAll(part)
if err != nil {
t.Fatal(err)
}
if part.FormName() == "attachment" {
fileName = part.FileName()
fileContent = string(data)
continue
}
fields[part.FormName()] = string(data)
}
return fields, fileName, fileContent
}
func TestReleaseService_List_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -84,48 +36,7 @@ func TestReleaseService_List_Good(t *testing.T) {
} }
} }
func TestReleaseService_ListFiltered_Good(t *testing.T) { func TestReleaseService_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/releases" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
want := map[string]string{
"draft": "true",
"pre-release": "true",
"q": "1.0",
"page": "1",
"limit": "50",
}
for key, wantValue := range want {
if got := r.URL.Query().Get(key); got != wantValue {
t.Errorf("got %s=%q, want %q", key, got, wantValue)
}
}
w.Header().Set("X-Total-Count", "1")
json.NewEncoder(w).Encode([]types.Release{{ID: 1, TagName: "v1.0.0", Title: "Release 1.0"}})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
releases, err := f.Releases.ListReleases(context.Background(), "core", "go-forge", ReleaseListOptions{
Draft: true,
PreRelease: true,
Query: "1.0",
})
if err != nil {
t.Fatal(err)
}
if len(releases) != 1 || releases[0].TagName != "v1.0.0" {
t.Fatalf("got %#v", releases)
}
}
func TestReleaseService_Get_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -150,7 +61,7 @@ func TestReleaseService_Get_Good(t *testing.T) {
} }
} }
func TestReleaseService_GetByTag_Good(t *testing.T) { func TestReleaseService_Good_GetByTag(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -174,146 +85,3 @@ func TestReleaseService_GetByTag_Good(t *testing.T) {
t.Errorf("got id=%d, want 1", release.ID) t.Errorf("got id=%d, want 1", release.ID)
} }
} }
func TestReleaseService_GetLatest_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/releases/latest" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.Release{ID: 3, TagName: "v2.1.0", Title: "Latest Release"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
release, err := f.Releases.GetLatest(context.Background(), "core", "go-forge")
if err != nil {
t.Fatal(err)
}
if release.TagName != "v2.1.0" {
t.Errorf("got tag=%q, want %q", release.TagName, "v2.1.0")
}
if release.Title != "Latest Release" {
t.Errorf("got title=%q, want %q", release.Title, "Latest Release")
}
}
func TestReleaseService_CreateAttachment_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/releases/1/assets" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
if got := r.URL.Query().Get("name"); got != "linux-amd64" {
t.Fatalf("got name=%q", got)
}
fields, filename, content := readMultipartReleaseAttachment(t, r)
if !reflect.DeepEqual(fields, map[string]string{}) {
t.Fatalf("got fields=%#v", fields)
}
if filename != "release.tar.gz" {
t.Fatalf("got filename=%q", filename)
}
if content != "release bytes" {
t.Fatalf("got content=%q", content)
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(types.Attachment{ID: 9, Name: filename})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
attachment, err := f.Releases.CreateAttachment(
context.Background(),
"core",
"go-forge",
1,
&ReleaseAttachmentUploadOptions{Name: "linux-amd64"},
"release.tar.gz",
bytes.NewBufferString("release bytes"),
)
if err != nil {
t.Fatal(err)
}
if attachment.Name != "release.tar.gz" {
t.Fatalf("got name=%q", attachment.Name)
}
}
func TestReleaseService_CreateAttachmentExternalURL_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/releases/1/assets" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
if got := r.URL.Query().Get("name"); got != "docs" {
t.Fatalf("got name=%q", got)
}
fields, filename, content := readMultipartReleaseAttachment(t, r)
if !reflect.DeepEqual(fields, map[string]string{"external_url": "https://example.com/release.tar.gz"}) {
t.Fatalf("got fields=%#v", fields)
}
if filename != "" || content != "" {
t.Fatalf("unexpected file upload: filename=%q content=%q", filename, content)
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(types.Attachment{ID: 10, Name: "docs"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
attachment, err := f.Releases.CreateAttachment(
context.Background(),
"core",
"go-forge",
1,
&ReleaseAttachmentUploadOptions{Name: "docs", ExternalURL: "https://example.com/release.tar.gz"},
"",
nil,
)
if err != nil {
t.Fatal(err)
}
if attachment.Name != "docs" {
t.Fatalf("got name=%q", attachment.Name)
}
}
func TestReleaseService_EditAttachment_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/core/go-forge/releases/1/assets/4" {
t.Errorf("wrong path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
var body types.EditAttachmentOptions
json.NewDecoder(r.Body).Decode(&body)
if body.Name != "release-notes.pdf" {
t.Fatalf("got body=%#v", body)
}
json.NewEncoder(w).Encode(types.Attachment{ID: 4, Name: body.Name})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
attachment, err := f.Releases.EditAttachment(context.Background(), "core", "go-forge", 1, 4, &types.EditAttachmentOptions{Name: "release-notes.pdf"})
if err != nil {
t.Fatal(err)
}
if attachment.Name != "release-notes.pdf" {
t.Fatalf("got name=%q", attachment.Name)
}
}

1049
repos.go

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,64 +3,27 @@ package forge
import ( import (
"context" "context"
"iter" "iter"
"strconv" "strings"
core "dappco.re/go/core"
) )
// Resource provides generic CRUD operations for a Forgejo API resource. // Resource provides generic CRUD operations for a Forgejo API resource.
// T is the resource type, C is the create options type, U is the update options type. // T is the resource type, C is the create options type, U is the update options type.
//
// Usage:
//
// r := forge.NewResource[types.Issue, types.CreateIssueOption, types.EditIssueOption](client, "/api/v1/repos/{owner}/{repo}/issues/{index}")
// _ = r
type Resource[T any, C any, U any] struct { type Resource[T any, C any, U any] struct {
client *Client client *Client
path string // item path: /api/v1/repos/{owner}/{repo}/issues/{index} path string // item path: /api/v1/repos/{owner}/{repo}/issues/{index}
collection string // collection path: /api/v1/repos/{owner}/{repo}/issues collection string // collection path: /api/v1/repos/{owner}/{repo}/issues
} }
// String returns a safe summary of the resource configuration.
//
// Usage:
//
// s := res.String()
func (r *Resource[T, C, U]) String() string {
if r == nil {
return "forge.Resource{<nil>}"
}
return core.Concat(
"forge.Resource{path=",
strconv.Quote(r.path),
", collection=",
strconv.Quote(r.collection),
"}",
)
}
// GoString returns a safe Go-syntax summary of the resource configuration.
//
// Usage:
//
// s := fmt.Sprintf("%#v", res)
func (r *Resource[T, C, U]) GoString() string { return r.String() }
// NewResource creates a new Resource for the given path pattern. // NewResource creates a new Resource for the given path pattern.
// The path should be the item path (e.g., /repos/{owner}/{repo}/issues/{index}). // The path should be the item path (e.g., /repos/{owner}/{repo}/issues/{index}).
// The collection path is derived by stripping the last /{placeholder} segment. // The collection path is derived by stripping the last /{placeholder} segment.
//
// Usage:
//
// r := forge.NewResource[types.Issue, types.CreateIssueOption, types.EditIssueOption](client, "/api/v1/repos/{owner}/{repo}/issues/{index}")
// _ = r
func NewResource[T any, C any, U any](c *Client, path string) *Resource[T, C, U] { func NewResource[T any, C any, U any](c *Client, path string) *Resource[T, C, U] {
collection := path collection := path
// Strip last segment if it's a pure placeholder like /{index} // Strip last segment if it's a pure placeholder like /{index}
// Don't strip if mixed like /repos or /{org}/repos // Don't strip if mixed like /repos or /{org}/repos
if i := lastIndexByte(path, '/'); i >= 0 { if i := strings.LastIndex(path, "/"); i >= 0 {
lastSeg := path[i+1:] lastSeg := path[i+1:]
if core.HasPrefix(lastSeg, "{") && core.HasSuffix(lastSeg, "}") { if strings.HasPrefix(lastSeg, "{") && strings.HasSuffix(lastSeg, "}") {
collection = path[:i] collection = path[:i]
} }
} }
@ -68,39 +31,21 @@ func NewResource[T any, C any, U any](c *Client, path string) *Resource[T, C, U]
} }
// List returns a single page of resources. // List returns a single page of resources.
//
// Usage:
//
// page, err := res.List(ctx, forge.Params{"owner": "core"}, forge.DefaultList)
func (r *Resource[T, C, U]) List(ctx context.Context, params Params, opts ListOptions) (*PagedResult[T], error) { func (r *Resource[T, C, U]) List(ctx context.Context, params Params, opts ListOptions) (*PagedResult[T], error) {
return ListPage[T](ctx, r.client, ResolvePath(r.collection, params), nil, opts) return ListPage[T](ctx, r.client, ResolvePath(r.collection, params), nil, opts)
} }
// ListAll returns all resources across all pages. // ListAll returns all resources across all pages.
//
// Usage:
//
// items, err := res.ListAll(ctx, forge.Params{"owner": "core"})
func (r *Resource[T, C, U]) ListAll(ctx context.Context, params Params) ([]T, error) { func (r *Resource[T, C, U]) ListAll(ctx context.Context, params Params) ([]T, error) {
return ListAll[T](ctx, r.client, ResolvePath(r.collection, params), nil) return ListAll[T](ctx, r.client, ResolvePath(r.collection, params), nil)
} }
// Iter returns an iterator over all resources across all pages. // Iter returns an iterator over all resources across all pages.
//
// Usage:
//
// for item, err := range res.Iter(ctx, forge.Params{"owner": "core"}) {
// _, _ = item, err
// }
func (r *Resource[T, C, U]) Iter(ctx context.Context, params Params) iter.Seq2[T, error] { func (r *Resource[T, C, U]) Iter(ctx context.Context, params Params) iter.Seq2[T, error] {
return ListIter[T](ctx, r.client, ResolvePath(r.collection, params), nil) return ListIter[T](ctx, r.client, ResolvePath(r.collection, params), nil)
} }
// Get returns a single resource by appending id to the path. // Get returns a single resource by appending id to the path.
//
// Usage:
//
// item, err := res.Get(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
func (r *Resource[T, C, U]) Get(ctx context.Context, params Params) (*T, error) { func (r *Resource[T, C, U]) Get(ctx context.Context, params Params) (*T, error) {
var out T var out T
if err := r.client.Get(ctx, ResolvePath(r.path, params), &out); err != nil { if err := r.client.Get(ctx, ResolvePath(r.path, params), &out); err != nil {
@ -110,10 +55,6 @@ func (r *Resource[T, C, U]) Get(ctx context.Context, params Params) (*T, error)
} }
// Create creates a new resource. // Create creates a new resource.
//
// Usage:
//
// item, err := res.Create(ctx, forge.Params{"owner": "core"}, body)
func (r *Resource[T, C, U]) Create(ctx context.Context, params Params, body *C) (*T, error) { func (r *Resource[T, C, U]) Create(ctx context.Context, params Params, body *C) (*T, error) {
var out T var out T
if err := r.client.Post(ctx, ResolvePath(r.collection, params), body, &out); err != nil { if err := r.client.Post(ctx, ResolvePath(r.collection, params), body, &out); err != nil {
@ -123,10 +64,6 @@ func (r *Resource[T, C, U]) Create(ctx context.Context, params Params, body *C)
} }
// Update modifies an existing resource. // Update modifies an existing resource.
//
// Usage:
//
// item, err := res.Update(ctx, forge.Params{"owner": "core", "repo": "go-forge"}, body)
func (r *Resource[T, C, U]) Update(ctx context.Context, params Params, body *U) (*T, error) { func (r *Resource[T, C, U]) Update(ctx context.Context, params Params, body *U) (*T, error) {
var out T var out T
if err := r.client.Patch(ctx, ResolvePath(r.path, params), body, &out); err != nil { if err := r.client.Patch(ctx, ResolvePath(r.path, params), body, &out); err != nil {
@ -136,10 +73,6 @@ func (r *Resource[T, C, U]) Update(ctx context.Context, params Params, body *U)
} }
// Delete removes a resource. // Delete removes a resource.
//
// Usage:
//
// err := res.Delete(ctx, forge.Params{"owner": "core", "repo": "go-forge"})
func (r *Resource[T, C, U]) Delete(ctx context.Context, params Params) error { func (r *Resource[T, C, U]) Delete(ctx context.Context, params Params) error {
return r.client.Delete(ctx, ResolvePath(r.path, params)) return r.client.Delete(ctx, ResolvePath(r.path, params))
} }

View file

@ -1,21 +0,0 @@
package forge
import (
"fmt"
"testing"
)
func TestResource_String_Good(t *testing.T) {
res := NewResource[int, struct{}, struct{}](NewClient("https://forge.lthn.ai", "tok"), "/api/v1/repos/{owner}/{repo}")
got := fmt.Sprint(res)
want := `forge.Resource{path="/api/v1/repos/{owner}/{repo}", collection="/api/v1/repos/{owner}"}`
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
if got := res.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", res); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}

View file

@ -2,7 +2,7 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -22,7 +22,7 @@ type testUpdate struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
} }
func TestResource_List_Good(t *testing.T) { func TestResource_Good_List(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/orgs/core/repos" { if r.URL.Path != "/api/v1/orgs/core/repos" {
t.Errorf("wrong path: %s", r.URL.Path) t.Errorf("wrong path: %s", r.URL.Path)
@ -44,7 +44,7 @@ func TestResource_List_Good(t *testing.T) {
} }
} }
func TestResource_Get_Good(t *testing.T) { func TestResource_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/core/go-forge" { if r.URL.Path != "/api/v1/repos/core/go-forge" {
t.Errorf("wrong path: %s", r.URL.Path) t.Errorf("wrong path: %s", r.URL.Path)
@ -65,7 +65,7 @@ func TestResource_Get_Good(t *testing.T) {
} }
} }
func TestResource_Create_Good(t *testing.T) { func TestResource_Good_Create(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method) t.Errorf("expected POST, got %s", r.Method)
@ -94,7 +94,7 @@ func TestResource_Create_Good(t *testing.T) {
} }
} }
func TestResource_Update_Good(t *testing.T) { func TestResource_Good_Update(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch { if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method) t.Errorf("expected PATCH, got %s", r.Method)
@ -116,7 +116,7 @@ func TestResource_Update_Good(t *testing.T) {
} }
} }
func TestResource_Delete_Good(t *testing.T) { func TestResource_Good_Delete(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete { if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method) t.Errorf("expected DELETE, got %s", r.Method)
@ -134,7 +134,7 @@ func TestResource_Delete_Good(t *testing.T) {
} }
} }
func TestResource_ListAll_Good(t *testing.T) { func TestResource_Good_ListAll(t *testing.T) {
page := 0 page := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
page++ page++
@ -159,7 +159,7 @@ func TestResource_ListAll_Good(t *testing.T) {
} }
} }
func TestResource_Iter_Good(t *testing.T) { func TestResource_Good_Iter(t *testing.T) {
page := 0 page := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
page++ page++
@ -190,7 +190,7 @@ func TestResource_Iter_Good(t *testing.T) {
} }
} }
func TestResource_IterError_Bad(t *testing.T) { func TestResource_Bad_IterError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": "server error"}) json.NewEncoder(w).Encode(map[string]string{"message": "server error"})
@ -212,7 +212,7 @@ func TestResource_IterError_Bad(t *testing.T) {
} }
} }
func TestResource_IterBreakEarly_Good(t *testing.T) { func TestResource_Good_IterBreakEarly(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Total-Count", "100") w.Header().Set("X-Total-Count", "100")
json.NewEncoder(w).Encode([]testItem{{1, "a"}, {2, "b"}, {3, "c"}}) json.NewEncoder(w).Encode([]testItem{{1, "a"}, {2, "b"}, {3, "c"}})

View file

@ -1,421 +0,0 @@
package forge
// String returns a safe summary of the actions service.
//
// Usage:
//
// s := &forge.ActionsService{}
// _ = s.String()
func (s *ActionsService) String() string {
if s == nil {
return "forge.ActionsService{<nil>}"
}
return serviceString("forge.ActionsService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the actions service.
//
// Usage:
//
// s := &forge.ActionsService{}
// _ = fmt.Sprintf("%#v", s)
func (s *ActionsService) GoString() string { return s.String() }
// String returns a safe summary of the ActivityPub service.
//
// Usage:
//
// s := &forge.ActivityPubService{}
// _ = s.String()
func (s *ActivityPubService) String() string {
if s == nil {
return "forge.ActivityPubService{<nil>}"
}
return serviceString("forge.ActivityPubService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the ActivityPub service.
//
// Usage:
//
// s := &forge.ActivityPubService{}
// _ = fmt.Sprintf("%#v", s)
func (s *ActivityPubService) GoString() string { return s.String() }
// String returns a safe summary of the admin service.
//
// Usage:
//
// s := &forge.AdminService{}
// _ = s.String()
func (s *AdminService) String() string {
if s == nil {
return "forge.AdminService{<nil>}"
}
return serviceString("forge.AdminService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the admin service.
//
// Usage:
//
// s := &forge.AdminService{}
// _ = fmt.Sprintf("%#v", s)
func (s *AdminService) GoString() string { return s.String() }
// String returns a safe summary of the branch service.
//
// Usage:
//
// s := &forge.BranchService{}
// _ = s.String()
func (s *BranchService) String() string {
if s == nil {
return "forge.BranchService{<nil>}"
}
return serviceString("forge.BranchService", "resource", &s.Resource)
}
// GoString returns a safe Go-syntax summary of the branch service.
//
// Usage:
//
// s := &forge.BranchService{}
// _ = fmt.Sprintf("%#v", s)
func (s *BranchService) GoString() string { return s.String() }
// String returns a safe summary of the commit service.
//
// Usage:
//
// s := &forge.CommitService{}
// _ = s.String()
func (s *CommitService) String() string {
if s == nil {
return "forge.CommitService{<nil>}"
}
return serviceString("forge.CommitService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the commit service.
//
// Usage:
//
// s := &forge.CommitService{}
// _ = fmt.Sprintf("%#v", s)
func (s *CommitService) GoString() string { return s.String() }
// String returns a safe summary of the content service.
//
// Usage:
//
// s := &forge.ContentService{}
// _ = s.String()
func (s *ContentService) String() string {
if s == nil {
return "forge.ContentService{<nil>}"
}
return serviceString("forge.ContentService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the content service.
//
// Usage:
//
// s := &forge.ContentService{}
// _ = fmt.Sprintf("%#v", s)
func (s *ContentService) GoString() string { return s.String() }
// String returns a safe summary of the issue service.
//
// Usage:
//
// s := &forge.IssueService{}
// _ = s.String()
func (s *IssueService) String() string {
if s == nil {
return "forge.IssueService{<nil>}"
}
return serviceString("forge.IssueService", "resource", &s.Resource)
}
// GoString returns a safe Go-syntax summary of the issue service.
//
// Usage:
//
// s := &forge.IssueService{}
// _ = fmt.Sprintf("%#v", s)
func (s *IssueService) GoString() string { return s.String() }
// String returns a safe summary of the label service.
//
// Usage:
//
// s := &forge.LabelService{}
// _ = s.String()
func (s *LabelService) String() string {
if s == nil {
return "forge.LabelService{<nil>}"
}
return serviceString("forge.LabelService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the label service.
//
// Usage:
//
// s := &forge.LabelService{}
// _ = fmt.Sprintf("%#v", s)
func (s *LabelService) GoString() string { return s.String() }
// String returns a safe summary of the milestone service.
//
// Usage:
//
// s := &forge.MilestoneService{}
// _ = s.String()
func (s *MilestoneService) String() string {
if s == nil {
return "forge.MilestoneService{<nil>}"
}
return serviceString("forge.MilestoneService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the milestone service.
//
// Usage:
//
// s := &forge.MilestoneService{}
// _ = fmt.Sprintf("%#v", s)
func (s *MilestoneService) GoString() string { return s.String() }
// String returns a safe summary of the misc service.
//
// Usage:
//
// s := &forge.MiscService{}
// _ = s.String()
func (s *MiscService) String() string {
if s == nil {
return "forge.MiscService{<nil>}"
}
return serviceString("forge.MiscService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the misc service.
//
// Usage:
//
// s := &forge.MiscService{}
// _ = fmt.Sprintf("%#v", s)
func (s *MiscService) GoString() string { return s.String() }
// String returns a safe summary of the notification service.
//
// Usage:
//
// s := &forge.NotificationService{}
// _ = s.String()
func (s *NotificationService) String() string {
if s == nil {
return "forge.NotificationService{<nil>}"
}
return serviceString("forge.NotificationService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the notification service.
//
// Usage:
//
// s := &forge.NotificationService{}
// _ = fmt.Sprintf("%#v", s)
func (s *NotificationService) GoString() string { return s.String() }
// String returns a safe summary of the organisation service.
//
// Usage:
//
// s := &forge.OrgService{}
// _ = s.String()
func (s *OrgService) String() string {
if s == nil {
return "forge.OrgService{<nil>}"
}
return serviceString("forge.OrgService", "resource", &s.Resource)
}
// GoString returns a safe Go-syntax summary of the organisation service.
//
// Usage:
//
// s := &forge.OrgService{}
// _ = fmt.Sprintf("%#v", s)
func (s *OrgService) GoString() string { return s.String() }
// String returns a safe summary of the package service.
//
// Usage:
//
// s := &forge.PackageService{}
// _ = s.String()
func (s *PackageService) String() string {
if s == nil {
return "forge.PackageService{<nil>}"
}
return serviceString("forge.PackageService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the package service.
//
// Usage:
//
// s := &forge.PackageService{}
// _ = fmt.Sprintf("%#v", s)
func (s *PackageService) GoString() string { return s.String() }
// String returns a safe summary of the pull request service.
//
// Usage:
//
// s := &forge.PullService{}
// _ = s.String()
func (s *PullService) String() string {
if s == nil {
return "forge.PullService{<nil>}"
}
return serviceString("forge.PullService", "resource", &s.Resource)
}
// GoString returns a safe Go-syntax summary of the pull request service.
//
// Usage:
//
// s := &forge.PullService{}
// _ = fmt.Sprintf("%#v", s)
func (s *PullService) GoString() string { return s.String() }
// String returns a safe summary of the release service.
//
// Usage:
//
// s := &forge.ReleaseService{}
// _ = s.String()
func (s *ReleaseService) String() string {
if s == nil {
return "forge.ReleaseService{<nil>}"
}
return serviceString("forge.ReleaseService", "resource", &s.Resource)
}
// GoString returns a safe Go-syntax summary of the release service.
//
// Usage:
//
// s := &forge.ReleaseService{}
// _ = fmt.Sprintf("%#v", s)
func (s *ReleaseService) GoString() string { return s.String() }
// String returns a safe summary of the repository service.
//
// Usage:
//
// s := &forge.RepoService{}
// _ = s.String()
func (s *RepoService) String() string {
if s == nil {
return "forge.RepoService{<nil>}"
}
return serviceString("forge.RepoService", "resource", &s.Resource)
}
// GoString returns a safe Go-syntax summary of the repository service.
//
// Usage:
//
// s := &forge.RepoService{}
// _ = fmt.Sprintf("%#v", s)
func (s *RepoService) GoString() string { return s.String() }
// String returns a safe summary of the team service.
//
// Usage:
//
// s := &forge.TeamService{}
// _ = s.String()
func (s *TeamService) String() string {
if s == nil {
return "forge.TeamService{<nil>}"
}
return serviceString("forge.TeamService", "resource", &s.Resource)
}
// GoString returns a safe Go-syntax summary of the team service.
//
// Usage:
//
// s := &forge.TeamService{}
// _ = fmt.Sprintf("%#v", s)
func (s *TeamService) GoString() string { return s.String() }
// String returns a safe summary of the user service.
//
// Usage:
//
// s := &forge.UserService{}
// _ = s.String()
func (s *UserService) String() string {
if s == nil {
return "forge.UserService{<nil>}"
}
return serviceString("forge.UserService", "resource", &s.Resource)
}
// GoString returns a safe Go-syntax summary of the user service.
//
// Usage:
//
// s := &forge.UserService{}
// _ = fmt.Sprintf("%#v", s)
func (s *UserService) GoString() string { return s.String() }
// String returns a safe summary of the webhook service.
//
// Usage:
//
// s := &forge.WebhookService{}
// _ = s.String()
func (s *WebhookService) String() string {
if s == nil {
return "forge.WebhookService{<nil>}"
}
return serviceString("forge.WebhookService", "resource", &s.Resource)
}
// GoString returns a safe Go-syntax summary of the webhook service.
//
// Usage:
//
// s := &forge.WebhookService{}
// _ = fmt.Sprintf("%#v", s)
func (s *WebhookService) GoString() string { return s.String() }
// String returns a safe summary of the wiki service.
//
// Usage:
//
// s := &forge.WikiService{}
// _ = s.String()
func (s *WikiService) String() string {
if s == nil {
return "forge.WikiService{<nil>}"
}
return serviceString("forge.WikiService", "client", s.client)
}
// GoString returns a safe Go-syntax summary of the wiki service.
//
// Usage:
//
// s := &forge.WikiService{}
// _ = fmt.Sprintf("%#v", s)
func (s *WikiService) GoString() string { return s.String() }

View file

@ -1,62 +0,0 @@
package forge
import (
"fmt"
"testing"
)
func TestClient_String_NilSafe(t *testing.T) {
var c *Client
want := "forge.Client{<nil>}"
if got := c.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(c); got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", c); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestForge_String_NilSafe(t *testing.T) {
var f *Forge
want := "forge.Forge{<nil>}"
if got := f.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(f); got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", f); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestResource_String_NilSafe(t *testing.T) {
var r *Resource[int, struct{}, struct{}]
want := "forge.Resource{<nil>}"
if got := r.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(r); got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", r); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}
func TestAPIError_String_NilSafe(t *testing.T) {
var e *APIError
want := "forge.APIError{<nil>}"
if got := e.String(); got != want {
t.Fatalf("got String()=%q, want %q", got, want)
}
if got := fmt.Sprint(e); got != want {
t.Fatalf("got fmt.Sprint=%q, want %q", got, want)
}
if got := fmt.Sprintf("%#v", e); got != want {
t.Fatalf("got GoString=%q, want %q", got, want)
}
}

View file

@ -2,17 +2,13 @@ package forge
import ( import (
"context" "context"
"fmt"
"iter" "iter"
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
// TeamService handles team operations. // TeamService handles team operations.
//
// Usage:
//
// f := forge.NewForge("https://forge.lthn.ai", "token")
// _, err := f.Teams.ListMembers(ctx, 42)
type TeamService struct { type TeamService struct {
Resource[types.Team, types.CreateTeamOption, types.EditTeamOption] Resource[types.Team, types.CreateTeamOption, types.EditTeamOption]
} }
@ -25,104 +21,62 @@ func newTeamService(c *Client) *TeamService {
} }
} }
// CreateOrgTeam creates a team within an organisation.
func (s *TeamService) CreateOrgTeam(ctx context.Context, org string, opts *types.CreateTeamOption) (*types.Team, error) {
path := ResolvePath("/api/v1/orgs/{org}/teams", pathParams("org", org))
var out types.Team
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
}
return &out, nil
}
// ListMembers returns all members of a team. // ListMembers returns all members of a team.
func (s *TeamService) ListMembers(ctx context.Context, teamID int64) ([]types.User, error) { func (s *TeamService) ListMembers(ctx context.Context, teamID int64) ([]types.User, error) {
path := ResolvePath("/api/v1/teams/{id}/members", pathParams("id", int64String(teamID))) path := fmt.Sprintf("/api/v1/teams/%d/members", teamID)
return ListAll[types.User](ctx, s.client, path, nil) return ListAll[types.User](ctx, s.client, path, nil)
} }
// IterMembers returns an iterator over all members of a team. // IterMembers returns an iterator over all members of a team.
func (s *TeamService) IterMembers(ctx context.Context, teamID int64) iter.Seq2[types.User, error] { func (s *TeamService) IterMembers(ctx context.Context, teamID int64) iter.Seq2[types.User, error] {
path := ResolvePath("/api/v1/teams/{id}/members", pathParams("id", int64String(teamID))) path := fmt.Sprintf("/api/v1/teams/%d/members", teamID)
return ListIter[types.User](ctx, s.client, path, nil) return ListIter[types.User](ctx, s.client, path, nil)
} }
// AddMember adds a user to a team. // AddMember adds a user to a team.
func (s *TeamService) AddMember(ctx context.Context, teamID int64, username string) error { func (s *TeamService) AddMember(ctx context.Context, teamID int64, username string) error {
path := ResolvePath("/api/v1/teams/{id}/members/{username}", pathParams("id", int64String(teamID), "username", username)) path := fmt.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)
return s.client.Put(ctx, path, nil, nil) return s.client.Put(ctx, path, nil, nil)
} }
// GetMember returns a particular member of a team.
func (s *TeamService) GetMember(ctx context.Context, teamID int64, username string) (*types.User, error) {
path := ResolvePath("/api/v1/teams/{id}/members/{username}", pathParams("id", int64String(teamID), "username", username))
var out types.User
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// RemoveMember removes a user from a team. // RemoveMember removes a user from a team.
func (s *TeamService) RemoveMember(ctx context.Context, teamID int64, username string) error { func (s *TeamService) RemoveMember(ctx context.Context, teamID int64, username string) error {
path := ResolvePath("/api/v1/teams/{id}/members/{username}", pathParams("id", int64String(teamID), "username", username)) path := fmt.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// ListRepos returns all repositories managed by a team. // ListRepos returns all repositories managed by a team.
func (s *TeamService) ListRepos(ctx context.Context, teamID int64) ([]types.Repository, error) { func (s *TeamService) ListRepos(ctx context.Context, teamID int64) ([]types.Repository, error) {
path := ResolvePath("/api/v1/teams/{id}/repos", pathParams("id", int64String(teamID))) path := fmt.Sprintf("/api/v1/teams/%d/repos", teamID)
return ListAll[types.Repository](ctx, s.client, path, nil) return ListAll[types.Repository](ctx, s.client, path, nil)
} }
// IterRepos returns an iterator over all repositories managed by a team. // IterRepos returns an iterator over all repositories managed by a team.
func (s *TeamService) IterRepos(ctx context.Context, teamID int64) iter.Seq2[types.Repository, error] { func (s *TeamService) IterRepos(ctx context.Context, teamID int64) iter.Seq2[types.Repository, error] {
path := ResolvePath("/api/v1/teams/{id}/repos", pathParams("id", int64String(teamID))) path := fmt.Sprintf("/api/v1/teams/%d/repos", teamID)
return ListIter[types.Repository](ctx, s.client, path, nil) return ListIter[types.Repository](ctx, s.client, path, nil)
} }
// AddRepo adds a repository to a team. // AddRepo adds a repository to a team.
func (s *TeamService) AddRepo(ctx context.Context, teamID int64, org, repo string) error { func (s *TeamService) AddRepo(ctx context.Context, teamID int64, org, repo string) error {
path := ResolvePath("/api/v1/teams/{id}/repos/{org}/{repo}", pathParams("id", int64String(teamID), "org", org, "repo", repo)) path := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, org, repo)
return s.client.Put(ctx, path, nil, nil) return s.client.Put(ctx, path, nil, nil)
} }
// RemoveRepo removes a repository from a team. // RemoveRepo removes a repository from a team.
func (s *TeamService) RemoveRepo(ctx context.Context, teamID int64, org, repo string) error { func (s *TeamService) RemoveRepo(ctx context.Context, teamID int64, org, repo string) error {
path := ResolvePath("/api/v1/teams/{id}/repos/{org}/{repo}", pathParams("id", int64String(teamID), "org", org, "repo", repo)) path := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, org, repo)
return s.client.Delete(ctx, path) return s.client.Delete(ctx, path)
} }
// GetRepo returns a particular repository managed by a team.
func (s *TeamService) GetRepo(ctx context.Context, teamID int64, org, repo string) (*types.Repository, error) {
path := ResolvePath("/api/v1/teams/{id}/repos/{org}/{repo}", pathParams("id", int64String(teamID), "org", org, "repo", repo))
var out types.Repository
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
}
return &out, nil
}
// ListOrgTeams returns all teams in an organisation. // ListOrgTeams returns all teams in an organisation.
func (s *TeamService) ListOrgTeams(ctx context.Context, org string) ([]types.Team, error) { func (s *TeamService) ListOrgTeams(ctx context.Context, org string) ([]types.Team, error) {
path := ResolvePath("/api/v1/orgs/{org}/teams", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/teams", org)
return ListAll[types.Team](ctx, s.client, path, nil) return ListAll[types.Team](ctx, s.client, path, nil)
} }
// IterOrgTeams returns an iterator over all teams in an organisation. // IterOrgTeams returns an iterator over all teams in an organisation.
func (s *TeamService) IterOrgTeams(ctx context.Context, org string) iter.Seq2[types.Team, error] { func (s *TeamService) IterOrgTeams(ctx context.Context, org string) iter.Seq2[types.Team, error] {
path := ResolvePath("/api/v1/orgs/{org}/teams", pathParams("org", org)) path := fmt.Sprintf("/api/v1/orgs/%s/teams", org)
return ListIter[types.Team](ctx, s.client, path, nil) return ListIter[types.Team](ctx, s.client, path, nil)
} }
// ListActivityFeeds returns a team's activity feed entries.
func (s *TeamService) ListActivityFeeds(ctx context.Context, teamID int64) ([]types.Activity, error) {
path := ResolvePath("/api/v1/teams/{id}/activities/feeds", pathParams("id", int64String(teamID)))
return ListAll[types.Activity](ctx, s.client, path, nil)
}
// IterActivityFeeds returns an iterator over a team's activity feed entries.
func (s *TeamService) IterActivityFeeds(ctx context.Context, teamID int64) iter.Seq2[types.Activity, error] {
path := ResolvePath("/api/v1/teams/{id}/activities/feeds", pathParams("id", int64String(teamID)))
return ListIter[types.Activity](ctx, s.client, path, nil)
}

View file

@ -2,7 +2,7 @@ package forge
import ( import (
"context" "context"
json "github.com/goccy/go-json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -10,7 +10,7 @@ import (
"dappco.re/go/core/forge/types" "dappco.re/go/core/forge/types"
) )
func TestTeamService_Get_Good(t *testing.T) { func TestTeamService_Good_Get(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -32,38 +32,7 @@ func TestTeamService_Get_Good(t *testing.T) {
} }
} }
func TestTeamService_CreateOrgTeam_Good(t *testing.T) { func TestTeamService_Good_ListMembers(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/orgs/core/teams" {
t.Errorf("wrong path: %s", r.URL.Path)
}
var opts types.CreateTeamOption
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
t.Fatal(err)
}
if opts.Name != "platform" {
t.Errorf("got name=%q, want %q", opts.Name, "platform")
}
json.NewEncoder(w).Encode(types.Team{ID: 7, Name: opts.Name})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
team, err := f.Teams.CreateOrgTeam(context.Background(), "core", &types.CreateTeamOption{
Name: "platform",
})
if err != nil {
t.Fatal(err)
}
if team.ID != 7 || team.Name != "platform" {
t.Fatalf("got %#v", team)
}
}
func TestTeamService_ListMembers_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method) t.Errorf("expected GET, got %s", r.Method)
@ -92,7 +61,7 @@ func TestTeamService_ListMembers_Good(t *testing.T) {
} }
} }
func TestTeamService_AddMember_Good(t *testing.T) { func TestTeamService_Good_AddMember(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut { if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method) t.Errorf("expected PUT, got %s", r.Method)
@ -110,25 +79,3 @@ func TestTeamService_AddMember_Good(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
func TestTeamService_GetMember_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/teams/42/members/alice" {
t.Errorf("wrong path: %s", r.URL.Path)
}
json.NewEncoder(w).Encode(types.User{ID: 1, UserName: "alice"})
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
member, err := f.Teams.GetMember(context.Background(), 42, "alice")
if err != nil {
t.Fatal(err)
}
if member.UserName != "alice" {
t.Errorf("got username=%q, want %q", member.UserName, "alice")
}
}

View file

@ -4,84 +4,58 @@ package types
import "time" import "time"
// ActionTask — ActionTask represents a ActionTask // ActionTask — ActionTask represents a ActionTask
//
// Usage:
//
// opts := ActionTask{DisplayTitle: "example"}
type ActionTask struct { type ActionTask struct {
CreatedAt time.Time `json:"created_at,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"`
DisplayTitle string `json:"display_title,omitempty"` DisplayTitle string `json:"display_title,omitempty"`
Event string `json:"event,omitempty"` Event string `json:"event,omitempty"`
HeadBranch string `json:"head_branch,omitempty"` HeadBranch string `json:"head_branch,omitempty"`
HeadSHA string `json:"head_sha,omitempty"` HeadSHA string `json:"head_sha,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
RunNumber int64 `json:"run_number,omitempty"` RunNumber int64 `json:"run_number,omitempty"`
RunStartedAt time.Time `json:"run_started_at,omitempty"` RunStartedAt time.Time `json:"run_started_at,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"`
WorkflowID string `json:"workflow_id,omitempty"` WorkflowID string `json:"workflow_id,omitempty"`
} }
// ActionTaskResponse — ActionTaskResponse returns a ActionTask // ActionTaskResponse — ActionTaskResponse returns a ActionTask
//
// Usage:
//
// opts := ActionTaskResponse{TotalCount: 1}
type ActionTaskResponse struct { type ActionTaskResponse struct {
Entries []*ActionTask `json:"workflow_runs,omitempty"` Entries []*ActionTask `json:"workflow_runs,omitempty"`
TotalCount int64 `json:"total_count,omitempty"` TotalCount int64 `json:"total_count,omitempty"`
} }
// ActionVariable — ActionVariable return value of the query API // ActionVariable — ActionVariable return value of the query API
//
// Usage:
//
// opts := ActionVariable{Name: "example"}
type ActionVariable struct { type ActionVariable struct {
Data string `json:"data,omitempty"` // the value of the variable Data string `json:"data,omitempty"` // the value of the variable
Name string `json:"name,omitempty"` // the name of the variable Name string `json:"name,omitempty"` // the name of the variable
OwnerID int64 `json:"owner_id,omitempty"` // the owner to which the variable belongs OwnerID int64 `json:"owner_id,omitempty"` // the owner to which the variable belongs
RepoID int64 `json:"repo_id,omitempty"` // the repository to which the variable belongs RepoID int64 `json:"repo_id,omitempty"` // the repository to which the variable belongs
} }
// CreateVariableOption — CreateVariableOption the option when creating variable // CreateVariableOption — CreateVariableOption the option when creating variable
//
// Usage:
//
// opts := CreateVariableOption{Value: "example"}
type CreateVariableOption struct { type CreateVariableOption struct {
Value string `json:"value"` // Value of the variable to create Value string `json:"value"` // Value of the variable to create
} }
// DispatchWorkflowOption — DispatchWorkflowOption options when dispatching a workflow // DispatchWorkflowOption — DispatchWorkflowOption options when dispatching a workflow
//
// Usage:
//
// opts := DispatchWorkflowOption{Ref: "main"}
type DispatchWorkflowOption struct { type DispatchWorkflowOption struct {
Inputs map[string]string `json:"inputs,omitempty"` // Input keys and values configured in the workflow file. Inputs map[string]any `json:"inputs,omitempty"` // Input keys and values configured in the workflow file.
Ref string `json:"ref"` // Git reference for the workflow Ref string `json:"ref"` // Git reference for the workflow
} }
// Secret — Secret represents a secret // Secret — Secret represents a secret
//
// Usage:
//
// opts := Secret{Name: "example"}
type Secret struct { type Secret struct {
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Name string `json:"name,omitempty"` // the secret's name Name string `json:"name,omitempty"` // the secret's name
} }
// UpdateVariableOption — UpdateVariableOption the option when updating variable // UpdateVariableOption — UpdateVariableOption the option when updating variable
//
// Usage:
//
// opts := UpdateVariableOption{Value: "example"}
type UpdateVariableOption struct { type UpdateVariableOption struct {
Name string `json:"name,omitempty"` // New name for the variable. If the field is empty, the variable name won't be updated. Name string `json:"name,omitempty"` // New name for the variable. If the field is empty, the variable name won't be updated.
Value string `json:"value"` // Value of the variable to update Value string `json:"value"` // Value of the variable to update
} }

View file

@ -4,30 +4,25 @@ package types
import "time" import "time"
// Usage:
//
// opts := Activity{RefName: "main"}
type Activity struct { type Activity struct {
ActUser *User `json:"act_user,omitempty"` ActUser *User `json:"act_user,omitempty"`
ActUserID int64 `json:"act_user_id,omitempty"` ActUserID int64 `json:"act_user_id,omitempty"`
Comment *Comment `json:"comment,omitempty"` Comment *Comment `json:"comment,omitempty"`
CommentID int64 `json:"comment_id,omitempty"` CommentID int64 `json:"comment_id,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Created time.Time `json:"created,omitempty"` Created time.Time `json:"created,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
IsPrivate bool `json:"is_private,omitempty"` IsPrivate bool `json:"is_private,omitempty"`
OpType string `json:"op_type,omitempty"` // the type of action OpType string `json:"op_type,omitempty"` // the type of action
RefName string `json:"ref_name,omitempty"` RefName string `json:"ref_name,omitempty"`
Repo *Repository `json:"repo,omitempty"` Repo *Repository `json:"repo,omitempty"`
RepoID int64 `json:"repo_id,omitempty"` RepoID int64 `json:"repo_id,omitempty"`
UserID int64 `json:"user_id,omitempty"` UserID int64 `json:"user_id,omitempty"`
} }
// ActivityPub — ActivityPub type // ActivityPub — ActivityPub type
//
// Usage:
//
// opts := ActivityPub{Context: "example"}
type ActivityPub struct { type ActivityPub struct {
Context string `json:"@context,omitempty"` Context string `json:"@context,omitempty"`
} }

View file

@ -4,24 +4,18 @@ package types
import "time" import "time"
// Cron — Cron represents a Cron task // Cron — Cron represents a Cron task
//
// Usage:
//
// opts := Cron{Name: "example"}
type Cron struct { type Cron struct {
ExecTimes int64 `json:"exec_times,omitempty"` ExecTimes int64 `json:"exec_times,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Next time.Time `json:"next,omitempty"` Next time.Time `json:"next,omitempty"`
Prev time.Time `json:"prev,omitempty"` Prev time.Time `json:"prev,omitempty"`
Schedule string `json:"schedule,omitempty"` Schedule string `json:"schedule,omitempty"`
} }
// RenameUserOption — RenameUserOption options when renaming a user // RenameUserOption — RenameUserOption options when renaming a user
//
// Usage:
//
// opts := RenameUserOption{NewName: "example"}
type RenameUserOption struct { type RenameUserOption struct {
NewName string `json:"new_username"` // New username for this user. This name cannot be in use yet by any other user. NewName string `json:"new_username"` // New username for this user. This name cannot be in use yet by any other user.
} }

View file

@ -4,138 +4,116 @@ package types
import "time" import "time"
// Branch — Branch represents a repository branch // Branch — Branch represents a repository branch
//
// Usage:
//
// opts := Branch{EffectiveBranchProtectionName: "main"}
type Branch struct { type Branch struct {
Commit *PayloadCommit `json:"commit,omitempty"` Commit *PayloadCommit `json:"commit,omitempty"`
EffectiveBranchProtectionName string `json:"effective_branch_protection_name,omitempty"` EffectiveBranchProtectionName string `json:"effective_branch_protection_name,omitempty"`
EnableStatusCheck bool `json:"enable_status_check,omitempty"` EnableStatusCheck bool `json:"enable_status_check,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Protected bool `json:"protected,omitempty"` Protected bool `json:"protected,omitempty"`
RequiredApprovals int64 `json:"required_approvals,omitempty"` RequiredApprovals int64 `json:"required_approvals,omitempty"`
StatusCheckContexts []string `json:"status_check_contexts,omitempty"` StatusCheckContexts []string `json:"status_check_contexts,omitempty"`
UserCanMerge bool `json:"user_can_merge,omitempty"` UserCanMerge bool `json:"user_can_merge,omitempty"`
UserCanPush bool `json:"user_can_push,omitempty"` UserCanPush bool `json:"user_can_push,omitempty"`
} }
// BranchProtection — BranchProtection represents a branch protection for a repository // BranchProtection — BranchProtection represents a branch protection for a repository
//
// Usage:
//
// opts := BranchProtection{BranchName: "main"}
type BranchProtection struct { type BranchProtection struct {
ApplyToAdmins bool `json:"apply_to_admins,omitempty"` ApplyToAdmins bool `json:"apply_to_admins,omitempty"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams,omitempty"` ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams,omitempty"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username,omitempty"` ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username,omitempty"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests,omitempty"` BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests,omitempty"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch,omitempty"` BlockOnOutdatedBranch bool `json:"block_on_outdated_branch,omitempty"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews,omitempty"` BlockOnRejectedReviews bool `json:"block_on_rejected_reviews,omitempty"`
BranchName string `json:"branch_name,omitempty"` // Deprecated: true BranchName string `json:"branch_name,omitempty"` // Deprecated: true
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
DismissStaleApprovals bool `json:"dismiss_stale_approvals,omitempty"` DismissStaleApprovals bool `json:"dismiss_stale_approvals,omitempty"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist,omitempty"` EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist,omitempty"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist,omitempty"` EnableMergeWhitelist bool `json:"enable_merge_whitelist,omitempty"`
EnablePush bool `json:"enable_push,omitempty"` EnablePush bool `json:"enable_push,omitempty"`
EnablePushWhitelist bool `json:"enable_push_whitelist,omitempty"` EnablePushWhitelist bool `json:"enable_push_whitelist,omitempty"`
EnableStatusCheck bool `json:"enable_status_check,omitempty"` EnableStatusCheck bool `json:"enable_status_check,omitempty"`
IgnoreStaleApprovals bool `json:"ignore_stale_approvals,omitempty"` IgnoreStaleApprovals bool `json:"ignore_stale_approvals,omitempty"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams,omitempty"` MergeWhitelistTeams []string `json:"merge_whitelist_teams,omitempty"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames,omitempty"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames,omitempty"`
ProtectedFilePatterns string `json:"protected_file_patterns,omitempty"` ProtectedFilePatterns string `json:"protected_file_patterns,omitempty"`
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys,omitempty"` PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys,omitempty"`
PushWhitelistTeams []string `json:"push_whitelist_teams,omitempty"` PushWhitelistTeams []string `json:"push_whitelist_teams,omitempty"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames,omitempty"` PushWhitelistUsernames []string `json:"push_whitelist_usernames,omitempty"`
RequireSignedCommits bool `json:"require_signed_commits,omitempty"` RequireSignedCommits bool `json:"require_signed_commits,omitempty"`
RequiredApprovals int64 `json:"required_approvals,omitempty"` RequiredApprovals int64 `json:"required_approvals,omitempty"`
RuleName string `json:"rule_name,omitempty"` RuleName string `json:"rule_name,omitempty"`
StatusCheckContexts []string `json:"status_check_contexts,omitempty"` StatusCheckContexts []string `json:"status_check_contexts,omitempty"`
UnprotectedFilePatterns string `json:"unprotected_file_patterns,omitempty"` UnprotectedFilePatterns string `json:"unprotected_file_patterns,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
} }
// CreateBranchProtectionOption — CreateBranchProtectionOption options for creating a branch protection // CreateBranchProtectionOption — CreateBranchProtectionOption options for creating a branch protection
//
// Usage:
//
// opts := CreateBranchProtectionOption{BranchName: "main"}
type CreateBranchProtectionOption struct { type CreateBranchProtectionOption struct {
ApplyToAdmins bool `json:"apply_to_admins,omitempty"` ApplyToAdmins bool `json:"apply_to_admins,omitempty"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams,omitempty"` ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams,omitempty"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username,omitempty"` ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username,omitempty"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests,omitempty"` BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests,omitempty"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch,omitempty"` BlockOnOutdatedBranch bool `json:"block_on_outdated_branch,omitempty"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews,omitempty"` BlockOnRejectedReviews bool `json:"block_on_rejected_reviews,omitempty"`
BranchName string `json:"branch_name,omitempty"` // Deprecated: true BranchName string `json:"branch_name,omitempty"` // Deprecated: true
DismissStaleApprovals bool `json:"dismiss_stale_approvals,omitempty"` DismissStaleApprovals bool `json:"dismiss_stale_approvals,omitempty"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist,omitempty"` EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist,omitempty"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist,omitempty"` EnableMergeWhitelist bool `json:"enable_merge_whitelist,omitempty"`
EnablePush bool `json:"enable_push,omitempty"` EnablePush bool `json:"enable_push,omitempty"`
EnablePushWhitelist bool `json:"enable_push_whitelist,omitempty"` EnablePushWhitelist bool `json:"enable_push_whitelist,omitempty"`
EnableStatusCheck bool `json:"enable_status_check,omitempty"` EnableStatusCheck bool `json:"enable_status_check,omitempty"`
IgnoreStaleApprovals bool `json:"ignore_stale_approvals,omitempty"` IgnoreStaleApprovals bool `json:"ignore_stale_approvals,omitempty"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams,omitempty"` MergeWhitelistTeams []string `json:"merge_whitelist_teams,omitempty"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames,omitempty"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames,omitempty"`
ProtectedFilePatterns string `json:"protected_file_patterns,omitempty"` ProtectedFilePatterns string `json:"protected_file_patterns,omitempty"`
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys,omitempty"` PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys,omitempty"`
PushWhitelistTeams []string `json:"push_whitelist_teams,omitempty"` PushWhitelistTeams []string `json:"push_whitelist_teams,omitempty"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames,omitempty"` PushWhitelistUsernames []string `json:"push_whitelist_usernames,omitempty"`
RequireSignedCommits bool `json:"require_signed_commits,omitempty"` RequireSignedCommits bool `json:"require_signed_commits,omitempty"`
RequiredApprovals int64 `json:"required_approvals,omitempty"` RequiredApprovals int64 `json:"required_approvals,omitempty"`
RuleName string `json:"rule_name,omitempty"` RuleName string `json:"rule_name,omitempty"`
StatusCheckContexts []string `json:"status_check_contexts,omitempty"` StatusCheckContexts []string `json:"status_check_contexts,omitempty"`
UnprotectedFilePatterns string `json:"unprotected_file_patterns,omitempty"` UnprotectedFilePatterns string `json:"unprotected_file_patterns,omitempty"`
} }
// CreateBranchRepoOption — CreateBranchRepoOption options when creating a branch in a repository // CreateBranchRepoOption — CreateBranchRepoOption options when creating a branch in a repository
//
// Usage:
//
// opts := CreateBranchRepoOption{BranchName: "main"}
type CreateBranchRepoOption struct { type CreateBranchRepoOption struct {
BranchName string `json:"new_branch_name"` // Name of the branch to create BranchName string `json:"new_branch_name"` // Name of the branch to create
OldBranchName string `json:"old_branch_name,omitempty"` // Deprecated: true Name of the old branch to create from OldBranchName string `json:"old_branch_name,omitempty"` // Deprecated: true Name of the old branch to create from
OldRefName string `json:"old_ref_name,omitempty"` // Name of the old branch/tag/commit to create from OldRefName string `json:"old_ref_name,omitempty"` // Name of the old branch/tag/commit to create from
} }
// EditBranchProtectionOption — EditBranchProtectionOption options for editing a branch protection // EditBranchProtectionOption — EditBranchProtectionOption options for editing a branch protection
//
// Usage:
//
// opts := EditBranchProtectionOption{ApprovalsWhitelistTeams: []string{"example"}}
type EditBranchProtectionOption struct { type EditBranchProtectionOption struct {
ApplyToAdmins bool `json:"apply_to_admins,omitempty"` ApplyToAdmins bool `json:"apply_to_admins,omitempty"`
ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams,omitempty"` ApprovalsWhitelistTeams []string `json:"approvals_whitelist_teams,omitempty"`
ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username,omitempty"` ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username,omitempty"`
BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests,omitempty"` BlockOnOfficialReviewRequests bool `json:"block_on_official_review_requests,omitempty"`
BlockOnOutdatedBranch bool `json:"block_on_outdated_branch,omitempty"` BlockOnOutdatedBranch bool `json:"block_on_outdated_branch,omitempty"`
BlockOnRejectedReviews bool `json:"block_on_rejected_reviews,omitempty"` BlockOnRejectedReviews bool `json:"block_on_rejected_reviews,omitempty"`
DismissStaleApprovals bool `json:"dismiss_stale_approvals,omitempty"` DismissStaleApprovals bool `json:"dismiss_stale_approvals,omitempty"`
EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist,omitempty"` EnableApprovalsWhitelist bool `json:"enable_approvals_whitelist,omitempty"`
EnableMergeWhitelist bool `json:"enable_merge_whitelist,omitempty"` EnableMergeWhitelist bool `json:"enable_merge_whitelist,omitempty"`
EnablePush bool `json:"enable_push,omitempty"` EnablePush bool `json:"enable_push,omitempty"`
EnablePushWhitelist bool `json:"enable_push_whitelist,omitempty"` EnablePushWhitelist bool `json:"enable_push_whitelist,omitempty"`
EnableStatusCheck bool `json:"enable_status_check,omitempty"` EnableStatusCheck bool `json:"enable_status_check,omitempty"`
IgnoreStaleApprovals bool `json:"ignore_stale_approvals,omitempty"` IgnoreStaleApprovals bool `json:"ignore_stale_approvals,omitempty"`
MergeWhitelistTeams []string `json:"merge_whitelist_teams,omitempty"` MergeWhitelistTeams []string `json:"merge_whitelist_teams,omitempty"`
MergeWhitelistUsernames []string `json:"merge_whitelist_usernames,omitempty"` MergeWhitelistUsernames []string `json:"merge_whitelist_usernames,omitempty"`
ProtectedFilePatterns string `json:"protected_file_patterns,omitempty"` ProtectedFilePatterns string `json:"protected_file_patterns,omitempty"`
PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys,omitempty"` PushWhitelistDeployKeys bool `json:"push_whitelist_deploy_keys,omitempty"`
PushWhitelistTeams []string `json:"push_whitelist_teams,omitempty"` PushWhitelistTeams []string `json:"push_whitelist_teams,omitempty"`
PushWhitelistUsernames []string `json:"push_whitelist_usernames,omitempty"` PushWhitelistUsernames []string `json:"push_whitelist_usernames,omitempty"`
RequireSignedCommits bool `json:"require_signed_commits,omitempty"` RequireSignedCommits bool `json:"require_signed_commits,omitempty"`
RequiredApprovals int64 `json:"required_approvals,omitempty"` RequiredApprovals int64 `json:"required_approvals,omitempty"`
StatusCheckContexts []string `json:"status_check_contexts,omitempty"` StatusCheckContexts []string `json:"status_check_contexts,omitempty"`
UnprotectedFilePatterns string `json:"unprotected_file_patterns,omitempty"` UnprotectedFilePatterns string `json:"unprotected_file_patterns,omitempty"`
} }
// UpdateBranchRepoOption — UpdateBranchRepoOption options when updating a branch in a repository // UpdateBranchRepoOption — UpdateBranchRepoOption options when updating a branch in a repository
//
// Usage:
//
// opts := UpdateBranchRepoOption{Name: "example"}
type UpdateBranchRepoOption struct { type UpdateBranchRepoOption struct {
Name string `json:"name"` // New branch name Name string `json:"name"` // New branch name
} }

View file

@ -4,21 +4,19 @@ package types
import "time" import "time"
// Comment — Comment represents a comment on a commit or issue // Comment — Comment represents a comment on a commit or issue
//
// Usage:
//
// opts := Comment{Body: "example"}
type Comment struct { type Comment struct {
Attachments []*Attachment `json:"assets,omitempty"` Attachments []*Attachment `json:"assets,omitempty"`
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
IssueURL string `json:"issue_url,omitempty"` IssueURL string `json:"issue_url,omitempty"`
OriginalAuthor string `json:"original_author,omitempty"` OriginalAuthor string `json:"original_author,omitempty"`
OriginalAuthorID int64 `json:"original_author_id,omitempty"` OriginalAuthorID int64 `json:"original_author_id,omitempty"`
PRURL string `json:"pull_request_url,omitempty"` PRURL string `json:"pull_request_url,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }

View file

@ -4,91 +4,65 @@ package types
import "time" import "time"
// Usage:
//
// opts := Commit{HTMLURL: "https://example.com"}
type Commit struct { type Commit struct {
Author *User `json:"author,omitempty"` Author *User `json:"author,omitempty"`
Commit *RepoCommit `json:"commit,omitempty"` Commit *RepoCommit `json:"commit,omitempty"`
Committer *User `json:"committer,omitempty"` Committer *User `json:"committer,omitempty"`
Created time.Time `json:"created,omitempty"` Created time.Time `json:"created,omitempty"`
Files []*CommitAffectedFiles `json:"files,omitempty"` Files []*CommitAffectedFiles `json:"files,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
Parents []*CommitMeta `json:"parents,omitempty"` Parents []*CommitMeta `json:"parents,omitempty"`
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
Stats *CommitStats `json:"stats,omitempty"` Stats *CommitStats `json:"stats,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// CommitAffectedFiles — CommitAffectedFiles store information about files affected by the commit // CommitAffectedFiles — CommitAffectedFiles store information about files affected by the commit
//
// Usage:
//
// opts := CommitAffectedFiles{Filename: "example"}
type CommitAffectedFiles struct { type CommitAffectedFiles struct {
Filename string `json:"filename,omitempty"` Filename string `json:"filename,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
} }
// CommitDateOptions — CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE // CommitDateOptions — CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
//
// Usage:
//
// opts := CommitDateOptions{Author: time.Now()}
type CommitDateOptions struct { type CommitDateOptions struct {
Author time.Time `json:"author,omitempty"` Author time.Time `json:"author,omitempty"`
Committer time.Time `json:"committer,omitempty"` Committer time.Time `json:"committer,omitempty"`
} }
// Usage:
//
// opts := CommitMeta{SHA: "example"}
type CommitMeta struct { type CommitMeta struct {
Created time.Time `json:"created,omitempty"` Created time.Time `json:"created,omitempty"`
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// CommitStats — CommitStats is statistics for a RepoCommit // CommitStats — CommitStats is statistics for a RepoCommit
//
// Usage:
//
// opts := CommitStats{Additions: 1}
type CommitStats struct { type CommitStats struct {
Additions int64 `json:"additions,omitempty"` Additions int64 `json:"additions,omitempty"`
Deletions int64 `json:"deletions,omitempty"` Deletions int64 `json:"deletions,omitempty"`
Total int64 `json:"total,omitempty"` Total int64 `json:"total,omitempty"`
} }
// CommitStatus — CommitStatus holds a single status of a single Commit // CommitStatus — CommitStatus holds a single status of a single Commit
//
// Usage:
//
// opts := CommitStatus{Description: "example"}
type CommitStatus struct { type CommitStatus struct {
Context string `json:"context,omitempty"` Context string `json:"context,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Creator *User `json:"creator,omitempty"` Creator *User `json:"creator,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Status CommitStatusState `json:"status,omitempty"` Status *CommitStatusState `json:"status,omitempty"`
TargetURL string `json:"target_url,omitempty"` TargetURL string `json:"target_url,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
} }
// CommitStatusState — CommitStatusState holds the state of a CommitStatus It can be "pending", "success", "error" and "failure" // CommitStatusState — CommitStatusState holds the state of a CommitStatus It can be "pending", "success", "error" and "failure"
// // CommitStatusState has no fields in the swagger spec.
// Usage: type CommitStatusState struct{}
//
// opts := CommitStatusState("example")
type CommitStatusState string
// Usage:
//
// opts := CommitUser{Name: "example"}
type CommitUser struct { type CommitUser struct {
Date string `json:"date,omitempty"` Date string `json:"date,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
} }

View file

@ -4,53 +4,35 @@ package types
import "time" import "time"
// Attachment — Attachment a generic attachment // Attachment — Attachment a generic attachment
//
// Usage:
//
// opts := Attachment{Name: "example"}
type Attachment struct { type Attachment struct {
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
DownloadCount int64 `json:"download_count,omitempty"` DownloadCount int64 `json:"download_count,omitempty"`
DownloadURL string `json:"browser_download_url,omitempty"` DownloadURL string `json:"browser_download_url,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Size int64 `json:"size,omitempty"` Size int64 `json:"size,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
UUID string `json:"uuid,omitempty"` UUID string `json:"uuid,omitempty"`
} }
// EditAttachmentOptions — EditAttachmentOptions options for editing attachments // EditAttachmentOptions — EditAttachmentOptions options for editing attachments
//
// Usage:
//
// opts := EditAttachmentOptions{Name: "example"}
type EditAttachmentOptions struct { type EditAttachmentOptions struct {
DownloadURL string `json:"browser_download_url,omitempty"` // (Can only be set if existing attachment is of external type) DownloadURL string `json:"browser_download_url,omitempty"` // (Can only be set if existing attachment is of external type)
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
} }
// Permission — Permission represents a set of permissions // Permission — Permission represents a set of permissions
//
// Usage:
//
// opts := Permission{Admin: true}
type Permission struct { type Permission struct {
Admin bool `json:"admin,omitempty"` Admin bool `json:"admin,omitempty"`
Pull bool `json:"pull,omitempty"` Pull bool `json:"pull,omitempty"`
Push bool `json:"push,omitempty"` Push bool `json:"push,omitempty"`
} }
// StateType — StateType issue state type // StateType is the state of an issue or PR: "open", "closed".
//
// Usage:
//
// opts := StateType("example")
type StateType string type StateType string
// TimeStamp — TimeStamp defines a timestamp // TimeStamp is a Forgejo timestamp string.
// type TimeStamp string
// Usage:
//
// opts := TimeStamp(1)
type TimeStamp int64

View file

@ -4,134 +4,101 @@ package types
import "time" import "time"
// ContentsResponse — ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content // ContentsResponse — ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content
//
// Usage:
//
// opts := ContentsResponse{Name: "example"}
type ContentsResponse struct { type ContentsResponse struct {
Content string `json:"content,omitempty"` // `content` is populated when `type` is `file`, otherwise null Content string `json:"content,omitempty"` // `content` is populated when `type` is `file`, otherwise null
DownloadURL string `json:"download_url,omitempty"` DownloadURL string `json:"download_url,omitempty"`
Encoding string `json:"encoding,omitempty"` // `encoding` is populated when `type` is `file`, otherwise null Encoding string `json:"encoding,omitempty"` // `encoding` is populated when `type` is `file`, otherwise null
GitURL string `json:"git_url,omitempty"` GitURL string `json:"git_url,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
LastCommitSHA string `json:"last_commit_sha,omitempty"` LastCommitSHA string `json:"last_commit_sha,omitempty"`
Links *FileLinksResponse `json:"_links,omitempty"` Links *FileLinksResponse `json:"_links,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
Size int64 `json:"size,omitempty"` Size int64 `json:"size,omitempty"`
SubmoduleGitURL string `json:"submodule_git_url,omitempty"` // `submodule_git_url` is populated when `type` is `submodule`, otherwise null SubmoduleGitURL string `json:"submodule_git_url,omitempty"` // `submodule_git_url` is populated when `type` is `submodule`, otherwise null
Target string `json:"target,omitempty"` // `target` is populated when `type` is `symlink`, otherwise null Target string `json:"target,omitempty"` // `target` is populated when `type` is `symlink`, otherwise null
Type string `json:"type,omitempty"` // `type` will be `file`, `dir`, `symlink`, or `submodule` Type string `json:"type,omitempty"` // `type` will be `file`, `dir`, `symlink`, or `submodule`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// CreateFileOptions — CreateFileOptions options for creating files Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) // CreateFileOptions — CreateFileOptions options for creating files Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
//
// Usage:
//
// opts := CreateFileOptions{ContentBase64: "example"}
type CreateFileOptions struct { type CreateFileOptions struct {
Author *Identity `json:"author,omitempty"` Author *Identity `json:"author,omitempty"`
BranchName string `json:"branch,omitempty"` // branch (optional) to base this file from. if not given, the default branch is used BranchName string `json:"branch,omitempty"` // branch (optional) to base this file from. if not given, the default branch is used
Committer *Identity `json:"committer,omitempty"` Committer *Identity `json:"committer,omitempty"`
ContentBase64 string `json:"content"` // content must be base64 encoded ContentBase64 string `json:"content"` // content must be base64 encoded
Dates *CommitDateOptions `json:"dates,omitempty"` Dates *CommitDateOptions `json:"dates,omitempty"`
Message string `json:"message,omitempty"` // message (optional) for the commit of this file. if not supplied, a default message will be used Message string `json:"message,omitempty"` // message (optional) for the commit of this file. if not supplied, a default message will be used
NewBranchName string `json:"new_branch,omitempty"` // new_branch (optional) will make a new branch from `branch` before creating the file NewBranchName string `json:"new_branch,omitempty"` // new_branch (optional) will make a new branch from `branch` before creating the file
Signoff bool `json:"signoff,omitempty"` // Add a Signed-off-by trailer by the committer at the end of the commit log message. Signoff bool `json:"signoff,omitempty"` // Add a Signed-off-by trailer by the committer at the end of the commit log message.
} }
// DeleteFileOptions — DeleteFileOptions options for deleting files (used for other File structs below) Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) // DeleteFileOptions — DeleteFileOptions options for deleting files (used for other File structs below) Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
//
// Usage:
//
// opts := DeleteFileOptions{SHA: "example"}
type DeleteFileOptions struct { type DeleteFileOptions struct {
Author *Identity `json:"author,omitempty"` Author *Identity `json:"author,omitempty"`
BranchName string `json:"branch,omitempty"` // branch (optional) to base this file from. if not given, the default branch is used BranchName string `json:"branch,omitempty"` // branch (optional) to base this file from. if not given, the default branch is used
Committer *Identity `json:"committer,omitempty"` Committer *Identity `json:"committer,omitempty"`
Dates *CommitDateOptions `json:"dates,omitempty"` Dates *CommitDateOptions `json:"dates,omitempty"`
Message string `json:"message,omitempty"` // message (optional) for the commit of this file. if not supplied, a default message will be used Message string `json:"message,omitempty"` // message (optional) for the commit of this file. if not supplied, a default message will be used
NewBranchName string `json:"new_branch,omitempty"` // new_branch (optional) will make a new branch from `branch` before creating the file NewBranchName string `json:"new_branch,omitempty"` // new_branch (optional) will make a new branch from `branch` before creating the file
SHA string `json:"sha"` // sha is the SHA for the file that already exists SHA string `json:"sha"` // sha is the SHA for the file that already exists
Signoff bool `json:"signoff,omitempty"` // Add a Signed-off-by trailer by the committer at the end of the commit log message. Signoff bool `json:"signoff,omitempty"` // Add a Signed-off-by trailer by the committer at the end of the commit log message.
} }
// Usage:
//
// opts := FileCommitResponse{HTMLURL: "https://example.com"}
type FileCommitResponse struct { type FileCommitResponse struct {
Author *CommitUser `json:"author,omitempty"` Author *CommitUser `json:"author,omitempty"`
Committer *CommitUser `json:"committer,omitempty"` Committer *CommitUser `json:"committer,omitempty"`
Created time.Time `json:"created,omitempty"` Created time.Time `json:"created,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Parents []*CommitMeta `json:"parents,omitempty"` Parents []*CommitMeta `json:"parents,omitempty"`
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
Tree *CommitMeta `json:"tree,omitempty"` Tree *CommitMeta `json:"tree,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// FileDeleteResponse — FileDeleteResponse contains information about a repo's file that was deleted // FileDeleteResponse — FileDeleteResponse contains information about a repo's file that was deleted
//
// Usage:
//
// opts := FileDeleteResponse{Commit: &FileCommitResponse{}}
type FileDeleteResponse struct { type FileDeleteResponse struct {
Commit *FileCommitResponse `json:"commit,omitempty"` Commit *FileCommitResponse `json:"commit,omitempty"`
Content any `json:"content,omitempty"` Content any `json:"content,omitempty"`
Verification *PayloadCommitVerification `json:"verification,omitempty"` Verification *PayloadCommitVerification `json:"verification,omitempty"`
} }
// FileLinksResponse — FileLinksResponse contains the links for a repo's file // FileLinksResponse — FileLinksResponse contains the links for a repo's file
//
// Usage:
//
// opts := FileLinksResponse{GitURL: "https://example.com"}
type FileLinksResponse struct { type FileLinksResponse struct {
GitURL string `json:"git,omitempty"` GitURL string `json:"git,omitempty"`
HTMLURL string `json:"html,omitempty"` HTMLURL string `json:"html,omitempty"`
Self string `json:"self,omitempty"` Self string `json:"self,omitempty"`
} }
// FileResponse — FileResponse contains information about a repo's file // FileResponse — FileResponse contains information about a repo's file
//
// Usage:
//
// opts := FileResponse{Commit: &FileCommitResponse{}}
type FileResponse struct { type FileResponse struct {
Commit *FileCommitResponse `json:"commit,omitempty"` Commit *FileCommitResponse `json:"commit,omitempty"`
Content *ContentsResponse `json:"content,omitempty"` Content *ContentsResponse `json:"content,omitempty"`
Verification *PayloadCommitVerification `json:"verification,omitempty"` Verification *PayloadCommitVerification `json:"verification,omitempty"`
} }
// FilesResponse — FilesResponse contains information about multiple files from a repo // FilesResponse — FilesResponse contains information about multiple files from a repo
//
// Usage:
//
// opts := FilesResponse{Files: {}}
type FilesResponse struct { type FilesResponse struct {
Commit *FileCommitResponse `json:"commit,omitempty"` Commit *FileCommitResponse `json:"commit,omitempty"`
Files []*ContentsResponse `json:"files,omitempty"` Files []*ContentsResponse `json:"files,omitempty"`
Verification *PayloadCommitVerification `json:"verification,omitempty"` Verification *PayloadCommitVerification `json:"verification,omitempty"`
} }
// UpdateFileOptions — UpdateFileOptions options for updating files Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) // UpdateFileOptions — UpdateFileOptions options for updating files Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
//
// Usage:
//
// opts := UpdateFileOptions{ContentBase64: "example"}
type UpdateFileOptions struct { type UpdateFileOptions struct {
Author *Identity `json:"author,omitempty"` Author *Identity `json:"author,omitempty"`
BranchName string `json:"branch,omitempty"` // branch (optional) to base this file from. if not given, the default branch is used BranchName string `json:"branch,omitempty"` // branch (optional) to base this file from. if not given, the default branch is used
Committer *Identity `json:"committer,omitempty"` Committer *Identity `json:"committer,omitempty"`
ContentBase64 string `json:"content"` // content must be base64 encoded ContentBase64 string `json:"content"` // content must be base64 encoded
Dates *CommitDateOptions `json:"dates,omitempty"` Dates *CommitDateOptions `json:"dates,omitempty"`
FromPath string `json:"from_path,omitempty"` // from_path (optional) is the path of the original file which will be moved/renamed to the path in the URL FromPath string `json:"from_path,omitempty"` // from_path (optional) is the path of the original file which will be moved/renamed to the path in the URL
Message string `json:"message,omitempty"` // message (optional) for the commit of this file. if not supplied, a default message will be used Message string `json:"message,omitempty"` // message (optional) for the commit of this file. if not supplied, a default message will be used
NewBranchName string `json:"new_branch,omitempty"` // new_branch (optional) will make a new branch from `branch` before creating the file NewBranchName string `json:"new_branch,omitempty"` // new_branch (optional) will make a new branch from `branch` before creating the file
SHA string `json:"sha"` // sha is the SHA for the file that already exists SHA string `json:"sha"` // sha is the SHA for the file that already exists
Signoff bool `json:"signoff,omitempty"` // Add a Signed-off-by trailer by the committer at the end of the commit log message. Signoff bool `json:"signoff,omitempty"` // Add a Signed-off-by trailer by the committer at the end of the commit log message.
} }

View file

@ -2,61 +2,41 @@
package types package types
// APIError — APIError is an api error with a message // APIError — APIError is an api error with a message
//
// Usage:
//
// opts := APIError{Message: "example"}
type APIError struct { type APIError struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// Usage:
//
// opts := APIForbiddenError{Message: "example"}
type APIForbiddenError struct { type APIForbiddenError struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// Usage:
//
// opts := APIInvalidTopicsError{InvalidTopics: []string{"example"}}
type APIInvalidTopicsError struct { type APIInvalidTopicsError struct {
InvalidTopics []string `json:"invalidTopics,omitempty"` InvalidTopics []string `json:"invalidTopics,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
} }
// Usage:
//
// opts := APINotFound{Errors: []string{"example"}}
type APINotFound struct { type APINotFound struct {
Errors []string `json:"errors,omitempty"` Errors []string `json:"errors,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// Usage:
//
// opts := APIRepoArchivedError{Message: "example"}
type APIRepoArchivedError struct { type APIRepoArchivedError struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// Usage:
//
// opts := APIUnauthorizedError{Message: "example"}
type APIUnauthorizedError struct { type APIUnauthorizedError struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// Usage:
//
// opts := APIValidationError{Message: "example"}
type APIValidationError struct { type APIValidationError struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }

View file

@ -2,61 +2,43 @@
package types package types
// NodeInfo — NodeInfo contains standardized way of exposing metadata about a server running one of the distributed social networks // NodeInfo — NodeInfo contains standardized way of exposing metadata about a server running one of the distributed social networks
//
// Usage:
//
// opts := NodeInfo{Protocols: []string{"example"}}
type NodeInfo struct { type NodeInfo struct {
Metadata map[string]any `json:"metadata,omitempty"` Metadata map[string]any `json:"metadata,omitempty"`
OpenRegistrations bool `json:"openRegistrations,omitempty"` OpenRegistrations bool `json:"openRegistrations,omitempty"`
Protocols []string `json:"protocols,omitempty"` Protocols []string `json:"protocols,omitempty"`
Services *NodeInfoServices `json:"services,omitempty"` Services *NodeInfoServices `json:"services,omitempty"`
Software *NodeInfoSoftware `json:"software,omitempty"` Software *NodeInfoSoftware `json:"software,omitempty"`
Usage *NodeInfoUsage `json:"usage,omitempty"` Usage *NodeInfoUsage `json:"usage,omitempty"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
} }
// NodeInfoServices — NodeInfoServices contains the third party sites this server can connect to via their application API // NodeInfoServices — NodeInfoServices contains the third party sites this server can connect to via their application API
//
// Usage:
//
// opts := NodeInfoServices{Inbound: []string{"example"}}
type NodeInfoServices struct { type NodeInfoServices struct {
Inbound []string `json:"inbound,omitempty"` Inbound []string `json:"inbound,omitempty"`
Outbound []string `json:"outbound,omitempty"` Outbound []string `json:"outbound,omitempty"`
} }
// NodeInfoSoftware — NodeInfoSoftware contains Metadata about server software in use // NodeInfoSoftware — NodeInfoSoftware contains Metadata about server software in use
//
// Usage:
//
// opts := NodeInfoSoftware{Name: "example"}
type NodeInfoSoftware struct { type NodeInfoSoftware struct {
Homepage string `json:"homepage,omitempty"` Homepage string `json:"homepage,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Repository string `json:"repository,omitempty"` Repository string `json:"repository,omitempty"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
} }
// NodeInfoUsage — NodeInfoUsage contains usage statistics for this server // NodeInfoUsage — NodeInfoUsage contains usage statistics for this server
//
// Usage:
//
// opts := NodeInfoUsage{LocalComments: 1}
type NodeInfoUsage struct { type NodeInfoUsage struct {
LocalComments int64 `json:"localComments,omitempty"` LocalComments int64 `json:"localComments,omitempty"`
LocalPosts int64 `json:"localPosts,omitempty"` LocalPosts int64 `json:"localPosts,omitempty"`
Users *NodeInfoUsageUsers `json:"users,omitempty"` Users *NodeInfoUsageUsers `json:"users,omitempty"`
} }
// NodeInfoUsageUsers — NodeInfoUsageUsers contains statistics about the users of this server // NodeInfoUsageUsers — NodeInfoUsageUsers contains statistics about the users of this server
//
// Usage:
//
// opts := NodeInfoUsageUsers{ActiveHalfyear: 1}
type NodeInfoUsageUsers struct { type NodeInfoUsageUsers struct {
ActiveHalfyear int64 `json:"activeHalfyear,omitempty"` ActiveHalfyear int64 `json:"activeHalfyear,omitempty"`
ActiveMonth int64 `json:"activeMonth,omitempty"` ActiveMonth int64 `json:"activeMonth,omitempty"`
Total int64 `json:"total,omitempty"` Total int64 `json:"total,omitempty"`
} }

View file

@ -2,133 +2,93 @@
package types package types
// AnnotatedTag — AnnotatedTag represents an annotated tag // AnnotatedTag — AnnotatedTag represents an annotated tag
//
// Usage:
//
// opts := AnnotatedTag{Message: "example"}
type AnnotatedTag struct { type AnnotatedTag struct {
ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count,omitempty"` ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Object *AnnotatedTagObject `json:"object,omitempty"` Object *AnnotatedTagObject `json:"object,omitempty"`
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
Tag string `json:"tag,omitempty"` Tag string `json:"tag,omitempty"`
Tagger *CommitUser `json:"tagger,omitempty"` Tagger *CommitUser `json:"tagger,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Verification *PayloadCommitVerification `json:"verification,omitempty"` Verification *PayloadCommitVerification `json:"verification,omitempty"`
} }
// AnnotatedTagObject — AnnotatedTagObject contains meta information of the tag object // AnnotatedTagObject — AnnotatedTagObject contains meta information of the tag object
//
// Usage:
//
// opts := AnnotatedTagObject{SHA: "example"}
type AnnotatedTagObject struct { type AnnotatedTagObject struct {
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// ChangedFile — ChangedFile store information about files affected by the pull request // ChangedFile — ChangedFile store information about files affected by the pull request
//
// Usage:
//
// opts := ChangedFile{ContentsURL: "https://example.com"}
type ChangedFile struct { type ChangedFile struct {
Additions int64 `json:"additions,omitempty"` Additions int64 `json:"additions,omitempty"`
Changes int64 `json:"changes,omitempty"` Changes int64 `json:"changes,omitempty"`
ContentsURL string `json:"contents_url,omitempty"` ContentsURL string `json:"contents_url,omitempty"`
Deletions int64 `json:"deletions,omitempty"` Deletions int64 `json:"deletions,omitempty"`
Filename string `json:"filename,omitempty"` Filename string `json:"filename,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
PreviousFilename string `json:"previous_filename,omitempty"` PreviousFilename string `json:"previous_filename,omitempty"`
RawURL string `json:"raw_url,omitempty"` RawURL string `json:"raw_url,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
} }
// EditGitHookOption — EditGitHookOption options when modifying one Git hook // EditGitHookOption — EditGitHookOption options when modifying one Git hook
//
// Usage:
//
// opts := EditGitHookOption{Content: "example"}
type EditGitHookOption struct { type EditGitHookOption struct {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
} }
// GitBlobResponse — GitBlobResponse represents a git blob // GitBlobResponse — GitBlobResponse represents a git blob
//
// Usage:
//
// opts := GitBlobResponse{Content: "example"}
type GitBlobResponse struct { type GitBlobResponse struct {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Encoding string `json:"encoding,omitempty"` Encoding string `json:"encoding,omitempty"`
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
Size int64 `json:"size,omitempty"` Size int64 `json:"size,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// GitEntry — GitEntry represents a git tree // GitEntry — GitEntry represents a git tree
//
// Usage:
//
// opts := GitEntry{Mode: "example"}
type GitEntry struct { type GitEntry struct {
Mode string `json:"mode,omitempty"` Mode string `json:"mode,omitempty"`
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
Size int64 `json:"size,omitempty"` Size int64 `json:"size,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// GitHook — GitHook represents a Git repository hook // GitHook — GitHook represents a Git repository hook
//
// Usage:
//
// opts := GitHook{Name: "example"}
type GitHook struct { type GitHook struct {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
IsActive bool `json:"is_active,omitempty"` IsActive bool `json:"is_active,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
} }
// Usage:
//
// opts := GitObject{SHA: "example"}
type GitObject struct { type GitObject struct {
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// GitTreeResponse — GitTreeResponse returns a git tree // GitTreeResponse — GitTreeResponse returns a git tree
//
// Usage:
//
// opts := GitTreeResponse{SHA: "example"}
type GitTreeResponse struct { type GitTreeResponse struct {
Entries []*GitEntry `json:"tree,omitempty"` Entries []*GitEntry `json:"tree,omitempty"`
Page int64 `json:"page,omitempty"` Page int64 `json:"page,omitempty"`
SHA string `json:"sha,omitempty"` SHA string `json:"sha,omitempty"`
TotalCount int64 `json:"total_count,omitempty"` TotalCount int64 `json:"total_count,omitempty"`
Truncated bool `json:"truncated,omitempty"` Truncated bool `json:"truncated,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// Note — Note contains information related to a git note // Note — Note contains information related to a git note
//
// Usage:
//
// opts := Note{Message: "example"}
type Note struct { type Note struct {
Commit *Commit `json:"commit,omitempty"` Commit *Commit `json:"commit,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
} }
// Usage:
//
// opts := NoteOptions{Message: "example"}
type NoteOptions struct { type NoteOptions struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
} }

View file

@ -4,87 +4,66 @@ package types
import "time" import "time"
// CreateHookOption — CreateHookOption options when create a hook // CreateHookOption — CreateHookOption options when create a hook
//
// Usage:
//
// opts := CreateHookOption{Type: "example"}
type CreateHookOption struct { type CreateHookOption struct {
Active bool `json:"active,omitempty"` Active bool `json:"active,omitempty"`
AuthorizationHeader string `json:"authorization_header,omitempty"` AuthorizationHeader string `json:"authorization_header,omitempty"`
BranchFilter string `json:"branch_filter,omitempty"` BranchFilter string `json:"branch_filter,omitempty"`
Config *CreateHookOptionConfig `json:"config"` Config *CreateHookOptionConfig `json:"config"`
Events []string `json:"events,omitempty"` Events []string `json:"events,omitempty"`
Type string `json:"type"` Type string `json:"type"`
} }
// CreateHookOptionConfig — CreateHookOptionConfig has all config options in it required are "content_type" and "url" Required // CreateHookOptionConfig — CreateHookOptionConfig has all config options in it required are "content_type" and "url" Required
// // CreateHookOptionConfig has no fields in the swagger spec.
// Usage: type CreateHookOptionConfig struct{}
//
// opts := CreateHookOptionConfig(map[string]any{"key": "value"})
type CreateHookOptionConfig map[string]any
// EditHookOption — EditHookOption options when modify one hook // EditHookOption — EditHookOption options when modify one hook
//
// Usage:
//
// opts := EditHookOption{AuthorizationHeader: "example"}
type EditHookOption struct { type EditHookOption struct {
Active bool `json:"active,omitempty"` Active bool `json:"active,omitempty"`
AuthorizationHeader string `json:"authorization_header,omitempty"` AuthorizationHeader string `json:"authorization_header,omitempty"`
BranchFilter string `json:"branch_filter,omitempty"` BranchFilter string `json:"branch_filter,omitempty"`
Config map[string]string `json:"config,omitempty"` Config map[string]any `json:"config,omitempty"`
Events []string `json:"events,omitempty"` Events []string `json:"events,omitempty"`
} }
// Hook — Hook a hook is a web hook when one repository changed // Hook — Hook a hook is a web hook when one repository changed
//
// Usage:
//
// opts := Hook{AuthorizationHeader: "example"}
type Hook struct { type Hook struct {
Active bool `json:"active,omitempty"` Active bool `json:"active,omitempty"`
AuthorizationHeader string `json:"authorization_header,omitempty"` AuthorizationHeader string `json:"authorization_header,omitempty"`
BranchFilter string `json:"branch_filter,omitempty"` BranchFilter string `json:"branch_filter,omitempty"`
Config map[string]string `json:"config,omitempty"` // Deprecated: use Metadata instead Config map[string]any `json:"config,omitempty"` // Deprecated: use Metadata instead
ContentType string `json:"content_type,omitempty"` ContentType string `json:"content_type,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Events []string `json:"events,omitempty"` Events []string `json:"events,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Metadata any `json:"metadata,omitempty"` Metadata any `json:"metadata,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
} }
// PayloadCommit — PayloadCommit represents a commit // PayloadCommit — PayloadCommit represents a commit
//
// Usage:
//
// opts := PayloadCommit{Added: []string{"example"}}
type PayloadCommit struct { type PayloadCommit struct {
Added []string `json:"added,omitempty"` Added []string `json:"added,omitempty"`
Author *PayloadUser `json:"author,omitempty"` Author *PayloadUser `json:"author,omitempty"`
Committer *PayloadUser `json:"committer,omitempty"` Committer *PayloadUser `json:"committer,omitempty"`
ID string `json:"id,omitempty"` // sha1 hash of the commit ID string `json:"id,omitempty"` // sha1 hash of the commit
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Modified []string `json:"modified,omitempty"` Modified []string `json:"modified,omitempty"`
Removed []string `json:"removed,omitempty"` Removed []string `json:"removed,omitempty"`
Timestamp time.Time `json:"timestamp,omitempty"` Timestamp time.Time `json:"timestamp,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Verification *PayloadCommitVerification `json:"verification,omitempty"` Verification *PayloadCommitVerification `json:"verification,omitempty"`
} }
// PayloadCommitVerification — PayloadCommitVerification represents the GPG verification of a commit // PayloadCommitVerification — PayloadCommitVerification represents the GPG verification of a commit
//
// Usage:
//
// opts := PayloadCommitVerification{Payload: "example"}
type PayloadCommitVerification struct { type PayloadCommitVerification struct {
Payload string `json:"payload,omitempty"` Payload string `json:"payload,omitempty"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
Signature string `json:"signature,omitempty"` Signature string `json:"signature,omitempty"`
Signer *PayloadUser `json:"signer,omitempty"` Signer *PayloadUser `json:"signer,omitempty"`
Verified bool `json:"verified,omitempty"` Verified bool `json:"verified,omitempty"`
} }

View file

@ -4,200 +4,142 @@ package types
import "time" import "time"
// CreateIssueCommentOption — CreateIssueCommentOption options for creating a comment on an issue // CreateIssueCommentOption — CreateIssueCommentOption options for creating a comment on an issue
//
// Usage:
//
// opts := CreateIssueCommentOption{Body: "example"}
type CreateIssueCommentOption struct { type CreateIssueCommentOption struct {
Body string `json:"body"` Body string `json:"body"`
Updated time.Time `json:"updated_at,omitempty"` Updated *time.Time `json:"updated_at,omitempty"`
} }
// CreateIssueOption — CreateIssueOption options to create one issue // CreateIssueOption — CreateIssueOption options to create one issue
//
// Usage:
//
// opts := CreateIssueOption{Title: "example"}
type CreateIssueOption struct { type CreateIssueOption struct {
Assignee string `json:"assignee,omitempty"` // deprecated Assignee string `json:"assignee,omitempty"` // deprecated
Assignees []string `json:"assignees,omitempty"` Assignees []string `json:"assignees,omitempty"`
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Closed bool `json:"closed,omitempty"` Closed bool `json:"closed,omitempty"`
Deadline time.Time `json:"due_date,omitempty"` Deadline time.Time `json:"due_date,omitempty"`
Labels []int64 `json:"labels,omitempty"` // list of label ids Labels []int64 `json:"labels,omitempty"` // list of label ids
Milestone int64 `json:"milestone,omitempty"` // milestone id Milestone int64 `json:"milestone,omitempty"` // milestone id
Ref string `json:"ref,omitempty"` Ref string `json:"ref,omitempty"`
Title string `json:"title"` Title string `json:"title"`
} }
// EditDeadlineOption — EditDeadlineOption options for creating a deadline // EditDeadlineOption — EditDeadlineOption options for creating a deadline
//
// Usage:
//
// opts := EditDeadlineOption{Deadline: time.Now()}
type EditDeadlineOption struct { type EditDeadlineOption struct {
Deadline time.Time `json:"due_date"` Deadline time.Time `json:"due_date"`
} }
// EditIssueCommentOption — EditIssueCommentOption options for editing a comment // EditIssueCommentOption — EditIssueCommentOption options for editing a comment
//
// Usage:
//
// opts := EditIssueCommentOption{Body: "example"}
type EditIssueCommentOption struct { type EditIssueCommentOption struct {
Body string `json:"body"` Body string `json:"body"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
} }
// EditIssueOption — EditIssueOption options for editing an issue // EditIssueOption — EditIssueOption options for editing an issue
//
// Usage:
//
// opts := EditIssueOption{Body: "example"}
type EditIssueOption struct { type EditIssueOption struct {
Assignee string `json:"assignee,omitempty"` // deprecated Assignee string `json:"assignee,omitempty"` // deprecated
Assignees []string `json:"assignees,omitempty"` Assignees []string `json:"assignees,omitempty"`
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Deadline time.Time `json:"due_date,omitempty"` Deadline time.Time `json:"due_date,omitempty"`
Milestone int64 `json:"milestone,omitempty"` Milestone int64 `json:"milestone,omitempty"`
Ref string `json:"ref,omitempty"` Ref string `json:"ref,omitempty"`
RemoveDeadline bool `json:"unset_due_date,omitempty"` RemoveDeadline bool `json:"unset_due_date,omitempty"`
State string `json:"state,omitempty"` State string `json:"state,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
} }
// Issue — Issue represents an issue in a repository // Issue — Issue represents an issue in a repository
//
// Usage:
//
// opts := Issue{Body: "example"}
type Issue struct { type Issue struct {
Assignee *User `json:"assignee,omitempty"` Assignee *User `json:"assignee,omitempty"`
Assignees []*User `json:"assignees,omitempty"` Assignees []*User `json:"assignees,omitempty"`
Attachments []*Attachment `json:"assets,omitempty"` Attachments []*Attachment `json:"assets,omitempty"`
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Closed time.Time `json:"closed_at,omitempty"` Closed time.Time `json:"closed_at,omitempty"`
Comments int64 `json:"comments,omitempty"` Comments int64 `json:"comments,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Deadline time.Time `json:"due_date,omitempty"` Deadline time.Time `json:"due_date,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Index int64 `json:"number,omitempty"` Index int64 `json:"number,omitempty"`
IsLocked bool `json:"is_locked,omitempty"` IsLocked bool `json:"is_locked,omitempty"`
Labels []*Label `json:"labels,omitempty"` Labels []*Label `json:"labels,omitempty"`
Milestone *Milestone `json:"milestone,omitempty"` Milestone *Milestone `json:"milestone,omitempty"`
OriginalAuthor string `json:"original_author,omitempty"` OriginalAuthor string `json:"original_author,omitempty"`
OriginalAuthorID int64 `json:"original_author_id,omitempty"` OriginalAuthorID int64 `json:"original_author_id,omitempty"`
PinOrder int64 `json:"pin_order,omitempty"` PinOrder int64 `json:"pin_order,omitempty"`
PullRequest *PullRequestMeta `json:"pull_request,omitempty"` PullRequest *PullRequestMeta `json:"pull_request,omitempty"`
Ref string `json:"ref,omitempty"` Ref string `json:"ref,omitempty"`
Repository *RepositoryMeta `json:"repository,omitempty"` Repository *RepositoryMeta `json:"repository,omitempty"`
State StateType `json:"state,omitempty"` State StateType `json:"state,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }
// Usage:
//
// opts := IssueConfig{BlankIssuesEnabled: true}
type IssueConfig struct { type IssueConfig struct {
BlankIssuesEnabled bool `json:"blank_issues_enabled,omitempty"` BlankIssuesEnabled bool `json:"blank_issues_enabled,omitempty"`
ContactLinks []*IssueConfigContactLink `json:"contact_links,omitempty"` ContactLinks []*IssueConfigContactLink `json:"contact_links,omitempty"`
} }
// Usage:
//
// opts := IssueConfigContactLink{Name: "example"}
type IssueConfigContactLink struct { type IssueConfigContactLink struct {
About string `json:"about,omitempty"` About string `json:"about,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// Usage:
//
// opts := IssueConfigValidation{Message: "example"}
type IssueConfigValidation struct { type IssueConfigValidation struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Valid bool `json:"valid,omitempty"` Valid bool `json:"valid,omitempty"`
} }
// IssueDeadline — IssueDeadline represents an issue deadline // IssueDeadline — IssueDeadline represents an issue deadline
//
// Usage:
//
// opts := IssueDeadline{Deadline: time.Now()}
type IssueDeadline struct { type IssueDeadline struct {
Deadline time.Time `json:"due_date,omitempty"` Deadline time.Time `json:"due_date,omitempty"`
} }
// IssueFormField — IssueFormField represents a form field // IssueFormField — IssueFormField represents a form field
//
// Usage:
//
// opts := IssueFormField{ID: "example"}
type IssueFormField struct { type IssueFormField struct {
Attributes map[string]any `json:"attributes,omitempty"` Attributes map[string]any `json:"attributes,omitempty"`
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Type IssueFormFieldType `json:"type,omitempty"` Type *IssueFormFieldType `json:"type,omitempty"`
Validations map[string]any `json:"validations,omitempty"` Validations map[string]any `json:"validations,omitempty"`
Visible []IssueFormFieldVisible `json:"visible,omitempty"` Visible []*IssueFormFieldVisible `json:"visible,omitempty"`
} }
// Usage: // IssueFormFieldType has no fields in the swagger spec.
// type IssueFormFieldType struct{}
// opts := IssueFormFieldType("example")
type IssueFormFieldType string
// IssueFormFieldVisible — IssueFormFieldVisible defines issue form field visible // IssueFormFieldVisible — IssueFormFieldVisible defines issue form field visible
// // IssueFormFieldVisible has no fields in the swagger spec.
// Usage: type IssueFormFieldVisible struct{}
//
// opts := IssueFormFieldVisible("example")
type IssueFormFieldVisible string
// IssueLabelsOption — IssueLabelsOption a collection of labels // IssueLabelsOption — IssueLabelsOption a collection of labels
//
// Usage:
//
// opts := IssueLabelsOption{Updated: time.Now()}
type IssueLabelsOption struct { type IssueLabelsOption struct {
Labels []any `json:"labels,omitempty"` // Labels can be a list of integers representing label IDs or a list of strings representing label names Labels []any `json:"labels,omitempty"` // Labels can be a list of integers representing label IDs or a list of strings representing label names
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
} }
// IssueMeta — IssueMeta basic issue information // IssueMeta — IssueMeta basic issue information
//
// Usage:
//
// opts := IssueMeta{Name: "example"}
type IssueMeta struct { type IssueMeta struct {
Index int64 `json:"index,omitempty"` Index int64 `json:"index,omitempty"`
Name string `json:"repo,omitempty"` Name string `json:"repo,omitempty"`
Owner string `json:"owner,omitempty"` Owner string `json:"owner,omitempty"`
} }
// IssueTemplate — IssueTemplate represents an issue template for a repository // IssueTemplate — IssueTemplate represents an issue template for a repository
//
// Usage:
//
// opts := IssueTemplate{FileName: "example"}
type IssueTemplate struct { type IssueTemplate struct {
About string `json:"about,omitempty"` About string `json:"about,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Fields []*IssueFormField `json:"body,omitempty"` Fields []*IssueFormField `json:"body,omitempty"`
FileName string `json:"file_name,omitempty"` FileName string `json:"file_name,omitempty"`
Labels IssueTemplateLabels `json:"labels,omitempty"` Labels *IssueTemplateLabels `json:"labels,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Ref string `json:"ref,omitempty"` Ref string `json:"ref,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
} }
// Usage: // IssueTemplateLabels has no fields in the swagger spec.
// type IssueTemplateLabels struct{}
// opts := IssueTemplateLabels([]string{"example"})
type IssueTemplateLabels []string

View file

@ -4,88 +4,66 @@ package types
import "time" import "time"
// CreateGPGKeyOption — CreateGPGKeyOption options create user GPG key // CreateGPGKeyOption — CreateGPGKeyOption options create user GPG key
//
// Usage:
//
// opts := CreateGPGKeyOption{ArmoredKey: "example"}
type CreateGPGKeyOption struct { type CreateGPGKeyOption struct {
ArmoredKey string `json:"armored_public_key"` // An armored GPG key to add ArmoredKey string `json:"armored_public_key"` // An armored GPG key to add
Signature string `json:"armored_signature,omitempty"` Signature string `json:"armored_signature,omitempty"`
} }
// CreateKeyOption — CreateKeyOption options when creating a key // CreateKeyOption — CreateKeyOption options when creating a key
//
// Usage:
//
// opts := CreateKeyOption{Title: "example"}
type CreateKeyOption struct { type CreateKeyOption struct {
Key string `json:"key"` // An armored SSH key to add Key string `json:"key"` // An armored SSH key to add
ReadOnly bool `json:"read_only,omitempty"` // Describe if the key has only read access or read/write ReadOnly bool `json:"read_only,omitempty"` // Describe if the key has only read access or read/write
Title string `json:"title"` // Title of the key to add Title string `json:"title"` // Title of the key to add
} }
// DeployKey — DeployKey a deploy key // DeployKey — DeployKey a deploy key
//
// Usage:
//
// opts := DeployKey{Title: "example"}
type DeployKey struct { type DeployKey struct {
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"` Fingerprint string `json:"fingerprint,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Key string `json:"key,omitempty"` Key string `json:"key,omitempty"`
KeyID int64 `json:"key_id,omitempty"` KeyID int64 `json:"key_id,omitempty"`
ReadOnly bool `json:"read_only,omitempty"` ReadOnly bool `json:"read_only,omitempty"`
Repository *Repository `json:"repository,omitempty"` Repository *Repository `json:"repository,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// GPGKey — GPGKey a user GPG key to sign commit and tag in repository // GPGKey — GPGKey a user GPG key to sign commit and tag in repository
//
// Usage:
//
// opts := GPGKey{KeyID: "example"}
type GPGKey struct { type GPGKey struct {
CanCertify bool `json:"can_certify,omitempty"` CanCertify bool `json:"can_certify,omitempty"`
CanEncryptComms bool `json:"can_encrypt_comms,omitempty"` CanEncryptComms bool `json:"can_encrypt_comms,omitempty"`
CanEncryptStorage bool `json:"can_encrypt_storage,omitempty"` CanEncryptStorage bool `json:"can_encrypt_storage,omitempty"`
CanSign bool `json:"can_sign,omitempty"` CanSign bool `json:"can_sign,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Emails []*GPGKeyEmail `json:"emails,omitempty"` Emails []*GPGKeyEmail `json:"emails,omitempty"`
Expires time.Time `json:"expires_at,omitempty"` Expires time.Time `json:"expires_at,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
KeyID string `json:"key_id,omitempty"` KeyID string `json:"key_id,omitempty"`
PrimaryKeyID string `json:"primary_key_id,omitempty"` PrimaryKeyID string `json:"primary_key_id,omitempty"`
PublicKey string `json:"public_key,omitempty"` PublicKey string `json:"public_key,omitempty"`
SubsKey []*GPGKey `json:"subkeys,omitempty"` SubsKey []*GPGKey `json:"subkeys,omitempty"`
Verified bool `json:"verified,omitempty"` Verified bool `json:"verified,omitempty"`
} }
// GPGKeyEmail — GPGKeyEmail an email attached to a GPGKey // GPGKeyEmail — GPGKeyEmail an email attached to a GPGKey
//
// Usage:
//
// opts := GPGKeyEmail{Email: "alice@example.com"}
type GPGKeyEmail struct { type GPGKeyEmail struct {
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Verified bool `json:"verified,omitempty"` Verified bool `json:"verified,omitempty"`
} }
// PublicKey — PublicKey publickey is a user key to push code to repository // PublicKey — PublicKey publickey is a user key to push code to repository
//
// Usage:
//
// opts := PublicKey{Title: "example"}
type PublicKey struct { type PublicKey struct {
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"` Fingerprint string `json:"fingerprint,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Key string `json:"key,omitempty"` Key string `json:"key,omitempty"`
KeyType string `json:"key_type,omitempty"` KeyType string `json:"key_type,omitempty"`
ReadOnly bool `json:"read_only,omitempty"` ReadOnly bool `json:"read_only,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }

View file

@ -4,64 +4,46 @@ package types
import "time" import "time"
// CreateLabelOption — CreateLabelOption options for creating a label // CreateLabelOption — CreateLabelOption options for creating a label
//
// Usage:
//
// opts := CreateLabelOption{Name: "example"}
type CreateLabelOption struct { type CreateLabelOption struct {
Color string `json:"color"` Color string `json:"color"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Exclusive bool `json:"exclusive,omitempty"` Exclusive bool `json:"exclusive,omitempty"`
IsArchived bool `json:"is_archived,omitempty"` IsArchived bool `json:"is_archived,omitempty"`
Name string `json:"name"` Name string `json:"name"`
} }
// DeleteLabelsOption — DeleteLabelOption options for deleting a label // DeleteLabelsOption — DeleteLabelOption options for deleting a label
//
// Usage:
//
// opts := DeleteLabelsOption{Updated: time.Now()}
type DeleteLabelsOption struct { type DeleteLabelsOption struct {
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
} }
// EditLabelOption — EditLabelOption options for editing a label // EditLabelOption — EditLabelOption options for editing a label
//
// Usage:
//
// opts := EditLabelOption{Description: "example"}
type EditLabelOption struct { type EditLabelOption struct {
Color string `json:"color,omitempty"` Color string `json:"color,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Exclusive bool `json:"exclusive,omitempty"` Exclusive bool `json:"exclusive,omitempty"`
IsArchived bool `json:"is_archived,omitempty"` IsArchived bool `json:"is_archived,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
} }
// Label — Label a label to an issue or a pr // Label — Label a label to an issue or a pr
//
// Usage:
//
// opts := Label{Description: "example"}
type Label struct { type Label struct {
Color string `json:"color,omitempty"` Color string `json:"color,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Exclusive bool `json:"exclusive,omitempty"` Exclusive bool `json:"exclusive,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
IsArchived bool `json:"is_archived,omitempty"` IsArchived bool `json:"is_archived,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// LabelTemplate — LabelTemplate info of a Label template // LabelTemplate — LabelTemplate info of a Label template
//
// Usage:
//
// opts := LabelTemplate{Description: "example"}
type LabelTemplate struct { type LabelTemplate struct {
Color string `json:"color,omitempty"` Color string `json:"color,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Exclusive bool `json:"exclusive,omitempty"` Exclusive bool `json:"exclusive,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
} }

View file

@ -4,44 +4,34 @@ package types
import "time" import "time"
// CreateMilestoneOption — CreateMilestoneOption options for creating a milestone // CreateMilestoneOption — CreateMilestoneOption options for creating a milestone
//
// Usage:
//
// opts := CreateMilestoneOption{Description: "example"}
type CreateMilestoneOption struct { type CreateMilestoneOption struct {
Deadline time.Time `json:"due_on,omitempty"` Deadline time.Time `json:"due_on,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
State string `json:"state,omitempty"` State string `json:"state,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
} }
// EditMilestoneOption — EditMilestoneOption options for editing a milestone // EditMilestoneOption — EditMilestoneOption options for editing a milestone
//
// Usage:
//
// opts := EditMilestoneOption{Description: "example"}
type EditMilestoneOption struct { type EditMilestoneOption struct {
Deadline time.Time `json:"due_on,omitempty"` Deadline time.Time `json:"due_on,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
State string `json:"state,omitempty"` State string `json:"state,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
} }
// Milestone — Milestone milestone is a collection of issues on one repository // Milestone — Milestone milestone is a collection of issues on one repository
//
// Usage:
//
// opts := Milestone{Description: "example"}
type Milestone struct { type Milestone struct {
Closed time.Time `json:"closed_at,omitempty"` Closed time.Time `json:"closed_at,omitempty"`
ClosedIssues int64 `json:"closed_issues,omitempty"` ClosedIssues int64 `json:"closed_issues,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Deadline time.Time `json:"due_on,omitempty"` Deadline time.Time `json:"due_on,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
OpenIssues int64 `json:"open_issues,omitempty"` OpenIssues int64 `json:"open_issues,omitempty"`
State StateType `json:"state,omitempty"` State StateType `json:"state,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
} }

View file

@ -4,360 +4,252 @@ package types
import "time" import "time"
// AddCollaboratorOption — AddCollaboratorOption options when adding a user as a collaborator of a repository // AddCollaboratorOption — AddCollaboratorOption options when adding a user as a collaborator of a repository
//
// Usage:
//
// opts := AddCollaboratorOption{Permission: "example"}
type AddCollaboratorOption struct { type AddCollaboratorOption struct {
Permission string `json:"permission,omitempty"` Permission string `json:"permission,omitempty"`
} }
// AddTimeOption — AddTimeOption options for adding time to an issue // AddTimeOption — AddTimeOption options for adding time to an issue
//
// Usage:
//
// opts := AddTimeOption{Time: 1}
type AddTimeOption struct { type AddTimeOption struct {
Created time.Time `json:"created,omitempty"` Created time.Time `json:"created,omitempty"`
Time int64 `json:"time"` // time in seconds Time int64 `json:"time"` // time in seconds
User string `json:"user_name,omitempty"` // User who spent the time (optional) User string `json:"user_name,omitempty"` // User who spent the time (optional)
} }
// ChangeFileOperation — ChangeFileOperation for creating, updating or deleting a file // ChangeFileOperation — ChangeFileOperation for creating, updating or deleting a file
//
// Usage:
//
// opts := ChangeFileOperation{Operation: "example"}
type ChangeFileOperation struct { type ChangeFileOperation struct {
ContentBase64 string `json:"content,omitempty"` // new or updated file content, must be base64 encoded ContentBase64 string `json:"content,omitempty"` // new or updated file content, must be base64 encoded
FromPath string `json:"from_path,omitempty"` // old path of the file to move FromPath string `json:"from_path,omitempty"` // old path of the file to move
Operation string `json:"operation"` // indicates what to do with the file Operation string `json:"operation"` // indicates what to do with the file
Path string `json:"path"` // path to the existing or new file Path string `json:"path"` // path to the existing or new file
SHA string `json:"sha,omitempty"` // sha is the SHA for the file that already exists, required for update or delete SHA string `json:"sha,omitempty"` // sha is the SHA for the file that already exists, required for update or delete
} }
// ChangeFilesOptions — ChangeFilesOptions options for creating, updating or deleting multiple files Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) // ChangeFilesOptions — ChangeFilesOptions options for creating, updating or deleting multiple files Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
//
// Usage:
//
// opts := ChangeFilesOptions{Files: {}}
type ChangeFilesOptions struct { type ChangeFilesOptions struct {
Author *Identity `json:"author,omitempty"` Author *Identity `json:"author,omitempty"`
BranchName string `json:"branch,omitempty"` // branch (optional) to base this file from. if not given, the default branch is used BranchName string `json:"branch,omitempty"` // branch (optional) to base this file from. if not given, the default branch is used
Committer *Identity `json:"committer,omitempty"` Committer *Identity `json:"committer,omitempty"`
Dates *CommitDateOptions `json:"dates,omitempty"` Dates *CommitDateOptions `json:"dates,omitempty"`
Files []*ChangeFileOperation `json:"files"` // list of file operations Files []*ChangeFileOperation `json:"files"` // list of file operations
Message string `json:"message,omitempty"` // message (optional) for the commit of this file. if not supplied, a default message will be used Message string `json:"message,omitempty"` // message (optional) for the commit of this file. if not supplied, a default message will be used
NewBranchName string `json:"new_branch,omitempty"` // new_branch (optional) will make a new branch from `branch` before creating the file NewBranchName string `json:"new_branch,omitempty"` // new_branch (optional) will make a new branch from `branch` before creating the file
Signoff bool `json:"signoff,omitempty"` // Add a Signed-off-by trailer by the committer at the end of the commit log message. Signoff bool `json:"signoff,omitempty"` // Add a Signed-off-by trailer by the committer at the end of the commit log message.
} }
// Usage:
//
// opts := Compare{TotalCommits: 1}
type Compare struct { type Compare struct {
Commits []*Commit `json:"commits,omitempty"` Commits []*Commit `json:"commits,omitempty"`
TotalCommits int64 `json:"total_commits,omitempty"` TotalCommits int64 `json:"total_commits,omitempty"`
} }
// CreateForkOption — CreateForkOption options for creating a fork // CreateForkOption — CreateForkOption options for creating a fork
//
// Usage:
//
// opts := CreateForkOption{Name: "example"}
type CreateForkOption struct { type CreateForkOption struct {
Name string `json:"name,omitempty"` // name of the forked repository Name string `json:"name,omitempty"` // name of the forked repository
Organization string `json:"organization,omitempty"` // organization name, if forking into an organization Organization string `json:"organization,omitempty"` // organization name, if forking into an organization
} }
// CreateOrUpdateSecretOption — CreateOrUpdateSecretOption options when creating or updating secret // CreateOrUpdateSecretOption — CreateOrUpdateSecretOption options when creating or updating secret
//
// Usage:
//
// opts := CreateOrUpdateSecretOption{Data: "example"}
type CreateOrUpdateSecretOption struct { type CreateOrUpdateSecretOption struct {
Data string `json:"data"` // Data of the secret to update Data string `json:"data"` // Data of the secret to update
} }
// DismissPullReviewOptions — DismissPullReviewOptions are options to dismiss a pull review // DismissPullReviewOptions — DismissPullReviewOptions are options to dismiss a pull review
//
// Usage:
//
// opts := DismissPullReviewOptions{Message: "example"}
type DismissPullReviewOptions struct { type DismissPullReviewOptions struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Priors bool `json:"priors,omitempty"` Priors bool `json:"priors,omitempty"`
} }
// ForgeLike — ForgeLike activity data type // ForgeLike — ForgeLike activity data type
//
// Usage:
//
// opts := ForgeLike{}
//
// ForgeLike has no fields in the swagger spec. // ForgeLike has no fields in the swagger spec.
type ForgeLike struct{} type ForgeLike struct{}
// GenerateRepoOption — GenerateRepoOption options when creating repository using a template // GenerateRepoOption — GenerateRepoOption options when creating repository using a template
//
// Usage:
//
// opts := GenerateRepoOption{Name: "example"}
type GenerateRepoOption struct { type GenerateRepoOption struct {
Avatar bool `json:"avatar,omitempty"` // include avatar of the template repo Avatar bool `json:"avatar,omitempty"` // include avatar of the template repo
DefaultBranch string `json:"default_branch,omitempty"` // Default branch of the new repository DefaultBranch string `json:"default_branch,omitempty"` // Default branch of the new repository
Description string `json:"description,omitempty"` // Description of the repository to create Description string `json:"description,omitempty"` // Description of the repository to create
GitContent bool `json:"git_content,omitempty"` // include git content of default branch in template repo GitContent bool `json:"git_content,omitempty"` // include git content of default branch in template repo
GitHooks bool `json:"git_hooks,omitempty"` // include git hooks in template repo GitHooks bool `json:"git_hooks,omitempty"` // include git hooks in template repo
Labels bool `json:"labels,omitempty"` // include labels in template repo Labels bool `json:"labels,omitempty"` // include labels in template repo
Name string `json:"name"` // Name of the repository to create Name string `json:"name"` // Name of the repository to create
Owner string `json:"owner"` // The organization or person who will own the new repository Owner string `json:"owner"` // The organization or person who will own the new repository
Private bool `json:"private,omitempty"` // Whether the repository is private Private bool `json:"private,omitempty"` // Whether the repository is private
ProtectedBranch bool `json:"protected_branch,omitempty"` // include protected branches in template repo ProtectedBranch bool `json:"protected_branch,omitempty"` // include protected branches in template repo
Topics bool `json:"topics,omitempty"` // include topics in template repo Topics bool `json:"topics,omitempty"` // include topics in template repo
Webhooks bool `json:"webhooks,omitempty"` // include webhooks in template repo Webhooks bool `json:"webhooks,omitempty"` // include webhooks in template repo
} }
// GitignoreTemplateInfo — GitignoreTemplateInfo name and text of a gitignore template // GitignoreTemplateInfo — GitignoreTemplateInfo name and text of a gitignore template
//
// Usage:
//
// opts := GitignoreTemplateInfo{Name: "example"}
type GitignoreTemplateInfo struct { type GitignoreTemplateInfo struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
} }
// Identity — Identity for a person's identity like an author or committer // Identity — Identity for a person's identity like an author or committer
//
// Usage:
//
// opts := Identity{Name: "example"}
type Identity struct { type Identity struct {
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
} }
// LicenseTemplateInfo — LicensesInfo contains information about a License // LicenseTemplateInfo — LicensesInfo contains information about a License
//
// Usage:
//
// opts := LicenseTemplateInfo{Body: "example"}
type LicenseTemplateInfo struct { type LicenseTemplateInfo struct {
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Implementation string `json:"implementation,omitempty"` Implementation string `json:"implementation,omitempty"`
Key string `json:"key,omitempty"` Key string `json:"key,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// LicensesTemplateListEntry — LicensesListEntry is used for the API // LicensesTemplateListEntry — LicensesListEntry is used for the API
//
// Usage:
//
// opts := LicensesTemplateListEntry{Name: "example"}
type LicensesTemplateListEntry struct { type LicensesTemplateListEntry struct {
Key string `json:"key,omitempty"` Key string `json:"key,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// MarkdownOption — MarkdownOption markdown options // MarkdownOption — MarkdownOption markdown options
//
// Usage:
//
// opts := MarkdownOption{Context: "example"}
type MarkdownOption struct { type MarkdownOption struct {
Context string `json:"Context,omitempty"` // Context to render in: body Context string `json:"Context,omitempty"` // Context to render in: body
Mode string `json:"Mode,omitempty"` // Mode to render (comment, gfm, markdown) in: body Mode string `json:"Mode,omitempty"` // Mode to render (comment, gfm, markdown) in: body
Text string `json:"Text,omitempty"` // Text markdown to render in: body Text string `json:"Text,omitempty"` // Text markdown to render in: body
Wiki bool `json:"Wiki,omitempty"` // Is it a wiki page ? in: body Wiki bool `json:"Wiki,omitempty"` // Is it a wiki page ? in: body
} }
// MarkupOption — MarkupOption markup options // MarkupOption — MarkupOption markup options
//
// Usage:
//
// opts := MarkupOption{BranchPath: "main"}
type MarkupOption struct { type MarkupOption struct {
BranchPath string `json:"BranchPath,omitempty"` // The current branch path where the form gets posted in: body BranchPath string `json:"BranchPath,omitempty"` // The current branch path where the form gets posted in: body
Context string `json:"Context,omitempty"` // Context to render in: body Context string `json:"Context,omitempty"` // Context to render in: body
FilePath string `json:"FilePath,omitempty"` // File path for detecting extension in file mode in: body FilePath string `json:"FilePath,omitempty"` // File path for detecting extension in file mode in: body
Mode string `json:"Mode,omitempty"` // Mode to render (comment, gfm, markdown, file) in: body Mode string `json:"Mode,omitempty"` // Mode to render (comment, gfm, markdown, file) in: body
Text string `json:"Text,omitempty"` // Text markup to render in: body Text string `json:"Text,omitempty"` // Text markup to render in: body
Wiki bool `json:"Wiki,omitempty"` // Is it a wiki page ? in: body Wiki bool `json:"Wiki,omitempty"` // Is it a wiki page ? in: body
} }
// MergePullRequestOption — MergePullRequestForm form for merging Pull Request // MergePullRequestOption — MergePullRequestForm form for merging Pull Request
//
// Usage:
//
// opts := MergePullRequestOption{Do: "example"}
type MergePullRequestOption struct { type MergePullRequestOption struct {
DeleteBranchAfterMerge bool `json:"delete_branch_after_merge,omitempty"` DeleteBranchAfterMerge bool `json:"delete_branch_after_merge,omitempty"`
Do string `json:"Do"` Do string `json:"Do"`
ForceMerge bool `json:"force_merge,omitempty"` ForceMerge bool `json:"force_merge,omitempty"`
HeadCommitID string `json:"head_commit_id,omitempty"` HeadCommitID string `json:"head_commit_id,omitempty"`
MergeCommitID string `json:"MergeCommitID,omitempty"` MergeCommitID string `json:"MergeCommitID,omitempty"`
MergeMessageField string `json:"MergeMessageField,omitempty"` MergeMessageField string `json:"MergeMessageField,omitempty"`
MergeTitleField string `json:"MergeTitleField,omitempty"` MergeTitleField string `json:"MergeTitleField,omitempty"`
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"` MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
} }
// MigrateRepoOptions — MigrateRepoOptions options for migrating repository's this is used to interact with api v1 // MigrateRepoOptions — MigrateRepoOptions options for migrating repository's this is used to interact with api v1
//
// Usage:
//
// opts := MigrateRepoOptions{RepoName: "example"}
type MigrateRepoOptions struct { type MigrateRepoOptions struct {
AuthPassword string `json:"auth_password,omitempty"` AuthPassword string `json:"auth_password,omitempty"`
AuthToken string `json:"auth_token,omitempty"` AuthToken string `json:"auth_token,omitempty"`
AuthUsername string `json:"auth_username,omitempty"` AuthUsername string `json:"auth_username,omitempty"`
CloneAddr string `json:"clone_addr"` CloneAddr string `json:"clone_addr"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Issues bool `json:"issues,omitempty"` Issues bool `json:"issues,omitempty"`
LFS bool `json:"lfs,omitempty"` LFS bool `json:"lfs,omitempty"`
LFSEndpoint string `json:"lfs_endpoint,omitempty"` LFSEndpoint string `json:"lfs_endpoint,omitempty"`
Labels bool `json:"labels,omitempty"` Labels bool `json:"labels,omitempty"`
Milestones bool `json:"milestones,omitempty"` Milestones bool `json:"milestones,omitempty"`
Mirror bool `json:"mirror,omitempty"` Mirror bool `json:"mirror,omitempty"`
MirrorInterval string `json:"mirror_interval,omitempty"` MirrorInterval string `json:"mirror_interval,omitempty"`
Private bool `json:"private,omitempty"` Private bool `json:"private,omitempty"`
PullRequests bool `json:"pull_requests,omitempty"` PullRequests bool `json:"pull_requests,omitempty"`
Releases bool `json:"releases,omitempty"` Releases bool `json:"releases,omitempty"`
RepoName string `json:"repo_name"` RepoName string `json:"repo_name"`
RepoOwner string `json:"repo_owner,omitempty"` // Name of User or Organisation who will own Repo after migration RepoOwner string `json:"repo_owner,omitempty"` // Name of User or Organisation who will own Repo after migration
RepoOwnerID int64 `json:"uid,omitempty"` // deprecated (only for backwards compatibility) RepoOwnerID int64 `json:"uid,omitempty"` // deprecated (only for backwards compatibility)
Service string `json:"service,omitempty"` Service string `json:"service,omitempty"`
Wiki bool `json:"wiki,omitempty"` Wiki bool `json:"wiki,omitempty"`
} }
// NewIssuePinsAllowed — NewIssuePinsAllowed represents an API response that says if new Issue Pins are allowed // NewIssuePinsAllowed — NewIssuePinsAllowed represents an API response that says if new Issue Pins are allowed
//
// Usage:
//
// opts := NewIssuePinsAllowed{Issues: true}
type NewIssuePinsAllowed struct { type NewIssuePinsAllowed struct {
Issues bool `json:"issues,omitempty"` Issues bool `json:"issues,omitempty"`
PullRequests bool `json:"pull_requests,omitempty"` PullRequests bool `json:"pull_requests,omitempty"`
} }
// NotifySubjectType — NotifySubjectType represent type of notification subject // NotifySubjectType — NotifySubjectType represent type of notification subject
// // NotifySubjectType has no fields in the swagger spec.
// Usage: type NotifySubjectType struct{}
//
// opts := NotifySubjectType("example")
type NotifySubjectType string
// PRBranchInfo — PRBranchInfo information about a branch // PRBranchInfo — PRBranchInfo information about a branch
//
// Usage:
//
// opts := PRBranchInfo{Name: "example"}
type PRBranchInfo struct { type PRBranchInfo struct {
Name string `json:"label,omitempty"` Name string `json:"label,omitempty"`
Ref string `json:"ref,omitempty"` Ref string `json:"ref,omitempty"`
Repo *Repository `json:"repo,omitempty"` Repo *Repository `json:"repo,omitempty"`
RepoID int64 `json:"repo_id,omitempty"` RepoID int64 `json:"repo_id,omitempty"`
Sha string `json:"sha,omitempty"` Sha string `json:"sha,omitempty"`
} }
// PayloadUser — PayloadUser represents the author or committer of a commit // PayloadUser — PayloadUser represents the author or committer of a commit
//
// Usage:
//
// opts := PayloadUser{Name: "example"}
type PayloadUser struct { type PayloadUser struct {
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Name string `json:"name,omitempty"` // Full name of the commit author Name string `json:"name,omitempty"` // Full name of the commit author
UserName string `json:"username,omitempty"` UserName string `json:"username,omitempty"`
} }
// Usage:
//
// opts := Reference{Ref: "main"}
type Reference struct { type Reference struct {
Object *GitObject `json:"object,omitempty"` Object *GitObject `json:"object,omitempty"`
Ref string `json:"ref,omitempty"` Ref string `json:"ref,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// ReplaceFlagsOption — ReplaceFlagsOption options when replacing the flags of a repository // ReplaceFlagsOption — ReplaceFlagsOption options when replacing the flags of a repository
//
// Usage:
//
// opts := ReplaceFlagsOption{Flags: []string{"example"}}
type ReplaceFlagsOption struct { type ReplaceFlagsOption struct {
Flags []string `json:"flags,omitempty"` Flags []string `json:"flags,omitempty"`
} }
// SearchResults — SearchResults results of a successful search // SearchResults — SearchResults results of a successful search
//
// Usage:
//
// opts := SearchResults{OK: true}
type SearchResults struct { type SearchResults struct {
Data []*Repository `json:"data,omitempty"` Data []*Repository `json:"data,omitempty"`
OK bool `json:"ok,omitempty"` OK bool `json:"ok,omitempty"`
} }
// ServerVersion — ServerVersion wraps the version of the server // ServerVersion — ServerVersion wraps the version of the server
//
// Usage:
//
// opts := ServerVersion{Version: "example"}
type ServerVersion struct { type ServerVersion struct {
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
} }
// TimelineComment — TimelineComment represents a timeline comment (comment of any type) on a commit or issue // TimelineComment — TimelineComment represents a timeline comment (comment of any type) on a commit or issue
//
// Usage:
//
// opts := TimelineComment{Body: "example"}
type TimelineComment struct { type TimelineComment struct {
Assignee *User `json:"assignee,omitempty"` Assignee *User `json:"assignee,omitempty"`
AssigneeTeam *Team `json:"assignee_team,omitempty"` AssigneeTeam *Team `json:"assignee_team,omitempty"`
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
DependentIssue *Issue `json:"dependent_issue,omitempty"` DependentIssue *Issue `json:"dependent_issue,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
IssueURL string `json:"issue_url,omitempty"` IssueURL string `json:"issue_url,omitempty"`
Label *Label `json:"label,omitempty"` Label *Label `json:"label,omitempty"`
Milestone *Milestone `json:"milestone,omitempty"` Milestone *Milestone `json:"milestone,omitempty"`
NewRef string `json:"new_ref,omitempty"` NewRef string `json:"new_ref,omitempty"`
NewTitle string `json:"new_title,omitempty"` NewTitle string `json:"new_title,omitempty"`
OldMilestone *Milestone `json:"old_milestone,omitempty"` OldMilestone *Milestone `json:"old_milestone,omitempty"`
OldProjectID int64 `json:"old_project_id,omitempty"` OldProjectID int64 `json:"old_project_id,omitempty"`
OldRef string `json:"old_ref,omitempty"` OldRef string `json:"old_ref,omitempty"`
OldTitle string `json:"old_title,omitempty"` OldTitle string `json:"old_title,omitempty"`
PRURL string `json:"pull_request_url,omitempty"` PRURL string `json:"pull_request_url,omitempty"`
ProjectID int64 `json:"project_id,omitempty"` ProjectID int64 `json:"project_id,omitempty"`
RefAction string `json:"ref_action,omitempty"` RefAction string `json:"ref_action,omitempty"`
RefComment *Comment `json:"ref_comment,omitempty"` RefComment *Comment `json:"ref_comment,omitempty"`
RefCommitSHA string `json:"ref_commit_sha,omitempty"` // commit SHA where issue/PR was referenced RefCommitSHA string `json:"ref_commit_sha,omitempty"` // commit SHA where issue/PR was referenced
RefIssue *Issue `json:"ref_issue,omitempty"` RefIssue *Issue `json:"ref_issue,omitempty"`
RemovedAssignee bool `json:"removed_assignee,omitempty"` // whether the assignees were removed or added RemovedAssignee bool `json:"removed_assignee,omitempty"` // whether the assignees were removed or added
ResolveDoer *User `json:"resolve_doer,omitempty"` ResolveDoer *User `json:"resolve_doer,omitempty"`
ReviewID int64 `json:"review_id,omitempty"` ReviewID int64 `json:"review_id,omitempty"`
TrackedTime *TrackedTime `json:"tracked_time,omitempty"` TrackedTime *TrackedTime `json:"tracked_time,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }
// WatchInfo — WatchInfo represents an API watch status of one repository // WatchInfo — WatchInfo represents an API watch status of one repository
//
// Usage:
//
// opts := WatchInfo{RepositoryURL: "https://example.com"}
type WatchInfo struct { type WatchInfo struct {
CreatedAt time.Time `json:"created_at,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"`
Ignored bool `json:"ignored,omitempty"` Ignored bool `json:"ignored,omitempty"`
Reason any `json:"reason,omitempty"` Reason any `json:"reason,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"` RepositoryURL string `json:"repository_url,omitempty"`
Subscribed bool `json:"subscribed,omitempty"` Subscribed bool `json:"subscribed,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }

View file

@ -4,41 +4,31 @@ package types
import "time" import "time"
// NotificationCount — NotificationCount number of unread notifications // NotificationCount — NotificationCount number of unread notifications
//
// Usage:
//
// opts := NotificationCount{New: 1}
type NotificationCount struct { type NotificationCount struct {
New int64 `json:"new,omitempty"` New int64 `json:"new,omitempty"`
} }
// NotificationSubject — NotificationSubject contains the notification subject (Issue/Pull/Commit) // NotificationSubject — NotificationSubject contains the notification subject (Issue/Pull/Commit)
//
// Usage:
//
// opts := NotificationSubject{Title: "example"}
type NotificationSubject struct { type NotificationSubject struct {
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
LatestCommentHTMLURL string `json:"latest_comment_html_url,omitempty"` LatestCommentHTMLURL string `json:"latest_comment_html_url,omitempty"`
LatestCommentURL string `json:"latest_comment_url,omitempty"` LatestCommentURL string `json:"latest_comment_url,omitempty"`
State StateType `json:"state,omitempty"` State StateType `json:"state,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Type NotifySubjectType `json:"type,omitempty"` Type *NotifySubjectType `json:"type,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
} }
// NotificationThread — NotificationThread expose Notification on API // NotificationThread — NotificationThread expose Notification on API
//
// Usage:
//
// opts := NotificationThread{URL: "https://example.com"}
type NotificationThread struct { type NotificationThread struct {
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Pinned bool `json:"pinned,omitempty"` Pinned bool `json:"pinned,omitempty"`
Repository *Repository `json:"repository,omitempty"` Repository *Repository `json:"repository,omitempty"`
Subject *NotificationSubject `json:"subject,omitempty"` Subject *NotificationSubject `json:"subject,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Unread bool `json:"unread,omitempty"` Unread bool `json:"unread,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"`
} }

View file

@ -4,47 +4,35 @@ package types
import "time" import "time"
// Usage:
//
// opts := AccessToken{Name: "example"}
type AccessToken struct { type AccessToken struct {
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Scopes []string `json:"scopes,omitempty"` Scopes []string `json:"scopes,omitempty"`
Token string `json:"sha1,omitempty"` Token string `json:"sha1,omitempty"`
TokenLastEight string `json:"token_last_eight,omitempty"` TokenLastEight string `json:"token_last_eight,omitempty"`
} }
// CreateAccessTokenOption — CreateAccessTokenOption options when create access token // CreateAccessTokenOption — CreateAccessTokenOption options when create access token
//
// Usage:
//
// opts := CreateAccessTokenOption{Name: "example"}
type CreateAccessTokenOption struct { type CreateAccessTokenOption struct {
Name string `json:"name"` Name string `json:"name"`
Scopes []string `json:"scopes,omitempty"` Scopes []string `json:"scopes,omitempty"`
} }
// CreateOAuth2ApplicationOptions — CreateOAuth2ApplicationOptions holds options to create an oauth2 application // CreateOAuth2ApplicationOptions — CreateOAuth2ApplicationOptions holds options to create an oauth2 application
//
// Usage:
//
// opts := CreateOAuth2ApplicationOptions{Name: "example"}
type CreateOAuth2ApplicationOptions struct { type CreateOAuth2ApplicationOptions struct {
ConfidentialClient bool `json:"confidential_client,omitempty"` ConfidentialClient bool `json:"confidential_client,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
RedirectURIs []string `json:"redirect_uris,omitempty"` RedirectURIs []string `json:"redirect_uris,omitempty"`
} }
// Usage:
//
// opts := OAuth2Application{Name: "example"}
type OAuth2Application struct { type OAuth2Application struct {
ClientID string `json:"client_id,omitempty"` ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"` ClientSecret string `json:"client_secret,omitempty"`
ConfidentialClient bool `json:"confidential_client,omitempty"` ConfidentialClient bool `json:"confidential_client,omitempty"`
Created time.Time `json:"created,omitempty"` Created time.Time `json:"created,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
RedirectURIs []string `json:"redirect_uris,omitempty"` RedirectURIs []string `json:"redirect_uris,omitempty"`
} }

View file

@ -2,65 +2,51 @@
package types package types
// CreateOrgOption — CreateOrgOption options for creating an organization // CreateOrgOption — CreateOrgOption options for creating an organization
//
// Usage:
//
// opts := CreateOrgOption{UserName: "example"}
type CreateOrgOption struct { type CreateOrgOption struct {
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
FullName string `json:"full_name,omitempty"` FullName string `json:"full_name,omitempty"`
Location string `json:"location,omitempty"` Location string `json:"location,omitempty"`
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access,omitempty"` RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access,omitempty"`
UserName string `json:"username"` UserName string `json:"username"`
Visibility string `json:"visibility,omitempty"` // possible values are `public` (default), `limited` or `private` Visibility string `json:"visibility,omitempty"` // possible values are `public` (default), `limited` or `private`
Website string `json:"website,omitempty"` Website string `json:"website,omitempty"`
} }
// EditOrgOption — EditOrgOption options for editing an organization // EditOrgOption — EditOrgOption options for editing an organization
//
// Usage:
//
// opts := EditOrgOption{Description: "example"}
type EditOrgOption struct { type EditOrgOption struct {
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
FullName string `json:"full_name,omitempty"` FullName string `json:"full_name,omitempty"`
Location string `json:"location,omitempty"` Location string `json:"location,omitempty"`
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access,omitempty"` RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access,omitempty"`
Visibility string `json:"visibility,omitempty"` // possible values are `public`, `limited` or `private` Visibility string `json:"visibility,omitempty"` // possible values are `public`, `limited` or `private`
Website string `json:"website,omitempty"` Website string `json:"website,omitempty"`
} }
// Organization — Organization represents an organization // Organization — Organization represents an organization
//
// Usage:
//
// opts := Organization{Description: "example"}
type Organization struct { type Organization struct {
AvatarURL string `json:"avatar_url,omitempty"` AvatarURL string `json:"avatar_url,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
FullName string `json:"full_name,omitempty"` FullName string `json:"full_name,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Location string `json:"location,omitempty"` Location string `json:"location,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access,omitempty"` RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access,omitempty"`
UserName string `json:"username,omitempty"` // deprecated UserName string `json:"username,omitempty"` // deprecated
Visibility string `json:"visibility,omitempty"` Visibility string `json:"visibility,omitempty"`
Website string `json:"website,omitempty"` Website string `json:"website,omitempty"`
} }
// OrganizationPermissions — OrganizationPermissions list different users permissions on an organization // OrganizationPermissions — OrganizationPermissions list different users permissions on an organization
//
// Usage:
//
// opts := OrganizationPermissions{CanCreateRepository: true}
type OrganizationPermissions struct { type OrganizationPermissions struct {
CanCreateRepository bool `json:"can_create_repository,omitempty"` CanCreateRepository bool `json:"can_create_repository,omitempty"`
CanRead bool `json:"can_read,omitempty"` CanRead bool `json:"can_read,omitempty"`
CanWrite bool `json:"can_write,omitempty"` CanWrite bool `json:"can_write,omitempty"`
IsAdmin bool `json:"is_admin,omitempty"` IsAdmin bool `json:"is_admin,omitempty"`
IsOwner bool `json:"is_owner,omitempty"` IsOwner bool `json:"is_owner,omitempty"`
} }

View file

@ -4,34 +4,28 @@ package types
import "time" import "time"
// Package — Package represents a package // Package — Package represents a package
//
// Usage:
//
// opts := Package{Name: "example"}
type Package struct { type Package struct {
CreatedAt time.Time `json:"created_at,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"`
Creator *User `json:"creator,omitempty"` Creator *User `json:"creator,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Owner *User `json:"owner,omitempty"` Owner *User `json:"owner,omitempty"`
Repository *Repository `json:"repository,omitempty"` Repository *Repository `json:"repository,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
} }
// PackageFile — PackageFile represents a package file // PackageFile — PackageFile represents a package file
//
// Usage:
//
// opts := PackageFile{Name: "example"}
type PackageFile struct { type PackageFile struct {
HashMD5 string `json:"md5,omitempty"` HashMD5 string `json:"md5,omitempty"`
HashSHA1 string `json:"sha1,omitempty"` HashSHA1 string `json:"sha1,omitempty"`
HashSHA256 string `json:"sha256,omitempty"` HashSHA256 string `json:"sha256,omitempty"`
HashSHA512 string `json:"sha512,omitempty"` HashSHA512 string `json:"sha512,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Size int64 `json:"Size,omitempty"` Size int64 `json:"Size,omitempty"`
} }

View file

@ -4,191 +4,150 @@ package types
import "time" import "time"
// CreatePullRequestOption — CreatePullRequestOption options when creating a pull request // CreatePullRequestOption — CreatePullRequestOption options when creating a pull request
//
// Usage:
//
// opts := CreatePullRequestOption{Body: "example"}
type CreatePullRequestOption struct { type CreatePullRequestOption struct {
Assignee string `json:"assignee,omitempty"` Assignee string `json:"assignee,omitempty"`
Assignees []string `json:"assignees,omitempty"` Assignees []string `json:"assignees,omitempty"`
Base string `json:"base,omitempty"` Base string `json:"base,omitempty"`
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Deadline time.Time `json:"due_date,omitempty"` Deadline time.Time `json:"due_date,omitempty"`
Head string `json:"head,omitempty"` Head string `json:"head,omitempty"`
Labels []int64 `json:"labels,omitempty"` Labels []int64 `json:"labels,omitempty"`
Milestone int64 `json:"milestone,omitempty"` Milestone int64 `json:"milestone,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
} }
// CreatePullReviewComment — CreatePullReviewComment represent a review comment for creation api // CreatePullReviewComment — CreatePullReviewComment represent a review comment for creation api
//
// Usage:
//
// opts := CreatePullReviewComment{Body: "example"}
type CreatePullReviewComment struct { type CreatePullReviewComment struct {
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
NewLineNum int64 `json:"new_position,omitempty"` // if comment to new file line or 0 NewLineNum int64 `json:"new_position,omitempty"` // if comment to new file line or 0
OldLineNum int64 `json:"old_position,omitempty"` // if comment to old file line or 0 OldLineNum int64 `json:"old_position,omitempty"` // if comment to old file line or 0
Path string `json:"path,omitempty"` // the tree path Path string `json:"path,omitempty"` // the tree path
} }
// CreatePullReviewCommentOptions — CreatePullReviewCommentOptions are options to create a pull review comment // CreatePullReviewCommentOptions — CreatePullReviewCommentOptions are options to create a pull review comment
// // CreatePullReviewCommentOptions has no fields in the swagger spec.
// Usage: type CreatePullReviewCommentOptions struct{}
//
// opts := CreatePullReviewCommentOptions(CreatePullReviewComment{})
type CreatePullReviewCommentOptions CreatePullReviewComment
// CreatePullReviewOptions — CreatePullReviewOptions are options to create a pull review // CreatePullReviewOptions — CreatePullReviewOptions are options to create a pull review
//
// Usage:
//
// opts := CreatePullReviewOptions{Body: "example"}
type CreatePullReviewOptions struct { type CreatePullReviewOptions struct {
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Comments []*CreatePullReviewComment `json:"comments,omitempty"` Comments []*CreatePullReviewComment `json:"comments,omitempty"`
CommitID string `json:"commit_id,omitempty"` CommitID string `json:"commit_id,omitempty"`
Event ReviewStateType `json:"event,omitempty"` Event *ReviewStateType `json:"event,omitempty"`
} }
// EditPullRequestOption — EditPullRequestOption options when modify pull request // EditPullRequestOption — EditPullRequestOption options when modify pull request
//
// Usage:
//
// opts := EditPullRequestOption{Body: "example"}
type EditPullRequestOption struct { type EditPullRequestOption struct {
AllowMaintainerEdit bool `json:"allow_maintainer_edit,omitempty"` AllowMaintainerEdit bool `json:"allow_maintainer_edit,omitempty"`
Assignee string `json:"assignee,omitempty"` Assignee string `json:"assignee,omitempty"`
Assignees []string `json:"assignees,omitempty"` Assignees []string `json:"assignees,omitempty"`
Base string `json:"base,omitempty"` Base string `json:"base,omitempty"`
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Deadline time.Time `json:"due_date,omitempty"` Deadline time.Time `json:"due_date,omitempty"`
Labels []int64 `json:"labels,omitempty"` Labels []int64 `json:"labels,omitempty"`
Milestone int64 `json:"milestone,omitempty"` Milestone int64 `json:"milestone,omitempty"`
RemoveDeadline bool `json:"unset_due_date,omitempty"` RemoveDeadline bool `json:"unset_due_date,omitempty"`
State string `json:"state,omitempty"` State string `json:"state,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
} }
// PullRequest — PullRequest represents a pull request // PullRequest — PullRequest represents a pull request
//
// Usage:
//
// opts := PullRequest{Body: "example"}
type PullRequest struct { type PullRequest struct {
Additions int64 `json:"additions,omitempty"` Additions int64 `json:"additions,omitempty"`
AllowMaintainerEdit bool `json:"allow_maintainer_edit,omitempty"` AllowMaintainerEdit bool `json:"allow_maintainer_edit,omitempty"`
Assignee *User `json:"assignee,omitempty"` Assignee *User `json:"assignee,omitempty"`
Assignees []*User `json:"assignees,omitempty"` Assignees []*User `json:"assignees,omitempty"`
Base *PRBranchInfo `json:"base,omitempty"` Base *PRBranchInfo `json:"base,omitempty"`
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
ChangedFiles int64 `json:"changed_files,omitempty"` ChangedFiles int64 `json:"changed_files,omitempty"`
Closed time.Time `json:"closed_at,omitempty"` Closed time.Time `json:"closed_at,omitempty"`
Comments int64 `json:"comments,omitempty"` Comments int64 `json:"comments,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Deadline time.Time `json:"due_date,omitempty"` Deadline time.Time `json:"due_date,omitempty"`
Deletions int64 `json:"deletions,omitempty"` Deletions int64 `json:"deletions,omitempty"`
DiffURL string `json:"diff_url,omitempty"` DiffURL string `json:"diff_url,omitempty"`
Draft bool `json:"draft,omitempty"` Draft bool `json:"draft,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
HasMerged bool `json:"merged,omitempty"` HasMerged bool `json:"merged,omitempty"`
Head *PRBranchInfo `json:"head,omitempty"` Head *PRBranchInfo `json:"head,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Index int64 `json:"number,omitempty"` Index int64 `json:"number,omitempty"`
IsLocked bool `json:"is_locked,omitempty"` IsLocked bool `json:"is_locked,omitempty"`
Labels []*Label `json:"labels,omitempty"` Labels []*Label `json:"labels,omitempty"`
MergeBase string `json:"merge_base,omitempty"` MergeBase string `json:"merge_base,omitempty"`
Mergeable bool `json:"mergeable,omitempty"` Mergeable bool `json:"mergeable,omitempty"`
Merged time.Time `json:"merged_at,omitempty"` Merged time.Time `json:"merged_at,omitempty"`
MergedBy *User `json:"merged_by,omitempty"` MergedBy *User `json:"merged_by,omitempty"`
MergedCommitID string `json:"merge_commit_sha,omitempty"` MergedCommitID string `json:"merge_commit_sha,omitempty"`
Milestone *Milestone `json:"milestone,omitempty"` Milestone *Milestone `json:"milestone,omitempty"`
PatchURL string `json:"patch_url,omitempty"` PatchURL string `json:"patch_url,omitempty"`
PinOrder int64 `json:"pin_order,omitempty"` PinOrder int64 `json:"pin_order,omitempty"`
RequestedReviewers []*User `json:"requested_reviewers,omitempty"` RequestedReviewers []*User `json:"requested_reviewers,omitempty"`
RequestedReviewersTeams []*Team `json:"requested_reviewers_teams,omitempty"` RequestedReviewersTeams []*Team `json:"requested_reviewers_teams,omitempty"`
ReviewComments int64 `json:"review_comments,omitempty"` // number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR) ReviewComments int64 `json:"review_comments,omitempty"` // number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
State StateType `json:"state,omitempty"` State StateType `json:"state,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }
// PullRequestMeta — PullRequestMeta PR info if an issue is a PR // PullRequestMeta — PullRequestMeta PR info if an issue is a PR
//
// Usage:
//
// opts := PullRequestMeta{HTMLURL: "https://example.com"}
type PullRequestMeta struct { type PullRequestMeta struct {
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
HasMerged bool `json:"merged,omitempty"` HasMerged bool `json:"merged,omitempty"`
IsWorkInProgress bool `json:"draft,omitempty"` IsWorkInProgress bool `json:"draft,omitempty"`
Merged time.Time `json:"merged_at,omitempty"` Merged time.Time `json:"merged_at,omitempty"`
} }
// PullReview — PullReview represents a pull request review // PullReview — PullReview represents a pull request review
//
// Usage:
//
// opts := PullReview{Body: "example"}
type PullReview struct { type PullReview struct {
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
CodeCommentsCount int64 `json:"comments_count,omitempty"` CodeCommentsCount int64 `json:"comments_count,omitempty"`
CommitID string `json:"commit_id,omitempty"` CommitID string `json:"commit_id,omitempty"`
Dismissed bool `json:"dismissed,omitempty"` Dismissed bool `json:"dismissed,omitempty"`
HTMLPullURL string `json:"pull_request_url,omitempty"` HTMLPullURL string `json:"pull_request_url,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Official bool `json:"official,omitempty"` Official bool `json:"official,omitempty"`
Stale bool `json:"stale,omitempty"` Stale bool `json:"stale,omitempty"`
State ReviewStateType `json:"state,omitempty"` State *ReviewStateType `json:"state,omitempty"`
Submitted time.Time `json:"submitted_at,omitempty"` Submitted time.Time `json:"submitted_at,omitempty"`
Team *Team `json:"team,omitempty"` Team *Team `json:"team,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }
// PullReviewComment — PullReviewComment represents a comment on a pull request review // PullReviewComment — PullReviewComment represents a comment on a pull request review
//
// Usage:
//
// opts := PullReviewComment{Body: "example"}
type PullReviewComment struct { type PullReviewComment struct {
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
CommitID string `json:"commit_id,omitempty"` CommitID string `json:"commit_id,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
DiffHunk string `json:"diff_hunk,omitempty"` DiffHunk string `json:"diff_hunk,omitempty"`
HTMLPullURL string `json:"pull_request_url,omitempty"` HTMLPullURL string `json:"pull_request_url,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
LineNum int `json:"position,omitempty"` LineNum int `json:"position,omitempty"`
OldLineNum int `json:"original_position,omitempty"` OldLineNum int `json:"original_position,omitempty"`
OrigCommitID string `json:"original_commit_id,omitempty"` OrigCommitID string `json:"original_commit_id,omitempty"`
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
Resolver *User `json:"resolver,omitempty"` Resolver *User `json:"resolver,omitempty"`
ReviewID int64 `json:"pull_request_review_id,omitempty"` ReviewID int64 `json:"pull_request_review_id,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }
// PullReviewRequestOptions — PullReviewRequestOptions are options to add or remove pull review requests // PullReviewRequestOptions — PullReviewRequestOptions are options to add or remove pull review requests
//
// Usage:
//
// opts := PullReviewRequestOptions{Reviewers: []string{"example"}}
type PullReviewRequestOptions struct { type PullReviewRequestOptions struct {
Reviewers []string `json:"reviewers,omitempty"` Reviewers []string `json:"reviewers,omitempty"`
TeamReviewers []string `json:"team_reviewers,omitempty"` TeamReviewers []string `json:"team_reviewers,omitempty"`
} }
// SubmitPullReviewOptions — SubmitPullReviewOptions are options to submit a pending pull review // SubmitPullReviewOptions — SubmitPullReviewOptions are options to submit a pending pull review
//
// Usage:
//
// opts := SubmitPullReviewOptions{Body: "example"}
type SubmitPullReviewOptions struct { type SubmitPullReviewOptions struct {
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
Event ReviewStateType `json:"event,omitempty"` Event *ReviewStateType `json:"event,omitempty"`
} }

View file

@ -2,197 +2,123 @@
package types package types
// CreateQuotaGroupOptions — CreateQutaGroupOptions represents the options for creating a quota group // CreateQuotaGroupOptions — CreateQutaGroupOptions represents the options for creating a quota group
//
// Usage:
//
// opts := CreateQuotaGroupOptions{Name: "example"}
type CreateQuotaGroupOptions struct { type CreateQuotaGroupOptions struct {
Name string `json:"name,omitempty"` // Name of the quota group to create Name string `json:"name,omitempty"` // Name of the quota group to create
Rules []*CreateQuotaRuleOptions `json:"rules,omitempty"` // Rules to add to the newly created group. If a rule does not exist, it will be created. Rules []*CreateQuotaRuleOptions `json:"rules,omitempty"` // Rules to add to the newly created group. If a rule does not exist, it will be created.
} }
// CreateQuotaRuleOptions — CreateQuotaRuleOptions represents the options for creating a quota rule // CreateQuotaRuleOptions — CreateQuotaRuleOptions represents the options for creating a quota rule
//
// Usage:
//
// opts := CreateQuotaRuleOptions{Name: "example"}
type CreateQuotaRuleOptions struct { type CreateQuotaRuleOptions struct {
Limit int64 `json:"limit,omitempty"` // The limit set by the rule Limit int64 `json:"limit,omitempty"` // The limit set by the rule
Name string `json:"name,omitempty"` // Name of the rule to create Name string `json:"name,omitempty"` // Name of the rule to create
Subjects []string `json:"subjects,omitempty"` // The subjects affected by the rule Subjects []string `json:"subjects,omitempty"` // The subjects affected by the rule
} }
// EditQuotaRuleOptions — EditQuotaRuleOptions represents the options for editing a quota rule // EditQuotaRuleOptions — EditQuotaRuleOptions represents the options for editing a quota rule
//
// Usage:
//
// opts := EditQuotaRuleOptions{Subjects: []string{"example"}}
type EditQuotaRuleOptions struct { type EditQuotaRuleOptions struct {
Limit int64 `json:"limit,omitempty"` // The limit set by the rule Limit int64 `json:"limit,omitempty"` // The limit set by the rule
Subjects []string `json:"subjects,omitempty"` // The subjects affected by the rule Subjects []string `json:"subjects,omitempty"` // The subjects affected by the rule
} }
// QuotaGroup — QuotaGroup represents a quota group // QuotaGroup — QuotaGroup represents a quota group
//
// Usage:
//
// opts := QuotaGroup{Name: "example"}
type QuotaGroup struct { type QuotaGroup struct {
Name string `json:"name,omitempty"` // Name of the group Name string `json:"name,omitempty"` // Name of the group
Rules []*QuotaRuleInfo `json:"rules,omitempty"` // Rules associated with the group Rules []*QuotaRuleInfo `json:"rules,omitempty"` // Rules associated with the group
} }
// QuotaGroupList — QuotaGroupList represents a list of quota groups // QuotaGroupList — QuotaGroupList represents a list of quota groups
// // QuotaGroupList has no fields in the swagger spec.
// Usage: type QuotaGroupList struct{}
//
// opts := QuotaGroupList([]*QuotaGroup{})
type QuotaGroupList []*QuotaGroup
// QuotaInfo — QuotaInfo represents information about a user's quota // QuotaInfo — QuotaInfo represents information about a user's quota
//
// Usage:
//
// opts := QuotaInfo{Groups: {}}
type QuotaInfo struct { type QuotaInfo struct {
Groups QuotaGroupList `json:"groups,omitempty"` Groups *QuotaGroupList `json:"groups,omitempty"`
Used *QuotaUsed `json:"used,omitempty"` Used *QuotaUsed `json:"used,omitempty"`
} }
// QuotaRuleInfo — QuotaRuleInfo contains information about a quota rule // QuotaRuleInfo — QuotaRuleInfo contains information about a quota rule
//
// Usage:
//
// opts := QuotaRuleInfo{Name: "example"}
type QuotaRuleInfo struct { type QuotaRuleInfo struct {
Limit int64 `json:"limit,omitempty"` // The limit set by the rule Limit int64 `json:"limit,omitempty"` // The limit set by the rule
Name string `json:"name,omitempty"` // Name of the rule (only shown to admins) Name string `json:"name,omitempty"` // Name of the rule (only shown to admins)
Subjects []string `json:"subjects,omitempty"` // Subjects the rule affects Subjects []string `json:"subjects,omitempty"` // Subjects the rule affects
} }
// QuotaUsed — QuotaUsed represents the quota usage of a user // QuotaUsed — QuotaUsed represents the quota usage of a user
//
// Usage:
//
// opts := QuotaUsed{Size: &QuotaUsedSize{}}
type QuotaUsed struct { type QuotaUsed struct {
Size *QuotaUsedSize `json:"size,omitempty"` Size *QuotaUsedSize `json:"size,omitempty"`
} }
// QuotaUsedArtifact — QuotaUsedArtifact represents an artifact counting towards a user's quota // QuotaUsedArtifact — QuotaUsedArtifact represents an artifact counting towards a user's quota
//
// Usage:
//
// opts := QuotaUsedArtifact{Name: "example"}
type QuotaUsedArtifact struct { type QuotaUsedArtifact struct {
HTMLURL string `json:"html_url,omitempty"` // HTML URL to the action run containing the artifact HTMLURL string `json:"html_url,omitempty"` // HTML URL to the action run containing the artifact
Name string `json:"name,omitempty"` // Name of the artifact Name string `json:"name,omitempty"` // Name of the artifact
Size int64 `json:"size,omitempty"` // Size of the artifact (compressed) Size int64 `json:"size,omitempty"` // Size of the artifact (compressed)
} }
// QuotaUsedArtifactList — QuotaUsedArtifactList represents a list of artifacts counting towards a user's quota // QuotaUsedArtifactList — QuotaUsedArtifactList represents a list of artifacts counting towards a user's quota
// // QuotaUsedArtifactList has no fields in the swagger spec.
// Usage: type QuotaUsedArtifactList struct{}
//
// opts := QuotaUsedArtifactList([]*QuotaUsedArtifact{})
type QuotaUsedArtifactList []*QuotaUsedArtifact
// QuotaUsedAttachment — QuotaUsedAttachment represents an attachment counting towards a user's quota // QuotaUsedAttachment — QuotaUsedAttachment represents an attachment counting towards a user's quota
//
// Usage:
//
// opts := QuotaUsedAttachment{Name: "example"}
type QuotaUsedAttachment struct { type QuotaUsedAttachment struct {
APIURL string `json:"api_url,omitempty"` // API URL for the attachment APIURL string `json:"api_url,omitempty"` // API URL for the attachment
ContainedIn map[string]any `json:"contained_in,omitempty"` // Context for the attachment: URLs to the containing object ContainedIn map[string]any `json:"contained_in,omitempty"` // Context for the attachment: URLs to the containing object
Name string `json:"name,omitempty"` // Filename of the attachment Name string `json:"name,omitempty"` // Filename of the attachment
Size int64 `json:"size,omitempty"` // Size of the attachment (in bytes) Size int64 `json:"size,omitempty"` // Size of the attachment (in bytes)
} }
// QuotaUsedAttachmentList — QuotaUsedAttachmentList represents a list of attachment counting towards a user's quota // QuotaUsedAttachmentList — QuotaUsedAttachmentList represents a list of attachment counting towards a user's quota
// // QuotaUsedAttachmentList has no fields in the swagger spec.
// Usage: type QuotaUsedAttachmentList struct{}
//
// opts := QuotaUsedAttachmentList([]*QuotaUsedAttachment{})
type QuotaUsedAttachmentList []*QuotaUsedAttachment
// QuotaUsedPackage — QuotaUsedPackage represents a package counting towards a user's quota // QuotaUsedPackage — QuotaUsedPackage represents a package counting towards a user's quota
//
// Usage:
//
// opts := QuotaUsedPackage{Name: "example"}
type QuotaUsedPackage struct { type QuotaUsedPackage struct {
HTMLURL string `json:"html_url,omitempty"` // HTML URL to the package version HTMLURL string `json:"html_url,omitempty"` // HTML URL to the package version
Name string `json:"name,omitempty"` // Name of the package Name string `json:"name,omitempty"` // Name of the package
Size int64 `json:"size,omitempty"` // Size of the package version Size int64 `json:"size,omitempty"` // Size of the package version
Type string `json:"type,omitempty"` // Type of the package Type string `json:"type,omitempty"` // Type of the package
Version string `json:"version,omitempty"` // Version of the package Version string `json:"version,omitempty"` // Version of the package
} }
// QuotaUsedPackageList — QuotaUsedPackageList represents a list of packages counting towards a user's quota // QuotaUsedPackageList — QuotaUsedPackageList represents a list of packages counting towards a user's quota
// // QuotaUsedPackageList has no fields in the swagger spec.
// Usage: type QuotaUsedPackageList struct{}
//
// opts := QuotaUsedPackageList([]*QuotaUsedPackage{})
type QuotaUsedPackageList []*QuotaUsedPackage
// QuotaUsedSize — QuotaUsedSize represents the size-based quota usage of a user // QuotaUsedSize — QuotaUsedSize represents the size-based quota usage of a user
//
// Usage:
//
// opts := QuotaUsedSize{Assets: &QuotaUsedSizeAssets{}}
type QuotaUsedSize struct { type QuotaUsedSize struct {
Assets *QuotaUsedSizeAssets `json:"assets,omitempty"` Assets *QuotaUsedSizeAssets `json:"assets,omitempty"`
Git *QuotaUsedSizeGit `json:"git,omitempty"` Git *QuotaUsedSizeGit `json:"git,omitempty"`
Repos *QuotaUsedSizeRepos `json:"repos,omitempty"` Repos *QuotaUsedSizeRepos `json:"repos,omitempty"`
} }
// QuotaUsedSizeAssets — QuotaUsedSizeAssets represents the size-based asset usage of a user // QuotaUsedSizeAssets — QuotaUsedSizeAssets represents the size-based asset usage of a user
//
// Usage:
//
// opts := QuotaUsedSizeAssets{Artifacts: 1}
type QuotaUsedSizeAssets struct { type QuotaUsedSizeAssets struct {
Artifacts int64 `json:"artifacts,omitempty"` // Storage size used for the user's artifacts Artifacts int64 `json:"artifacts,omitempty"` // Storage size used for the user's artifacts
Attachments *QuotaUsedSizeAssetsAttachments `json:"attachments,omitempty"` Attachments *QuotaUsedSizeAssetsAttachments `json:"attachments,omitempty"`
Packages *QuotaUsedSizeAssetsPackages `json:"packages,omitempty"` Packages *QuotaUsedSizeAssetsPackages `json:"packages,omitempty"`
} }
// QuotaUsedSizeAssetsAttachments — QuotaUsedSizeAssetsAttachments represents the size-based attachment quota usage of a user // QuotaUsedSizeAssetsAttachments — QuotaUsedSizeAssetsAttachments represents the size-based attachment quota usage of a user
//
// Usage:
//
// opts := QuotaUsedSizeAssetsAttachments{Issues: 1}
type QuotaUsedSizeAssetsAttachments struct { type QuotaUsedSizeAssetsAttachments struct {
Issues int64 `json:"issues,omitempty"` // Storage size used for the user's issue & comment attachments Issues int64 `json:"issues,omitempty"` // Storage size used for the user's issue & comment attachments
Releases int64 `json:"releases,omitempty"` // Storage size used for the user's release attachments Releases int64 `json:"releases,omitempty"` // Storage size used for the user's release attachments
} }
// QuotaUsedSizeAssetsPackages — QuotaUsedSizeAssetsPackages represents the size-based package quota usage of a user // QuotaUsedSizeAssetsPackages — QuotaUsedSizeAssetsPackages represents the size-based package quota usage of a user
//
// Usage:
//
// opts := QuotaUsedSizeAssetsPackages{All: 1}
type QuotaUsedSizeAssetsPackages struct { type QuotaUsedSizeAssetsPackages struct {
All int64 `json:"all,omitempty"` // Storage suze used for the user's packages All int64 `json:"all,omitempty"` // Storage suze used for the user's packages
} }
// QuotaUsedSizeGit — QuotaUsedSizeGit represents the size-based git (lfs) quota usage of a user // QuotaUsedSizeGit — QuotaUsedSizeGit represents the size-based git (lfs) quota usage of a user
//
// Usage:
//
// opts := QuotaUsedSizeGit{LFS: 1}
type QuotaUsedSizeGit struct { type QuotaUsedSizeGit struct {
LFS int64 `json:"LFS,omitempty"` // Storage size of the user's Git LFS objects LFS int64 `json:"LFS,omitempty"` // Storage size of the user's Git LFS objects
} }
// QuotaUsedSizeRepos — QuotaUsedSizeRepos represents the size-based repository quota usage of a user // QuotaUsedSizeRepos — QuotaUsedSizeRepos represents the size-based repository quota usage of a user
//
// Usage:
//
// opts := QuotaUsedSizeRepos{Private: 1}
type QuotaUsedSizeRepos struct { type QuotaUsedSizeRepos struct {
Private int64 `json:"private,omitempty"` // Storage size of the user's private repositories Private int64 `json:"private,omitempty"` // Storage size of the user's private repositories
Public int64 `json:"public,omitempty"` // Storage size of the user's public repositories Public int64 `json:"public,omitempty"` // Storage size of the user's public repositories
} }

View file

@ -4,22 +4,16 @@ package types
import "time" import "time"
// EditReactionOption — EditReactionOption contain the reaction type // EditReactionOption — EditReactionOption contain the reaction type
//
// Usage:
//
// opts := EditReactionOption{Reaction: "example"}
type EditReactionOption struct { type EditReactionOption struct {
Reaction string `json:"content,omitempty"` Reaction string `json:"content,omitempty"`
} }
// Reaction — Reaction contain one reaction // Reaction — Reaction contain one reaction
//
// Usage:
//
// opts := Reaction{Reaction: "example"}
type Reaction struct { type Reaction struct {
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
Reaction string `json:"content,omitempty"` Reaction string `json:"content,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }

View file

@ -4,58 +4,48 @@ package types
import "time" import "time"
// CreateReleaseOption — CreateReleaseOption options when creating a release // CreateReleaseOption — CreateReleaseOption options when creating a release
//
// Usage:
//
// opts := CreateReleaseOption{TagName: "v1.0.0"}
type CreateReleaseOption struct { type CreateReleaseOption struct {
HideArchiveLinks bool `json:"hide_archive_links,omitempty"` HideArchiveLinks bool `json:"hide_archive_links,omitempty"`
IsDraft bool `json:"draft,omitempty"` IsDraft bool `json:"draft,omitempty"`
IsPrerelease bool `json:"prerelease,omitempty"` IsPrerelease bool `json:"prerelease,omitempty"`
Note string `json:"body,omitempty"` Note string `json:"body,omitempty"`
TagName string `json:"tag_name"` TagName string `json:"tag_name"`
Target string `json:"target_commitish,omitempty"` Target string `json:"target_commitish,omitempty"`
Title string `json:"name,omitempty"` Title string `json:"name,omitempty"`
} }
// EditReleaseOption — EditReleaseOption options when editing a release // EditReleaseOption — EditReleaseOption options when editing a release
//
// Usage:
//
// opts := EditReleaseOption{TagName: "v1.0.0"}
type EditReleaseOption struct { type EditReleaseOption struct {
HideArchiveLinks bool `json:"hide_archive_links,omitempty"` HideArchiveLinks bool `json:"hide_archive_links,omitempty"`
IsDraft bool `json:"draft,omitempty"` IsDraft bool `json:"draft,omitempty"`
IsPrerelease bool `json:"prerelease,omitempty"` IsPrerelease bool `json:"prerelease,omitempty"`
Note string `json:"body,omitempty"` Note string `json:"body,omitempty"`
TagName string `json:"tag_name,omitempty"` TagName string `json:"tag_name,omitempty"`
Target string `json:"target_commitish,omitempty"` Target string `json:"target_commitish,omitempty"`
Title string `json:"name,omitempty"` Title string `json:"name,omitempty"`
} }
// Release — Release represents a repository release // Release — Release represents a repository release
//
// Usage:
//
// opts := Release{TagName: "v1.0.0"}
type Release struct { type Release struct {
ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count,omitempty"` ArchiveDownloadCount *TagArchiveDownloadCount `json:"archive_download_count,omitempty"`
Attachments []*Attachment `json:"assets,omitempty"` Attachments []*Attachment `json:"assets,omitempty"`
Author *User `json:"author,omitempty"` Author *User `json:"author,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
HideArchiveLinks bool `json:"hide_archive_links,omitempty"` HideArchiveLinks bool `json:"hide_archive_links,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
IsDraft bool `json:"draft,omitempty"` IsDraft bool `json:"draft,omitempty"`
IsPrerelease bool `json:"prerelease,omitempty"` IsPrerelease bool `json:"prerelease,omitempty"`
Note string `json:"body,omitempty"` Note string `json:"body,omitempty"`
PublishedAt time.Time `json:"published_at,omitempty"` PublishedAt time.Time `json:"published_at,omitempty"`
TagName string `json:"tag_name,omitempty"` TagName string `json:"tag_name,omitempty"`
TarURL string `json:"tarball_url,omitempty"` TarURL string `json:"tarball_url,omitempty"`
Target string `json:"target_commitish,omitempty"` Target string `json:"target_commitish,omitempty"`
Title string `json:"name,omitempty"` Title string `json:"name,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
UploadURL string `json:"upload_url,omitempty"` UploadURL string `json:"upload_url,omitempty"`
ZipURL string `json:"zipball_url,omitempty"` ZipURL string `json:"zipball_url,omitempty"`
} }

View file

@ -4,270 +4,214 @@ package types
import "time" import "time"
// Usage:
//
// opts := CreatePushMirrorOption{Interval: "example"}
type CreatePushMirrorOption struct { type CreatePushMirrorOption struct {
Interval string `json:"interval,omitempty"` Interval string `json:"interval,omitempty"`
RemoteAddress string `json:"remote_address,omitempty"` RemoteAddress string `json:"remote_address,omitempty"`
RemotePassword string `json:"remote_password,omitempty"` RemotePassword string `json:"remote_password,omitempty"`
RemoteUsername string `json:"remote_username,omitempty"` RemoteUsername string `json:"remote_username,omitempty"`
SyncOnCommit bool `json:"sync_on_commit,omitempty"` SyncOnCommit bool `json:"sync_on_commit,omitempty"`
UseSSH bool `json:"use_ssh,omitempty"` UseSSH bool `json:"use_ssh,omitempty"`
} }
// CreateRepoOption — CreateRepoOption options when creating repository // CreateRepoOption — CreateRepoOption options when creating repository
//
// Usage:
//
// opts := CreateRepoOption{Name: "example"}
type CreateRepoOption struct { type CreateRepoOption struct {
AutoInit bool `json:"auto_init,omitempty"` // Whether the repository should be auto-initialized? AutoInit bool `json:"auto_init,omitempty"` // Whether the repository should be auto-initialized?
DefaultBranch string `json:"default_branch,omitempty"` // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch,omitempty"` // DefaultBranch of the repository (used when initializes and in template)
Description string `json:"description,omitempty"` // Description of the repository to create Description string `json:"description,omitempty"` // Description of the repository to create
Gitignores string `json:"gitignores,omitempty"` // Gitignores to use Gitignores string `json:"gitignores,omitempty"` // Gitignores to use
IssueLabels string `json:"issue_labels,omitempty"` // Label-Set to use IssueLabels string `json:"issue_labels,omitempty"` // Label-Set to use
License string `json:"license,omitempty"` // License to use License string `json:"license,omitempty"` // License to use
Name string `json:"name"` // Name of the repository to create Name string `json:"name"` // Name of the repository to create
ObjectFormatName string `json:"object_format_name,omitempty"` // ObjectFormatName of the underlying git repository ObjectFormatName string `json:"object_format_name,omitempty"` // ObjectFormatName of the underlying git repository
Private bool `json:"private,omitempty"` // Whether the repository is private Private bool `json:"private,omitempty"` // Whether the repository is private
Readme string `json:"readme,omitempty"` // Readme of the repository to create Readme string `json:"readme,omitempty"` // Readme of the repository to create
Template bool `json:"template,omitempty"` // Whether the repository is template Template bool `json:"template,omitempty"` // Whether the repository is template
TrustModel string `json:"trust_model,omitempty"` // TrustModel of the repository TrustModel string `json:"trust_model,omitempty"` // TrustModel of the repository
} }
// EditRepoOption — EditRepoOption options when editing a repository's properties // EditRepoOption — EditRepoOption options when editing a repository's properties
//
// Usage:
//
// opts := EditRepoOption{Description: "example"}
type EditRepoOption struct { type EditRepoOption struct {
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge,omitempty"` // either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging. AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge,omitempty"` // either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging.
AllowManualMerge bool `json:"allow_manual_merge,omitempty"` // either `true` to allow mark pr as merged manually, or `false` to prevent it. AllowManualMerge bool `json:"allow_manual_merge,omitempty"` // either `true` to allow mark pr as merged manually, or `false` to prevent it.
AllowMerge bool `json:"allow_merge_commits,omitempty"` // either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. AllowMerge bool `json:"allow_merge_commits,omitempty"` // either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits.
AllowRebase bool `json:"allow_rebase,omitempty"` // either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. AllowRebase bool `json:"allow_rebase,omitempty"` // either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.
AllowRebaseMerge bool `json:"allow_rebase_explicit,omitempty"` // either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. AllowRebaseMerge bool `json:"allow_rebase_explicit,omitempty"` // either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits.
AllowRebaseUpdate bool `json:"allow_rebase_update,omitempty"` // either `true` to allow updating pull request branch by rebase, or `false` to prevent it. AllowRebaseUpdate bool `json:"allow_rebase_update,omitempty"` // either `true` to allow updating pull request branch by rebase, or `false` to prevent it.
AllowSquash bool `json:"allow_squash_merge,omitempty"` // either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. AllowSquash bool `json:"allow_squash_merge,omitempty"` // either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.
Archived bool `json:"archived,omitempty"` // set to `true` to archive this repository. Archived bool `json:"archived,omitempty"` // set to `true` to archive this repository.
AutodetectManualMerge bool `json:"autodetect_manual_merge,omitempty"` // either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur. AutodetectManualMerge bool `json:"autodetect_manual_merge,omitempty"` // either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur.
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit,omitempty"` // set to `true` to allow edits from maintainers by default DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit,omitempty"` // set to `true` to allow edits from maintainers by default
DefaultBranch string `json:"default_branch,omitempty"` // sets the default branch for this repository. DefaultBranch string `json:"default_branch,omitempty"` // sets the default branch for this repository.
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge,omitempty"` // set to `true` to delete pr branch after merge by default DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge,omitempty"` // set to `true` to delete pr branch after merge by default
DefaultMergeStyle string `json:"default_merge_style,omitempty"` // set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only". DefaultMergeStyle string `json:"default_merge_style,omitempty"` // set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only".
DefaultUpdateStyle string `json:"default_update_style,omitempty"` // set to a update style to be used by this repository: "rebase" or "merge" DefaultUpdateStyle string `json:"default_update_style,omitempty"` // set to a update style to be used by this repository: "rebase" or "merge"
Description string `json:"description,omitempty"` // a short description of the repository. Description string `json:"description,omitempty"` // a short description of the repository.
EnablePrune bool `json:"enable_prune,omitempty"` // enable prune - remove obsolete remote-tracking references when mirroring EnablePrune bool `json:"enable_prune,omitempty"` // enable prune - remove obsolete remote-tracking references when mirroring
ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"` ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"`
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
GloballyEditableWiki bool `json:"globally_editable_wiki,omitempty"` // set the globally editable state of the wiki GloballyEditableWiki bool `json:"globally_editable_wiki,omitempty"` // set the globally editable state of the wiki
HasActions bool `json:"has_actions,omitempty"` // either `true` to enable actions unit, or `false` to disable them. HasActions bool `json:"has_actions,omitempty"` // either `true` to enable actions unit, or `false` to disable them.
HasIssues bool `json:"has_issues,omitempty"` // either `true` to enable issues for this repository or `false` to disable them. HasIssues bool `json:"has_issues,omitempty"` // either `true` to enable issues for this repository or `false` to disable them.
HasPackages bool `json:"has_packages,omitempty"` // either `true` to enable packages unit, or `false` to disable them. HasPackages bool `json:"has_packages,omitempty"` // either `true` to enable packages unit, or `false` to disable them.
HasProjects bool `json:"has_projects,omitempty"` // either `true` to enable project unit, or `false` to disable them. HasProjects bool `json:"has_projects,omitempty"` // either `true` to enable project unit, or `false` to disable them.
HasPullRequests bool `json:"has_pull_requests,omitempty"` // either `true` to allow pull requests, or `false` to prevent pull request. HasPullRequests bool `json:"has_pull_requests,omitempty"` // either `true` to allow pull requests, or `false` to prevent pull request.
HasReleases bool `json:"has_releases,omitempty"` // either `true` to enable releases unit, or `false` to disable them. HasReleases bool `json:"has_releases,omitempty"` // either `true` to enable releases unit, or `false` to disable them.
HasWiki bool `json:"has_wiki,omitempty"` // either `true` to enable the wiki for this repository or `false` to disable it. HasWiki bool `json:"has_wiki,omitempty"` // either `true` to enable the wiki for this repository or `false` to disable it.
IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts,omitempty"` // either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts,omitempty"` // either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace.
InternalTracker *InternalTracker `json:"internal_tracker,omitempty"` InternalTracker *InternalTracker `json:"internal_tracker,omitempty"`
MirrorInterval string `json:"mirror_interval,omitempty"` // set to a string like `8h30m0s` to set the mirror interval time MirrorInterval string `json:"mirror_interval,omitempty"` // set to a string like `8h30m0s` to set the mirror interval time
Name string `json:"name,omitempty"` // name of the repository Name string `json:"name,omitempty"` // name of the repository
Private bool `json:"private,omitempty"` // either `true` to make the repository private or `false` to make it public. Note: you will get a 422 error if the organization restricts changing repository visibility to organization owners and a non-owner tries to change the value of private. Private bool `json:"private,omitempty"` // either `true` to make the repository private or `false` to make it public. Note: you will get a 422 error if the organization restricts changing repository visibility to organization owners and a non-owner tries to change the value of private.
Template bool `json:"template,omitempty"` // either `true` to make this repository a template or `false` to make it a normal repository Template bool `json:"template,omitempty"` // either `true` to make this repository a template or `false` to make it a normal repository
Website string `json:"website,omitempty"` // a URL with more information about the repository. Website string `json:"website,omitempty"` // a URL with more information about the repository.
WikiBranch string `json:"wiki_branch,omitempty"` // sets the branch used for this repository's wiki. WikiBranch string `json:"wiki_branch,omitempty"` // sets the branch used for this repository's wiki.
} }
// ExternalTracker — ExternalTracker represents settings for external tracker // ExternalTracker — ExternalTracker represents settings for external tracker
//
// Usage:
//
// opts := ExternalTracker{ExternalTrackerFormat: "example"}
type ExternalTracker struct { type ExternalTracker struct {
ExternalTrackerFormat string `json:"external_tracker_format,omitempty"` // External Issue Tracker URL Format. Use the placeholders {user}, {repo} and {index} for the username, repository name and issue index. ExternalTrackerFormat string `json:"external_tracker_format,omitempty"` // External Issue Tracker URL Format. Use the placeholders {user}, {repo} and {index} for the username, repository name and issue index.
ExternalTrackerRegexpPattern string `json:"external_tracker_regexp_pattern,omitempty"` // External Issue Tracker issue regular expression ExternalTrackerRegexpPattern string `json:"external_tracker_regexp_pattern,omitempty"` // External Issue Tracker issue regular expression
ExternalTrackerStyle string `json:"external_tracker_style,omitempty"` // External Issue Tracker Number Format, either `numeric`, `alphanumeric`, or `regexp` ExternalTrackerStyle string `json:"external_tracker_style,omitempty"` // External Issue Tracker Number Format, either `numeric`, `alphanumeric`, or `regexp`
ExternalTrackerURL string `json:"external_tracker_url,omitempty"` // URL of external issue tracker. ExternalTrackerURL string `json:"external_tracker_url,omitempty"` // URL of external issue tracker.
} }
// ExternalWiki — ExternalWiki represents setting for external wiki // ExternalWiki — ExternalWiki represents setting for external wiki
//
// Usage:
//
// opts := ExternalWiki{ExternalWikiURL: "https://example.com"}
type ExternalWiki struct { type ExternalWiki struct {
ExternalWikiURL string `json:"external_wiki_url,omitempty"` // URL of external wiki. ExternalWikiURL string `json:"external_wiki_url,omitempty"` // URL of external wiki.
} }
// InternalTracker — InternalTracker represents settings for internal tracker // InternalTracker — InternalTracker represents settings for internal tracker
//
// Usage:
//
// opts := InternalTracker{AllowOnlyContributorsToTrackTime: true}
type InternalTracker struct { type InternalTracker struct {
AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time,omitempty"` // Let only contributors track time (Built-in issue tracker) AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time,omitempty"` // Let only contributors track time (Built-in issue tracker)
EnableIssueDependencies bool `json:"enable_issue_dependencies,omitempty"` // Enable dependencies for issues and pull requests (Built-in issue tracker) EnableIssueDependencies bool `json:"enable_issue_dependencies,omitempty"` // Enable dependencies for issues and pull requests (Built-in issue tracker)
EnableTimeTracker bool `json:"enable_time_tracker,omitempty"` // Enable time tracking (Built-in issue tracker) EnableTimeTracker bool `json:"enable_time_tracker,omitempty"` // Enable time tracking (Built-in issue tracker)
} }
// PushMirror — PushMirror represents information of a push mirror // PushMirror — PushMirror represents information of a push mirror
//
// Usage:
//
// opts := PushMirror{RemoteName: "example"}
type PushMirror struct { type PushMirror struct {
CreatedUnix time.Time `json:"created,omitempty"` CreatedUnix time.Time `json:"created,omitempty"`
Interval string `json:"interval,omitempty"` Interval string `json:"interval,omitempty"`
LastError string `json:"last_error,omitempty"` LastError string `json:"last_error,omitempty"`
LastUpdateUnix time.Time `json:"last_update,omitempty"` LastUpdateUnix time.Time `json:"last_update,omitempty"`
PublicKey string `json:"public_key,omitempty"` PublicKey string `json:"public_key,omitempty"`
RemoteAddress string `json:"remote_address,omitempty"` RemoteAddress string `json:"remote_address,omitempty"`
RemoteName string `json:"remote_name,omitempty"` RemoteName string `json:"remote_name,omitempty"`
RepoName string `json:"repo_name,omitempty"` RepoName string `json:"repo_name,omitempty"`
SyncOnCommit bool `json:"sync_on_commit,omitempty"` SyncOnCommit bool `json:"sync_on_commit,omitempty"`
} }
// RepoCollaboratorPermission — RepoCollaboratorPermission to get repository permission for a collaborator // RepoCollaboratorPermission — RepoCollaboratorPermission to get repository permission for a collaborator
//
// Usage:
//
// opts := RepoCollaboratorPermission{RoleName: "example"}
type RepoCollaboratorPermission struct { type RepoCollaboratorPermission struct {
Permission string `json:"permission,omitempty"` Permission string `json:"permission,omitempty"`
RoleName string `json:"role_name,omitempty"` RoleName string `json:"role_name,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
} }
// Usage:
//
// opts := RepoCommit{Message: "example"}
type RepoCommit struct { type RepoCommit struct {
Author *CommitUser `json:"author,omitempty"` Author *CommitUser `json:"author,omitempty"`
Committer *CommitUser `json:"committer,omitempty"` Committer *CommitUser `json:"committer,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Tree *CommitMeta `json:"tree,omitempty"` Tree *CommitMeta `json:"tree,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Verification *PayloadCommitVerification `json:"verification,omitempty"` Verification *PayloadCommitVerification `json:"verification,omitempty"`
} }
// RepoTopicOptions — RepoTopicOptions a collection of repo topic names // RepoTopicOptions — RepoTopicOptions a collection of repo topic names
//
// Usage:
//
// opts := RepoTopicOptions{Topics: []string{"example"}}
type RepoTopicOptions struct { type RepoTopicOptions struct {
Topics []string `json:"topics,omitempty"` // list of topic names Topics []string `json:"topics,omitempty"` // list of topic names
} }
// RepoTransfer — RepoTransfer represents a pending repo transfer // RepoTransfer — RepoTransfer represents a pending repo transfer
//
// Usage:
//
// opts := RepoTransfer{Teams: {}}
type RepoTransfer struct { type RepoTransfer struct {
Doer *User `json:"doer,omitempty"` Doer *User `json:"doer,omitempty"`
Recipient *User `json:"recipient,omitempty"` Recipient *User `json:"recipient,omitempty"`
Teams []*Team `json:"teams,omitempty"` Teams []*Team `json:"teams,omitempty"`
} }
// Repository — Repository represents a repository // Repository — Repository represents a repository
//
// Usage:
//
// opts := Repository{Description: "example"}
type Repository struct { type Repository struct {
AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge,omitempty"` AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge,omitempty"`
AllowMerge bool `json:"allow_merge_commits,omitempty"` AllowMerge bool `json:"allow_merge_commits,omitempty"`
AllowRebase bool `json:"allow_rebase,omitempty"` AllowRebase bool `json:"allow_rebase,omitempty"`
AllowRebaseMerge bool `json:"allow_rebase_explicit,omitempty"` AllowRebaseMerge bool `json:"allow_rebase_explicit,omitempty"`
AllowRebaseUpdate bool `json:"allow_rebase_update,omitempty"` AllowRebaseUpdate bool `json:"allow_rebase_update,omitempty"`
AllowSquash bool `json:"allow_squash_merge,omitempty"` AllowSquash bool `json:"allow_squash_merge,omitempty"`
Archived bool `json:"archived,omitempty"` Archived bool `json:"archived,omitempty"`
ArchivedAt time.Time `json:"archived_at,omitempty"` ArchivedAt time.Time `json:"archived_at,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"` AvatarURL string `json:"avatar_url,omitempty"`
CloneURL string `json:"clone_url,omitempty"` CloneURL string `json:"clone_url,omitempty"`
Created time.Time `json:"created_at,omitempty"` Created time.Time `json:"created_at,omitempty"`
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit,omitempty"` DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit,omitempty"`
DefaultBranch string `json:"default_branch,omitempty"` DefaultBranch string `json:"default_branch,omitempty"`
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge,omitempty"` DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge,omitempty"`
DefaultMergeStyle string `json:"default_merge_style,omitempty"` DefaultMergeStyle string `json:"default_merge_style,omitempty"`
DefaultUpdateStyle string `json:"default_update_style,omitempty"` DefaultUpdateStyle string `json:"default_update_style,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Empty bool `json:"empty,omitempty"` Empty bool `json:"empty,omitempty"`
ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"` ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"`
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
Fork bool `json:"fork,omitempty"` Fork bool `json:"fork,omitempty"`
Forks int64 `json:"forks_count,omitempty"` Forks int64 `json:"forks_count,omitempty"`
FullName string `json:"full_name,omitempty"` FullName string `json:"full_name,omitempty"`
GloballyEditableWiki bool `json:"globally_editable_wiki,omitempty"` GloballyEditableWiki bool `json:"globally_editable_wiki,omitempty"`
HTMLURL string `json:"html_url,omitempty"` HTMLURL string `json:"html_url,omitempty"`
HasActions bool `json:"has_actions,omitempty"` HasActions bool `json:"has_actions,omitempty"`
HasIssues bool `json:"has_issues,omitempty"` HasIssues bool `json:"has_issues,omitempty"`
HasPackages bool `json:"has_packages,omitempty"` HasPackages bool `json:"has_packages,omitempty"`
HasProjects bool `json:"has_projects,omitempty"` HasProjects bool `json:"has_projects,omitempty"`
HasPullRequests bool `json:"has_pull_requests,omitempty"` HasPullRequests bool `json:"has_pull_requests,omitempty"`
HasReleases bool `json:"has_releases,omitempty"` HasReleases bool `json:"has_releases,omitempty"`
HasWiki bool `json:"has_wiki,omitempty"` HasWiki bool `json:"has_wiki,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts,omitempty"` IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts,omitempty"`
Internal bool `json:"internal,omitempty"` Internal bool `json:"internal,omitempty"`
InternalTracker *InternalTracker `json:"internal_tracker,omitempty"` InternalTracker *InternalTracker `json:"internal_tracker,omitempty"`
Language string `json:"language,omitempty"` Language string `json:"language,omitempty"`
LanguagesURL string `json:"languages_url,omitempty"` LanguagesURL string `json:"languages_url,omitempty"`
Link string `json:"link,omitempty"` Link string `json:"link,omitempty"`
Mirror bool `json:"mirror,omitempty"` Mirror bool `json:"mirror,omitempty"`
MirrorInterval string `json:"mirror_interval,omitempty"` MirrorInterval string `json:"mirror_interval,omitempty"`
MirrorUpdated time.Time `json:"mirror_updated,omitempty"` MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
ObjectFormatName string `json:"object_format_name,omitempty"` // ObjectFormatName of the underlying git repository ObjectFormatName string `json:"object_format_name,omitempty"` // ObjectFormatName of the underlying git repository
OpenIssues int64 `json:"open_issues_count,omitempty"` OpenIssues int64 `json:"open_issues_count,omitempty"`
OpenPulls int64 `json:"open_pr_counter,omitempty"` OpenPulls int64 `json:"open_pr_counter,omitempty"`
OriginalURL string `json:"original_url,omitempty"` OriginalURL string `json:"original_url,omitempty"`
Owner *User `json:"owner,omitempty"` Owner *User `json:"owner,omitempty"`
Parent *Repository `json:"parent,omitempty"` Parent *Repository `json:"parent,omitempty"`
Permissions *Permission `json:"permissions,omitempty"` Permissions *Permission `json:"permissions,omitempty"`
Private bool `json:"private,omitempty"` Private bool `json:"private,omitempty"`
Releases int64 `json:"release_counter,omitempty"` Releases int64 `json:"release_counter,omitempty"`
RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"` RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"`
SSHURL string `json:"ssh_url,omitempty"` SSHURL string `json:"ssh_url,omitempty"`
Size int64 `json:"size,omitempty"` Size int64 `json:"size,omitempty"`
Stars int64 `json:"stars_count,omitempty"` Stars int64 `json:"stars_count,omitempty"`
Template bool `json:"template,omitempty"` Template bool `json:"template,omitempty"`
Topics []string `json:"topics,omitempty"` Topics []string `json:"topics,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Updated time.Time `json:"updated_at,omitempty"` Updated time.Time `json:"updated_at,omitempty"`
Watchers int64 `json:"watchers_count,omitempty"` Watchers int64 `json:"watchers_count,omitempty"`
Website string `json:"website,omitempty"` Website string `json:"website,omitempty"`
WikiBranch string `json:"wiki_branch,omitempty"` WikiBranch string `json:"wiki_branch,omitempty"`
} }
// RepositoryMeta — RepositoryMeta basic repository information // RepositoryMeta — RepositoryMeta basic repository information
//
// Usage:
//
// opts := RepositoryMeta{FullName: "example"}
type RepositoryMeta struct { type RepositoryMeta struct {
FullName string `json:"full_name,omitempty"` FullName string `json:"full_name,omitempty"`
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Owner string `json:"owner,omitempty"` Owner string `json:"owner,omitempty"`
} }
// TransferRepoOption — TransferRepoOption options when transfer a repository's ownership // TransferRepoOption — TransferRepoOption options when transfer a repository's ownership
//
// Usage:
//
// opts := TransferRepoOption{NewOwner: "example"}
type TransferRepoOption struct { type TransferRepoOption struct {
NewOwner string `json:"new_owner"` NewOwner string `json:"new_owner"`
TeamIDs []int64 `json:"team_ids,omitempty"` // ID of the team or teams to add to the repository. Teams can only be added to organization-owned repositories. TeamIDs []int64 `json:"team_ids,omitempty"` // ID of the team or teams to add to the repository. Teams can only be added to organization-owned repositories.
} }
// UpdateRepoAvatarOption — UpdateRepoAvatarUserOption options when updating the repo avatar // UpdateRepoAvatarOption — UpdateRepoAvatarUserOption options when updating the repo avatar
//
// Usage:
//
// opts := UpdateRepoAvatarOption{Image: "example"}
type UpdateRepoAvatarOption struct { type UpdateRepoAvatarOption struct {
Image string `json:"image,omitempty"` // image must be base64 encoded Image string `json:"image,omitempty"` // image must be base64 encoded
} }

View file

@ -2,9 +2,8 @@
package types package types
// ReviewStateType — ReviewStateType review state type // ReviewStateType — ReviewStateType review state type
// // ReviewStateType has no fields in the swagger spec.
// Usage: type ReviewStateType struct{}
//
// opts := ReviewStateType("example")
type ReviewStateType string

View file

@ -2,52 +2,38 @@
package types package types
// GeneralAPISettings — GeneralAPISettings contains global api settings exposed by it // GeneralAPISettings — GeneralAPISettings contains global api settings exposed by it
//
// Usage:
//
// opts := GeneralAPISettings{DefaultGitTreesPerPage: 1}
type GeneralAPISettings struct { type GeneralAPISettings struct {
DefaultGitTreesPerPage int64 `json:"default_git_trees_per_page,omitempty"` DefaultGitTreesPerPage int64 `json:"default_git_trees_per_page,omitempty"`
DefaultMaxBlobSize int64 `json:"default_max_blob_size,omitempty"` DefaultMaxBlobSize int64 `json:"default_max_blob_size,omitempty"`
DefaultPagingNum int64 `json:"default_paging_num,omitempty"` DefaultPagingNum int64 `json:"default_paging_num,omitempty"`
MaxResponseItems int64 `json:"max_response_items,omitempty"` MaxResponseItems int64 `json:"max_response_items,omitempty"`
} }
// GeneralAttachmentSettings — GeneralAttachmentSettings contains global Attachment settings exposed by API // GeneralAttachmentSettings — GeneralAttachmentSettings contains global Attachment settings exposed by API
//
// Usage:
//
// opts := GeneralAttachmentSettings{AllowedTypes: "example"}
type GeneralAttachmentSettings struct { type GeneralAttachmentSettings struct {
AllowedTypes string `json:"allowed_types,omitempty"` AllowedTypes string `json:"allowed_types,omitempty"`
Enabled bool `json:"enabled,omitempty"` Enabled bool `json:"enabled,omitempty"`
MaxFiles int64 `json:"max_files,omitempty"` MaxFiles int64 `json:"max_files,omitempty"`
MaxSize int64 `json:"max_size,omitempty"` MaxSize int64 `json:"max_size,omitempty"`
} }
// GeneralRepoSettings — GeneralRepoSettings contains global repository settings exposed by API // GeneralRepoSettings — GeneralRepoSettings contains global repository settings exposed by API
//
// Usage:
//
// opts := GeneralRepoSettings{ForksDisabled: true}
type GeneralRepoSettings struct { type GeneralRepoSettings struct {
ForksDisabled bool `json:"forks_disabled,omitempty"` ForksDisabled bool `json:"forks_disabled,omitempty"`
HTTPGitDisabled bool `json:"http_git_disabled,omitempty"` HTTPGitDisabled bool `json:"http_git_disabled,omitempty"`
LFSDisabled bool `json:"lfs_disabled,omitempty"` LFSDisabled bool `json:"lfs_disabled,omitempty"`
MigrationsDisabled bool `json:"migrations_disabled,omitempty"` MigrationsDisabled bool `json:"migrations_disabled,omitempty"`
MirrorsDisabled bool `json:"mirrors_disabled,omitempty"` MirrorsDisabled bool `json:"mirrors_disabled,omitempty"`
StarsDisabled bool `json:"stars_disabled,omitempty"` StarsDisabled bool `json:"stars_disabled,omitempty"`
TimeTrackingDisabled bool `json:"time_tracking_disabled,omitempty"` TimeTrackingDisabled bool `json:"time_tracking_disabled,omitempty"`
} }
// GeneralUISettings — GeneralUISettings contains global ui settings exposed by API // GeneralUISettings — GeneralUISettings contains global ui settings exposed by API
//
// Usage:
//
// opts := GeneralUISettings{AllowedReactions: []string{"example"}}
type GeneralUISettings struct { type GeneralUISettings struct {
AllowedReactions []string `json:"allowed_reactions,omitempty"` AllowedReactions []string `json:"allowed_reactions,omitempty"`
CustomEmojis []string `json:"custom_emojis,omitempty"` CustomEmojis []string `json:"custom_emojis,omitempty"`
DefaultTheme string `json:"default_theme,omitempty"` DefaultTheme string `json:"default_theme,omitempty"`
} }

Some files were not shown because too many files have changed in this diff Show more