From 4af621e58080b8eb0395bb48cef142dee352c643 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 22:32:51 +0000 Subject: [PATCH] feat(repo): add push mirror helpers Co-Authored-By: Virgil --- repos.go | 44 ++++++++++++++++ repos_test.go | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) diff --git a/repos.go b/repos.go index 105198f..0081581 100644 --- a/repos.go +++ b/repos.go @@ -198,6 +198,44 @@ func (s *RepoService) DeleteAvatar(ctx context.Context, owner, repo string) erro return s.client.Delete(ctx, path) } +// ListPushMirrors returns all push mirrors configured for a repository. +func (s *RepoService) ListPushMirrors(ctx context.Context, owner, repo string) ([]types.PushMirror, error) { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/push_mirrors", pathParams("owner", owner, "repo", repo)) + return ListAll[types.PushMirror](ctx, s.client, path, nil) +} + +// IterPushMirrors returns an iterator over all push mirrors configured for a repository. +func (s *RepoService) IterPushMirrors(ctx context.Context, owner, repo string) iter.Seq2[types.PushMirror, error] { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/push_mirrors", pathParams("owner", owner, "repo", repo)) + return ListIter[types.PushMirror](ctx, s.client, path, nil) +} + +// GetPushMirror returns a push mirror by its remote name. +func (s *RepoService) GetPushMirror(ctx context.Context, owner, repo, name string) (*types.PushMirror, error) { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/push_mirrors/{name}", pathParams("owner", owner, "repo", repo, "name", name)) + var out types.PushMirror + if err := s.client.Get(ctx, path, &out); err != nil { + return nil, err + } + return &out, nil +} + +// CreatePushMirror adds a push mirror to a repository. +func (s *RepoService) CreatePushMirror(ctx context.Context, owner, repo string, opts *types.CreatePushMirrorOption) (*types.PushMirror, error) { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/push_mirrors", pathParams("owner", owner, "repo", repo)) + var out types.PushMirror + if err := s.client.Post(ctx, path, opts, &out); err != nil { + return nil, err + } + return &out, nil +} + +// DeletePushMirror removes a push mirror from a repository by remote name. +func (s *RepoService) DeletePushMirror(ctx context.Context, owner, repo, name string) error { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/push_mirrors/{name}", pathParams("owner", owner, "repo", repo, "name", name)) + return s.client.Delete(ctx, path) +} + // GetSubscription returns the current user's watch state for a repository. func (s *RepoService) GetSubscription(ctx context.Context, owner, repo string) (*types.WatchInfo, error) { path := ResolvePath("/api/v1/repos/{owner}/{repo}/subscription", pathParams("owner", owner, "repo", repo)) @@ -262,3 +300,9 @@ func (s *RepoService) MirrorSync(ctx context.Context, owner, repo string) error path := ResolvePath("/api/v1/repos/{owner}/{repo}/mirror-sync", pathParams("owner", owner, "repo", repo)) return s.client.Post(ctx, path, nil, nil) } + +// SyncPushMirrors triggers a sync across all push mirrors configured for a repository. +func (s *RepoService) SyncPushMirrors(ctx context.Context, owner, repo string) error { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/push_mirrors-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 df59491..1eca550 100644 --- a/repos_test.go +++ b/repos_test.go @@ -180,6 +180,143 @@ func TestRepoService_DeleteAvatar_Good(t *testing.T) { } } +func TestRepoService_ListPushMirrors_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/push_mirrors" { + t.Errorf("wrong path: %s", r.URL.Path) + http.NotFound(w, r) + return + } + w.Header().Set("X-Total-Count", "2") + json.NewEncoder(w).Encode([]types.PushMirror{ + {RemoteName: "mirror-a"}, + {RemoteName: "mirror-b"}, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + mirrors, err := f.Repos.ListPushMirrors(context.Background(), "core", "go-forge") + if err != nil { + t.Fatal(err) + } + if len(mirrors) != 2 || mirrors[0].RemoteName != "mirror-a" || mirrors[1].RemoteName != "mirror-b" { + t.Fatalf("got %#v", mirrors) + } +} + +func TestRepoService_GetPushMirror_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/push_mirrors/mirror-a" { + t.Errorf("wrong path: %s", r.URL.Path) + http.NotFound(w, r) + return + } + json.NewEncoder(w).Encode(types.PushMirror{ + RemoteName: "mirror-a", + RemoteAddress: "ssh://git@example.com/core/go-forge.git", + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + mirror, err := f.Repos.GetPushMirror(context.Background(), "core", "go-forge", "mirror-a") + if err != nil { + t.Fatal(err) + } + if mirror.RemoteName != "mirror-a" { + t.Fatalf("got remote_name=%q", mirror.RemoteName) + } +} + +func TestRepoService_CreatePushMirror_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/push_mirrors" { + t.Errorf("wrong path: %s", r.URL.Path) + http.NotFound(w, r) + return + } + var body types.CreatePushMirrorOption + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode body: %v", err) + } + if body.RemoteAddress != "ssh://git@example.com/core/go-forge.git" || !body.SyncOnCommit { + t.Fatalf("got %#v", body) + } + json.NewEncoder(w).Encode(types.PushMirror{ + RemoteName: "mirror-a", + RemoteAddress: body.RemoteAddress, + SyncOnCommit: body.SyncOnCommit, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + mirror, err := f.Repos.CreatePushMirror(context.Background(), "core", "go-forge", &types.CreatePushMirrorOption{ + RemoteAddress: "ssh://git@example.com/core/go-forge.git", + RemoteUsername: "git", + RemotePassword: "secret", + Interval: "1h", + SyncOnCommit: true, + UseSSH: true, + }) + if err != nil { + t.Fatal(err) + } + if mirror.RemoteName != "mirror-a" || !mirror.SyncOnCommit { + t.Fatalf("got %#v", mirror) + } +} + +func TestRepoService_DeletePushMirror_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/push_mirrors/mirror-a" { + 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.Repos.DeletePushMirror(context.Background(), "core", "go-forge", "mirror-a"); err != nil { + t.Fatal(err) + } +} + +func TestRepoService_SyncPushMirrors_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/push_mirrors-sync" { + 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.Repos.SyncPushMirrors(context.Background(), "core", "go-forge"); err != nil { + t.Fatal(err) + } +} + func TestRepoService_GetSubscription_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet {