From 79bb4277b383aea061d133cf27ce7fc05bb2249e Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 22:27:49 +0000 Subject: [PATCH] feat(webhooks): add git hook helpers Co-Authored-By: Virgil --- docs/api-contract.md | 4 ++ webhooks.go | 32 +++++++++++++ webhooks_test.go | 110 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) diff --git a/docs/api-contract.md b/docs/api-contract.md index 7388d0b..ae87f6d 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -217,6 +217,10 @@ 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 | 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.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` | diff --git a/webhooks.go b/webhooks.go index 0479e37..a700ea3 100644 --- a/webhooks.go +++ b/webhooks.go @@ -32,6 +32,38 @@ func (s *WebhookService) TestHook(ctx context.Context, owner, repo string, id in return s.client.Post(ctx, path, nil, nil) } +// ListGitHooks returns all Git hooks for a repository. +func (s *WebhookService) ListGitHooks(ctx context.Context, owner, repo string) ([]types.GitHook, error) { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/hooks/git", pathParams("owner", owner, "repo", repo)) + return ListAll[types.GitHook](ctx, s.client, path, nil) +} + +// GetGitHook returns a single Git hook for a repository. +func (s *WebhookService) GetGitHook(ctx context.Context, owner, repo, id string) (*types.GitHook, error) { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/hooks/git/{id}", pathParams("owner", owner, "repo", repo, "id", id)) + var out types.GitHook + if err := s.client.Get(ctx, path, &out); err != nil { + return nil, err + } + return &out, nil +} + +// EditGitHook updates an existing Git hook in a repository. +func (s *WebhookService) EditGitHook(ctx context.Context, owner, repo, id string, opts *types.EditGitHookOption) (*types.GitHook, error) { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/hooks/git/{id}", pathParams("owner", owner, "repo", repo, "id", id)) + var out types.GitHook + if err := s.client.Patch(ctx, path, opts, &out); err != nil { + return nil, err + } + return &out, nil +} + +// DeleteGitHook deletes a Git hook from a repository. +func (s *WebhookService) DeleteGitHook(ctx context.Context, owner, repo, id string) error { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/hooks/git/{id}", pathParams("owner", owner, "repo", repo, "id", id)) + return s.client.Delete(ctx, path) +} + // ListUserHooks returns all webhooks for the authenticated user. func (s *WebhookService) ListUserHooks(ctx context.Context) ([]types.Hook, error) { return ListAll[types.Hook](ctx, s.client, "/api/v1/user/hooks", nil) diff --git a/webhooks_test.go b/webhooks_test.go index e36522b..84965d0 100644 --- a/webhooks_test.go +++ b/webhooks_test.go @@ -127,6 +127,116 @@ func TestWebhookService_TestHook_Good(t *testing.T) { } } +func TestWebhookService_ListGitHooks_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/hooks/git" { + t.Errorf("wrong path: %s", r.URL.Path) + } + w.Header().Set("X-Total-Count", "1") + json.NewEncoder(w).Encode([]types.GitHook{ + {Name: "pre-receive", Content: "#!/bin/sh\nexit 0", IsActive: true}, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + hooks, err := f.Webhooks.ListGitHooks(context.Background(), "core", "go-forge") + if err != nil { + t.Fatal(err) + } + if len(hooks) != 1 { + t.Fatalf("got %d hooks, want 1", len(hooks)) + } + if hooks[0].Name != "pre-receive" { + t.Errorf("got name=%q, want %q", hooks[0].Name, "pre-receive") + } +} + +func TestWebhookService_GetGitHook_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/hooks/git/pre-receive" { + t.Errorf("wrong path: %s", r.URL.Path) + } + json.NewEncoder(w).Encode(types.GitHook{ + Name: "pre-receive", + Content: "#!/bin/sh\nexit 0", + IsActive: true, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + hook, err := f.Webhooks.GetGitHook(context.Background(), "core", "go-forge", "pre-receive") + if err != nil { + t.Fatal(err) + } + if hook.Name != "pre-receive" { + t.Errorf("got name=%q, want %q", hook.Name, "pre-receive") + } + if !hook.IsActive { + t.Error("expected is_active=true") + } +} + +func TestWebhookService_EditGitHook_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/hooks/git/pre-receive" { + t.Errorf("wrong path: %s", r.URL.Path) + } + var opts types.EditGitHookOption + if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { + t.Fatal(err) + } + if opts.Content != "#!/bin/sh\nexit 0" { + t.Fatalf("unexpected edit payload: %+v", opts) + } + json.NewEncoder(w).Encode(types.GitHook{ + Name: "pre-receive", + Content: opts.Content, + IsActive: true, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + hook, err := f.Webhooks.EditGitHook(context.Background(), "core", "go-forge", "pre-receive", &types.EditGitHookOption{ + Content: "#!/bin/sh\nexit 0", + }) + if err != nil { + t.Fatal(err) + } + if hook.Content != "#!/bin/sh\nexit 0" { + t.Errorf("got content=%q, want %q", hook.Content, "#!/bin/sh\nexit 0") + } +} + +func TestWebhookService_DeleteGitHook_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/hooks/git/pre-receive" { + t.Errorf("wrong path: %s", r.URL.Path) + } + w.WriteHeader(http.StatusNoContent) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + if err := f.Webhooks.DeleteGitHook(context.Background(), "core", "go-forge", "pre-receive"); err != nil { + t.Fatal(err) + } +} + func TestWebhookService_ListUserHooks_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet {