Fix service path segment escaping

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-23 06:47:10 +00:00
parent 244ff651c3
commit 9cdab89c6c
19 changed files with 435 additions and 131 deletions

View file

@ -2,7 +2,6 @@ package forge
import (
"context"
"fmt"
"iter"
"dappco.re/go/core/forge/types"
@ -21,82 +20,82 @@ func newActionsService(c *Client) *ActionsService {
// ListRepoSecrets returns all secrets for a repository.
func (s *ActionsService) ListRepoSecrets(ctx context.Context, owner, repo string) ([]types.Secret, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "actions", "secrets")
return ListAll[types.Secret](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "actions", "secrets")
return ListIter[types.Secret](ctx, s.client, path, nil)
}
// CreateRepoSecret creates or updates a secret in a repository.
// Forgejo expects a PUT with {"data": "secret-value"} body.
func (s *ActionsService) CreateRepoSecret(ctx context.Context, owner, repo, name string, data string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets/%s", owner, repo, name)
path := escapePathSegments("/api/v1/repos", owner, repo, "actions", "secrets", name)
body := map[string]string{"data": data}
return s.client.Put(ctx, path, body, nil)
}
// DeleteRepoSecret removes a secret from a repository.
func (s *ActionsService) DeleteRepoSecret(ctx context.Context, owner, repo, name string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets/%s", owner, repo, name)
path := escapePathSegments("/api/v1/repos", owner, repo, "actions", "secrets", name)
return s.client.Delete(ctx, path)
}
// ListRepoVariables returns all action variables for a repository.
func (s *ActionsService) ListRepoVariables(ctx context.Context, owner, repo string) ([]types.ActionVariable, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "actions", "variables")
return ListAll[types.ActionVariable](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "actions", "variables")
return ListIter[types.ActionVariable](ctx, s.client, path, nil)
}
// CreateRepoVariable creates a new action variable in a repository.
// Forgejo expects a POST with {"value": "var-value"} body.
func (s *ActionsService) CreateRepoVariable(ctx context.Context, owner, repo, name, value string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/%s", owner, repo, name)
path := escapePathSegments("/api/v1/repos", owner, repo, "actions", "variables", name)
body := types.CreateVariableOption{Value: value}
return s.client.Post(ctx, path, body, nil)
}
// DeleteRepoVariable removes an action variable from a repository.
func (s *ActionsService) DeleteRepoVariable(ctx context.Context, owner, repo, name string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/%s", owner, repo, name)
path := escapePathSegments("/api/v1/repos", owner, repo, "actions", "variables", name)
return s.client.Delete(ctx, path)
}
// ListOrgSecrets returns all secrets for an organisation.
func (s *ActionsService) ListOrgSecrets(ctx context.Context, org string) ([]types.Secret, error) {
path := fmt.Sprintf("/api/v1/orgs/%s/actions/secrets", org)
path := escapePathSegments("/api/v1/orgs", org, "actions", "secrets")
return ListAll[types.Secret](ctx, s.client, path, nil)
}
// IterOrgSecrets returns an iterator over all secrets for an organisation.
func (s *ActionsService) IterOrgSecrets(ctx context.Context, org string) iter.Seq2[types.Secret, error] {
path := fmt.Sprintf("/api/v1/orgs/%s/actions/secrets", org)
path := escapePathSegments("/api/v1/orgs", org, "actions", "secrets")
return ListIter[types.Secret](ctx, s.client, path, nil)
}
// ListOrgVariables returns all action variables for an organisation.
func (s *ActionsService) ListOrgVariables(ctx context.Context, org string) ([]types.ActionVariable, error) {
path := fmt.Sprintf("/api/v1/orgs/%s/actions/variables", org)
path := escapePathSegments("/api/v1/orgs", org, "actions", "variables")
return ListAll[types.ActionVariable](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/orgs/%s/actions/variables", org)
path := escapePathSegments("/api/v1/orgs", org, "actions", "variables")
return ListIter[types.ActionVariable](ctx, s.client, path, nil)
}
// DispatchWorkflow triggers a workflow run.
func (s *ActionsService) DispatchWorkflow(ctx context.Context, owner, repo, workflow string, opts map[string]any) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/actions/workflows/%s/dispatches", owner, repo, workflow)
path := escapePathSegments("/api/v1/repos", owner, repo, "actions", "workflows", workflow, "dispatches")
return s.client.Post(ctx, path, opts, nil)
}

View file

