// Package gitea implements the internal Gitea REST operations used by delivery. package gitea import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" ) type FailureKind string const ( FailureInvalid FailureKind = "invalid_request" FailureNotFound FailureKind = "not_found" FailureUnauthorized FailureKind = "unauthorized" FailureConflict FailureKind = "conflict" FailureMalformed FailureKind = "malformed_response" FailureUnverifiable FailureKind = "unverifiable" FailureTransport FailureKind = "transport" FailureRejected FailureKind = "rejected" FailureUnknown FailureKind = "unknown" ) // Failure describes how a Gitea operation failed. StatusCode is zero when no // HTTP response was received. type Failure struct { Kind FailureKind Operation string StatusCode int Err error } func (f *Failure) Error() string { if f.StatusCode != 0 { return fmt.Sprintf("gitea %s: %s (HTTP %d)", f.Operation, f.Kind, f.StatusCode) } if f.Err != nil { return fmt.Sprintf("gitea %s: %s: %v", f.Operation, f.Kind, f.Err) } return fmt.Sprintf("gitea %s: %s", f.Operation, f.Kind) } func (f *Failure) Unwrap() error { return f.Err } type Destination struct { BaseURL string Owner string Repository string Token string } type release struct { ID int64 URL string TagName string Title string Body string Draft bool Prerelease bool } type Client struct { HTTPClient *http.Client } type tagResponse struct { Commit struct { SHA string `json:"sha"` } `json:"commit"` } type releaseResponse struct { ID int64 `json:"id"` HTMLURL string `json:"html_url"` TagName string `json:"tag_name"` TargetCommitish string `json:"target_commitish"` Name string `json:"name"` Body string `json:"body"` Draft bool `json:"draft"` Prerelease bool `json:"prerelease"` } type createReleaseRequest struct { TagName string `json:"tag_name"` TargetCommitish string `json:"target_commitish"` Name string `json:"name"` Body string `json:"body"` Draft bool `json:"draft"` Prerelease bool `json:"prerelease"` } func (c Client) getRelease(ctx context.Context, client *http.Client, token, endpoint string) (release, int, error) { request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return release{}, 0, failure(FailureInvalid, "find release", 0, err) } request.Header.Set("Authorization", "token "+token) request.Header.Set("Accept", "application/json") response, err := client.Do(request) if err != nil { return release{}, 0, failure(FailureTransport, "find release", 0, err) } defer response.Body.Close() if response.StatusCode == http.StatusNotFound { return release{}, response.StatusCode, failure(FailureNotFound, "find release", response.StatusCode, nil) } if response.StatusCode < 200 || response.StatusCode >= 300 { return release{}, response.StatusCode, responseFailure("find release", response.StatusCode) } var decoded releaseResponse if err := decodeJSON(response.Body, &decoded); err != nil { return release{}, response.StatusCode, failure(FailureMalformed, "decode release", response.StatusCode, err) } return decoded.release(), response.StatusCode, nil } func (c Client) getJSON(ctx context.Context, client *http.Client, token, operation, endpoint string, destination any) error { request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return failure(FailureInvalid, operation, 0, err) } request.Header.Set("Authorization", "token "+token) request.Header.Set("Accept", "application/json") response, err := client.Do(request) if err != nil { return failure(FailureTransport, operation, 0, err) } defer response.Body.Close() if response.StatusCode < 200 || response.StatusCode >= 300 { return responseFailure(operation, response.StatusCode) } if err := decodeJSON(response.Body, destination); err != nil { return failure(FailureMalformed, operation, response.StatusCode, err) } return nil } func (c Client) postRelease(ctx context.Context, client *http.Client, token, endpoint string, body []byte) (release, int, error) { request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { return release{}, 0, failure(FailureInvalid, "create release", 0, err) } request.Header.Set("Authorization", "token "+token) request.Header.Set("Accept", "application/json") request.Header.Set("Content-Type", "application/json") response, err := client.Do(request) if err != nil { return release{}, 0, failure(FailureTransport, "create release", 0, err) } defer response.Body.Close() if response.StatusCode < 200 || response.StatusCode >= 300 { return release{}, response.StatusCode, responseFailure("create release", response.StatusCode) } var decoded releaseResponse if err := decodeJSON(response.Body, &decoded); err != nil { return release{}, response.StatusCode, failure(FailureMalformed, "decode created release", response.StatusCode, err) } return decoded.release(), response.StatusCode, nil } func (c Client) httpClient() *http.Client { client := http.DefaultClient if c.HTTPClient != nil { clientCopy := *c.HTTPClient clientCopy.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } return &clientCopy } clientCopy := *client clientCopy.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } return &clientCopy } func parseBaseURL(raw string) (*url.URL, error) { parsed, err := url.Parse(raw) if err != nil { return nil, err } if (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" { return nil, errors.New("base URL must be an absolute HTTP(S) origin or path without credentials, query, or fragment") } return parsed, nil } func (response releaseResponse) release() release { return release{ID: response.ID, URL: response.HTMLURL, TagName: response.TagName, Title: response.Name, Body: response.Body, Draft: response.Draft, Prerelease: response.Prerelease} } func validObjectID(value string) bool { if len(value) != 40 && len(value) != 64 { return false } for _, r := range value { if !(r >= '0' && r <= '9') && !(r >= 'a' && r <= 'f') { return false } } return true } func responseFailure(operation string, status int) error { kind := FailureRejected switch status { case http.StatusUnauthorized, http.StatusForbidden: kind = FailureUnauthorized case http.StatusConflict: kind = FailureConflict case http.StatusNotFound: kind = FailureNotFound } return failure(kind, operation, status, nil) } func failure(kind FailureKind, operation string, status int, err error) error { return &Failure{Kind: kind, Operation: operation, StatusCode: status, Err: err} } func decodeJSON(reader io.Reader, destination any) error { decoder := json.NewDecoder(reader) if err := decoder.Decode(destination); err != nil { return err } var trailing any if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { if err == nil { return errors.New("response contains multiple JSON values") } return err } return nil }