mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
203 lines
9.1 KiB
Go
203 lines
9.1 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
gitea "gitea.dev/sdk"
|
|
"gitea.dev/tea/internal/transport"
|
|
)
|
|
|
|
// Release describes a repository release.
|
|
type Release struct {
|
|
ID int64 `json:"id"`
|
|
Tag string `json:"tag"`
|
|
Target string `json:"target"`
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
URL string `json:"url"`
|
|
Draft bool `json:"draft"`
|
|
Prerelease bool `json:"prerelease"`
|
|
Author string `json:"author"`
|
|
Assets []ReleaseAsset `json:"assets"`
|
|
CreatedAt string `json:"created_at"`
|
|
PublishedAt *string `json:"published_at"`
|
|
}
|
|
|
|
func releaseFrom(release *gitea.Release) Release {
|
|
return Release{
|
|
ID: release.ID, Tag: release.TagName, Target: release.Target, Title: release.Title, Body: release.Note, URL: release.HTMLURL,
|
|
Draft: release.IsDraft, Prerelease: release.IsPrerelease, Author: login(release.Publisher), Assets: convertAll(release.Attachments, assetFrom),
|
|
CreatedAt: timestamp(release.CreatedAt), PublishedAt: optionalTimestamp(&release.PublishedAt),
|
|
}
|
|
}
|
|
|
|
// ReleaseOptions distinguishes omitted fields from explicit empty or false values.
|
|
type ReleaseOptions struct {
|
|
Tag *string `json:"tag_name,omitempty"`
|
|
Target *string `json:"target_commitish,omitempty"`
|
|
Title *string `json:"name,omitempty"`
|
|
Body *string `json:"body,omitempty"`
|
|
Draft *bool `json:"draft,omitempty"`
|
|
Prerelease *bool `json:"prerelease,omitempty"`
|
|
}
|
|
|
|
// Releases implements repository release workflows.
|
|
type Releases struct {
|
|
Client *transport.Client
|
|
Repository string
|
|
}
|
|
|
|
func (s Releases) endpoint() string { return "repos/" + s.Repository + "/releases" }
|
|
|
|
// ListPage lists a page of releases, with whether more follow and how many there are.
|
|
func (s Releases) ListPage(ctx context.Context, limit, page int) (Page[Release], error) {
|
|
window, err := listPage(limit, page, func(limit, page int) ([]*gitea.Release, http.Header, error) {
|
|
var headers http.Header
|
|
releases, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) ([]*gitea.Release, *gitea.Response, error) {
|
|
releases, resp, err := c.Releases.ListReleases(ctx, owner, repo, gitea.ListReleasesOptions{ListOptions: listOptions(limit, page)})
|
|
headers = headerOf(resp)
|
|
return releases, resp, err
|
|
})
|
|
return releases, headers, err
|
|
})
|
|
if err != nil {
|
|
return Page[Release]{}, err
|
|
}
|
|
return convertPage(window, releaseFrom), nil
|
|
}
|
|
|
|
// View retrieves a release by its tag, including numeric tags.
|
|
func (s Releases) View(ctx context.Context, tag string) (Release, error) {
|
|
item, err := repoConvert(ctx, s.Client, s.Repository, releaseFrom, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Release, *gitea.Response, error) {
|
|
return c.Releases.GetReleaseByTag(ctx, owner, repo, tag)
|
|
})
|
|
return item, Classify(err, "release", tag, s.Repository)
|
|
}
|
|
|
|
// Latest retrieves the most recent release that is neither a draft nor a prerelease.
|
|
func (s Releases) Latest(ctx context.Context) (Release, error) {
|
|
release, err := repoConvert(ctx, s.Client, s.Repository, releaseFrom, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Release, *gitea.Response, error) {
|
|
return c.Releases.GetLatestRelease(ctx, owner, repo)
|
|
})
|
|
return release, Classify(err, "latest release", "", s.Repository)
|
|
}
|
|
|
|
// Archive opens the source archive of the repository at ref; format is zip or tar.gz.
|
|
func (s Releases) Archive(ctx context.Context, ref, format string) (io.ReadCloser, error) {
|
|
return repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (io.ReadCloser, *gitea.Response, error) {
|
|
return c.Repositories.GetArchiveReader(ctx, owner, repo, ref, gitea.ArchiveType("."+format))
|
|
})
|
|
}
|
|
|
|
// Create creates a release and lets Gitea create a missing tag at the supplied target.
|
|
func (s Releases) Create(ctx context.Context, opts ReleaseOptions) (Release, error) {
|
|
item, err := repoConvert(ctx, s.Client, s.Repository, releaseFrom, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Release, *gitea.Response, error) {
|
|
return c.Releases.CreateRelease(ctx, owner, repo, gitea.CreateReleaseOption{
|
|
TagName: deref(opts.Tag), Target: deref(opts.Target), Title: deref(opts.Title), Note: deref(opts.Body),
|
|
IsDraft: deref(opts.Draft), IsPrerelease: deref(opts.Prerelease),
|
|
})
|
|
})
|
|
if apiErr, ok := errors.AsType[*transport.APIError](err); ok && apiErr.Status == http.StatusNotFound && strings.Contains(apiErr.Message, "target") {
|
|
// The release does not exist yet, so a 404 here names the unresolvable target, not the release.
|
|
return item, &NotFoundError{Kind: "target", Key: strconv.Quote(deref(opts.Target)), Scope: s.Repository, Err: err}
|
|
}
|
|
return item, Classify(err, "release", deref(opts.Tag), s.Repository)
|
|
}
|
|
|
|
// TagExists reports whether the repository on the server has the tag.
|
|
func (s Releases) TagExists(ctx context.Context, tag string) (bool, error) {
|
|
_, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Tag, *gitea.Response, error) {
|
|
return c.Repositories.GetTag(ctx, owner, repo, tag)
|
|
})
|
|
if IsNotFound(err) {
|
|
return false, nil
|
|
}
|
|
return err == nil, err
|
|
}
|
|
|
|
// Edit changes only supplied release fields; Gitea ignores empty text fields.
|
|
func (s Releases) Edit(ctx context.Context, id int64, opts ReleaseOptions) (Release, error) {
|
|
return repoConvert(ctx, s.Client, s.Repository, releaseFrom, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Release, *gitea.Response, error) {
|
|
return c.Releases.EditRelease(ctx, owner, repo, id, gitea.EditReleaseOption{
|
|
TagName: deref(opts.Tag), Target: deref(opts.Target), Title: deref(opts.Title), Note: deref(opts.Body),
|
|
IsDraft: opts.Draft, IsPrerelease: opts.Prerelease,
|
|
})
|
|
})
|
|
}
|
|
|
|
// Delete removes a release and optionally its tag; partial failures identify the completed step.
|
|
func (s Releases) Delete(ctx context.Context, item Release, deleteTag bool) error {
|
|
if err := repoDo(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Response, error) {
|
|
return c.Releases.DeleteRelease(ctx, owner, repo, item.ID)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if deleteTag {
|
|
if err := repoDo(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Response, error) {
|
|
return c.Repositories.DeleteTag(ctx, owner, repo, item.Tag)
|
|
}); err != nil {
|
|
return fmt.Errorf("release deleted, but tag deletion failed: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Assets applies CLI pagination to Gitea's unpaginated attachment collection.
|
|
func (s Releases) Assets(ctx context.Context, id int64, limit, page int) ([]ReleaseAsset, error) {
|
|
items, err := s.AllAssets(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pageOf(items, limit, page)
|
|
}
|
|
|
|
// AllAssets retrieves the full collection; this endpoint has no server pagination.
|
|
func (s Releases) AllAssets(ctx context.Context, id int64) ([]ReleaseAsset, error) {
|
|
attachments, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) ([]*gitea.Attachment, *gitea.Response, error) {
|
|
return c.Releases.ListReleaseAttachments(ctx, owner, repo, id, gitea.ListReleaseAttachmentsOptions{Page: -1})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return convertAll(attachments, assetFrom), nil
|
|
}
|
|
|
|
// DeleteAsset removes one asset from the selected release.
|
|
func (s Releases) DeleteAsset(ctx context.Context, id, asset int64) error {
|
|
return repoDo(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Response, error) {
|
|
return c.Releases.DeleteReleaseAttachment(ctx, owner, repo, id, asset)
|
|
})
|
|
}
|
|
|
|
// RenameAsset changes only an attachment's display name.
|
|
func (s Releases) RenameAsset(ctx context.Context, id, asset int64, name string) (ReleaseAsset, error) {
|
|
return repoConvert(ctx, s.Client, s.Repository, assetFrom, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Attachment, *gitea.Response, error) {
|
|
return c.Releases.EditReleaseAttachment(ctx, owner, repo, id, asset, gitea.EditAttachmentOptions{Name: name})
|
|
})
|
|
}
|
|
|
|
// UploadAsset streams a multipart upload without buffering the asset in memory.
|
|
func (s Releases) UploadAsset(ctx context.Context, id int64, name string, input io.Reader) (ReleaseAsset, error) {
|
|
return uploadAttachment(ctx, s.Client, fmt.Sprintf("%s/%d/assets", s.endpoint(), id), name, input)
|
|
}
|
|
|
|
// DownloadAsset opens a repository-validated attachment stream at its direct instance URL.
|
|
func (s Releases) DownloadAsset(ctx context.Context, id, asset int64) (io.ReadCloser, error) {
|
|
attachment, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Attachment, *gitea.Response, error) {
|
|
return c.Releases.GetReleaseAttachment(ctx, owner, repo, id, asset)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return downloadAttachment(ctx, s.Client, attachment)
|
|
}
|