@ -2,7 +2,6 @@ package forge
import (
"context"
"fmt"
"iter"
"dappco.re/go/core/forge/types"
@ -23,19 +22,19 @@ func newBranchService(c *Client) *BranchService {
// ListBranchProtections returns all branch protections for a repository.
func (s *BranchService) ListBranchProtections(ctx context.Context, owner, repo string) ([]types.BranchProtection, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "branch_protections")
return ListAll[types.BranchProtection](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "branch_protections")
return ListIter[types.BranchProtection](ctx, s.client, path, nil)
}
// GetBranchProtection returns a single branch protection by name.
func (s *BranchService) GetBranchProtection(ctx context.Context, owner, repo, name string) (*types.BranchProtection, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
path := escapePathSegments("/api/v1/repos", owner, repo, "branch_protections", name)
var out types.BranchProtection
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -45,7 +44,7 @@ func (s *BranchService) GetBranchProtection(ctx context.Context, owner, repo, na
// CreateBranchProtection creates a new branch protection rule.
func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo string, opts *types.CreateBranchProtectionOption) (*types.BranchProtection, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "branch_protections")
var out types.BranchProtection
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
@ -55,7 +54,7 @@ func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo
// EditBranchProtection updates an existing branch protection rule.
func (s *BranchService) EditBranchProtection(ctx context.Context, owner, repo, name string, opts *types.EditBranchProtectionOption) (*types.BranchProtection, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
path := escapePathSegments("/api/v1/repos", owner, repo, "branch_protections", name)
var out types.BranchProtection
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
@ -65,6 +64,6 @@ func (s *BranchService) EditBranchProtection(ctx context.Context, owner, repo, n
// DeleteBranchProtection deletes a branch protection rule.
func (s *BranchService) DeleteBranchProtection(ctx context.Context, owner, repo, name string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections/%s", owner, repo, name)
path := escapePathSegments("/api/v1/repos", owner, repo, "branch_protections", name)
return s.client.Delete(ctx, path)
}

View file

@ -2,7 +2,6 @@ package forge
import (
"context"
"fmt"
"iter"
"dappco.re/go/core/forge/types"
@ -51,7 +50,7 @@ func (s *CommitService) Get(ctx context.Context, params Params) (*types.Commit,
// 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) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, ref)
path := escapePathSegments("/api/v1/repos", owner, repo, "statuses", ref)
var out types.CombinedStatus
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -61,7 +60,7 @@ func (s *CommitService) GetCombinedStatus(ctx context.Context, owner, repo, ref
// ListStatuses returns all commit statuses for a given ref.
func (s *CommitService) ListStatuses(ctx context.Context, owner, repo, ref string) ([]types.CommitStatus, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/commits/%s/statuses", owner, repo, ref)
path := escapePathSegments("/api/v1/repos", owner, repo, "commits", ref, "statuses")
var out []types.CommitStatus
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -71,7 +70,7 @@ func (s *CommitService) ListStatuses(ctx context.Context, owner, repo, ref strin
// 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) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", owner, repo, sha)
path := escapePathSegments("/api/v1/repos", owner, repo, "statuses", sha)
var out types.CommitStatus
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
@ -81,7 +80,7 @@ func (s *CommitService) CreateStatus(ctx context.Context, owner, repo, sha strin
// GetNote returns the git note for a given commit SHA.
func (s *CommitService) GetNote(ctx context.Context, owner, repo, sha string) (*types.Note, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/git/notes/%s", owner, repo, sha)
path := escapePathSegments("/api/v1/repos", owner, repo, "git", "notes", sha)
var out types.Note
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err

View file

@ -2,7 +2,6 @@ package forge
import (
"context"
"fmt"
"dappco.re/go/core/forge/types"
)
@ -19,7 +18,7 @@ func newContentService(c *Client) *ContentService {
// 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) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
path := escapePathSegments("/api/v1/repos", owner, repo, "contents", filepath)
var out types.ContentsResponse
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -29,7 +28,7 @@ func (s *ContentService) GetFile(ctx context.Context, owner, repo, filepath stri
// 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) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
path := escapePathSegments("/api/v1/repos", owner, repo, "contents", filepath)
var out types.FileResponse
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
@ -39,7 +38,7 @@ func (s *ContentService) CreateFile(ctx context.Context, owner, repo, filepath s
// 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) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
path := escapePathSegments("/api/v1/repos", owner, repo, "contents", filepath)
var out types.FileResponse
if err := s.client.Put(ctx, path, opts, &out); err != nil {
return nil, err
@ -49,12 +48,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.
func (s *ContentService) DeleteFile(ctx context.Context, owner, repo, filepath string, opts *types.DeleteFileOptions) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", owner, repo, filepath)
path := escapePathSegments("/api/v1/repos", owner, repo, "contents", filepath)
return s.client.DeleteWithBody(ctx, path, opts)
}
// GetRawFile returns the raw file content as bytes.
func (s *ContentService) GetRawFile(ctx context.Context, owner, repo, filepath string) ([]byte, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/raw/%s", owner, repo, filepath)
path := escapePathSegments("/api/v1/repos", owner, repo, "raw", filepath)
return s.client.GetRaw(ctx, path)
}

View file

@ -2,8 +2,8 @@ package forge
import (
"context"
"fmt"
"iter"
"strconv"
"dappco.re/go/core/forge/types"
)
@ -23,77 +23,77 @@ func newIssueService(c *Client) *IssueService {
// Pin pins an issue.
func (s *IssueService) Pin(ctx context.Context, owner, repo string, index int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "pin")
return s.client.Post(ctx, path, nil, nil)
}
// Unpin unpins an issue.
func (s *IssueService) Unpin(ctx context.Context, owner, repo string, index int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "pin")
return s.client.Delete(ctx, path)
}
// SetDeadline sets or updates the deadline on an issue.
func (s *IssueService) SetDeadline(ctx context.Context, owner, repo string, index int64, deadline string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/deadline", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "deadline")
body := map[string]string{"due_date": deadline}
return s.client.Post(ctx, path, body, nil)
}
// AddReaction adds a reaction to an issue.
func (s *IssueService) AddReaction(ctx context.Context, owner, repo string, index int64, reaction string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "reactions")
body := map[string]string{"content": reaction}
return s.client.Post(ctx, path, body, nil)
}
// DeleteReaction removes a reaction from an issue.
func (s *IssueService) DeleteReaction(ctx context.Context, owner, repo string, index int64, reaction string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "reactions")
body := map[string]string{"content": reaction}
return s.client.DeleteWithBody(ctx, path, body)
}
// StartStopwatch starts the stopwatch on an issue.
func (s *IssueService) StartStopwatch(ctx context.Context, owner, repo string, index int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/stopwatch/start", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "stopwatch", "start")
return s.client.Post(ctx, path, nil, nil)
}
// StopStopwatch stops the stopwatch on an issue.
func (s *IssueService) StopStopwatch(ctx context.Context, owner, repo string, index int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/stopwatch/stop", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "stopwatch", "stop")
return s.client.Post(ctx, path, nil, nil)
}
// AddLabels adds labels to an issue.
func (s *IssueService) AddLabels(ctx context.Context, owner, repo string, index int64, labelIDs []int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "labels")
body := types.IssueLabelsOption{Labels: toAnySlice(labelIDs)}
return s.client.Post(ctx, path, body, nil)
}
// RemoveLabel removes a single label from an issue.
func (s *IssueService) RemoveLabel(ctx context.Context, owner, repo string, index int64, labelID int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels/%d", owner, repo, index, labelID)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "labels", strconv.FormatInt(labelID, 10))
return s.client.Delete(ctx, path)
}
// ListComments returns all comments on an issue.
func (s *IssueService) ListComments(ctx context.Context, owner, repo string, index int64) ([]types.Comment, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "comments")
return ListAll[types.Comment](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "comments")
return ListIter[types.Comment](ctx, s.client, path, nil)
}
// CreateComment creates a comment on an issue.
func (s *IssueService) CreateComment(ctx context.Context, owner, repo string, index int64, body string) (*types.Comment, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "issues", strconv.FormatInt(index, 10), "comments")
opts := types.CreateIssueCommentOption{Body: body}
var out types.Comment
if err := s.client.Post(ctx, path, opts, &out); err != nil {

View file

@ -2,8 +2,8 @@ package forge
import (
"context"
"fmt"
"iter"
"strconv"
"dappco.re/go/core/forge/types"
)
@ -20,19 +20,19 @@ func newLabelService(c *Client) *LabelService {
// ListRepoLabels returns all labels for a repository.
func (s *LabelService) ListRepoLabels(ctx context.Context, owner, repo string) ([]types.Label, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "labels")
return ListAll[types.Label](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "labels")
return ListIter[types.Label](ctx, s.client, path, nil)
}
// GetRepoLabel returns a single label by ID.
func (s *LabelService) GetRepoLabel(ctx context.Context, owner, repo string, id int64) (*types.Label, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
path := escapePathSegments("/api/v1/repos", owner, repo, "labels", strconv.FormatInt(id, 10))
var out types.Label
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -42,7 +42,7 @@ func (s *LabelService) GetRepoLabel(ctx context.Context, owner, repo string, id
// CreateRepoLabel creates a new label in a repository.
func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string, opts *types.CreateLabelOption) (*types.Label, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "labels")
var out types.Label
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
@ -52,7 +52,7 @@ func (s *LabelService) CreateRepoLabel(ctx context.Context, owner, repo string,
// 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) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
path := escapePathSegments("/api/v1/repos", owner, repo, "labels", strconv.FormatInt(id, 10))
var out types.Label
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
@ -62,25 +62,25 @@ func (s *LabelService) EditRepoLabel(ctx context.Context, owner, repo string, id
// DeleteRepoLabel deletes a label from a repository.
func (s *LabelService) DeleteRepoLabel(ctx context.Context, owner, repo string, id int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner, repo, id)
path := escapePathSegments("/api/v1/repos", owner, repo, "labels", strconv.FormatInt(id, 10))
return s.client.Delete(ctx, path)
}
// ListOrgLabels returns all labels for an organisation.
func (s *LabelService) ListOrgLabels(ctx context.Context, org string) ([]types.Label, error) {
path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
path := escapePathSegments("/api/v1/orgs", org, "labels")
return ListAll[types.Label](ctx, s.client, path, nil)
}
// IterOrgLabels returns an iterator over all labels for an organisation.
func (s *LabelService) IterOrgLabels(ctx context.Context, org string) iter.Seq2[types.Label, error] {
path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
path := escapePathSegments("/api/v1/orgs", org, "labels")
return ListIter[types.Label](ctx, s.client, path, nil)
}
// CreateOrgLabel creates a new label in an organisation.
func (s *LabelService) CreateOrgLabel(ctx context.Context, org string, opts *types.CreateLabelOption) (*types.Label, error) {
path := fmt.Sprintf("/api/v1/orgs/%s/labels", org)
path := escapePathSegments("/api/v1/orgs", org, "labels")
var out types.Label
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err

View file

@ -2,7 +2,6 @@ package forge
import (
"context"
"fmt"
"dappco.re/go/core/forge/types"
)
@ -41,7 +40,7 @@ func (s *MiscService) ListLicenses(ctx context.Context) ([]types.LicensesTemplat
// GetLicense returns a single licence template by name.
func (s *MiscService) GetLicense(ctx context.Context, name string) (*types.LicenseTemplateInfo, error) {
path := fmt.Sprintf("/api/v1/licenses/%s", name)
path := escapePathSegments("/api/v1/licenses", name)
var out types.LicenseTemplateInfo
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -60,7 +59,7 @@ func (s *MiscService) ListGitignoreTemplates(ctx context.Context) ([]string, err
// GetGitignoreTemplate returns a single gitignore template by name.
func (s *MiscService) GetGitignoreTemplate(ctx context.Context, name string) (*types.GitignoreTemplateInfo, error) {
path := fmt.Sprintf("/api/v1/gitignore/templates/%s", name)
path := escapePathSegments("/api/v1/gitignore/templates", name)
var out types.GitignoreTemplateInfo
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err

View file

@ -2,8 +2,8 @@ package forge
import (
"context"
"fmt"
"iter"
"strconv"
"dappco.re/go/core/forge/types"
)
@ -30,13 +30,13 @@ func (s *NotificationService) Iter(ctx context.Context) iter.Seq2[types.Notifica
// ListRepo returns all notifications for a specific repository.
func (s *NotificationService) ListRepo(ctx context.Context, owner, repo string) ([]types.NotificationThread, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/notifications", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "notifications")
return ListAll[types.NotificationThread](ctx, s.client, path, nil)
}
// IterRepo returns an iterator over all notifications for a specific repository.
func (s *NotificationService) IterRepo(ctx context.Context, owner, repo string) iter.Seq2[types.NotificationThread, error] {
path := fmt.Sprintf("/api/v1/repos/%s/%s/notifications", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "notifications")
return ListIter[types.NotificationThread](ctx, s.client, path, nil)
}
@ -47,7 +47,7 @@ func (s *NotificationService) MarkRead(ctx context.Context) error {
// GetThread returns a single notification thread by ID.
func (s *NotificationService) GetThread(ctx context.Context, id int64) (*types.NotificationThread, error) {
path := fmt.Sprintf("/api/v1/notifications/threads/%d", id)
path := escapePathSegments("/api/v1/notifications/threads", strconv.FormatInt(id, 10))
var out types.NotificationThread
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -57,6 +57,6 @@ func (s *NotificationService) GetThread(ctx context.Context, id int64) (*types.N
// MarkThreadRead marks a single notification thread as read.
func (s *NotificationService) MarkThreadRead(ctx context.Context, id int64) error {
path := fmt.Sprintf("/api/v1/notifications/threads/%d", id)
path := escapePathSegments("/api/v1/notifications/threads", strconv.FormatInt(id, 10))
return s.client.Patch(ctx, path, nil, nil)
}

13
orgs.go
View file

@ -2,7 +2,6 @@ package forge
import (
"context"
"fmt"
"iter"
"dappco.re/go/core/forge/types"
@ -23,37 +22,37 @@ func newOrgService(c *Client) *OrgService {
// ListMembers returns all members of an organisation.
func (s *OrgService) ListMembers(ctx context.Context, org string) ([]types.User, error) {
path := fmt.Sprintf("/api/v1/orgs/%s/members", org)
path := escapePathSegments("/api/v1/orgs", org, "members")
return ListAll[types.User](ctx, s.client, path, nil)
}
// IterMembers returns an iterator over all members of an organisation.
func (s *OrgService) IterMembers(ctx context.Context, org string) iter.Seq2[types.User, error] {
path := fmt.Sprintf("/api/v1/orgs/%s/members", org)
path := escapePathSegments("/api/v1/orgs", org, "members")
return ListIter[types.User](ctx, s.client, path, nil)
}
// AddMember adds a user to an organisation.
func (s *OrgService) AddMember(ctx context.Context, org, username string) error {
path := fmt.Sprintf("/api/v1/orgs/%s/members/%s", org, username)
path := escapePathSegments("/api/v1/orgs", org, "members", username)
return s.client.Put(ctx, path, nil, nil)
}
// RemoveMember removes a user from an organisation.
func (s *OrgService) RemoveMember(ctx context.Context, org, username string) error {
path := fmt.Sprintf("/api/v1/orgs/%s/members/%s", org, username)
path := escapePathSegments("/api/v1/orgs", org, "members", username)
return s.client.Delete(ctx, path)
}
// ListUserOrgs returns all organisations for a user.
func (s *OrgService) ListUserOrgs(ctx context.Context, username string) ([]types.Organization, error) {
path := fmt.Sprintf("/api/v1/users/%s/orgs", username)
path := escapePathSegments("/api/v1/users", username, "orgs")
return ListAll[types.Organization](ctx, s.client, path, nil)
}
// IterUserOrgs returns an iterator over all organisations for a user.
func (s *OrgService) IterUserOrgs(ctx context.Context, username string) iter.Seq2[types.Organization, error] {
path := fmt.Sprintf("/api/v1/users/%s/orgs", username)
path := escapePathSegments("/api/v1/users", username, "orgs")
return ListIter[types.Organization](ctx, s.client, path, nil)
}

View file

@ -2,7 +2,6 @@ package forge
import (
"context"
"fmt"
"iter"
"dappco.re/go/core/forge/types"
@ -20,19 +19,19 @@ func newPackageService(c *Client) *PackageService {
// List returns all packages for a given owner.
func (s *PackageService) List(ctx context.Context, owner string) ([]types.Package, error) {
path := fmt.Sprintf("/api/v1/packages/%s", owner)
path := escapePathSegments("/api/v1/packages", owner)
return ListAll[types.Package](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/packages/%s", owner)
path := escapePathSegments("/api/v1/packages", owner)
return ListIter[types.Package](ctx, s.client, path, nil)
}
// 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) {
path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s", owner, pkgType, name, version)
path := escapePathSegments("/api/v1/packages", owner, pkgType, name, version)
var out types.Package
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -42,18 +41,18 @@ func (s *PackageService) Get(ctx context.Context, owner, pkgType, name, version
// Delete removes a package by owner, type, name, and version.
func (s *PackageService) Delete(ctx context.Context, owner, pkgType, name, version string) error {
path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s", owner, pkgType, name, version)
path := escapePathSegments("/api/v1/packages", owner, pkgType, name, version)
return s.client.Delete(ctx, path)
}
// ListFiles returns all files for a specific package version.
func (s *PackageService) ListFiles(ctx context.Context, owner, pkgType, name, version string) ([]types.PackageFile, error) {
path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s/files", owner, pkgType, name, version)
path := escapePathSegments("/api/v1/packages", owner, pkgType, name, version, "files")
return ListAll[types.PackageFile](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s/files", owner, pkgType, name, version)
path := escapePathSegments("/api/v1/packages", owner, pkgType, name, version, "files")
return ListIter[types.PackageFile](ctx, s.client, path, nil)
}

22
path_escape.go Normal file
View file

@ -0,0 +1,22 @@
package forge
import (
"net/url"
"strings"
)
// escapePathSegments joins path segments while escaping each segment.
func escapePathSegments(base string, segments ...string) string {
if len(segments) == 0 {
return base
}
var b strings.Builder
b.Grow(len(base) + len(segments)*4)
b.WriteString(base)
for _, segment := range segments {
b.WriteByte('/')
b.WriteString(url.PathEscape(segment))
}
return b.String()
}

257
path_escape_test.go Normal file
View file

@ -0,0 +1,257 @@
package forge
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
func TestServicePathEscaping_Good(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
method string
wantPath string
status int
headers map[string]string
body string
call func(context.Context, *Forge) error
}{
{
name: "contents raw file",
method: http.MethodGet,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/raw/docs%2Fread%20me.md",
body: "raw data",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Contents.GetRawFile(ctx, "core/team", "go forge", "docs/read me.md")
return err
},
},
{
name: "wiki delete page",
method: http.MethodDelete,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/wiki/page/Runbook%2FIntro",
call: func(ctx context.Context, f *Forge) error {
return f.Wiki.DeletePage(ctx, "core/team", "go forge", "Runbook/Intro")
},
},
{
name: "actions delete repo secret",
method: http.MethodDelete,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/actions/secrets/DEPLOY%2FKEY",
call: func(ctx context.Context, f *Forge) error {
return f.Actions.DeleteRepoSecret(ctx, "core/team", "go forge", "DEPLOY/KEY")
},
},
{
name: "actions list org secrets",
method: http.MethodGet,
wantPath: "/api/v1/orgs/ops%2Fsec/actions/secrets",
headers: map[string]string{"X-Total-Count": "0"},
body: "[]",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Actions.ListOrgSecrets(ctx, "ops/sec")
return err
},
},
{
name: "actions dispatch workflow",
method: http.MethodPost,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/actions/workflows/build%2Frelease.yml/dispatches",
call: func(ctx context.Context, f *Forge) error {
return f.Actions.DispatchWorkflow(ctx, "core/team", "go forge", "build/release.yml", map[string]any{"ref": "main"})
},
},
{
name: "branches delete protection",
method: http.MethodDelete,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/branch_protections/main%2Frelease",
call: func(ctx context.Context, f *Forge) error {
return f.Branches.DeleteBranchProtection(ctx, "core/team", "go forge", "main/release")
},
},
{
name: "commits list statuses",
method: http.MethodGet,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/commits/feature%2Fone/statuses",
body: "[]",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Commits.ListStatuses(ctx, "core/team", "go forge", "feature/one")
return err
},
},
{
name: "issues remove label",
method: http.MethodDelete,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/issues/42/labels/7",
call: func(ctx context.Context, f *Forge) error {
return f.Issues.RemoveLabel(ctx, "core/team", "go forge", 42, 7)
},
},
{
name: "labels list org labels",
method: http.MethodGet,
wantPath: "/api/v1/orgs/ops%2Fsec/labels",
headers: map[string]string{"X-Total-Count": "0"},
body: "[]",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Labels.ListOrgLabels(ctx, "ops/sec")
return err
},
},
{
name: "misc get license",
method: http.MethodGet,
wantPath: "/api/v1/licenses/Apache%202.0%2Fcustom",
body: "{}",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Misc.GetLicense(ctx, "Apache 2.0/custom")
return err
},
},
{
name: "misc get gitignore template",
method: http.MethodGet,
wantPath: "/api/v1/gitignore/templates/Go%2FTooling",
body: "{}",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Misc.GetGitignoreTemplate(ctx, "Go/Tooling")
return err
},
},
{
name: "notifications list repo",
method: http.MethodGet,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/notifications",
headers: map[string]string{"X-Total-Count": "0"},
body: "[]",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Notifications.ListRepo(ctx, "core/team", "go forge")
return err
},
},
{
name: "orgs add member",
method: http.MethodPut,
wantPath: "/api/v1/orgs/ops%2Fsec/members/alice%2Fbob",
call: func(ctx context.Context, f *Forge) error {
return f.Orgs.AddMember(ctx, "ops/sec", "alice/bob")
},
},
{
name: "orgs list user orgs",
method: http.MethodGet,
wantPath: "/api/v1/users/alice%2Fbob/orgs",
headers: map[string]string{"X-Total-Count": "0"},
body: "[]",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Orgs.ListUserOrgs(ctx, "alice/bob")
return err
},
},
{
name: "packages get",
method: http.MethodGet,
wantPath: "/api/v1/packages/ops%2Fsec/container%2Fv2/app%2Fservice/v1.0%2Fbeta",
body: "{}",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Packages.Get(ctx, "ops/sec", "container/v2", "app/service", "v1.0/beta")
return err
},
},
{
name: "pulls undismiss review",
method: http.MethodPost,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/pulls/42/reviews/9/undismissals",
call: func(ctx context.Context, f *Forge) error {
return f.Pulls.UndismissReview(ctx, "core/team", "go forge", 42, 9)
},
},
{
name: "releases delete by tag",
method: http.MethodDelete,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/releases/tags/v1.0%2Frelease",
call: func(ctx context.Context, f *Forge) error {
return f.Releases.DeleteByTag(ctx, "core/team", "go forge", "v1.0/release")
},
},
{
name: "repos list org repos",
method: http.MethodGet,
wantPath: "/api/v1/orgs/ops%2Fsec/repos",
headers: map[string]string{"X-Total-Count": "0"},
body: "[]",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Repos.ListOrgRepos(ctx, "ops/sec")
return err
},
},
{
name: "repos mirror sync",
method: http.MethodPost,
wantPath: "/api/v1/repos/core%2Fteam/go%20forge/mirror-sync",
call: func(ctx context.Context, f *Forge) error {
return f.Repos.MirrorSync(ctx, "core/team", "go forge")
},
},
{
name: "users star repo",
method: http.MethodPut,
wantPath: "/api/v1/user/starred/core%2Fteam/go%20forge",
call: func(ctx context.Context, f *Forge) error {
return f.Users.Star(ctx, "core/team", "go forge")
},
},
{
name: "teams add repo",
method: http.MethodPut,
wantPath: "/api/v1/teams/42/repos/ops%2Fsec/go%20forge",
call: func(ctx context.Context, f *Forge) error {
return f.Teams.AddRepo(ctx, 42, "ops/sec", "go forge")
},
},
{
name: "webhooks list org hooks",
method: http.MethodGet,
wantPath: "/api/v1/orgs/ops%2Fsec/hooks",
headers: map[string]string{"X-Total-Count": "0"},
body: "[]",
call: func(ctx context.Context, f *Forge) error {
_, err := f.Webhooks.ListOrgHooks(ctx, "ops/sec")
return err
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != tc.method {
t.Errorf("expected %s, got %s", tc.method, r.Method)
}
if r.URL.EscapedPath() != tc.wantPath {
t.Errorf("wrong path: %s", r.URL.EscapedPath())
}
for k, v := range tc.headers {
w.Header().Set(k, v)
}
if tc.status != 0 {
w.WriteHeader(tc.status)
}
if tc.body != "" {
if _, err := w.Write([]byte(tc.body)); err != nil {
t.Fatalf("write response: %v", err)
}
}
}))
defer srv.Close()
f := NewForge(srv.URL, "tok")
if err := tc.call(ctx, f); err != nil {
t.Fatal(err)
}
})
}
}

View file

@ -2,8 +2,8 @@ package forge
import (
"context"
"fmt"
"iter"
"strconv"
"dappco.re/go/core/forge/types"
)
@ -23,32 +23,32 @@ func newPullService(c *Client) *PullService {
// 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 {
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "pulls", strconv.FormatInt(index, 10), "merge")
body := map[string]string{"Do": method}
return s.client.Post(ctx, path, body, nil)
}
// Update updates a pull request branch with the base branch.
func (s *PullService) Update(ctx context.Context, owner, repo string, index int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/update", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "pulls", strconv.FormatInt(index, 10), "update")
return s.client.Post(ctx, path, nil, nil)
}
// ListReviews returns all reviews on a pull request.
func (s *PullService) ListReviews(ctx context.Context, owner, repo string, index int64) ([]types.PullReview, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "pulls", strconv.FormatInt(index, 10), "reviews")
return ListAll[types.PullReview](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "pulls", strconv.FormatInt(index, 10), "reviews")
return ListIter[types.PullReview](ctx, s.client, path, nil)
}
// SubmitReview creates a new review on a pull request.
func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, index int64, review map[string]any) (*types.PullReview, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", owner, repo, index)
path := escapePathSegments("/api/v1/repos", owner, repo, "pulls", strconv.FormatInt(index, 10), "reviews")
var out types.PullReview
if err := s.client.Post(ctx, path, review, &out); err != nil {
return nil, err
@ -58,13 +58,31 @@ func (s *PullService) SubmitReview(ctx context.Context, owner, repo string, inde
// DismissReview dismisses a pull request review.
func (s *PullService) DismissReview(ctx context.Context, owner, repo string, index, reviewID int64, msg string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/dismissals", owner, repo, index, reviewID)
path := escapePathSegments(
"/api/v1/repos",
owner,
repo,
"pulls",
strconv.FormatInt(index, 10),
"reviews",
strconv.FormatInt(reviewID, 10),
"dismissals",
)
body := map[string]string{"message": msg}
return s.client.Post(ctx, path, body, nil)
}
// UndismissReview undismisses a pull request review.
func (s *PullService) UndismissReview(ctx context.Context, owner, repo string, index, reviewID int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews/%d/undismissals", owner, repo, index, reviewID)
path := escapePathSegments(
"/api/v1/repos",
owner,
repo,
"pulls",
strconv.FormatInt(index, 10),
"reviews",
strconv.FormatInt(reviewID, 10),
"undismissals",
)
return s.client.Post(ctx, path, nil, nil)
}

View file

@ -2,8 +2,8 @@ package forge
import (
"context"
"fmt"
"iter"
"strconv"
"dappco.re/go/core/forge/types"
)
@ -23,7 +23,7 @@ func newReleaseService(c *Client) *ReleaseService {
// GetByTag returns a release by its tag name.
func (s *ReleaseService) GetByTag(ctx context.Context, owner, repo, tag string) (*types.Release, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner, repo, tag)
path := escapePathSegments("/api/v1/repos", owner, repo, "releases", "tags", tag)
var out types.Release
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -33,25 +33,33 @@ func (s *ReleaseService) GetByTag(ctx context.Context, owner, repo, tag string)
// DeleteByTag deletes a release by its tag name.
func (s *ReleaseService) DeleteByTag(ctx context.Context, owner, repo, tag string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner, repo, tag)
path := escapePathSegments("/api/v1/repos", owner, repo, "releases", "tags", tag)
return s.client.Delete(ctx, path)
}
// ListAssets returns all assets for a release.
func (s *ReleaseService) ListAssets(ctx context.Context, owner, repo string, releaseID int64) ([]types.Attachment, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner, repo, releaseID)
path := escapePathSegments("/api/v1/repos", owner, repo, "releases", strconv.FormatInt(releaseID, 10), "assets")
return ListAll[types.Attachment](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner, repo, releaseID)
path := escapePathSegments("/api/v1/repos", owner, repo, "releases", strconv.FormatInt(releaseID, 10), "assets")
return ListIter[types.Attachment](ctx, s.client, path, nil)
}
// GetAsset returns a single asset for a release.
func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) (*types.Attachment, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", owner, repo, releaseID, assetID)
path := escapePathSegments(
"/api/v1/repos",
owner,
repo,
"releases",
strconv.FormatInt(releaseID, 10),
"assets",
strconv.FormatInt(assetID, 10),
)
var out types.Attachment
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -61,6 +69,14 @@ func (s *ReleaseService) GetAsset(ctx context.Context, owner, repo string, relea
// DeleteAsset deletes a single asset from a release.
func (s *ReleaseService) DeleteAsset(ctx context.Context, owner, repo string, releaseID, assetID int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", owner, repo, releaseID, assetID)
path := escapePathSegments(
"/api/v1/repos",
owner,
repo,
"releases",
strconv.FormatInt(releaseID, 10),
"assets",
strconv.FormatInt(assetID, 10),
)
return s.client.Delete(ctx, path)
}

View file

@ -22,12 +22,12 @@ func newRepoService(c *Client) *RepoService {
// ListOrgRepos returns all repositories for an organisation.
func (s *RepoService) ListOrgRepos(ctx context.Context, org string) ([]types.Repository, error) {
return ListAll[types.Repository](ctx, s.client, "/api/v1/orgs/"+org+"/repos", nil)
return ListAll[types.Repository](ctx, s.client, escapePathSegments("/api/v1/orgs", org, "repos"), nil)
}
// IterOrgRepos returns an iterator over all repositories for an organisation.
func (s *RepoService) IterOrgRepos(ctx context.Context, org string) iter.Seq2[types.Repository, error] {
return ListIter[types.Repository](ctx, s.client, "/api/v1/orgs/"+org+"/repos", nil)
return ListIter[types.Repository](ctx, s.client, escapePathSegments("/api/v1/orgs", org, "repos"), nil)
}
// ListUserRepos returns all repositories for the authenticated user.
@ -47,7 +47,7 @@ func (s *RepoService) Fork(ctx context.Context, owner, repo, org string) (*types
body["organization"] = org
}
var out types.Repository
err := s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/forks", body, &out)
err := s.client.Post(ctx, escapePathSegments("/api/v1/repos", owner, repo, "forks"), body, &out)
if err != nil {
return nil, err
}
@ -56,20 +56,20 @@ func (s *RepoService) Fork(ctx context.Context, owner, repo, org string) (*types
// Transfer initiates a repository transfer.
func (s *RepoService) Transfer(ctx context.Context, owner, repo string, opts map[string]any) error {
return s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/transfer", opts, nil)
return s.client.Post(ctx, escapePathSegments("/api/v1/repos", owner, repo, "transfer"), opts, nil)
}
// AcceptTransfer accepts a pending repository transfer.
func (s *RepoService) AcceptTransfer(ctx context.Context, owner, repo string) error {
return s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/transfer/accept", nil, nil)
return s.client.Post(ctx, escapePathSegments("/api/v1/repos", owner, repo, "transfer", "accept"), nil, nil)
}
// RejectTransfer rejects a pending repository transfer.
func (s *RepoService) RejectTransfer(ctx context.Context, owner, repo string) error {
return s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/transfer/reject", nil, nil)
return s.client.Post(ctx, escapePathSegments("/api/v1/repos", owner, repo, "transfer", "reject"), nil, nil)
}
// MirrorSync triggers a mirror sync.
func (s *RepoService) MirrorSync(ctx context.Context, owner, repo string) error {
return s.client.Post(ctx, "/api/v1/repos/"+owner+"/"+repo+"/mirror-sync", nil, nil)
return s.client.Post(ctx, escapePathSegments("/api/v1/repos", owner, repo, "mirror-sync"), nil, nil)
}

View file

@ -2,8 +2,8 @@ package forge
import (
"context"
"fmt"
"iter"
"strconv"
"dappco.re/go/core/forge/types"
)
@ -23,60 +23,60 @@ func newTeamService(c *Client) *TeamService {
// ListMembers returns all members of a team.
func (s *TeamService) ListMembers(ctx context.Context, teamID int64) ([]types.User, error) {
path := fmt.Sprintf("/api/v1/teams/%d/members", teamID)
path := escapePathSegments("/api/v1/teams", strconv.FormatInt(teamID, 10), "members")
return ListAll[types.User](ctx, s.client, path, nil)
}
// IterMembers returns an iterator over all members of a team.
func (s *TeamService) IterMembers(ctx context.Context, teamID int64) iter.Seq2[types.User, error] {
path := fmt.Sprintf("/api/v1/teams/%d/members", teamID)
path := escapePathSegments("/api/v1/teams", strconv.FormatInt(teamID, 10), "members")
return ListIter[types.User](ctx, s.client, path, nil)
}
// AddMember adds a user to a team.
func (s *TeamService) AddMember(ctx context.Context, teamID int64, username string) error {
path := fmt.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)
path := escapePathSegments("/api/v1/teams", strconv.FormatInt(teamID, 10), "members", username)
return s.client.Put(ctx, path, nil, nil)
}
// RemoveMember removes a user from a team.
func (s *TeamService) RemoveMember(ctx context.Context, teamID int64, username string) error {
path := fmt.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)
path := escapePathSegments("/api/v1/teams", strconv.FormatInt(teamID, 10), "members", username)
return s.client.Delete(ctx, path)
}
// ListRepos returns all repositories managed by a team.
func (s *TeamService) ListRepos(ctx context.Context, teamID int64) ([]types.Repository, error) {
path := fmt.Sprintf("/api/v1/teams/%d/repos", teamID)
path := escapePathSegments("/api/v1/teams", strconv.FormatInt(teamID, 10), "repos")
return ListAll[types.Repository](ctx, s.client, path, nil)
}
// 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] {
path := fmt.Sprintf("/api/v1/teams/%d/repos", teamID)
path := escapePathSegments("/api/v1/teams", strconv.FormatInt(teamID, 10), "repos")
return ListIter[types.Repository](ctx, s.client, path, nil)
}
// AddRepo adds a repository to a team.
func (s *TeamService) AddRepo(ctx context.Context, teamID int64, org, repo string) error {
path := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, org, repo)
path := escapePathSegments("/api/v1/teams", strconv.FormatInt(teamID, 10), "repos", org, repo)
return s.client.Put(ctx, path, nil, nil)
}
// RemoveRepo removes a repository from a team.
func (s *TeamService) RemoveRepo(ctx context.Context, teamID int64, org, repo string) error {
path := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, org, repo)
path := escapePathSegments("/api/v1/teams", strconv.FormatInt(teamID, 10), "repos", org, repo)
return s.client.Delete(ctx, path)
}
// ListOrgTeams returns all teams in an organisation.
func (s *TeamService) ListOrgTeams(ctx context.Context, org string) ([]types.Team, error) {
path := fmt.Sprintf("/api/v1/orgs/%s/teams", org)
path := escapePathSegments("/api/v1/orgs", org, "teams")
return ListAll[types.Team](ctx, s.client, path, nil)
}
// IterOrgTeams returns an iterator over all teams in an organisation.
func (s *TeamService) IterOrgTeams(ctx context.Context, org string) iter.Seq2[types.Team, error] {
path := fmt.Sprintf("/api/v1/orgs/%s/teams", org)
path := escapePathSegments("/api/v1/orgs", org, "teams")
return ListIter[types.Team](ctx, s.client, path, nil)
}

View file

@ -2,7 +2,6 @@ package forge
import (
"context"
"fmt"
"iter"
"dappco.re/go/core/forge/types"
@ -32,60 +31,60 @@ func (s *UserService) GetCurrent(ctx context.Context) (*types.User, error) {
// ListFollowers returns all followers of a user.
func (s *UserService) ListFollowers(ctx context.Context, username string) ([]types.User, error) {
path := fmt.Sprintf("/api/v1/users/%s/followers", username)
path := escapePathSegments("/api/v1/users", username, "followers")
return ListAll[types.User](ctx, s.client, path, nil)
}
// IterFollowers returns an iterator over all followers of a user.
func (s *UserService) IterFollowers(ctx context.Context, username string) iter.Seq2[types.User, error] {
path := fmt.Sprintf("/api/v1/users/%s/followers", username)
path := escapePathSegments("/api/v1/users", username, "followers")
return ListIter[types.User](ctx, s.client, path, nil)
}
// ListFollowing returns all users that a user is following.
func (s *UserService) ListFollowing(ctx context.Context, username string) ([]types.User, error) {
path := fmt.Sprintf("/api/v1/users/%s/following", username)
path := escapePathSegments("/api/v1/users", username, "following")
return ListAll[types.User](ctx, s.client, path, nil)
}
// IterFollowing returns an iterator over all users that a user is following.
func (s *UserService) IterFollowing(ctx context.Context, username string) iter.Seq2[types.User, error] {
path := fmt.Sprintf("/api/v1/users/%s/following", username)
path := escapePathSegments("/api/v1/users", username, "following")
return ListIter[types.User](ctx, s.client, path, nil)
}
// Follow follows a user as the authenticated user.
func (s *UserService) Follow(ctx context.Context, username string) error {
path := fmt.Sprintf("/api/v1/user/following/%s", username)
path := escapePathSegments("/api/v1/user/following", username)
return s.client.Put(ctx, path, nil, nil)
}
// Unfollow unfollows a user as the authenticated user.
func (s *UserService) Unfollow(ctx context.Context, username string) error {
path := fmt.Sprintf("/api/v1/user/following/%s", username)
path := escapePathSegments("/api/v1/user/following", username)
return s.client.Delete(ctx, path)
}
// ListStarred returns all repositories starred by a user.
func (s *UserService) ListStarred(ctx context.Context, username string) ([]types.Repository, error) {
path := fmt.Sprintf("/api/v1/users/%s/starred", username)
path := escapePathSegments("/api/v1/users", username, "starred")
return ListAll[types.Repository](ctx, s.client, path, nil)
}
// IterStarred returns an iterator over all repositories starred by a user.
func (s *UserService) IterStarred(ctx context.Context, username string) iter.Seq2[types.Repository, error] {
path := fmt.Sprintf("/api/v1/users/%s/starred", username)
path := escapePathSegments("/api/v1/users", username, "starred")
return ListIter[types.Repository](ctx, s.client, path, nil)
}
// Star stars a repository as the authenticated user.
func (s *UserService) Star(ctx context.Context, owner, repo string) error {
path := fmt.Sprintf("/api/v1/user/starred/%s/%s", owner, repo)
path := escapePathSegments("/api/v1/user/starred", owner, repo)
return s.client.Put(ctx, path, nil, nil)
}
// Unstar unstars a repository as the authenticated user.
func (s *UserService) Unstar(ctx context.Context, owner, repo string) error {
path := fmt.Sprintf("/api/v1/user/starred/%s/%s", owner, repo)
path := escapePathSegments("/api/v1/user/starred", owner, repo)
return s.client.Delete(ctx, path)
}

View file

@ -2,8 +2,8 @@ package forge
import (
"context"
"fmt"
"iter"
"strconv"
"dappco.re/go/core/forge/types"
)
@ -24,18 +24,18 @@ func newWebhookService(c *Client) *WebhookService {
// TestHook triggers a test delivery for a webhook.
func (s *WebhookService) TestHook(ctx context.Context, owner, repo string, id int64) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d/tests", owner, repo, id)
path := escapePathSegments("/api/v1/repos", owner, repo, "hooks", strconv.FormatInt(id, 10), "tests")
return s.client.Post(ctx, path, nil, nil)
}
// ListOrgHooks returns all webhooks for an organisation.
func (s *WebhookService) ListOrgHooks(ctx context.Context, org string) ([]types.Hook, error) {
path := fmt.Sprintf("/api/v1/orgs/%s/hooks", org)
path := escapePathSegments("/api/v1/orgs", org, "hooks")
return ListAll[types.Hook](ctx, s.client, path, nil)
}
// IterOrgHooks returns an iterator over all webhooks for an organisation.
func (s *WebhookService) IterOrgHooks(ctx context.Context, org string) iter.Seq2[types.Hook, error] {
path := fmt.Sprintf("/api/v1/orgs/%s/hooks", org)
path := escapePathSegments("/api/v1/orgs", org, "hooks")
return ListIter[types.Hook](ctx, s.client, path, nil)
}

11
wiki.go
View file

@ -2,7 +2,6 @@ package forge
import (
"context"
"fmt"
"dappco.re/go/core/forge/types"
)
@ -19,7 +18,7 @@ func newWikiService(c *Client) *WikiService {
// ListPages returns all wiki page metadata for a repository.
func (s *WikiService) ListPages(ctx context.Context, owner, repo string) ([]types.WikiPageMetaData, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/pages", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "wiki", "pages")
var out []types.WikiPageMetaData
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -29,7 +28,7 @@ func (s *WikiService) ListPages(ctx context.Context, owner, repo string) ([]type
// GetPage returns a single wiki page by name.
func (s *WikiService) GetPage(ctx context.Context, owner, repo, pageName string) (*types.WikiPage, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
path := escapePathSegments("/api/v1/repos", owner, repo, "wiki", "page", pageName)
var out types.WikiPage
if err := s.client.Get(ctx, path, &out); err != nil {
return nil, err
@ -39,7 +38,7 @@ func (s *WikiService) GetPage(ctx context.Context, owner, repo, pageName string)
// CreatePage creates a new wiki page.
func (s *WikiService) CreatePage(ctx context.Context, owner, repo string, opts *types.CreateWikiPageOptions) (*types.WikiPage, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", owner, repo)
path := escapePathSegments("/api/v1/repos", owner, repo, "wiki", "new")
var out types.WikiPage
if err := s.client.Post(ctx, path, opts, &out); err != nil {
return nil, err
@ -49,7 +48,7 @@ func (s *WikiService) CreatePage(ctx context.Context, owner, repo string, opts *
// EditPage updates an existing wiki page.
func (s *WikiService) EditPage(ctx context.Context, owner, repo, pageName string, opts *types.CreateWikiPageOptions) (*types.WikiPage, error) {
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
path := escapePathSegments("/api/v1/repos", owner, repo, "wiki", "page", pageName)
var out types.WikiPage
if err := s.client.Patch(ctx, path, opts, &out); err != nil {
return nil, err
@ -59,6 +58,6 @@ func (s *WikiService) EditPage(ctx context.Context, owner, repo, pageName string
// DeletePage removes a wiki page.
func (s *WikiService) DeletePage(ctx context.Context, owner, repo, pageName string) error {
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s", owner, repo, pageName)
path := escapePathSegments("/api/v1/repos", owner, repo, "wiki", "page", pageName)
return s.client.Delete(ctx, path)
}