1239fdee70
The transport was hand-rolled net/http against the REST API. The payload shapes were ours, in internal/wire, which meant every field Gitea learned was a field somebody here had to notice; and "does this instance have issue dependencies?" had to be guessed from a status code, because a 404 from a missing route and a 404 from a missing issue look alike. The SDK settles both. The shapes are maintained by the people who maintain the server, and the client negotiates the server's version when it is built, so the dependency endpoint is now gated on `>= 1.20.0` — verified against the release where the route appears, not assumed. Below the gate nothing is requested at all. internal/wire keeps what the SDK has no answer for: addressing. The SDK takes an owner, a name and an int64 and never parses, while `42`, `#42`, `owner/repo#42` and an issue URL are four spellings of one address, all four are what somebody has in hand, and Key is what the ledger is keyed by. The payload structs go. Four things that had to survive the move, and did: - request bodies still land in .kettle/payload/, now via an http.RoundTripper on the client the SDK is given — which is better than before, because it files every request rather than the ones a call site remembered to name; - errors still carry the HTTP status AND the response body, and a decode failure on a 2xx is deliberately not an APIError, so the dependency probe cannot read a bad decode as "feature missing"; - the number -> slug ledger is untouched, entries still outlive the files they name; - Client.For(repo) still re-points at another repository for one call. What it cost, written down in AGENTS.md where it happened. internal/mapping's layering test was a fact about the import graph — nothing in its closure could open a socket — and the SDK ships its types and its client in one package, so the test now asserts what is still true: the bridge performs no I/O, checked on direct imports plus a grep for time.Now. A run makes one extra request before it does anything. Gitea's issue edit endpoint carries no labels, so a push whose labels changed needs a second call; push makes it and says so. go.mod requires go 1.26, which the SDK sets and which is now the floor for building this binary. internal/issue and internal/project are byte-identical. The domain did not notice, which is the whole argument for the layering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
238 lines
8.4 KiB
Go
238 lines
8.4 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package gitea
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Hook a hook is a web hook when one repository changed
|
|
type Hook struct {
|
|
ID int64 `json:"id"`
|
|
Type string `json:"type"`
|
|
URL string `json:"-"`
|
|
BranchFilter string `json:"branch_filter"`
|
|
Config map[string]string `json:"config"`
|
|
Events []string `json:"events"`
|
|
AuthorizationHeader string `json:"authorization_header"`
|
|
Active bool `json:"active"`
|
|
Updated time.Time `json:"updated_at"`
|
|
Created time.Time `json:"created_at"`
|
|
}
|
|
|
|
// HookType represent all webhook types gitea currently offer
|
|
type HookType string
|
|
|
|
const (
|
|
// HookTypeDingtalk webhook that dingtalk understand
|
|
HookTypeDingtalk HookType = "dingtalk"
|
|
// HookTypeDiscord webhook that discord understand
|
|
HookTypeDiscord HookType = "discord"
|
|
// HookTypeGitea webhook that gitea understand
|
|
HookTypeGitea HookType = "gitea"
|
|
// HookTypeGogs webhook that gogs understand
|
|
HookTypeGogs HookType = "gogs"
|
|
// HookTypeMsteams webhook that msteams understand
|
|
HookTypeMsteams HookType = "msteams"
|
|
// HookTypeSlack webhook that slack understand
|
|
HookTypeSlack HookType = "slack"
|
|
// HookTypeTelegram webhook that telegram understand
|
|
HookTypeTelegram HookType = "telegram"
|
|
// HookTypeFeishu webhook that feishu understand
|
|
HookTypeFeishu HookType = "feishu"
|
|
)
|
|
|
|
// ListHooksOptions options for listing hooks
|
|
type ListHooksOptions struct {
|
|
ListOptions
|
|
}
|
|
|
|
// ListOrgHooks list all the hooks of one organization
|
|
func (c *Client) ListOrgHooks(org string, opt ListHooksOptions) ([]*Hook, *Response, error) {
|
|
if err := escapeValidatePathSegments(&org); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
opt.setDefaults()
|
|
hooks := make([]*Hook, 0, opt.PageSize)
|
|
resp, err := c.getParsedResponse("GET", fmt.Sprintf("/orgs/%s/hooks?%s", org, opt.getURLQuery().Encode()), nil, nil, &hooks)
|
|
return hooks, resp, err
|
|
}
|
|
|
|
// ListMyHooks list all the hooks of the authenticated user
|
|
func (c *Client) ListMyHooks(opt ListHooksOptions) ([]*Hook, *Response, error) {
|
|
opt.setDefaults()
|
|
hooks := make([]*Hook, 0, opt.PageSize)
|
|
resp, err := c.getParsedResponse("GET", fmt.Sprintf("/user/hooks?%s", opt.getURLQuery().Encode()), nil, nil, &hooks)
|
|
return hooks, resp, err
|
|
}
|
|
|
|
// ListRepoHooks list all the hooks of one repository
|
|
func (c *Client) ListRepoHooks(user, repo string, opt ListHooksOptions) ([]*Hook, *Response, error) {
|
|
if err := escapeValidatePathSegments(&user, &repo); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
opt.setDefaults()
|
|
hooks := make([]*Hook, 0, opt.PageSize)
|
|
resp, err := c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/hooks?%s", user, repo, opt.getURLQuery().Encode()), nil, nil, &hooks)
|
|
return hooks, resp, err
|
|
}
|
|
|
|
// GetOrgHook get a hook of an organization
|
|
func (c *Client) GetOrgHook(org string, id int64) (*Hook, *Response, error) {
|
|
if err := escapeValidatePathSegments(&org); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
h := new(Hook)
|
|
resp, err := c.getParsedResponse("GET", fmt.Sprintf("/orgs/%s/hooks/%d", org, id), nil, nil, h)
|
|
return h, resp, err
|
|
}
|
|
|
|
// GetMyHook get a hook of the authenticated user
|
|
func (c *Client) GetMyHook(id int64) (*Hook, *Response, error) {
|
|
h := new(Hook)
|
|
resp, err := c.getParsedResponse("GET", fmt.Sprintf("/user/hooks/%d", id), nil, nil, h)
|
|
return h, resp, err
|
|
}
|
|
|
|
// GetRepoHook get a hook of a repository
|
|
func (c *Client) GetRepoHook(user, repo string, id int64) (*Hook, *Response, error) {
|
|
if err := escapeValidatePathSegments(&user, &repo); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
h := new(Hook)
|
|
resp, err := c.getParsedResponse("GET", fmt.Sprintf("/repos/%s/%s/hooks/%d", user, repo, id), nil, nil, h)
|
|
return h, resp, err
|
|
}
|
|
|
|
// CreateHookOption options when create a hook
|
|
type CreateHookOption struct {
|
|
Type HookType `json:"type"`
|
|
Config map[string]string `json:"config"`
|
|
Events []string `json:"events"`
|
|
BranchFilter string `json:"branch_filter"`
|
|
Active bool `json:"active"`
|
|
AuthorizationHeader string `json:"authorization_header"`
|
|
}
|
|
|
|
// Validate the CreateHookOption struct
|
|
func (opt CreateHookOption) Validate() error {
|
|
if len(opt.Type) == 0 {
|
|
return fmt.Errorf("hook type needed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateOrgHook create one hook for an organization, with options
|
|
func (c *Client) CreateOrgHook(org string, opt CreateHookOption) (*Hook, *Response, error) {
|
|
if err := escapeValidatePathSegments(&org); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := opt.Validate(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
body, err := json.Marshal(&opt)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
h := new(Hook)
|
|
resp, err := c.getParsedResponse("POST", fmt.Sprintf("/orgs/%s/hooks", org), jsonHeader, bytes.NewReader(body), h)
|
|
return h, resp, err
|
|
}
|
|
|
|
// CreateMyHook create one hook for the authenticated user, with options
|
|
func (c *Client) CreateMyHook(opt CreateHookOption) (*Hook, *Response, error) {
|
|
if err := opt.Validate(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
body, err := json.Marshal(&opt)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
h := new(Hook)
|
|
resp, err := c.getParsedResponse("POST", "/user/hooks", jsonHeader, bytes.NewReader(body), h)
|
|
return h, resp, err
|
|
}
|
|
|
|
// CreateRepoHook create one hook for a repository, with options
|
|
func (c *Client) CreateRepoHook(user, repo string, opt CreateHookOption) (*Hook, *Response, error) {
|
|
if err := escapeValidatePathSegments(&user, &repo); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
body, err := json.Marshal(&opt)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
h := new(Hook)
|
|
resp, err := c.getParsedResponse("POST", fmt.Sprintf("/repos/%s/%s/hooks", user, repo), jsonHeader, bytes.NewReader(body), h)
|
|
return h, resp, err
|
|
}
|
|
|
|
// EditHookOption options when modify one hook
|
|
type EditHookOption struct {
|
|
Config map[string]string `json:"config"`
|
|
Events []string `json:"events"`
|
|
BranchFilter string `json:"branch_filter"`
|
|
Active *bool `json:"active"`
|
|
AuthorizationHeader string `json:"authorization_header"`
|
|
}
|
|
|
|
// EditOrgHook modify one hook of an organization, with hook id and options
|
|
func (c *Client) EditOrgHook(org string, id int64, opt EditHookOption) (*Response, error) {
|
|
if err := escapeValidatePathSegments(&org); err != nil {
|
|
return nil, err
|
|
}
|
|
body, err := json.Marshal(&opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/orgs/%s/hooks/%d", org, id), jsonHeader, bytes.NewReader(body))
|
|
}
|
|
|
|
// EditMyHook modify one hook of the authenticated user, with hook id and options
|
|
func (c *Client) EditMyHook(id int64, opt EditHookOption) (*Response, error) {
|
|
body, err := json.Marshal(&opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/user/hooks/%d", id), jsonHeader, bytes.NewReader(body))
|
|
}
|
|
|
|
// EditRepoHook modify one hook of a repository, with hook id and options
|
|
func (c *Client) EditRepoHook(user, repo string, id int64, opt EditHookOption) (*Response, error) {
|
|
if err := escapeValidatePathSegments(&user, &repo); err != nil {
|
|
return nil, err
|
|
}
|
|
body, err := json.Marshal(&opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.doRequestWithStatusHandle("PATCH", fmt.Sprintf("/repos/%s/%s/hooks/%d", user, repo, id), jsonHeader, bytes.NewReader(body))
|
|
}
|
|
|
|
// DeleteOrgHook delete one hook from an organization, with hook id
|
|
func (c *Client) DeleteOrgHook(org string, id int64) (*Response, error) {
|
|
if err := escapeValidatePathSegments(&org); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/orgs/%s/hooks/%d", org, id), nil, nil)
|
|
}
|
|
|
|
// DeleteMyHook delete one hook from the authenticated user, with hook id
|
|
func (c *Client) DeleteMyHook(id int64) (*Response, error) {
|
|
return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/user/hooks/%d", id), nil, nil)
|
|
}
|
|
|
|
// DeleteRepoHook delete one hook from a repository, with hook id
|
|
func (c *Client) DeleteRepoHook(user, repo string, id int64) (*Response, error) {
|
|
if err := escapeValidatePathSegments(&user, &repo); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.doRequestWithStatusHandle("DELETE", fmt.Sprintf("/repos/%s/%s/hooks/%d", user, repo, id), nil, nil)
|
|
}
|