From 419e622659d3daf76c16753c4efdf9fce329381c Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 08:15:33 +0000 Subject: [PATCH] fix: URL-escape repository path params --- repos.go | 21 ++++++++++++++------- repos_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/repos.go b/repos.go index a7abbc5..12672c7 100644 --- a/repos.go +++ b/repos.go @@ -27,12 +27,14 @@ 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) + path := ResolvePath("/api/v1/orgs/{org}/repos", pathParams("org", org)) + return ListAll[types.Repository](ctx, s.client, path, 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) + path := ResolvePath("/api/v1/orgs/{org}/repos", pathParams("org", org)) + return ListIter[types.Repository](ctx, s.client, path, nil) } // ListUserRepos returns all repositories for the authenticated user. @@ -86,7 +88,8 @@ 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) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/forks", pathParams("owner", owner, "repo", repo)) + err := s.client.Post(ctx, path, body, &out) if err != nil { return nil, err } @@ -95,20 +98,24 @@ 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) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/transfer", pathParams("owner", owner, "repo", repo)) + return s.client.Post(ctx, path, 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) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/transfer/accept", pathParams("owner", owner, "repo", repo)) + return s.client.Post(ctx, path, 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) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/transfer/reject", pathParams("owner", owner, "repo", repo)) + return s.client.Post(ctx, path, 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) + path := ResolvePath("/api/v1/repos/{owner}/{repo}/mirror-sync", pathParams("owner", owner, "repo", repo)) + return s.client.Post(ctx, path, nil, nil) } diff --git a/repos_test.go b/repos_test.go index b2d1d89..3ba3b8e 100644 --- a/repos_test.go +++ b/repos_test.go @@ -63,3 +63,45 @@ func TestRepoService_UpdateTopics_Good(t *testing.T) { t.Fatal(err) } } + +func TestRepoService_PathParamsAreEscaped_Good(t *testing.T) { + owner := "acme org" + repo := "my/repo" + org := "team alpha" + + t.Run("ListOrgRepos", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/orgs/team%20alpha/repos" { + t.Errorf("got path %q, want %q", r.URL.Path, "/api/v1/orgs/team%20alpha/repos") + http.NotFound(w, r) + return + } + json.NewEncoder(w).Encode([]types.Repository{}) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + _, err := f.Repos.ListOrgRepos(context.Background(), org) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("Fork", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + want := "/api/v1/repos/acme%20org/my%2Frepo/forks" + if r.URL.Path != want { + t.Errorf("got path %q, want %q", r.URL.Path, want) + http.NotFound(w, r) + return + } + json.NewEncoder(w).Encode(types.Repository{Name: repo}) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + if _, err := f.Repos.Fork(context.Background(), owner, repo, ""); err != nil { + t.Fatal(err) + } + }) +}